Yanz Mini Shell
[_]
[-]
[X]
[
HomeShell 1
] [
HomeShell 2
] [
Upload
] [
Command Shell
] [
Scripting
] [
About
]
[ Directory ] =>
/
home
admin
web
uyoxpress.com
public_html
Action
[*]
New File
[*]
New Folder
Sensitive File
[*]
/etc/passwd
[*]
/etc/shadow
[*]
/etc/resolv.conf
[
Delete
] [
Edit
] [
Rename
] [
Back
]
mime-type-detection/composer.json 0000644 00000001441 15007532371 0013153 0 ustar 00 { "name": "league/mime-type-detection", "description": "Mime-type detection for Flysystem", "license": "MIT", "authors": [ { "name": "Frank de Jonge", "email": "info@frankdejonge.nl" } ], "scripts": { "test": "vendor/bin/phpunit", "phpstan": "vendor/bin/phpstan analyse -l 6 src" }, "require": { "php": "^7.4 || ^8.0", "ext-fileinfo": "*" }, "require-dev": { "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0", "phpstan/phpstan": "^0.12.68", "friendsofphp/php-cs-fixer": "^3.2" }, "autoload": { "psr-4": { "League\\MimeTypeDetection\\": "src" } }, "config": { "platform": { "php": "7.4.0" } } } mime-type-detection/LICENSE 0000644 00000002047 15007532371 0011441 0 ustar 00 Copyright (c) 2013-2023 Frank de Jonge 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. mime-type-detection/CHANGELOG.md 0000644 00000001615 15007532371 0012245 0 ustar 00 # Changelog ## 1.15.0 - 2024-01-28 - Updated lookup ## 1.14.0 - 2022-10-17 ### Updated - Updated lookup ## 1.13.0 - 2023-08-05 ### Added - A reverse lookup mechanism to fetch one or all extensions for a given mimetype ## 1.12.0 - 2023-08-03 ### Updated - Updated lookup ## 1.11.0 - 2023-04-17 ### Updated - Updated lookup ## 1.10.0 - 2022-04-11 ### Fixed - Added Flysystem v1 inconclusive mime-types and made it configurable as a constructor parameter. ## 1.9.0 - 2021-11-21 ### Updated - Updated lookup ## 1.8.0 - 2021-09-25 ### Added - Added the decorator `OverridingExtensionToMimeTypeMap` which allows you to override values. ## 1.7.0 - 2021-01-18 ### Added - Added a `bufferSampleSize` parameter to the `FinfoMimeTypeDetector` class that allows you to send a reduced content sample which costs less memory. ## 1.6.0 - 2021-01-18 ### Changes - Updated generated mime-type map mime-type-detection/src/OverridingExtensionToMimeTypeMap.php 0000644 00000001226 15007532371 0020352 0 ustar 00 <?php namespace League\MimeTypeDetection; class OverridingExtensionToMimeTypeMap implements ExtensionToMimeTypeMap { /** * @var ExtensionToMimeTypeMap */ private $innerMap; /** * @var string[] */ private $overrides; /** * @param array<string, string> $overrides */ public function __construct(ExtensionToMimeTypeMap $innerMap, array $overrides) { $this->innerMap = $innerMap; $this->overrides = $overrides; } public function lookupMimeType(string $extension): ?string { return $this->overrides[$extension] ?? $this->innerMap->lookupMimeType($extension); } } mime-type-detection/src/FinfoMimeTypeDetector.php 0000644 00000005337 15007532371 0016146 0 ustar 00 <?php declare(strict_types=1); namespace League\MimeTypeDetection; use const FILEINFO_MIME_TYPE; use const PATHINFO_EXTENSION; use finfo; class FinfoMimeTypeDetector implements MimeTypeDetector, ExtensionLookup { private const INCONCLUSIVE_MIME_TYPES = [ 'application/x-empty', 'text/plain', 'text/x-asm', 'application/octet-stream', 'inode/x-empty', ]; /** * @var finfo */ private $finfo; /** * @var ExtensionToMimeTypeMap */ private $extensionMap; /** * @var int|null */ private $bufferSampleSize; /** * @var array<string> */ private $inconclusiveMimetypes; public function __construct( string $magicFile = '', ExtensionToMimeTypeMap $extensionMap = null, ?int $bufferSampleSize = null, array $inconclusiveMimetypes = self::INCONCLUSIVE_MIME_TYPES ) { $this->finfo = new finfo(FILEINFO_MIME_TYPE, $magicFile); $this->extensionMap = $extensionMap ?: new GeneratedExtensionToMimeTypeMap(); $this->bufferSampleSize = $bufferSampleSize; $this->inconclusiveMimetypes = $inconclusiveMimetypes; } public function detectMimeType(string $path, $contents): ?string { $mimeType = is_string($contents) ? (@$this->finfo->buffer($this->takeSample($contents)) ?: null) : null; if ($mimeType !== null && ! in_array($mimeType, $this->inconclusiveMimetypes)) { return $mimeType; } return $this->detectMimeTypeFromPath($path); } public function detectMimeTypeFromPath(string $path): ?string { $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); return $this->extensionMap->lookupMimeType($extension); } public function detectMimeTypeFromFile(string $path): ?string { return @$this->finfo->file($path) ?: null; } public function detectMimeTypeFromBuffer(string $contents): ?string { return @$this->finfo->buffer($this->takeSample($contents)) ?: null; } private function takeSample(string $contents): string { if ($this->bufferSampleSize === null) { return $contents; } return (string) substr($contents, 0, $this->bufferSampleSize); } public function lookupExtension(string $mimetype): ?string { return $this->extensionMap instanceof ExtensionLookup ? $this->extensionMap->lookupExtension($mimetype) : null; } public function lookupAllExtensions(string $mimetype): array { return $this->extensionMap instanceof ExtensionLookup ? $this->extensionMap->lookupAllExtensions($mimetype) : []; } } mime-type-detection/src/EmptyExtensionToMimeTypeMap.php 0000644 00000000356 15007532371 0017343 0 ustar 00 <?php declare(strict_types=1); namespace League\MimeTypeDetection; class EmptyExtensionToMimeTypeMap implements ExtensionToMimeTypeMap { public function lookupMimeType(string $extension): ?string { return null; } } mime-type-detection/src/ExtensionMimeTypeDetector.php 0000644 00000002651 15007532371 0017055 0 ustar 00 <?php declare(strict_types=1); namespace League\MimeTypeDetection; use const PATHINFO_EXTENSION; class ExtensionMimeTypeDetector implements MimeTypeDetector, ExtensionLookup { /** * @var ExtensionToMimeTypeMap */ private $extensions; public function __construct(ExtensionToMimeTypeMap $extensions = null) { $this->extensions = $extensions ?: new GeneratedExtensionToMimeTypeMap(); } public function detectMimeType(string $path, $contents): ?string { return $this->detectMimeTypeFromPath($path); } public function detectMimeTypeFromPath(string $path): ?string { $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION)); return $this->extensions->lookupMimeType($extension); } public function detectMimeTypeFromFile(string $path): ?string { return $this->detectMimeTypeFromPath($path); } public function detectMimeTypeFromBuffer(string $contents): ?string { return null; } public function lookupExtension(string $mimetype): ?string { return $this->extensions instanceof ExtensionLookup ? $this->extensions->lookupExtension($mimetype) : null; } public function lookupAllExtensions(string $mimetype): array { return $this->extensions instanceof ExtensionLookup ? $this->extensions->lookupAllExtensions($mimetype) : []; } } mime-type-detection/src/ExtensionLookup.php 0000644 00000000416 15007532371 0015100 0 ustar 00 <?php declare(strict_types=1); namespace League\MimeTypeDetection; interface ExtensionLookup { public function lookupExtension(string $mimetype): ?string; /** * @return string[] */ public function lookupAllExtensions(string $mimetype): array; } mime-type-detection/src/GeneratedExtensionToMimeTypeMap.php 0000644 00000316553 15007532371 0020154 0 ustar 00 <?php declare(strict_types=1); namespace League\MimeTypeDetection; class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap, ExtensionLookup { /** * @var array<string, string> * * @internal */ public const MIME_TYPES_FOR_EXTENSIONS = [ '1km' => 'application/vnd.1000minds.decision-model+xml', '3dml' => 'text/vnd.in3d.3dml', '3ds' => 'image/x-3ds', '3g2' => 'video/3gpp2', '3gp' => 'video/3gp', '3gpp' => 'video/3gpp', '3mf' => 'model/3mf', '7z' => 'application/x-7z-compressed', '7zip' => 'application/x-7z-compressed', '123' => 'application/vnd.lotus-1-2-3', 'aab' => 'application/x-authorware-bin', 'aac' => 'audio/acc', 'aam' => 'application/x-authorware-map', 'aas' => 'application/x-authorware-seg', 'abw' => 'application/x-abiword', 'ac' => 'application/vnd.nokia.n-gage.ac+xml', 'ac3' => 'audio/ac3', 'acc' => 'application/vnd.americandynamics.acc', 'ace' => 'application/x-ace-compressed', 'acu' => 'application/vnd.acucobol', 'acutc' => 'application/vnd.acucorp', 'adp' => 'audio/adpcm', 'adts' => 'audio/aac', 'aep' => 'application/vnd.audiograph', 'afm' => 'application/x-font-type1', 'afp' => 'application/vnd.ibm.modcap', 'age' => 'application/vnd.age', 'ahead' => 'application/vnd.ahead.space', 'ai' => 'application/pdf', 'aif' => 'audio/x-aiff', 'aifc' => 'audio/x-aiff', 'aiff' => 'audio/x-aiff', 'air' => 'application/vnd.adobe.air-application-installer-package+zip', 'ait' => 'application/vnd.dvb.ait', 'ami' => 'application/vnd.amiga.ami', 'aml' => 'application/automationml-aml+xml', 'amlx' => 'application/automationml-amlx+zip', 'amr' => 'audio/amr', 'apk' => 'application/vnd.android.package-archive', 'apng' => 'image/apng', 'appcache' => 'text/cache-manifest', 'appinstaller' => 'application/appinstaller', 'application' => 'application/x-ms-application', 'appx' => 'application/appx', 'appxbundle' => 'application/appxbundle', 'apr' => 'application/vnd.lotus-approach', 'arc' => 'application/x-freearc', 'arj' => 'application/x-arj', 'asc' => 'application/pgp-signature', 'asf' => 'video/x-ms-asf', 'asm' => 'text/x-asm', 'aso' => 'application/vnd.accpac.simply.aso', 'asx' => 'video/x-ms-asf', 'atc' => 'application/vnd.acucorp', 'atom' => 'application/atom+xml', 'atomcat' => 'application/atomcat+xml', 'atomdeleted' => 'application/atomdeleted+xml', 'atomsvc' => 'application/atomsvc+xml', 'atx' => 'application/vnd.antix.game-component', 'au' => 'audio/x-au', 'avci' => 'image/avci', 'avcs' => 'image/avcs', 'avi' => 'video/x-msvideo', 'avif' => 'image/avif', 'aw' => 'application/applixware', 'azf' => 'application/vnd.airzip.filesecure.azf', 'azs' => 'application/vnd.airzip.filesecure.azs', 'azv' => 'image/vnd.airzip.accelerator.azv', 'azw' => 'application/vnd.amazon.ebook', 'b16' => 'image/vnd.pco.b16', 'bat' => 'application/x-msdownload', 'bcpio' => 'application/x-bcpio', 'bdf' => 'application/x-font-bdf', 'bdm' => 'application/vnd.syncml.dm+wbxml', 'bdoc' => 'application/x-bdoc', 'bed' => 'application/vnd.realvnc.bed', 'bh2' => 'application/vnd.fujitsu.oasysprs', 'bin' => 'application/octet-stream', 'blb' => 'application/x-blorb', 'blorb' => 'application/x-blorb', 'bmi' => 'application/vnd.bmi', 'bmml' => 'application/vnd.balsamiq.bmml+xml', 'bmp' => 'image/bmp', 'book' => 'application/vnd.framemaker', 'box' => 'application/vnd.previewsystems.box', 'boz' => 'application/x-bzip2', 'bpk' => 'application/octet-stream', 'bpmn' => 'application/octet-stream', 'brf' => 'application/braille', 'bsp' => 'model/vnd.valve.source.compiled-map', 'btf' => 'image/prs.btif', 'btif' => 'image/prs.btif', 'buffer' => 'application/octet-stream', 'bz' => 'application/x-bzip', 'bz2' => 'application/x-bzip2', 'c' => 'text/x-c', 'c4d' => 'application/vnd.clonk.c4group', 'c4f' => 'application/vnd.clonk.c4group', 'c4g' => 'application/vnd.clonk.c4group', 'c4p' => 'application/vnd.clonk.c4group', 'c4u' => 'application/vnd.clonk.c4group', 'c11amc' => 'application/vnd.cluetrust.cartomobile-config', 'c11amz' => 'application/vnd.cluetrust.cartomobile-config-pkg', 'cab' => 'application/vnd.ms-cab-compressed', 'caf' => 'audio/x-caf', 'cap' => 'application/vnd.tcpdump.pcap', 'car' => 'application/vnd.curl.car', 'cat' => 'application/vnd.ms-pki.seccat', 'cb7' => 'application/x-cbr', 'cba' => 'application/x-cbr', 'cbr' => 'application/x-cbr', 'cbt' => 'application/x-cbr', 'cbz' => 'application/x-cbr', 'cc' => 'text/x-c', 'cco' => 'application/x-cocoa', 'cct' => 'application/x-director', 'ccxml' => 'application/ccxml+xml', 'cdbcmsg' => 'application/vnd.contact.cmsg', 'cdf' => 'application/x-netcdf', 'cdfx' => 'application/cdfx+xml', 'cdkey' => 'application/vnd.mediastation.cdkey', 'cdmia' => 'application/cdmi-capability', 'cdmic' => 'application/cdmi-container', 'cdmid' => 'application/cdmi-domain', 'cdmio' => 'application/cdmi-object', 'cdmiq' => 'application/cdmi-queue', 'cdr' => 'application/cdr', 'cdx' => 'chemical/x-cdx', 'cdxml' => 'application/vnd.chemdraw+xml', 'cdy' => 'application/vnd.cinderella', 'cer' => 'application/pkix-cert', 'cfs' => 'application/x-cfs-compressed', 'cgm' => 'image/cgm', 'chat' => 'application/x-chat', 'chm' => 'application/vnd.ms-htmlhelp', 'chrt' => 'application/vnd.kde.kchart', 'cif' => 'chemical/x-cif', 'cii' => 'application/vnd.anser-web-certificate-issue-initiation', 'cil' => 'application/vnd.ms-artgalry', 'cjs' => 'application/node', 'cla' => 'application/vnd.claymore', 'class' => 'application/octet-stream', 'cld' => 'model/vnd.cld', 'clkk' => 'application/vnd.crick.clicker.keyboard', 'clkp' => 'application/vnd.crick.clicker.palette', 'clkt' => 'application/vnd.crick.clicker.template', 'clkw' => 'application/vnd.crick.clicker.wordbank', 'clkx' => 'application/vnd.crick.clicker', 'clp' => 'application/x-msclip', 'cmc' => 'application/vnd.cosmocaller', 'cmdf' => 'chemical/x-cmdf', 'cml' => 'chemical/x-cml', 'cmp' => 'application/vnd.yellowriver-custom-menu', 'cmx' => 'image/x-cmx', 'cod' => 'application/vnd.rim.cod', 'coffee' => 'text/coffeescript', 'com' => 'application/x-msdownload', 'conf' => 'text/plain', 'cpio' => 'application/x-cpio', 'cpl' => 'application/cpl+xml', 'cpp' => 'text/x-c', 'cpt' => 'application/mac-compactpro', 'crd' => 'application/x-mscardfile', 'crl' => 'application/pkix-crl', 'crt' => 'application/x-x509-ca-cert', 'crx' => 'application/x-chrome-extension', 'cryptonote' => 'application/vnd.rig.cryptonote', 'csh' => 'application/x-csh', 'csl' => 'application/vnd.citationstyles.style+xml', 'csml' => 'chemical/x-csml', 'csp' => 'application/vnd.commonspace', 'csr' => 'application/octet-stream', 'css' => 'text/css', 'cst' => 'application/x-director', 'csv' => 'text/csv', 'cu' => 'application/cu-seeme', 'curl' => 'text/vnd.curl', 'cwl' => 'application/cwl', 'cww' => 'application/prs.cww', 'cxt' => 'application/x-director', 'cxx' => 'text/x-c', 'dae' => 'model/vnd.collada+xml', 'daf' => 'application/vnd.mobius.daf', 'dart' => 'application/vnd.dart', 'dataless' => 'application/vnd.fdsn.seed', 'davmount' => 'application/davmount+xml', 'dbf' => 'application/vnd.dbf', 'dbk' => 'application/docbook+xml', 'dcr' => 'application/x-director', 'dcurl' => 'text/vnd.curl.dcurl', 'dd2' => 'application/vnd.oma.dd2+xml', 'ddd' => 'application/vnd.fujixerox.ddd', 'ddf' => 'application/vnd.syncml.dmddf+xml', 'dds' => 'image/vnd.ms-dds', 'deb' => 'application/x-debian-package', 'def' => 'text/plain', 'deploy' => 'application/octet-stream', 'der' => 'application/x-x509-ca-cert', 'dfac' => 'application/vnd.dreamfactory', 'dgc' => 'application/x-dgc-compressed', 'dib' => 'image/bmp', 'dic' => 'text/x-c', 'dir' => 'application/x-director', 'dis' => 'application/vnd.mobius.dis', 'disposition-notification' => 'message/disposition-notification', 'dist' => 'application/octet-stream', 'distz' => 'application/octet-stream', 'djv' => 'image/vnd.djvu', 'djvu' => 'image/vnd.djvu', 'dll' => 'application/octet-stream', 'dmg' => 'application/x-apple-diskimage', 'dmn' => 'application/octet-stream', 'dmp' => 'application/vnd.tcpdump.pcap', 'dms' => 'application/octet-stream', 'dna' => 'application/vnd.dna', 'doc' => 'application/msword', 'docm' => 'application/vnd.ms-word.template.macroEnabled.12', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'dot' => 'application/msword', 'dotm' => 'application/vnd.ms-word.template.macroEnabled.12', 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'dp' => 'application/vnd.osgi.dp', 'dpg' => 'application/vnd.dpgraph', 'dpx' => 'image/dpx', 'dra' => 'audio/vnd.dra', 'drle' => 'image/dicom-rle', 'dsc' => 'text/prs.lines.tag', 'dssc' => 'application/dssc+der', 'dst' => 'application/octet-stream', 'dtb' => 'application/x-dtbook+xml', 'dtd' => 'application/xml-dtd', 'dts' => 'audio/vnd.dts', 'dtshd' => 'audio/vnd.dts.hd', 'dump' => 'application/octet-stream', 'dvb' => 'video/vnd.dvb.file', 'dvi' => 'application/x-dvi', 'dwd' => 'application/atsc-dwd+xml', 'dwf' => 'model/vnd.dwf', 'dwg' => 'image/vnd.dwg', 'dxf' => 'image/vnd.dxf', 'dxp' => 'application/vnd.spotfire.dxp', 'dxr' => 'application/x-director', 'ear' => 'application/java-archive', 'ecelp4800' => 'audio/vnd.nuera.ecelp4800', 'ecelp7470' => 'audio/vnd.nuera.ecelp7470', 'ecelp9600' => 'audio/vnd.nuera.ecelp9600', 'ecma' => 'application/ecmascript', 'edm' => 'application/vnd.novadigm.edm', 'edx' => 'application/vnd.novadigm.edx', 'efif' => 'application/vnd.picsel', 'ei6' => 'application/vnd.pg.osasli', 'elc' => 'application/octet-stream', 'emf' => 'image/emf', 'eml' => 'message/rfc822', 'emma' => 'application/emma+xml', 'emotionml' => 'application/emotionml+xml', 'emz' => 'application/x-msmetafile', 'eol' => 'audio/vnd.digital-winds', 'eot' => 'application/vnd.ms-fontobject', 'eps' => 'application/postscript', 'epub' => 'application/epub+zip', 'es3' => 'application/vnd.eszigno3+xml', 'esa' => 'application/vnd.osgi.subsystem', 'esf' => 'application/vnd.epson.esf', 'et3' => 'application/vnd.eszigno3+xml', 'etx' => 'text/x-setext', 'eva' => 'application/x-eva', 'evy' => 'application/x-envoy', 'exe' => 'application/octet-stream', 'exi' => 'application/exi', 'exp' => 'application/express', 'exr' => 'image/aces', 'ext' => 'application/vnd.novadigm.ext', 'ez' => 'application/andrew-inset', 'ez2' => 'application/vnd.ezpix-album', 'ez3' => 'application/vnd.ezpix-package', 'f' => 'text/x-fortran', 'f4v' => 'video/mp4', 'f77' => 'text/x-fortran', 'f90' => 'text/x-fortran', 'fbs' => 'image/vnd.fastbidsheet', 'fcdt' => 'application/vnd.adobe.formscentral.fcdt', 'fcs' => 'application/vnd.isac.fcs', 'fdf' => 'application/vnd.fdf', 'fdt' => 'application/fdt+xml', 'fe_launch' => 'application/vnd.denovo.fcselayout-link', 'fg5' => 'application/vnd.fujitsu.oasysgp', 'fgd' => 'application/x-director', 'fh' => 'image/x-freehand', 'fh4' => 'image/x-freehand', 'fh5' => 'image/x-freehand', 'fh7' => 'image/x-freehand', 'fhc' => 'image/x-freehand', 'fig' => 'application/x-xfig', 'fits' => 'image/fits', 'flac' => 'audio/x-flac', 'fli' => 'video/x-fli', 'flo' => 'application/vnd.micrografx.flo', 'flv' => 'video/x-flv', 'flw' => 'application/vnd.kde.kivio', 'flx' => 'text/vnd.fmi.flexstor', 'fly' => 'text/vnd.fly', 'fm' => 'application/vnd.framemaker', 'fnc' => 'application/vnd.frogans.fnc', 'fo' => 'application/vnd.software602.filler.form+xml', 'for' => 'text/x-fortran', 'fpx' => 'image/vnd.fpx', 'frame' => 'application/vnd.framemaker', 'fsc' => 'application/vnd.fsc.weblaunch', 'fst' => 'image/vnd.fst', 'ftc' => 'application/vnd.fluxtime.clip', 'fti' => 'application/vnd.anser-web-funds-transfer-initiation', 'fvt' => 'video/vnd.fvt', 'fxp' => 'application/vnd.adobe.fxp', 'fxpl' => 'application/vnd.adobe.fxp', 'fzs' => 'application/vnd.fuzzysheet', 'g2w' => 'application/vnd.geoplan', 'g3' => 'image/g3fax', 'g3w' => 'application/vnd.geospace', 'gac' => 'application/vnd.groove-account', 'gam' => 'application/x-tads', 'gbr' => 'application/rpki-ghostbusters', 'gca' => 'application/x-gca-compressed', 'gdl' => 'model/vnd.gdl', 'gdoc' => 'application/vnd.google-apps.document', 'ged' => 'text/vnd.familysearch.gedcom', 'geo' => 'application/vnd.dynageo', 'geojson' => 'application/geo+json', 'gex' => 'application/vnd.geometry-explorer', 'ggb' => 'application/vnd.geogebra.file', 'ggt' => 'application/vnd.geogebra.tool', 'ghf' => 'application/vnd.groove-help', 'gif' => 'image/gif', 'gim' => 'application/vnd.groove-identity-message', 'glb' => 'model/gltf-binary', 'gltf' => 'model/gltf+json', 'gml' => 'application/gml+xml', 'gmx' => 'application/vnd.gmx', 'gnumeric' => 'application/x-gnumeric', 'gpg' => 'application/gpg-keys', 'gph' => 'application/vnd.flographit', 'gpx' => 'application/gpx+xml', 'gqf' => 'application/vnd.grafeq', 'gqs' => 'application/vnd.grafeq', 'gram' => 'application/srgs', 'gramps' => 'application/x-gramps-xml', 'gre' => 'application/vnd.geometry-explorer', 'grv' => 'application/vnd.groove-injector', 'grxml' => 'application/srgs+xml', 'gsf' => 'application/x-font-ghostscript', 'gsheet' => 'application/vnd.google-apps.spreadsheet', 'gslides' => 'application/vnd.google-apps.presentation', 'gtar' => 'application/x-gtar', 'gtm' => 'application/vnd.groove-tool-message', 'gtw' => 'model/vnd.gtw', 'gv' => 'text/vnd.graphviz', 'gxf' => 'application/gxf', 'gxt' => 'application/vnd.geonext', 'gz' => 'application/gzip', 'gzip' => 'application/gzip', 'h' => 'text/x-c', 'h261' => 'video/h261', 'h263' => 'video/h263', 'h264' => 'video/h264', 'hal' => 'application/vnd.hal+xml', 'hbci' => 'application/vnd.hbci', 'hbs' => 'text/x-handlebars-template', 'hdd' => 'application/x-virtualbox-hdd', 'hdf' => 'application/x-hdf', 'heic' => 'image/heic', 'heics' => 'image/heic-sequence', 'heif' => 'image/heif', 'heifs' => 'image/heif-sequence', 'hej2' => 'image/hej2k', 'held' => 'application/atsc-held+xml', 'hh' => 'text/x-c', 'hjson' => 'application/hjson', 'hlp' => 'application/winhlp', 'hpgl' => 'application/vnd.hp-hpgl', 'hpid' => 'application/vnd.hp-hpid', 'hps' => 'application/vnd.hp-hps', 'hqx' => 'application/mac-binhex40', 'hsj2' => 'image/hsj2', 'htc' => 'text/x-component', 'htke' => 'application/vnd.kenameaapp', 'htm' => 'text/html', 'html' => 'text/html', 'hvd' => 'application/vnd.yamaha.hv-dic', 'hvp' => 'application/vnd.yamaha.hv-voice', 'hvs' => 'application/vnd.yamaha.hv-script', 'i2g' => 'application/vnd.intergeo', 'icc' => 'application/vnd.iccprofile', 'ice' => 'x-conference/x-cooltalk', 'icm' => 'application/vnd.iccprofile', 'ico' => 'image/x-icon', 'ics' => 'text/calendar', 'ief' => 'image/ief', 'ifb' => 'text/calendar', 'ifm' => 'application/vnd.shana.informed.formdata', 'iges' => 'model/iges', 'igl' => 'application/vnd.igloader', 'igm' => 'application/vnd.insors.igm', 'igs' => 'model/iges', 'igx' => 'application/vnd.micrografx.igx', 'iif' => 'application/vnd.shana.informed.interchange', 'img' => 'application/octet-stream', 'imp' => 'application/vnd.accpac.simply.imp', 'ims' => 'application/vnd.ms-ims', 'in' => 'text/plain', 'ini' => 'text/plain', 'ink' => 'application/inkml+xml', 'inkml' => 'application/inkml+xml', 'install' => 'application/x-install-instructions', 'iota' => 'application/vnd.astraea-software.iota', 'ipfix' => 'application/ipfix', 'ipk' => 'application/vnd.shana.informed.package', 'irm' => 'application/vnd.ibm.rights-management', 'irp' => 'application/vnd.irepository.package+xml', 'iso' => 'application/x-iso9660-image', 'itp' => 'application/vnd.shana.informed.formtemplate', 'its' => 'application/its+xml', 'ivp' => 'application/vnd.immervision-ivp', 'ivu' => 'application/vnd.immervision-ivu', 'jad' => 'text/vnd.sun.j2me.app-descriptor', 'jade' => 'text/jade', 'jam' => 'application/vnd.jam', 'jar' => 'application/java-archive', 'jardiff' => 'application/x-java-archive-diff', 'java' => 'text/x-java-source', 'jhc' => 'image/jphc', 'jisp' => 'application/vnd.jisp', 'jls' => 'image/jls', 'jlt' => 'application/vnd.hp-jlyt', 'jng' => 'image/x-jng', 'jnlp' => 'application/x-java-jnlp-file', 'joda' => 'application/vnd.joost.joda-archive', 'jp2' => 'image/jp2', 'jpe' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'jpf' => 'image/jpx', 'jpg' => 'image/jpeg', 'jpg2' => 'image/jp2', 'jpgm' => 'video/jpm', 'jpgv' => 'video/jpeg', 'jph' => 'image/jph', 'jpm' => 'video/jpm', 'jpx' => 'image/jpx', 'js' => 'application/javascript', 'json' => 'application/json', 'json5' => 'application/json5', 'jsonld' => 'application/ld+json', 'jsonml' => 'application/jsonml+json', 'jsx' => 'text/jsx', 'jt' => 'model/jt', 'jxr' => 'image/jxr', 'jxra' => 'image/jxra', 'jxrs' => 'image/jxrs', 'jxs' => 'image/jxs', 'jxsc' => 'image/jxsc', 'jxsi' => 'image/jxsi', 'jxss' => 'image/jxss', 'kar' => 'audio/midi', 'karbon' => 'application/vnd.kde.karbon', 'kdb' => 'application/octet-stream', 'kdbx' => 'application/x-keepass2', 'key' => 'application/x-iwork-keynote-sffkey', 'kfo' => 'application/vnd.kde.kformula', 'kia' => 'application/vnd.kidspiration', 'kml' => 'application/vnd.google-earth.kml+xml', 'kmz' => 'application/vnd.google-earth.kmz', 'kne' => 'application/vnd.kinar', 'knp' => 'application/vnd.kinar', 'kon' => 'application/vnd.kde.kontour', 'kpr' => 'application/vnd.kde.kpresenter', 'kpt' => 'application/vnd.kde.kpresenter', 'kpxx' => 'application/vnd.ds-keypoint', 'ksp' => 'application/vnd.kde.kspread', 'ktr' => 'application/vnd.kahootz', 'ktx' => 'image/ktx', 'ktx2' => 'image/ktx2', 'ktz' => 'application/vnd.kahootz', 'kwd' => 'application/vnd.kde.kword', 'kwt' => 'application/vnd.kde.kword', 'lasxml' => 'application/vnd.las.las+xml', 'latex' => 'application/x-latex', 'lbd' => 'application/vnd.llamagraphics.life-balance.desktop', 'lbe' => 'application/vnd.llamagraphics.life-balance.exchange+xml', 'les' => 'application/vnd.hhe.lesson-player', 'less' => 'text/less', 'lgr' => 'application/lgr+xml', 'lha' => 'application/octet-stream', 'link66' => 'application/vnd.route66.link66+xml', 'list' => 'text/plain', 'list3820' => 'application/vnd.ibm.modcap', 'listafp' => 'application/vnd.ibm.modcap', 'litcoffee' => 'text/coffeescript', 'lnk' => 'application/x-ms-shortcut', 'log' => 'text/plain', 'lostxml' => 'application/lost+xml', 'lrf' => 'application/octet-stream', 'lrm' => 'application/vnd.ms-lrm', 'ltf' => 'application/vnd.frogans.ltf', 'lua' => 'text/x-lua', 'luac' => 'application/x-lua-bytecode', 'lvp' => 'audio/vnd.lucent.voice', 'lwp' => 'application/vnd.lotus-wordpro', 'lzh' => 'application/octet-stream', 'm1v' => 'video/mpeg', 'm2a' => 'audio/mpeg', 'm2v' => 'video/mpeg', 'm3a' => 'audio/mpeg', 'm3u' => 'text/plain', 'm3u8' => 'application/vnd.apple.mpegurl', 'm4a' => 'audio/x-m4a', 'm4p' => 'application/mp4', 'm4s' => 'video/iso.segment', 'm4u' => 'application/vnd.mpegurl', 'm4v' => 'video/x-m4v', 'm13' => 'application/x-msmediaview', 'm14' => 'application/x-msmediaview', 'm21' => 'application/mp21', 'ma' => 'application/mathematica', 'mads' => 'application/mads+xml', 'maei' => 'application/mmt-aei+xml', 'mag' => 'application/vnd.ecowin.chart', 'maker' => 'application/vnd.framemaker', 'man' => 'text/troff', 'manifest' => 'text/cache-manifest', 'map' => 'application/json', 'mar' => 'application/octet-stream', 'markdown' => 'text/markdown', 'mathml' => 'application/mathml+xml', 'mb' => 'application/mathematica', 'mbk' => 'application/vnd.mobius.mbk', 'mbox' => 'application/mbox', 'mc1' => 'application/vnd.medcalcdata', 'mcd' => 'application/vnd.mcd', 'mcurl' => 'text/vnd.curl.mcurl', 'md' => 'text/markdown', 'mdb' => 'application/x-msaccess', 'mdi' => 'image/vnd.ms-modi', 'mdx' => 'text/mdx', 'me' => 'text/troff', 'mesh' => 'model/mesh', 'meta4' => 'application/metalink4+xml', 'metalink' => 'application/metalink+xml', 'mets' => 'application/mets+xml', 'mfm' => 'application/vnd.mfmp', 'mft' => 'application/rpki-manifest', 'mgp' => 'application/vnd.osgeo.mapguide.package', 'mgz' => 'application/vnd.proteus.magazine', 'mid' => 'audio/midi', 'midi' => 'audio/midi', 'mie' => 'application/x-mie', 'mif' => 'application/vnd.mif', 'mime' => 'message/rfc822', 'mj2' => 'video/mj2', 'mjp2' => 'video/mj2', 'mjs' => 'text/javascript', 'mk3d' => 'video/x-matroska', 'mka' => 'audio/x-matroska', 'mkd' => 'text/x-markdown', 'mks' => 'video/x-matroska', 'mkv' => 'video/x-matroska', 'mlp' => 'application/vnd.dolby.mlp', 'mmd' => 'application/vnd.chipnuts.karaoke-mmd', 'mmf' => 'application/vnd.smaf', 'mml' => 'text/mathml', 'mmr' => 'image/vnd.fujixerox.edmics-mmr', 'mng' => 'video/x-mng', 'mny' => 'application/x-msmoney', 'mobi' => 'application/x-mobipocket-ebook', 'mods' => 'application/mods+xml', 'mov' => 'video/quicktime', 'movie' => 'video/x-sgi-movie', 'mp2' => 'audio/mpeg', 'mp2a' => 'audio/mpeg', 'mp3' => 'audio/mpeg', 'mp4' => 'video/mp4', 'mp4a' => 'audio/mp4', 'mp4s' => 'application/mp4', 'mp4v' => 'video/mp4', 'mp21' => 'application/mp21', 'mpc' => 'application/vnd.mophun.certificate', 'mpd' => 'application/dash+xml', 'mpe' => 'video/mpeg', 'mpeg' => 'video/mpeg', 'mpf' => 'application/media-policy-dataset+xml', 'mpg' => 'video/mpeg', 'mpg4' => 'video/mp4', 'mpga' => 'audio/mpeg', 'mpkg' => 'application/vnd.apple.installer+xml', 'mpm' => 'application/vnd.blueice.multipass', 'mpn' => 'application/vnd.mophun.application', 'mpp' => 'application/vnd.ms-project', 'mpt' => 'application/vnd.ms-project', 'mpy' => 'application/vnd.ibm.minipay', 'mqy' => 'application/vnd.mobius.mqy', 'mrc' => 'application/marc', 'mrcx' => 'application/marcxml+xml', 'ms' => 'text/troff', 'mscml' => 'application/mediaservercontrol+xml', 'mseed' => 'application/vnd.fdsn.mseed', 'mseq' => 'application/vnd.mseq', 'msf' => 'application/vnd.epson.msf', 'msg' => 'application/vnd.ms-outlook', 'msh' => 'model/mesh', 'msi' => 'application/x-msdownload', 'msix' => 'application/msix', 'msixbundle' => 'application/msixbundle', 'msl' => 'application/vnd.mobius.msl', 'msm' => 'application/octet-stream', 'msp' => 'application/octet-stream', 'msty' => 'application/vnd.muvee.style', 'mtl' => 'model/mtl', 'mts' => 'model/vnd.mts', 'mus' => 'application/vnd.musician', 'musd' => 'application/mmt-usd+xml', 'musicxml' => 'application/vnd.recordare.musicxml+xml', 'mvb' => 'application/x-msmediaview', 'mvt' => 'application/vnd.mapbox-vector-tile', 'mwf' => 'application/vnd.mfer', 'mxf' => 'application/mxf', 'mxl' => 'application/vnd.recordare.musicxml', 'mxmf' => 'audio/mobile-xmf', 'mxml' => 'application/xv+xml', 'mxs' => 'application/vnd.triscape.mxs', 'mxu' => 'video/vnd.mpegurl', 'n-gage' => 'application/vnd.nokia.n-gage.symbian.install', 'n3' => 'text/n3', 'nb' => 'application/mathematica', 'nbp' => 'application/vnd.wolfram.player', 'nc' => 'application/x-netcdf', 'ncx' => 'application/x-dtbncx+xml', 'ndjson' => 'application/x-ndjson', 'nfo' => 'text/x-nfo', 'ngdat' => 'application/vnd.nokia.n-gage.data', 'nitf' => 'application/vnd.nitf', 'nlu' => 'application/vnd.neurolanguage.nlu', 'nml' => 'application/vnd.enliven', 'nnd' => 'application/vnd.noblenet-directory', 'nns' => 'application/vnd.noblenet-sealer', 'nnw' => 'application/vnd.noblenet-web', 'npx' => 'image/vnd.net-fpx', 'nq' => 'application/n-quads', 'nsc' => 'application/x-conference', 'nsf' => 'application/vnd.lotus-notes', 'nt' => 'application/n-triples', 'ntf' => 'application/vnd.nitf', 'numbers' => 'application/x-iwork-numbers-sffnumbers', 'nzb' => 'application/x-nzb', 'oa2' => 'application/vnd.fujitsu.oasys2', 'oa3' => 'application/vnd.fujitsu.oasys3', 'oas' => 'application/vnd.fujitsu.oasys', 'obd' => 'application/x-msbinder', 'obgx' => 'application/vnd.openblox.game+xml', 'obj' => 'model/obj', 'oda' => 'application/oda', 'odb' => 'application/vnd.oasis.opendocument.database', 'odc' => 'application/vnd.oasis.opendocument.chart', 'odf' => 'application/vnd.oasis.opendocument.formula', 'odft' => 'application/vnd.oasis.opendocument.formula-template', 'odg' => 'application/vnd.oasis.opendocument.graphics', 'odi' => 'application/vnd.oasis.opendocument.image', 'odm' => 'application/vnd.oasis.opendocument.text-master', 'odp' => 'application/vnd.oasis.opendocument.presentation', 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', 'odt' => 'application/vnd.oasis.opendocument.text', 'oga' => 'audio/ogg', 'ogex' => 'model/vnd.opengex', 'ogg' => 'audio/ogg', 'ogv' => 'video/ogg', 'ogx' => 'application/ogg', 'omdoc' => 'application/omdoc+xml', 'onepkg' => 'application/onenote', 'onetmp' => 'application/onenote', 'onetoc' => 'application/onenote', 'onetoc2' => 'application/onenote', 'opf' => 'application/oebps-package+xml', 'opml' => 'text/x-opml', 'oprc' => 'application/vnd.palm', 'opus' => 'audio/ogg', 'org' => 'text/x-org', 'osf' => 'application/vnd.yamaha.openscoreformat', 'osfpvg' => 'application/vnd.yamaha.openscoreformat.osfpvg+xml', 'osm' => 'application/vnd.openstreetmap.data+xml', 'otc' => 'application/vnd.oasis.opendocument.chart-template', 'otf' => 'font/otf', 'otg' => 'application/vnd.oasis.opendocument.graphics-template', 'oth' => 'application/vnd.oasis.opendocument.text-web', 'oti' => 'application/vnd.oasis.opendocument.image-template', 'otp' => 'application/vnd.oasis.opendocument.presentation-template', 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template', 'ott' => 'application/vnd.oasis.opendocument.text-template', 'ova' => 'application/x-virtualbox-ova', 'ovf' => 'application/x-virtualbox-ovf', 'owl' => 'application/rdf+xml', 'oxps' => 'application/oxps', 'oxt' => 'application/vnd.openofficeorg.extension', 'p' => 'text/x-pascal', 'p7a' => 'application/x-pkcs7-signature', 'p7b' => 'application/x-pkcs7-certificates', 'p7c' => 'application/pkcs7-mime', 'p7m' => 'application/pkcs7-mime', 'p7r' => 'application/x-pkcs7-certreqresp', 'p7s' => 'application/pkcs7-signature', 'p8' => 'application/pkcs8', 'p10' => 'application/x-pkcs10', 'p12' => 'application/x-pkcs12', 'pac' => 'application/x-ns-proxy-autoconfig', 'pages' => 'application/x-iwork-pages-sffpages', 'pas' => 'text/x-pascal', 'paw' => 'application/vnd.pawaafile', 'pbd' => 'application/vnd.powerbuilder6', 'pbm' => 'image/x-portable-bitmap', 'pcap' => 'application/vnd.tcpdump.pcap', 'pcf' => 'application/x-font-pcf', 'pcl' => 'application/vnd.hp-pcl', 'pclxl' => 'application/vnd.hp-pclxl', 'pct' => 'image/x-pict', 'pcurl' => 'application/vnd.curl.pcurl', 'pcx' => 'image/x-pcx', 'pdb' => 'application/x-pilot', 'pde' => 'text/x-processing', 'pdf' => 'application/pdf', 'pem' => 'application/x-x509-user-cert', 'pfa' => 'application/x-font-type1', 'pfb' => 'application/x-font-type1', 'pfm' => 'application/x-font-type1', 'pfr' => 'application/font-tdpfr', 'pfx' => 'application/x-pkcs12', 'pgm' => 'image/x-portable-graymap', 'pgn' => 'application/x-chess-pgn', 'pgp' => 'application/pgp', 'phar' => 'application/octet-stream', 'php' => 'application/x-httpd-php', 'php3' => 'application/x-httpd-php', 'php4' => 'application/x-httpd-php', 'phps' => 'application/x-httpd-php-source', 'phtml' => 'application/x-httpd-php', 'pic' => 'image/x-pict', 'pkg' => 'application/octet-stream', 'pki' => 'application/pkixcmp', 'pkipath' => 'application/pkix-pkipath', 'pkpass' => 'application/vnd.apple.pkpass', 'pl' => 'application/x-perl', 'plb' => 'application/vnd.3gpp.pic-bw-large', 'plc' => 'application/vnd.mobius.plc', 'plf' => 'application/vnd.pocketlearn', 'pls' => 'application/pls+xml', 'pm' => 'application/x-perl', 'pml' => 'application/vnd.ctc-posml', 'png' => 'image/png', 'pnm' => 'image/x-portable-anymap', 'portpkg' => 'application/vnd.macports.portpkg', 'pot' => 'application/vnd.ms-powerpoint', 'potm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', 'ppa' => 'application/vnd.ms-powerpoint', 'ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12', 'ppd' => 'application/vnd.cups-ppd', 'ppm' => 'image/x-portable-pixmap', 'pps' => 'application/vnd.ms-powerpoint', 'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', 'ppt' => 'application/powerpoint', 'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'pqa' => 'application/vnd.palm', 'prc' => 'model/prc', 'pre' => 'application/vnd.lotus-freelance', 'prf' => 'application/pics-rules', 'provx' => 'application/provenance+xml', 'ps' => 'application/postscript', 'psb' => 'application/vnd.3gpp.pic-bw-small', 'psd' => 'application/x-photoshop', 'psf' => 'application/x-font-linux-psf', 'pskcxml' => 'application/pskc+xml', 'pti' => 'image/prs.pti', 'ptid' => 'application/vnd.pvi.ptid1', 'pub' => 'application/x-mspublisher', 'pv' => 'application/octet-stream', 'pvb' => 'application/vnd.3gpp.pic-bw-var', 'pwn' => 'application/vnd.3m.post-it-notes', 'pxf' => 'application/octet-stream', 'pya' => 'audio/vnd.ms-playready.media.pya', 'pyo' => 'model/vnd.pytha.pyox', 'pyox' => 'model/vnd.pytha.pyox', 'pyv' => 'video/vnd.ms-playready.media.pyv', 'qam' => 'application/vnd.epson.quickanime', 'qbo' => 'application/vnd.intu.qbo', 'qfx' => 'application/vnd.intu.qfx', 'qps' => 'application/vnd.publishare-delta-tree', 'qt' => 'video/quicktime', 'qwd' => 'application/vnd.quark.quarkxpress', 'qwt' => 'application/vnd.quark.quarkxpress', 'qxb' => 'application/vnd.quark.quarkxpress', 'qxd' => 'application/vnd.quark.quarkxpress', 'qxl' => 'application/vnd.quark.quarkxpress', 'qxt' => 'application/vnd.quark.quarkxpress', 'ra' => 'audio/x-realaudio', 'ram' => 'audio/x-pn-realaudio', 'raml' => 'application/raml+yaml', 'rapd' => 'application/route-apd+xml', 'rar' => 'application/x-rar', 'ras' => 'image/x-cmu-raster', 'rcprofile' => 'application/vnd.ipunplugged.rcprofile', 'rdf' => 'application/rdf+xml', 'rdz' => 'application/vnd.data-vision.rdz', 'relo' => 'application/p2p-overlay+xml', 'rep' => 'application/vnd.businessobjects', 'res' => 'application/x-dtbresource+xml', 'rgb' => 'image/x-rgb', 'rif' => 'application/reginfo+xml', 'rip' => 'audio/vnd.rip', 'ris' => 'application/x-research-info-systems', 'rl' => 'application/resource-lists+xml', 'rlc' => 'image/vnd.fujixerox.edmics-rlc', 'rld' => 'application/resource-lists-diff+xml', 'rm' => 'audio/x-pn-realaudio', 'rmi' => 'audio/midi', 'rmp' => 'audio/x-pn-realaudio-plugin', 'rms' => 'application/vnd.jcp.javame.midlet-rms', 'rmvb' => 'application/vnd.rn-realmedia-vbr', 'rnc' => 'application/relax-ng-compact-syntax', 'rng' => 'application/xml', 'roa' => 'application/rpki-roa', 'roff' => 'text/troff', 'rp9' => 'application/vnd.cloanto.rp9', 'rpm' => 'audio/x-pn-realaudio-plugin', 'rpss' => 'application/vnd.nokia.radio-presets', 'rpst' => 'application/vnd.nokia.radio-preset', 'rq' => 'application/sparql-query', 'rs' => 'application/rls-services+xml', 'rsa' => 'application/x-pkcs7', 'rsat' => 'application/atsc-rsat+xml', 'rsd' => 'application/rsd+xml', 'rsheet' => 'application/urc-ressheet+xml', 'rss' => 'application/rss+xml', 'rtf' => 'text/rtf', 'rtx' => 'text/richtext', 'run' => 'application/x-makeself', 'rusd' => 'application/route-usd+xml', 'rv' => 'video/vnd.rn-realvideo', 's' => 'text/x-asm', 's3m' => 'audio/s3m', 'saf' => 'application/vnd.yamaha.smaf-audio', 'sass' => 'text/x-sass', 'sbml' => 'application/sbml+xml', 'sc' => 'application/vnd.ibm.secure-container', 'scd' => 'application/x-msschedule', 'scm' => 'application/vnd.lotus-screencam', 'scq' => 'application/scvp-cv-request', 'scs' => 'application/scvp-cv-response', 'scss' => 'text/x-scss', 'scurl' => 'text/vnd.curl.scurl', 'sda' => 'application/vnd.stardivision.draw', 'sdc' => 'application/vnd.stardivision.calc', 'sdd' => 'application/vnd.stardivision.impress', 'sdkd' => 'application/vnd.solent.sdkm+xml', 'sdkm' => 'application/vnd.solent.sdkm+xml', 'sdp' => 'application/sdp', 'sdw' => 'application/vnd.stardivision.writer', 'sea' => 'application/octet-stream', 'see' => 'application/vnd.seemail', 'seed' => 'application/vnd.fdsn.seed', 'sema' => 'application/vnd.sema', 'semd' => 'application/vnd.semd', 'semf' => 'application/vnd.semf', 'senmlx' => 'application/senml+xml', 'sensmlx' => 'application/sensml+xml', 'ser' => 'application/java-serialized-object', 'setpay' => 'application/set-payment-initiation', 'setreg' => 'application/set-registration-initiation', 'sfd-hdstx' => 'application/vnd.hydrostatix.sof-data', 'sfs' => 'application/vnd.spotfire.sfs', 'sfv' => 'text/x-sfv', 'sgi' => 'image/sgi', 'sgl' => 'application/vnd.stardivision.writer-global', 'sgm' => 'text/sgml', 'sgml' => 'text/sgml', 'sh' => 'application/x-sh', 'shar' => 'application/x-shar', 'shex' => 'text/shex', 'shf' => 'application/shf+xml', 'shtml' => 'text/html', 'sid' => 'image/x-mrsid-image', 'sieve' => 'application/sieve', 'sig' => 'application/pgp-signature', 'sil' => 'audio/silk', 'silo' => 'model/mesh', 'sis' => 'application/vnd.symbian.install', 'sisx' => 'application/vnd.symbian.install', 'sit' => 'application/x-stuffit', 'sitx' => 'application/x-stuffitx', 'siv' => 'application/sieve', 'skd' => 'application/vnd.koan', 'skm' => 'application/vnd.koan', 'skp' => 'application/vnd.koan', 'skt' => 'application/vnd.koan', 'sldm' => 'application/vnd.ms-powerpoint.slide.macroenabled.12', 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', 'slim' => 'text/slim', 'slm' => 'text/slim', 'sls' => 'application/route-s-tsid+xml', 'slt' => 'application/vnd.epson.salt', 'sm' => 'application/vnd.stepmania.stepchart', 'smf' => 'application/vnd.stardivision.math', 'smi' => 'application/smil', 'smil' => 'application/smil', 'smv' => 'video/x-smv', 'smzip' => 'application/vnd.stepmania.package', 'snd' => 'audio/basic', 'snf' => 'application/x-font-snf', 'so' => 'application/octet-stream', 'spc' => 'application/x-pkcs7-certificates', 'spdx' => 'text/spdx', 'spf' => 'application/vnd.yamaha.smaf-phrase', 'spl' => 'application/x-futuresplash', 'spot' => 'text/vnd.in3d.spot', 'spp' => 'application/scvp-vp-response', 'spq' => 'application/scvp-vp-request', 'spx' => 'audio/ogg', 'sql' => 'application/x-sql', 'src' => 'application/x-wais-source', 'srt' => 'application/x-subrip', 'sru' => 'application/sru+xml', 'srx' => 'application/sparql-results+xml', 'ssdl' => 'application/ssdl+xml', 'sse' => 'application/vnd.kodak-descriptor', 'ssf' => 'application/vnd.epson.ssf', 'ssml' => 'application/ssml+xml', 'sst' => 'application/octet-stream', 'st' => 'application/vnd.sailingtracker.track', 'stc' => 'application/vnd.sun.xml.calc.template', 'std' => 'application/vnd.sun.xml.draw.template', 'step' => 'application/STEP', 'stf' => 'application/vnd.wt.stf', 'sti' => 'application/vnd.sun.xml.impress.template', 'stk' => 'application/hyperstudio', 'stl' => 'model/stl', 'stp' => 'application/STEP', 'stpx' => 'model/step+xml', 'stpxz' => 'model/step-xml+zip', 'stpz' => 'model/step+zip', 'str' => 'application/vnd.pg.format', 'stw' => 'application/vnd.sun.xml.writer.template', 'styl' => 'text/stylus', 'stylus' => 'text/stylus', 'sub' => 'text/vnd.dvb.subtitle', 'sus' => 'application/vnd.sus-calendar', 'susp' => 'application/vnd.sus-calendar', 'sv4cpio' => 'application/x-sv4cpio', 'sv4crc' => 'application/x-sv4crc', 'svc' => 'application/vnd.dvb.service', 'svd' => 'application/vnd.svd', 'svg' => 'image/svg+xml', 'svgz' => 'image/svg+xml', 'swa' => 'application/x-director', 'swf' => 'application/x-shockwave-flash', 'swi' => 'application/vnd.aristanetworks.swi', 'swidtag' => 'application/swid+xml', 'sxc' => 'application/vnd.sun.xml.calc', 'sxd' => 'application/vnd.sun.xml.draw', 'sxg' => 'application/vnd.sun.xml.writer.global', 'sxi' => 'application/vnd.sun.xml.impress', 'sxm' => 'application/vnd.sun.xml.math', 'sxw' => 'application/vnd.sun.xml.writer', 't' => 'text/troff', 't3' => 'application/x-t3vm-image', 't38' => 'image/t38', 'taglet' => 'application/vnd.mynfc', 'tao' => 'application/vnd.tao.intent-module-archive', 'tap' => 'image/vnd.tencent.tap', 'tar' => 'application/x-tar', 'tcap' => 'application/vnd.3gpp2.tcap', 'tcl' => 'application/x-tcl', 'td' => 'application/urc-targetdesc+xml', 'teacher' => 'application/vnd.smart.teacher', 'tei' => 'application/tei+xml', 'teicorpus' => 'application/tei+xml', 'tex' => 'application/x-tex', 'texi' => 'application/x-texinfo', 'texinfo' => 'application/x-texinfo', 'text' => 'text/plain', 'tfi' => 'application/thraud+xml', 'tfm' => 'application/x-tex-tfm', 'tfx' => 'image/tiff-fx', 'tga' => 'image/x-tga', 'tgz' => 'application/x-tar', 'thmx' => 'application/vnd.ms-officetheme', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'tk' => 'application/x-tcl', 'tmo' => 'application/vnd.tmobile-livetv', 'toml' => 'application/toml', 'torrent' => 'application/x-bittorrent', 'tpl' => 'application/vnd.groove-tool-template', 'tpt' => 'application/vnd.trid.tpt', 'tr' => 'text/troff', 'tra' => 'application/vnd.trueapp', 'trig' => 'application/trig', 'trm' => 'application/x-msterminal', 'ts' => 'video/mp2t', 'tsd' => 'application/timestamped-data', 'tsv' => 'text/tab-separated-values', 'ttc' => 'font/collection', 'ttf' => 'font/ttf', 'ttl' => 'text/turtle', 'ttml' => 'application/ttml+xml', 'twd' => 'application/vnd.simtech-mindmapper', 'twds' => 'application/vnd.simtech-mindmapper', 'txd' => 'application/vnd.genomatix.tuxedo', 'txf' => 'application/vnd.mobius.txf', 'txt' => 'text/plain', 'u3d' => 'model/u3d', 'u8dsn' => 'message/global-delivery-status', 'u8hdr' => 'message/global-headers', 'u8mdn' => 'message/global-disposition-notification', 'u8msg' => 'message/global', 'u32' => 'application/x-authorware-bin', 'ubj' => 'application/ubjson', 'udeb' => 'application/x-debian-package', 'ufd' => 'application/vnd.ufdl', 'ufdl' => 'application/vnd.ufdl', 'ulx' => 'application/x-glulx', 'umj' => 'application/vnd.umajin', 'unityweb' => 'application/vnd.unity', 'uo' => 'application/vnd.uoml+xml', 'uoml' => 'application/vnd.uoml+xml', 'uri' => 'text/uri-list', 'uris' => 'text/uri-list', 'urls' => 'text/uri-list', 'usda' => 'model/vnd.usda', 'usdz' => 'model/vnd.usdz+zip', 'ustar' => 'application/x-ustar', 'utz' => 'application/vnd.uiq.theme', 'uu' => 'text/x-uuencode', 'uva' => 'audio/vnd.dece.audio', 'uvd' => 'application/vnd.dece.data', 'uvf' => 'application/vnd.dece.data', 'uvg' => 'image/vnd.dece.graphic', 'uvh' => 'video/vnd.dece.hd', 'uvi' => 'image/vnd.dece.graphic', 'uvm' => 'video/vnd.dece.mobile', 'uvp' => 'video/vnd.dece.pd', 'uvs' => 'video/vnd.dece.sd', 'uvt' => 'application/vnd.dece.ttml+xml', 'uvu' => 'video/vnd.uvvu.mp4', 'uvv' => 'video/vnd.dece.video', 'uvva' => 'audio/vnd.dece.audio', 'uvvd' => 'application/vnd.dece.data', 'uvvf' => 'application/vnd.dece.data', 'uvvg' => 'image/vnd.dece.graphic', 'uvvh' => 'video/vnd.dece.hd', 'uvvi' => 'image/vnd.dece.graphic', 'uvvm' => 'video/vnd.dece.mobile', 'uvvp' => 'video/vnd.dece.pd', 'uvvs' => 'video/vnd.dece.sd', 'uvvt' => 'application/vnd.dece.ttml+xml', 'uvvu' => 'video/vnd.uvvu.mp4', 'uvvv' => 'video/vnd.dece.video', 'uvvx' => 'application/vnd.dece.unspecified', 'uvvz' => 'application/vnd.dece.zip', 'uvx' => 'application/vnd.dece.unspecified', 'uvz' => 'application/vnd.dece.zip', 'vbox' => 'application/x-virtualbox-vbox', 'vbox-extpack' => 'application/x-virtualbox-vbox-extpack', 'vcard' => 'text/vcard', 'vcd' => 'application/x-cdlink', 'vcf' => 'text/x-vcard', 'vcg' => 'application/vnd.groove-vcard', 'vcs' => 'text/x-vcalendar', 'vcx' => 'application/vnd.vcx', 'vdi' => 'application/x-virtualbox-vdi', 'vds' => 'model/vnd.sap.vds', 'vhd' => 'application/x-virtualbox-vhd', 'vis' => 'application/vnd.visionary', 'viv' => 'video/vnd.vivo', 'vlc' => 'application/videolan', 'vmdk' => 'application/x-virtualbox-vmdk', 'vob' => 'video/x-ms-vob', 'vor' => 'application/vnd.stardivision.writer', 'vox' => 'application/x-authorware-bin', 'vrml' => 'model/vrml', 'vsd' => 'application/vnd.visio', 'vsf' => 'application/vnd.vsf', 'vss' => 'application/vnd.visio', 'vst' => 'application/vnd.visio', 'vsw' => 'application/vnd.visio', 'vtf' => 'image/vnd.valve.source.texture', 'vtt' => 'text/vtt', 'vtu' => 'model/vnd.vtu', 'vxml' => 'application/voicexml+xml', 'w3d' => 'application/x-director', 'wad' => 'application/x-doom', 'wadl' => 'application/vnd.sun.wadl+xml', 'war' => 'application/java-archive', 'wasm' => 'application/wasm', 'wav' => 'audio/x-wav', 'wax' => 'audio/x-ms-wax', 'wbmp' => 'image/vnd.wap.wbmp', 'wbs' => 'application/vnd.criticaltools.wbs+xml', 'wbxml' => 'application/wbxml', 'wcm' => 'application/vnd.ms-works', 'wdb' => 'application/vnd.ms-works', 'wdp' => 'image/vnd.ms-photo', 'weba' => 'audio/webm', 'webapp' => 'application/x-web-app-manifest+json', 'webm' => 'video/webm', 'webmanifest' => 'application/manifest+json', 'webp' => 'image/webp', 'wg' => 'application/vnd.pmi.widget', 'wgsl' => 'text/wgsl', 'wgt' => 'application/widget', 'wif' => 'application/watcherinfo+xml', 'wks' => 'application/vnd.ms-works', 'wm' => 'video/x-ms-wm', 'wma' => 'audio/x-ms-wma', 'wmd' => 'application/x-ms-wmd', 'wmf' => 'image/wmf', 'wml' => 'text/vnd.wap.wml', 'wmlc' => 'application/wmlc', 'wmls' => 'text/vnd.wap.wmlscript', 'wmlsc' => 'application/vnd.wap.wmlscriptc', 'wmv' => 'video/x-ms-wmv', 'wmx' => 'video/x-ms-wmx', 'wmz' => 'application/x-msmetafile', 'woff' => 'font/woff', 'woff2' => 'font/woff2', 'word' => 'application/msword', 'wpd' => 'application/vnd.wordperfect', 'wpl' => 'application/vnd.ms-wpl', 'wps' => 'application/vnd.ms-works', 'wqd' => 'application/vnd.wqd', 'wri' => 'application/x-mswrite', 'wrl' => 'model/vrml', 'wsc' => 'message/vnd.wfa.wsc', 'wsdl' => 'application/wsdl+xml', 'wspolicy' => 'application/wspolicy+xml', 'wtb' => 'application/vnd.webturbo', 'wvx' => 'video/x-ms-wvx', 'x3d' => 'model/x3d+xml', 'x3db' => 'model/x3d+fastinfoset', 'x3dbz' => 'model/x3d+binary', 'x3dv' => 'model/x3d-vrml', 'x3dvz' => 'model/x3d+vrml', 'x3dz' => 'model/x3d+xml', 'x32' => 'application/x-authorware-bin', 'x_b' => 'model/vnd.parasolid.transmit.binary', 'x_t' => 'model/vnd.parasolid.transmit.text', 'xaml' => 'application/xaml+xml', 'xap' => 'application/x-silverlight-app', 'xar' => 'application/vnd.xara', 'xav' => 'application/xcap-att+xml', 'xbap' => 'application/x-ms-xbap', 'xbd' => 'application/vnd.fujixerox.docuworks.binder', 'xbm' => 'image/x-xbitmap', 'xca' => 'application/xcap-caps+xml', 'xcs' => 'application/calendar+xml', 'xdf' => 'application/xcap-diff+xml', 'xdm' => 'application/vnd.syncml.dm+xml', 'xdp' => 'application/vnd.adobe.xdp+xml', 'xdssc' => 'application/dssc+xml', 'xdw' => 'application/vnd.fujixerox.docuworks', 'xel' => 'application/xcap-el+xml', 'xenc' => 'application/xenc+xml', 'xer' => 'application/patch-ops-error+xml', 'xfdf' => 'application/xfdf', 'xfdl' => 'application/vnd.xfdl', 'xht' => 'application/xhtml+xml', 'xhtm' => 'application/vnd.pwg-xhtml-print+xml', 'xhtml' => 'application/xhtml+xml', 'xhvml' => 'application/xv+xml', 'xif' => 'image/vnd.xiff', 'xl' => 'application/excel', 'xla' => 'application/vnd.ms-excel', 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', 'xlc' => 'application/vnd.ms-excel', 'xlf' => 'application/xliff+xml', 'xlm' => 'application/vnd.ms-excel', 'xls' => 'application/vnd.ms-excel', 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', 'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xlt' => 'application/vnd.ms-excel', 'xltm' => 'application/vnd.ms-excel.template.macroEnabled.12', 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 'xlw' => 'application/vnd.ms-excel', 'xm' => 'audio/xm', 'xml' => 'application/xml', 'xns' => 'application/xcap-ns+xml', 'xo' => 'application/vnd.olpc-sugar', 'xop' => 'application/xop+xml', 'xpi' => 'application/x-xpinstall', 'xpl' => 'application/xproc+xml', 'xpm' => 'image/x-xpixmap', 'xpr' => 'application/vnd.is-xpr', 'xps' => 'application/vnd.ms-xpsdocument', 'xpw' => 'application/vnd.intercon.formnet', 'xpx' => 'application/vnd.intercon.formnet', 'xsd' => 'application/xml', 'xsf' => 'application/prs.xsf+xml', 'xsl' => 'application/xml', 'xslt' => 'application/xslt+xml', 'xsm' => 'application/vnd.syncml+xml', 'xspf' => 'application/xspf+xml', 'xul' => 'application/vnd.mozilla.xul+xml', 'xvm' => 'application/xv+xml', 'xvml' => 'application/xv+xml', 'xwd' => 'image/x-xwindowdump', 'xyz' => 'chemical/x-xyz', 'xz' => 'application/x-xz', 'yaml' => 'text/yaml', 'yang' => 'application/yang', 'yin' => 'application/yin+xml', 'yml' => 'text/yaml', 'ymp' => 'text/x-suse-ymp', 'z' => 'application/x-compress', 'z1' => 'application/x-zmachine', 'z2' => 'application/x-zmachine', 'z3' => 'application/x-zmachine', 'z4' => 'application/x-zmachine', 'z5' => 'application/x-zmachine', 'z6' => 'application/x-zmachine', 'z7' => 'application/x-zmachine', 'z8' => 'application/x-zmachine', 'zaz' => 'application/vnd.zzazz.deck+xml', 'zip' => 'application/zip', 'zir' => 'application/vnd.zul', 'zirz' => 'application/vnd.zul', 'zmm' => 'application/vnd.handheld-entertainment+xml', 'zsh' => 'text/x-scriptzsh', ]; /** * @var array<string, string> * * @internal */ public const EXTENSIONS_FOR_MIME_TIMES = [ 'application/andrew-inset' => ['ez'], 'application/appinstaller' => ['appinstaller'], 'application/applixware' => ['aw'], 'application/appx' => ['appx'], 'application/appxbundle' => ['appxbundle'], 'application/atom+xml' => ['atom'], 'application/atomcat+xml' => ['atomcat'], 'application/atomdeleted+xml' => ['atomdeleted'], 'application/atomsvc+xml' => ['atomsvc'], 'application/atsc-dwd+xml' => ['dwd'], 'application/atsc-held+xml' => ['held'], 'application/atsc-rsat+xml' => ['rsat'], 'application/automationml-aml+xml' => ['aml'], 'application/automationml-amlx+zip' => ['amlx'], 'application/bdoc' => ['bdoc'], 'application/calendar+xml' => ['xcs'], 'application/ccxml+xml' => ['ccxml'], 'application/cdfx+xml' => ['cdfx'], 'application/cdmi-capability' => ['cdmia'], 'application/cdmi-container' => ['cdmic'], 'application/cdmi-domain' => ['cdmid'], 'application/cdmi-object' => ['cdmio'], 'application/cdmi-queue' => ['cdmiq'], 'application/cpl+xml' => ['cpl'], 'application/cu-seeme' => ['cu'], 'application/cwl' => ['cwl'], 'application/dash+xml' => ['mpd'], 'application/dash-patch+xml' => ['mpp'], 'application/davmount+xml' => ['davmount'], 'application/docbook+xml' => ['dbk'], 'application/dssc+der' => ['dssc'], 'application/dssc+xml' => ['xdssc'], 'application/ecmascript' => ['ecma'], 'application/emma+xml' => ['emma'], 'application/emotionml+xml' => ['emotionml'], 'application/epub+zip' => ['epub'], 'application/exi' => ['exi'], 'application/express' => ['exp'], 'application/fdf' => ['fdf'], 'application/fdt+xml' => ['fdt'], 'application/font-tdpfr' => ['pfr'], 'application/geo+json' => ['geojson'], 'application/gml+xml' => ['gml'], 'application/gpx+xml' => ['gpx'], 'application/gxf' => ['gxf'], 'application/gzip' => ['gz', 'gzip'], 'application/hjson' => ['hjson'], 'application/hyperstudio' => ['stk'], 'application/inkml+xml' => ['ink', 'inkml'], 'application/ipfix' => ['ipfix'], 'application/its+xml' => ['its'], 'application/java-archive' => ['jar', 'war', 'ear'], 'application/java-serialized-object' => ['ser'], 'application/java-vm' => ['class'], 'application/javascript' => ['js'], 'application/json' => ['json', 'map'], 'application/json5' => ['json5'], 'application/jsonml+json' => ['jsonml'], 'application/ld+json' => ['jsonld'], 'application/lgr+xml' => ['lgr'], 'application/lost+xml' => ['lostxml'], 'application/mac-binhex40' => ['hqx'], 'application/mac-compactpro' => ['cpt'], 'application/mads+xml' => ['mads'], 'application/manifest+json' => ['webmanifest'], 'application/marc' => ['mrc'], 'application/marcxml+xml' => ['mrcx'], 'application/mathematica' => ['ma', 'nb', 'mb'], 'application/mathml+xml' => ['mathml'], 'application/mbox' => ['mbox'], 'application/media-policy-dataset+xml' => ['mpf'], 'application/mediaservercontrol+xml' => ['mscml'], 'application/metalink+xml' => ['metalink'], 'application/metalink4+xml' => ['meta4'], 'application/mets+xml' => ['mets'], 'application/mmt-aei+xml' => ['maei'], 'application/mmt-usd+xml' => ['musd'], 'application/mods+xml' => ['mods'], 'application/mp21' => ['m21', 'mp21'], 'application/mp4' => ['mp4', 'mpg4', 'mp4s', 'm4p'], 'application/msix' => ['msix'], 'application/msixbundle' => ['msixbundle'], 'application/msword' => ['doc', 'dot', 'word'], 'application/mxf' => ['mxf'], 'application/n-quads' => ['nq'], 'application/n-triples' => ['nt'], 'application/node' => ['cjs'], 'application/octet-stream' => ['bin', 'dms', 'lrf', 'mar', 'so', 'dist', 'distz', 'pkg', 'bpk', 'dump', 'elc', 'deploy', 'exe', 'dll', 'deb', 'dmg', 'iso', 'img', 'msi', 'msp', 'msm', 'buffer', 'phar', 'lha', 'lzh', 'class', 'sea', 'dmn', 'bpmn', 'kdb', 'sst', 'csr', 'dst', 'pv', 'pxf'], 'application/oda' => ['oda'], 'application/oebps-package+xml' => ['opf'], 'application/ogg' => ['ogx'], 'application/omdoc+xml' => ['omdoc'], 'application/onenote' => ['onetoc', 'onetoc2', 'onetmp', 'onepkg'], 'application/oxps' => ['oxps'], 'application/p2p-overlay+xml' => ['relo'], 'application/patch-ops-error+xml' => ['xer'], 'application/pdf' => ['pdf', 'ai'], 'application/pgp-encrypted' => ['pgp'], 'application/pgp-keys' => ['asc'], 'application/pgp-signature' => ['sig', 'asc'], 'application/pics-rules' => ['prf'], 'application/pkcs10' => ['p10'], 'application/pkcs7-mime' => ['p7m', 'p7c'], 'application/pkcs7-signature' => ['p7s'], 'application/pkcs8' => ['p8'], 'application/pkix-attr-cert' => ['ac'], 'application/pkix-cert' => ['cer'], 'application/pkix-crl' => ['crl'], 'application/pkix-pkipath' => ['pkipath'], 'application/pkixcmp' => ['pki'], 'application/pls+xml' => ['pls'], 'application/postscript' => ['ai', 'eps', 'ps'], 'application/provenance+xml' => ['provx'], 'application/prs.cww' => ['cww'], 'application/prs.xsf+xml' => ['xsf'], 'application/pskc+xml' => ['pskcxml'], 'application/raml+yaml' => ['raml'], 'application/rdf+xml' => ['rdf', 'owl'], 'application/reginfo+xml' => ['rif'], 'application/relax-ng-compact-syntax' => ['rnc'], 'application/resource-lists+xml' => ['rl'], 'application/resource-lists-diff+xml' => ['rld'], 'application/rls-services+xml' => ['rs'], 'application/route-apd+xml' => ['rapd'], 'application/route-s-tsid+xml' => ['sls'], 'application/route-usd+xml' => ['rusd'], 'application/rpki-ghostbusters' => ['gbr'], 'application/rpki-manifest' => ['mft'], 'application/rpki-roa' => ['roa'], 'application/rsd+xml' => ['rsd'], 'application/rss+xml' => ['rss'], 'application/rtf' => ['rtf'], 'application/sbml+xml' => ['sbml'], 'application/scvp-cv-request' => ['scq'], 'application/scvp-cv-response' => ['scs'], 'application/scvp-vp-request' => ['spq'], 'application/scvp-vp-response' => ['spp'], 'application/sdp' => ['sdp'], 'application/senml+xml' => ['senmlx'], 'application/sensml+xml' => ['sensmlx'], 'application/set-payment-initiation' => ['setpay'], 'application/set-registration-initiation' => ['setreg'], 'application/shf+xml' => ['shf'], 'application/sieve' => ['siv', 'sieve'], 'application/smil+xml' => ['smi', 'smil'], 'application/sparql-query' => ['rq'], 'application/sparql-results+xml' => ['srx'], 'application/sql' => ['sql'], 'application/srgs' => ['gram'], 'application/srgs+xml' => ['grxml'], 'application/sru+xml' => ['sru'], 'application/ssdl+xml' => ['ssdl'], 'application/ssml+xml' => ['ssml'], 'application/swid+xml' => ['swidtag'], 'application/tei+xml' => ['tei', 'teicorpus'], 'application/thraud+xml' => ['tfi'], 'application/timestamped-data' => ['tsd'], 'application/toml' => ['toml'], 'application/trig' => ['trig'], 'application/ttml+xml' => ['ttml'], 'application/ubjson' => ['ubj'], 'application/urc-ressheet+xml' => ['rsheet'], 'application/urc-targetdesc+xml' => ['td'], 'application/vnd.1000minds.decision-model+xml' => ['1km'], 'application/vnd.3gpp.pic-bw-large' => ['plb'], 'application/vnd.3gpp.pic-bw-small' => ['psb'], 'application/vnd.3gpp.pic-bw-var' => ['pvb'], 'application/vnd.3gpp2.tcap' => ['tcap'], 'application/vnd.3m.post-it-notes' => ['pwn'], 'application/vnd.accpac.simply.aso' => ['aso'], 'application/vnd.accpac.simply.imp' => ['imp'], 'application/vnd.acucobol' => ['acu'], 'application/vnd.acucorp' => ['atc', 'acutc'], 'application/vnd.adobe.air-application-installer-package+zip' => ['air'], 'application/vnd.adobe.formscentral.fcdt' => ['fcdt'], 'application/vnd.adobe.fxp' => ['fxp', 'fxpl'], 'application/vnd.adobe.xdp+xml' => ['xdp'], 'application/vnd.adobe.xfdf' => ['xfdf'], 'application/vnd.age' => ['age'], 'application/vnd.ahead.space' => ['ahead'], 'application/vnd.airzip.filesecure.azf' => ['azf'], 'application/vnd.airzip.filesecure.azs' => ['azs'], 'application/vnd.amazon.ebook' => ['azw'], 'application/vnd.americandynamics.acc' => ['acc'], 'application/vnd.amiga.ami' => ['ami'], 'application/vnd.android.package-archive' => ['apk'], 'application/vnd.anser-web-certificate-issue-initiation' => ['cii'], 'application/vnd.anser-web-funds-transfer-initiation' => ['fti'], 'application/vnd.antix.game-component' => ['atx'], 'application/vnd.apple.installer+xml' => ['mpkg'], 'application/vnd.apple.keynote' => ['key'], 'application/vnd.apple.mpegurl' => ['m3u8'], 'application/vnd.apple.numbers' => ['numbers'], 'application/vnd.apple.pages' => ['pages'], 'application/vnd.apple.pkpass' => ['pkpass'], 'application/vnd.aristanetworks.swi' => ['swi'], 'application/vnd.astraea-software.iota' => ['iota'], 'application/vnd.audiograph' => ['aep'], 'application/vnd.balsamiq.bmml+xml' => ['bmml'], 'application/vnd.blueice.multipass' => ['mpm'], 'application/vnd.bmi' => ['bmi'], 'application/vnd.businessobjects' => ['rep'], 'application/vnd.chemdraw+xml' => ['cdxml'], 'application/vnd.chipnuts.karaoke-mmd' => ['mmd'], 'application/vnd.cinderella' => ['cdy'], 'application/vnd.citationstyles.style+xml' => ['csl'], 'application/vnd.claymore' => ['cla'], 'application/vnd.cloanto.rp9' => ['rp9'], 'application/vnd.clonk.c4group' => ['c4g', 'c4d', 'c4f', 'c4p', 'c4u'], 'application/vnd.cluetrust.cartomobile-config' => ['c11amc'], 'application/vnd.cluetrust.cartomobile-config-pkg' => ['c11amz'], 'application/vnd.commonspace' => ['csp'], 'application/vnd.contact.cmsg' => ['cdbcmsg'], 'application/vnd.cosmocaller' => ['cmc'], 'application/vnd.crick.clicker' => ['clkx'], 'application/vnd.crick.clicker.keyboard' => ['clkk'], 'application/vnd.crick.clicker.palette' => ['clkp'], 'application/vnd.crick.clicker.template' => ['clkt'], 'application/vnd.crick.clicker.wordbank' => ['clkw'], 'application/vnd.criticaltools.wbs+xml' => ['wbs'], 'application/vnd.ctc-posml' => ['pml'], 'application/vnd.cups-ppd' => ['ppd'], 'application/vnd.curl.car' => ['car'], 'application/vnd.curl.pcurl' => ['pcurl'], 'application/vnd.dart' => ['dart'], 'application/vnd.data-vision.rdz' => ['rdz'], 'application/vnd.dbf' => ['dbf'], 'application/vnd.dece.data' => ['uvf', 'uvvf', 'uvd', 'uvvd'], 'application/vnd.dece.ttml+xml' => ['uvt', 'uvvt'], 'application/vnd.dece.unspecified' => ['uvx', 'uvvx'], 'application/vnd.dece.zip' => ['uvz', 'uvvz'], 'application/vnd.denovo.fcselayout-link' => ['fe_launch'], 'application/vnd.dna' => ['dna'], 'application/vnd.dolby.mlp' => ['mlp'], 'application/vnd.dpgraph' => ['dpg'], 'application/vnd.dreamfactory' => ['dfac'], 'application/vnd.ds-keypoint' => ['kpxx'], 'application/vnd.dvb.ait' => ['ait'], 'application/vnd.dvb.service' => ['svc'], 'application/vnd.dynageo' => ['geo'], 'application/vnd.ecowin.chart' => ['mag'], 'application/vnd.enliven' => ['nml'], 'application/vnd.epson.esf' => ['esf'], 'application/vnd.epson.msf' => ['msf'], 'application/vnd.epson.quickanime' => ['qam'], 'application/vnd.epson.salt' => ['slt'], 'application/vnd.epson.ssf' => ['ssf'], 'application/vnd.eszigno3+xml' => ['es3', 'et3'], 'application/vnd.ezpix-album' => ['ez2'], 'application/vnd.ezpix-package' => ['ez3'], 'application/vnd.fdf' => ['fdf'], 'application/vnd.fdsn.mseed' => ['mseed'], 'application/vnd.fdsn.seed' => ['seed', 'dataless'], 'application/vnd.flographit' => ['gph'], 'application/vnd.fluxtime.clip' => ['ftc'], 'application/vnd.framemaker' => ['fm', 'frame', 'maker', 'book'], 'application/vnd.frogans.fnc' => ['fnc'], 'application/vnd.frogans.ltf' => ['ltf'], 'application/vnd.fsc.weblaunch' => ['fsc'], 'application/vnd.fujitsu.oasys' => ['oas'], 'application/vnd.fujitsu.oasys2' => ['oa2'], 'application/vnd.fujitsu.oasys3' => ['oa3'], 'application/vnd.fujitsu.oasysgp' => ['fg5'], 'application/vnd.fujitsu.oasysprs' => ['bh2'], 'application/vnd.fujixerox.ddd' => ['ddd'], 'application/vnd.fujixerox.docuworks' => ['xdw'], 'application/vnd.fujixerox.docuworks.binder' => ['xbd'], 'application/vnd.fuzzysheet' => ['fzs'], 'application/vnd.genomatix.tuxedo' => ['txd'], 'application/vnd.geogebra.file' => ['ggb'], 'application/vnd.geogebra.tool' => ['ggt'], 'application/vnd.geometry-explorer' => ['gex', 'gre'], 'application/vnd.geonext' => ['gxt'], 'application/vnd.geoplan' => ['g2w'], 'application/vnd.geospace' => ['g3w'], 'application/vnd.gmx' => ['gmx'], 'application/vnd.google-apps.document' => ['gdoc'], 'application/vnd.google-apps.presentation' => ['gslides'], 'application/vnd.google-apps.spreadsheet' => ['gsheet'], 'application/vnd.google-earth.kml+xml' => ['kml'], 'application/vnd.google-earth.kmz' => ['kmz'], 'application/vnd.grafeq' => ['gqf', 'gqs'], 'application/vnd.groove-account' => ['gac'], 'application/vnd.groove-help' => ['ghf'], 'application/vnd.groove-identity-message' => ['gim'], 'application/vnd.groove-injector' => ['grv'], 'application/vnd.groove-tool-message' => ['gtm'], 'application/vnd.groove-tool-template' => ['tpl'], 'application/vnd.groove-vcard' => ['vcg'], 'application/vnd.hal+xml' => ['hal'], 'application/vnd.handheld-entertainment+xml' => ['zmm'], 'application/vnd.hbci' => ['hbci'], 'application/vnd.hhe.lesson-player' => ['les'], 'application/vnd.hp-hpgl' => ['hpgl'], 'application/vnd.hp-hpid' => ['hpid'], 'application/vnd.hp-hps' => ['hps'], 'application/vnd.hp-jlyt' => ['jlt'], 'application/vnd.hp-pcl' => ['pcl'], 'application/vnd.hp-pclxl' => ['pclxl'], 'application/vnd.hydrostatix.sof-data' => ['sfd-hdstx'], 'application/vnd.ibm.minipay' => ['mpy'], 'application/vnd.ibm.modcap' => ['afp', 'listafp', 'list3820'], 'application/vnd.ibm.rights-management' => ['irm'], 'application/vnd.ibm.secure-container' => ['sc'], 'application/vnd.iccprofile' => ['icc', 'icm'], 'application/vnd.igloader' => ['igl'], 'application/vnd.immervision-ivp' => ['ivp'], 'application/vnd.immervision-ivu' => ['ivu'], 'application/vnd.insors.igm' => ['igm'], 'application/vnd.intercon.formnet' => ['xpw', 'xpx'], 'application/vnd.intergeo' => ['i2g'], 'application/vnd.intu.qbo' => ['qbo'], 'application/vnd.intu.qfx' => ['qfx'], 'application/vnd.ipunplugged.rcprofile' => ['rcprofile'], 'application/vnd.irepository.package+xml' => ['irp'], 'application/vnd.is-xpr' => ['xpr'], 'application/vnd.isac.fcs' => ['fcs'], 'application/vnd.jam' => ['jam'], 'application/vnd.jcp.javame.midlet-rms' => ['rms'], 'application/vnd.jisp' => ['jisp'], 'application/vnd.joost.joda-archive' => ['joda'], 'application/vnd.kahootz' => ['ktz', 'ktr'], 'application/vnd.kde.karbon' => ['karbon'], 'application/vnd.kde.kchart' => ['chrt'], 'application/vnd.kde.kformula' => ['kfo'], 'application/vnd.kde.kivio' => ['flw'], 'application/vnd.kde.kontour' => ['kon'], 'application/vnd.kde.kpresenter' => ['kpr', 'kpt'], 'application/vnd.kde.kspread' => ['ksp'], 'application/vnd.kde.kword' => ['kwd', 'kwt'], 'application/vnd.kenameaapp' => ['htke'], 'application/vnd.kidspiration' => ['kia'], 'application/vnd.kinar' => ['kne', 'knp'], 'application/vnd.koan' => ['skp', 'skd', 'skt', 'skm'], 'application/vnd.kodak-descriptor' => ['sse'], 'application/vnd.las.las+xml' => ['lasxml'], 'application/vnd.llamagraphics.life-balance.desktop' => ['lbd'], 'application/vnd.llamagraphics.life-balance.exchange+xml' => ['lbe'], 'application/vnd.lotus-1-2-3' => ['123'], 'application/vnd.lotus-approach' => ['apr'], 'application/vnd.lotus-freelance' => ['pre'], 'application/vnd.lotus-notes' => ['nsf'], 'application/vnd.lotus-organizer' => ['org'], 'application/vnd.lotus-screencam' => ['scm'], 'application/vnd.lotus-wordpro' => ['lwp'], 'application/vnd.macports.portpkg' => ['portpkg'], 'application/vnd.mapbox-vector-tile' => ['mvt'], 'application/vnd.mcd' => ['mcd'], 'application/vnd.medcalcdata' => ['mc1'], 'application/vnd.mediastation.cdkey' => ['cdkey'], 'application/vnd.mfer' => ['mwf'], 'application/vnd.mfmp' => ['mfm'], 'application/vnd.micrografx.flo' => ['flo'], 'application/vnd.micrografx.igx' => ['igx'], 'application/vnd.mif' => ['mif'], 'application/vnd.mobius.daf' => ['daf'], 'application/vnd.mobius.dis' => ['dis'], 'application/vnd.mobius.mbk' => ['mbk'], 'application/vnd.mobius.mqy' => ['mqy'], 'application/vnd.mobius.msl' => ['msl'], 'application/vnd.mobius.plc' => ['plc'], 'application/vnd.mobius.txf' => ['txf'], 'application/vnd.mophun.application' => ['mpn'], 'application/vnd.mophun.certificate' => ['mpc'], 'application/vnd.mozilla.xul+xml' => ['xul'], 'application/vnd.ms-artgalry' => ['cil'], 'application/vnd.ms-cab-compressed' => ['cab'], 'application/vnd.ms-excel' => ['xls', 'xlm', 'xla', 'xlc', 'xlt', 'xlw'], 'application/vnd.ms-excel.addin.macroenabled.12' => ['xlam'], 'application/vnd.ms-excel.sheet.binary.macroenabled.12' => ['xlsb'], 'application/vnd.ms-excel.sheet.macroenabled.12' => ['xlsm'], 'application/vnd.ms-excel.template.macroenabled.12' => ['xltm'], 'application/vnd.ms-fontobject' => ['eot'], 'application/vnd.ms-htmlhelp' => ['chm'], 'application/vnd.ms-ims' => ['ims'], 'application/vnd.ms-lrm' => ['lrm'], 'application/vnd.ms-officetheme' => ['thmx'], 'application/vnd.ms-outlook' => ['msg'], 'application/vnd.ms-pki.seccat' => ['cat'], 'application/vnd.ms-pki.stl' => ['stl'], 'application/vnd.ms-powerpoint' => ['ppt', 'pps', 'pot', 'ppa'], 'application/vnd.ms-powerpoint.addin.macroenabled.12' => ['ppam'], 'application/vnd.ms-powerpoint.presentation.macroenabled.12' => ['pptm'], 'application/vnd.ms-powerpoint.slide.macroenabled.12' => ['sldm'], 'application/vnd.ms-powerpoint.slideshow.macroenabled.12' => ['ppsm'], 'application/vnd.ms-powerpoint.template.macroenabled.12' => ['potm'], 'application/vnd.ms-project' => ['mpp', 'mpt'], 'application/vnd.ms-word.document.macroenabled.12' => ['docm'], 'application/vnd.ms-word.template.macroenabled.12' => ['dotm'], 'application/vnd.ms-works' => ['wps', 'wks', 'wcm', 'wdb'], 'application/vnd.ms-wpl' => ['wpl'], 'application/vnd.ms-xpsdocument' => ['xps'], 'application/vnd.mseq' => ['mseq'], 'application/vnd.musician' => ['mus'], 'application/vnd.muvee.style' => ['msty'], 'application/vnd.mynfc' => ['taglet'], 'application/vnd.neurolanguage.nlu' => ['nlu'], 'application/vnd.nitf' => ['ntf', 'nitf'], 'application/vnd.noblenet-directory' => ['nnd'], 'application/vnd.noblenet-sealer' => ['nns'], 'application/vnd.noblenet-web' => ['nnw'], 'application/vnd.nokia.n-gage.ac+xml' => ['ac'], 'application/vnd.nokia.n-gage.data' => ['ngdat'], 'application/vnd.nokia.n-gage.symbian.install' => ['n-gage'], 'application/vnd.nokia.radio-preset' => ['rpst'], 'application/vnd.nokia.radio-presets' => ['rpss'], 'application/vnd.novadigm.edm' => ['edm'], 'application/vnd.novadigm.edx' => ['edx'], 'application/vnd.novadigm.ext' => ['ext'], 'application/vnd.oasis.opendocument.chart' => ['odc'], 'application/vnd.oasis.opendocument.chart-template' => ['otc'], 'application/vnd.oasis.opendocument.database' => ['odb'], 'application/vnd.oasis.opendocument.formula' => ['odf'], 'application/vnd.oasis.opendocument.formula-template' => ['odft'], 'application/vnd.oasis.opendocument.graphics' => ['odg'], 'application/vnd.oasis.opendocument.graphics-template' => ['otg'], 'application/vnd.oasis.opendocument.image' => ['odi'], 'application/vnd.oasis.opendocument.image-template' => ['oti'], 'application/vnd.oasis.opendocument.presentation' => ['odp'], 'application/vnd.oasis.opendocument.presentation-template' => ['otp'], 'application/vnd.oasis.opendocument.spreadsheet' => ['ods'], 'application/vnd.oasis.opendocument.spreadsheet-template' => ['ots'], 'application/vnd.oasis.opendocument.text' => ['odt'], 'application/vnd.oasis.opendocument.text-master' => ['odm'], 'application/vnd.oasis.opendocument.text-template' => ['ott'], 'application/vnd.oasis.opendocument.text-web' => ['oth'], 'application/vnd.olpc-sugar' => ['xo'], 'application/vnd.oma.dd2+xml' => ['dd2'], 'application/vnd.openblox.game+xml' => ['obgx'], 'application/vnd.openofficeorg.extension' => ['oxt'], 'application/vnd.openstreetmap.data+xml' => ['osm'], 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => ['pptx'], 'application/vnd.openxmlformats-officedocument.presentationml.slide' => ['sldx'], 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => ['ppsx'], 'application/vnd.openxmlformats-officedocument.presentationml.template' => ['potx'], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => ['xlsx'], 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => ['xltx'], 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => ['docx'], 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => ['dotx'], 'application/vnd.osgeo.mapguide.package' => ['mgp'], 'application/vnd.osgi.dp' => ['dp'], 'application/vnd.osgi.subsystem' => ['esa'], 'application/vnd.palm' => ['pdb', 'pqa', 'oprc'], 'application/vnd.pawaafile' => ['paw'], 'application/vnd.pg.format' => ['str'], 'application/vnd.pg.osasli' => ['ei6'], 'application/vnd.picsel' => ['efif'], 'application/vnd.pmi.widget' => ['wg'], 'application/vnd.pocketlearn' => ['plf'], 'application/vnd.powerbuilder6' => ['pbd'], 'application/vnd.previewsystems.box' => ['box'], 'application/vnd.proteus.magazine' => ['mgz'], 'application/vnd.publishare-delta-tree' => ['qps'], 'application/vnd.pvi.ptid1' => ['ptid'], 'application/vnd.pwg-xhtml-print+xml' => ['xhtm'], 'application/vnd.quark.quarkxpress' => ['qxd', 'qxt', 'qwd', 'qwt', 'qxl', 'qxb'], 'application/vnd.rar' => ['rar'], 'application/vnd.realvnc.bed' => ['bed'], 'application/vnd.recordare.musicxml' => ['mxl'], 'application/vnd.recordare.musicxml+xml' => ['musicxml'], 'application/vnd.rig.cryptonote' => ['cryptonote'], 'application/vnd.rim.cod' => ['cod'], 'application/vnd.rn-realmedia' => ['rm'], 'application/vnd.rn-realmedia-vbr' => ['rmvb'], 'application/vnd.route66.link66+xml' => ['link66'], 'application/vnd.sailingtracker.track' => ['st'], 'application/vnd.seemail' => ['see'], 'application/vnd.sema' => ['sema'], 'application/vnd.semd' => ['semd'], 'application/vnd.semf' => ['semf'], 'application/vnd.shana.informed.formdata' => ['ifm'], 'application/vnd.shana.informed.formtemplate' => ['itp'], 'application/vnd.shana.informed.interchange' => ['iif'], 'application/vnd.shana.informed.package' => ['ipk'], 'application/vnd.simtech-mindmapper' => ['twd', 'twds'], 'application/vnd.smaf' => ['mmf'], 'application/vnd.smart.teacher' => ['teacher'], 'application/vnd.software602.filler.form+xml' => ['fo'], 'application/vnd.solent.sdkm+xml' => ['sdkm', 'sdkd'], 'application/vnd.spotfire.dxp' => ['dxp'], 'application/vnd.spotfire.sfs' => ['sfs'], 'application/vnd.stardivision.calc' => ['sdc'], 'application/vnd.stardivision.draw' => ['sda'], 'application/vnd.stardivision.impress' => ['sdd'], 'application/vnd.stardivision.math' => ['smf'], 'application/vnd.stardivision.writer' => ['sdw', 'vor'], 'application/vnd.stardivision.writer-global' => ['sgl'], 'application/vnd.stepmania.package' => ['smzip'], 'application/vnd.stepmania.stepchart' => ['sm'], 'application/vnd.sun.wadl+xml' => ['wadl'], 'application/vnd.sun.xml.calc' => ['sxc'], 'application/vnd.sun.xml.calc.template' => ['stc'], 'application/vnd.sun.xml.draw' => ['sxd'], 'application/vnd.sun.xml.draw.template' => ['std'], 'application/vnd.sun.xml.impress' => ['sxi'], 'application/vnd.sun.xml.impress.template' => ['sti'], 'application/vnd.sun.xml.math' => ['sxm'], 'application/vnd.sun.xml.writer' => ['sxw'], 'application/vnd.sun.xml.writer.global' => ['sxg'], 'application/vnd.sun.xml.writer.template' => ['stw'], 'application/vnd.sus-calendar' => ['sus', 'susp'], 'application/vnd.svd' => ['svd'], 'application/vnd.symbian.install' => ['sis', 'sisx'], 'application/vnd.syncml+xml' => ['xsm'], 'application/vnd.syncml.dm+wbxml' => ['bdm'], 'application/vnd.syncml.dm+xml' => ['xdm'], 'application/vnd.syncml.dmddf+xml' => ['ddf'], 'application/vnd.tao.intent-module-archive' => ['tao'], 'application/vnd.tcpdump.pcap' => ['pcap', 'cap', 'dmp'], 'application/vnd.tmobile-livetv' => ['tmo'], 'application/vnd.trid.tpt' => ['tpt'], 'application/vnd.triscape.mxs' => ['mxs'], 'application/vnd.trueapp' => ['tra'], 'application/vnd.ufdl' => ['ufd', 'ufdl'], 'application/vnd.uiq.theme' => ['utz'], 'application/vnd.umajin' => ['umj'], 'application/vnd.unity' => ['unityweb'], 'application/vnd.uoml+xml' => ['uoml', 'uo'], 'application/vnd.vcx' => ['vcx'], 'application/vnd.visio' => ['vsd', 'vst', 'vss', 'vsw'], 'application/vnd.visionary' => ['vis'], 'application/vnd.vsf' => ['vsf'], 'application/vnd.wap.wbxml' => ['wbxml'], 'application/vnd.wap.wmlc' => ['wmlc'], 'application/vnd.wap.wmlscriptc' => ['wmlsc'], 'application/vnd.webturbo' => ['wtb'], 'application/vnd.wolfram.player' => ['nbp'], 'application/vnd.wordperfect' => ['wpd'], 'application/vnd.wqd' => ['wqd'], 'application/vnd.wt.stf' => ['stf'], 'application/vnd.xara' => ['xar'], 'application/vnd.xfdl' => ['xfdl'], 'application/vnd.yamaha.hv-dic' => ['hvd'], 'application/vnd.yamaha.hv-script' => ['hvs'], 'application/vnd.yamaha.hv-voice' => ['hvp'], 'application/vnd.yamaha.openscoreformat' => ['osf'], 'application/vnd.yamaha.openscoreformat.osfpvg+xml' => ['osfpvg'], 'application/vnd.yamaha.smaf-audio' => ['saf'], 'application/vnd.yamaha.smaf-phrase' => ['spf'], 'application/vnd.yellowriver-custom-menu' => ['cmp'], 'application/vnd.zul' => ['zir', 'zirz'], 'application/vnd.zzazz.deck+xml' => ['zaz'], 'application/voicexml+xml' => ['vxml'], 'application/wasm' => ['wasm'], 'application/watcherinfo+xml' => ['wif'], 'application/widget' => ['wgt'], 'application/winhlp' => ['hlp'], 'application/wsdl+xml' => ['wsdl'], 'application/wspolicy+xml' => ['wspolicy'], 'application/x-7z-compressed' => ['7z', '7zip'], 'application/x-abiword' => ['abw'], 'application/x-ace-compressed' => ['ace'], 'application/x-apple-diskimage' => ['dmg'], 'application/x-arj' => ['arj'], 'application/x-authorware-bin' => ['aab', 'x32', 'u32', 'vox'], 'application/x-authorware-map' => ['aam'], 'application/x-authorware-seg' => ['aas'], 'application/x-bcpio' => ['bcpio'], 'application/x-bdoc' => ['bdoc'], 'application/x-bittorrent' => ['torrent'], 'application/x-blorb' => ['blb', 'blorb'], 'application/x-bzip' => ['bz'], 'application/x-bzip2' => ['bz2', 'boz'], 'application/x-cbr' => ['cbr', 'cba', 'cbt', 'cbz', 'cb7'], 'application/x-cdlink' => ['vcd'], 'application/x-cfs-compressed' => ['cfs'], 'application/x-chat' => ['chat'], 'application/x-chess-pgn' => ['pgn'], 'application/x-chrome-extension' => ['crx'], 'application/x-cocoa' => ['cco'], 'application/x-conference' => ['nsc'], 'application/x-cpio' => ['cpio'], 'application/x-csh' => ['csh'], 'application/x-debian-package' => ['deb', 'udeb'], 'application/x-dgc-compressed' => ['dgc'], 'application/x-director' => ['dir', 'dcr', 'dxr', 'cst', 'cct', 'cxt', 'w3d', 'fgd', 'swa'], 'application/x-doom' => ['wad'], 'application/x-dtbncx+xml' => ['ncx'], 'application/x-dtbook+xml' => ['dtb'], 'application/x-dtbresource+xml' => ['res'], 'application/x-dvi' => ['dvi'], 'application/x-envoy' => ['evy'], 'application/x-eva' => ['eva'], 'application/x-font-bdf' => ['bdf'], 'application/x-font-ghostscript' => ['gsf'], 'application/x-font-linux-psf' => ['psf'], 'application/x-font-pcf' => ['pcf'], 'application/x-font-snf' => ['snf'], 'application/x-font-type1' => ['pfa', 'pfb', 'pfm', 'afm'], 'application/x-freearc' => ['arc'], 'application/x-futuresplash' => ['spl'], 'application/x-gca-compressed' => ['gca'], 'application/x-glulx' => ['ulx'], 'application/x-gnumeric' => ['gnumeric'], 'application/x-gramps-xml' => ['gramps'], 'application/x-gtar' => ['gtar'], 'application/x-hdf' => ['hdf'], 'application/x-httpd-php' => ['php', 'php4', 'php3', 'phtml'], 'application/x-install-instructions' => ['install'], 'application/x-iso9660-image' => ['iso'], 'application/x-iwork-keynote-sffkey' => ['key'], 'application/x-iwork-numbers-sffnumbers' => ['numbers'], 'application/x-iwork-pages-sffpages' => ['pages'], 'application/x-java-archive-diff' => ['jardiff'], 'application/x-java-jnlp-file' => ['jnlp'], 'application/x-keepass2' => ['kdbx'], 'application/x-latex' => ['latex'], 'application/x-lua-bytecode' => ['luac'], 'application/x-lzh-compressed' => ['lzh', 'lha'], 'application/x-makeself' => ['run'], 'application/x-mie' => ['mie'], 'application/x-mobipocket-ebook' => ['prc', 'mobi'], 'application/x-ms-application' => ['application'], 'application/x-ms-shortcut' => ['lnk'], 'application/x-ms-wmd' => ['wmd'], 'application/x-ms-wmz' => ['wmz'], 'application/x-ms-xbap' => ['xbap'], 'application/x-msaccess' => ['mdb'], 'application/x-msbinder' => ['obd'], 'application/x-mscardfile' => ['crd'], 'application/x-msclip' => ['clp'], 'application/x-msdos-program' => ['exe'], 'application/x-msdownload' => ['exe', 'dll', 'com', 'bat', 'msi'], 'application/x-msmediaview' => ['mvb', 'm13', 'm14'], 'application/x-msmetafile' => ['wmf', 'wmz', 'emf', 'emz'], 'application/x-msmoney' => ['mny'], 'application/x-mspublisher' => ['pub'], 'application/x-msschedule' => ['scd'], 'application/x-msterminal' => ['trm'], 'application/x-mswrite' => ['wri'], 'application/x-netcdf' => ['nc', 'cdf'], 'application/x-ns-proxy-autoconfig' => ['pac'], 'application/x-nzb' => ['nzb'], 'application/x-perl' => ['pl', 'pm'], 'application/x-pilot' => ['prc', 'pdb'], 'application/x-pkcs12' => ['p12', 'pfx'], 'application/x-pkcs7-certificates' => ['p7b', 'spc'], 'application/x-pkcs7-certreqresp' => ['p7r'], 'application/x-rar-compressed' => ['rar'], 'application/x-redhat-package-manager' => ['rpm'], 'application/x-research-info-systems' => ['ris'], 'application/x-sea' => ['sea'], 'application/x-sh' => ['sh'], 'application/x-shar' => ['shar'], 'application/x-shockwave-flash' => ['swf'], 'application/x-silverlight-app' => ['xap'], 'application/x-sql' => ['sql'], 'application/x-stuffit' => ['sit'], 'application/x-stuffitx' => ['sitx'], 'application/x-subrip' => ['srt'], 'application/x-sv4cpio' => ['sv4cpio'], 'application/x-sv4crc' => ['sv4crc'], 'application/x-t3vm-image' => ['t3'], 'application/x-tads' => ['gam'], 'application/x-tar' => ['tar', 'tgz'], 'application/x-tcl' => ['tcl', 'tk'], 'application/x-tex' => ['tex'], 'application/x-tex-tfm' => ['tfm'], 'application/x-texinfo' => ['texinfo', 'texi'], 'application/x-tgif' => ['obj'], 'application/x-ustar' => ['ustar'], 'application/x-virtualbox-hdd' => ['hdd'], 'application/x-virtualbox-ova' => ['ova'], 'application/x-virtualbox-ovf' => ['ovf'], 'application/x-virtualbox-vbox' => ['vbox'], 'application/x-virtualbox-vbox-extpack' => ['vbox-extpack'], 'application/x-virtualbox-vdi' => ['vdi'], 'application/x-virtualbox-vhd' => ['vhd'], 'application/x-virtualbox-vmdk' => ['vmdk'], 'application/x-wais-source' => ['src'], 'application/x-web-app-manifest+json' => ['webapp'], 'application/x-x509-ca-cert' => ['der', 'crt', 'pem'], 'application/x-xfig' => ['fig'], 'application/x-xliff+xml' => ['xlf'], 'application/x-xpinstall' => ['xpi'], 'application/x-xz' => ['xz'], 'application/x-zmachine' => ['z1', 'z2', 'z3', 'z4', 'z5', 'z6', 'z7', 'z8'], 'application/xaml+xml' => ['xaml'], 'application/xcap-att+xml' => ['xav'], 'application/xcap-caps+xml' => ['xca'], 'application/xcap-diff+xml' => ['xdf'], 'application/xcap-el+xml' => ['xel'], 'application/xcap-ns+xml' => ['xns'], 'application/xenc+xml' => ['xenc'], 'application/xfdf' => ['xfdf'], 'application/xhtml+xml' => ['xhtml', 'xht'], 'application/xliff+xml' => ['xlf'], 'application/xml' => ['xml', 'xsl', 'xsd', 'rng'], 'application/xml-dtd' => ['dtd'], 'application/xop+xml' => ['xop'], 'application/xproc+xml' => ['xpl'], 'application/xslt+xml' => ['xsl', 'xslt'], 'application/xspf+xml' => ['xspf'], 'application/xv+xml' => ['mxml', 'xhvml', 'xvml', 'xvm'], 'application/yang' => ['yang'], 'application/yin+xml' => ['yin'], 'application/zip' => ['zip'], 'audio/3gpp' => ['3gpp'], 'audio/aac' => ['adts', 'aac'], 'audio/adpcm' => ['adp'], 'audio/amr' => ['amr'], 'audio/basic' => ['au', 'snd'], 'audio/midi' => ['mid', 'midi', 'kar', 'rmi'], 'audio/mobile-xmf' => ['mxmf'], 'audio/mp3' => ['mp3'], 'audio/mp4' => ['m4a', 'mp4a'], 'audio/mpeg' => ['mpga', 'mp2', 'mp2a', 'mp3', 'm2a', 'm3a'], 'audio/ogg' => ['oga', 'ogg', 'spx', 'opus'], 'audio/s3m' => ['s3m'], 'audio/silk' => ['sil'], 'audio/vnd.dece.audio' => ['uva', 'uvva'], 'audio/vnd.digital-winds' => ['eol'], 'audio/vnd.dra' => ['dra'], 'audio/vnd.dts' => ['dts'], 'audio/vnd.dts.hd' => ['dtshd'], 'audio/vnd.lucent.voice' => ['lvp'], 'audio/vnd.ms-playready.media.pya' => ['pya'], 'audio/vnd.nuera.ecelp4800' => ['ecelp4800'], 'audio/vnd.nuera.ecelp7470' => ['ecelp7470'], 'audio/vnd.nuera.ecelp9600' => ['ecelp9600'], 'audio/vnd.rip' => ['rip'], 'audio/wav' => ['wav'], 'audio/wave' => ['wav'], 'audio/webm' => ['weba'], 'audio/x-aac' => ['aac'], 'audio/x-aiff' => ['aif', 'aiff', 'aifc'], 'audio/x-caf' => ['caf'], 'audio/x-flac' => ['flac'], 'audio/x-m4a' => ['m4a'], 'audio/x-matroska' => ['mka'], 'audio/x-mpegurl' => ['m3u'], 'audio/x-ms-wax' => ['wax'], 'audio/x-ms-wma' => ['wma'], 'audio/x-pn-realaudio' => ['ram', 'ra', 'rm'], 'audio/x-pn-realaudio-plugin' => ['rmp', 'rpm'], 'audio/x-realaudio' => ['ra'], 'audio/x-wav' => ['wav'], 'audio/xm' => ['xm'], 'chemical/x-cdx' => ['cdx'], 'chemical/x-cif' => ['cif'], 'chemical/x-cmdf' => ['cmdf'], 'chemical/x-cml' => ['cml'], 'chemical/x-csml' => ['csml'], 'chemical/x-xyz' => ['xyz'], 'font/collection' => ['ttc'], 'font/otf' => ['otf'], 'font/ttf' => ['ttf'], 'font/woff' => ['woff'], 'font/woff2' => ['woff2'], 'image/aces' => ['exr'], 'image/apng' => ['apng'], 'image/avci' => ['avci'], 'image/avcs' => ['avcs'], 'image/avif' => ['avif'], 'image/bmp' => ['bmp', 'dib'], 'image/cgm' => ['cgm'], 'image/dicom-rle' => ['drle'], 'image/dpx' => ['dpx'], 'image/emf' => ['emf'], 'image/fits' => ['fits'], 'image/g3fax' => ['g3'], 'image/gif' => ['gif'], 'image/heic' => ['heic'], 'image/heic-sequence' => ['heics'], 'image/heif' => ['heif'], 'image/heif-sequence' => ['heifs'], 'image/hej2k' => ['hej2'], 'image/hsj2' => ['hsj2'], 'image/ief' => ['ief'], 'image/jls' => ['jls'], 'image/jp2' => ['jp2', 'jpg2'], 'image/jpeg' => ['jpeg', 'jpg', 'jpe'], 'image/jph' => ['jph'], 'image/jphc' => ['jhc'], 'image/jpm' => ['jpm', 'jpgm'], 'image/jpx' => ['jpx', 'jpf'], 'image/jxr' => ['jxr'], 'image/jxra' => ['jxra'], 'image/jxrs' => ['jxrs'], 'image/jxs' => ['jxs'], 'image/jxsc' => ['jxsc'], 'image/jxsi' => ['jxsi'], 'image/jxss' => ['jxss'], 'image/ktx' => ['ktx'], 'image/ktx2' => ['ktx2'], 'image/png' => ['png'], 'image/prs.btif' => ['btif', 'btf'], 'image/prs.pti' => ['pti'], 'image/sgi' => ['sgi'], 'image/svg+xml' => ['svg', 'svgz'], 'image/t38' => ['t38'], 'image/tiff' => ['tif', 'tiff'], 'image/tiff-fx' => ['tfx'], 'image/vnd.adobe.photoshop' => ['psd'], 'image/vnd.airzip.accelerator.azv' => ['azv'], 'image/vnd.dece.graphic' => ['uvi', 'uvvi', 'uvg', 'uvvg'], 'image/vnd.djvu' => ['djvu', 'djv'], 'image/vnd.dvb.subtitle' => ['sub'], 'image/vnd.dwg' => ['dwg'], 'image/vnd.dxf' => ['dxf'], 'image/vnd.fastbidsheet' => ['fbs'], 'image/vnd.fpx' => ['fpx'], 'image/vnd.fst' => ['fst'], 'image/vnd.fujixerox.edmics-mmr' => ['mmr'], 'image/vnd.fujixerox.edmics-rlc' => ['rlc'], 'image/vnd.microsoft.icon' => ['ico'], 'image/vnd.ms-dds' => ['dds'], 'image/vnd.ms-modi' => ['mdi'], 'image/vnd.ms-photo' => ['wdp'], 'image/vnd.net-fpx' => ['npx'], 'image/vnd.pco.b16' => ['b16'], 'image/vnd.tencent.tap' => ['tap'], 'image/vnd.valve.source.texture' => ['vtf'], 'image/vnd.wap.wbmp' => ['wbmp'], 'image/vnd.xiff' => ['xif'], 'image/vnd.zbrush.pcx' => ['pcx'], 'image/webp' => ['webp'], 'image/wmf' => ['wmf'], 'image/x-3ds' => ['3ds'], 'image/x-cmu-raster' => ['ras'], 'image/x-cmx' => ['cmx'], 'image/x-freehand' => ['fh', 'fhc', 'fh4', 'fh5', 'fh7'], 'image/x-icon' => ['ico'], 'image/x-jng' => ['jng'], 'image/x-mrsid-image' => ['sid'], 'image/x-ms-bmp' => ['bmp'], 'image/x-pcx' => ['pcx'], 'image/x-pict' => ['pic', 'pct'], 'image/x-portable-anymap' => ['pnm'], 'image/x-portable-bitmap' => ['pbm'], 'image/x-portable-graymap' => ['pgm'], 'image/x-portable-pixmap' => ['ppm'], 'image/x-rgb' => ['rgb'], 'image/x-tga' => ['tga'], 'image/x-xbitmap' => ['xbm'], 'image/x-xpixmap' => ['xpm'], 'image/x-xwindowdump' => ['xwd'], 'message/disposition-notification' => ['disposition-notification'], 'message/global' => ['u8msg'], 'message/global-delivery-status' => ['u8dsn'], 'message/global-disposition-notification' => ['u8mdn'], 'message/global-headers' => ['u8hdr'], 'message/rfc822' => ['eml', 'mime'], 'message/vnd.wfa.wsc' => ['wsc'], 'model/3mf' => ['3mf'], 'model/gltf+json' => ['gltf'], 'model/gltf-binary' => ['glb'], 'model/iges' => ['igs', 'iges'], 'model/jt' => ['jt'], 'model/mesh' => ['msh', 'mesh', 'silo'], 'model/mtl' => ['mtl'], 'model/obj' => ['obj'], 'model/prc' => ['prc'], 'model/step+xml' => ['stpx'], 'model/step+zip' => ['stpz'], 'model/step-xml+zip' => ['stpxz'], 'model/stl' => ['stl'], 'model/u3d' => ['u3d'], 'model/vnd.cld' => ['cld'], 'model/vnd.collada+xml' => ['dae'], 'model/vnd.dwf' => ['dwf'], 'model/vnd.gdl' => ['gdl'], 'model/vnd.gtw' => ['gtw'], 'model/vnd.mts' => ['mts'], 'model/vnd.opengex' => ['ogex'], 'model/vnd.parasolid.transmit.binary' => ['x_b'], 'model/vnd.parasolid.transmit.text' => ['x_t'], 'model/vnd.pytha.pyox' => ['pyo', 'pyox'], 'model/vnd.sap.vds' => ['vds'], 'model/vnd.usda' => ['usda'], 'model/vnd.usdz+zip' => ['usdz'], 'model/vnd.valve.source.compiled-map' => ['bsp'], 'model/vnd.vtu' => ['vtu'], 'model/vrml' => ['wrl', 'vrml'], 'model/x3d+binary' => ['x3db', 'x3dbz'], 'model/x3d+fastinfoset' => ['x3db'], 'model/x3d+vrml' => ['x3dv', 'x3dvz'], 'model/x3d+xml' => ['x3d', 'x3dz'], 'model/x3d-vrml' => ['x3dv'], 'text/cache-manifest' => ['appcache', 'manifest'], 'text/calendar' => ['ics', 'ifb'], 'text/coffeescript' => ['coffee', 'litcoffee'], 'text/css' => ['css'], 'text/csv' => ['csv'], 'text/html' => ['html', 'htm', 'shtml'], 'text/jade' => ['jade'], 'text/javascript' => ['js', 'mjs'], 'text/jsx' => ['jsx'], 'text/less' => ['less'], 'text/markdown' => ['md', 'markdown'], 'text/mathml' => ['mml'], 'text/mdx' => ['mdx'], 'text/n3' => ['n3'], 'text/plain' => ['txt', 'text', 'conf', 'def', 'list', 'log', 'in', 'ini', 'm3u'], 'text/prs.lines.tag' => ['dsc'], 'text/richtext' => ['rtx'], 'text/rtf' => ['rtf'], 'text/sgml' => ['sgml', 'sgm'], 'text/shex' => ['shex'], 'text/slim' => ['slim', 'slm'], 'text/spdx' => ['spdx'], 'text/stylus' => ['stylus', 'styl'], 'text/tab-separated-values' => ['tsv'], 'text/troff' => ['t', 'tr', 'roff', 'man', 'me', 'ms'], 'text/turtle' => ['ttl'], 'text/uri-list' => ['uri', 'uris', 'urls'], 'text/vcard' => ['vcard'], 'text/vnd.curl' => ['curl'], 'text/vnd.curl.dcurl' => ['dcurl'], 'text/vnd.curl.mcurl' => ['mcurl'], 'text/vnd.curl.scurl' => ['scurl'], 'text/vnd.dvb.subtitle' => ['sub'], 'text/vnd.familysearch.gedcom' => ['ged'], 'text/vnd.fly' => ['fly'], 'text/vnd.fmi.flexstor' => ['flx'], 'text/vnd.graphviz' => ['gv'], 'text/vnd.in3d.3dml' => ['3dml'], 'text/vnd.in3d.spot' => ['spot'], 'text/vnd.sun.j2me.app-descriptor' => ['jad'], 'text/vnd.wap.wml' => ['wml'], 'text/vnd.wap.wmlscript' => ['wmls'], 'text/vtt' => ['vtt'], 'text/wgsl' => ['wgsl'], 'text/x-asm' => ['s', 'asm'], 'text/x-c' => ['c', 'cc', 'cxx', 'cpp', 'h', 'hh', 'dic'], 'text/x-component' => ['htc'], 'text/x-fortran' => ['f', 'for', 'f77', 'f90'], 'text/x-handlebars-template' => ['hbs'], 'text/x-java-source' => ['java'], 'text/x-lua' => ['lua'], 'text/x-markdown' => ['mkd'], 'text/x-nfo' => ['nfo'], 'text/x-opml' => ['opml'], 'text/x-org' => ['org'], 'text/x-pascal' => ['p', 'pas'], 'text/x-processing' => ['pde'], 'text/x-sass' => ['sass'], 'text/x-scss' => ['scss'], 'text/x-setext' => ['etx'], 'text/x-sfv' => ['sfv'], 'text/x-suse-ymp' => ['ymp'], 'text/x-uuencode' => ['uu'], 'text/x-vcalendar' => ['vcs'], 'text/x-vcard' => ['vcf'], 'text/xml' => ['xml'], 'text/yaml' => ['yaml', 'yml'], 'video/3gpp' => ['3gp', '3gpp'], 'video/3gpp2' => ['3g2'], 'video/h261' => ['h261'], 'video/h263' => ['h263'], 'video/h264' => ['h264'], 'video/iso.segment' => ['m4s'], 'video/jpeg' => ['jpgv'], 'video/jpm' => ['jpm', 'jpgm'], 'video/mj2' => ['mj2', 'mjp2'], 'video/mp2t' => ['ts'], 'video/mp4' => ['mp4', 'mp4v', 'mpg4', 'f4v'], 'video/mpeg' => ['mpeg', 'mpg', 'mpe', 'm1v', 'm2v'], 'video/ogg' => ['ogv'], 'video/quicktime' => ['qt', 'mov'], 'video/vnd.dece.hd' => ['uvh', 'uvvh'], 'video/vnd.dece.mobile' => ['uvm', 'uvvm'], 'video/vnd.dece.pd' => ['uvp', 'uvvp'], 'video/vnd.dece.sd' => ['uvs', 'uvvs'], 'video/vnd.dece.video' => ['uvv', 'uvvv'], 'video/vnd.dvb.file' => ['dvb'], 'video/vnd.fvt' => ['fvt'], 'video/vnd.mpegurl' => ['mxu', 'm4u'], 'video/vnd.ms-playready.media.pyv' => ['pyv'], 'video/vnd.uvvu.mp4' => ['uvu', 'uvvu'], 'video/vnd.vivo' => ['viv'], 'video/webm' => ['webm'], 'video/x-f4v' => ['f4v'], 'video/x-fli' => ['fli'], 'video/x-flv' => ['flv'], 'video/x-m4v' => ['m4v'], 'video/x-matroska' => ['mkv', 'mk3d', 'mks'], 'video/x-mng' => ['mng'], 'video/x-ms-asf' => ['asf', 'asx'], 'video/x-ms-vob' => ['vob'], 'video/x-ms-wm' => ['wm'], 'video/x-ms-wmv' => ['wmv'], 'video/x-ms-wmx' => ['wmx'], 'video/x-ms-wvx' => ['wvx'], 'video/x-msvideo' => ['avi'], 'video/x-sgi-movie' => ['movie'], 'video/x-smv' => ['smv'], 'x-conference/x-cooltalk' => ['ice'], 'application/x-photoshop' => ['psd'], 'application/smil' => ['smi', 'smil'], 'application/powerpoint' => ['ppt'], 'application/vnd.ms-powerpoint.addin.macroEnabled.12' => ['ppam'], 'application/vnd.ms-powerpoint.presentation.macroEnabled.12' => ['pptm', 'potm'], 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12' => ['ppsm'], 'application/wbxml' => ['wbxml'], 'application/wmlc' => ['wmlc'], 'application/x-httpd-php-source' => ['phps'], 'application/x-compress' => ['z'], 'application/x-rar' => ['rar'], 'video/vnd.rn-realvideo' => ['rv'], 'application/vnd.ms-word.template.macroEnabled.12' => ['docm', 'dotm'], 'application/vnd.ms-excel.sheet.macroEnabled.12' => ['xlsm'], 'application/vnd.ms-excel.template.macroEnabled.12' => ['xltm'], 'application/vnd.ms-excel.addin.macroEnabled.12' => ['xlam'], 'application/vnd.ms-excel.sheet.binary.macroEnabled.12' => ['xlsb'], 'application/excel' => ['xl'], 'application/x-x509-user-cert' => ['pem'], 'application/x-pkcs10' => ['p10'], 'application/x-pkcs7-signature' => ['p7a'], 'application/pgp' => ['pgp'], 'application/gpg-keys' => ['gpg'], 'application/x-pkcs7' => ['rsa'], 'video/3gp' => ['3gp'], 'audio/acc' => ['aac'], 'application/vnd.mpegurl' => ['m4u'], 'application/videolan' => ['vlc'], 'audio/x-au' => ['au'], 'audio/ac3' => ['ac3'], 'text/x-scriptzsh' => ['zsh'], 'application/cdr' => ['cdr'], 'application/STEP' => ['step', 'stp'], 'application/x-ndjson' => ['ndjson'], 'application/braille' => ['brf'], ]; public function lookupMimeType(string $extension): ?string { return self::MIME_TYPES_FOR_EXTENSIONS[$extension] ?? null; } public function lookupExtension(string $mimetype): ?string { return self::EXTENSIONS_FOR_MIME_TIMES[$mimetype][0] ?? null; } /** * @return string[] */ public function lookupAllExtensions(string $mimetype): array { return self::EXTENSIONS_FOR_MIME_TIMES[$mimetype] ?? []; } } mime-type-detection/src/MimeTypeDetector.php 0000644 00000000665 15007532371 0015163 0 ustar 00 <?php declare(strict_types=1); namespace League\MimeTypeDetection; interface MimeTypeDetector { /** * @param string|resource $contents */ public function detectMimeType(string $path, $contents): ?string; public function detectMimeTypeFromBuffer(string $contents): ?string; public function detectMimeTypeFromPath(string $path): ?string; public function detectMimeTypeFromFile(string $path): ?string; } mime-type-detection/src/ExtensionToMimeTypeMap.php 0000644 00000000253 15007532371 0016320 0 ustar 00 <?php declare(strict_types=1); namespace League\MimeTypeDetection; interface ExtensionToMimeTypeMap { public function lookupMimeType(string $extension): ?string; } oauth1-client/.travis.yml 0000644 00000002551 15007532371 0011340 0 ustar 00 language: php matrix: include: - php: 7.1 dist: bionic env: COMPOSER_OPTS="" - php: 7.1 dist: bionic env: COMPOSER_OPTS="--prefer-lowest" - php: 7.2 dist: bionic env: COMPOSER_OPTS="" - php: 7.2 dist: bionic env: COMPOSER_OPTS="--prefer-lowest" - php: 7.3 dist: bionic env: COMPOSER_OPTS="" - php: 7.3 dist: bionic env: COMPOSER_OPTS="--prefer-lowest" - php: 7.4 dist: bionic env: COMPOSER_OPTS="" - php: 7.4 dist: bionic env: COMPOSER_OPTS="--prefer-lowest" - php: 8.0 dist: bionic env: COMPOSER_OPTS="" - php: 8.0 dist: bionic env: COMPOSER_OPTS="--prefer-lowest" - php: nightly dist: bionic env: COMPOSER_OPTS="--ignore-platform-reqs" - php: nightly dist: bionic env: COMPOSER_OPTS="--ignore-platform-reqs --prefer-lowest" allow_failures: - php: nightly env: COMPOSER_OPTS="--ignore-platform-reqs" - php: nightly env: COMPOSER_OPTS="--ignore-platform-reqs --prefer-lowest" install: - travis_retry composer update --prefer-dist $COMPOSER_OPTS script: - composer php-cs-fixer:lint - composer test:unit - composer analyze after_script: - wget https://scrutinizer-ci.com/ocular.phar - php ocular.phar code-coverage:upload --format=php-clover coverage.xml oauth1-client/.scrutinizer.yml 0000644 00000001617 15007532371 0012413 0 ustar 00 filter: excluded_paths: [tests/*] checks: php: code_rating: true remove_extra_empty_lines: true remove_php_closing_tag: true remove_trailing_whitespace: true fix_use_statements: remove_unused: true preserve_multiple: false preserve_blanklines: true order_alphabetically: true fix_php_opening_tag: true fix_linefeed: true fix_line_ending: true fix_identation_4spaces: true fix_doc_comments: true tools: external_code_coverage: timeout: 600 runs: 4 php_analyzer: true php_code_coverage: false php_code_sniffer: config: standard: PSR2 filter: paths: ['src'] php_loc: enabled: true excluded_dirs: [vendor, tests] php_cpd: enabled: true excluded_dirs: [vendor, tests] oauth1-client/phpunit.xml 0000644 00000001565 15007532371 0011444 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <phpunit backupGlobals="false" backupStaticAttributes="false" bootstrap="./phpunit.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" verbose="true" > <testsuites> <testsuite name="oauth1-client/tests"> <directory suffix="Test.php">./tests</directory> </testsuite> </testsuites> <filter> <whitelist> <directory suffix=".php">./src</directory> </whitelist> </filter> <logging> <log type="coverage-text" target="php://stdout" showUncoveredFiles="true"/> <log type="coverage-clover" target="coverage.xml" showUncoveredFiles="true"/> </logging> </phpunit> oauth1-client/.php_cs.dist 0000644 00000001736 15007532371 0011452 0 ustar 00 <?php $finder = PhpCsFixer\Finder::create()->in([__DIR__ . '/src', __DIR__ . '/tests']); return PhpCsFixer\Config::create() ->setRules([ '@PSR2' => true, 'array_syntax' => ['syntax' => 'short'], 'binary_operator_spaces' => true, 'blank_line_before_return' => true, 'cast_spaces' => true, 'concat_space' => ['spacing' => 'one'], 'no_singleline_whitespace_before_semicolons' => true, 'not_operator_with_space' => true, 'ordered_imports' => true, 'phpdoc_align' => true, 'phpdoc_indent' => true, 'phpdoc_no_access' => true, 'phpdoc_no_alias_tag' => true, 'phpdoc_no_package' => true, 'phpdoc_scalar' => true, 'phpdoc_separation' => true, 'phpdoc_summary' => true, 'phpdoc_to_comment' => true, 'phpdoc_trim' => true, 'single_blank_line_at_eof' => true, 'ternary_operator_spaces' => true, ]) ->setFinder($finder); oauth1-client/composer.json 0000644 00000003366 15007532371 0011756 0 ustar 00 { "name": "league/oauth1-client", "description": "OAuth 1.0 Client Library", "license": "MIT", "require": { "php": ">=7.1||>=8.0", "ext-json": "*", "ext-openssl": "*", "guzzlehttp/guzzle": "^6.0|^7.0", "guzzlehttp/psr7": "^1.7|^2.0" }, "require-dev": { "ext-simplexml": "*", "phpunit/phpunit": "^7.5||9.5", "mockery/mockery": "^1.3.3", "phpstan/phpstan": "^0.12.42", "friendsofphp/php-cs-fixer": "^2.17" }, "scripts": { "analyze": "vendor/bin/phpstan analyse -l 6 src/", "php-cs-fixer:lint": "PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer fix --verbose --dry-run", "php-cs-fixer:format": "PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer fix", "test:unit": "vendor/bin/phpunit --coverage-text --coverage-clover coverage.xml" }, "suggest": { "ext-simplexml": "For decoding XML-based responses." }, "keywords": [ "oauth", "oauth1", "authorization", "authentication", "idp", "identity", "sso", "single sign on", "bitbucket", "trello", "tumblr", "twitter" ], "authors": [ { "name": "Ben Corlett", "email": "bencorlett@me.com", "homepage": "http://www.webcomm.com.au", "role": "Developer" } ], "autoload": { "psr-4": { "League\\OAuth1\\Client\\": "src/" } }, "autoload-dev": { "psr-4": { "League\\OAuth1\\Client\\Tests\\": "tests/" } }, "extra": { "branch-alias": { "dev-master": "1.0-dev", "dev-develop": "2.0-dev" } } } oauth1-client/CONTRIBUTING.md 0000644 00000002260 15007532371 0011455 0 ustar 00 # Contributing Contributions are **welcome** and will be fully **credited**. We accept contributions via Pull Requests on [Github](https://github.com/thephpleague/oauth1-client). ## Pull Requests - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). - **Add tests!** - Your patch won't be accepted if it doesn't have tests. - **Document any change in behaviour** - Make sure the README and any other relevant documentation are kept up-to-date. - **Consider our release cycle** - We try to follow semver. Randomly breaking public APIs is not an option. - **Create topic branches** - Don't ask us to pull from your master branch. - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. ## Running Tests ``` bash $ phpunit ``` **Happy coding**! oauth1-client/LICENSE 0000644 00000002112 15007532371 0010225 0 ustar 00 The MIT License (MIT) Copyright (c) 2013 Ben Corlett <bencorlett@me.com> 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. oauth1-client/CHANGELOG.md 0000644 00000002106 15007532371 0011034 0 ustar 00 # Changelog ## v1.10.1 - Fix deprecation error (#147) ## v1.10.0 - Adds customizable application scope (setting the `x_auth_access_type` query parameter when fetching temporary credentials) on the Twitter provider - thanks to @Diegslapasteque ## v1.9.3 - Reverts bug in `v1.9.1` and will reintroduce `x_auth_access_type` to Twitter provider in `v1.10.0`. ## v1.9.2 - Adds `x_auth_access_type` to Twitter provider - thanks to @Diegslapasteque ## v1.9.1 - Remove deprecated Guzzle function call. ## v1.9.0 - Adds support for PHP 8.0. - Allows optional authorization URL parameters to be passed. ## v1.8.2 - Fixes an issue where the base string used to generate signatures did not account for non-standard ports. ## v1.8.1 - Reverts the public API changes introduced in v1.8.0 where language level type declarations and return types that were introduced caused inheritence to break. - Fixes a Composer warning with relation to autoloading test files. ## v1.8.0 - We allow installation with Guzzle 6 **or** Guzzle 7. - The minimum PHP version has been bumped from PHP 5.6 to 7.1. oauth1-client/rfc5849.txt 0000644 00000235622 15007532371 0011103 0 ustar 00 Internet Engineering Task Force (IETF) E. Hammer-Lahav, Ed. Request for Comments: 5849 April 2010 Category: Informational ISSN: 2070-1721 The OAuth 1.0 Protocol Abstract OAuth provides a method for clients to access server resources on behalf of a resource owner (such as a different client or an end- user). It also provides a process for end-users to authorize third- party access to their server resources without sharing their credentials (typically, a username and password pair), using user- agent redirections. Status of This Memo This document is not an Internet Standards Track specification; it is published for informational purposes. This document is a product of the Internet Engineering Task Force (IETF). It represents the consensus of the IETF community. It has received public review and has been approved for publication by the Internet Engineering Steering Group (IESG). Not all documents approved by the IESG are a candidate for any level of Internet Standard; see Section 2 of RFC 5741. Information about the current status of this document, any errata, and how to provide feedback on it may be obtained at http://www.rfc-editor.org/info/rfc5849. Copyright Notice Copyright (c) 2010 IETF Trust and the persons identified as the document authors. All rights reserved. This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (http://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Simplified BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Simplified BSD License. Hammer-Lahav Informational [Page 1] RFC 5849 OAuth 1.0 April 2010 Table of Contents 1. Introduction ....................................................3 1.1. Terminology ................................................4 1.2. Example ....................................................5 1.3. Notational Conventions .....................................7 2. Redirection-Based Authorization .................................8 2.1. Temporary Credentials ......................................9 2.2. Resource Owner Authorization ..............................10 2.3. Token Credentials .........................................12 3. Authenticated Requests .........................................14 3.1. Making Requests ...........................................14 3.2. Verifying Requests ........................................16 3.3. Nonce and Timestamp .......................................17 3.4. Signature .................................................18 3.4.1. Signature Base String ..............................18 3.4.2. HMAC-SHA1 ..........................................25 3.4.3. RSA-SHA1 ...........................................25 3.4.4. PLAINTEXT ..........................................26 3.5. Parameter Transmission ....................................26 3.5.1. Authorization Header ...............................27 3.5.2. Form-Encoded Body ..................................28 3.5.3. Request URI Query ..................................28 3.6. Percent Encoding ..........................................29 4. Security Considerations ........................................29 4.1. RSA-SHA1 Signature Method .................................29 4.2. Confidentiality of Requests ...............................30 4.3. Spoofing by Counterfeit Servers ...........................30 4.4. Proxying and Caching of Authenticated Content .............30 4.5. Plaintext Storage of Credentials ..........................30 4.6. Secrecy of the Client Credentials .........................31 4.7. Phishing Attacks ..........................................31 4.8. Scoping of Access Requests ................................31 4.9. Entropy of Secrets ........................................32 4.10. Denial-of-Service / Resource-Exhaustion Attacks ..........32 4.11. SHA-1 Cryptographic Attacks ..............................33 4.12. Signature Base String Limitations ........................33 4.13. Cross-Site Request Forgery (CSRF) ........................33 4.14. User Interface Redress ...................................34 4.15. Automatic Processing of Repeat Authorizations ............34 5. Acknowledgments ................................................35 Appendix A. Differences from the Community Edition ...............36 6. References .....................................................37 6.1. Normative References ......................................37 6.2. Informative References ....................................38 Hammer-Lahav Informational [Page 2] RFC 5849 OAuth 1.0 April 2010 1. Introduction The OAuth protocol was originally created by a small community of web developers from a variety of websites and other Internet services who wanted to solve the common problem of enabling delegated access to protected resources. The resulting OAuth protocol was stabilized at version 1.0 in October 2007, and revised in June 2009 (Revision A) as published at <http://oauth.net/core/1.0a>. This specification provides an informational documentation of OAuth Core 1.0 Revision A, addresses several errata reported since that time, and makes numerous editorial clarifications. While this specification is not an item of the IETF's OAuth Working Group, which at the time of writing is working on an OAuth version that can be appropriate for publication on the standards track, it has been transferred to the IETF for change control by authors of the original work. In the traditional client-server authentication model, the client uses its credentials to access its resources hosted by the server. With the increasing use of distributed web services and cloud computing, third-party applications require access to these server- hosted resources. OAuth introduces a third role to the traditional client-server authentication model: the resource owner. In the OAuth model, the client (which is not the resource owner, but is acting on its behalf) requests access to resources controlled by the resource owner, but hosted by the server. In addition, OAuth allows the server to verify not only the resource owner authorization, but also the identity of the client making the request. OAuth provides a method for clients to access server resources on behalf of a resource owner (such as a different client or an end- user). It also provides a process for end-users to authorize third- party access to their server resources without sharing their credentials (typically, a username and password pair), using user- agent redirections. For example, a web user (resource owner) can grant a printing service (client) access to her private photos stored at a photo sharing service (server), without sharing her username and password with the printing service. Instead, she authenticates directly with the photo sharing service which issues the printing service delegation-specific credentials. Hammer-Lahav Informational [Page 3] RFC 5849 OAuth 1.0 April 2010 In order for the client to access resources, it first has to obtain permission from the resource owner. This permission is expressed in the form of a token and matching shared-secret. The purpose of the token is to make it unnecessary for the resource owner to share its credentials with the client. Unlike the resource owner credentials, tokens can be issued with a restricted scope and limited lifetime, and revoked independently. This specification consists of two parts. The first part defines a redirection-based user-agent process for end-users to authorize client access to their resources, by authenticating directly with the server and provisioning tokens to the client for use with the authentication method. The second part defines a method for making authenticated HTTP [RFC2616] requests using two sets of credentials, one identifying the client making the request, and a second identifying the resource owner on whose behalf the request is being made. The use of OAuth with any transport protocol other than [RFC2616] is undefined. 1.1. Terminology client An HTTP client (per [RFC2616]) capable of making OAuth- authenticated requests (Section 3). server An HTTP server (per [RFC2616]) capable of accepting OAuth- authenticated requests (Section 3). protected resource An access-restricted resource that can be obtained from the server using an OAuth-authenticated request (Section 3). resource owner An entity capable of accessing and controlling protected resources by using credentials to authenticate with the server. credentials Credentials are a pair of a unique identifier and a matching shared secret. OAuth defines three classes of credentials: client, temporary, and token, used to identify and authenticate the client making the request, the authorization request, and the access grant, respectively. Hammer-Lahav Informational [Page 4] RFC 5849 OAuth 1.0 April 2010 token A unique identifier issued by the server and used by the client to associate authenticated requests with the resource owner whose authorization is requested or has been obtained by the client. Tokens have a matching shared-secret that is used by the client to establish its ownership of the token, and its authority to represent the resource owner. The original community specification used a somewhat different terminology that maps to this specifications as follows (original community terms provided on left): Consumer: client Service Provider: server User: resource owner Consumer Key and Secret: client credentials Request Token and Secret: temporary credentials Access Token and Secret: token credentials 1.2. Example Jane (resource owner) has recently uploaded some private vacation photos (protected resources) to her photo sharing site 'photos.example.net' (server). She would like to use the 'printer.example.com' website (client) to print one of these photos. Typically, Jane signs into 'photos.example.net' using her username and password. However, Jane does not wish to share her username and password with the 'printer.example.com' website, which needs to access the photo in order to print it. In order to provide its users with better service, 'printer.example.com' has signed up for a set of 'photos.example.net' client credentials ahead of time: Client Identifier dpf43f3p2l4k3l03 Client Shared-Secret: kd94hf93k423kf44 The 'printer.example.com' website has also configured its application to use the protocol endpoints listed in the 'photos.example.net' API documentation, which use the "HMAC-SHA1" signature method: Hammer-Lahav Informational [Page 5] RFC 5849 OAuth 1.0 April 2010 Temporary Credential Request https://photos.example.net/initiate Resource Owner Authorization URI: https://photos.example.net/authorize Token Request URI: https://photos.example.net/token Before 'printer.example.com' can ask Jane to grant it access to the photos, it must first establish a set of temporary credentials with 'photos.example.net' to identify the delegation request. To do so, the client sends the following HTTPS [RFC2818] request to the server: POST /initiate HTTP/1.1 Host: photos.example.net Authorization: OAuth realm="Photos", oauth_consumer_key="dpf43f3p2l4k3l03", oauth_signature_method="HMAC-SHA1", oauth_timestamp="137131200", oauth_nonce="wIjqoS", oauth_callback="http%3A%2F%2Fprinter.example.com%2Fready", oauth_signature="74KNZJeDHnMBp0EMJ9ZHt%2FXKycU%3D" The server validates the request and replies with a set of temporary credentials in the body of the HTTP response (line breaks are for display purposes only): HTTP/1.1 200 OK Content-Type: application/x-www-form-urlencoded oauth_token=hh5s93j4hdidpola&oauth_token_secret=hdhd0244k9j7ao03& oauth_callback_confirmed=true The client redirects Jane's user-agent to the server's Resource Owner Authorization endpoint to obtain Jane's approval for accessing her private photos: https://photos.example.net/authorize?oauth_token=hh5s93j4hdidpola The server requests Jane to sign in using her username and password and if successful, asks her to approve granting 'printer.example.com' access to her private photos. Jane approves the request and her user-agent is redirected to the callback URI provided by the client in the previous request (line breaks are for display purposes only): http://printer.example.com/ready? oauth_token=hh5s93j4hdidpola&oauth_verifier=hfdp7dh39dks9884 Hammer-Lahav Informational [Page 6] RFC 5849 OAuth 1.0 April 2010 The callback request informs the client that Jane completed the authorization process. The client then requests a set of token credentials using its temporary credentials (over a secure Transport Layer Security (TLS) channel): POST /token HTTP/1.1 Host: photos.example.net Authorization: OAuth realm="Photos", oauth_consumer_key="dpf43f3p2l4k3l03", oauth_token="hh5s93j4hdidpola", oauth_signature_method="HMAC-SHA1", oauth_timestamp="137131201", oauth_nonce="walatlh", oauth_verifier="hfdp7dh39dks9884", oauth_signature="gKgrFCywp7rO0OXSjdot%2FIHF7IU%3D" The server validates the request and replies with a set of token credentials in the body of the HTTP response: HTTP/1.1 200 OK Content-Type: application/x-www-form-urlencoded oauth_token=nnch734d00sl2jdk&oauth_token_secret=pfkkdhi9sl3r4s00 With a set of token credentials, the client is now ready to request the private photo: GET /photos?file=vacation.jpg&size=original HTTP/1.1 Host: photos.example.net Authorization: OAuth realm="Photos", oauth_consumer_key="dpf43f3p2l4k3l03", oauth_token="nnch734d00sl2jdk", oauth_signature_method="HMAC-SHA1", oauth_timestamp="137131202", oauth_nonce="chapoH", oauth_signature="MdpQcU8iPSUjWoN%2FUDMsK2sui9I%3D" The 'photos.example.net' server validates the request and responds with the requested photo. 'printer.example.com' is able to continue accessing Jane's private photos using the same set of token credentials for the duration of Jane's authorization, or until Jane revokes access. 1.3. Notational Conventions The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC2119]. Hammer-Lahav Informational [Page 7] RFC 5849 OAuth 1.0 April 2010 2. Redirection-Based Authorization OAuth uses tokens to represent the authorization granted to the client by the resource owner. Typically, token credentials are issued by the server at the resource owner's request, after authenticating the resource owner's identity (usually using a username and password). There are many ways in which a server can facilitate the provisioning of token credentials. This section defines one such way, using HTTP redirections and the resource owner's user-agent. This redirection- based authorization method includes three steps: 1. The client obtains a set of temporary credentials from the server (in the form of an identifier and shared-secret). The temporary credentials are used to identify the access request throughout the authorization process. 2. The resource owner authorizes the server to grant the client's access request (identified by the temporary credentials). 3. The client uses the temporary credentials to request a set of token credentials from the server, which will enable it to access the resource owner's protected resources. The server MUST revoke the temporary credentials after being used once to obtain the token credentials. It is RECOMMENDED that the temporary credentials have a limited lifetime. Servers SHOULD enable resource owners to revoke token credentials after they have been issued to clients. In order for the client to perform these steps, the server needs to advertise the URIs of the following three endpoints: Temporary Credential Request The endpoint used by the client to obtain a set of temporary credentials as described in Section 2.1. Resource Owner Authorization The endpoint to which the resource owner is redirected to grant authorization as described in Section 2.2. Token Request The endpoint used by the client to request a set of token credentials using the set of temporary credentials as described in Section 2.3. Hammer-Lahav Informational [Page 8] RFC 5849 OAuth 1.0 April 2010 The three URIs advertised by the server MAY include a query component as defined by [RFC3986], Section 3, but if present, the query MUST NOT contain any parameters beginning with the "oauth_" prefix, to avoid conflicts with the protocol parameters added to the URIs when used. The methods in which the server advertises and documents its three endpoints are beyond the scope of this specification. Clients should avoid making assumptions about the size of tokens and other server- generated values, which are left undefined by this specification. In addition, protocol parameters MAY include values that require encoding when transmitted. Clients and servers should not make assumptions about the possible range of their values. 2.1. Temporary Credentials The client obtains a set of temporary credentials from the server by making an authenticated (Section 3) HTTP "POST" request to the Temporary Credential Request endpoint (unless the server advertises another HTTP request method for the client to use). The client constructs a request URI by adding the following REQUIRED parameter to the request (in addition to the other protocol parameters, using the same parameter transmission method): oauth_callback: An absolute URI back to which the server will redirect the resource owner when the Resource Owner Authorization step (Section 2.2) is completed. If the client is unable to receive callbacks or a callback URI has been established via other means, the parameter value MUST be set to "oob" (case sensitive), to indicate an out-of-band configuration. Servers MAY specify additional parameters. When making the request, the client authenticates using only the client credentials. The client MAY omit the empty "oauth_token" protocol parameter from the request and MUST use the empty string as the token secret value. Since the request results in the transmission of plain text credentials in the HTTP response, the server MUST require the use of a transport-layer mechanisms such as TLS or Secure Socket Layer (SSL) (or a secure channel with equivalent protections). Hammer-Lahav Informational [Page 9] RFC 5849 OAuth 1.0 April 2010 For example, the client makes the following HTTPS request: POST /request_temp_credentials HTTP/1.1 Host: server.example.com Authorization: OAuth realm="Example", oauth_consumer_key="jd83jd92dhsh93js", oauth_signature_method="PLAINTEXT", oauth_callback="http%3A%2F%2Fclient.example.net%2Fcb%3Fx%3D1", oauth_signature="ja893SD9%26" The server MUST verify (Section 3.2) the request and if valid, respond back to the client with a set of temporary credentials (in the form of an identifier and shared-secret). The temporary credentials are included in the HTTP response body using the "application/x-www-form-urlencoded" content type as defined by [W3C.REC-html40-19980424] with a 200 status code (OK). The response contains the following REQUIRED parameters: oauth_token The temporary credentials identifier. oauth_token_secret The temporary credentials shared-secret. oauth_callback_confirmed MUST be present and set to "true". The parameter is used to differentiate from previous versions of the protocol. Note that even though the parameter names include the term 'token', these credentials are not token credentials, but are used in the next two steps in a similar manner to token credentials. For example (line breaks are for display purposes only): HTTP/1.1 200 OK Content-Type: application/x-www-form-urlencoded oauth_token=hdk48Djdsa&oauth_token_secret=xyz4992k83j47x0b& oauth_callback_confirmed=true 2.2. Resource Owner Authorization Before the client requests a set of token credentials from the server, it MUST send the user to the server to authorize the request. The client constructs a request URI by adding the following REQUIRED query parameter to the Resource Owner Authorization endpoint URI: Hammer-Lahav Informational [Page 10] RFC 5849 OAuth 1.0 April 2010 oauth_token The temporary credentials identifier obtained in Section 2.1 in the "oauth_token" parameter. Servers MAY declare this parameter as OPTIONAL, in which case they MUST provide a way for the resource owner to indicate the identifier through other means. Servers MAY specify additional parameters. The client directs the resource owner to the constructed URI using an HTTP redirection response, or by other means available to it via the resource owner's user-agent. The request MUST use the HTTP "GET" method. For example, the client redirects the resource owner's user-agent to make the following HTTPS request: GET /authorize_access?oauth_token=hdk48Djdsa HTTP/1.1 Host: server.example.com The way in which the server handles the authorization request, including whether it uses a secure channel such as TLS/SSL is beyond the scope of this specification. However, the server MUST first verify the identity of the resource owner. When asking the resource owner to authorize the requested access, the server SHOULD present to the resource owner information about the client requesting access based on the association of the temporary credentials with the client identity. When displaying any such information, the server SHOULD indicate if the information has been verified. After receiving an authorization decision from the resource owner, the server redirects the resource owner to the callback URI if one was provided in the "oauth_callback" parameter or by other means. To make sure that the resource owner granting access is the same resource owner returning back to the client to complete the process, the server MUST generate a verification code: an unguessable value passed to the client via the resource owner and REQUIRED to complete the process. The server constructs the request URI by adding the following REQUIRED parameters to the callback URI query component: oauth_token The temporary credentials identifier received from the client. Hammer-Lahav Informational [Page 11] RFC 5849 OAuth 1.0 April 2010 oauth_verifier The verification code. If the callback URI already includes a query component, the server MUST append the OAuth parameters to the end of the existing query. For example, the server redirects the resource owner's user-agent to make the following HTTP request: GET /cb?x=1&oauth_token=hdk48Djdsa&oauth_verifier=473f82d3 HTTP/1.1 Host: client.example.net If the client did not provide a callback URI, the server SHOULD display the value of the verification code, and instruct the resource owner to manually inform the client that authorization is completed. If the server knows a client to be running on a limited device, it SHOULD ensure that the verifier value is suitable for manual entry. 2.3. Token Credentials The client obtains a set of token credentials from the server by making an authenticated (Section 3) HTTP "POST" request to the Token Request endpoint (unless the server advertises another HTTP request method for the client to use). The client constructs a request URI by adding the following REQUIRED parameter to the request (in addition to the other protocol parameters, using the same parameter transmission method): oauth_verifier The verification code received from the server in the previous step. When making the request, the client authenticates using the client credentials as well as the temporary credentials. The temporary credentials are used as a substitute for token credentials in the authenticated request and transmitted using the "oauth_token" parameter. Since the request results in the transmission of plain text credentials in the HTTP response, the server MUST require the use of a transport-layer mechanism such as TLS or SSL (or a secure channel with equivalent protections). Hammer-Lahav Informational [Page 12] RFC 5849 OAuth 1.0 April 2010 For example, the client makes the following HTTPS request: POST /request_token HTTP/1.1 Host: server.example.com Authorization: OAuth realm="Example", oauth_consumer_key="jd83jd92dhsh93js", oauth_token="hdk48Djdsa", oauth_signature_method="PLAINTEXT", oauth_verifier="473f82d3", oauth_signature="ja893SD9%26xyz4992k83j47x0b" The server MUST verify (Section 3.2) the validity of the request, ensure that the resource owner has authorized the provisioning of token credentials to the client, and ensure that the temporary credentials have not expired or been used before. The server MUST also verify the verification code received from the client. If the request is valid and authorized, the token credentials are included in the HTTP response body using the "application/x-www-form-urlencoded" content type as defined by [W3C.REC-html40-19980424] with a 200 status code (OK). The response contains the following REQUIRED parameters: oauth_token The token identifier. oauth_token_secret The token shared-secret. For example: HTTP/1.1 200 OK Content-Type: application/x-www-form-urlencoded oauth_token=j49ddk933skd9dks&oauth_token_secret=ll399dj47dskfjdk The server must retain the scope, duration, and other attributes approved by the resource owner, and enforce these restrictions when receiving a client request made with the token credentials issued. Once the client receives and stores the token credentials, it can proceed to access protected resources on behalf of the resource owner by making authenticated requests (Section 3) using the client credentials together with the token credentials received. Hammer-Lahav Informational [Page 13] RFC 5849 OAuth 1.0 April 2010 3. Authenticated Requests The HTTP authentication methods defined by [RFC2617] enable clients to make authenticated HTTP requests. Clients using these methods gain access to protected resources by using their credentials (typically, a username and password pair), which allow the server to verify their authenticity. Using these methods for delegation requires the client to assume the role of the resource owner. OAuth provides a method designed to include two sets of credentials with each request, one to identify the client, and another to identify the resource owner. Before a client can make authenticated requests on behalf of the resource owner, it must obtain a token authorized by the resource owner. Section 2 provides one such method through which the client can obtain a token authorized by the resource owner. The client credentials take the form of a unique identifier and an associated shared-secret or RSA key pair. Prior to making authenticated requests, the client establishes a set of credentials with the server. The process and requirements for provisioning these are outside the scope of this specification. Implementers are urged to consider the security ramifications of using client credentials, some of which are described in Section 4.6. Making authenticated requests requires prior knowledge of the server's configuration. OAuth includes multiple methods for transmitting protocol parameters with requests (Section 3.5), as well as multiple methods for the client to prove its rightful ownership of the credentials used (Section 3.4). The way in which clients discover the required configuration is outside the scope of this specification. 3.1. Making Requests An authenticated request includes several protocol parameters. Each parameter name begins with the "oauth_" prefix, and the parameter names and values are case sensitive. Clients make authenticated requests by calculating the values of a set of protocol parameters and adding them to the HTTP request as follows: 1. The client assigns value to each of these REQUIRED (unless specified otherwise) protocol parameters: Hammer-Lahav Informational [Page 14] RFC 5849 OAuth 1.0 April 2010 oauth_consumer_key The identifier portion of the client credentials (equivalent to a username). The parameter name reflects a deprecated term (Consumer Key) used in previous revisions of the specification, and has been retained to maintain backward compatibility. oauth_token The token value used to associate the request with the resource owner. If the request is not associated with a resource owner (no token available), clients MAY omit the parameter. oauth_signature_method The name of the signature method used by the client to sign the request, as defined in Section 3.4. oauth_timestamp The timestamp value as defined in Section 3.3. The parameter MAY be omitted when using the "PLAINTEXT" signature method. oauth_nonce The nonce value as defined in Section 3.3. The parameter MAY be omitted when using the "PLAINTEXT" signature method. oauth_version OPTIONAL. If present, MUST be set to "1.0". Provides the version of the authentication process as defined in this specification. 2. The protocol parameters are added to the request using one of the transmission methods listed in Section 3.5. Each parameter MUST NOT appear more than once per request. 3. The client calculates and assigns the value of the "oauth_signature" parameter as described in Section 3.4 and adds the parameter to the request using the same method as in the previous step. 4. The client sends the authenticated HTTP request to the server. For example, to make the following HTTP request authenticated (the "c2&a3=2+q" string in the following examples is used to illustrate the impact of a form-encoded entity-body): POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded c2&a3=2+q Hammer-Lahav Informational [Page 15] RFC 5849 OAuth 1.0 April 2010 The client assigns values to the following protocol parameters using its client credentials, token credentials, the current timestamp, a uniquely generated nonce, and indicates that it will use the "HMAC-SHA1" signature method: oauth_consumer_key: 9djdj82h48djs9d2 oauth_token: kkk9d7dh3k39sjv7 oauth_signature_method: HMAC-SHA1 oauth_timestamp: 137131201 oauth_nonce: 7d8f3e4a The client adds the protocol parameters to the request using the OAuth HTTP "Authorization" header field: Authorization: OAuth realm="Example", oauth_consumer_key="9djdj82h48djs9d2", oauth_token="kkk9d7dh3k39sjv7", oauth_signature_method="HMAC-SHA1", oauth_timestamp="137131201", oauth_nonce="7d8f3e4a" Then, it calculates the value of the "oauth_signature" parameter (using client secret "j49sk3j29djd" and token secret "dh893hdasih9"), adds it to the request, and sends the HTTP request to the server: POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded Authorization: OAuth realm="Example", oauth_consumer_key="9djdj82h48djs9d2", oauth_token="kkk9d7dh3k39sjv7", oauth_signature_method="HMAC-SHA1", oauth_timestamp="137131201", oauth_nonce="7d8f3e4a", oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D" c2&a3=2+q 3.2. Verifying Requests Servers receiving an authenticated request MUST validate it by: o Recalculating the request signature independently as described in Section 3.4 and comparing it to the value received from the client via the "oauth_signature" parameter. Hammer-Lahav Informational [Page 16] RFC 5849 OAuth 1.0 April 2010 o If using the "HMAC-SHA1" or "RSA-SHA1" signature methods, ensuring that the combination of nonce/timestamp/token (if present) received from the client has not been used before in a previous request (the server MAY reject requests with stale timestamps as described in Section 3.3). o If a token is present, verifying the scope and status of the client authorization as represented by the token (the server MAY choose to restrict token usage to the client to which it was issued). o If the "oauth_version" parameter is present, ensuring its value is "1.0". If the request fails verification, the server SHOULD respond with the appropriate HTTP response status code. The server MAY include further details about why the request was rejected in the response body. The server SHOULD return a 400 (Bad Request) status code when receiving a request with unsupported parameters, an unsupported signature method, missing parameters, or duplicated protocol parameters. The server SHOULD return a 401 (Unauthorized) status code when receiving a request with invalid client credentials, an invalid or expired token, an invalid signature, or an invalid or used nonce. 3.3. Nonce and Timestamp The timestamp value MUST be a positive integer. Unless otherwise specified by the server's documentation, the timestamp is expressed in the number of seconds since January 1, 1970 00:00:00 GMT. A nonce is a random string, uniquely generated by the client to allow the server to verify that a request has never been made before and helps prevent replay attacks when requests are made over a non-secure channel. The nonce value MUST be unique across all requests with the same timestamp, client credentials, and token combinations. To avoid the need to retain an infinite number of nonce values for future checks, servers MAY choose to restrict the time period after which a request with an old timestamp is rejected. Note that this restriction implies a level of synchronization between the client's and server's clocks. Servers applying such a restriction MAY provide a way for the client to sync with the server's clock; alternatively, both systems could synchronize with a trusted time service. Details of clock synchronization strategies are beyond the scope of this specification. Hammer-Lahav Informational [Page 17] RFC 5849 OAuth 1.0 April 2010 3.4. Signature OAuth-authenticated requests can have two sets of credentials: those passed via the "oauth_consumer_key" parameter and those in the "oauth_token" parameter. In order for the server to verify the authenticity of the request and prevent unauthorized access, the client needs to prove that it is the rightful owner of the credentials. This is accomplished using the shared-secret (or RSA key) part of each set of credentials. OAuth provides three methods for the client to prove its rightful ownership of the credentials: "HMAC-SHA1", "RSA-SHA1", and "PLAINTEXT". These methods are generally referred to as signature methods, even though "PLAINTEXT" does not involve a signature. In addition, "RSA-SHA1" utilizes an RSA key instead of the shared- secrets associated with the client credentials. OAuth does not mandate a particular signature method, as each implementation can have its own unique requirements. Servers are free to implement and document their own custom methods. Recommending any particular method is beyond the scope of this specification. Implementers should review the Security Considerations section (Section 4) before deciding on which method to support. The client declares which signature method is used via the "oauth_signature_method" parameter. It then generates a signature (or a string of an equivalent value) and includes it in the "oauth_signature" parameter. The server verifies the signature as specified for each method. The signature process does not change the request or its parameters, with the exception of the "oauth_signature" parameter. 3.4.1. Signature Base String The signature base string is a consistent, reproducible concatenation of several of the HTTP request elements into a single string. The string is used as an input to the "HMAC-SHA1" and "RSA-SHA1" signature methods. The signature base string includes the following components of the HTTP request: o The HTTP request method (e.g., "GET", "POST", etc.). o The authority as declared by the HTTP "Host" request header field. Hammer-Lahav Informational [Page 18] RFC 5849 OAuth 1.0 April 2010 o The path and query components of the request resource URI. o The protocol parameters excluding the "oauth_signature". o Parameters included in the request entity-body if they comply with the strict restrictions defined in Section 3.4.1.3. The signature base string does not cover the entire HTTP request. Most notably, it does not include the entity-body in most requests, nor does it include most HTTP entity-headers. It is important to note that the server cannot verify the authenticity of the excluded request components without using additional protections such as SSL/ TLS or other methods. 3.4.1.1. String Construction The signature base string is constructed by concatenating together, in order, the following HTTP request elements: 1. The HTTP request method in uppercase. For example: "HEAD", "GET", "POST", etc. If the request uses a custom HTTP method, it MUST be encoded (Section 3.6). 2. An "&" character (ASCII code 38). 3. The base string URI from Section 3.4.1.2, after being encoded (Section 3.6). 4. An "&" character (ASCII code 38). 5. The request parameters as normalized in Section 3.4.1.3.2, after being encoded (Section 3.6). For example, the HTTP request: POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded Authorization: OAuth realm="Example", oauth_consumer_key="9djdj82h48djs9d2", oauth_token="kkk9d7dh3k39sjv7", oauth_signature_method="HMAC-SHA1", oauth_timestamp="137131201", oauth_nonce="7d8f3e4a", oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D" c2&a3=2+q Hammer-Lahav Informational [Page 19] RFC 5849 OAuth 1.0 April 2010 is represented by the following signature base string (line breaks are for display purposes only): POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q %26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_ key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk 9d7dh3k39sjv7 3.4.1.2. Base String URI The scheme, authority, and path of the request resource URI [RFC3986] are included by constructing an "http" or "https" URI representing the request resource (without the query or fragment) as follows: 1. The scheme and host MUST be in lowercase. 2. The host and port values MUST match the content of the HTTP request "Host" header field. 3. The port MUST be included if it is not the default port for the scheme, and MUST be excluded if it is the default. Specifically, the port MUST be excluded when making an HTTP request [RFC2616] to port 80 or when making an HTTPS request [RFC2818] to port 443. All other non-default port numbers MUST be included. For example, the HTTP request: GET /r%20v/X?id=123 HTTP/1.1 Host: EXAMPLE.COM:80 is represented by the base string URI: "http://example.com/r%20v/X". In another example, the HTTPS request: GET /?q=1 HTTP/1.1 Host: www.example.net:8080 is represented by the base string URI: "https://www.example.net:8080/". 3.4.1.3. Request Parameters In order to guarantee a consistent and reproducible representation of the request parameters, the parameters are collected and decoded to their original decoded form. They are then sorted and encoded in a particular manner that is often different from their original encoding scheme, and concatenated into a single string. Hammer-Lahav Informational [Page 20] RFC 5849 OAuth 1.0 April 2010 3.4.1.3.1. Parameter Sources The parameters from the following sources are collected into a single list of name/value pairs: o The query component of the HTTP request URI as defined by [RFC3986], Section 3.4. The query component is parsed into a list of name/value pairs by treating it as an "application/x-www-form-urlencoded" string, separating the names and values and decoding them as defined by [W3C.REC-html40-19980424], Section 17.13.4. o The OAuth HTTP "Authorization" header field (Section 3.5.1) if present. The header's content is parsed into a list of name/value pairs excluding the "realm" parameter if present. The parameter values are decoded as defined by Section 3.5.1. o The HTTP request entity-body, but only if all of the following conditions are met: * The entity-body is single-part. * The entity-body follows the encoding requirements of the "application/x-www-form-urlencoded" content-type as defined by [W3C.REC-html40-19980424]. * The HTTP request entity-header includes the "Content-Type" header field set to "application/x-www-form-urlencoded". The entity-body is parsed into a list of decoded name/value pairs as described in [W3C.REC-html40-19980424], Section 17.13.4. The "oauth_signature" parameter MUST be excluded from the signature base string if present. Parameters not explicitly included in the request MUST be excluded from the signature base string (e.g., the "oauth_version" parameter when omitted). Hammer-Lahav Informational [Page 21] RFC 5849 OAuth 1.0 April 2010 For example, the HTTP request: POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded Authorization: OAuth realm="Example", oauth_consumer_key="9djdj82h48djs9d2", oauth_token="kkk9d7dh3k39sjv7", oauth_signature_method="HMAC-SHA1", oauth_timestamp="137131201", oauth_nonce="7d8f3e4a", oauth_signature="djosJKDKJSD8743243%2Fjdk33klY%3D" c2&a3=2+q contains the following (fully decoded) parameters used in the signature base sting: +------------------------+------------------+ | Name | Value | +------------------------+------------------+ | b5 | =%3D | | a3 | a | | c@ | | | a2 | r b | | oauth_consumer_key | 9djdj82h48djs9d2 | | oauth_token | kkk9d7dh3k39sjv7 | | oauth_signature_method | HMAC-SHA1 | | oauth_timestamp | 137131201 | | oauth_nonce | 7d8f3e4a | | c2 | | | a3 | 2 q | +------------------------+------------------+ Note that the value of "b5" is "=%3D" and not "==". Both "c@" and "c2" have empty values. While the encoding rules specified in this specification for the purpose of constructing the signature base string exclude the use of a "+" character (ASCII code 43) to represent an encoded space character (ASCII code 32), this practice is widely used in "application/x-www-form-urlencoded" encoded values, and MUST be properly decoded, as demonstrated by one of the "a3" parameter instances (the "a3" parameter is used twice in this request). Hammer-Lahav Informational [Page 22] RFC 5849 OAuth 1.0 April 2010 3.4.1.3.2. Parameters Normalization The parameters collected in Section 3.4.1.3 are normalized into a single string as follows: 1. First, the name and value of each parameter are encoded (Section 3.6). 2. The parameters are sorted by name, using ascending byte value ordering. If two or more parameters share the same name, they are sorted by their value. 3. The name of each parameter is concatenated to its corresponding value using an "=" character (ASCII code 61) as a separator, even if the value is empty. 4. The sorted name/value pairs are concatenated together into a single string by using an "&" character (ASCII code 38) as separator. For example, the list of parameters from the previous section would be normalized as follows: Encoded: +------------------------+------------------+ | Name | Value | +------------------------+------------------+ | b5 | %3D%253D | | a3 | a | | c%40 | | | a2 | r%20b | | oauth_consumer_key | 9djdj82h48djs9d2 | | oauth_token | kkk9d7dh3k39sjv7 | | oauth_signature_method | HMAC-SHA1 | | oauth_timestamp | 137131201 | | oauth_nonce | 7d8f3e4a | | c2 | | | a3 | 2%20q | +------------------------+------------------+ Hammer-Lahav Informational [Page 23] RFC 5849 OAuth 1.0 April 2010 Sorted: +------------------------+------------------+ | Name | Value | +------------------------+------------------+ | a2 | r%20b | | a3 | 2%20q | | a3 | a | | b5 | %3D%253D | | c%40 | | | c2 | | | oauth_consumer_key | 9djdj82h48djs9d2 | | oauth_nonce | 7d8f3e4a | | oauth_signature_method | HMAC-SHA1 | | oauth_timestamp | 137131201 | | oauth_token | kkk9d7dh3k39sjv7 | +------------------------+------------------+ Concatenated Pairs: +-------------------------------------+ | Name=Value | +-------------------------------------+ | a2=r%20b | | a3=2%20q | | a3=a | | b5=%3D%253D | | c%40= | | c2= | | oauth_consumer_key=9djdj82h48djs9d2 | | oauth_nonce=7d8f3e4a | | oauth_signature_method=HMAC-SHA1 | | oauth_timestamp=137131201 | | oauth_token=kkk9d7dh3k39sjv7 | +-------------------------------------+ and concatenated together into a single string (line breaks are for display purposes only): a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9dj dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1 &oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7 Hammer-Lahav Informational [Page 24] RFC 5849 OAuth 1.0 April 2010 3.4.2. HMAC-SHA1 The "HMAC-SHA1" signature method uses the HMAC-SHA1 signature algorithm as defined in [RFC2104]: digest = HMAC-SHA1 (key, text) The HMAC-SHA1 function variables are used in following way: text is set to the value of the signature base string from Section 3.4.1.1. key is set to the concatenated values of: 1. The client shared-secret, after being encoded (Section 3.6). 2. An "&" character (ASCII code 38), which MUST be included even when either secret is empty. 3. The token shared-secret, after being encoded (Section 3.6). digest is used to set the value of the "oauth_signature" protocol parameter, after the result octet string is base64-encoded per [RFC2045], Section 6.8. 3.4.3. RSA-SHA1 The "RSA-SHA1" signature method uses the RSASSA-PKCS1-v1_5 signature algorithm as defined in [RFC3447], Section 8.2 (also known as PKCS#1), using SHA-1 as the hash function for EMSA-PKCS1-v1_5. To use this method, the client MUST have established client credentials with the server that included its RSA public key (in a manner that is beyond the scope of this specification). The signature base string is signed using the client's RSA private key per [RFC3447], Section 8.2.1: S = RSASSA-PKCS1-V1_5-SIGN (K, M) Where: K is set to the client's RSA private key, M is set to the value of the signature base string from Section 3.4.1.1, and Hammer-Lahav Informational [Page 25] RFC 5849 OAuth 1.0 April 2010 S is the result signature used to set the value of the "oauth_signature" protocol parameter, after the result octet string is base64-encoded per [RFC2045] section 6.8. The server verifies the signature per [RFC3447] section 8.2.2: RSASSA-PKCS1-V1_5-VERIFY ((n, e), M, S) Where: (n, e) is set to the client's RSA public key, M is set to the value of the signature base string from Section 3.4.1.1, and S is set to the octet string value of the "oauth_signature" protocol parameter received from the client. 3.4.4. PLAINTEXT The "PLAINTEXT" method does not employ a signature algorithm. It MUST be used with a transport-layer mechanism such as TLS or SSL (or sent over a secure channel with equivalent protections). It does not utilize the signature base string or the "oauth_timestamp" and "oauth_nonce" parameters. The "oauth_signature" protocol parameter is set to the concatenated value of: 1. The client shared-secret, after being encoded (Section 3.6). 2. An "&" character (ASCII code 38), which MUST be included even when either secret is empty. 3. The token shared-secret, after being encoded (Section 3.6). 3.5. Parameter Transmission When making an OAuth-authenticated request, protocol parameters as well as any other parameter using the "oauth_" prefix SHALL be included in the request using one and only one of the following locations, listed in order of decreasing preference: 1. The HTTP "Authorization" header field as described in Section 3.5.1. 2. The HTTP request entity-body as described in Section 3.5.2. Hammer-Lahav Informational [Page 26] RFC 5849 OAuth 1.0 April 2010 3. The HTTP request URI query as described in Section 3.5.3. In addition to these three methods, future extensions MAY define other methods for including protocol parameters in the request. 3.5.1. Authorization Header Protocol parameters can be transmitted using the HTTP "Authorization" header field as defined by [RFC2617] with the auth-scheme name set to "OAuth" (case insensitive). For example: Authorization: OAuth realm="Example", oauth_consumer_key="0685bd9184jfhq22", oauth_token="ad180jjd733klru7", oauth_signature_method="HMAC-SHA1", oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D", oauth_timestamp="137131200", oauth_nonce="4572616e48616d6d65724c61686176", oauth_version="1.0" Protocol parameters SHALL be included in the "Authorization" header field as follows: 1. Parameter names and values are encoded per Parameter Encoding (Section 3.6). 2. Each parameter's name is immediately followed by an "=" character (ASCII code 61), a """ character (ASCII code 34), the parameter value (MAY be empty), and another """ character (ASCII code 34). 3. Parameters are separated by a "," character (ASCII code 44) and OPTIONAL linear whitespace per [RFC2617]. 4. The OPTIONAL "realm" parameter MAY be added and interpreted per [RFC2617] section 1.2. Servers MAY indicate their support for the "OAuth" auth-scheme by returning the HTTP "WWW-Authenticate" response header field upon client requests for protected resources. As per [RFC2617], such a response MAY include additional HTTP "WWW-Authenticate" header fields: For example: WWW-Authenticate: OAuth realm="http://server.example.com/" Hammer-Lahav Informational [Page 27] RFC 5849 OAuth 1.0 April 2010 The realm parameter defines a protection realm per [RFC2617], Section 1.2. 3.5.2. Form-Encoded Body Protocol parameters can be transmitted in the HTTP request entity- body, but only if the following REQUIRED conditions are met: o The entity-body is single-part. o The entity-body follows the encoding requirements of the "application/x-www-form-urlencoded" content-type as defined by [W3C.REC-html40-19980424]. o The HTTP request entity-header includes the "Content-Type" header field set to "application/x-www-form-urlencoded". For example (line breaks are for display purposes only): oauth_consumer_key=0685bd9184jfhq22&oauth_token=ad180jjd733klr u7&oauth_signature_method=HMAC-SHA1&oauth_signature=wOJIO9A2W5 mFwDgiDvZbTSMK%2FPY%3D&oauth_timestamp=137131200&oauth_nonce=4 572616e48616d6d65724c61686176&oauth_version=1.0 The entity-body MAY include other request-specific parameters, in which case, the protocol parameters SHOULD be appended following the request-specific parameters, properly separated by an "&" character (ASCII code 38). 3.5.3. Request URI Query Protocol parameters can be transmitted by being added to the HTTP request URI as a query parameter as defined by [RFC3986], Section 3. For example (line breaks are for display purposes only): GET /example/path?oauth_consumer_key=0685bd9184jfhq22& oauth_token=ad180jjd733klru7&oauth_signature_method=HM AC-SHA1&oauth_signature=wOJIO9A2W5mFwDgiDvZbTSMK%2FPY% 3D&oauth_timestamp=137131200&oauth_nonce=4572616e48616 d6d65724c61686176&oauth_version=1.0 HTTP/1.1 The request URI MAY include other request-specific query parameters, in which case, the protocol parameters SHOULD be appended following the request-specific parameters, properly separated by an "&" character (ASCII code 38). Hammer-Lahav Informational [Page 28] RFC 5849 OAuth 1.0 April 2010 3.6. Percent Encoding Existing percent-encoding methods do not guarantee a consistent construction of the signature base string. The following percent- encoding method is not defined to replace the existing encoding methods defined by [RFC3986] and [W3C.REC-html40-19980424]. It is used only in the construction of the signature base string and the "Authorization" header field. This specification defines the following method for percent-encoding strings: 1. Text values are first encoded as UTF-8 octets per [RFC3629] if they are not already. This does not include binary values that are not intended for human consumption. 2. The values are then escaped using the [RFC3986] percent-encoding (%XX) mechanism as follows: * Characters in the unreserved character set as defined by [RFC3986], Section 2.3 (ALPHA, DIGIT, "-", ".", "_", "~") MUST NOT be encoded. * All other characters MUST be encoded. * The two hexadecimal characters used to represent encoded characters MUST be uppercase. This method is different from the encoding scheme used by the "application/x-www-form-urlencoded" content-type (for example, it encodes space characters as "%20" and not using the "+" character). It MAY be different from the percent-encoding functions provided by web-development frameworks (e.g., encode different characters, use lowercase hexadecimal characters). 4. Security Considerations As stated in [RFC2617], the greatest sources of risks are usually found not in the core protocol itself but in policies and procedures surrounding its use. Implementers are strongly encouraged to assess how this protocol addresses their security requirements. 4.1. RSA-SHA1 Signature Method Authenticated requests made with "RSA-SHA1" signatures do not use the token shared-secret, or any provisioned client shared-secret. This means the request relies completely on the secrecy of the private key used by the client to sign requests. Hammer-Lahav Informational [Page 29] RFC 5849 OAuth 1.0 April 2010 4.2. Confidentiality of Requests While this protocol provides a mechanism for verifying the integrity of requests, it provides no guarantee of request confidentiality. Unless further precautions are taken, eavesdroppers will have full access to request content. Servers should carefully consider the kinds of data likely to be sent as part of such requests, and should employ transport-layer security mechanisms to protect sensitive resources. 4.3. Spoofing by Counterfeit Servers This protocol makes no attempt to verify the authenticity of the server. A hostile party could take advantage of this by intercepting the client's requests and returning misleading or otherwise incorrect responses. Service providers should consider such attacks when developing services using this protocol, and should require transport-layer security for any requests where the authenticity of the server or of request responses is an issue. 4.4. Proxying and Caching of Authenticated Content The HTTP Authorization scheme (Section 3.5.1) is optional. However, [RFC2616] relies on the "Authorization" and "WWW-Authenticate" header fields to distinguish authenticated content so that it can be protected. Proxies and caches, in particular, may fail to adequately protect requests not using these header fields. For example, private authenticated content may be stored in (and thus retrievable from) publicly accessible caches. Servers not using the HTTP "Authorization" header field should take care to use other mechanisms, such as the "Cache-Control" header field, to ensure that authenticated content is protected. 4.5. Plaintext Storage of Credentials The client shared-secret and token shared-secret function the same way passwords do in traditional authentication systems. In order to compute the signatures used in methods other than "RSA-SHA1", the server must have access to these secrets in plaintext form. This is in contrast, for example, to modern operating systems, which store only a one-way hash of user credentials. If an attacker were to gain access to these secrets -- or worse, to the server's database of all such secrets -- he or she would be able to perform any action on behalf of any resource owner. Accordingly, it is critical that servers protect these secrets from unauthorized access. Hammer-Lahav Informational [Page 30] RFC 5849 OAuth 1.0 April 2010 4.6. Secrecy of the Client Credentials In many cases, the client application will be under the control of potentially untrusted parties. For example, if the client is a desktop application with freely available source code or an executable binary, an attacker may be able to download a copy for analysis. In such cases, attackers will be able to recover the client credentials. Accordingly, servers should not use the client credentials alone to verify the identity of the client. Where possible, other factors such as IP address should be used as well. 4.7. Phishing Attacks Wide deployment of this and similar protocols may cause resource owners to become inured to the practice of being redirected to websites where they are asked to enter their passwords. If resource owners are not careful to verify the authenticity of these websites before entering their credentials, it will be possible for attackers to exploit this practice to steal resource owners' passwords. Servers should attempt to educate resource owners about the risks phishing attacks pose, and should provide mechanisms that make it easy for resource owners to confirm the authenticity of their sites. Client developers should consider the security implications of how they interact with a user-agent (e.g., separate window, embedded), and the ability of the end-user to verify the authenticity of the server website. 4.8. Scoping of Access Requests By itself, this protocol does not provide any method for scoping the access rights granted to a client. However, most applications do require greater granularity of access rights. For example, servers may wish to make it possible to grant access to some protected resources but not others, or to grant only limited access (such as read-only access) to those protected resources. When implementing this protocol, servers should consider the types of access resource owners may wish to grant clients, and should provide mechanisms to do so. Servers should also take care to ensure that resource owners understand the access they are granting, as well as any risks that may be involved. Hammer-Lahav Informational [Page 31] RFC 5849 OAuth 1.0 April 2010 4.9. Entropy of Secrets Unless a transport-layer security protocol is used, eavesdroppers will have full access to authenticated requests and signatures, and will thus be able to mount offline brute-force attacks to recover the credentials used. Servers should be careful to assign shared-secrets that are long enough, and random enough, to resist such attacks for at least the length of time that the shared-secrets are valid. For example, if shared-secrets are valid for two weeks, servers should ensure that it is not possible to mount a brute force attack that recovers the shared-secret in less than two weeks. Of course, servers are urged to err on the side of caution, and use the longest secrets reasonable. It is equally important that the pseudo-random number generator (PRNG) used to generate these secrets be of sufficiently high quality. Many PRNG implementations generate number sequences that may appear to be random, but that nevertheless exhibit patterns or other weaknesses that make cryptanalysis or brute force attacks easier. Implementers should be careful to use cryptographically secure PRNGs to avoid these problems. 4.10. Denial-of-Service / Resource-Exhaustion Attacks This specification includes a number of features that may make resource exhaustion attacks against servers possible. For example, this protocol requires servers to track used nonces. If an attacker is able to use many nonces quickly, the resources required to track them may exhaust available capacity. And again, this protocol can require servers to perform potentially expensive computations in order to verify the signature on incoming requests. An attacker may exploit this to perform a denial-of-service attack by sending a large number of invalid requests to the server. Resource Exhaustion attacks are by no means specific to this specification. However, implementers should be careful to consider the additional avenues of attack that this protocol exposes, and design their implementations accordingly. For example, entropy starvation typically results in either a complete denial of service while the system waits for new entropy or else in weak (easily guessable) secrets. When implementing this protocol, servers should consider which of these presents a more serious risk for their application and design accordingly. Hammer-Lahav Informational [Page 32] RFC 5849 OAuth 1.0 April 2010 4.11. SHA-1 Cryptographic Attacks SHA-1, the hash algorithm used in "HMAC-SHA1" and "RSA-SHA1" signature methods, has been shown to have a number of cryptographic weaknesses that significantly reduce its resistance to collision attacks. While these weaknesses do not seem to affect the use of SHA-1 with the Hash-based Message Authentication Code (HMAC) and should not affect the "HMAC-SHA1" signature method, it may affect the use of the "RSA-SHA1" signature method. NIST has announced that it will phase out use of SHA-1 in digital signatures by 2010 [NIST_SHA-1Comments]. Practically speaking, these weaknesses are difficult to exploit, and by themselves do not pose a significant risk to users of this protocol. They may, however, make more efficient attacks possible, and servers should take this into account when considering whether SHA-1 provides an adequate level of security for their applications. 4.12. Signature Base String Limitations The signature base string has been designed to support the signature methods defined in this specification. Those designing additional signature methods, should evaluated the compatibility of the signature base string with their security requirements. Since the signature base string does not cover the entire HTTP request, such as most request entity-body, most entity-headers, and the order in which parameters are sent, servers should employ additional mechanisms to protect such elements. 4.13. Cross-Site Request Forgery (CSRF) Cross-Site Request Forgery (CSRF) is a web-based attack whereby HTTP requests are transmitted from a user that the website trusts or has authenticated. CSRF attacks on authorization approvals can allow an attacker to obtain authorization to protected resources without the consent of the User. Servers SHOULD strongly consider best practices in CSRF prevention at all the protocol authorization endpoints. CSRF attacks on OAuth callback URIs hosted by clients are also possible. Clients should prevent CSRF attacks on OAuth callback URIs by verifying that the resource owner at the client site intended to complete the OAuth negotiation with the server. The methods for preventing such CSRF attacks are beyond the scope of this specification. Hammer-Lahav Informational [Page 33] RFC 5849 OAuth 1.0 April 2010 4.14. User Interface Redress Servers should protect the authorization process against user interface (UI) redress attacks (also known as "clickjacking"). As of the time of this writing, no complete defenses against UI redress are available. Servers can mitigate the risk of UI redress attacks using the following techniques: o JavaScript frame busting. o JavaScript frame busting, and requiring that browsers have JavaScript enabled on the authorization page. o Browser-specific anti-framing techniques. o Requiring password reentry before issuing OAuth tokens. 4.15. Automatic Processing of Repeat Authorizations Servers may wish to automatically process authorization requests (Section 2.2) from clients that have been previously authorized by the resource owner. When the resource owner is redirected to the server to grant access, the server detects that the resource owner has already granted access to that particular client. Instead of prompting the resource owner for approval, the server automatically redirects the resource owner back to the client. If the client credentials are compromised, automatic processing creates additional security risks. An attacker can use the stolen client credentials to redirect the resource owner to the server with an authorization request. The server will then grant access to the resource owner's data without the resource owner's explicit approval, or even awareness of an attack. If no automatic approval is implemented, an attacker must use social engineering to convince the resource owner to approve access. Servers can mitigate the risks associated with automatic processing by limiting the scope of token credentials obtained through automated approvals. Tokens credentials obtained through explicit resource owner consent can remain unaffected. Clients can mitigate the risks associated with automatic processing by protecting their client credentials. Hammer-Lahav Informational [Page 34] RFC 5849 OAuth 1.0 April 2010 5. Acknowledgments This specification is directly based on the OAuth Core 1.0 Revision A community specification, which in turn was modeled after existing proprietary protocols and best practices that have been independently implemented by various companies. The community specification was edited by Eran Hammer-Lahav and authored by: Mark Atwood, Dirk Balfanz, Darren Bounds, Richard M. Conlan, Blaine Cook, Leah Culver, Breno de Medeiros, Brian Eaton, Kellan Elliott-McCrea, Larry Halff, Eran Hammer-Lahav, Ben Laurie, Chris Messina, John Panzer, Sam Quigley, David Recordon, Eran Sandler, Jonathan Sergent, Todd Sieling, Brian Slesinsky, and Andy Smith. The editor would like to thank the following individuals for their invaluable contribution to the publication of this edition of the protocol: Lisa Dusseault, Justin Hart, Avshalom Houri, Chris Messina, Mark Nottingham, Tim Polk, Peter Saint-Andre, Joseph Smarr, and Paul Walker. Hammer-Lahav Informational [Page 35] RFC 5849 OAuth 1.0 April 2010 Appendix A. Differences from the Community Edition This specification includes the following changes made to the original community document [OAuthCore1.0_RevisionA] in order to correct mistakes and omissions identified since the document was originally published at <http://oauth.net>. o Changed using TLS/SSL when sending or requesting plain text credentials from SHOULD to MUST. This change affects any use of the "PLAINTEXT" signature method, as well as requesting temporary credentials (Section 2.1) and obtaining token credentials (Section 2.3). o Adjusted nonce language to indicate it is unique per token/ timestamp/client combination. o Removed the requirement for timestamps to be equal to or greater than the timestamp used in the previous request. o Changed the nonce and timestamp parameters to OPTIONAL when using the "PLAINTEXT" signature method. o Extended signature base string coverage that includes "application/x-www-form-urlencoded" entity-body parameters when the HTTP method used is other than "POST" and URI query parameters when the HTTP method used is other than "GET". o Incorporated corrections to the instructions in each signature method to encode the signature value before inserting it into the "oauth_signature" parameter, removing errors that would have caused double-encoded values. o Allowed omitting the "oauth_token" parameter when empty. o Permitted sending requests for temporary credentials with an empty "oauth_token" parameter. o Removed the restrictions from defining additional "oauth_" parameters. Hammer-Lahav Informational [Page 36] RFC 5849 OAuth 1.0 April 2010 6. References 6.1. Normative References [RFC2045] Freed, N. and N. Borenstein, "Multipurpose Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies", RFC 2045, November 1996. [RFC2104] Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: Keyed- Hashing for Message Authentication", RFC 2104, February 1997. [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, March 1997. [RFC2616] Fielding, R., Gettys, J., Mogul, J., Frystyk, H., Masinter, L., Leach, P., and T. Berners-Lee, "Hypertext Transfer Protocol -- HTTP/1.1", RFC 2616, June 1999. [RFC2617] Franks, J., Hallam-Baker, P., Hostetler, J., Lawrence, S., Leach, P., Luotonen, A., and L. Stewart, "HTTP Authentication: Basic and Digest Access Authentication", RFC 2617, June 1999. [RFC2818] Rescorla, E., "HTTP Over TLS", RFC 2818, May 2000. [RFC3447] Jonsson, J. and B. Kaliski, "Public-Key Cryptography Standards (PKCS) #1: RSA Cryptography Specifications Version 2.1", RFC 3447, February 2003. [RFC3629] Yergeau, F., "UTF-8, a transformation format of ISO 10646", STD 63, RFC 3629, November 2003. [RFC3986] Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform Resource Identifier (URI): Generic Syntax", STD 66, RFC 3986, January 2005. [W3C.REC-html40-19980424] Hors, A., Raggett, D., and I. Jacobs, "HTML 4.0 Specification", World Wide Web Consortium Recommendation REC-html40-19980424, April 1998, <http://www.w3.org/TR/1998/REC-html40-19980424>. Hammer-Lahav Informational [Page 37] RFC 5849 OAuth 1.0 April 2010 6.2. Informative References [NIST_SHA-1Comments] Burr, W., "NIST Comments on Cryptanalytic Attacks on SHA-1", <http://csrc.nist.gov/groups/ST/hash/statement.html>. [OAuthCore1.0_RevisionA] OAuth Community, "OAuth Core 1.0 Revision A", <http://oauth.net/core/1.0a>. Author's Address Eran Hammer-Lahav (editor) EMail: eran@hueniverse.com URI: http://hueniverse.com Hammer-Lahav Informational [Page 38] oauth1-client/tests/PlainTextSignatureTest.php 0000644 00000002401 15007532371 0015526 0 ustar 00 <?php namespace League\OAuth1\Client\Tests; use League\OAuth1\Client\Signature\PlainTextSignature; use Mockery as m; use PHPUnit\Framework\TestCase; class PlainTextSignatureTest extends TestCase { protected function tearDown(): void { m::close(); parent::tearDown(); } public function testSigningRequest() { $signature = new PlainTextSignature($this->getMockClientCredentials()); $this->assertEquals('clientsecret&', $signature->sign($uri = 'http://www.example.com/')); $signature->setCredentials($this->getMockCredentials()); $this->assertEquals('clientsecret&tokensecret', $signature->sign($uri)); $this->assertEquals('PLAINTEXT', $signature->method()); } protected function getMockClientCredentials() { $clientCredentials = m::mock('League\OAuth1\Client\Credentials\ClientCredentialsInterface'); $clientCredentials->shouldReceive('getSecret')->andReturn('clientsecret'); return $clientCredentials; } protected function getMockCredentials() { $credentials = m::mock('League\OAuth1\Client\Credentials\CredentialsInterface'); $credentials->shouldReceive('getSecret')->andReturn('tokensecret'); return $credentials; } } oauth1-client/tests/RsaClientCredentialsTest.php 0000644 00000004231 15007532371 0016001 0 ustar 00 <?php namespace League\OAuth1\Client\Tests; use League\OAuth1\Client\Credentials\CredentialsException; use League\OAuth1\Client\Credentials\RsaClientCredentials; use OpenSSLAsymmetricKey; use PHPUnit\Framework\TestCase; class RsaClientCredentialsTest extends TestCase { public function testGetRsaPublicKey() { $credentials = new RsaClientCredentials(); $credentials->setRsaPublicKey(__DIR__ . '/test_rsa_publickey.pem'); /** @var resource|OpenSSLAsymmetricKey $key */ $key = $credentials->getRsaPublicKey(); $this->assertFalse(is_null($key)); $this->assertEquals($key, $credentials->getRsaPublicKey()); } public function testGetRsaPublicKeyNotExists() { $this->expectException(CredentialsException::class); $credentials = new RsaClientCredentials(); $credentials->setRsaPublicKey('fail'); $credentials->getRsaPublicKey(); } public function testGetRsaPublicKeyInvalid() { $this->expectException(CredentialsException::class); $credentials = new RsaClientCredentials(); $credentials->setRsaPublicKey(__DIR__ . '/test_rsa_invalidkey.pem'); $credentials->getRsaPublicKey(); } public function testGetRsaPrivateKey() { $credentials = new RsaClientCredentials(); $credentials->setRsaPrivateKey(__DIR__ . '/test_rsa_privatekey.pem'); /** @var resource|OpenSSLAsymmetricKey $key */ $key = $credentials->getRsaPrivateKey(); $this->assertFalse(is_null($key)); $this->assertEquals($key, $credentials->getRsaPrivateKey()); } public function testGetRsaPrivateKeyNotExists() { $this->expectException(CredentialsException::class); $credentials = new RsaClientCredentials(); $credentials->setRsaPrivateKey('fail'); $credentials->getRsaPrivateKey(); } public function testGetRsaPrivateKeyInvalid() { $this->expectException(CredentialsException::class); $credentials = new RsaClientCredentials(); $credentials->setRsaPrivateKey(__DIR__ . '/test_rsa_invalidkey.pem'); $credentials->getRsaPrivateKey(); } } oauth1-client/tests/RsaSha1SignatureTest.php 0000644 00000010476 15007532371 0015073 0 ustar 00 <?php namespace League\OAuth1\Client\Tests; use League\OAuth1\Client\Credentials\ClientCredentialsInterface; use League\OAuth1\Client\Credentials\RsaClientCredentials; use League\OAuth1\Client\Signature\RsaSha1Signature; use Mockery; use PHPUnit\Framework\TestCase; class RsaSha1SignatureTest extends TestCase { public function testMethod() { $signature = new RsaSha1Signature($this->getClientCredentials()); $this->assertEquals('RSA-SHA1', $signature->method()); } public function testSigningRequest() { $signature = new RsaSha1Signature($this->getClientCredentials()); $uri = 'http://www.example.com/?qux=corge'; $parameters = ['foo' => 'bar', 'baz' => null]; $this->assertEquals('h8vpV4CYnLwss+rWicKE4sY6AiW2+DT6Fe7qB8jA7LSLhX5jvLEeX1D8E2ynSePSksAY48j+OSLu9vo5juS2duwNK8UA2Rtnnvuj6UFxpx70dpjHAsQg6EbycGptL/SChDkxfpG8LhuwX1FlFa+H0jLYXI5Dy8j90g51GRJbj48=', $signature->sign($uri, $parameters)); } public function testQueryStringFromArray() { $array = ['a' => 'b']; $res = $this->invokeQueryStringFromData($array); $this->assertSame( 'a%3Db', $res ); } public function testQueryStringFromIndexedArray() { $array = ['a', 'b']; $res = $this->invokeQueryStringFromData($array); $this->assertSame( '0%3Da%261%3Db', $res ); } public function testQueryStringFromMultiDimensionalArray() { $array = [ 'a' => [ 'b' => [ 'c' => 'd', ], 'e' => [ 'f' => 'g', ], ], 'h' => 'i', 'empty' => '', 'null' => null, 'false' => false, ]; // Convert to query string. $res = $this->invokeQueryStringFromData($array); $this->assertSame( 'a%5Bb%5D%5Bc%5D%3Dd%26a%5Be%5D%5Bf%5D%3Dg%26h%3Di%26empty%3D%26null%3D%26false%3D', $res ); // Reverse engineer the string. $res = urldecode($res); $this->assertSame( 'a[b][c]=d&a[e][f]=g&h=i&empty=&null=&false=', $res ); // Finally, parse the string back to an array. parse_str($res, $original_array); // And ensure it matches the orignal array (approximately). $this->assertSame( [ 'a' => [ 'b' => [ 'c' => 'd', ], 'e' => [ 'f' => 'g', ], ], 'h' => 'i', 'empty' => '', 'null' => '', // null value gets lost in string translation 'false' => '', // false value gets lost in string translation ], $original_array ); } public function testSigningRequestWithMultiDimensionalParams() { $signature = new RsaSha1Signature($this->getClientCredentials()); $uri = 'http://www.example.com/'; $parameters = [ 'a' => [ 'b' => [ 'c' => 'd', ], 'e' => [ 'f' => 'g', ], ], 'h' => 'i', 'empty' => '', 'null' => null, 'false' => false, ]; $this->assertEquals('X9EkmOEbA5CoF2Hicf3ciAumpp1zkKxnVZkh/mEwWyF2DDcrfou9XF11WvbBu3G4loJGeX4GY1FsIrQpsjEILbn0e7Alyii/x8VA9mBwdqMhQVl49jF0pdowocc03M04cAbAOMNObT7tMmDs+YTFgRxEGCiUkq9AizP1cW3+eBo=', $signature->sign($uri, $parameters)); } protected function invokeQueryStringFromData(array $args) { $signature = new RsaSha1Signature(Mockery::mock(ClientCredentialsInterface::class)); $refl = new \ReflectionObject($signature); $method = $refl->getMethod('queryStringFromData'); $method->setAccessible(true); return $method->invokeArgs($signature, [$args]); } protected function getClientCredentials() { $credentials = new RsaClientCredentials(); $credentials->setRsaPublicKey(__DIR__ . '/test_rsa_publickey.pem'); $credentials->setRsaPrivateKey(__DIR__ . '/test_rsa_privatekey.pem'); return $credentials; } } oauth1-client/tests/TwitterServerTest.php 0000644 00000002103 15007532371 0014564 0 ustar 00 <?php namespace League\OAuth1\Client\Tests; use Generator; use League\OAuth1\Client\Server\Twitter; use PHPUnit\Framework\TestCase; class TwitterServerTest extends TestCase { public function sampleTemporaryCredentialUrls(): Generator { yield 'No application scope' => [ null, 'https://api.twitter.com/oauth/request_token', ]; yield "Read" => [ 'read', 'https://api.twitter.com/oauth/request_token?x_auth_access_type=read', ]; yield "Write" => [ 'write', 'https://api.twitter.com/oauth/request_token?x_auth_access_type=write', ]; } /** @dataProvider sampleTemporaryCredentialUrls */ public function testItProvidesNoApplicationScopeByDefault(?string $applicationScope, string $url): void { $twitter = new Twitter([ 'identifier' => 'mykey', 'secret' => 'mysecret', 'callback_uri' => 'http://app.dev/', 'scope' => $applicationScope, ]); self::assertEquals($url, $twitter->urlTemporaryCredentials()); } } oauth1-client/tests/stubs/ServerStub.php 0000644 00000003013 15007532371 0014340 0 ustar 00 <?php namespace League\OAuth1\Client\Tests; use League\OAuth1\Client\Credentials\TokenCredentials; use League\OAuth1\Client\Server\Server; use League\OAuth1\Client\Server\User; class ServerStub extends Server { /** * @inheritDoc */ public function urlTemporaryCredentials() { return 'http://www.example.com/temporary'; } /** * @inheritDoc */ public function urlAuthorization() { return 'http://www.example.com/authorize'; } /** * @inheritDoc */ public function urlTokenCredentials() { return 'http://www.example.com/token'; } /** * @inheritDoc */ public function urlUserDetails() { return 'http://www.example.com/user'; } /** * @inheritDoc */ public function userDetails($data, TokenCredentials $tokenCredentials) { $user = new User; $user->firstName = $data['foo']; return $user; } /** * @inheritDoc */ public function userUid($data, TokenCredentials $tokenCredentials) { return isset($data['id']) ? $data['id'] : null; } /** * @inheritDoc */ public function userEmail($data, TokenCredentials $tokenCredentials) { return isset($data['contact_email']) ? $data['contact_email'] : null; } /** * @inheritDoc */ public function userScreenName($data, TokenCredentials $tokenCredentials) { return isset($data['username']) ? $data['username'] : null; } } oauth1-client/tests/XingServerTest.php 0000644 00000024473 15007532371 0014045 0 ustar 00 <?php namespace League\OAuth1\Client\Tests; use InvalidArgumentException; use League\OAuth1\Client\Credentials\ClientCredentials; use League\OAuth1\Client\Server\Xing; use Mockery as m; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; class XingTest extends TestCase { protected function tearDown(): void { m::close(); parent::tearDown(); } public function testCreatingWithArray() { $server = new Xing($this->getMockClientCredentials()); $credentials = $server->getClientCredentials(); $this->assertInstanceOf('League\OAuth1\Client\Credentials\ClientCredentialsInterface', $credentials); $this->assertEquals($this->getApplicationKey(), $credentials->getIdentifier()); $this->assertEquals('mysecret', $credentials->getSecret()); $this->assertEquals('http://app.dev/', $credentials->getCallbackUri()); } public function testCreatingWithObject() { $credentials = new ClientCredentials; $credentials->setIdentifier('myidentifier'); $credentials->setSecret('mysecret'); $credentials->setCallbackUri('http://app.dev/'); $server = new Xing($credentials); $this->assertEquals($credentials, $server->getClientCredentials()); } public function testGettingTemporaryCredentials() { $server = m::mock('League\OAuth1\Client\Server\Xing[createHttpClient]', [$this->getMockClientCredentials()]); $server->shouldReceive('createHttpClient')->andReturn($client = m::mock('stdClass')); $me = $this; $client->shouldReceive('post')->with('https://api.xing.com/v1/request_token', m::on(function ($options) use ($me) { $headers = $options['headers']; $me->assertTrue(isset($headers['Authorization'])); // OAuth protocol specifies a strict number of // headers should be sent, in the correct order. // We'll validate that here. $pattern = '/OAuth oauth_consumer_key=".*?", oauth_nonce="[a-zA-Z0-9]+", oauth_signature_method="HMAC-SHA1", oauth_timestamp="\d{10}", oauth_version="1.0", oauth_callback="' . preg_quote('http%3A%2F%2Fapp.dev%2F', '/') . '", oauth_signature=".*?"/'; $matches = preg_match($pattern, $headers['Authorization']); $me->assertEquals(1, $matches, 'Asserting that the authorization header contains the correct expression.'); return true; }))->once()->andReturn($response = m::mock(ResponseInterface::class)); $response->shouldReceive('getBody')->andReturn('oauth_token=temporarycredentialsidentifier&oauth_token_secret=temporarycredentialssecret&oauth_callback_confirmed=true'); $credentials = $server->getTemporaryCredentials(); $this->assertInstanceOf('League\OAuth1\Client\Credentials\TemporaryCredentials', $credentials); $this->assertEquals('temporarycredentialsidentifier', $credentials->getIdentifier()); $this->assertEquals('temporarycredentialssecret', $credentials->getSecret()); } public function testGettingDefaultAuthorizationUrl() { $server = new Xing($this->getMockClientCredentials()); $expected = 'https://api.xing.com/v1/authorize?oauth_token=foo'; $this->assertEquals($expected, $server->getAuthorizationUrl('foo')); $credentials = m::mock('League\OAuth1\Client\Credentials\TemporaryCredentials'); $credentials->shouldReceive('getIdentifier')->andReturn('foo'); $this->assertEquals($expected, $server->getAuthorizationUrl($credentials)); } public function testGettingTokenCredentialsFailsWithManInTheMiddle() { $server = new Xing($this->getMockClientCredentials()); $credentials = m::mock('League\OAuth1\Client\Credentials\TemporaryCredentials'); $credentials->shouldReceive('getIdentifier')->andReturn('foo'); $this->expectException(InvalidArgumentException::class); $server->getTokenCredentials($credentials, 'bar', 'verifier'); } public function testGettingTokenCredentials() { $server = m::mock('League\OAuth1\Client\Server\Xing[createHttpClient]', [$this->getMockClientCredentials()]); $temporaryCredentials = m::mock('League\OAuth1\Client\Credentials\TemporaryCredentials'); $temporaryCredentials->shouldReceive('getIdentifier')->andReturn('temporarycredentialsidentifier'); $temporaryCredentials->shouldReceive('getSecret')->andReturn('temporarycredentialssecret'); $server->shouldReceive('createHttpClient')->andReturn($client = m::mock('stdClass')); $me = $this; $client->shouldReceive('post')->with('https://api.xing.com/v1/access_token', m::on(function ($options) use ($me) { $headers = $options['headers']; $body = $options['form_params']; $me->assertTrue(isset($headers['Authorization'])); // OAuth protocol specifies a strict number of // headers should be sent, in the correct order. // We'll validate that here. $pattern = '/OAuth oauth_consumer_key=".*?", oauth_nonce="[a-zA-Z0-9]+", oauth_signature_method="HMAC-SHA1", oauth_timestamp="\d{10}", oauth_version="1.0", oauth_token="temporarycredentialsidentifier", oauth_signature=".*?"/'; $matches = preg_match($pattern, $headers['Authorization']); $me->assertEquals(1, $matches, 'Asserting that the authorization header contains the correct expression.'); $me->assertSame($body, ['oauth_verifier' => 'myverifiercode']); return true; }))->once()->andReturn($response = m::mock(ResponseInterface::class)); $response->shouldReceive('getBody')->andReturn('oauth_token=tokencredentialsidentifier&oauth_token_secret=tokencredentialssecret'); $credentials = $server->getTokenCredentials($temporaryCredentials, 'temporarycredentialsidentifier', 'myverifiercode'); $this->assertInstanceOf('League\OAuth1\Client\Credentials\TokenCredentials', $credentials); $this->assertEquals('tokencredentialsidentifier', $credentials->getIdentifier()); $this->assertEquals('tokencredentialssecret', $credentials->getSecret()); } public function testGettingUserDetails() { $server = m::mock('League\OAuth1\Client\Server\Xing[createHttpClient,protocolHeader]', [$this->getMockClientCredentials()]); $temporaryCredentials = m::mock('League\OAuth1\Client\Credentials\TokenCredentials'); $temporaryCredentials->shouldReceive('getIdentifier')->andReturn('tokencredentialsidentifier'); $temporaryCredentials->shouldReceive('getSecret')->andReturn('tokencredentialssecret'); $server->shouldReceive('createHttpClient')->andReturn($client = m::mock('stdClass')); $me = $this; $client->shouldReceive('get')->with('https://api.xing.com/v1/users/me', m::on(function ($options) use ($me) { $headers = $options['headers']; $me->assertTrue(isset($headers['Authorization'])); // OAuth protocol specifies a strict number of // headers should be sent, in the correct order. // We'll validate that here. $pattern = '/OAuth oauth_consumer_key=".*?", oauth_nonce="[a-zA-Z0-9]+", oauth_signature_method="HMAC-SHA1", oauth_timestamp="\d{10}", oauth_version="1.0", oauth_token="tokencredentialsidentifier", oauth_signature=".*?"/'; $matches = preg_match($pattern, $headers['Authorization']); $me->assertEquals(1, $matches, 'Asserting that the authorization header contains the correct expression.'); return true; }))->once()->andReturn($response = m::mock(ResponseInterface::class)); $response->shouldReceive('getBody')->once()->andReturn($this->getUserPayload()); $user = $server->getUserDetails($temporaryCredentials); $this->assertInstanceOf('League\OAuth1\Client\Server\User', $user); $this->assertEquals('Roman Gelembjuk', $user->name); $this->assertEquals('17144430_0f9409', $server->getUserUid($temporaryCredentials)); $this->assertEquals('XXXXXXXXXX@gmail.com', $server->getUserEmail($temporaryCredentials)); $this->assertEquals('Roman Gelembjuk', $server->getUserScreenName($temporaryCredentials)); } protected function getMockClientCredentials() { return [ 'identifier' => $this->getApplicationKey(), 'secret' => 'mysecret', 'callback_uri' => 'http://app.dev/', ]; } protected function getApplicationKey() { return 'abcdefghijk'; } protected function getApplicationExpiration($days = 0) { return is_numeric($days) && $days > 0 ? $days . 'day' . ($days == 1 ? '' : 's') : 'never'; } protected function getApplicationName() { return 'fizz buzz'; } private function getUserPayload() { return '{ "users":[ { "id":"17144430_0f9409", "active_email":"XXXXXXXXXX@gmail.com", "time_zone": { "utc_offset":3.0, "name":"Europe/Kiev" }, "display_name":"Roman Gelembjuk", "first_name":"Roman", "last_name":"Gelembjuk", "gender":"m", "page_name":"Roman_Gelembjuk", "birth_date": {"year":null,"month":null,"day":null}, "wants":null, "haves":null, "interests":null, "web_profiles":{}, "badges":[], "photo_urls": { "large":"https://x1.xingassets.com/assets/frontend_minified/img/users/nobody_m.140x185.jpg", "maxi_thumb":"https://x1.xingassets.com/assets/frontend_minified/img/users/nobody_m.70x93.jpg", "medium_thumb":"https://x1.xingassets.com/assets/frontend_minified/img/users/nobody_m.57x75.jpg" }, "permalink":"https://www.xing.com/profile/Roman_Gelembjuk", "languages":{"en":null}, "employment_status":"EMPLOYEE", "organisation_member":null, "instant_messaging_accounts":{}, "educational_background": {"degree":null,"primary_school":null,"schools":[],"qualifications":[]}, "private_address":{ "street":null, "zip_code":null, "city":null, "province":null, "country":null, "email":"XXXXXXXX@gmail.com", "fax":null, "phone":null, "mobile_phone":null} ,"business_address": { "street":null, "zip_code":null, "city":"Ivano-Frankivsk", "province":null, "country":"UA", "email":null, "fax":null,"phone":null,"mobile_phone":null }, "premium_services":[] }]}'; } } oauth1-client/tests/test_rsa_invalidkey.pem 0000644 00000000023 15007532371 0015127 0 ustar 00 not a valid RSA key oauth1-client/tests/test_rsa_privatekey.pem 0000644 00000001567 15007532371 0015171 0 ustar 00 -----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQDJScPCpHHPakw9v4nhxi+cBumCml3WpMNDaE3Cxnkf6HALzoi8 fQPx3XRfKSoLG6b7uTG+vDoLCL49ZCyYwrnggsFF08bJMqIhUHlZrkiyT5UhdTIh XEEFfb6XmieHNtra+ur8E3PVal4PEdOCEmJehBMJkiCsxNOJ/kCeYSMdbQIDAQAB AoGAMo7JkcEWKP/NCJFsg33xBWKjEj/NpBUcSnkPVwXc9IvAYObOZ3GLJRv3l9NS ERov9fgNK5hBh/X5OphHr1LxtqU5gAFYx5Qgt/WG3ZH9KScXkaPS3Oq2qK9krbA2 BXYKP4NEqhjJTecy7M8bju5+lsjteyqVSsVLHdLhUfPRbE0CQQDyYP6iChqZm1AK A8x8PKvJsd4zSdxWXUYSmD7mAtek5VeWblbcXYYdeYPN6hNmqzaLelmrZI51x1Uf hf1ryIfrAkEA1JmdSsNuxi9MOY3HqErWYsqZ//mVOxVyCAwf7OWQ0rTHEQBhQwpS 9nk0YFHI9t6nVUwXrZ7/7UJPTu8OjPKyBwJAYw2OomwcqM/XKvCYfeFRl1DwbOdv e4AM5gaAFgHtXP85B0o6hz5VU/BYFCvoF9o6pU+wG6IxsiJvQD3C7mx6VwJAbXYW PVs4WsQZe/ya0vSNQ1pLRjdr9XrKNoh/m4prMYGwiPloGotjQdIP/JO/ZBQplcpS 2qrl3HPqv5poJHwE2wJBAM37BVINHR0zcSHfFLmSYrqmLx2oC3UAB3exmpYSUgOz wJqPfmPWuuXuu6h0Z2DazUan+2EiecX6C4ywkm+qk1I= -----END RSA PRIVATE KEY----- oauth1-client/tests/TrelloServerTest.php 0000644 00000036332 15007532371 0014376 0 ustar 00 <?php namespace League\OAuth1\Client\Tests; use InvalidArgumentException; use League\OAuth1\Client\Credentials\ClientCredentials; use League\OAuth1\Client\Server\Trello; use Mockery as m; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; class TrelloTest extends TestCase { protected function tearDown(): void { m::close(); parent::tearDown(); } public function testCreatingWithArray() { $server = new Trello($this->getMockClientCredentials()); $credentials = $server->getClientCredentials(); $this->assertInstanceOf('League\OAuth1\Client\Credentials\ClientCredentialsInterface', $credentials); $this->assertEquals($this->getApplicationKey(), $credentials->getIdentifier()); $this->assertEquals('mysecret', $credentials->getSecret()); $this->assertEquals('http://app.dev/', $credentials->getCallbackUri()); } public function testCreatingWithObject() { $credentials = new ClientCredentials; $credentials->setIdentifier('myidentifier'); $credentials->setSecret('mysecret'); $credentials->setCallbackUri('http://app.dev/'); $server = new Trello($credentials); $this->assertEquals($credentials, $server->getClientCredentials()); } public function testGettingTemporaryCredentials() { $server = m::mock('League\OAuth1\Client\Server\Trello[createHttpClient]', [$this->getMockClientCredentials()]); $server->shouldReceive('createHttpClient')->andReturn($client = m::mock('stdClass')); $me = $this; $client->shouldReceive('post')->with('https://trello.com/1/OAuthGetRequestToken', m::on(function ($options) use ($me) { $headers = $options['headers']; $me->assertTrue(isset($headers['Authorization'])); // OAuth protocol specifies a strict number of // headers should be sent, in the correct order. // We'll validate that here. $pattern = '/OAuth oauth_consumer_key=".*?", oauth_nonce="[a-zA-Z0-9]+", oauth_signature_method="HMAC-SHA1", oauth_timestamp="\d{10}", oauth_version="1.0", oauth_callback="' . preg_quote('http%3A%2F%2Fapp.dev%2F', '/') . '", oauth_signature=".*?"/'; $matches = preg_match($pattern, $headers['Authorization']); $me->assertEquals(1, $matches, 'Asserting that the authorization header contains the correct expression.'); return true; }))->once()->andReturn($response = m::mock(ResponseInterface::class)); $response->shouldReceive('getBody')->andReturn('oauth_token=temporarycredentialsidentifier&oauth_token_secret=temporarycredentialssecret&oauth_callback_confirmed=true'); $credentials = $server->getTemporaryCredentials(); $this->assertInstanceOf('League\OAuth1\Client\Credentials\TemporaryCredentials', $credentials); $this->assertEquals('temporarycredentialsidentifier', $credentials->getIdentifier()); $this->assertEquals('temporarycredentialssecret', $credentials->getSecret()); } public function testGettingDefaultAuthorizationUrl() { $server = new Trello($this->getMockClientCredentials()); $expected = 'https://trello.com/1/OAuthAuthorizeToken?response_type=fragment&scope=read&expiration=1day&oauth_token=foo'; $this->assertEquals($expected, $server->getAuthorizationUrl('foo')); $credentials = m::mock('League\OAuth1\Client\Credentials\TemporaryCredentials'); $credentials->shouldReceive('getIdentifier')->andReturn('foo'); $this->assertEquals($expected, $server->getAuthorizationUrl($credentials)); } public function testGettingAuthorizationUrlWithExpirationAfterConstructingWithExpiration() { $credentials = $this->getMockClientCredentials(); $expiration = $this->getApplicationExpiration(2); $credentials['expiration'] = $expiration; $server = new Trello($credentials); $expected = 'https://trello.com/1/OAuthAuthorizeToken?response_type=fragment&scope=read&expiration=' . urlencode($expiration) . '&oauth_token=foo'; $this->assertEquals($expected, $server->getAuthorizationUrl('foo')); $credentials = m::mock('League\OAuth1\Client\Credentials\TemporaryCredentials'); $credentials->shouldReceive('getIdentifier')->andReturn('foo'); $this->assertEquals($expected, $server->getAuthorizationUrl($credentials)); } public function testGettingAuthorizationUrlWithExpirationAfterSettingExpiration() { $expiration = $this->getApplicationExpiration(2); $server = new Trello($this->getMockClientCredentials()); $server->setApplicationExpiration($expiration); $expected = 'https://trello.com/1/OAuthAuthorizeToken?response_type=fragment&scope=read&expiration=' . urlencode($expiration) . '&oauth_token=foo'; $this->assertEquals($expected, $server->getAuthorizationUrl('foo')); $credentials = m::mock('League\OAuth1\Client\Credentials\TemporaryCredentials'); $credentials->shouldReceive('getIdentifier')->andReturn('foo'); $this->assertEquals($expected, $server->getAuthorizationUrl($credentials)); } public function testGettingAuthorizationUrlWithNameAfterConstructingWithName() { $credentials = $this->getMockClientCredentials(); $name = $this->getApplicationName(); $credentials['name'] = $name; $server = new Trello($credentials); $expected = 'https://trello.com/1/OAuthAuthorizeToken?response_type=fragment&scope=read&expiration=1day&name=' . urlencode($name) . '&oauth_token=foo'; $this->assertEquals($expected, $server->getAuthorizationUrl('foo')); $credentials = m::mock('League\OAuth1\Client\Credentials\TemporaryCredentials'); $credentials->shouldReceive('getIdentifier')->andReturn('foo'); $this->assertEquals($expected, $server->getAuthorizationUrl($credentials)); } public function testGettingAuthorizationUrlWithNameAfterSettingName() { $name = $this->getApplicationName(); $server = new Trello($this->getMockClientCredentials()); $server->setApplicationName($name); $expected = 'https://trello.com/1/OAuthAuthorizeToken?response_type=fragment&scope=read&expiration=1day&name=' . urlencode($name) . '&oauth_token=foo'; $this->assertEquals($expected, $server->getAuthorizationUrl('foo')); $credentials = m::mock('League\OAuth1\Client\Credentials\TemporaryCredentials'); $credentials->shouldReceive('getIdentifier')->andReturn('foo'); $this->assertEquals($expected, $server->getAuthorizationUrl($credentials)); } public function testGettingAuthorizationUrlWithScopeAfterConstructingWithScope() { $credentials = $this->getMockClientCredentials(); $scope = $this->getApplicationScope(false); $credentials['scope'] = $scope; $server = new Trello($credentials); $expected = 'https://trello.com/1/OAuthAuthorizeToken?response_type=fragment&scope=' . urlencode($scope) . '&expiration=1day&oauth_token=foo'; $this->assertEquals($expected, $server->getAuthorizationUrl('foo')); $credentials = m::mock('League\OAuth1\Client\Credentials\TemporaryCredentials'); $credentials->shouldReceive('getIdentifier')->andReturn('foo'); $this->assertEquals($expected, $server->getAuthorizationUrl($credentials)); } public function testGettingAuthorizationUrlWithScopeAfterSettingScope() { $scope = $this->getApplicationScope(false); $server = new Trello($this->getMockClientCredentials()); $server->setApplicationScope($scope); $expected = 'https://trello.com/1/OAuthAuthorizeToken?response_type=fragment&scope=' . urlencode($scope) . '&expiration=1day&oauth_token=foo'; $this->assertEquals($expected, $server->getAuthorizationUrl('foo')); $credentials = m::mock('League\OAuth1\Client\Credentials\TemporaryCredentials'); $credentials->shouldReceive('getIdentifier')->andReturn('foo'); $this->assertEquals($expected, $server->getAuthorizationUrl($credentials)); } public function testGettingTokenCredentialsFailsWithManInTheMiddle() { $server = new Trello($this->getMockClientCredentials()); $credentials = m::mock('League\OAuth1\Client\Credentials\TemporaryCredentials'); $credentials->shouldReceive('getIdentifier')->andReturn('foo'); $this->expectException(InvalidArgumentException::class); $server->getTokenCredentials($credentials, 'bar', 'verifier'); } public function testGettingTokenCredentials() { $server = m::mock('League\OAuth1\Client\Server\Trello[createHttpClient]', [$this->getMockClientCredentials()]); $temporaryCredentials = m::mock('League\OAuth1\Client\Credentials\TemporaryCredentials'); $temporaryCredentials->shouldReceive('getIdentifier')->andReturn('temporarycredentialsidentifier'); $temporaryCredentials->shouldReceive('getSecret')->andReturn('temporarycredentialssecret'); $server->shouldReceive('createHttpClient')->andReturn($client = m::mock('stdClass')); $me = $this; $client->shouldReceive('post')->with('https://trello.com/1/OAuthGetAccessToken', m::on(function ($options) use ($me) { $headers = $options['headers']; $body = $options['form_params']; $me->assertTrue(isset($headers['Authorization'])); // OAuth protocol specifies a strict number of // headers should be sent, in the correct order. // We'll validate that here. $pattern = '/OAuth oauth_consumer_key=".*?", oauth_nonce="[a-zA-Z0-9]+", oauth_signature_method="HMAC-SHA1", oauth_timestamp="\d{10}", oauth_version="1.0", oauth_token="temporarycredentialsidentifier", oauth_signature=".*?"/'; $matches = preg_match($pattern, $headers['Authorization']); $me->assertEquals(1, $matches, 'Asserting that the authorization header contains the correct expression.'); $me->assertSame($body, ['oauth_verifier' => 'myverifiercode']); return true; }))->once()->andReturn($response = m::mock(ResponseInterface::class)); $response->shouldReceive('getBody')->andReturn('oauth_token=tokencredentialsidentifier&oauth_token_secret=tokencredentialssecret'); $credentials = $server->getTokenCredentials($temporaryCredentials, 'temporarycredentialsidentifier', 'myverifiercode'); $this->assertInstanceOf('League\OAuth1\Client\Credentials\TokenCredentials', $credentials); $this->assertEquals('tokencredentialsidentifier', $credentials->getIdentifier()); $this->assertEquals('tokencredentialssecret', $credentials->getSecret()); } public function testGettingUserDetails() { $server = m::mock('League\OAuth1\Client\Server\Trello[createHttpClient,protocolHeader]', [$this->getMockClientCredentials()]); $temporaryCredentials = m::mock('League\OAuth1\Client\Credentials\TokenCredentials'); $temporaryCredentials->shouldReceive('getIdentifier')->andReturn('tokencredentialsidentifier'); $temporaryCredentials->shouldReceive('getSecret')->andReturn('tokencredentialssecret'); $server->shouldReceive('createHttpClient')->andReturn($client = m::mock('stdClass')); $me = $this; $client->shouldReceive('get')->with('https://trello.com/1/members/me?key=' . $this->getApplicationKey() . '&token=' . $this->getAccessToken(), m::on(function ($options) use ($me) { $headers = $options['headers']; $me->assertTrue(isset($headers['Authorization'])); // OAuth protocol specifies a strict number of // headers should be sent, in the correct order. // We'll validate that here. $pattern = '/OAuth oauth_consumer_key=".*?", oauth_nonce="[a-zA-Z0-9]+", oauth_signature_method="HMAC-SHA1", oauth_timestamp="\d{10}", oauth_version="1.0", oauth_token="tokencredentialsidentifier", oauth_signature=".*?"/'; $matches = preg_match($pattern, $headers['Authorization']); $me->assertEquals(1, $matches, 'Asserting that the authorization header contains the correct expression.'); return true; }))->once()->andReturn($response = m::mock(ResponseInterface::class)); $response->shouldReceive('getBody')->once()->andReturn($this->getUserPayload()); $user = $server ->setAccessToken($this->getAccessToken()) ->getUserDetails($temporaryCredentials); $this->assertInstanceOf('League\OAuth1\Client\Server\User', $user); $this->assertEquals('Matilda Wormwood', $user->name); $this->assertEquals('545df696e29c0dddaed31967', $server->getUserUid($temporaryCredentials)); $this->assertEquals(null, $server->getUserEmail($temporaryCredentials)); $this->assertEquals('matildawormwood12', $server->getUserScreenName($temporaryCredentials)); } protected function getMockClientCredentials() { return [ 'identifier' => $this->getApplicationKey(), 'secret' => 'mysecret', 'callback_uri' => 'http://app.dev/', ]; } protected function getAccessToken() { return 'lmnopqrstuvwxyz'; } protected function getApplicationKey() { return 'abcdefghijk'; } protected function getApplicationExpiration($days = 0) { return is_numeric($days) && $days > 0 ? $days . 'day' . ($days == 1 ? '' : 's') : 'never'; } protected function getApplicationName() { return 'fizz buzz'; } protected function getApplicationScope($readonly = true) { return $readonly ? 'read' : 'read,write'; } private function getUserPayload() { return '{ "id": "545df696e29c0dddaed31967", "avatarHash": null, "bio": "I have magical powers", "bioData": null, "confirmed": true, "fullName": "Matilda Wormwood", "idPremOrgsAdmin": [], "initials": "MW", "memberType": "normal", "products": [], "status": "idle", "url": "https://trello.com/matildawormwood12", "username": "matildawormwood12", "avatarSource": "none", "email": null, "gravatarHash": "39aaaada0224f26f0bb8f1965326dcb7", "idBoards": [ "545df696e29c0dddaed31968", "545e01d6c7b2dd962b5b46cb" ], "idOrganizations": [ "54adfd79f9aea14f84009a85", "54adfde13b0e706947bc4789" ], "loginTypes": null, "oneTimeMessagesDismissed": [], "prefs": { "sendSummaries": true, "minutesBetweenSummaries": 1, "minutesBeforeDeadlineToNotify": 1440, "colorBlind": false, "timezoneInfo": { "timezoneNext": "CDT", "dateNext": "2015-03-08T08:00:00.000Z", "offsetNext": 300, "timezoneCurrent": "CST", "offsetCurrent": 360 } }, "trophies": [], "uploadedAvatarHash": null, "premiumFeatures": [], "idBoardsPinned": null }'; } } oauth1-client/tests/HmacSha1SignatureTest.php 0000644 00000010340 15007532371 0015204 0 ustar 00 <?php namespace League\OAuth1\Client\Tests; use League\OAuth1\Client\Signature\HmacSha1Signature; use Mockery as m; use PHPUnit\Framework\TestCase; class HmacSha1SignatureTest extends TestCase { protected function tearDown(): void { m::close(); parent::tearDown(); } public function testSigningRequest() { $signature = new HmacSha1Signature($this->getMockClientCredentials()); $uri = 'http://www.example.com/?qux=corge'; $parameters = ['foo' => 'bar', 'baz' => null]; $this->assertEquals('A3Y7C1SUHXR1EBYIUlT3d6QT1cQ=', $signature->sign($uri, $parameters)); } public function testSigningRequestWhereThePortIsNotStandard() { $signature = new HmacSha1Signature($this->getMockClientCredentials()); $uri = 'http://www.example.com:8080/?qux=corge'; $parameters = ['foo' => 'bar', 'baz' => null]; $this->assertEquals('ECcWxyi5UOC1G0MxH0ygm6Pd6JE=', $signature->sign($uri, $parameters)); } public function testQueryStringFromArray() { $array = ['a' => 'b']; $res = $this->invokeQueryStringFromData($array); $this->assertSame( 'a%3Db', $res ); } public function testQueryStringFromIndexedArray() { $array = ['a', 'b']; $res = $this->invokeQueryStringFromData($array); $this->assertSame( '0%3Da%261%3Db', $res ); } public function testQueryStringFromMultiDimensionalArray() { $array = [ 'a' => [ 'b' => [ 'c' => 'd', ], 'e' => [ 'f' => 'g', ], ], 'h' => 'i', 'empty' => '', 'null' => null, 'false' => false, ]; // Convert to query string. $res = $this->invokeQueryStringFromData($array); $this->assertSame( 'a%5Bb%5D%5Bc%5D%3Dd%26a%5Be%5D%5Bf%5D%3Dg%26h%3Di%26empty%3D%26null%3D%26false%3D', $res ); // Reverse engineer the string. $res = urldecode($res); $this->assertSame( 'a[b][c]=d&a[e][f]=g&h=i&empty=&null=&false=', $res ); // Finally, parse the string back to an array. parse_str($res, $original_array); // And ensure it matches the orignal array (approximately). $this->assertSame( [ 'a' => [ 'b' => [ 'c' => 'd', ], 'e' => [ 'f' => 'g', ], ], 'h' => 'i', 'empty' => '', 'null' => '', // null value gets lost in string translation 'false' => '', // false value gets lost in string translation ], $original_array ); } public function testSigningRequestWithMultiDimensionalParams() { $signature = new HmacSha1Signature($this->getMockClientCredentials()); $uri = 'http://www.example.com/'; $parameters = [ 'a' => [ 'b' => [ 'c' => 'd', ], 'e' => [ 'f' => 'g', ], ], 'h' => 'i', 'empty' => '', 'null' => null, 'false' => false, ]; $this->assertEquals('ZUxiJKugeEplaZm9e4hshN0I70U=', $signature->sign($uri, $parameters)); } protected function invokeQueryStringFromData(array $args) { $signature = new HmacSha1Signature(m::mock('League\OAuth1\Client\Credentials\ClientCredentialsInterface')); $refl = new \ReflectionObject($signature); $method = $refl->getMethod('queryStringFromData'); $method->setAccessible(true); return $method->invokeArgs($signature, [$args]); } protected function getMockClientCredentials() { $clientCredentials = m::mock('League\OAuth1\Client\Credentials\ClientCredentialsInterface'); $clientCredentials->shouldReceive('getSecret')->andReturn('clientsecret'); return $clientCredentials; } } oauth1-client/tests/ServerTest.php 0000644 00000034100 15007532371 0013203 0 ustar 00 <?php namespace League\OAuth1\Client\Tests; use InvalidArgumentException; use League\OAuth1\Client\Credentials\ClientCredentials; use League\OAuth1\Client\Credentials\RsaClientCredentials; use League\OAuth1\Client\Signature\RsaSha1Signature; use Mockery as m; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; class ServerTest extends TestCase { /** * Setup resources and dependencies. */ public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); require_once __DIR__ . '/stubs/ServerStub.php'; } protected function tearDown(): void { m::close(); parent::tearDown(); } public function testCreatingWithArray() { $server = new ServerStub($this->getMockClientCredentials()); $credentials = $server->getClientCredentials(); $this->assertInstanceOf('League\OAuth1\Client\Credentials\ClientCredentialsInterface', $credentials); $this->assertEquals('myidentifier', $credentials->getIdentifier()); $this->assertEquals('mysecret', $credentials->getSecret()); $this->assertEquals('http://app.dev/', $credentials->getCallbackUri()); } public function testCreatingWithArrayRsa() { $config = [ 'identifier' => 'app_key', 'secret' => 'secret', 'callback_uri' => 'https://example.com/callback', 'rsa_public_key' => __DIR__ . '/test_rsa_publickey.pem', 'rsa_private_key' => __DIR__ . '/test_rsa_privatekey.pem', ]; $server = new ServerStub($config); $credentials = $server->getClientCredentials(); $this->assertInstanceOf(RsaClientCredentials::class, $credentials); $signature = $server->getSignature(); $this->assertInstanceOf(RsaSha1Signature::class, $signature); } public function testCreatingWithObject() { $credentials = new ClientCredentials; $credentials->setIdentifier('myidentifier'); $credentials->setSecret('mysecret'); $credentials->setCallbackUri('http://app.dev/'); $server = new ServerStub($credentials); $this->assertEquals($credentials, $server->getClientCredentials()); } public function testCreatingWithInvalidInput() { $this->expectException(InvalidArgumentException::class); new ServerStub(uniqid()); } public function testGettingTemporaryCredentials() { $server = m::mock('League\OAuth1\Client\Tests\ServerStub[createHttpClient]', [$this->getMockClientCredentials()]); $server->shouldReceive('createHttpClient')->andReturn($client = m::mock('stdClass')); $me = $this; $client->shouldReceive('post')->with('http://www.example.com/temporary', m::on(function ($options) use ($me) { $headers = $options['headers']; $me->assertTrue(isset($headers['Authorization'])); // OAuth protocol specifies a strict number of // headers should be sent, in the correct order. // We'll validate that here. $pattern = '/OAuth oauth_consumer_key=".*?", oauth_nonce="[a-zA-Z0-9]+", oauth_signature_method="HMAC-SHA1", oauth_timestamp="\d{10}", oauth_version="1.0", oauth_callback="' . preg_quote('http%3A%2F%2Fapp.dev%2F', '/') . '", oauth_signature=".*?"/'; $matches = preg_match($pattern, $headers['Authorization']); $me->assertEquals(1, $matches, 'Asserting that the authorization header contains the correct expression.'); return true; }))->once()->andReturn($response = m::mock('stdClass')); $response->shouldReceive('getBody') ->andReturn('oauth_token=temporarycredentialsidentifier&oauth_token_secret=temporarycredentialssecret&oauth_callback_confirmed=true'); $credentials = $server->getTemporaryCredentials(); $this->assertInstanceOf('League\OAuth1\Client\Credentials\TemporaryCredentials', $credentials); $this->assertEquals('temporarycredentialsidentifier', $credentials->getIdentifier()); $this->assertEquals('temporarycredentialssecret', $credentials->getSecret()); } public function testGettingAuthorizationUrl() { $server = new ServerStub($this->getMockClientCredentials()); $expected = 'http://www.example.com/authorize?oauth_token=foo'; $this->assertEquals($expected, $server->getAuthorizationUrl('foo')); $credentials = m::mock('League\OAuth1\Client\Credentials\TemporaryCredentials'); $credentials->shouldReceive('getIdentifier')->andReturn('foo'); $this->assertEquals($expected, $server->getAuthorizationUrl($credentials)); } public function testGettingAuthorizationUrlWithOptions() { $server = new ServerStub($this->getMockClientCredentials()); $expected = 'http://www.example.com/authorize?oauth_token=foo'; $this->assertEquals($expected, $server->getAuthorizationUrl('foo', ['oauth_token' => 'bar'])); $expected = 'http://www.example.com/authorize?test=bar&oauth_token=foo'; $this->assertEquals($expected, $server->getAuthorizationUrl('foo', ['test' => 'bar'])); } public function testGettingTokenCredentialsFailsWithManInTheMiddle() { $server = new ServerStub($this->getMockClientCredentials()); $credentials = m::mock('League\OAuth1\Client\Credentials\TemporaryCredentials'); $credentials->shouldReceive('getIdentifier')->andReturn('foo'); $this->expectException(InvalidArgumentException::class); $server->getTokenCredentials($credentials, 'bar', 'verifier'); } public function testGettingTokenCredentials() { $server = m::mock('League\OAuth1\Client\Tests\ServerStub[createHttpClient]', [$this->getMockClientCredentials()]); $temporaryCredentials = m::mock('League\OAuth1\Client\Credentials\TemporaryCredentials'); $temporaryCredentials->shouldReceive('getIdentifier')->andReturn('temporarycredentialsidentifier'); $temporaryCredentials->shouldReceive('getSecret')->andReturn('temporarycredentialssecret'); $server->shouldReceive('createHttpClient')->andReturn($client = m::mock('stdClass')); $me = $this; $client->shouldReceive('post')->with('http://www.example.com/token', m::on(function ($options) use ($me) { $headers = $options['headers']; $body = $options['form_params']; $me->assertTrue(isset($headers['Authorization'])); $me->assertFalse(isset($headers['User-Agent'])); // OAuth protocol specifies a strict number of // headers should be sent, in the correct order. // We'll validate that here. $pattern = '/OAuth oauth_consumer_key=".*?", oauth_nonce="[a-zA-Z0-9]+", oauth_signature_method="HMAC-SHA1", oauth_timestamp="\d{10}", oauth_version="1.0", oauth_token="temporarycredentialsidentifier", oauth_signature=".*?"/'; $matches = preg_match($pattern, $headers['Authorization']); $me->assertEquals(1, $matches, 'Asserting that the authorization header contains the correct expression.'); $me->assertSame($body, ['oauth_verifier' => 'myverifiercode']); return true; }))->once()->andReturn($response = m::mock('stdClass')); $response->shouldReceive('getBody') ->andReturn('oauth_token=tokencredentialsidentifier&oauth_token_secret=tokencredentialssecret'); $credentials = $server->getTokenCredentials($temporaryCredentials, 'temporarycredentialsidentifier', 'myverifiercode'); $this->assertInstanceOf('League\OAuth1\Client\Credentials\TokenCredentials', $credentials); $this->assertEquals('tokencredentialsidentifier', $credentials->getIdentifier()); $this->assertEquals('tokencredentialssecret', $credentials->getSecret()); } public function testGettingTokenCredentialsWithUserAgent() { $userAgent = 'FooBar'; $server = m::mock('League\OAuth1\Client\Tests\ServerStub[createHttpClient]', [$this->getMockClientCredentials()]); $temporaryCredentials = m::mock('League\OAuth1\Client\Credentials\TemporaryCredentials'); $temporaryCredentials->shouldReceive('getIdentifier')->andReturn('temporarycredentialsidentifier'); $temporaryCredentials->shouldReceive('getSecret')->andReturn('temporarycredentialssecret'); $server->shouldReceive('createHttpClient')->andReturn($client = m::mock('stdClass')); $me = $this; $client->shouldReceive('post')->with('http://www.example.com/token', m::on(function ($options) use ($me, $userAgent) { $headers = $options['headers']; $body = $options['form_params']; $me->assertTrue(isset($headers['Authorization'])); $me->assertTrue(isset($headers['User-Agent'])); $me->assertEquals($userAgent, $headers['User-Agent']); // OAuth protocol specifies a strict number of // headers should be sent, in the correct order. // We'll validate that here. $pattern = '/OAuth oauth_consumer_key=".*?", oauth_nonce="[a-zA-Z0-9]+", oauth_signature_method="HMAC-SHA1", oauth_timestamp="\d{10}", oauth_version="1.0", oauth_token="temporarycredentialsidentifier", oauth_signature=".*?"/'; $matches = preg_match($pattern, $headers['Authorization']); $me->assertEquals(1, $matches, 'Asserting that the authorization header contains the correct expression.'); $me->assertSame($body, ['oauth_verifier' => 'myverifiercode']); return true; }))->once()->andReturn($response = m::mock('stdClass')); $response->shouldReceive('getBody') ->andReturn('oauth_token=tokencredentialsidentifier&oauth_token_secret=tokencredentialssecret'); $credentials = $server->setUserAgent($userAgent) ->getTokenCredentials($temporaryCredentials, 'temporarycredentialsidentifier', 'myverifiercode'); $this->assertInstanceOf('League\OAuth1\Client\Credentials\TokenCredentials', $credentials); $this->assertEquals('tokencredentialsidentifier', $credentials->getIdentifier()); $this->assertEquals('tokencredentialssecret', $credentials->getSecret()); } public function testGettingUserDetails() { $server = m::mock( 'League\OAuth1\Client\Tests\ServerStub[createHttpClient,protocolHeader]', [$this->getMockClientCredentials()] ); $temporaryCredentials = m::mock('League\OAuth1\Client\Credentials\TokenCredentials'); $temporaryCredentials->shouldReceive('getIdentifier')->andReturn('tokencredentialsidentifier'); $temporaryCredentials->shouldReceive('getSecret')->andReturn('tokencredentialssecret'); $server->shouldReceive('createHttpClient')->andReturn($client = m::mock('stdClass')); $me = $this; $client->shouldReceive('get')->with('http://www.example.com/user', m::on(function ($options) use ($me) { $headers = $options['headers']; $me->assertTrue(isset($headers['Authorization'])); // OAuth protocol specifies a strict number of // headers should be sent, in the correct order. // We'll validate that here. $pattern = '/OAuth oauth_consumer_key=".*?", oauth_nonce="[a-zA-Z0-9]+", oauth_signature_method="HMAC-SHA1", oauth_timestamp="\d{10}", oauth_version="1.0", oauth_token="tokencredentialsidentifier", oauth_signature=".*?"/'; $matches = preg_match($pattern, $headers['Authorization']); $me->assertEquals(1, $matches, 'Asserting that the authorization header contains the correct expression.'); return true; }))->once()->andReturn($response = m::mock(ResponseInterface::class)); $response->shouldReceive('getBody')->once()->andReturn(json_encode([ 'foo' => 'bar', 'id' => 123, 'contact_email' => 'baz@qux.com', 'username' => 'fred', ])); $user = $server->getUserDetails($temporaryCredentials); $this->assertInstanceOf('League\OAuth1\Client\Server\User', $user); $this->assertEquals('bar', $user->firstName); $this->assertEquals(123, $server->getUserUid($temporaryCredentials)); $this->assertEquals('baz@qux.com', $server->getUserEmail($temporaryCredentials)); $this->assertEquals('fred', $server->getUserScreenName($temporaryCredentials)); } public function testGettingHeaders() { $server = new ServerStub($this->getMockClientCredentials()); $tokenCredentials = m::mock('League\OAuth1\Client\Credentials\TokenCredentials'); $tokenCredentials->shouldReceive('getIdentifier')->andReturn('mock_identifier'); $tokenCredentials->shouldReceive('getSecret')->andReturn('mock_secret'); // OAuth protocol specifies a strict number of // headers should be sent, in the correct order. // We'll validate that here. $pattern = '/OAuth oauth_consumer_key=".*?", oauth_nonce="[a-zA-Z0-9]+", oauth_signature_method="HMAC-SHA1", oauth_timestamp="\d{10}", oauth_version="1.0", oauth_token="mock_identifier", oauth_signature=".*?"/'; // With a GET request $headers = $server->getHeaders($tokenCredentials, 'GET', 'http://example.com/'); $this->assertTrue(isset($headers['Authorization'])); $matches = preg_match($pattern, $headers['Authorization']); $this->assertEquals(1, $matches, 'Asserting that the authorization header contains the correct expression.'); // With a POST request $headers = $server->getHeaders($tokenCredentials, 'POST', 'http://example.com/', ['body' => 'params']); $this->assertTrue(isset($headers['Authorization'])); $matches = preg_match($pattern, $headers['Authorization']); $this->assertEquals(1, $matches, 'Asserting that the authorization header contains the correct expression.'); } protected function getMockClientCredentials() { return [ 'identifier' => 'myidentifier', 'secret' => 'mysecret', 'callback_uri' => 'http://app.dev/', ]; } } oauth1-client/tests/ClientCredentialsTest.php 0000644 00000001327 15007532371 0015336 0 ustar 00 <?php namespace League\OAuth1\Client\Tests; use League\OAuth1\Client\Credentials\ClientCredentials; use Mockery as m; use PHPUnit\Framework\TestCase; class ClientCredentialsTest extends TestCase { protected function tearDown(): void { m::close(); parent::tearDown(); } public function testManipulating() { $credentials = new ClientCredentials; $this->assertNull($credentials->getIdentifier()); $credentials->setIdentifier('foo'); $this->assertEquals('foo', $credentials->getIdentifier()); $this->assertNull($credentials->getSecret()); $credentials->setSecret('foo'); $this->assertEquals('foo', $credentials->getSecret()); } } oauth1-client/tests/test_rsa_publickey.pem 0000644 00000002143 15007532371 0014764 0 ustar 00 -----BEGIN CERTIFICATE----- MIIDEDCCAnmgAwIBAgIJAMhMVuHMz+EgMA0GCSqGSIb3DQEBBQUAMGQxCzAJBgNV BAYTAlVTMQswCQYDVQQIEwJUWDEPMA0GA1UEBxMGQXVzdGluMSEwHwYDVQQKExhJ bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxFDASBgNVBAMTC2V4YW1wbGUuY29tMB4X DTE2MTEwMjIxMDUzNVoXDTIxMTEwMTIxMDUzNVowZDELMAkGA1UEBhMCVVMxCzAJ BgNVBAgTAlRYMQ8wDQYDVQQHEwZBdXN0aW4xITAfBgNVBAoTGEludGVybmV0IFdp ZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAxMLZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcN AQEBBQADgY0AMIGJAoGBAMlJw8Kkcc9qTD2/ieHGL5wG6YKaXdakw0NoTcLGeR/o cAvOiLx9A/HddF8pKgsbpvu5Mb68OgsIvj1kLJjCueCCwUXTxskyoiFQeVmuSLJP lSF1MiFcQQV9vpeaJ4c22tr66vwTc9VqXg8R04ISYl6EEwmSIKzE04n+QJ5hIx1t AgMBAAGjgckwgcYwHQYDVR0OBBYEFLYKQbsK2oqRO83NbNXC2R6MkNcRMIGWBgNV HSMEgY4wgYuAFLYKQbsK2oqRO83NbNXC2R6MkNcRoWikZjBkMQswCQYDVQQGEwJV UzELMAkGA1UECBMCVFgxDzANBgNVBAcTBkF1c3RpbjEhMB8GA1UEChMYSW50ZXJu ZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDEwtleGFtcGxlLmNvbYIJAMhMVuHM z+EgMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEASwEwGQnRCqcNrb6k g6/xpeyHE9/ruTBIE4dtArQI0NosaERC3i4nRKbgfJfIuYEaYiga4dC51CQqKrbH YZ0dgYMzzp21OMQyKdz3E7csMJv5xxe5D2svdPzbBGU5+N80FYLx17f3UqsYFaAC X0/YTQsdlM5tHZOd1ZbQUrERqLs= -----END CERTIFICATE----- oauth1-client/CONDUCT.md 0000644 00000004146 15007532371 0010652 0 ustar 00 # Contributor Code of Conduct As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery * Personal attacks * Trolling or insulting/derogatory comments * Public or private harassment * Publishing other's private information, such as physical or electronic addresses, without explicit permission * Other unethical or unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community in a direct capacity. Personal views, beliefs and values of individuals do not necessarily reflect those of the organisation or affiliated individuals and organisations. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) oauth1-client/README.md 0000644 00000025305 15007532371 0010510 0 ustar 00 # OAuth 1.0 Client [](https://github.com/thephpleague/oauth1-client/releases) [](LICENSE.md) [](https://travis-ci.org/thephpleague/oauth1-client) [](https://scrutinizer-ci.com/g/thephpleague/oauth1-client/code-structure) [](https://scrutinizer-ci.com/g/thephpleague/oauth1-client) [](https://packagist.org/packages/thephpleague/oauth1-client) OAuth 1 Client is an OAuth [RFC 5849 standards-compliant](http://tools.ietf.org/html/rfc5849) library for authenticating against OAuth 1 servers. It has built in support for: - Bitbucket - Magento - Trello - Tumblr - Twitter - Uservoice - Xing Adding support for other providers is trivial. The library requires PHP 7.1+ and is PSR-2 compatible. ### Third-Party Providers If you would like to support other providers, please make them available as a Composer package, then link to them below. These providers allow integration with other providers not supported by `oauth1-client`. They may require an older version so please help them out with a pull request if you notice this. - [Intuit](https://packagist.org/packages/wheniwork/oauth1-intuit) - [500px](https://packagist.org/packages/mechant/oauth1-500px) - [Etsy](https://packagist.org/packages/y0lk/oauth1-etsy) - [Xero](https://packagist.org/packages/Invoiced/oauth1-xero) - [Garmin](https://packagist.org/packages/techgyani/garmin-wellness) - [Goodreads](https://packagist.org/packages/netgalley/oauth1-goodreads) #### Terminology (as per the RFC 5849 specification): client An HTTP client (per [RFC2616]) capable of making OAuth- authenticated requests (Section 3). server An HTTP server (per [RFC2616]) capable of accepting OAuth- authenticated requests (Section 3). protected resource An access-restricted resource that can be obtained from the server using an OAuth-authenticated request (Section 3). resource owner An entity capable of accessing and controlling protected resources by using credentials to authenticate with the server. credentials Credentials are a pair of a unique identifier and a matching shared secret. OAuth defines three classes of credentials: client, temporary, and token, used to identify and authenticate the client making the request, the authorization request, and the access grant, respectively. token A unique identifier issued by the server and used by the client to associate authenticated requests with the resource owner whose authorization is requested or has been obtained by the client. Tokens have a matching shared-secret that is used by the client to establish its ownership of the token, and its authority to represent the resource owner. The original community specification used a somewhat different terminology that maps to this specifications as follows (original community terms provided on left): Consumer: client Service Provider: server User: resource owner Consumer Key and Secret: client credentials Request Token and Secret: temporary credentials Access Token and Secret: token credentials ## Install Via Composer ```shell $ composer require league/oauth1-client ``` ## Usage ### Bitbucket ```php $server = new League\OAuth1\Client\Server\Bitbucket([ 'identifier' => 'your-identifier', 'secret' => 'your-secret', 'callback_uri' => "http://your-callback-uri/", ]); ``` ### Trello ```php $server = new League\OAuth1\Client\Server\Trello([ 'identifier' => 'your-identifier', 'secret' => 'your-secret', 'callback_uri' => 'http://your-callback-uri/', 'name' => 'your-application-name', // optional, defaults to null 'expiration' => 'your-application-expiration', // optional ('never', '1day', '2days'), defaults to '1day' 'scope' => 'your-application-scope' // optional ('read', 'read,write'), defaults to 'read' ]); ``` ### Tumblr ```php $server = new League\OAuth1\Client\Server\Tumblr([ 'identifier' => 'your-identifier', 'secret' => 'your-secret', 'callback_uri' => "http://your-callback-uri/", ]); ``` ### Twitter ```php $server = new League\OAuth1\Client\Server\Twitter([ 'identifier' => 'your-identifier', 'secret' => 'your-secret', 'callback_uri' => "http://your-callback-uri/", 'scope' => 'your-application-scope' // optional ('read', 'write'), empty by default ]); ``` ### Xing ```php $server = new League\OAuth1\Client\Server\Xing([ 'identifier' => 'your-consumer-key', 'secret' => 'your-consumer-secret', 'callback_uri' => "http://your-callback-uri/", ]); ``` ### Showing a Login Button To begin, it's advisable that you include a login button on your website. Most servers (Twitter, Tumblr etc) have resources available for making buttons that are familiar to users. Some servers actually require you use their buttons as part of their terms. ```html <a href="authenticate.php">Login With Twitter</a> ``` ### Retrieving Temporary Credentials The first step to authenticating with OAuth 1 is to retrieve temporary credentials. These have been referred to as **request tokens** in earlier versions of OAuth 1. To do this, we'll retrieve and store temporary credentials in the session, and redirect the user to the server: ```php // Retrieve temporary credentials $temporaryCredentials = $server->getTemporaryCredentials(); // Store credentials in the session, we'll need them later $_SESSION['temporary_credentials'] = serialize($temporaryCredentials); session_write_close(); // Second part of OAuth 1.0 authentication is to redirect the // resource owner to the login screen on the server. $server->authorize($temporaryCredentials); ``` The user will be redirected to the familiar login screen on the server, where they will login to their account and authorise your app to access their data. ### Retrieving Token Credentials Once the user has authenticated (or denied) your application, they will be redirected to the `callback_uri` which you specified when creating the server. > Note, some servers (such as Twitter) require that the callback URI you specify when authenticating matches what you registered with their app. This is to stop a potential third party impersonating you. This is actually part of the protocol however some servers choose to ignore this. > > Because of this, we actually require you specify a callback URI for all servers, regardless of whether the server requires it or not. This is good practice. You'll need to handle when the user is redirected back. This will involve retrieving token credentials, which you may then use to make calls to the server on behalf of the user. These have been referred to as **access tokens** in earlier versions of OAuth 1. ```php if (isset($_GET['oauth_token']) && isset($_GET['oauth_verifier'])) { // Retrieve the temporary credentials we saved before $temporaryCredentials = unserialize($_SESSION['temporary_credentials']); // We will now retrieve token credentials from the server $tokenCredentials = $server->getTokenCredentials($temporaryCredentials, $_GET['oauth_token'], $_GET['oauth_verifier']); } ``` Now, you may choose to do what you need with the token credentials. You may store them in a database, in the session, or use them as one-off and then forget about them. All credentials, (`client credentials`, `temporary credentials` and `token credentials`) all implement `League\OAuth1\Client\Credentials\CredentialsInterface` and have two sets of setters and getters exposed: ```php var_dump($tokenCredentials->getIdentifier()); var_dump($tokenCredentials->getSecret()); ``` In earlier versions of OAuth 1, the token credentials identifier and token credentials secret were referred to as **access token** and **access token secret**. Don't be scared by the new terminology here - they are the same. This package is using the exact terminology in the RFC 5849 OAuth 1 standard. > Twitter will send back an error message in the `denied` query string parameter, allowing you to provide feedback. Some servers do not send back an error message, but rather do not provide the successful `oauth_token` and `oauth_verifier` parameters. ### Accessing User Information Now you have token credentials stored somewhere, you may use them to make calls against the server, as an authenticated user. While this package is not intended to be a wrapper for every server's API, it does include basic methods that you may use to retrieve limited information. An example of where this may be useful is if you are using social logins, you only need limited information to confirm who the user is. The four exposed methods are: ```php // User is an instance of League\OAuth1\Client\Server\User $user = $server->getUserDetails($tokenCredentials); // UID is a string / integer unique representation of the user $uid = $server->getUserUid($tokenCredentials); // Email is either a string or null (as some providers do not supply this data) $email = $server->getUserEmail($tokenCredentials); // Screen name is also known as a username (Twitter handle etc) $screenName = $server->getUserScreenName($tokenCredentials); ``` > `League\OAuth1\Client\Server\User` exposes a number of default public properties and also stores any additional data in an extra array - `$user->extra`. You may also iterate over a user's properties as if it was an array, `foreach ($user as $key => $value)`. ## Examples Examples may be found under the [resources/examples](https://github.com/thephpleague/oauth1-client/tree/master/resources/examples) directory, which take the usage instructions here and go into a bit more depth. They are working examples that would only you substitute in your client credentials to have working. ## Testing ``` bash $ phpunit ``` ## Contributing Please see [CONTRIBUTING](https://github.com/thephpleague/oauth1-client/blob/master/CONTRIBUTING.md) for details. ## Credits - [Ben Corlett](https://github.com/bencorlett) - [Steven Maguire](https://github.com/stevenmaguire) - [All Contributors](https://github.com/thephpleague/oauth1-client/contributors) ## License The MIT License (MIT). Please see [License File](https://github.com/thephpleague/oauth1-client/blob/master/LICENSE) for more information. oauth1-client/phpstan.neon 0000644 00000000662 15007532371 0011566 0 ustar 00 parameters: checkMissingIterableValueType: false checkGenericClassInNonGenericObjectType: false ignoreErrors: - message: '#has unknown class OpenSSLAsymmetricKey as its type#' path: src/Credentials/RsaClientCredentials.php - message: '#invalid type OpenSSLAsymmetricKey#' path: src/Credentials/RsaClientCredentials.php reportUnmatchedIgnoredErrors: false oauth1-client/phpunit.php 0000644 00000000123 15007532371 0011420 0 ustar 00 <?php include __DIR__ . '/vendor/autoload.php'; date_default_timezone_set('UTC'); oauth1-client/.gitignore 0000644 00000000134 15007532371 0011212 0 ustar 00 /build /vendor /composer.lock /coverage.xml /.phpunit.result.cache /.php_cs.cache .DS_Store oauth1-client/resources/examples/twitter.php 0000644 00000006277 15007532371 0015303 0 ustar 00 <?php require_once __DIR__.'/../../vendor/autoload.php'; // Create server $server = new League\OAuth1\Client\Server\Twitter(array( 'identifier' => 'your-identifier', 'secret' => 'your-secret', 'callback_uri' => "http://your-callback-uri/", )); // Start session session_start(); // Step 4 if (isset($_GET['user'])) { // Check somebody hasn't manually entered this URL in, // by checking that we have the token credentials in // the session. if ( ! isset($_SESSION['token_credentials'])) { echo 'No token credentials.'; exit(1); } // Retrieve our token credentials. From here, it's play time! $tokenCredentials = unserialize($_SESSION['token_credentials']); // // Below is an example of retrieving the identifier & secret // // (formally known as access token key & secret in earlier // // OAuth 1.0 specs). // $identifier = $tokenCredentials->getIdentifier(); // $secret = $tokenCredentials->getSecret(); // Some OAuth clients try to act as an API wrapper for // the server and it's API. We don't. This is what you // get - the ability to access basic information. If // you want to get fancy, you should be grabbing a // package for interacting with the APIs, by using // the identifier & secret that this package was // designed to retrieve for you. But, for fun, // here's basic user information. $user = $server->getUserDetails($tokenCredentials); var_dump($user); // Step 3 } elseif (isset($_GET['oauth_token']) && isset($_GET['oauth_verifier'])) { // Retrieve the temporary credentials from step 2 $temporaryCredentials = unserialize($_SESSION['temporary_credentials']); // Third and final part to OAuth 1.0 authentication is to retrieve token // credentials (formally known as access tokens in earlier OAuth 1.0 // specs). $tokenCredentials = $server->getTokenCredentials($temporaryCredentials, $_GET['oauth_token'], $_GET['oauth_verifier']); // Now, we'll store the token credentials and discard the temporary // ones - they're irrelevant at this stage. unset($_SESSION['temporary_credentials']); $_SESSION['token_credentials'] = serialize($tokenCredentials); session_write_close(); // Redirect to the user page header("Location: http://{$_SERVER['HTTP_HOST']}/?user=user"); exit; // Step 2.5 - denied request to authorize client } elseif (isset($_GET['denied'])) { echo 'Hey! You denied the client access to your Twitter account! If you did this by mistake, you should <a href="?go=go">try again</a>.'; // Step 2 } elseif (isset($_GET['go'])) { // First part of OAuth 1.0 authentication is retrieving temporary credentials. // These identify you as a client to the server. $temporaryCredentials = $server->getTemporaryCredentials(); // Store the credentials in the session. $_SESSION['temporary_credentials'] = serialize($temporaryCredentials); session_write_close(); // Second part of OAuth 1.0 authentication is to redirect the // resource owner to the login screen on the server. $server->authorize($temporaryCredentials); // Step 1 } else { // Display link to start process echo '<a href="?go=go">Login</a>'; } oauth1-client/resources/examples/xing.php 0000644 00000006271 15007532371 0014540 0 ustar 00 <?php require_once __DIR__.'/../../vendor/autoload.php'; // Create server $server = new League\OAuth1\Client\Server\Xing(array( 'identifier' => 'your-identifier', 'secret' => 'your-secret', 'callback_uri' => "http://your-callback-uri/", )); // Start session session_start(); // Step 4 if (isset($_GET['user'])) { // Check somebody hasn't manually entered this URL in, // by checking that we have the token credentials in // the session. if ( ! isset($_SESSION['token_credentials'])) { echo 'No token credentials.'; exit(1); } // Retrieve our token credentials. From here, it's play time! $tokenCredentials = unserialize($_SESSION['token_credentials']); // // Below is an example of retrieving the identifier & secret // // (formally known as access token key & secret in earlier // // OAuth 1.0 specs). // $identifier = $tokenCredentials->getIdentifier(); // $secret = $tokenCredentials->getSecret(); // Some OAuth clients try to act as an API wrapper for // the server and it's API. We don't. This is what you // get - the ability to access basic information. If // you want to get fancy, you should be grabbing a // package for interacting with the APIs, by using // the identifier & secret that this package was // designed to retrieve for you. But, for fun, // here's basic user information. $user = $server->getUserDetails($tokenCredentials); var_dump($user); // Step 3 } elseif (isset($_GET['oauth_token']) && isset($_GET['oauth_verifier'])) { // Retrieve the temporary credentials from step 2 $temporaryCredentials = unserialize($_SESSION['temporary_credentials']); // Third and final part to OAuth 1.0 authentication is to retrieve token // credentials (formally known as access tokens in earlier OAuth 1.0 // specs). $tokenCredentials = $server->getTokenCredentials($temporaryCredentials, $_GET['oauth_token'], $_GET['oauth_verifier']); // Now, we'll store the token credentials and discard the temporary // ones - they're irrelevant at this stage. unset($_SESSION['temporary_credentials']); $_SESSION['token_credentials'] = serialize($tokenCredentials); session_write_close(); // Redirect to the user page header("Location: http://{$_SERVER['HTTP_HOST']}/?user=user"); exit; // Step 2.5 - denied request to authorize client } elseif (isset($_GET['denied'])) { echo 'Hey! You denied the client access to your Xing account! If you did this by mistake, you should <a href="?go=go">try again</a>.'; // Step 2 } elseif (isset($_GET['go'])) { // First part of OAuth 1.0 authentication is retrieving temporary credentials. // These identify you as a client to the server. $temporaryCredentials = $server->getTemporaryCredentials(); // Store the credentials in the session. $_SESSION['temporary_credentials'] = serialize($temporaryCredentials); session_write_close(); // Second part of OAuth 1.0 authentication is to redirect the // resource owner to the login screen on the server. $server->authorize($temporaryCredentials); // Step 1 } else { // Display link to start process echo '<a href="?go=go">Login</a>'; } oauth1-client/resources/examples/tumblr.php 0000644 00000005732 15007532371 0015101 0 ustar 00 <?php require_once __DIR__.'/../../vendor/autoload.php'; // Create server $server = new League\OAuth1\Client\Server\Tumblr(array( 'identifier' => 'your-identifier', 'secret' => 'your-secret', 'callback_uri' => "http://your-callback-uri/", )); // Start session session_start(); // Step 4 if (isset($_GET['user'])) { // Check somebody hasn't manually entered this URL in, // by checking that we have the token credentials in // the session. if ( ! isset($_SESSION['token_credentials'])) { echo 'No token credentials.'; exit(1); } // Retrieve our token credentials. From here, it's play time! $tokenCredentials = unserialize($_SESSION['token_credentials']); // // Below is an example of retrieving the identifier & secret // // (formally known as access token key & secret in earlier // // OAuth 1.0 specs). // $identifier = $tokenCredentials->getIdentifier(); // $secret = $tokenCredentials->getSecret(); // Some OAuth clients try to act as an API wrapper for // the server and it's API. We don't. This is what you // get - the ability to access basic information. If // you want to get fancy, you should be grabbing a // package for interacting with the APIs, by using // the identifier & secret that this package was // designed to retrieve for you. But, for fun, // here's basic user information. $user = $server->getUserDetails($tokenCredentials); var_dump($user); // Step 3 } elseif (isset($_GET['oauth_token']) && isset($_GET['oauth_verifier'])) { // Retrieve the temporary credentials from step 2 $temporaryCredentials = unserialize($_SESSION['temporary_credentials']); // Third and final part to OAuth 1.0 authentication is to retrieve token // credentials (formally known as access tokens in earlier OAuth 1.0 // specs). $tokenCredentials = $server->getTokenCredentials($temporaryCredentials, $_GET['oauth_token'], $_GET['oauth_verifier']); // Now, we'll store the token credentials and discard the temporary // ones - they're irrelevant at this stage. unset($_SESSION['temporary_credentials']); $_SESSION['token_credentials'] = serialize($tokenCredentials); session_write_close(); // Redirect to the user page header("Location: http://{$_SERVER['HTTP_HOST']}/?user=user"); exit; // Step 2 } elseif (isset($_GET['go'])) { // First part of OAuth 1.0 authentication is retrieving temporary credentials. // These identify you as a client to the server. $temporaryCredentials = $server->getTemporaryCredentials(); // Store the credentials in the session. $_SESSION['temporary_credentials'] = serialize($temporaryCredentials); session_write_close(); // Second part of OAuth 1.0 authentication is to redirect the // resource owner to the login screen on the server. $server->authorize($temporaryCredentials); // Step 1 } else { // Display link to start process echo '<a href="?go=go">Login</a>'; } oauth1-client/src/Signature/PlainTextSignature.php 0000644 00000000575 15007532371 0016266 0 ustar 00 <?php namespace League\OAuth1\Client\Signature; class PlainTextSignature extends Signature implements SignatureInterface { /** * @inheritDoc */ public function method() { return 'PLAINTEXT'; } /** * @inheritDoc */ public function sign($uri, array $parameters = [], $method = 'POST') { return $this->key(); } } oauth1-client/src/Signature/SignatureInterface.php 0000644 00000002233 15007532371 0016247 0 ustar 00 <?php namespace League\OAuth1\Client\Signature; use League\OAuth1\Client\Credentials\ClientCredentialsInterface; use League\OAuth1\Client\Credentials\CredentialsInterface; interface SignatureInterface { /** * Create a new signature instance. * * @param ClientCredentialsInterface $clientCredentials */ public function __construct(ClientCredentialsInterface $clientCredentials); /** * Set the credentials used in the signature. These can be temporary * credentials when getting token credentials during the OAuth * authentication process, or token credentials when querying * the API. * * @param CredentialsInterface $credentials * * @return void */ public function setCredentials(CredentialsInterface $credentials); /** * Get the OAuth signature method. * * @return string */ public function method(); /** * Sign the given request for the client. * * @param string $uri * @param array $parameters * @param string $method * * @return string */ public function sign($uri, array $parameters = [], $method = 'POST'); } oauth1-client/src/Signature/HmacSha1Signature.php 0000644 00000001415 15007532371 0015735 0 ustar 00 <?php namespace League\OAuth1\Client\Signature; class HmacSha1Signature extends Signature implements SignatureInterface { use EncodesUrl; /** * @inheritDoc */ public function method() { return 'HMAC-SHA1'; } /** * @inheritDoc */ public function sign($uri, array $parameters = [], $method = 'POST') { $url = $this->createUrl($uri); $baseString = $this->baseString($url, $method, $parameters); return base64_encode($this->hash($baseString)); } /** * Hashes a string with the signature's key. * * @param string $string * * @return string */ protected function hash($string) { return hash_hmac('sha1', $string, $this->key(), true); } } oauth1-client/src/Signature/Signature.php 0000644 00000002221 15007532371 0014423 0 ustar 00 <?php namespace League\OAuth1\Client\Signature; use League\OAuth1\Client\Credentials\ClientCredentialsInterface; use League\OAuth1\Client\Credentials\CredentialsInterface; abstract class Signature implements SignatureInterface { /** * The client credentials. * * @var ClientCredentialsInterface */ protected $clientCredentials; /** * The (temporary or token) credentials. * * @var CredentialsInterface */ protected $credentials; /** * @inheritDoc */ public function __construct(ClientCredentialsInterface $clientCredentials) { $this->clientCredentials = $clientCredentials; } /** * @inheritDoc */ public function setCredentials(CredentialsInterface $credentials) { $this->credentials = $credentials; } /** * Generate a signing key. * * @return string */ protected function key() { $key = rawurlencode($this->clientCredentials->getSecret()) . '&'; if ($this->credentials !== null) { $key .= rawurlencode($this->credentials->getSecret()); } return $key; } } oauth1-client/src/Signature/RsaSha1Signature.php 0000644 00000001464 15007532371 0015616 0 ustar 00 <?php namespace League\OAuth1\Client\Signature; use League\OAuth1\Client\Credentials\RsaClientCredentials; class RsaSha1Signature extends Signature implements SignatureInterface { use EncodesUrl; /** * @inheritDoc */ public function method() { return 'RSA-SHA1'; } /** * @inheritDoc */ public function sign($uri, array $parameters = [], $method = 'POST') { $url = $this->createUrl($uri); $baseString = $this->baseString($url, $method, $parameters); /** @var RsaClientCredentials $clientCredentials */ $clientCredentials = $this->clientCredentials; $privateKey = $clientCredentials->getRsaPrivateKey(); openssl_sign($baseString, $signature, $privateKey); return base64_encode($signature); } } oauth1-client/src/Signature/EncodesUrl.php 0000644 00000006235 15007532371 0014536 0 ustar 00 <?php namespace League\OAuth1\Client\Signature; use GuzzleHttp\Psr7; use GuzzleHttp\Psr7\Uri; use Psr\Http\Message\UriInterface; trait EncodesUrl { /** * Create a Guzzle url for the given URI. * * @param string $uri * * @return UriInterface */ protected function createUrl($uri) { return Psr7\Utils::uriFor($uri); } /** * Generate a base string for a RSA-SHA1 signature * based on the given a url, method, and any parameters. * * @param UriInterface $url * @param string $method * @param array $parameters * * @return string */ protected function baseString(UriInterface $url, $method = 'POST', array $parameters = []) { $baseString = rawurlencode($method) . '&'; $schemeHostPath = Uri::fromParts([ 'scheme' => $url->getScheme(), 'host' => $url->getHost(), 'port' => $url->getPort(), 'path' => $url->getPath(), ]); $baseString .= rawurlencode($schemeHostPath) . '&'; parse_str($url->getQuery(), $query); $data = array_merge($query, $parameters); // normalize data key/values $data = $this->normalizeArray($data); ksort($data); $baseString .= $this->queryStringFromData($data); return $baseString; } /** * Return a copy of the given array with all keys and values rawurlencoded. * * @param array $array Array to normalize * * @return array Normalized array */ protected function normalizeArray(array $array = []) { $normalizedArray = []; foreach ($array as $key => $value) { $key = rawurlencode(rawurldecode($key)); if (is_array($value)) { $normalizedArray[$key] = $this->normalizeArray($value); } else { $normalizedArray[$key] = rawurlencode(rawurldecode($value)); } } return $normalizedArray; } /** * Creates an array of rawurlencoded strings out of each array key/value pair * Handles multi-dimensional arrays recursively. * * @param array $data Array of parameters to convert. * @param array|null $queryParams Array to extend. False by default. * @param string $prevKey Optional Array key to append * * @return string rawurlencoded string version of data */ protected function queryStringFromData($data, $queryParams = null, $prevKey = '') { if ($initial = (null === $queryParams)) { $queryParams = []; } foreach ($data as $key => $value) { if ($prevKey) { $key = $prevKey . '[' . $key . ']'; // Handle multi-dimensional array } if (is_array($value)) { $queryParams = $this->queryStringFromData($value, $queryParams, $key); } else { $queryParams[] = rawurlencode($key . '=' . $value); // join with equals sign } } if ($initial) { return implode('%26', $queryParams); // join with ampersand } return $queryParams; } } oauth1-client/src/Server/Twitter.php 0000644 00000010046 15007532371 0013435 0 ustar 00 <?php namespace League\OAuth1\Client\Server; use League\OAuth1\Client\Credentials\TokenCredentials; class Twitter extends Server { /** * Application scope. * * @var ?string */ protected $applicationScope = null; /** * @inheritDoc */ public function __construct($clientCredentials, SignatureInterface $signature = null) { parent::__construct($clientCredentials, $signature); if (is_array($clientCredentials)) { $this->parseConfiguration($clientCredentials); } } /** * Set the application scope. * * @param ?string $applicationScope * * @return Twitter */ public function setApplicationScope($applicationScope) { $this->applicationScope = $applicationScope; return $this; } /** * Get application scope. * * @return ?string */ public function getApplicationScope() { return $this->applicationScope; } /** * @inheritDoc */ public function urlTemporaryCredentials() { $url = 'https://api.twitter.com/oauth/request_token'; $queryParams = $this->temporaryCredentialsQueryParameters(); return empty($queryParams) ? $url : $url . '?' . $queryParams; } /** * @inheritDoc */ public function urlAuthorization() { return 'https://api.twitter.com/oauth/authenticate'; } /** * @inheritDoc */ public function urlTokenCredentials() { return 'https://api.twitter.com/oauth/access_token'; } /** * @inheritDoc */ public function urlUserDetails() { return 'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true'; } /** * @inheritDoc */ public function userDetails($data, TokenCredentials $tokenCredentials) { $user = new User(); $user->uid = $data['id_str']; $user->nickname = $data['screen_name']; $user->name = $data['name']; $user->location = $data['location']; $user->description = $data['description']; $user->imageUrl = $data['profile_image_url']; if (isset($data['email'])) { $user->email = $data['email']; } $used = ['id', 'screen_name', 'name', 'location', 'description', 'profile_image_url', 'email']; foreach ($data as $key => $value) { if (strpos($key, 'url') !== false) { if ( ! in_array($key, $used)) { $used[] = $key; } $user->urls[$key] = $value; } } // Save all extra data $user->extra = array_diff_key($data, array_flip($used)); return $user; } /** * @inheritDoc */ public function userUid($data, TokenCredentials $tokenCredentials) { return $data['id']; } /** * @inheritDoc */ public function userEmail($data, TokenCredentials $tokenCredentials) { return null; } /** * @inheritDoc */ public function userScreenName($data, TokenCredentials $tokenCredentials) { return $data['name']; } /** * Query parameters for a Twitter OAuth request to get temporary credentials. * * @return string */ protected function temporaryCredentialsQueryParameters() { $queryParams = []; if ($scope = $this->getApplicationScope()) { $queryParams['x_auth_access_type'] = $scope; } return http_build_query($queryParams); } /** * Parse configuration array to set attributes. * * @param array $configuration * * @return void */ private function parseConfiguration(array $configuration = []) { $configToPropertyMap = [ 'scope' => 'applicationScope', ]; foreach ($configToPropertyMap as $config => $property) { if (isset($configuration[$config])) { $this->$property = $configuration[$config]; } } } } oauth1-client/src/Server/Xing.php 0000644 00000004443 15007532371 0012704 0 ustar 00 <?php namespace League\OAuth1\Client\Server; use League\OAuth1\Client\Credentials\TokenCredentials; class Xing extends Server { const XING_API_ENDPOINT = 'https://api.xing.com'; /** * @inheritDoc */ public function urlTemporaryCredentials() { return self::XING_API_ENDPOINT . '/v1/request_token'; } /** * @inheritDoc */ public function urlAuthorization() { return self::XING_API_ENDPOINT . '/v1/authorize'; } /** * @inheritDoc */ public function urlTokenCredentials() { return self::XING_API_ENDPOINT . '/v1/access_token'; } /** * @inheritDoc */ public function urlUserDetails() { return self::XING_API_ENDPOINT . '/v1/users/me'; } /** * @inheritDoc */ public function userDetails($data, TokenCredentials $tokenCredentials) { if ( ! isset($data['users'][0])) { throw new \Exception('Not possible to get user info'); } $data = $data['users'][0]; $user = new User(); $user->uid = $data['id']; $user->nickname = $data['display_name']; $user->name = $data['display_name']; $user->firstName = $data['first_name']; $user->lastName = $data['last_name']; $user->location = $data['private_address']['country']; if ($user->location == '') { $user->location = $data['business_address']['country']; } $user->description = $data['employment_status']; $user->imageUrl = $data['photo_urls']['maxi_thumb']; $user->email = $data['active_email']; $user->urls['permalink'] = $data['permalink']; return $user; } /** * @inheritDoc */ public function userUid($data, TokenCredentials $tokenCredentials) { $data = $data['users'][0]; return $data['id']; } /** * @inheritDoc */ public function userEmail($data, TokenCredentials $tokenCredentials) { $data = $data['users'][0]; return $data['active_email']; } /** * @inheritDoc */ public function userScreenName($data, TokenCredentials $tokenCredentials) { $data = $data['users'][0]; return $data['display_name']; } } oauth1-client/src/Server/Tumblr.php 0000644 00000004323 15007532371 0013241 0 ustar 00 <?php namespace League\OAuth1\Client\Server; use League\OAuth1\Client\Credentials\TokenCredentials; use LogicException; use RuntimeException; class Tumblr extends Server { /** * @inheritDoc */ public function urlTemporaryCredentials() { return 'https://www.tumblr.com/oauth/request_token'; } /** * @inheritDoc */ public function urlAuthorization() { return 'https://www.tumblr.com/oauth/authorize'; } /** * @inheritDoc */ public function urlTokenCredentials() { return 'https://www.tumblr.com/oauth/access_token'; } /** * @inheritDoc */ public function urlUserDetails() { return 'https://api.tumblr.com/v2/user/info'; } /** * @inheritDoc */ public function userDetails($data, TokenCredentials $tokenCredentials) { // If the API has broke, return nothing if ( ! isset($data['response']['user']) || ! is_array($data['response']['user'])) { throw new LogicException('Not possible to get user info'); } $data = $data['response']['user']; $user = new User(); $user->nickname = $data['name']; // Save all extra data $used = ['name']; $user->extra = array_diff_key($data, array_flip($used)); return $user; } /** * @inheritDoc */ public function userUid($data, TokenCredentials $tokenCredentials) { if ( ! isset($data['response']['user']) || ! is_array($data['response']['user'])) { throw new LogicException('Not possible to get user UUID'); } $data = $data['response']['user']; return $data['name']; } /** * @inheritDoc */ public function userEmail($data, TokenCredentials $tokenCredentials) { return null; } /** * @inheritDoc */ public function userScreenName($data, TokenCredentials $tokenCredentials) { if ( ! isset($data['response']['user']) || ! is_array($data['response']['user'])) { throw new LogicException('Not possible to get user screen name'); } $data = $data['response']['user']; return $data['name']; } } oauth1-client/src/Server/Bitbucket.php 0000644 00000004124 15007532371 0013707 0 ustar 00 <?php namespace League\OAuth1\Client\Server; use League\OAuth1\Client\Credentials\TokenCredentials; class Bitbucket extends Server { /** * @inheritDoc */ public function urlTemporaryCredentials() { return 'https://bitbucket.org/api/1.0/oauth/request_token'; } /** * @inheritDoc */ public function urlAuthorization() { return 'https://bitbucket.org/api/1.0/oauth/authenticate'; } /** * @inheritDoc */ public function urlTokenCredentials() { return 'https://bitbucket.org/api/1.0/oauth/access_token'; } /** * @inheritDoc */ public function urlUserDetails() { return 'https://bitbucket.org/api/1.0/user'; } /** * @inheritDoc */ public function userDetails($data, TokenCredentials $tokenCredentials) { $user = new User(); $user->uid = $data['user']['username']; $user->nickname = $data['user']['username']; $user->name = $data['user']['display_name']; $user->firstName = $data['user']['first_name']; $user->lastName = $data['user']['last_name']; $user->imageUrl = $data['user']['avatar']; $used = ['username', 'display_name', 'avatar']; foreach ($data as $key => $value) { if (strpos($key, 'url') !== false) { if ( ! in_array($key, $used)) { $used[] = $key; } $user->urls[$key] = $value; } } // Save all extra data $user->extra = array_diff_key($data, array_flip($used)); return $user; } /** * @inheritDoc */ public function userUid($data, TokenCredentials $tokenCredentials) { return $data['user']['username']; } /** * @inheritDoc */ public function userEmail($data, TokenCredentials $tokenCredentials) { return null; } /** * @inheritDoc */ public function userScreenName($data, TokenCredentials $tokenCredentials) { return $data['user']['display_name']; } } oauth1-client/src/Server/User.php 0000644 00000003762 15007532371 0012720 0 ustar 00 <?php namespace League\OAuth1\Client\Server; use ArrayIterator; class User implements \IteratorAggregate { /** * The user's unique ID. * * @var mixed */ public $uid; /** * The user's nickname (screen name, username etc). * * @var mixed */ public $nickname; /** * The user's name. * * @var mixed */ public $name; /** * The user's first name. * * @var string */ public $firstName; /** * The user's last name. * * @var string */ public $lastName; /** * The user's email. * * @var string */ public $email; /** * The user's location. * * @var string|array */ public $location; /** * The user's description. * * @var string */ public $description; /** * The user's image URL. * * @var string */ public $imageUrl; /** * The users' URLs. * * @var string|array */ public $urls = []; /** * Any extra data. * * @var array */ public $extra = []; /** * Set a property on the user. * * @param string $key * @param mixed $value * * @return void */ public function __set($key, $value) { if (isset($this->{$key})) { $this->{$key} = $value; } } /** * Tells if a property is set. * * @param string $key * * @return bool */ public function __isset($key) { return isset($this->{$key}); } /** * Get a property from the user. * * @param string $key * * @return mixed */ public function __get($key) { if (isset($this->{$key})) { return $this->{$key}; } } /** * @inheritDoc */ #[\ReturnTypeWillChange] public function getIterator() { return new ArrayIterator(get_object_vars($this)); } } oauth1-client/src/Server/Server.php 0000644 00000051755 15007532371 0013255 0 ustar 00 <?php namespace League\OAuth1\Client\Server; use GuzzleHttp\Client as GuzzleHttpClient; use GuzzleHttp\Exception\BadResponseException; use League\OAuth1\Client\Credentials\ClientCredentials; use League\OAuth1\Client\Credentials\ClientCredentialsInterface; use League\OAuth1\Client\Credentials\CredentialsException; use League\OAuth1\Client\Credentials\CredentialsInterface; use League\OAuth1\Client\Credentials\RsaClientCredentials; use League\OAuth1\Client\Credentials\TemporaryCredentials; use League\OAuth1\Client\Credentials\TokenCredentials; use League\OAuth1\Client\Signature\HmacSha1Signature; use League\OAuth1\Client\Signature\RsaSha1Signature; use League\OAuth1\Client\Signature\SignatureInterface; use SimpleXMLElement; use Throwable; abstract class Server { /** * Client credentials. * * @var ClientCredentialsInterface */ protected $clientCredentials; /** * Signature. * * @var SignatureInterface */ protected $signature; /** * The response type for data returned from API calls. * * @var string */ protected $responseType = 'json'; /** * Cached user details response. * * @var array|SimpleXMLElement */ protected $cachedUserDetailsResponse; /** * Optional user agent. * * @var string */ protected $userAgent; /** * Create a new server instance. * * @param ClientCredentialsInterface|array $clientCredentials * @param SignatureInterface $signature */ public function __construct($clientCredentials, SignatureInterface $signature = null) { // Pass through an array or client credentials, we don't care if (is_array($clientCredentials)) { $clientCredentials = $this->createClientCredentials($clientCredentials); } elseif ( ! $clientCredentials instanceof ClientCredentialsInterface) { throw new \InvalidArgumentException('Client credentials must be an array or valid object.'); } $this->clientCredentials = $clientCredentials; if ( ! $signature && $clientCredentials instanceof RsaClientCredentials) { $signature = new RsaSha1Signature($clientCredentials); } $this->signature = $signature ?: new HmacSha1Signature($clientCredentials); } /** * Gets temporary credentials by performing a request to * the server. * * @return TemporaryCredentials * * @throws CredentialsException */ public function getTemporaryCredentials() { $uri = $this->urlTemporaryCredentials(); $client = $this->createHttpClient(); $header = $this->temporaryCredentialsProtocolHeader($uri); $authorizationHeader = ['Authorization' => $header]; $headers = $this->buildHttpClientHeaders($authorizationHeader); try { $response = $client->post($uri, [ 'headers' => $headers, ]); return $this->createTemporaryCredentials((string) $response->getBody()); } catch (BadResponseException $e) { $this->handleTemporaryCredentialsBadResponse($e); } throw new CredentialsException('Failed to get temporary credentials'); } /** * Get the authorization URL by passing in the temporary credentials * identifier or an object instance. * * @param TemporaryCredentials|string $temporaryIdentifier * @param array $options * * @return string */ public function getAuthorizationUrl($temporaryIdentifier, array $options = []) { // Somebody can pass through an instance of temporary // credentials and we'll extract the identifier from there. if ($temporaryIdentifier instanceof TemporaryCredentials) { $temporaryIdentifier = $temporaryIdentifier->getIdentifier(); } $parameters = array_merge($options, ['oauth_token' => $temporaryIdentifier]); $url = $this->urlAuthorization(); $queryString = http_build_query($parameters); return $this->buildUrl($url, $queryString); } /** * Redirect the client to the authorization URL. * * @param TemporaryCredentials|string $temporaryIdentifier * * @return void */ public function authorize($temporaryIdentifier) { $url = $this->getAuthorizationUrl($temporaryIdentifier); header('Location: ' . $url); } /** * Retrieves token credentials by passing in the temporary credentials, * the temporary credentials identifier as passed back by the server * and finally the verifier code. * * @param TemporaryCredentials $temporaryCredentials * @param string $temporaryIdentifier * @param string $verifier * * @return TokenCredentials * * @throws CredentialsException */ public function getTokenCredentials(TemporaryCredentials $temporaryCredentials, $temporaryIdentifier, $verifier) { if ($temporaryIdentifier !== $temporaryCredentials->getIdentifier()) { throw new \InvalidArgumentException( 'Temporary identifier passed back by server does not match that of stored temporary credentials. Potential man-in-the-middle.' ); } $uri = $this->urlTokenCredentials(); $bodyParameters = ['oauth_verifier' => $verifier]; $client = $this->createHttpClient(); $headers = $this->getHeaders($temporaryCredentials, 'POST', $uri, $bodyParameters); try { $response = $client->post($uri, [ 'headers' => $headers, 'form_params' => $bodyParameters, ]); return $this->createTokenCredentials((string) $response->getBody()); } catch (BadResponseException $e) { $this->handleTokenCredentialsBadResponse($e); } throw new CredentialsException('Failed to get token credentials.'); } /** * Get user details by providing valid token credentials. * * @param TokenCredentials $tokenCredentials * @param bool $force * * @return \League\OAuth1\Client\Server\User */ public function getUserDetails(TokenCredentials $tokenCredentials, $force = false) { $data = $this->fetchUserDetails($tokenCredentials, $force); return $this->userDetails($data, $tokenCredentials); } /** * Get the user's unique identifier (primary key). * * @param TokenCredentials $tokenCredentials * @param bool $force * * @return string|int */ public function getUserUid(TokenCredentials $tokenCredentials, $force = false) { $data = $this->fetchUserDetails($tokenCredentials, $force); return $this->userUid($data, $tokenCredentials); } /** * Get the user's email, if available. * * @param TokenCredentials $tokenCredentials * @param bool $force * * @return string|null */ public function getUserEmail(TokenCredentials $tokenCredentials, $force = false) { $data = $this->fetchUserDetails($tokenCredentials, $force); return $this->userEmail($data, $tokenCredentials); } /** * Get the user's screen name (username), if available. * * @param TokenCredentials $tokenCredentials * @param bool $force * * @return string */ public function getUserScreenName(TokenCredentials $tokenCredentials, $force = false) { $data = $this->fetchUserDetails($tokenCredentials, $force); return $this->userScreenName($data, $tokenCredentials); } /** * Fetch user details from the remote service. * * @param TokenCredentials $tokenCredentials * @param bool $force * * @return array HTTP client response */ protected function fetchUserDetails(TokenCredentials $tokenCredentials, $force = true) { if ( ! $this->cachedUserDetailsResponse || $force) { $url = $this->urlUserDetails(); $client = $this->createHttpClient(); $headers = $this->getHeaders($tokenCredentials, 'GET', $url); try { $response = $client->get($url, [ 'headers' => $headers, ]); } catch (BadResponseException $e) { $response = $e->getResponse(); $body = $response->getBody(); $statusCode = $response->getStatusCode(); throw new \Exception( "Received error [$body] with status code [$statusCode] when retrieving token credentials." ); } switch ($this->responseType) { case 'json': $this->cachedUserDetailsResponse = json_decode((string) $response->getBody(), true); break; case 'xml': $this->cachedUserDetailsResponse = simplexml_load_string((string) $response->getBody()); break; case 'string': parse_str((string) $response->getBody(), $this->cachedUserDetailsResponse); break; default: throw new \InvalidArgumentException("Invalid response type [{$this->responseType}]."); } } return $this->cachedUserDetailsResponse; } /** * Get the client credentials associated with the server. * * @return ClientCredentialsInterface */ public function getClientCredentials() { return $this->clientCredentials; } /** * Get the signature associated with the server. * * @return SignatureInterface */ public function getSignature() { return $this->signature; } /** * Creates a Guzzle HTTP client for the given URL. * * @return GuzzleHttpClient */ public function createHttpClient() { return new GuzzleHttpClient(); } /** * Set the user agent value. * * @param string $userAgent * * @return Server */ public function setUserAgent($userAgent = null) { $this->userAgent = $userAgent; return $this; } /** * Get all headers required to created an authenticated request. * * @param CredentialsInterface $credentials * @param string $method * @param string $url * @param array $bodyParameters * * @return array */ public function getHeaders(CredentialsInterface $credentials, $method, $url, array $bodyParameters = []) { $header = $this->protocolHeader(strtoupper($method), $url, $credentials, $bodyParameters); $authorizationHeader = ['Authorization' => $header]; $headers = $this->buildHttpClientHeaders($authorizationHeader); return $headers; } /** * Get Guzzle HTTP client default headers. * * @return array */ protected function getHttpClientDefaultHeaders() { $defaultHeaders = []; if ( ! empty($this->userAgent)) { $defaultHeaders['User-Agent'] = $this->userAgent; } return $defaultHeaders; } /** * Build Guzzle HTTP client headers. * * @param array $headers * * @return array */ protected function buildHttpClientHeaders($headers = []) { $defaultHeaders = $this->getHttpClientDefaultHeaders(); return array_merge($headers, $defaultHeaders); } /** * Creates a client credentials instance from an array of credentials. * * @param array $clientCredentials * * @return ClientCredentials */ protected function createClientCredentials(array $clientCredentials) { $keys = ['identifier', 'secret']; foreach ($keys as $key) { if ( ! isset($clientCredentials[$key])) { throw new \InvalidArgumentException("Missing client credentials key [$key] from options."); } } if (isset($clientCredentials['rsa_private_key']) && isset($clientCredentials['rsa_public_key'])) { $_clientCredentials = new RsaClientCredentials(); $_clientCredentials->setRsaPrivateKey($clientCredentials['rsa_private_key']); $_clientCredentials->setRsaPublicKey($clientCredentials['rsa_public_key']); } else { $_clientCredentials = new ClientCredentials(); } $_clientCredentials->setIdentifier($clientCredentials['identifier']); $_clientCredentials->setSecret($clientCredentials['secret']); if (isset($clientCredentials['callback_uri'])) { $_clientCredentials->setCallbackUri($clientCredentials['callback_uri']); } return $_clientCredentials; } /** * Handle a bad response coming back when getting temporary credentials. * * @param BadResponseException $e * * @return void * * @throws CredentialsException */ protected function handleTemporaryCredentialsBadResponse(BadResponseException $e) { $response = $e->getResponse(); $body = $response->getBody(); $statusCode = $response->getStatusCode(); throw new CredentialsException( "Received HTTP status code [$statusCode] with message \"$body\" when getting temporary credentials." ); } /** * Creates temporary credentials from the body response. * * @param string $body * * @return TemporaryCredentials */ protected function createTemporaryCredentials($body) { parse_str($body, $data); if ( ! $data || ! is_array($data)) { throw new CredentialsException('Unable to parse temporary credentials response.'); } if ( ! isset($data['oauth_callback_confirmed']) || $data['oauth_callback_confirmed'] != 'true') { throw new CredentialsException('Error in retrieving temporary credentials.'); } $temporaryCredentials = new TemporaryCredentials(); $temporaryCredentials->setIdentifier($data['oauth_token']); $temporaryCredentials->setSecret($data['oauth_token_secret']); return $temporaryCredentials; } /** * Handle a bad response coming back when getting token credentials. * * @param BadResponseException $e * * @return void * * @throws CredentialsException */ protected function handleTokenCredentialsBadResponse(BadResponseException $e) { $response = $e->getResponse(); $body = $response->getBody(); $statusCode = $response->getStatusCode(); throw new CredentialsException( "Received HTTP status code [$statusCode] with message \"$body\" when getting token credentials." ); } /** * Creates token credentials from the body response. * * @param string $body * * @return TokenCredentials */ protected function createTokenCredentials($body) { parse_str($body, $data); if ( ! $data || ! is_array($data)) { throw new CredentialsException('Unable to parse token credentials response.'); } if (isset($data['error'])) { throw new CredentialsException("Error [{$data['error']}] in retrieving token credentials."); } $tokenCredentials = new TokenCredentials(); $tokenCredentials->setIdentifier($data['oauth_token']); $tokenCredentials->setSecret($data['oauth_token_secret']); return $tokenCredentials; } /** * Get the base protocol parameters for an OAuth request. * Each request builds on these parameters. * * @return array * * @see OAuth 1.0 RFC 5849 Section 3.1 */ protected function baseProtocolParameters() { $dateTime = new \DateTime(); return [ 'oauth_consumer_key' => $this->clientCredentials->getIdentifier(), 'oauth_nonce' => $this->nonce(), 'oauth_signature_method' => $this->signature->method(), 'oauth_timestamp' => $dateTime->format('U'), 'oauth_version' => '1.0', ]; } /** * Any additional required protocol parameters for an * OAuth request. * * @return array */ protected function additionalProtocolParameters() { return []; } /** * Generate the OAuth protocol header for a temporary credentials * request, based on the URI. * * @param string $uri * * @return string */ protected function temporaryCredentialsProtocolHeader($uri) { $parameters = array_merge($this->baseProtocolParameters(), [ 'oauth_callback' => $this->clientCredentials->getCallbackUri(), ]); $parameters['oauth_signature'] = $this->signature->sign($uri, $parameters, 'POST'); return $this->normalizeProtocolParameters($parameters); } /** * Generate the OAuth protocol header for requests other than temporary * credentials, based on the URI, method, given credentials & body query * string. * * @param string $method * @param string $uri * @param CredentialsInterface $credentials * @param array $bodyParameters * * @return string */ protected function protocolHeader($method, $uri, CredentialsInterface $credentials, array $bodyParameters = []) { $parameters = array_merge( $this->baseProtocolParameters(), $this->additionalProtocolParameters(), [ 'oauth_token' => $credentials->getIdentifier(), ] ); $this->signature->setCredentials($credentials); $parameters['oauth_signature'] = $this->signature->sign( $uri, array_merge($parameters, $bodyParameters), $method ); return $this->normalizeProtocolParameters($parameters); } /** * Takes an array of protocol parameters and normalizes them * to be used as a HTTP header. * * @param array $parameters * * @return string */ protected function normalizeProtocolParameters(array $parameters) { array_walk($parameters, function (&$value, $key) { $value = rawurlencode($key) . '="' . rawurlencode($value) . '"'; }); return 'OAuth ' . implode(', ', $parameters); } /** * Generate a random string. * * @param int $length * * @return string * * @see OAuth 1.0 RFC 5849 Section 3.3 */ protected function nonce($length = 32) { $pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; return substr(str_shuffle(str_repeat($pool, 5)), 0, $length); } /** * Build a url by combining hostname and query string after checking for * exisiting '?' character in host. * * @param string $host * @param string $queryString * * @return string */ protected function buildUrl($host, $queryString) { return $host . (strpos($host, '?') !== false ? '&' : '?') . $queryString; } /** * Get the URL for retrieving temporary credentials. * * @return string */ abstract public function urlTemporaryCredentials(); /** * Get the URL for redirecting the resource owner to authorize the client. * * @return string */ abstract public function urlAuthorization(); /** * Get the URL retrieving token credentials. * * @return string */ abstract public function urlTokenCredentials(); /** * Get the URL for retrieving user details. * * @return string */ abstract public function urlUserDetails(); /** * Take the decoded data from the user details URL and convert * it to a User object. * * @param mixed $data * @param TokenCredentials $tokenCredentials * * @return User */ abstract public function userDetails($data, TokenCredentials $tokenCredentials); /** * Take the decoded data from the user details URL and extract * the user's UID. * * @param mixed $data * @param TokenCredentials $tokenCredentials * * @return string|int */ abstract public function userUid($data, TokenCredentials $tokenCredentials); /** * Take the decoded data from the user details URL and extract * the user's email. * * @param mixed $data * @param TokenCredentials $tokenCredentials * * @return string|null */ abstract public function userEmail($data, TokenCredentials $tokenCredentials); /** * Take the decoded data from the user details URL and extract * the user's screen name. * * @param mixed $data * @param TokenCredentials $tokenCredentials * * @return string|null */ abstract public function userScreenName($data, TokenCredentials $tokenCredentials); } oauth1-client/src/Server/Magento.php 0000644 00000011765 15007532371 0013376 0 ustar 00 <?php namespace League\OAuth1\Client\Server; use League\OAuth1\Client\Credentials\TemporaryCredentials; use League\OAuth1\Client\Credentials\TokenCredentials; use League\OAuth1\Client\Signature\SignatureInterface; /** * Magento OAuth 1.0a. * * This class reflects two Magento oddities: * - Magento expects the oauth_verifier to be located in the header instead of * the post body. * - Magento expects the Accept to be located in the header * * Additionally, this is initialized with two additional parameters: * - Boolean 'admin' to use the admin vs customer * - String 'host' with the path to the magento host */ class Magento extends Server { /** * Admin url. * * @var string */ protected $adminUrl; /** * Base uri. * * @var string */ protected $baseUri; /** * Server is admin. * * @var bool */ protected $isAdmin = false; /** * oauth_verifier stored for use with. * * @var string */ private $verifier; /** * @inheritDoc */ public function __construct($clientCredentials, SignatureInterface $signature = null) { parent::__construct($clientCredentials, $signature); if (is_array($clientCredentials)) { $this->parseConfigurationArray($clientCredentials); } } /** * @inheritDoc */ public function urlTemporaryCredentials() { return $this->baseUri . '/oauth/initiate'; } /** * @inheritDoc */ public function urlAuthorization() { return $this->isAdmin ? $this->adminUrl : $this->baseUri . '/oauth/authorize'; } /** * @inheritDoc */ public function urlTokenCredentials() { return $this->baseUri . '/oauth/token'; } /** * @inheritDoc */ public function urlUserDetails() { return $this->baseUri . '/api/rest/customers'; } /** * @inheritDoc */ public function userDetails($data, TokenCredentials $tokenCredentials) { if ( ! is_array($data) || ! count($data)) { throw new \Exception('Not possible to get user info'); } $id = key($data); $data = current($data); $user = new User(); $user->uid = $id; $mapping = [ 'email' => 'email', 'firstName' => 'firstname', 'lastName' => 'lastname', ]; foreach ($mapping as $userKey => $dataKey) { if ( ! isset($data[$dataKey])) { continue; } $user->{$userKey} = $data[$dataKey]; } $user->extra = array_diff_key($data, array_flip($mapping)); return $user; } /** * @inheritDoc */ public function userUid($data, TokenCredentials $tokenCredentials) { return key($data); } /** * @inheritDoc */ public function userEmail($data, TokenCredentials $tokenCredentials) { $data = current($data); if ( ! isset($data['email'])) { return null; } return $data['email']; } /** * @inheritDoc */ public function userScreenName($data, TokenCredentials $tokenCredentials) { return null; } /** * @inheritDoc */ public function getTokenCredentials(TemporaryCredentials $temporaryCredentials, $temporaryIdentifier, $verifier) { $this->verifier = $verifier; return parent::getTokenCredentials($temporaryCredentials, $temporaryIdentifier, $verifier); } /** * @inheritDoc */ protected function additionalProtocolParameters() { return [ 'oauth_verifier' => $this->verifier, ]; } protected function getHttpClientDefaultHeaders() { $defaultHeaders = parent::getHttpClientDefaultHeaders(); // Accept header is required, @see Mage_Api2_Model_Renderer::factory $defaultHeaders['Accept'] = 'application/json'; return $defaultHeaders; } /** * Parse configuration array to set attributes. * * @param array $configuration * * @return void * * @throws \Exception */ private function parseConfigurationArray(array $configuration = []) { if ( ! isset($configuration['host'])) { throw new \Exception('Missing Magento Host'); } $url = parse_url($configuration['host']); $this->baseUri = sprintf('%s://%s', $url['scheme'], $url['host']); if (isset($url['port'])) { $this->baseUri .= ':' . $url['port']; } if (isset($url['path'])) { $this->baseUri .= '/' . trim($url['path'], '/'); } $this->isAdmin = ! empty($configuration['admin']); if ( ! empty($configuration['adminUrl'])) { $this->adminUrl = $configuration['adminUrl'] . '/oauth_authorize'; } else { $this->adminUrl = $this->baseUri . '/admin/oauth_authorize'; } } } oauth1-client/src/Server/Trello.php 0000644 00000012104 15007532371 0013231 0 ustar 00 <?php namespace League\OAuth1\Client\Server; use League\OAuth1\Client\Credentials\TokenCredentials; use League\OAuth1\Client\Signature\SignatureInterface; class Trello extends Server { /** * Access token. * * @var string */ protected $accessToken; /** * Application expiration. * * @var string */ protected $applicationExpiration; /** * Application key. * * @var string */ protected $applicationKey; /** * Application name. * * @var string */ protected $applicationName; /** * Application scope. * * @var string */ protected $applicationScope; /** * @inheritDoc */ public function __construct($clientCredentials, SignatureInterface $signature = null) { parent::__construct($clientCredentials, $signature); if (is_array($clientCredentials)) { $this->parseConfiguration($clientCredentials); } } /** * Set the access token. * * @param string $accessToken * * @return Trello */ public function setAccessToken($accessToken) { $this->accessToken = $accessToken; return $this; } /** * Set the application expiration. * * @param string $applicationExpiration * * @return Trello */ public function setApplicationExpiration($applicationExpiration) { $this->applicationExpiration = $applicationExpiration; return $this; } /** * Get application expiration. * * @return string */ public function getApplicationExpiration() { return $this->applicationExpiration ?: '1day'; } /** * Set the application name. * * @param string $applicationName * * @return Trello */ public function setApplicationName($applicationName) { $this->applicationName = $applicationName; return $this; } /** * Get application name. * * @return string|null */ public function getApplicationName() { return $this->applicationName ?: null; } /** * Set the application scope. * * @param string $applicationScope * * @return Trello */ public function setApplicationScope($applicationScope) { $this->applicationScope = $applicationScope; return $this; } /** * Get application scope. * * @return string */ public function getApplicationScope() { return $this->applicationScope ?: 'read'; } /** * @inheritDoc */ public function urlTemporaryCredentials() { return 'https://trello.com/1/OAuthGetRequestToken'; } /** * @inheritDoc */ public function urlAuthorization() { return 'https://trello.com/1/OAuthAuthorizeToken?' . $this->buildAuthorizationQueryParameters(); } /** * @inheritDoc */ public function urlTokenCredentials() { return 'https://trello.com/1/OAuthGetAccessToken'; } /** * @inheritDoc */ public function urlUserDetails() { return 'https://trello.com/1/members/me?key=' . $this->applicationKey . '&token=' . $this->accessToken; } /** * @inheritDoc */ public function userDetails($data, TokenCredentials $tokenCredentials) { $user = new User(); $user->nickname = $data['username']; $user->name = $data['fullName']; $user->extra = (array) $data; return $user; } /** * @inheritDoc */ public function userUid($data, TokenCredentials $tokenCredentials) { return $data['id']; } /** * @inheritDoc */ public function userEmail($data, TokenCredentials $tokenCredentials) { return null; } /** * @inheritDoc */ public function userScreenName($data, TokenCredentials $tokenCredentials) { return $data['username']; } /** * Build authorization query parameters. * * @return string */ private function buildAuthorizationQueryParameters() { $params = [ 'response_type' => 'fragment', 'scope' => $this->getApplicationScope(), 'expiration' => $this->getApplicationExpiration(), 'name' => $this->getApplicationName(), ]; return http_build_query($params); } /** * Parse configuration array to set attributes. * * @param array $configuration * * @return void */ private function parseConfiguration(array $configuration = []) { $configToPropertyMap = [ 'identifier' => 'applicationKey', 'expiration' => 'applicationExpiration', 'name' => 'applicationName', 'scope' => 'applicationScope', ]; foreach ($configToPropertyMap as $config => $property) { if (isset($configuration[$config])) { $this->$property = $configuration[$config]; } } } } oauth1-client/src/Server/Uservoice.php 0000644 00000005402 15007532371 0013737 0 ustar 00 <?php namespace League\OAuth1\Client\Server; use InvalidArgumentException; use League\OAuth1\Client\Credentials\TokenCredentials; use League\OAuth1\Client\Signature\SignatureInterface; class Uservoice extends Server { /** * The base URL, used to generate the auth endpoints. * * @var string */ protected $base; /** * @inheritDoc */ public function __construct($clientCredentials, SignatureInterface $signature = null) { parent::__construct($clientCredentials, $signature); if (is_array($clientCredentials)) { $this->parseConfigurationArray($clientCredentials); } } /** * @inheritDoc */ public function urlTemporaryCredentials() { return $this->base . '/oauth/request_token'; } /** * @inheritDoc */ public function urlAuthorization() { return $this->base . '/oauth/authorize'; } /** * @inheritDoc */ public function urlTokenCredentials() { return $this->base . '/oauth/access_token'; } /** * @inheritDoc */ public function urlUserDetails() { return $this->base . '/api/v1/users/current.json'; } /** * @inheritDoc */ public function userDetails($data, TokenCredentials $tokenCredentials) { $user = new User(); $user->uid = $data['user']['id']; $user->name = $data['user']['name']; $user->imageUrl = $data['user']['avatar_url']; $user->email = $data['user']['email']; if ($data['user']['name']) { $parts = explode(' ', $data['user']['name'], 2); $user->firstName = $parts[0]; if (2 === count($parts)) { $user->lastName = $parts[1]; } } $user->urls[] = $data['user']['url']; return $user; } /** * @inheritDoc */ public function userUid($data, TokenCredentials $tokenCredentials) { return $data['user']['id']; } /** * @inheritDoc */ public function userEmail($data, TokenCredentials $tokenCredentials) { return $data['user']['email']; } /** * @inheritDoc */ public function userScreenName($data, TokenCredentials $tokenCredentials) { return $data['user']['name']; } /** * Parse configuration array to set attributes. * * @param array $configuration * * @return void * * @throws InvalidArgumentException */ private function parseConfigurationArray(array $configuration = []) { if (isset($configuration['host'])) { throw new InvalidArgumentException('Missing host'); } $this->base = trim($configuration['host'], '/'); } } oauth1-client/src/Credentials/TokenCredentials.php 0000644 00000000203 15007532371 0016212 0 ustar 00 <?php namespace League\OAuth1\Client\Credentials; class TokenCredentials extends Credentials implements CredentialsInterface { } oauth1-client/src/Credentials/Credentials.php 0000644 00000001440 15007532371 0015215 0 ustar 00 <?php namespace League\OAuth1\Client\Credentials; abstract class Credentials implements CredentialsInterface { /** * The credentials identifier. * * @var string */ protected $identifier; /** * The credentials secret. * * @var string */ protected $secret; /** * @inheritDoc */ public function getIdentifier() { return $this->identifier; } /** * @inheritDoc */ public function setIdentifier($identifier) { $this->identifier = $identifier; } /** * @inheritDoc */ public function getSecret() { return $this->secret; } /** * @inheritDoc */ public function setSecret($secret) { $this->secret = $secret; } } oauth1-client/src/Credentials/CredentialsInterface.php 0000644 00000001210 15007532371 0017031 0 ustar 00 <?php namespace League\OAuth1\Client\Credentials; interface CredentialsInterface { /** * Get the credentials identifier. * * @return string */ public function getIdentifier(); /** * Set the credentials identifier. * * @param string $identifier * * @return void */ public function setIdentifier($identifier); /** * Get the credentials secret. * * @return string */ public function getSecret(); /** * Set the credentials secret. * * @param string $secret * * @return void */ public function setSecret($secret); } oauth1-client/src/Credentials/ClientCredentialsInterface.php 0000644 00000000646 15007532371 0020204 0 ustar 00 <?php namespace League\OAuth1\Client\Credentials; interface ClientCredentialsInterface extends CredentialsInterface { /** * Get the credentials callback URI. * * @return string */ public function getCallbackUri(); /** * Set the credentials callback URI. * * @param string $callbackUri * * @return void */ public function setCallbackUri($callbackUri); } oauth1-client/src/Credentials/RsaClientCredentials.php 0000644 00000004725 15007532371 0017033 0 ustar 00 <?php namespace League\OAuth1\Client\Credentials; use OpenSSLAsymmetricKey; class RsaClientCredentials extends ClientCredentials { /** * @var string */ protected $rsaPublicKeyFile; /** * @var string */ protected $rsaPrivateKeyFile; /** * @var resource|OpenSSLAsymmetricKey|null */ protected $rsaPublicKey; /** * @var resource|OpenSSLAsymmetricKey|null */ protected $rsaPrivateKey; /** * Sets the path to the RSA public key. * * @param string $filename * * @return self */ public function setRsaPublicKey($filename) { $this->rsaPublicKeyFile = $filename; $this->rsaPublicKey = null; return $this; } /** * Sets the path to the RSA private key. * * @param string $filename * * @return self */ public function setRsaPrivateKey($filename) { $this->rsaPrivateKeyFile = $filename; $this->rsaPrivateKey = null; return $this; } /** * Gets the RSA public key. * * @throws CredentialsException when the key could not be loaded. * * @return resource|OpenSSLAsymmetricKey */ public function getRsaPublicKey() { if ($this->rsaPublicKey) { return $this->rsaPublicKey; } if ( ! file_exists($this->rsaPublicKeyFile)) { throw new CredentialsException('Could not read the public key file.'); } $this->rsaPublicKey = openssl_get_publickey(file_get_contents($this->rsaPublicKeyFile)); if ( ! $this->rsaPublicKey) { throw new CredentialsException('Cannot access public key for signing'); } return $this->rsaPublicKey; } /** * Gets the RSA private key. * * @throws CredentialsException when the key could not be loaded. * * @return resource|OpenSSLAsymmetricKey */ public function getRsaPrivateKey() { if ($this->rsaPrivateKey) { return $this->rsaPrivateKey; } if ( ! file_exists($this->rsaPrivateKeyFile)) { throw new CredentialsException('Could not read the private key file.'); } $this->rsaPrivateKey = openssl_pkey_get_private(file_get_contents($this->rsaPrivateKeyFile)); if ( ! $this->rsaPrivateKey) { throw new CredentialsException('Cannot access private key for signing'); } return $this->rsaPrivateKey; } } oauth1-client/src/Credentials/TemporaryCredentials.php 0000644 00000000207 15007532371 0017120 0 ustar 00 <?php namespace League\OAuth1\Client\Credentials; class TemporaryCredentials extends Credentials implements CredentialsInterface { } oauth1-client/src/Credentials/ClientCredentials.php 0000644 00000000771 15007532371 0016362 0 ustar 00 <?php namespace League\OAuth1\Client\Credentials; class ClientCredentials extends Credentials implements ClientCredentialsInterface { /** * The credentials callback URI. * * @var string */ protected $callbackUri; /** * @inheritDoc */ public function getCallbackUri() { return $this->callbackUri; } /** * @inheritDoc */ public function setCallbackUri($callbackUri) { $this->callbackUri = $callbackUri; } } oauth1-client/src/Credentials/CredentialsException.php 0000644 00000000165 15007532371 0017077 0 ustar 00 <?php namespace League\OAuth1\Client\Credentials; use Exception; class CredentialsException extends Exception { } uri/UriInfo.php 0000644 00000005167 15007532371 0007443 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri; use League\Uri\Contracts\UriInterface; use Psr\Http\Message\UriInterface as Psr7UriInterface; /** * @deprecated since version 7.0.0 * @codeCoverageIgnore * @see BaseUri */ final class UriInfo { /** * @codeCoverageIgnore */ private function __construct() { } /** * Tells whether the URI represents an absolute URI. */ public static function isAbsolute(Psr7UriInterface|UriInterface $uri): bool { return BaseUri::from($uri)->isAbsolute(); } /** * Tell whether the URI represents a network path. */ public static function isNetworkPath(Psr7UriInterface|UriInterface $uri): bool { return BaseUri::from($uri)->isNetworkPath(); } /** * Tells whether the URI represents an absolute path. */ public static function isAbsolutePath(Psr7UriInterface|UriInterface $uri): bool { return BaseUri::from($uri)->isAbsolutePath(); } /** * Tell whether the URI represents a relative path. * */ public static function isRelativePath(Psr7UriInterface|UriInterface $uri): bool { return BaseUri::from($uri)->isRelativePath(); } /** * Tells whether both URI refers to the same document. */ public static function isSameDocument(Psr7UriInterface|UriInterface $uri, Psr7UriInterface|UriInterface $baseUri): bool { return BaseUri::from($baseUri)->isSameDocument($uri); } /** * Returns the URI origin property as defined by WHATWG URL living standard. * * {@see https://url.spec.whatwg.org/#origin} * * For URI without a special scheme the method returns null * For URI with the file scheme the method will return null (as this is left to the implementation decision) * For URI with a special scheme the method returns the scheme followed by its authority (without the userinfo part) */ public static function getOrigin(Psr7UriInterface|UriInterface $uri): ?string { return BaseUri::from($uri)->origin()?->__toString(); } /** * Tells whether two URI do not share the same origin. * * @see UriInfo::getOrigin() */ public static function isCrossOrigin(Psr7UriInterface|UriInterface $uri, Psr7UriInterface|UriInterface $baseUri): bool { return BaseUri::from($baseUri)->isCrossOrigin($uri); } } uri/UriTemplate/Template.php 0000644 00000010335 15007532371 0012067 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\UriTemplate; use League\Uri\Exceptions\SyntaxError; use Stringable; use function array_filter; use function array_map; use function array_reduce; use function array_unique; use function preg_match_all; use function preg_replace; use function str_contains; use function str_replace; use const PREG_SET_ORDER; /** * @internal The class exposes the internal representation of a Template and its usage */ final class Template implements Stringable { /** * Expression regular expression pattern. */ private const REGEXP_EXPRESSION_DETECTOR = '/(?<expression>\{[^}]*})/x'; /** @var array<Expression> */ private readonly array $expressions; /** @var array<string> */ public readonly array $variableNames; private function __construct(public readonly string $value, Expression ...$expressions) { $this->expressions = $expressions; $this->variableNames = array_unique( array_merge( ...array_map( static fn (Expression $expression): array => $expression->variableNames, $expressions ) ) ); } /** * @throws SyntaxError if the template contains invalid expressions * @throws SyntaxError if the template contains invalid variable specification */ public static function new(Stringable|string $template): self { $template = (string) $template; /** @var string $remainder */ $remainder = preg_replace(self::REGEXP_EXPRESSION_DETECTOR, '', $template); if (str_contains($remainder, '{') || str_contains($remainder, '}')) { throw new SyntaxError('The template "'.$template.'" contains invalid expressions.'); } preg_match_all(self::REGEXP_EXPRESSION_DETECTOR, $template, $founds, PREG_SET_ORDER); return new self($template, ...array_values( array_reduce($founds, function (array $carry, array $found): array { if (!isset($carry[$found['expression']])) { $carry[$found['expression']] = Expression::new($found['expression']); } return $carry; }, []) )); } /** * @throws TemplateCanNotBeExpanded if the variables are invalid */ public function expand(iterable $variables = []): string { if (!$variables instanceof VariableBag) { $variables = new VariableBag($variables); } return $this->expandAll($variables); } /** * @throws TemplateCanNotBeExpanded if the variables are invalid or missing */ public function expandOrFail(iterable $variables = []): string { if (!$variables instanceof VariableBag) { $variables = new VariableBag($variables); } $missing = array_filter($this->variableNames, fn (string $name): bool => !isset($variables[$name])); if ([] !== $missing) { throw TemplateCanNotBeExpanded::dueToMissingVariables(...$missing); } return $this->expandAll($variables); } private function expandAll(VariableBag $variables): string { return array_reduce( $this->expressions, fn (string $uri, Expression $expr): string => str_replace($expr->value, $expr->expand($variables), $uri), $this->value ); } public function __toString(): string { return $this->value; } /** * DEPRECATION WARNING! This method will be removed in the next major point release. * * @throws SyntaxError if the template contains invalid expressions * @throws SyntaxError if the template contains invalid variable specification * @deprecated Since version 7.0.0 * @codeCoverageIgnore * @see Template::new() * * Create a new instance from a string. * */ public static function createFromString(Stringable|string $template): self { return self::new($template); } } uri/UriTemplate/TemplateCanNotBeExpanded.php 0000644 00000002507 15007532371 0015114 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\UriTemplate; use InvalidArgumentException; use League\Uri\Contracts\UriException; class TemplateCanNotBeExpanded extends InvalidArgumentException implements UriException { public readonly array $variablesNames; public function __construct(string $message = '', string ...$variableNames) { parent::__construct($message, 0, null); $this->variablesNames = $variableNames; } public static function dueToUnableToProcessValueListWithPrefix(string $variableName): self { return new self('The ":" modifier cannot be applied on "'.$variableName.'" since it is a list of values.', $variableName); } public static function dueToNestedListOfValue(string $variableName): self { return new self('The "'.$variableName.'" cannot be a nested list.', $variableName); } public static function dueToMissingVariables(string ...$variableNames): self { return new self('The following required variables are missing: `'.implode('`, `', $variableNames).'`.', ...$variableNames); } } uri/UriTemplate/VariableBag.php 0000644 00000007417 15007532371 0012462 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\UriTemplate; use ArrayAccess; use Closure; use Countable; use IteratorAggregate; use Stringable; use Traversable; use function array_filter; use function is_bool; use function is_scalar; use const ARRAY_FILTER_USE_BOTH; /** * @internal The class exposes the internal representation of variable bags * * @phpstan-type InputValue string|bool|int|float|array<string|bool|int|float> * * @implements ArrayAccess<string, InputValue> * @implements IteratorAggregate<string, InputValue> */ final class VariableBag implements ArrayAccess, Countable, IteratorAggregate { /** * @var array<string,string|array<string>> */ private array $variables = []; /** * @param iterable<array-key, InputValue> $variables */ public function __construct(iterable $variables = []) { foreach ($variables as $name => $value) { $this->assign((string) $name, $value); } } public function count(): int { return count($this->variables); } public function getIterator(): Traversable { yield from $this->variables; } public function offsetExists(mixed $offset): bool { return array_key_exists($offset, $this->variables); } public function offsetUnset(mixed $offset): void { unset($this->variables[$offset]); } public function offsetSet(mixed $offset, mixed $value): void { $this->assign($offset, $value); /* @phpstan-ignore-line */ } public function offsetGet(mixed $offset): mixed { return $this->fetch($offset); } /** * Tells whether the bag is empty or not. */ public function isEmpty(): bool { return [] === $this->variables; } /** * Tells whether the bag is empty or not. */ public function isNotEmpty(): bool { return [] !== $this->variables; } /** * Fetches the variable value if none found returns null. * * @return null|string|array<string> */ public function fetch(string $name): null|string|array { return $this->variables[$name] ?? null; } /** * @param Stringable|InputValue $value */ public function assign(string $name, Stringable|string|bool|int|float|array|null $value): void { $this->variables[$name] = $this->normalizeValue($value, $name, true); } /** * @param Stringable|InputValue $value * * @throws TemplateCanNotBeExpanded if the value contains nested list */ private function normalizeValue( Stringable|string|float|int|bool|array|null $value, string $name, bool $isNestedListAllowed ): array|string { return match (true) { is_bool($value) => true === $value ? '1' : '0', (null === $value || is_scalar($value) || $value instanceof Stringable) => (string) $value, !$isNestedListAllowed => throw TemplateCanNotBeExpanded::dueToNestedListOfValue($name), default => array_map(fn ($var): array|string => self::normalizeValue($var, $name, false), $value), }; } /** * Replaces elements from passed variables into the current instance. */ public function replace(VariableBag $variables): self { return new self($this->variables + $variables->variables); } /** * Filters elements using the closure. */ public function filter(Closure $fn): self { return new self(array_filter($this->variables, $fn, ARRAY_FILTER_USE_BOTH)); } } uri/UriTemplate/Expression.php 0000644 00000005423 15007532371 0012455 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\UriTemplate; use League\Uri\Exceptions\SyntaxError; use Stringable; use function array_filter; use function array_map; use function array_unique; use function explode; use function implode; /** * @internal The class exposes the internal representation of an Expression and its usage * @link https://www.rfc-editor.org/rfc/rfc6570#section-2.2 */ final class Expression { /** @var array<VarSpecifier> */ private readonly array $varSpecifiers; /** @var array<string> */ public readonly array $variableNames; public readonly string $value; private function __construct(public readonly Operator $operator, VarSpecifier ...$varSpecifiers) { $this->varSpecifiers = $varSpecifiers; $this->variableNames = array_unique( array_map( static fn (VarSpecifier $varSpecifier): string => $varSpecifier->name, $varSpecifiers ) ); $this->value = '{'.$operator->value.implode(',', array_map( static fn (VarSpecifier $varSpecifier): string => $varSpecifier->toString(), $varSpecifiers )).'}'; } /** * @throws SyntaxError if the expression is invalid */ public static function new(Stringable|string $expression): self { $parts = Operator::parseExpression($expression); return new Expression($parts['operator'], ...array_map( static fn (string $varSpec): VarSpecifier => VarSpecifier::new($varSpec), explode(',', $parts['variables']) )); } /** * DEPRECATION WARNING! This method will be removed in the next major point release. * * @throws SyntaxError if the expression is invalid * @see Expression::new() * * @deprecated Since version 7.0.0 * @codeCoverageIgnore */ public static function createFromString(Stringable|string $expression): self { return self::new($expression); } public function expand(VariableBag $variables): string { $expanded = implode( $this->operator->separator(), array_filter( array_map( fn (VarSpecifier $varSpecifier): string => $this->operator->expand($varSpecifier, $variables), $this->varSpecifiers ), static fn ($value): bool => '' !== $value ) ); return match ('') { $expanded => '', default => $this->operator->first().$expanded, }; } } uri/UriTemplate/VarSpecifier.php 0000644 00000004267 15007532371 0012705 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\UriTemplate; use League\Uri\Exceptions\SyntaxError; use function preg_match; /** * @internal The class exposes the internal representation of a Var Specifier * @link https://www.rfc-editor.org/rfc/rfc6570#section-2.3 */ final class VarSpecifier { /** * Variables specification regular expression pattern. * * @link https://tools.ietf.org/html/rfc6570#section-2.3 */ private const REGEXP_VARSPEC = '/^(?<name>(?:[A-z0-9_\.]|%[0-9a-fA-F]{2})+)(?<modifier>\:(?<position>\d+)|\*)?$/'; private const MODIFIER_POSITION_MAX_POSITION = 10_000; private function __construct( public readonly string $name, public readonly string $modifier, public readonly int $position ) { } public static function new(string $specification): self { if (1 !== preg_match(self::REGEXP_VARSPEC, $specification, $parsed)) { throw new SyntaxError('The variable specification "'.$specification.'" is invalid.'); } $properties = ['name' => $parsed['name'], 'modifier' => $parsed['modifier'] ?? '', 'position' => $parsed['position'] ?? '']; if ('' !== $properties['position']) { $properties['position'] = (int) $properties['position']; $properties['modifier'] = ':'; } if ('' === $properties['position']) { $properties['position'] = 0; } if (self::MODIFIER_POSITION_MAX_POSITION <= $properties['position']) { throw new SyntaxError('The variable specification "'.$specification.'" is invalid the position modifier must be lower than 10000.'); } return new self($properties['name'], $properties['modifier'], $properties['position']); } public function toString(): string { return $this->name.$this->modifier.match (true) { 0 < $this->position => $this->position, default => '', }; } } uri/UriTemplate/Operator.php 0000644 00000015037 15007532371 0012113 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\UriTemplate; use League\Uri\Encoder; use League\Uri\Exceptions\SyntaxError; use Stringable; use function implode; use function is_array; use function preg_match; use function rawurlencode; use function str_contains; use function substr; /** * Processing behavior according to the expression type operator. * * @internal The class exposes the internal representation of an Operator and its usage * * @link https://www.rfc-editor.org/rfc/rfc6570#section-2.2 * @link https://tools.ietf.org/html/rfc6570#appendix-A */ enum Operator: string { /** * Expression regular expression pattern. * * @link https://tools.ietf.org/html/rfc6570#section-2.2 */ private const REGEXP_EXPRESSION = '/^\{(?:(?<operator>[\.\/;\?&\=,\!@\|\+#])?(?<variables>[^\}]*))\}$/'; /** * Reserved Operator characters. * * @link https://tools.ietf.org/html/rfc6570#section-2.2 */ private const RESERVED_OPERATOR = '=,!@|'; case None = ''; case ReservedChars = '+'; case Label = '.'; case Path = '/'; case PathParam = ';'; case Query = '?'; case QueryPair = '&'; case Fragment = '#'; public function first(): string { return match ($this) { self::None, self::ReservedChars => '', default => $this->value, }; } public function separator(): string { return match ($this) { self::None, self::ReservedChars, self::Fragment => ',', self::Query, self::QueryPair => '&', default => $this->value, }; } public function isNamed(): bool { return match ($this) { self::Query, self::PathParam, self::QueryPair => true, default => false, }; } /** * Removes percent encoding on reserved characters (used with + and # modifiers). */ public function decode(string $var): string { return match ($this) { Operator::ReservedChars, Operator::Fragment => (string) Encoder::encodeQueryOrFragment($var), default => rawurlencode($var), }; } /** * @throws SyntaxError if the expression is invalid * @throws SyntaxError if the operator used in the expression is invalid * @throws SyntaxError if the contained variable specifiers are invalid * * @return array{operator:Operator, variables:string} */ public static function parseExpression(Stringable|string $expression): array { $expression = (string) $expression; if (1 !== preg_match(self::REGEXP_EXPRESSION, $expression, $parts)) { throw new SyntaxError('The expression "'.$expression.'" is invalid.'); } /** @var array{operator:string, variables:string} $parts */ $parts = $parts + ['operator' => '']; if ('' !== $parts['operator'] && str_contains(self::RESERVED_OPERATOR, $parts['operator'])) { throw new SyntaxError('The operator used in the expression "'.$expression.'" is reserved.'); } return [ 'operator' => self::from($parts['operator']), 'variables' => $parts['variables'], ]; } /** * Replaces an expression with the given variables. * * @throws TemplateCanNotBeExpanded if the variables is an array and a ":" modifier needs to be applied * @throws TemplateCanNotBeExpanded if the variables contains nested array values */ public function expand(VarSpecifier $varSpecifier, VariableBag $variables): string { $value = $variables->fetch($varSpecifier->name); if (null === $value) { return ''; } [$expanded, $actualQuery] = $this->inject($value, $varSpecifier); if (!$actualQuery) { return $expanded; } if ('&' !== $this->separator() && '' === $expanded) { return $varSpecifier->name; } return $varSpecifier->name.'='.$expanded; } /** * @param string|array<string> $value * * @return array{0:string, 1:bool} */ private function inject(array|string $value, VarSpecifier $varSpec): array { if (is_array($value)) { return $this->replaceList($value, $varSpec); } if (':' === $varSpec->modifier) { $value = substr($value, 0, $varSpec->position); } return [$this->decode($value), $this->isNamed()]; } /** * Expands an expression using a list of values. * * @param array<string> $value * * @throws TemplateCanNotBeExpanded if the variables is an array and a ":" modifier needs to be applied * * @return array{0:string, 1:bool} */ private function replaceList(array $value, VarSpecifier $varSpec): array { if (':' === $varSpec->modifier) { throw TemplateCanNotBeExpanded::dueToUnableToProcessValueListWithPrefix($varSpec->name); } if ([] === $value) { return ['', false]; } $pairs = []; $isList = array_is_list($value); $useQuery = $this->isNamed(); foreach ($value as $key => $var) { if (!$isList) { $key = rawurlencode((string) $key); } $var = $this->decode($var); if ('*' === $varSpec->modifier) { if (!$isList) { $var = $key.'='.$var; } elseif ($key > 0 && $useQuery) { $var = $varSpec->name.'='.$var; } } $pairs[$key] = $var; } if ('*' === $varSpec->modifier) { if (!$isList) { // Don't prepend the value name when using the `explode` modifier with an associative array. $useQuery = false; } return [implode($this->separator(), $pairs), $useQuery]; } if (!$isList) { // When an associative array is encountered and the `explode` modifier is not set, then // the result must be a comma separated list of keys followed by their respective values. $retVal = []; foreach ($pairs as $offset => $data) { $retVal[$offset] = $offset.','.$data; } $pairs = $retVal; } return [implode(',', $pairs), $useQuery]; } } uri/UriTemplate.php 0000644 00000007336 15007532371 0010323 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri; use League\Uri\Contracts\UriException; use League\Uri\Contracts\UriInterface; use League\Uri\Exceptions\SyntaxError; use League\Uri\UriTemplate\Template; use League\Uri\UriTemplate\TemplateCanNotBeExpanded; use League\Uri\UriTemplate\VariableBag; use Stringable; use function array_fill_keys; use function array_key_exists; /** * Defines the URI Template syntax and the process for expanding a URI Template into a URI reference. * * @link https://tools.ietf.org/html/rfc6570 * @package League\Uri * @author Ignace Nyamagana Butera <nyamsprod@gmail.com> * @since 6.1.0 */ final class UriTemplate { private readonly Template $template; private readonly VariableBag $defaultVariables; /** * @throws SyntaxError if the template syntax is invalid * @throws TemplateCanNotBeExpanded if the template or the variables are invalid */ public function __construct(Stringable|string $template, iterable $defaultVariables = []) { $this->template = $template instanceof Template ? $template : Template::new($template); $this->defaultVariables = $this->filterVariables($defaultVariables); } private function filterVariables(iterable $variables): VariableBag { if (!$variables instanceof VariableBag) { $variables = new VariableBag($variables); } return $variables ->filter(fn ($value, string|int $name) => array_key_exists( $name, array_fill_keys($this->template->variableNames, 1) )); } public function getTemplate(): string { return $this->template->value; } /** * @return array<string> */ public function getVariableNames(): array { return $this->template->variableNames; } public function getDefaultVariables(): array { return iterator_to_array($this->defaultVariables); } /** * Returns a new instance with the updated default variables. * * This method MUST retain the state of the current instance, and return * an instance that contains the modified default variables. * * If present, variables whose name is not part of the current template * possible variable names are removed. * * @throws TemplateCanNotBeExpanded if the variables are invalid */ public function withDefaultVariables(iterable $defaultVariables): self { $defaultVariables = $this->filterVariables($defaultVariables); if ($defaultVariables == $this->defaultVariables) { return $this; } return new self($this->template, $defaultVariables); } /** * @throws TemplateCanNotBeExpanded if the variables are invalid * @throws UriException if the resulting expansion cannot be converted to a UriInterface instance */ public function expand(iterable $variables = []): UriInterface { return Uri::new($this->template->expand( $this->filterVariables($variables)->replace($this->defaultVariables) )); } /** * @throws TemplateCanNotBeExpanded if the variables are invalid or missing * @throws UriException if the resulting expansion cannot be converted to a UriInterface instance */ public function expandOrFail(iterable $variables = []): UriInterface { return Uri::new($this->template->expandOrFail( $this->filterVariables($variables)->replace($this->defaultVariables) )); } } uri/composer.json 0000644 00000004004 15007532371 0010066 0 ustar 00 { "name": "league/uri", "type": "library", "description" : "URI manipulation library", "keywords": [ "url", "uri", "rfc3986", "rfc3987", "rfc6570", "psr-7", "parse_url", "http", "https", "ws", "ftp", "data-uri", "file-uri", "middleware", "parse_str", "query-string", "querystring", "hostname", "uri-template" ], "license": "MIT", "homepage": "https://uri.thephpleague.com", "authors": [ { "name" : "Ignace Nyamagana Butera", "email" : "nyamsprod@gmail.com", "homepage" : "https://nyamsprod.com" } ], "support": { "forum": "https://thephpleague.slack.com", "docs": "https://uri.thephpleague.com", "issues": "https://github.com/thephpleague/uri-src/issues" }, "funding": [ { "type": "github", "url": "https://github.com/sponsors/nyamsprod" } ], "require": { "php": "^8.1", "league/uri-interfaces": "^7.3" }, "autoload": { "psr-4": { "League\\Uri\\": "" } }, "conflict": { "league/uri-schemes": "^1.0" }, "suggest": { "ext-bcmath": "to improve IPV4 host parsing", "ext-fileinfo": "to create Data URI from file contennts", "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", "league/uri-components" : "Needed to easily manipulate URI objects components", "php-64bit": "to improve IPV4 host parsing", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "extra": { "branch-alias": { "dev-master": "7.x-dev" } }, "config": { "sort-packages": true } } uri/LICENSE 0000644 00000002102 15007532371 0006346 0 ustar 00 The MIT License (MIT) Copyright (c) 2015 ignace nyamagana butera 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. uri/Http.php 0000644 00000016552 15007532371 0007007 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri; use JsonSerializable; use League\Uri\Contracts\UriException; use League\Uri\Contracts\UriInterface; use League\Uri\Exceptions\SyntaxError; use League\Uri\UriTemplate\TemplateCanNotBeExpanded; use Psr\Http\Message\UriInterface as Psr7UriInterface; use Stringable; /** * @phpstan-import-type InputComponentMap from UriString */ final class Http implements Stringable, Psr7UriInterface, JsonSerializable { private function __construct(private readonly UriInterface $uri) { if (null === $this->uri->getScheme() && '' === $this->uri->getHost()) { throw new SyntaxError('An URI without scheme cannot contain an empty host string according to PSR-7: '.$uri); } $port = $this->uri->getPort(); if (null !== $port && ($port < 0 || $port > 65535)) { throw new SyntaxError('The URI port is outside the established TCP and UDP port ranges: '.$uri); } } /** * Create a new instance from a string or a stringable object. */ public static function new(Stringable|string $uri = ''): self { return match (true) { $uri instanceof UriInterface => new self($uri), default => new self(Uri::new($uri)), }; } /** * Create a new instance from a hash of parse_url parts. * * @param InputComponentMap $components a hash representation of the URI similar * to PHP parse_url function result */ public static function fromComponents(array $components): self { return new self(Uri::fromComponents($components)); } /** * Create a new instance from the environment. */ public static function fromServer(array $server): self { return new self(Uri::fromServer($server)); } /** * Create a new instance from a URI and a Base URI. * * The returned URI must be absolute. */ public static function fromBaseUri(Stringable|string $uri, Stringable|string|null $baseUri = null): self { return new self(Uri::fromBaseUri($uri, $baseUri)); } /** * Creates a new instance from a template. * * @throws TemplateCanNotBeExpanded if the variables are invalid or missing * @throws UriException if the variables are invalid or missing */ public static function fromTemplate(Stringable|string $template, iterable $variables = []): self { return new self(Uri::fromTemplate($template, $variables)); } public function getScheme(): string { return $this->uri->getScheme() ?? ''; } public function getAuthority(): string { return $this->uri->getAuthority() ?? ''; } public function getUserInfo(): string { return $this->uri->getUserInfo() ?? ''; } public function getHost(): string { return $this->uri->getHost() ?? ''; } public function getPort(): ?int { return $this->uri->getPort(); } public function getPath(): string { return $this->uri->getPath(); } public function getQuery(): string { return $this->uri->getQuery() ?? ''; } public function getFragment(): string { return $this->uri->getFragment() ?? ''; } public function __toString(): string { return $this->uri->toString(); } public function jsonSerialize(): string { return $this->uri->toString(); } /** * Safely stringify input when possible for League UriInterface compatibility. */ private function filterInput(string $str): ?string { return match ('') { $str => null, default => $str, }; } private function newInstance(UriInterface $uri): self { return match ($this->uri->toString()) { $uri->toString() => $this, default => new self($uri), }; } public function withScheme(string $scheme): self { return $this->newInstance($this->uri->withScheme($this->filterInput($scheme))); } public function withUserInfo(string $user, ?string $password = null): self { return $this->newInstance($this->uri->withUserInfo($this->filterInput($user), $password)); } public function withHost(string $host): self { return $this->newInstance($this->uri->withHost($this->filterInput($host))); } public function withPort(?int $port): self { return $this->newInstance($this->uri->withPort($port)); } public function withPath(string $path): self { return $this->newInstance($this->uri->withPath($path)); } public function withQuery(string $query): self { return $this->newInstance($this->uri->withQuery($this->filterInput($query))); } public function withFragment(string $fragment): self { return $this->newInstance($this->uri->withFragment($this->filterInput($fragment))); } /** * DEPRECATION WARNING! This method will be removed in the next major point release. * * @deprecated Since version 7.0.0 * @codeCoverageIgnore * @see Http::new() * * Create a new instance from a string. */ public static function createFromString(Stringable|string $uri = ''): self { return self::new($uri); } /** * DEPRECATION WARNING! This method will be removed in the next major point release. * * @deprecated Since version 7.0.0 * @codeCoverageIgnore * @see Http::fromComponents() * * Create a new instance from a hash of parse_url parts. * * @param InputComponentMap $components a hash representation of the URI similar * to PHP parse_url function result */ public static function createFromComponents(array $components): self { return self::fromComponents($components); } /** * DEPRECATION WARNING! This method will be removed in the next major point release. * * @deprecated Since version 7.0.0 * @codeCoverageIgnore * @see Http::fromServer() * * Create a new instance from the environment. */ public static function createFromServer(array $server): self { return self::fromServer($server); } /** * DEPRECATION WARNING! This method will be removed in the next major point release. * * @deprecated Since version 7.0.0 * @codeCoverageIgnore * @see Http::new() * * Create a new instance from a URI object. */ public static function createFromUri(Psr7UriInterface|UriInterface $uri): self { return self::new($uri); } /** * DEPRECATION WARNING! This method will be removed in the next major point release. * * @deprecated Since version 7.0.0 * @codeCoverageIgnore * @see Http::fromBaseUri() * * Create a new instance from a URI and a Base URI. * * The returned URI must be absolute. */ public static function createFromBaseUri(Stringable|string $uri, Stringable|string|null $baseUri = null): self { return self::fromBaseUri($uri, $baseUri); } } uri/BaseUri.php 0000644 00000047432 15007532371 0007423 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri; use JsonSerializable; use League\Uri\Contracts\UriAccess; use League\Uri\Contracts\UriInterface; use League\Uri\Exceptions\MissingFeature; use League\Uri\Idna\Converter; use League\Uri\IPv4\Converter as IPv4Converter; use Psr\Http\Message\UriFactoryInterface; use Psr\Http\Message\UriInterface as Psr7UriInterface; use Stringable; use function array_pop; use function array_reduce; use function count; use function end; use function explode; use function implode; use function in_array; use function preg_match; use function rawurldecode; use function str_repeat; use function str_replace; use function strpos; use function substr; /** * @phpstan-import-type ComponentMap from UriInterface */ class BaseUri implements Stringable, JsonSerializable, UriAccess { /** @var array<string,int> */ final protected const WHATWG_SPECIAL_SCHEMES = ['ftp' => 1, 'http' => 1, 'https' => 1, 'ws' => 1, 'wss' => 1]; /** @var array<string,int> */ final protected const DOT_SEGMENTS = ['.' => 1, '..' => 1]; protected readonly Psr7UriInterface|UriInterface|null $origin; protected readonly ?string $nullValue; final protected function __construct( protected readonly Psr7UriInterface|UriInterface $uri, protected readonly ?UriFactoryInterface $uriFactory ) { $this->nullValue = $this->uri instanceof Psr7UriInterface ? '' : null; $this->origin = $this->computeOrigin($this->uri, $this->nullValue); } public static function from(Stringable|string $uri, ?UriFactoryInterface $uriFactory = null): static { return new static(static::formatHost(static::filterUri($uri, $uriFactory)), $uriFactory); } public function withUriFactory(UriFactoryInterface $uriFactory): static { return new static($this->uri, $uriFactory); } public function withoutUriFactory(): static { return new static($this->uri, null); } public function getUri(): Psr7UriInterface|UriInterface { return $this->uri; } public function getUriString(): string { return $this->uri->__toString(); } public function jsonSerialize(): string { return $this->uri->__toString(); } public function __toString(): string { return $this->uri->__toString(); } public function origin(): ?self { return match (null) { $this->origin => null, default => new self($this->origin, $this->uriFactory), }; } /** * Returns the Unix filesystem path. * * The method will return null if a scheme is present and is not the `file` scheme */ public function unixPath(): ?string { return match ($this->uri->getScheme()) { 'file', $this->nullValue => rawurldecode($this->uri->getPath()), default => null, }; } /** * Returns the Windows filesystem path. * * The method will return null if a scheme is present and is not the `file` scheme */ public function windowsPath(): ?string { static $regexpWindowsPath = ',^(?<root>[a-zA-Z]:),'; if (!in_array($this->uri->getScheme(), ['file', $this->nullValue], true)) { return null; } $originalPath = $this->uri->getPath(); $path = $originalPath; if ('/' === ($path[0] ?? '')) { $path = substr($path, 1); } if (1 === preg_match($regexpWindowsPath, $path, $matches)) { $root = $matches['root']; $path = substr($path, strlen($root)); return $root.str_replace('/', '\\', rawurldecode($path)); } $host = $this->uri->getHost(); return match ($this->nullValue) { $host => str_replace('/', '\\', rawurldecode($originalPath)), default => '\\\\'.$host.'\\'.str_replace('/', '\\', rawurldecode($path)), }; } /** * Returns a string representation of a File URI according to RFC8089. * * The method will return null if the URI scheme is not the `file` scheme */ public function toRfc8089(): ?string { $path = $this->uri->getPath(); return match (true) { 'file' !== $this->uri->getScheme() => null, in_array($this->uri->getAuthority(), ['', null, 'localhost'], true) => 'file:'.match (true) { '' === $path, '/' === $path[0] => $path, default => '/'.$path, }, default => (string) $this->uri, }; } /** * Tells whether the `file` scheme base URI represents a local file. */ public function isLocalFile(): bool { return match (true) { 'file' !== $this->uri->getScheme() => false, in_array($this->uri->getAuthority(), ['', null, 'localhost'], true) => true, default => false, }; } /** * Tells whether two URI do not share the same origin. */ public function isCrossOrigin(Stringable|string $uri): bool { if (null === $this->origin) { return true; } $uri = static::filterUri($uri); $uriOrigin = $this->computeOrigin($uri, $uri instanceof Psr7UriInterface ? '' : null); return match(true) { null === $uriOrigin, $uriOrigin->__toString() !== $this->origin->__toString() => true, default => false, }; } /** * Tells whether the URI is absolute. */ public function isAbsolute(): bool { return $this->nullValue !== $this->uri->getScheme(); } /** * Tells whether the URI is a network path. */ public function isNetworkPath(): bool { return $this->nullValue === $this->uri->getScheme() && $this->nullValue !== $this->uri->getAuthority(); } /** * Tells whether the URI is an absolute path. */ public function isAbsolutePath(): bool { return $this->nullValue === $this->uri->getScheme() && $this->nullValue === $this->uri->getAuthority() && '/' === ($this->uri->getPath()[0] ?? ''); } /** * Tells whether the URI is a relative path. */ public function isRelativePath(): bool { return $this->nullValue === $this->uri->getScheme() && $this->nullValue === $this->uri->getAuthority() && '/' !== ($this->uri->getPath()[0] ?? ''); } /** * Tells whether both URI refers to the same document. */ public function isSameDocument(Stringable|string $uri): bool { return $this->normalize(static::filterUri($uri)) === $this->normalize($this->uri); } /** * Tells whether the URI contains an Internationalized Domain Name (IDN). */ public function hasIdn(): bool { return Converter::isIdn($this->uri->getHost()); } /** * Resolves a URI against a base URI using RFC3986 rules. * * This method MUST retain the state of the submitted URI instance, and return * a URI instance of the same type that contains the applied modifications. * * This method MUST be transparent when dealing with error and exceptions. * It MUST not alter or silence them apart from validating its own parameters. */ public function resolve(Stringable|string $uri): static { $uri = static::formatHost(static::filterUri($uri, $this->uriFactory)); $null = $uri instanceof Psr7UriInterface ? '' : null; if ($null !== $uri->getScheme()) { return new static( $uri->withPath(static::removeDotSegments($uri->getPath())), $this->uriFactory ); } if ($null !== $uri->getAuthority()) { return new static( $uri ->withScheme($this->uri->getScheme()) ->withPath(static::removeDotSegments($uri->getPath())), $this->uriFactory ); } $user = $null; $pass = null; $userInfo = $this->uri->getUserInfo(); if (null !== $userInfo) { [$user, $pass] = explode(':', $userInfo, 2) + [1 => null]; } [$path, $query] = $this->resolvePathAndQuery($uri); return new static( $uri ->withPath($this->removeDotSegments($path)) ->withQuery($query) ->withHost($this->uri->getHost()) ->withPort($this->uri->getPort()) ->withUserInfo((string) $user, $pass) ->withScheme($this->uri->getScheme()), $this->uriFactory ); } /** * Relativize a URI according to a base URI. * * This method MUST retain the state of the submitted URI instance, and return * a URI instance of the same type that contains the applied modifications. * * This method MUST be transparent when dealing with error and exceptions. * It MUST not alter of silence them apart from validating its own parameters. */ public function relativize(Stringable|string $uri): static { $uri = static::formatHost(static::filterUri($uri, $this->uriFactory)); if ($this->canNotBeRelativize($uri)) { return new static($uri, $this->uriFactory); } $null = $uri instanceof Psr7UriInterface ? '' : null; $uri = $uri->withScheme($null)->withPort(null)->withUserInfo($null)->withHost($null); $targetPath = $uri->getPath(); $basePath = $this->uri->getPath(); return new static( match (true) { $targetPath !== $basePath => $uri->withPath(static::relativizePath($targetPath, $basePath)), static::componentEquals('query', $uri) => $uri->withPath('')->withQuery($null), $null === $uri->getQuery() => $uri->withPath(static::formatPathWithEmptyBaseQuery($targetPath)), default => $uri->withPath(''), }, $this->uriFactory ); } final protected function computeOrigin(Psr7UriInterface|UriInterface $uri, ?string $nullValue): Psr7UriInterface|UriInterface|null { $scheme = $uri->getScheme(); if ('blob' !== $scheme) { return match (true) { isset(static::WHATWG_SPECIAL_SCHEMES[$scheme]) => $uri ->withFragment($nullValue) ->withQuery($nullValue) ->withPath('') ->withUserInfo($nullValue), default => null, }; } $components = UriString::parse($uri->getPath()); if ($uri instanceof Psr7UriInterface) { /** @var ComponentMap $components */ $components = array_map(fn ($component) => null === $component ? '' : $component, $components); } return match (true) { null !== $components['scheme'] && isset(static::WHATWG_SPECIAL_SCHEMES[strtolower($components['scheme'])]) => $uri ->withFragment($nullValue) ->withQuery($nullValue) ->withPath('') ->withHost($components['host']) ->withPort($components['port']) ->withScheme($components['scheme']) ->withUserInfo($nullValue), default => null, }; } /** * Normalizes a URI for comparison; this URI string representation is not suitable for usage as per RFC guidelines. */ final protected function normalize(Psr7UriInterface|UriInterface $uri): string { $null = $uri instanceof Psr7UriInterface ? '' : null; $path = $uri->getPath(); if ('/' === ($path[0] ?? '') || '' !== $uri->getScheme().$uri->getAuthority()) { $path = $this->removeDotSegments($path); } $query = $uri->getQuery(); $pairs = null === $query ? [] : explode('&', $query); sort($pairs); static $regexpEncodedChars = ',%(2[D|E]|3\d|4[1-9|A-F]|5[\d|AF]|6[1-9|A-F]|7[\d|E]),i'; $value = preg_replace_callback( $regexpEncodedChars, static fn (array $matches): string => rawurldecode($matches[0]), [$path, implode('&', $pairs)] ) ?? ['', $null]; [$path, $query] = $value + ['', $null]; if ($null !== $uri->getAuthority() && '' === $path) { $path = '/'; } return $uri ->withHost(Uri::fromComponents(['host' => $uri->getHost()])->getHost()) ->withPath($path) ->withQuery([] === $pairs ? $null : $query) ->withFragment($null) ->__toString(); } /** * Input URI normalization to allow Stringable and string URI. */ final protected static function filterUri(Stringable|string $uri, UriFactoryInterface|null $uriFactory = null): Psr7UriInterface|UriInterface { return match (true) { $uri instanceof UriAccess => $uri->getUri(), $uri instanceof Psr7UriInterface, $uri instanceof UriInterface => $uri, $uriFactory instanceof UriFactoryInterface => $uriFactory->createUri((string) $uri), default => Uri::new($uri), }; } /** * Remove dot segments from the URI path as per RFC specification. */ final protected function removeDotSegments(string $path): string { if (!str_contains($path, '.')) { return $path; } $reducer = function (array $carry, string $segment): array { if ('..' === $segment) { array_pop($carry); return $carry; } if (!isset(static::DOT_SEGMENTS[$segment])) { $carry[] = $segment; } return $carry; }; $oldSegments = explode('/', $path); $newPath = implode('/', array_reduce($oldSegments, $reducer(...), [])); if (isset(static::DOT_SEGMENTS[end($oldSegments)])) { $newPath .= '/'; } // @codeCoverageIgnoreStart // added because some PSR-7 implementations do not respect RFC3986 if (str_starts_with($path, '/') && !str_starts_with($newPath, '/')) { return '/'.$newPath; } // @codeCoverageIgnoreEnd return $newPath; } /** * Resolves an URI path and query component. * * @return array{0:string, 1:string|null} */ final protected function resolvePathAndQuery(Psr7UriInterface|UriInterface $uri): array { $targetPath = $uri->getPath(); $null = $uri instanceof Psr7UriInterface ? '' : null; if (str_starts_with($targetPath, '/')) { return [$targetPath, $uri->getQuery()]; } if ('' === $targetPath) { $targetQuery = $uri->getQuery(); if ($null === $targetQuery) { $targetQuery = $this->uri->getQuery(); } $targetPath = $this->uri->getPath(); //@codeCoverageIgnoreStart //because some PSR-7 Uri implementations allow this RFC3986 forbidden construction if (null !== $this->uri->getAuthority() && !str_starts_with($targetPath, '/')) { $targetPath = '/'.$targetPath; } //@codeCoverageIgnoreEnd return [$targetPath, $targetQuery]; } $basePath = $this->uri->getPath(); if (null !== $this->uri->getAuthority() && '' === $basePath) { $targetPath = '/'.$targetPath; } if ('' !== $basePath) { $segments = explode('/', $basePath); array_pop($segments); if ([] !== $segments) { $targetPath = implode('/', $segments).'/'.$targetPath; } } return [$targetPath, $uri->getQuery()]; } /** * Tells whether the component value from both URI object equals. * * @pqram 'query'|'authority'|'scheme' $property */ final protected function componentEquals(string $property, Psr7UriInterface|UriInterface $uri): bool { $getComponent = function (string $property, Psr7UriInterface|UriInterface $uri): ?string { $component = match ($property) { 'query' => $uri->getQuery(), 'authority' => $uri->getAuthority(), default => $uri->getScheme(), }; return match (true) { $uri instanceof UriInterface, '' !== $component => $component, default => null, }; }; return $getComponent($property, $uri) === $getComponent($property, $this->uri); } /** * Filter the URI object. */ final protected static function formatHost(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface { $host = $uri->getHost(); try { $converted = IPv4Converter::fromEnvironment()->toDecimal($host); } catch (MissingFeature) { $converted = null; } return match (true) { null !== $converted => $uri->withHost($converted), '' === $host, $uri instanceof UriInterface => $uri, default => $uri->withHost((string) Uri::fromComponents(['host' => $host])->getHost()), }; } /** * Tells whether the submitted URI object can be relativized. */ final protected function canNotBeRelativize(Psr7UriInterface|UriInterface $uri): bool { return !static::componentEquals('scheme', $uri) || !static::componentEquals('authority', $uri) || static::from($uri)->isRelativePath(); } /** * Relatives the URI for an authority-less target URI. */ final protected static function relativizePath(string $path, string $basePath): string { $baseSegments = static::getSegments($basePath); $targetSegments = static::getSegments($path); $targetBasename = array_pop($targetSegments); array_pop($baseSegments); foreach ($baseSegments as $offset => $segment) { if (!isset($targetSegments[$offset]) || $segment !== $targetSegments[$offset]) { break; } unset($baseSegments[$offset], $targetSegments[$offset]); } $targetSegments[] = $targetBasename; return static::formatPath( str_repeat('../', count($baseSegments)).implode('/', $targetSegments), $basePath ); } /** * returns the path segments. * * @return string[] */ final protected static function getSegments(string $path): array { return explode('/', match (true) { '' === $path, '/' !== $path[0] => $path, default => substr($path, 1), }); } /** * Formatting the path to keep a valid URI. */ final protected static function formatPath(string $path, string $basePath): string { $colonPosition = strpos($path, ':'); $slashPosition = strpos($path, '/'); return match (true) { '' === $path => match (true) { '' === $basePath, '/' === $basePath => $basePath, default => './', }, false === $colonPosition => $path, false === $slashPosition, $colonPosition < $slashPosition => "./$path", default => $path, }; } /** * Formatting the path to keep a resolvable URI. */ final protected static function formatPathWithEmptyBaseQuery(string $path): string { $targetSegments = static::getSegments($path); /** @var string $basename */ $basename = end($targetSegments); return '' === $basename ? './' : $basename; } } uri/Uri.php 0000644 00000120104 15007532371 0006614 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri; use finfo; use League\Uri\Contracts\UriComponentInterface; use League\Uri\Contracts\UriException; use League\Uri\Contracts\UriInterface; use League\Uri\Exceptions\ConversionFailed; use League\Uri\Exceptions\MissingFeature; use League\Uri\Exceptions\SyntaxError; use League\Uri\Idna\Converter as IdnConverter; use League\Uri\UriTemplate\TemplateCanNotBeExpanded; use Psr\Http\Message\UriInterface as Psr7UriInterface; use SensitiveParameter; use Stringable; use function array_filter; use function array_key_first; use function array_map; use function base64_decode; use function base64_encode; use function count; use function explode; use function file_get_contents; use function filter_var; use function implode; use function in_array; use function inet_pton; use function ltrim; use function preg_match; use function preg_replace_callback; use function rawurlencode; use function str_contains; use function str_replace; use function strlen; use function strpos; use function strspn; use function strtolower; use function substr; use const FILEINFO_MIME; use const FILTER_FLAG_IPV4; use const FILTER_FLAG_IPV6; use const FILTER_NULL_ON_FAILURE; use const FILTER_VALIDATE_BOOLEAN; use const FILTER_VALIDATE_IP; /** * @phpstan-import-type ComponentMap from UriString * @phpstan-import-type InputComponentMap from UriString */ final class Uri implements UriInterface { /** * RFC3986 invalid characters. * * @link https://tools.ietf.org/html/rfc3986#section-2.2 * * @var string */ private const REGEXP_INVALID_CHARS = '/[\x00-\x1f\x7f]/'; /** * RFC3986 schema regular expression pattern. * * @link https://tools.ietf.org/html/rfc3986#section-3.1 * * @var string */ private const REGEXP_SCHEME = ',^[a-z]([-a-z\d+.]+)?$,i'; /** * RFC3986 host identified by a registered name regular expression pattern. * * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 * * @var string */ private const REGEXP_HOST_REGNAME = '/^( (?<unreserved>[a-z\d_~\-\.])| (?<sub_delims>[!$&\'()*+,;=])| (?<encoded>%[A-F\d]{2}) )+$/x'; /** * RFC3986 delimiters of the generic URI components regular expression pattern. * * @link https://tools.ietf.org/html/rfc3986#section-2.2 * * @var string */ private const REGEXP_HOST_GEN_DELIMS = '/[:\/?#\[\]@ ]/'; // Also includes space. /** * RFC3986 IPvFuture regular expression pattern. * * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 * * @var string */ private const REGEXP_HOST_IPFUTURE = '/^ v(?<version>[A-F\d])+\. (?: (?<unreserved>[a-z\d_~\-\.])| (?<sub_delims>[!$&\'()*+,;=:]) # also include the : character )+ $/ix'; /** * RFC3986 IPvFuture host and port component. * * @var string */ private const REGEXP_HOST_PORT = ',^(?<host>(\[.*]|[^:])*)(:(?<port>[^/?#]*))?$,x'; /** * Significant 10 bits of IP to detect Zone ID regular expression pattern. * * @var string */ private const HOST_ADDRESS_BLOCK = "\xfe\x80"; /** * Regular expression pattern to for file URI. * <volume> contains the volume but not the volume separator. * The volume separator may be URL-encoded (`|` as `%7C`) by ::formatPath(), * so we account for that here. * * @var string */ private const REGEXP_FILE_PATH = ',^(?<delim>/)?(?<volume>[a-zA-Z])(?:[:|\|]|%7C)(?<rest>.*)?,'; /** * Mimetype regular expression pattern. * * @link https://tools.ietf.org/html/rfc2397 * * @var string */ private const REGEXP_MIMETYPE = ',^\w+/[-.\w]+(?:\+[-.\w]+)?$,'; /** * Base64 content regular expression pattern. * * @link https://tools.ietf.org/html/rfc2397 * * @var string */ private const REGEXP_BINARY = ',(;|^)base64$,'; /** * Windows file path string regular expression pattern. * <root> contains both the volume and volume separator. * * @var string */ private const REGEXP_WINDOW_PATH = ',^(?<root>[a-zA-Z][:|\|]),'; /** * Supported schemes and corresponding default port. * * @var array<string, int|null> */ private const SCHEME_DEFAULT_PORT = [ 'data' => null, 'file' => null, 'ftp' => 21, 'gopher' => 70, 'http' => 80, 'https' => 443, 'ws' => 80, 'wss' => 443, ]; /** * Maximum number of formatted host cached. * * @var int */ private const MAXIMUM_FORMATTED_HOST_CACHED = 100; /** * All ASCII letters sorted by typical frequency of occurrence. * * @var string */ private const ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; /** @readonly */ private ?string $scheme; /** @readonly */ private ?string $userInfo; /** @readonly */ private ?string $host; /** @readonly */ private ?int $port; /** @readonly */ private ?string $authority; /** @readonly */ private string $path; /** @readonly */ private ?string $query; /** @readonly */ private ?string $fragment; private ?string $uri; private function __construct( ?string $scheme, ?string $user, #[SensitiveParameter] ?string $pass, ?string $host, ?int $port, string $path, ?string $query, ?string $fragment ) { $this->scheme = $this->formatScheme($scheme); $this->userInfo = $this->formatUserInfo($user, $pass); $this->host = $this->formatHost($host); $this->port = $this->formatPort($port); $this->authority = $this->setAuthority(); $this->path = $this->formatPath($path); $this->query = Encoder::encodeQueryOrFragment($query); $this->fragment = Encoder::encodeQueryOrFragment($fragment); $this->assertValidState(); } /** * Format the Scheme and Host component. * * @throws SyntaxError if the scheme is invalid */ private function formatScheme(?string $scheme): ?string { if (null === $scheme || array_key_exists($scheme, self::SCHEME_DEFAULT_PORT)) { return $scheme; } $formattedScheme = strtolower($scheme); if (array_key_exists($formattedScheme, self::SCHEME_DEFAULT_PORT) || 1 === preg_match(self::REGEXP_SCHEME, $formattedScheme)) { return $formattedScheme; } throw new SyntaxError('The scheme `'.$scheme.'` is invalid.'); } /** * Set the UserInfo component. */ private function formatUserInfo( ?string $user, #[SensitiveParameter] ?string $password ): ?string { return match (null) { $password => Encoder::encodeUser($user), default => Encoder::encodeUser($user).':'.Encoder::encodePassword($password), }; } /** * Validate and Format the Host component. */ private function formatHost(?string $host): ?string { if (null === $host || '' === $host) { return $host; } static $formattedHostCache = []; if (isset($formattedHostCache[$host])) { return $formattedHostCache[$host]; } $formattedHost = '[' === $host[0] ? $this->formatIp($host) : $this->formatRegisteredName($host); $formattedHostCache[$host] = $formattedHost; if (self::MAXIMUM_FORMATTED_HOST_CACHED < count($formattedHostCache)) { unset($formattedHostCache[array_key_first($formattedHostCache)]); } return $formattedHost; } /** * Validate and format a registered name. * * The host is converted to its ascii representation if needed * * @throws MissingFeature if the submitted host required missing or misconfigured IDN support * @throws SyntaxError if the submitted host is not a valid registered name * @throws ConversionFailed if the submitted IDN host cannot be converted to a valid ascii form */ private function formatRegisteredName(string $host): string { $formattedHost = rawurldecode($host); return match (1) { preg_match(self::REGEXP_HOST_REGNAME, $formattedHost) => $formattedHost, preg_match(self::REGEXP_HOST_GEN_DELIMS, $formattedHost) => throw new SyntaxError('The host `'.$host.'` is invalid : a registered name cannot contain URI delimiters or spaces.'), default => IdnConverter::toAsciiOrFail($host), }; } /** * Validate and Format the IPv6/IPvfuture host. * * @throws SyntaxError if the submitted host is not a valid IP host */ private function formatIp(string $host): string { $ip = substr($host, 1, -1); if (false !== filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { return $host; } if (1 === preg_match(self::REGEXP_HOST_IPFUTURE, $ip, $matches) && !in_array($matches['version'], ['4', '6'], true)) { return $host; } $pos = strpos($ip, '%'); if (false === $pos) { throw new SyntaxError('The host `'.$host.'` is invalid : the IP host is malformed.'); } if (1 === preg_match(self::REGEXP_HOST_GEN_DELIMS, rawurldecode(substr($ip, $pos)))) { throw new SyntaxError('The host `'.$host.'` is invalid : the IP host is malformed.'); } $ip = substr($ip, 0, $pos); if (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { throw new SyntaxError('The host `'.$host.'` is invalid : the IP host is malformed.'); } //Only the address block fe80::/10 can have a Zone ID attach to //let's detect the link local significant 10 bits if (str_starts_with((string)inet_pton($ip), self::HOST_ADDRESS_BLOCK)) { return $host; } throw new SyntaxError('The host `'.$host.'` is invalid : the IP host is malformed.'); } /** * Format the Port component. * * @throws SyntaxError */ private function formatPort(?int $port = null): ?int { $defaultPort = self::SCHEME_DEFAULT_PORT[$this->scheme] ?? null; return match (true) { null === $port, $defaultPort === $port => null, 0 > $port => throw new SyntaxError('The port `'.$port.'` is invalid.'), default => $port, }; } /** * Create a new instance from a string. */ public static function new(Stringable|string $uri = ''): self { $components = match (true) { $uri instanceof UriInterface => $uri->getComponents(), $uri instanceof Psr7UriInterface => (function (Psr7UriInterface $uri): array { $normalize = fn ($component) => '' !== $component ? $component : null; $userInfo = $uri->getUserInfo(); [$user, $pass] = '' !== $userInfo ? explode(':', $userInfo, 2) : ['', '']; return [ 'scheme' => $normalize($uri->getScheme()), 'user' => $normalize($user), 'pass' => $normalize($pass), 'host' => $normalize($uri->getHost()), 'port' => $uri->getPort(), 'path' => $uri->getPath(), 'query' => $normalize($uri->getQuery()), 'fragment' => $normalize($uri->getFragment()), ]; })($uri), default => UriString::parse($uri), }; return new self( $components['scheme'], $components['user'], $components['pass'], $components['host'], $components['port'], $components['path'], $components['query'], $components['fragment'] ); } /** * Creates a new instance from a URI and a Base URI. * * The returned URI must be absolute. */ public static function fromBaseUri(Stringable|string $uri, Stringable|string|null $baseUri = null): self { $uri = self::new($uri); $baseUri = BaseUri::from($baseUri ?? $uri); /** @var self $uri */ $uri = match (true) { $baseUri->isAbsolute() => $baseUri->resolve($uri)->getUri(), default => throw new SyntaxError('the URI `'.$baseUri.'` must be absolute.'), }; return $uri; } /** * Creates a new instance from a template. * * @throws TemplateCanNotBeExpanded if the variables are invalid or missing * @throws UriException if the resulting expansion cannot be converted to a UriInterface instance */ public static function fromTemplate(UriTemplate|Stringable|string $template, iterable $variables = []): self { return match (true) { $template instanceof UriTemplate => self::fromComponents($template->expand($variables)->getComponents()), $template instanceof UriTemplate\Template => self::new($template->expand($variables)), default => self::new(UriTemplate\Template::new($template)->expand($variables)), }; } /** * Create a new instance from a hash representation of the URI similar * to PHP parse_url function result. * * @param InputComponentMap $components a hash representation of the URI similar to PHP parse_url function result */ public static function fromComponents(array $components = []): self { $components += [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => '', 'query' => null, 'fragment' => null, ]; if (null === $components['path']) { $components['path'] = ''; } return new self( $components['scheme'], $components['user'], $components['pass'], $components['host'], $components['port'], $components['path'], $components['query'], $components['fragment'] ); } /** * Create a new instance from a data file path. * * @param resource|null $context * * @throws MissingFeature If ext/fileinfo is not installed * @throws SyntaxError If the file does not exist or is not readable */ public static function fromFileContents(Stringable|string $path, $context = null): self { FeatureDetection::supportsFileDetection(); $path = (string) $path; $fileArguments = [$path, false]; $mimeArguments = [$path, FILEINFO_MIME]; if (null !== $context) { $fileArguments[] = $context; $mimeArguments[] = $context; } set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true); $raw = file_get_contents(...$fileArguments); restore_error_handler(); if (false === $raw) { throw new SyntaxError('The file `'.$path.'` does not exist or is not readable.'); } $mimetype = (string) (new finfo(FILEINFO_MIME))->file(...$mimeArguments); return Uri::fromComponents([ 'scheme' => 'data', 'path' => str_replace(' ', '', $mimetype.';base64,'.base64_encode($raw)), ]); } /** * Create a new instance from a data string. * * @throws SyntaxError If the parameter syntax is invalid */ public static function fromData(string $data, string $mimetype = '', string $parameters = ''): self { static $regexpMimetype = ',^\w+/[-.\w]+(?:\+[-.\w]+)?$,'; $mimetype = match (true) { '' === $mimetype => 'text/plain', 1 === preg_match($regexpMimetype, $mimetype) => $mimetype, default => throw new SyntaxError('Invalid mimeType, `'.$mimetype.'`.'), }; if ('' === $parameters) { return self::fromComponents([ 'scheme' => 'data', 'path' => self::formatDataPath($mimetype.','.rawurlencode($data)), ]); } $isInvalidParameter = static function (string $parameter): bool { $properties = explode('=', $parameter); return 2 !== count($properties) || 'base64' === strtolower($properties[0]); }; if (str_starts_with($parameters, ';')) { $parameters = substr($parameters, 1); } return match ([]) { array_filter(explode(';', $parameters), $isInvalidParameter) => self::fromComponents([ 'scheme' => 'data', 'path' => self::formatDataPath($mimetype.';'.$parameters.','.rawurlencode($data)), ]), default => throw new SyntaxError(sprintf('Invalid mediatype parameters, `%s`.', $parameters)) }; } /** * Create a new instance from a Unix path string. */ public static function fromUnixPath(Stringable|string $path): self { $path = implode('/', array_map(rawurlencode(...), explode('/', (string) $path))); return Uri::fromComponents(match (true) { '/' !== ($path[0] ?? '') => ['path' => $path], default => ['path' => $path, 'scheme' => 'file', 'host' => ''], }); } /** * Create a new instance from a local Windows path string. */ public static function fromWindowsPath(Stringable|string $path): self { $path = (string) $path; $root = ''; if (1 === preg_match(self::REGEXP_WINDOW_PATH, $path, $matches)) { $root = substr($matches['root'], 0, -1).':'; $path = substr($path, strlen($root)); } $path = str_replace('\\', '/', $path); $path = implode('/', array_map(rawurlencode(...), explode('/', $path))); //Local Windows absolute path if ('' !== $root) { return Uri::fromComponents(['path' => '/'.$root.$path, 'scheme' => 'file', 'host' => '']); } //UNC Windows Path if (!str_starts_with($path, '//')) { return Uri::fromComponents(['path' => $path]); } [$host, $path] = explode('/', substr($path, 2), 2) + [1 => '']; return Uri::fromComponents(['host' => $host, 'path' => '/'.$path, 'scheme' => 'file']); } /** * Creates a new instance from a RFC8089 compatible URI. * * @see https://datatracker.ietf.org/doc/html/rfc8089 */ public static function fromRfc8089(Stringable|string $uri): UriInterface { $fileUri = self::new((string) preg_replace(',^(file:/)([^/].*)$,i', 'file:///$2', (string) $uri)); $scheme = $fileUri->getScheme(); return match (true) { 'file' !== $scheme => throw new SyntaxError('As per RFC8089, the URI scheme must be `file`.'), 'localhost' === $fileUri->getAuthority() => $fileUri->withHost(''), default => $fileUri, }; } /** * Create a new instance from the environment. */ public static function fromServer(array $server): self { $components = ['scheme' => self::fetchScheme($server)]; [$components['user'], $components['pass']] = self::fetchUserInfo($server); [$components['host'], $components['port']] = self::fetchHostname($server); [$components['path'], $components['query']] = self::fetchRequestUri($server); return Uri::fromComponents($components); } /** * Returns the environment scheme. */ private static function fetchScheme(array $server): string { $server += ['HTTPS' => '']; return match (true) { false !== filter_var($server['HTTPS'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) => 'https', default => 'http', }; } /** * Returns the environment user info. * * @return non-empty-array{0: ?string, 1: ?string} */ private static function fetchUserInfo(array $server): array { $server += ['PHP_AUTH_USER' => null, 'PHP_AUTH_PW' => null, 'HTTP_AUTHORIZATION' => '']; $user = $server['PHP_AUTH_USER']; $pass = $server['PHP_AUTH_PW']; if (str_starts_with(strtolower($server['HTTP_AUTHORIZATION']), 'basic')) { $userinfo = base64_decode(substr($server['HTTP_AUTHORIZATION'], 6), true); if (false === $userinfo) { throw new SyntaxError('The user info could not be detected'); } [$user, $pass] = explode(':', $userinfo, 2) + [1 => null]; } if (null !== $user) { $user = rawurlencode($user); } if (null !== $pass) { $pass = rawurlencode($pass); } return [$user, $pass]; } /** * Returns the environment host. * * @throws SyntaxError If the host cannot be detected * * @return array{0:string|null, 1:int|null} */ private static function fetchHostname(array $server): array { $server += ['SERVER_PORT' => null]; if (null !== $server['SERVER_PORT']) { $server['SERVER_PORT'] = (int) $server['SERVER_PORT']; } if (isset($server['HTTP_HOST']) && 1 === preg_match(self::REGEXP_HOST_PORT, $server['HTTP_HOST'], $matches)) { $matches += ['host' => null, 'port' => null]; if (null !== $matches['port']) { $matches['port'] = (int) $matches['port']; } if (null !== $matches['host']) { $matches['host'] = (string) $matches['host']; } return [$matches['host'], $matches['port'] ?? $server['SERVER_PORT']]; } if (!isset($server['SERVER_ADDR'])) { throw new SyntaxError('The host could not be detected'); } if (false === filter_var($server['SERVER_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { return ['['.$server['SERVER_ADDR'].']', $server['SERVER_PORT']]; } return [$server['SERVER_ADDR'], $server['SERVER_PORT']]; } /** * Returns the environment path. * * @return non-empty-array{0:?string, 1:?string} */ private static function fetchRequestUri(array $server): array { $server += ['IIS_WasUrlRewritten' => null, 'UNENCODED_URL' => '', 'PHP_SELF' => '', 'QUERY_STRING' => null]; if ('1' === $server['IIS_WasUrlRewritten'] && '' !== $server['UNENCODED_URL']) { return explode('?', $server['UNENCODED_URL'], 2) + [1 => null]; } if (isset($server['REQUEST_URI'])) { [$path] = explode('?', $server['REQUEST_URI'], 2); $query = ('' !== $server['QUERY_STRING']) ? $server['QUERY_STRING'] : null; return [$path, $query]; } return [$server['PHP_SELF'], $server['QUERY_STRING']]; } /** * Generate the URI authority part. */ private function setAuthority(): ?string { $authority = null; if (null !== $this->userInfo) { $authority = $this->userInfo.'@'; } if (null !== $this->host) { $authority .= $this->host; } if (null !== $this->port) { $authority .= ':'.$this->port; } return $authority; } /** * Format the Path component. */ private function formatPath(string $path): string { return match (true) { 'data' === $this->scheme => Encoder::encodePath(self::formatDataPath($path)), 'file' === $this->scheme => $this->formatFilePath(Encoder::encodePath($path)), default => Encoder::encodePath($path), }; } /** * Filter the Path component. * * @link https://tools.ietf.org/html/rfc2397 * * @throws SyntaxError If the path is not compliant with RFC2397 */ private static function formatDataPath(string $path): string { if ('' == $path) { return 'text/plain;charset=us-ascii,'; } if (strlen($path) !== strspn($path, self::ASCII) || !str_contains($path, ',')) { throw new SyntaxError('The path `'.$path.'` is invalid according to RFC2937.'); } $parts = explode(',', $path, 2) + [1 => null]; $mediatype = explode(';', (string) $parts[0], 2) + [1 => null]; $data = (string) $parts[1]; $mimetype = $mediatype[0]; if (null === $mimetype || '' === $mimetype) { $mimetype = 'text/plain'; } $parameters = $mediatype[1]; if (null === $parameters || '' === $parameters) { $parameters = 'charset=us-ascii'; } self::assertValidPath($mimetype, $parameters, $data); return $mimetype.';'.$parameters.','.$data; } /** * Assert the path is a compliant with RFC2397. * * @link https://tools.ietf.org/html/rfc2397 * * @throws SyntaxError If the mediatype or the data are not compliant with the RFC2397 */ private static function assertValidPath(string $mimetype, string $parameters, string $data): void { if (1 !== preg_match(self::REGEXP_MIMETYPE, $mimetype)) { throw new SyntaxError('The path mimetype `'.$mimetype.'` is invalid.'); } $isBinary = 1 === preg_match(self::REGEXP_BINARY, $parameters, $matches); if ($isBinary) { $parameters = substr($parameters, 0, - strlen($matches[0])); } $res = array_filter(array_filter(explode(';', $parameters), self::validateParameter(...))); if ([] !== $res) { throw new SyntaxError('The path parameters `'.$parameters.'` is invalid.'); } if (!$isBinary) { return; } $res = base64_decode($data, true); if (false === $res || $data !== base64_encode($res)) { throw new SyntaxError('The path data `'.$data.'` is invalid.'); } } /** * Validate mediatype parameter. */ private static function validateParameter(string $parameter): bool { $properties = explode('=', $parameter); return 2 != count($properties) || 'base64' === strtolower($properties[0]); } /** * Format path component for file scheme. */ private function formatFilePath(string $path): string { return (string) preg_replace_callback( self::REGEXP_FILE_PATH, static fn (array $matches): string => $matches['delim'].$matches['volume'].':'.$matches['rest'], $path ); } /** * assert the URI internal state is valid. * * @link https://tools.ietf.org/html/rfc3986#section-3 * @link https://tools.ietf.org/html/rfc3986#section-3.3 * * @throws SyntaxError if the URI is in an invalid state according to RFC3986 * @throws SyntaxError if the URI is in an invalid state according to scheme specific rules */ private function assertValidState(): void { if (null !== $this->authority && ('' !== $this->path && '/' !== $this->path[0])) { throw new SyntaxError('If an authority is present the path must be empty or start with a `/`.'); } if (null === $this->authority && str_starts_with($this->path, '//')) { throw new SyntaxError('If there is no authority the path `'.$this->path.'` cannot start with a `//`.'); } $pos = strpos($this->path, ':'); if (null === $this->authority && null === $this->scheme && false !== $pos && !str_contains(substr($this->path, 0, $pos), '/') ) { throw new SyntaxError('In absence of a scheme and an authority the first path segment cannot contain a colon (":") character.'); } $this->uri = null; if (! match ($this->scheme) { 'data' => $this->isUriWithSchemeAndPathOnly(), 'file' => $this->isUriWithSchemeHostAndPathOnly(), 'ftp', 'gopher' => $this->isNonEmptyHostUriWithoutFragmentAndQuery(), 'http', 'https' => $this->isNonEmptyHostUri(), 'ws', 'wss' => $this->isNonEmptyHostUriWithoutFragment(), default => true, }) { throw new SyntaxError('The uri `'.$this.'` is invalid for the `'.$this->scheme.'` scheme.'); } } /** * URI validation for URI schemes which allows only scheme and path components. */ private function isUriWithSchemeAndPathOnly(): bool { return null === $this->authority && null === $this->query && null === $this->fragment; } /** * URI validation for URI schemes which allows only scheme, host and path components. */ private function isUriWithSchemeHostAndPathOnly(): bool { return null === $this->userInfo && null === $this->port && null === $this->query && null === $this->fragment && !('' != $this->scheme && null === $this->host); } /** * URI validation for URI schemes which disallow the empty '' host. */ private function isNonEmptyHostUri(): bool { return '' !== $this->host && !(null !== $this->scheme && null === $this->host); } /** * URI validation for URIs schemes which disallow the empty '' host * and forbids the fragment component. */ private function isNonEmptyHostUriWithoutFragment(): bool { return $this->isNonEmptyHostUri() && null === $this->fragment; } /** * URI validation for URIs schemes which disallow the empty '' host * and forbids fragment and query components. */ private function isNonEmptyHostUriWithoutFragmentAndQuery(): bool { return $this->isNonEmptyHostUri() && null === $this->fragment && null === $this->query; } /** * Generate the URI string representation from its components. * * @link https://tools.ietf.org/html/rfc3986#section-5.3 */ private function getUriString( ?string $scheme, ?string $authority, string $path, ?string $query, ?string $fragment ): string { if (null !== $scheme) { $scheme = $scheme.':'; } if (null !== $authority) { $authority = '//'.$authority; } if (null !== $query) { $query = '?'.$query; } if (null !== $fragment) { $fragment = '#'.$fragment; } return $scheme.$authority.$path.$query.$fragment; } public function toString(): string { return $this->uri ??= $this->getUriString( $this->scheme, $this->authority, $this->path, $this->query, $this->fragment ); } /** * {@inheritDoc} */ public function __toString(): string { return $this->toString(); } /** * {@inheritDoc} */ public function jsonSerialize(): string { return $this->toString(); } /** * @return ComponentMap */ public function getComponents(): array { [$user, $pass] = null !== $this->userInfo ? explode(':', $this->userInfo, 2) : [null, null]; return [ 'scheme' => $this->scheme, 'user' => $user, 'pass' => $pass, 'host' => $this->host, 'port' => $this->port, 'path' => $this->path, 'query' => $this->query, 'fragment' => $this->fragment, ]; } /** * {@inheritDoc} */ public function getScheme(): ?string { return $this->scheme; } /** * {@inheritDoc} */ public function getAuthority(): ?string { return $this->authority; } /** * {@inheritDoc} */ public function getUserInfo(): ?string { return $this->userInfo; } /** * {@inheritDoc} */ public function getHost(): ?string { return $this->host; } /** * {@inheritDoc} */ public function getPort(): ?int { return $this->port; } /** * {@inheritDoc} */ public function getPath(): string { return match (true) { str_starts_with($this->path, '//') => '/'.ltrim($this->path, '/'), default => $this->path, }; } /** * {@inheritDoc} */ public function getQuery(): ?string { return $this->query; } /** * {@inheritDoc} */ public function getFragment(): ?string { return $this->fragment; } /** * {@inheritDoc} */ public function withScheme(Stringable|string|null $scheme): UriInterface { $scheme = $this->formatScheme($this->filterString($scheme)); if ($scheme === $this->scheme) { return $this; } $clone = clone $this; $clone->scheme = $scheme; $clone->port = $clone->formatPort($clone->port); $clone->authority = $clone->setAuthority(); $clone->assertValidState(); return $clone; } /** * Filter a string. * * @throws SyntaxError if the submitted data cannot be converted to string */ private function filterString(Stringable|string|null $str): ?string { $str = match (true) { $str instanceof UriComponentInterface => $str->value(), null === $str => null, default => (string) $str, }; return match (true) { null === $str => null, 1 === preg_match(self::REGEXP_INVALID_CHARS, $str) => throw new SyntaxError('The component `'.$str.'` contains invalid characters.'), default => $str, }; } public function withUserInfo( Stringable|string|null $user, #[SensitiveParameter] Stringable|string|null $password = null ): UriInterface { $user_info = null; $user = $this->filterString($user); if (null !== $password) { $password = $this->filterString($password); } if ('' !== $user) { $user_info = $this->formatUserInfo($user, $password); } if ($user_info === $this->userInfo) { return $this; } $clone = clone $this; $clone->userInfo = $user_info; $clone->authority = $clone->setAuthority(); $clone->assertValidState(); return $clone; } public function withHost(Stringable|string|null $host): UriInterface { $host = $this->formatHost($this->filterString($host)); if ($host === $this->host) { return $this; } $clone = clone $this; $clone->host = $host; $clone->authority = $clone->setAuthority(); $clone->assertValidState(); return $clone; } public function withPort(int|null $port): UriInterface { $port = $this->formatPort($port); if ($port === $this->port) { return $this; } $clone = clone $this; $clone->port = $port; $clone->authority = $clone->setAuthority(); $clone->assertValidState(); return $clone; } public function withPath(Stringable|string $path): UriInterface { $path = $this->filterString($path); if (null === $path) { throw new SyntaxError('The path component cannot be null.'); } $path = $this->formatPath($path); if ($path === $this->path) { return $this; } $clone = clone $this; $clone->path = $path; $clone->assertValidState(); return $clone; } public function withQuery(Stringable|string|null $query): UriInterface { $query = Encoder::encodeQueryOrFragment($this->filterString($query)); if ($query === $this->query) { return $this; } $clone = clone $this; $clone->query = $query; $clone->assertValidState(); return $clone; } public function withFragment(Stringable|string|null $fragment): UriInterface { $fragment = Encoder::encodeQueryOrFragment($this->filterString($fragment)); if ($fragment === $this->fragment) { return $this; } $clone = clone $this; $clone->fragment = $fragment; $clone->assertValidState(); return $clone; } /** * DEPRECATION WARNING! This method will be removed in the next major point release. * * @deprecated Since version 7.0.0 * @codeCoverageIgnore * @see Uri::new() */ public static function createFromString(Stringable|string $uri = ''): self { return self::new($uri); } /** * DEPRECATION WARNING! This method will be removed in the next major point release. * * @deprecated Since version 7.0.0 * @codeCoverageIgnore * @see Uri::fromComponents() * * @param InputComponentMap $components a hash representation of the URI similar to PHP parse_url function result */ public static function createFromComponents(array $components = []): self { return self::fromComponents($components); } /** * DEPRECATION WARNING! This method will be removed in the next major point release. * * @param resource|null $context * * @throws MissingFeature If ext/fileinfo is not installed * @throws SyntaxError If the file does not exist or is not readable * @see Uri::fromFileContents() * * @deprecated Since version 7.0.0 * @codeCoverageIgnore */ public static function createFromDataPath(string $path, $context = null): self { return self::fromFileContents($path, $context); } /** * DEPRECATION WARNING! This method will be removed in the next major point release. * * @deprecated Since version 7.0.0 * @codeCoverageIgnore * @see Uri::fromBaseUri() * * Creates a new instance from a URI and a Base URI. * * The returned URI must be absolute. */ public static function createFromBaseUri( Stringable|UriInterface|String $uri, Stringable|UriInterface|String|null $baseUri = null ): UriInterface { return self::fromBaseUri($uri, $baseUri); } /** * DEPRECATION WARNING! This method will be removed in the next major point release. * * @deprecated Since version 7.0.0 * @codeCoverageIgnore * @see Uri::fromUnixPath() * * Create a new instance from a Unix path string. */ public static function createFromUnixPath(string $uri = ''): self { return self::fromUnixPath($uri); } /** * DEPRECATION WARNING! This method will be removed in the next major point release. * * @deprecated Since version 7.0.0 * @codeCoverageIgnore * @see Uri::fromWindowsPath() * * Create a new instance from a local Windows path string. */ public static function createFromWindowsPath(string $uri = ''): self { return self::fromWindowsPath($uri); } /** * DEPRECATION WARNING! This method will be removed in the next major point release. * * @deprecated Since version 7.0.0 * @codeCoverageIgnore * @see Uri::new() * * Create a new instance from a URI object. */ public static function createFromUri(Psr7UriInterface|UriInterface $uri): self { return self::new($uri); } /** * DEPRECATION WARNING! This method will be removed in the next major point release. * * @deprecated Since version 7.0.0 * @codeCoverageIgnore * @see Uri::fromServer() * * Create a new instance from the environment. */ public static function createFromServer(array $server): self { return self::fromServer($server); } } uri/HttpFactory.php 0000644 00000001033 15007532371 0010323 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri; use Psr\Http\Message\UriFactoryInterface; use Psr\Http\Message\UriInterface; final class HttpFactory implements UriFactoryInterface { public function createUri(string $uri = ''): UriInterface { return Http::new($uri); } } uri/UriResolver.php 0000644 00000003350 15007532371 0010341 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri; use League\Uri\Contracts\UriInterface; use Psr\Http\Message\UriInterface as Psr7UriInterface; /** * @deprecated since version 7.0.0 * @codeCoverageIgnore * @see BaseUri */ final class UriResolver { /** * Resolves a URI against a base URI using RFC3986 rules. * * This method MUST retain the state of the submitted URI instance, and return * a URI instance of the same type that contains the applied modifications. * * This method MUST be transparent when dealing with error and exceptions. * It MUST not alter or silence them apart from validating its own parameters. */ public static function resolve(Psr7UriInterface|UriInterface $uri, Psr7UriInterface|UriInterface $baseUri): Psr7UriInterface|UriInterface { return BaseUri::from($baseUri)->resolve($uri)->getUri(); } /** * Relativizes a URI according to a base URI. * * This method MUST retain the state of the submitted URI instance, and return * a URI instance of the same type that contains the applied modifications. * * This method MUST be transparent when dealing with error and exceptions. * It MUST not alter or silence them apart from validating its own parameters. */ public static function relativize(Psr7UriInterface|UriInterface $uri, Psr7UriInterface|UriInterface $baseUri): Psr7UriInterface|UriInterface { return BaseUri::from($baseUri)->relativize($uri)->getUri(); } } flysystem/composer.json 0000644 00000003651 15007532371 0011335 0 ustar 00 { "name": "league/flysystem", "description": "File storage abstraction for PHP", "keywords": [ "filesystem", "filesystems", "files", "storage", "aws", "s3", "ftp", "sftp", "webdav", "file", "cloud" ], "scripts": { "phpstan": "vendor/bin/phpstan analyse -l 6 src" }, "type": "library", "minimum-stability": "dev", "prefer-stable": true, "autoload": { "psr-4": { "League\\Flysystem\\": "src" } }, "require": { "php": "^8.0.2", "league/flysystem-local": "^3.0.0", "league/mime-type-detection": "^1.0.0" }, "require-dev": { "ext-zip": "*", "ext-fileinfo": "*", "ext-ftp": "*", "microsoft/azure-storage-blob": "^1.1", "phpunit/phpunit": "^9.5.11|^10.0", "phpstan/phpstan": "^1.10", "phpseclib/phpseclib": "^3.0.36", "aws/aws-sdk-php": "^3.295.10", "composer/semver": "^3.0", "friendsofphp/php-cs-fixer": "^3.5", "google/cloud-storage": "^1.23", "async-aws/s3": "^1.5 || ^2.0", "async-aws/simple-s3": "^1.1 || ^2.0", "sabre/dav": "^4.6.0" }, "conflict": { "async-aws/core": "<1.19.0", "async-aws/s3": "<1.14.0", "symfony/http-client": "<5.2", "guzzlehttp/ringphp": "<1.1.1", "guzzlehttp/guzzle": "<7.0", "aws/aws-sdk-php": "3.209.31 || 3.210.0", "phpseclib/phpseclib": "3.0.15" }, "license": "MIT", "authors": [ { "name": "Frank de Jonge", "email": "info@frankdejonge.nl" } ], "repositories": [ { "type": "package", "package": { "name": "league/flysystem-local", "version": "3.0.0", "dist": { "type": "path", "url": "src/Local" } } } ] } flysystem/LICENSE 0000644 00000002047 15007532371 0007616 0 ustar 00 Copyright (c) 2013-2024 Frank de Jonge 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. flysystem/src/PathPrefixer.php 0000644 00000002206 15007532371 0012507 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use function rtrim; use function strlen; use function substr; final class PathPrefixer { private string $prefix = ''; public function __construct(string $prefix, private string $separator = '/') { $this->prefix = rtrim($prefix, '\\/'); if ($this->prefix !== '' || $prefix === $separator) { $this->prefix .= $separator; } } public function prefixPath(string $path): string { return $this->prefix . ltrim($path, '\\/'); } public function stripPrefix(string $path): string { /* @var string */ return substr($path, strlen($this->prefix)); } public function stripDirectoryPrefix(string $path): string { return rtrim($this->stripPrefix($path), '\\/'); } public function prefixDirectoryPath(string $path): string { $prefixedPath = $this->prefixPath(rtrim($path, '\\/')); if ($prefixedPath === '' || substr($prefixedPath, -1) === $this->separator) { return $prefixedPath; } return $prefixedPath . $this->separator; } } flysystem/src/UnableToGenerateTemporaryUrl.php 0000644 00000001370 15007532371 0015661 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToGenerateTemporaryUrl extends RuntimeException implements FilesystemException { public function __construct(string $reason, string $path, ?Throwable $previous = null) { parent::__construct("Unable to generate temporary url for $path: $reason", 0, $previous); } public static function dueToError(string $path, Throwable $exception): static { return new static($exception->getMessage(), $path, $exception); } public static function noGeneratorConfigured(string $path, string $extraReason = ''): static { return new static('No generator was configured ' . $extraReason, $path); } } flysystem/src/FilesystemReader.php 0000644 00000004171 15007532371 0013360 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use DateTimeInterface; /** * This interface contains everything to read from and inspect * a filesystem. All methods containing are non-destructive. * * @method string publicUrl(string $path, array $config = []) Will be added in 4.0 * @method string temporaryUrl(string $path, DateTimeInterface $expiresAt, array $config = []) Will be added in 4.0 * @method string checksum(string $path, array $config = []) Will be added in 4.0 */ interface FilesystemReader { public const LIST_SHALLOW = false; public const LIST_DEEP = true; /** * @throws FilesystemException * @throws UnableToCheckExistence */ public function fileExists(string $location): bool; /** * @throws FilesystemException * @throws UnableToCheckExistence */ public function directoryExists(string $location): bool; /** * @throws FilesystemException * @throws UnableToCheckExistence */ public function has(string $location): bool; /** * @throws UnableToReadFile * @throws FilesystemException */ public function read(string $location): string; /** * @return resource * * @throws UnableToReadFile * @throws FilesystemException */ public function readStream(string $location); /** * @return DirectoryListing<StorageAttributes> * * @throws FilesystemException * @throws UnableToListContents */ public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing; /** * @throws UnableToRetrieveMetadata * @throws FilesystemException */ public function lastModified(string $path): int; /** * @throws UnableToRetrieveMetadata * @throws FilesystemException */ public function fileSize(string $path): int; /** * @throws UnableToRetrieveMetadata * @throws FilesystemException */ public function mimeType(string $path): string; /** * @throws UnableToRetrieveMetadata * @throws FilesystemException */ public function visibility(string $path): string; } flysystem/src/PathTraversalDetected.php 0000644 00000000742 15007532371 0014333 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; class PathTraversalDetected extends RuntimeException implements FilesystemException { private string $path; public function path(): string { return $this->path; } public static function forPath(string $path): PathTraversalDetected { $e = new PathTraversalDetected("Path traversal detected: {$path}"); $e->path = $path; return $e; } } flysystem/src/FilesystemOperationFailed.php 0000644 00000001672 15007532371 0015226 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; interface FilesystemOperationFailed extends FilesystemException { public const OPERATION_WRITE = 'WRITE'; public const OPERATION_UPDATE = 'UPDATE'; // not used public const OPERATION_EXISTENCE_CHECK = 'EXISTENCE_CHECK'; public const OPERATION_DIRECTORY_EXISTS = 'DIRECTORY_EXISTS'; public const OPERATION_FILE_EXISTS = 'FILE_EXISTS'; public const OPERATION_CREATE_DIRECTORY = 'CREATE_DIRECTORY'; public const OPERATION_DELETE = 'DELETE'; public const OPERATION_DELETE_DIRECTORY = 'DELETE_DIRECTORY'; public const OPERATION_MOVE = 'MOVE'; public const OPERATION_RETRIEVE_METADATA = 'RETRIEVE_METADATA'; public const OPERATION_COPY = 'COPY'; public const OPERATION_READ = 'READ'; public const OPERATION_SET_VISIBILITY = 'SET_VISIBILITY'; public const OPERATION_LIST_CONTENTS = 'LIST_CONTENTS'; public function operation(): string; } flysystem/src/UnableToWriteFile.php 0000644 00000001630 15007532371 0013432 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToWriteFile extends RuntimeException implements FilesystemOperationFailed { /** * @var string */ private $location = ''; /** * @var string */ private $reason; public static function atLocation(string $location, string $reason = '', ?Throwable $previous = null): UnableToWriteFile { $e = new static(rtrim("Unable to write file at location: {$location}. {$reason}"), 0, $previous); $e->location = $location; $e->reason = $reason; return $e; } public function operation(): string { return FilesystemOperationFailed::OPERATION_WRITE; } public function reason(): string { return $this->reason; } public function location(): string { return $this->location; } } flysystem/src/FileAttributes.php 0000644 00000005002 15007532371 0013031 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; class FileAttributes implements StorageAttributes { use ProxyArrayAccessToProperties; private string $type = StorageAttributes::TYPE_FILE; public function __construct( private string $path, private ?int $fileSize = null, private ?string $visibility = null, private ?int $lastModified = null, private ?string $mimeType = null, private array $extraMetadata = [] ) { $this->path = ltrim($this->path, '/'); } public function type(): string { return $this->type; } public function path(): string { return $this->path; } public function fileSize(): ?int { return $this->fileSize; } public function visibility(): ?string { return $this->visibility; } public function lastModified(): ?int { return $this->lastModified; } public function mimeType(): ?string { return $this->mimeType; } public function extraMetadata(): array { return $this->extraMetadata; } public function isFile(): bool { return true; } public function isDir(): bool { return false; } public function withPath(string $path): self { $clone = clone $this; $clone->path = $path; return $clone; } public static function fromArray(array $attributes): self { return new FileAttributes( $attributes[StorageAttributes::ATTRIBUTE_PATH], $attributes[StorageAttributes::ATTRIBUTE_FILE_SIZE] ?? null, $attributes[StorageAttributes::ATTRIBUTE_VISIBILITY] ?? null, $attributes[StorageAttributes::ATTRIBUTE_LAST_MODIFIED] ?? null, $attributes[StorageAttributes::ATTRIBUTE_MIME_TYPE] ?? null, $attributes[StorageAttributes::ATTRIBUTE_EXTRA_METADATA] ?? [] ); } public function jsonSerialize(): array { return [ StorageAttributes::ATTRIBUTE_TYPE => self::TYPE_FILE, StorageAttributes::ATTRIBUTE_PATH => $this->path, StorageAttributes::ATTRIBUTE_FILE_SIZE => $this->fileSize, StorageAttributes::ATTRIBUTE_VISIBILITY => $this->visibility, StorageAttributes::ATTRIBUTE_LAST_MODIFIED => $this->lastModified, StorageAttributes::ATTRIBUTE_MIME_TYPE => $this->mimeType, StorageAttributes::ATTRIBUTE_EXTRA_METADATA => $this->extraMetadata, ]; } } flysystem/src/UnableToReadFile.php 0000644 00000001635 15007532371 0013220 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToReadFile extends RuntimeException implements FilesystemOperationFailed { /** * @var string */ private $location = ''; /** * @var string */ private $reason = ''; public static function fromLocation(string $location, string $reason = '', ?Throwable $previous = null): UnableToReadFile { $e = new static(rtrim("Unable to read file from location: {$location}. {$reason}"), 0, $previous); $e->location = $location; $e->reason = $reason; return $e; } public function operation(): string { return FilesystemOperationFailed::OPERATION_READ; } public function reason(): string { return $this->reason; } public function location(): string { return $this->location; } } flysystem/src/InvalidStreamProvided.php 0000644 00000000341 15007532371 0014343 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use InvalidArgumentException as BaseInvalidArgumentException; class InvalidStreamProvided extends BaseInvalidArgumentException implements FilesystemException { } flysystem/src/UnableToSetVisibility.php 0000644 00000001726 15007532371 0014351 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; use function rtrim; final class UnableToSetVisibility extends RuntimeException implements FilesystemOperationFailed { /** * @var string */ private $location; /** * @var string */ private $reason; public function reason(): string { return $this->reason; } public static function atLocation(string $filename, string $extraMessage = '', ?Throwable $previous = null): self { $message = "Unable to set visibility for file {$filename}. $extraMessage"; $e = new static(rtrim($message), 0, $previous); $e->reason = $extraMessage; $e->location = $filename; return $e; } public function operation(): string { return FilesystemOperationFailed::OPERATION_SET_VISIBILITY; } public function location(): string { return $this->location; } } flysystem/src/SymbolicLinkEncountered.php 0000644 00000001023 15007532371 0014675 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; final class SymbolicLinkEncountered extends RuntimeException implements FilesystemException { private string $location; public function location(): string { return $this->location; } public static function atLocation(string $pathName): SymbolicLinkEncountered { $e = new static("Unsupported symbolic link encountered at location $pathName"); $e->location = $pathName; return $e; } } flysystem/src/UnableToListContents.php 0000644 00000001212 15007532371 0014165 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToListContents extends RuntimeException implements FilesystemOperationFailed { public static function atLocation(string $location, bool $deep, Throwable $previous): UnableToListContents { $message = "Unable to list contents for '$location', " . ($deep ? 'deep' : 'shallow') . " listing\n\n" . 'Reason: ' . $previous->getMessage(); return new UnableToListContents($message, 0, $previous); } public function operation(): string { return self::OPERATION_LIST_CONTENTS; } } flysystem/src/UnableToMoveFile.php 0000644 00000003142 15007532371 0013246 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToMoveFile extends RuntimeException implements FilesystemOperationFailed { /** * @var string */ private $source; /** * @var string */ private $destination; public static function sourceAndDestinationAreTheSame(string $source, string $destination): UnableToMoveFile { return UnableToMoveFile::because('Source and destination are the same', $source, $destination); } public function source(): string { return $this->source; } public function destination(): string { return $this->destination; } public static function fromLocationTo( string $sourcePath, string $destinationPath, ?Throwable $previous = null ): UnableToMoveFile { $message = $previous?->getMessage() ?? "Unable to move file from $sourcePath to $destinationPath"; $e = new static($message, 0, $previous); $e->source = $sourcePath; $e->destination = $destinationPath; return $e; } public static function because( string $reason, string $sourcePath, string $destinationPath, ): UnableToMoveFile { $message = "Unable to move file from $sourcePath to $destinationPath, because $reason"; $e = new static($message); $e->source = $sourcePath; $e->destination = $destinationPath; return $e; } public function operation(): string { return FilesystemOperationFailed::OPERATION_MOVE; } } flysystem/src/InvalidVisibilityProvided.php 0000644 00000001051 15007532371 0015236 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use InvalidArgumentException; use function var_export; class InvalidVisibilityProvided extends InvalidArgumentException implements FilesystemException { public static function withVisibility(string $visibility, string $expectedMessage): InvalidVisibilityProvided { $provided = var_export($visibility, true); $message = "Invalid visibility provided. Expected {$expectedMessage}, received {$provided}"; throw new InvalidVisibilityProvided($message); } } flysystem/src/FilesystemOperator.php 0000644 00000000212 15007532371 0013741 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; interface FilesystemOperator extends FilesystemReader, FilesystemWriter { } flysystem/src/ResolveIdenticalPathConflict.php 0000644 00000000306 15007532371 0015640 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; class ResolveIdenticalPathConflict { public const IGNORE = 'ignore'; public const FAIL = 'fail'; public const TRY = 'try'; } flysystem/src/UnableToResolveFilesystemMount.php 0000644 00000001323 15007532371 0016246 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; class UnableToResolveFilesystemMount extends RuntimeException implements FilesystemException { public static function becauseTheSeparatorIsMissing(string $path): UnableToResolveFilesystemMount { return new UnableToResolveFilesystemMount("Unable to resolve the filesystem mount because the path ($path) is missing a separator (://)."); } public static function becauseTheMountWasNotRegistered(string $mountIdentifier): UnableToResolveFilesystemMount { return new UnableToResolveFilesystemMount("Unable to resolve the filesystem mount because the mount ($mountIdentifier) was not registered."); } } flysystem/src/ChecksumAlgoIsNotSupported.php 0000644 00000000251 15007532371 0015334 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use InvalidArgumentException; final class ChecksumAlgoIsNotSupported extends InvalidArgumentException { } flysystem/src/Visibility.php 0000644 00000000243 15007532371 0012234 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; final class Visibility { public const PUBLIC = 'public'; public const PRIVATE = 'private'; } flysystem/src/FilesystemWriter.php 0000644 00000002650 15007532371 0013432 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; interface FilesystemWriter { /** * @throws UnableToWriteFile * @throws FilesystemException */ public function write(string $location, string $contents, array $config = []): void; /** * @param mixed $contents * * @throws UnableToWriteFile * @throws FilesystemException */ public function writeStream(string $location, $contents, array $config = []): void; /** * @throws UnableToSetVisibility * @throws FilesystemException */ public function setVisibility(string $path, string $visibility): void; /** * @throws UnableToDeleteFile * @throws FilesystemException */ public function delete(string $location): void; /** * @throws UnableToDeleteDirectory * @throws FilesystemException */ public function deleteDirectory(string $location): void; /** * @throws UnableToCreateDirectory * @throws FilesystemException */ public function createDirectory(string $location, array $config = []): void; /** * @throws UnableToMoveFile * @throws FilesystemException */ public function move(string $source, string $destination, array $config = []): void; /** * @throws UnableToCopyFile * @throws FilesystemException */ public function copy(string $source, string $destination, array $config = []): void; } flysystem/src/UnableToRetrieveMetadata.php 0000644 00000003753 15007532371 0014776 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToRetrieveMetadata extends RuntimeException implements FilesystemOperationFailed { /** * @var string */ private $location; /** * @var string */ private $metadataType; /** * @var string */ private $reason; public static function lastModified(string $location, string $reason = '', ?Throwable $previous = null): self { return static::create($location, FileAttributes::ATTRIBUTE_LAST_MODIFIED, $reason, $previous); } public static function visibility(string $location, string $reason = '', ?Throwable $previous = null): self { return static::create($location, FileAttributes::ATTRIBUTE_VISIBILITY, $reason, $previous); } public static function fileSize(string $location, string $reason = '', ?Throwable $previous = null): self { return static::create($location, FileAttributes::ATTRIBUTE_FILE_SIZE, $reason, $previous); } public static function mimeType(string $location, string $reason = '', ?Throwable $previous = null): self { return static::create($location, FileAttributes::ATTRIBUTE_MIME_TYPE, $reason, $previous); } public static function create(string $location, string $type, string $reason = '', ?Throwable $previous = null): self { $e = new static("Unable to retrieve the $type for file at location: $location. {$reason}", 0, $previous); $e->reason = $reason; $e->location = $location; $e->metadataType = $type; return $e; } public function reason(): string { return $this->reason; } public function location(): string { return $this->location; } public function metadataType(): string { return $this->metadataType; } public function operation(): string { return FilesystemOperationFailed::OPERATION_RETRIEVE_METADATA; } } flysystem/src/FilesystemException.php 0000644 00000000202 15007532371 0014103 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use Throwable; interface FilesystemException extends Throwable { } flysystem/src/UnableToCheckFileExistence.php 0000644 00000000367 15007532371 0015233 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; class UnableToCheckFileExistence extends UnableToCheckExistence { public function operation(): string { return FilesystemOperationFailed::OPERATION_FILE_EXISTS; } } flysystem/src/ChecksumProvider.php 0000644 00000000443 15007532371 0013364 0 ustar 00 <?php namespace League\Flysystem; interface ChecksumProvider { /** * @return string MD5 hash of the file contents * * @throws UnableToProvideChecksum * @throws ChecksumAlgoIsNotSupported */ public function checksum(string $path, Config $config): string; } flysystem/src/UnableToGeneratePublicUrl.php 0000644 00000001362 15007532371 0015116 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToGeneratePublicUrl extends RuntimeException implements FilesystemException { public function __construct(string $reason, string $path, ?Throwable $previous = null) { parent::__construct("Unable to generate public url for $path: $reason", 0, $previous); } public static function dueToError(string $path, Throwable $exception): static { return new static($exception->getMessage(), $path, $exception); } public static function noGeneratorConfigured(string $path, string $extraReason = ''): static { return new static('No generator was configured ' . $extraReason, $path); } } flysystem/src/CalculateChecksumFromStream.php 0000644 00000001431 15007532371 0015465 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use function hash_final; use function hash_init; use function hash_update_stream; trait CalculateChecksumFromStream { private function calculateChecksumFromStream(string $path, Config $config): string { try { $stream = $this->readStream($path); $algo = (string) $config->get('checksum_algo', 'md5'); $context = hash_init($algo); hash_update_stream($context, $stream); return hash_final($context); } catch (FilesystemException $exception) { throw new UnableToProvideChecksum($exception->getMessage(), $path, $exception); } } /** * @return resource */ abstract public function readStream(string $path); } flysystem/src/Filesystem.php 0000644 00000022655 15007532371 0012244 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use DateTimeInterface; use Generator; use League\Flysystem\UrlGeneration\PrefixPublicUrlGenerator; use League\Flysystem\UrlGeneration\PublicUrlGenerator; use League\Flysystem\UrlGeneration\ShardedPrefixPublicUrlGenerator; use League\Flysystem\UrlGeneration\TemporaryUrlGenerator; use Throwable; use function array_key_exists; use function is_array; class Filesystem implements FilesystemOperator { use CalculateChecksumFromStream; private Config $config; private PathNormalizer $pathNormalizer; public function __construct( private FilesystemAdapter $adapter, array $config = [], ?PathNormalizer $pathNormalizer = null, private ?PublicUrlGenerator $publicUrlGenerator = null, private ?TemporaryUrlGenerator $temporaryUrlGenerator = null, ) { $this->config = new Config($config); $this->pathNormalizer = $pathNormalizer ?? new WhitespacePathNormalizer(); } public function fileExists(string $location): bool { return $this->adapter->fileExists($this->pathNormalizer->normalizePath($location)); } public function directoryExists(string $location): bool { return $this->adapter->directoryExists($this->pathNormalizer->normalizePath($location)); } public function has(string $location): bool { $path = $this->pathNormalizer->normalizePath($location); return $this->adapter->fileExists($path) || $this->adapter->directoryExists($path); } public function write(string $location, string $contents, array $config = []): void { $this->adapter->write( $this->pathNormalizer->normalizePath($location), $contents, $this->config->extend($config) ); } public function writeStream(string $location, $contents, array $config = []): void { /* @var resource $contents */ $this->assertIsResource($contents); $this->rewindStream($contents); $this->adapter->writeStream( $this->pathNormalizer->normalizePath($location), $contents, $this->config->extend($config) ); } public function read(string $location): string { return $this->adapter->read($this->pathNormalizer->normalizePath($location)); } public function readStream(string $location) { return $this->adapter->readStream($this->pathNormalizer->normalizePath($location)); } public function delete(string $location): void { $this->adapter->delete($this->pathNormalizer->normalizePath($location)); } public function deleteDirectory(string $location): void { $this->adapter->deleteDirectory($this->pathNormalizer->normalizePath($location)); } public function createDirectory(string $location, array $config = []): void { $this->adapter->createDirectory( $this->pathNormalizer->normalizePath($location), $this->config->extend($config) ); } public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing { $path = $this->pathNormalizer->normalizePath($location); $listing = $this->adapter->listContents($path, $deep); return new DirectoryListing($this->pipeListing($location, $deep, $listing)); } private function pipeListing(string $location, bool $deep, iterable $listing): Generator { try { foreach ($listing as $item) { yield $item; } } catch (Throwable $exception) { throw UnableToListContents::atLocation($location, $deep, $exception); } } public function move(string $source, string $destination, array $config = []): void { $config = $this->resolveConfigForMoveAndCopy($config); $from = $this->pathNormalizer->normalizePath($source); $to = $this->pathNormalizer->normalizePath($destination); if ($from === $to) { $resolutionStrategy = $config->get(Config::OPTION_MOVE_IDENTICAL_PATH, ResolveIdenticalPathConflict::TRY); if ($resolutionStrategy === ResolveIdenticalPathConflict::FAIL) { throw UnableToMoveFile::sourceAndDestinationAreTheSame($source, $destination); } elseif ($resolutionStrategy === ResolveIdenticalPathConflict::IGNORE) { return; } } $this->adapter->move($from, $to, $config); } public function copy(string $source, string $destination, array $config = []): void { $config = $this->resolveConfigForMoveAndCopy($config); $from = $this->pathNormalizer->normalizePath($source); $to = $this->pathNormalizer->normalizePath($destination); if ($from === $to) { $resolutionStrategy = $config->get(Config::OPTION_COPY_IDENTICAL_PATH, ResolveIdenticalPathConflict::TRY); if ($resolutionStrategy === ResolveIdenticalPathConflict::FAIL) { throw UnableToCopyFile::sourceAndDestinationAreTheSame($source, $destination); } elseif ($resolutionStrategy === ResolveIdenticalPathConflict::IGNORE) { return; } } $this->adapter->copy($from, $to, $config); } public function lastModified(string $path): int { return $this->adapter->lastModified($this->pathNormalizer->normalizePath($path))->lastModified(); } public function fileSize(string $path): int { return $this->adapter->fileSize($this->pathNormalizer->normalizePath($path))->fileSize(); } public function mimeType(string $path): string { return $this->adapter->mimeType($this->pathNormalizer->normalizePath($path))->mimeType(); } public function setVisibility(string $path, string $visibility): void { $this->adapter->setVisibility($this->pathNormalizer->normalizePath($path), $visibility); } public function visibility(string $path): string { return $this->adapter->visibility($this->pathNormalizer->normalizePath($path))->visibility(); } public function publicUrl(string $path, array $config = []): string { $this->publicUrlGenerator ??= $this->resolvePublicUrlGenerator() ?? throw UnableToGeneratePublicUrl::noGeneratorConfigured($path); $config = $this->config->extend($config); return $this->publicUrlGenerator->publicUrl($this->pathNormalizer->normalizePath($path), $config); } public function temporaryUrl(string $path, DateTimeInterface $expiresAt, array $config = []): string { $generator = $this->temporaryUrlGenerator ?? $this->adapter; if ($generator instanceof TemporaryUrlGenerator) { return $generator->temporaryUrl( $this->pathNormalizer->normalizePath($path), $expiresAt, $this->config->extend($config) ); } throw UnableToGenerateTemporaryUrl::noGeneratorConfigured($path); } public function checksum(string $path, array $config = []): string { $config = $this->config->extend($config); if ( ! $this->adapter instanceof ChecksumProvider) { return $this->calculateChecksumFromStream($path, $config); } try { return $this->adapter->checksum($path, $config); } catch (ChecksumAlgoIsNotSupported) { return $this->calculateChecksumFromStream($path, $config); } } private function resolvePublicUrlGenerator(): ?PublicUrlGenerator { if ($publicUrl = $this->config->get('public_url')) { return match (true) { is_array($publicUrl) => new ShardedPrefixPublicUrlGenerator($publicUrl), default => new PrefixPublicUrlGenerator($publicUrl), }; } if ($this->adapter instanceof PublicUrlGenerator) { return $this->adapter; } return null; } /** * @param mixed $contents */ private function assertIsResource($contents): void { if (is_resource($contents) === false) { throw new InvalidStreamProvided( "Invalid stream provided, expected stream resource, received " . gettype($contents) ); } elseif ($type = get_resource_type($contents) !== 'stream') { throw new InvalidStreamProvided( "Invalid stream provided, expected stream resource, received resource of type " . $type ); } } /** * @param resource $resource */ private function rewindStream($resource): void { if (ftell($resource) !== 0 && stream_get_meta_data($resource)['seekable']) { rewind($resource); } } private function resolveConfigForMoveAndCopy(array $config): Config { $retainVisibility = $this->config->get(Config::OPTION_RETAIN_VISIBILITY, $config[Config::OPTION_RETAIN_VISIBILITY] ?? true); $fullConfig = $this->config->extend($config); /* * By default, we retain visibility. When we do not retain visibility, the visibility setting * from the default configuration is ignored. Only when it is set explicitly, we propagate the * setting. */ if ($retainVisibility && ! array_key_exists(Config::OPTION_VISIBILITY, $config)) { $fullConfig = $fullConfig->withoutSettings(Config::OPTION_VISIBILITY)->extend($config); } return $fullConfig; } } flysystem/src/PortableVisibilityGuard.php 0000644 00000000777 15007532371 0014724 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; final class PortableVisibilityGuard { public static function guardAgainstInvalidInput(string $visibility): void { if ($visibility !== Visibility::PUBLIC && $visibility !== Visibility::PRIVATE) { $className = Visibility::class; throw InvalidVisibilityProvided::withVisibility( $visibility, "either {$className}::PUBLIC or {$className}::PRIVATE" ); } } } flysystem/src/UnableToProvideChecksum.php 0000644 00000000603 15007532371 0014632 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToProvideChecksum extends RuntimeException implements FilesystemException { public function __construct(string $reason, string $path, ?Throwable $previous = null) { parent::__construct("Unable to get checksum for $path: $reason", 0, $previous); } } flysystem/src/DecoratedAdapter.php 0000644 00000004565 15007532371 0013313 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; abstract class DecoratedAdapter implements FilesystemAdapter { public function __construct(protected FilesystemAdapter $adapter) { } public function fileExists(string $path): bool { return $this->adapter->fileExists($path); } public function directoryExists(string $path): bool { return $this->adapter->directoryExists($path); } public function write(string $path, string $contents, Config $config): void { $this->adapter->write($path, $contents, $config); } public function writeStream(string $path, $contents, Config $config): void { $this->adapter->writeStream($path, $contents, $config); } public function read(string $path): string { return $this->adapter->read($path); } public function readStream(string $path) { return $this->adapter->readStream($path); } public function delete(string $path): void { $this->adapter->delete($path); } public function deleteDirectory(string $path): void { $this->adapter->deleteDirectory($path); } public function createDirectory(string $path, Config $config): void { $this->adapter->createDirectory($path, $config); } public function setVisibility(string $path, string $visibility): void { $this->adapter->setVisibility($path, $visibility); } public function visibility(string $path): FileAttributes { return $this->adapter->visibility($path); } public function mimeType(string $path): FileAttributes { return $this->adapter->mimeType($path); } public function lastModified(string $path): FileAttributes { return $this->adapter->lastModified($path); } public function fileSize(string $path): FileAttributes { return $this->adapter->fileSize($path); } public function listContents(string $path, bool $deep): iterable { return $this->adapter->listContents($path, $deep); } public function move(string $source, string $destination, Config $config): void { $this->adapter->move($source, $destination, $config); } public function copy(string $source, string $destination, Config $config): void { $this->adapter->copy($source, $destination, $config); } } flysystem/src/UnixVisibility/VisibilityConverter.php 0000644 00000000621 15007532371 0017117 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem\UnixVisibility; interface VisibilityConverter { public function forFile(string $visibility): int; public function forDirectory(string $visibility): int; public function inverseForFile(int $visibility): string; public function inverseForDirectory(int $visibility): string; public function defaultForDirectories(): int; } flysystem/src/UnixVisibility/PortableVisibilityConverter.php 0000644 00000004521 15007532371 0020613 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem\UnixVisibility; use League\Flysystem\PortableVisibilityGuard; use League\Flysystem\Visibility; class PortableVisibilityConverter implements VisibilityConverter { public function __construct( private int $filePublic = 0644, private int $filePrivate = 0600, private int $directoryPublic = 0755, private int $directoryPrivate = 0700, private string $defaultForDirectories = Visibility::PRIVATE ) { } public function forFile(string $visibility): int { PortableVisibilityGuard::guardAgainstInvalidInput($visibility); return $visibility === Visibility::PUBLIC ? $this->filePublic : $this->filePrivate; } public function forDirectory(string $visibility): int { PortableVisibilityGuard::guardAgainstInvalidInput($visibility); return $visibility === Visibility::PUBLIC ? $this->directoryPublic : $this->directoryPrivate; } public function inverseForFile(int $visibility): string { if ($visibility === $this->filePublic) { return Visibility::PUBLIC; } elseif ($visibility === $this->filePrivate) { return Visibility::PRIVATE; } return Visibility::PUBLIC; // default } public function inverseForDirectory(int $visibility): string { if ($visibility === $this->directoryPublic) { return Visibility::PUBLIC; } elseif ($visibility === $this->directoryPrivate) { return Visibility::PRIVATE; } return Visibility::PUBLIC; // default } public function defaultForDirectories(): int { return $this->defaultForDirectories === Visibility::PUBLIC ? $this->directoryPublic : $this->directoryPrivate; } /** * @param array<mixed> $permissionMap */ public static function fromArray(array $permissionMap, string $defaultForDirectories = Visibility::PRIVATE): PortableVisibilityConverter { return new PortableVisibilityConverter( $permissionMap['file']['public'] ?? 0644, $permissionMap['file']['private'] ?? 0600, $permissionMap['dir']['public'] ?? 0755, $permissionMap['dir']['private'] ?? 0700, $defaultForDirectories ); } } flysystem/src/UnableToDeleteDirectory.php 0000644 00000001716 15007532371 0014634 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToDeleteDirectory extends RuntimeException implements FilesystemOperationFailed { /** * @var string */ private $location = ''; /** * @var string */ private $reason; public static function atLocation( string $location, string $reason = '', ?Throwable $previous = null ): UnableToDeleteDirectory { $e = new static(rtrim("Unable to delete directory located at: {$location}. {$reason}"), 0, $previous); $e->location = $location; $e->reason = $reason; return $e; } public function operation(): string { return FilesystemOperationFailed::OPERATION_DELETE_DIRECTORY; } public function reason(): string { return $this->reason; } public function location(): string { return $this->location; } } flysystem/src/UrlGeneration/PublicUrlGenerator.php 0000644 00000000471 15007532371 0016436 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem\UrlGeneration; use League\Flysystem\Config; use League\Flysystem\UnableToGeneratePublicUrl; interface PublicUrlGenerator { /** * @throws UnableToGeneratePublicUrl */ public function publicUrl(string $path, Config $config): string; } flysystem/src/UrlGeneration/PrefixPublicUrlGenerator.php 0000644 00000000763 15007532371 0017620 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem\UrlGeneration; use League\Flysystem\Config; use League\Flysystem\PathPrefixer; class PrefixPublicUrlGenerator implements PublicUrlGenerator { private PathPrefixer $prefixer; public function __construct(string $urlPrefix) { $this->prefixer = new PathPrefixer($urlPrefix, '/'); } public function publicUrl(string $path, Config $config): string { return $this->prefixer->prefixPath($path); } } flysystem/src/UrlGeneration/TemporaryUrlGenerator.php 0000644 00000000571 15007532371 0017203 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem\UrlGeneration; use DateTimeInterface; use League\Flysystem\Config; use League\Flysystem\UnableToGenerateTemporaryUrl; interface TemporaryUrlGenerator { /** * @throws UnableToGenerateTemporaryUrl */ public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string; } flysystem/src/UrlGeneration/ChainedPublicUrlGenerator.php 0000644 00000001353 15007532371 0017712 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem\UrlGeneration; use League\Flysystem\Config; use League\Flysystem\UnableToGeneratePublicUrl; final class ChainedPublicUrlGenerator implements PublicUrlGenerator { /** * @param PublicUrlGenerator[] $generators */ public function __construct(private iterable $generators) { } public function publicUrl(string $path, Config $config): string { foreach ($this->generators as $generator) { try { return $generator->publicUrl($path, $config); } catch (UnableToGeneratePublicUrl) { } } throw new UnableToGeneratePublicUrl('No supported public url generator found.', $path); } } flysystem/src/UrlGeneration/ShardedPrefixPublicUrlGenerator.php 0000644 00000001674 15007532371 0021115 0 ustar 00 <?php namespace League\Flysystem\UrlGeneration; use InvalidArgumentException; use League\Flysystem\Config; use League\Flysystem\PathPrefixer; use function array_map; use function count; use function crc32; final class ShardedPrefixPublicUrlGenerator implements PublicUrlGenerator { /** @var PathPrefixer[] */ private array $prefixes; private int $count; /** * @param string[] $prefixes */ public function __construct(array $prefixes) { $this->count = count($prefixes); if ($this->count === 0) { throw new InvalidArgumentException('At least one prefix is required.'); } $this->prefixes = array_map(static fn (string $prefix) => new PathPrefixer($prefix, '/'), $prefixes); } public function publicUrl(string $path, Config $config): string { $index = abs(crc32($path)) % $this->count; return $this->prefixes[$index]->prefixPath($path); } } flysystem/src/Config.php 0000644 00000002615 15007532371 0011317 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use function array_diff_key; use function array_flip; use function array_merge; class Config { public const OPTION_COPY_IDENTICAL_PATH = 'copy_destination_same_as_source'; public const OPTION_MOVE_IDENTICAL_PATH = 'move_destination_same_as_source'; public const OPTION_VISIBILITY = 'visibility'; public const OPTION_DIRECTORY_VISIBILITY = 'directory_visibility'; public const OPTION_RETAIN_VISIBILITY = 'retain_visibility'; public function __construct(private array $options = []) { } /** * @param mixed $default * * @return mixed */ public function get(string $property, $default = null) { return $this->options[$property] ?? $default; } public function extend(array $options): Config { return new Config(array_merge($this->options, $options)); } public function withDefaults(array $defaults): Config { return new Config($this->options + $defaults); } public function toArray(): array { return $this->options; } public function withSetting(string $property, mixed $setting): Config { return $this->extend([$property => $setting]); } public function withoutSettings(string ...$settings): Config { return new Config(array_diff_key($this->options, array_flip($settings))); } } flysystem/src/UnableToDeleteFile.php 0000644 00000001633 15007532371 0013545 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToDeleteFile extends RuntimeException implements FilesystemOperationFailed { /** * @var string */ private $location = ''; /** * @var string */ private $reason; public static function atLocation(string $location, string $reason = '', ?Throwable $previous = null): UnableToDeleteFile { $e = new static(rtrim("Unable to delete file located at: {$location}. {$reason}"), 0, $previous); $e->location = $location; $e->reason = $reason; return $e; } public function operation(): string { return FilesystemOperationFailed::OPERATION_DELETE; } public function reason(): string { return $this->reason; } public function location(): string { return $this->location; } } flysystem/src/FilesystemAdapter.php 0000644 00000005340 15007532371 0013535 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; interface FilesystemAdapter { /** * @throws FilesystemException * @throws UnableToCheckExistence */ public function fileExists(string $path): bool; /** * @throws FilesystemException * @throws UnableToCheckExistence */ public function directoryExists(string $path): bool; /** * @throws UnableToWriteFile * @throws FilesystemException */ public function write(string $path, string $contents, Config $config): void; /** * @param resource $contents * * @throws UnableToWriteFile * @throws FilesystemException */ public function writeStream(string $path, $contents, Config $config): void; /** * @throws UnableToReadFile * @throws FilesystemException */ public function read(string $path): string; /** * @return resource * * @throws UnableToReadFile * @throws FilesystemException */ public function readStream(string $path); /** * @throws UnableToDeleteFile * @throws FilesystemException */ public function delete(string $path): void; /** * @throws UnableToDeleteDirectory * @throws FilesystemException */ public function deleteDirectory(string $path): void; /** * @throws UnableToCreateDirectory * @throws FilesystemException */ public function createDirectory(string $path, Config $config): void; /** * @throws InvalidVisibilityProvided * @throws FilesystemException */ public function setVisibility(string $path, string $visibility): void; /** * @throws UnableToRetrieveMetadata * @throws FilesystemException */ public function visibility(string $path): FileAttributes; /** * @throws UnableToRetrieveMetadata * @throws FilesystemException */ public function mimeType(string $path): FileAttributes; /** * @throws UnableToRetrieveMetadata * @throws FilesystemException */ public function lastModified(string $path): FileAttributes; /** * @throws UnableToRetrieveMetadata * @throws FilesystemException */ public function fileSize(string $path): FileAttributes; /** * @return iterable<StorageAttributes> * * @throws FilesystemException */ public function listContents(string $path, bool $deep): iterable; /** * @throws UnableToMoveFile * @throws FilesystemException */ public function move(string $source, string $destination, Config $config): void; /** * @throws UnableToCopyFile * @throws FilesystemException */ public function copy(string $source, string $destination, Config $config): void; } flysystem/src/CorruptedPathDetected.php 0000644 00000000474 15007532371 0014341 0 ustar 00 <?php namespace League\Flysystem; use RuntimeException; final class CorruptedPathDetected extends RuntimeException implements FilesystemException { public static function forPath(string $path): CorruptedPathDetected { return new CorruptedPathDetected("Corrupted path detected: " . $path); } } flysystem/src/UnableToMountFilesystem.php 0000644 00000001623 15007532371 0014711 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use LogicException; class UnableToMountFilesystem extends LogicException implements FilesystemException { /** * @param mixed $key */ public static function becauseTheKeyIsNotValid($key): UnableToMountFilesystem { return new UnableToMountFilesystem( 'Unable to mount filesystem, key was invalid. String expected, received: ' . gettype($key) ); } /** * @param mixed $filesystem */ public static function becauseTheFilesystemWasNotValid($filesystem): UnableToMountFilesystem { $received = is_object($filesystem) ? get_class($filesystem) : gettype($filesystem); return new UnableToMountFilesystem( 'Unable to mount filesystem, filesystem was invalid. Instance of ' . FilesystemOperator::class . ' expected, received: ' . $received ); } } flysystem/src/UnableToCheckExistence.php 0000644 00000001246 15007532371 0014430 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; class UnableToCheckExistence extends RuntimeException implements FilesystemOperationFailed { final public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null) { parent::__construct($message, $code, $previous); } public static function forLocation(string $path, ?Throwable $exception = null): static { return new static("Unable to check existence for: {$path}", 0, $exception); } public function operation(): string { return FilesystemOperationFailed::OPERATION_EXISTENCE_CHECK; } } flysystem/src/UnableToCreateDirectory.php 0000644 00000002516 15007532371 0014634 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToCreateDirectory extends RuntimeException implements FilesystemOperationFailed { private string $location; private string $reason = ''; public static function atLocation(string $dirname, string $errorMessage = '', ?Throwable $previous = null): UnableToCreateDirectory { $message = "Unable to create a directory at {$dirname}. {$errorMessage}"; $e = new static(rtrim($message), 0, $previous); $e->location = $dirname; $e->reason = $errorMessage; return $e; } public static function dueToFailure(string $dirname, Throwable $previous): UnableToCreateDirectory { $reason = $previous instanceof UnableToCreateDirectory ? $previous->reason() : ''; $message = "Unable to create a directory at $dirname. $reason"; $e = new static(rtrim($message), 0, $previous); $e->location = $dirname; $e->reason = $reason ?: $message; return $e; } public function operation(): string { return FilesystemOperationFailed::OPERATION_CREATE_DIRECTORY; } public function reason(): string { return $this->reason; } public function location(): string { return $this->location; } } flysystem/src/MountManager.php 0000644 00000035777 15007532371 0012526 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use DateTimeInterface; use Throwable; use function compact; use function method_exists; use function sprintf; class MountManager implements FilesystemOperator { /** * @var array<string, FilesystemOperator> */ private $filesystems = []; /** * @var Config */ private $config; /** * MountManager constructor. * * @param array<string,FilesystemOperator> $filesystems */ public function __construct(array $filesystems = [], array $config = []) { $this->mountFilesystems($filesystems); $this->config = new Config($config); } /** * It is not recommended to mount filesystems after creation because interacting * with the Mount Manager becomes unpredictable. Use this as an escape hatch. */ public function dangerouslyMountFilesystems(string $key, FilesystemOperator $filesystem): void { $this->mountFilesystem($key, $filesystem); } /** * @param array<string,FilesystemOperator> $filesystems */ public function extend(array $filesystems, array $config = []): MountManager { $clone = clone $this; $clone->config = $this->config->extend($config); $clone->mountFilesystems($filesystems); return $clone; } public function fileExists(string $location): bool { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { return $filesystem->fileExists($path); } catch (Throwable $exception) { throw UnableToCheckFileExistence::forLocation($location, $exception); } } public function has(string $location): bool { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { return $filesystem->fileExists($path) || $filesystem->directoryExists($path); } catch (Throwable $exception) { throw UnableToCheckExistence::forLocation($location, $exception); } } public function directoryExists(string $location): bool { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { return $filesystem->directoryExists($path); } catch (Throwable $exception) { throw UnableToCheckDirectoryExistence::forLocation($location, $exception); } } public function read(string $location): string { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { return $filesystem->read($path); } catch (UnableToReadFile $exception) { throw UnableToReadFile::fromLocation($location, $exception->reason(), $exception); } } public function readStream(string $location) { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { return $filesystem->readStream($path); } catch (UnableToReadFile $exception) { throw UnableToReadFile::fromLocation($location, $exception->reason(), $exception); } } public function listContents(string $location, bool $deep = self::LIST_SHALLOW): DirectoryListing { /** @var FilesystemOperator $filesystem */ [$filesystem, $path, $mountIdentifier] = $this->determineFilesystemAndPath($location); return $filesystem ->listContents($path, $deep) ->map( function (StorageAttributes $attributes) use ($mountIdentifier) { return $attributes->withPath(sprintf('%s://%s', $mountIdentifier, $attributes->path())); } ); } public function lastModified(string $location): int { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { return $filesystem->lastModified($path); } catch (UnableToRetrieveMetadata $exception) { throw UnableToRetrieveMetadata::lastModified($location, $exception->reason(), $exception); } } public function fileSize(string $location): int { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { return $filesystem->fileSize($path); } catch (UnableToRetrieveMetadata $exception) { throw UnableToRetrieveMetadata::fileSize($location, $exception->reason(), $exception); } } public function mimeType(string $location): string { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { return $filesystem->mimeType($path); } catch (UnableToRetrieveMetadata $exception) { throw UnableToRetrieveMetadata::mimeType($location, $exception->reason(), $exception); } } public function visibility(string $path): string { /** @var FilesystemOperator $filesystem */ [$filesystem, $location] = $this->determineFilesystemAndPath($path); try { return $filesystem->visibility($location); } catch (UnableToRetrieveMetadata $exception) { throw UnableToRetrieveMetadata::visibility($path, $exception->reason(), $exception); } } public function write(string $location, string $contents, array $config = []): void { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { $filesystem->write($path, $contents, $this->config->extend($config)->toArray()); } catch (UnableToWriteFile $exception) { throw UnableToWriteFile::atLocation($location, $exception->reason(), $exception); } } public function writeStream(string $location, $contents, array $config = []): void { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); $filesystem->writeStream($path, $contents, $this->config->extend($config)->toArray()); } public function setVisibility(string $path, string $visibility): void { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($path); $filesystem->setVisibility($path, $visibility); } public function delete(string $location): void { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { $filesystem->delete($path); } catch (UnableToDeleteFile $exception) { throw UnableToDeleteFile::atLocation($location, $exception->reason(), $exception); } } public function deleteDirectory(string $location): void { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { $filesystem->deleteDirectory($path); } catch (UnableToDeleteDirectory $exception) { throw UnableToDeleteDirectory::atLocation($location, $exception->reason(), $exception); } } public function createDirectory(string $location, array $config = []): void { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($location); try { $filesystem->createDirectory($path, $this->config->extend($config)->toArray()); } catch (UnableToCreateDirectory $exception) { throw UnableToCreateDirectory::dueToFailure($location, $exception); } } public function move(string $source, string $destination, array $config = []): void { /** @var FilesystemOperator $sourceFilesystem */ /* @var FilesystemOperator $destinationFilesystem */ [$sourceFilesystem, $sourcePath] = $this->determineFilesystemAndPath($source); [$destinationFilesystem, $destinationPath] = $this->determineFilesystemAndPath($destination); $sourceFilesystem === $destinationFilesystem ? $this->moveInTheSameFilesystem( $sourceFilesystem, $sourcePath, $destinationPath, $source, $destination, $config, ) : $this->moveAcrossFilesystems($source, $destination, $config); } public function copy(string $source, string $destination, array $config = []): void { /** @var FilesystemOperator $sourceFilesystem */ /* @var FilesystemOperator $destinationFilesystem */ [$sourceFilesystem, $sourcePath] = $this->determineFilesystemAndPath($source); [$destinationFilesystem, $destinationPath] = $this->determineFilesystemAndPath($destination); $sourceFilesystem === $destinationFilesystem ? $this->copyInSameFilesystem( $sourceFilesystem, $sourcePath, $destinationPath, $source, $destination, $config, ) : $this->copyAcrossFilesystem( $sourceFilesystem, $sourcePath, $destinationFilesystem, $destinationPath, $source, $destination, $config, ); } public function publicUrl(string $path, array $config = []): string { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($path); if ( ! method_exists($filesystem, 'publicUrl')) { throw new UnableToGeneratePublicUrl(sprintf('%s does not support generating public urls.', $filesystem::class), $path); } return $filesystem->publicUrl($path, $config); } public function temporaryUrl(string $path, DateTimeInterface $expiresAt, array $config = []): string { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($path); if ( ! method_exists($filesystem, 'temporaryUrl')) { throw new UnableToGenerateTemporaryUrl(sprintf('%s does not support generating public urls.', $filesystem::class), $path); } return $filesystem->temporaryUrl($path, $expiresAt, $this->config->extend($config)->toArray()); } public function checksum(string $path, array $config = []): string { /** @var FilesystemOperator $filesystem */ [$filesystem, $path] = $this->determineFilesystemAndPath($path); if ( ! method_exists($filesystem, 'checksum')) { throw new UnableToProvideChecksum(sprintf('%s does not support providing checksums.', $filesystem::class), $path); } return $filesystem->checksum($path, $this->config->extend($config)->toArray()); } private function mountFilesystems(array $filesystems): void { foreach ($filesystems as $key => $filesystem) { $this->guardAgainstInvalidMount($key, $filesystem); /* @var string $key */ /* @var FilesystemOperator $filesystem */ $this->mountFilesystem($key, $filesystem); } } private function guardAgainstInvalidMount(mixed $key, mixed $filesystem): void { if ( ! is_string($key)) { throw UnableToMountFilesystem::becauseTheKeyIsNotValid($key); } if ( ! $filesystem instanceof FilesystemOperator) { throw UnableToMountFilesystem::becauseTheFilesystemWasNotValid($filesystem); } } private function mountFilesystem(string $key, FilesystemOperator $filesystem): void { $this->filesystems[$key] = $filesystem; } /** * @param string $path * * @return array{0:FilesystemOperator, 1:string, 2:string} */ private function determineFilesystemAndPath(string $path): array { if (strpos($path, '://') < 1) { throw UnableToResolveFilesystemMount::becauseTheSeparatorIsMissing($path); } /** @var string $mountIdentifier */ /** @var string $mountPath */ [$mountIdentifier, $mountPath] = explode('://', $path, 2); if ( ! array_key_exists($mountIdentifier, $this->filesystems)) { throw UnableToResolveFilesystemMount::becauseTheMountWasNotRegistered($mountIdentifier); } return [$this->filesystems[$mountIdentifier], $mountPath, $mountIdentifier]; } private function copyInSameFilesystem( FilesystemOperator $sourceFilesystem, string $sourcePath, string $destinationPath, string $source, string $destination, array $config, ): void { try { $sourceFilesystem->copy($sourcePath, $destinationPath, $this->config->extend($config)->toArray()); } catch (UnableToCopyFile $exception) { throw UnableToCopyFile::fromLocationTo($source, $destination, $exception); } } private function copyAcrossFilesystem( FilesystemOperator $sourceFilesystem, string $sourcePath, FilesystemOperator $destinationFilesystem, string $destinationPath, string $source, string $destination, array $config, ): void { $config = $this->config->extend($config); $retainVisibility = (bool) $config->get(Config::OPTION_RETAIN_VISIBILITY, true); $visibility = $config->get(Config::OPTION_VISIBILITY); try { if ($visibility == null && $retainVisibility) { $visibility = $sourceFilesystem->visibility($sourcePath); $config = $config->extend(compact('visibility')); } $stream = $sourceFilesystem->readStream($sourcePath); $destinationFilesystem->writeStream($destinationPath, $stream, $config->toArray()); } catch (UnableToRetrieveMetadata | UnableToReadFile | UnableToWriteFile $exception) { throw UnableToCopyFile::fromLocationTo($source, $destination, $exception); } } private function moveInTheSameFilesystem( FilesystemOperator $sourceFilesystem, string $sourcePath, string $destinationPath, string $source, string $destination, array $config, ): void { try { $sourceFilesystem->move($sourcePath, $destinationPath, $this->config->extend($config)->toArray()); } catch (UnableToMoveFile $exception) { throw UnableToMoveFile::fromLocationTo($source, $destination, $exception); } } private function moveAcrossFilesystems(string $source, string $destination, array $config = []): void { try { $this->copy($source, $destination, $config); $this->delete($source); } catch (UnableToCopyFile | UnableToDeleteFile $exception) { throw UnableToMoveFile::fromLocationTo($source, $destination, $exception); } } } flysystem/src/StorageAttributes.php 0000644 00000002013 15007532371 0013555 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use ArrayAccess; use JsonSerializable; interface StorageAttributes extends JsonSerializable, ArrayAccess { public const ATTRIBUTE_PATH = 'path'; public const ATTRIBUTE_TYPE = 'type'; public const ATTRIBUTE_FILE_SIZE = 'file_size'; public const ATTRIBUTE_VISIBILITY = 'visibility'; public const ATTRIBUTE_LAST_MODIFIED = 'last_modified'; public const ATTRIBUTE_MIME_TYPE = 'mime_type'; public const ATTRIBUTE_EXTRA_METADATA = 'extra_metadata'; public const TYPE_FILE = 'file'; public const TYPE_DIRECTORY = 'dir'; public function path(): string; public function type(): string; public function visibility(): ?string; public function lastModified(): ?int; public static function fromArray(array $attributes): StorageAttributes; public function isFile(): bool; public function isDir(): bool; public function withPath(string $path): StorageAttributes; public function extraMetadata(): array; } flysystem/src/DirectoryAttributes.php 0000644 00000004027 15007532371 0014124 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; class DirectoryAttributes implements StorageAttributes { use ProxyArrayAccessToProperties; private string $type = StorageAttributes::TYPE_DIRECTORY; public function __construct( private string $path, private ?string $visibility = null, private ?int $lastModified = null, private array $extraMetadata = [] ) { $this->path = trim($this->path, '/'); } public function path(): string { return $this->path; } public function type(): string { return $this->type; } public function visibility(): ?string { return $this->visibility; } public function lastModified(): ?int { return $this->lastModified; } public function extraMetadata(): array { return $this->extraMetadata; } public function isFile(): bool { return false; } public function isDir(): bool { return true; } public function withPath(string $path): self { $clone = clone $this; $clone->path = $path; return $clone; } public static function fromArray(array $attributes): self { return new DirectoryAttributes( $attributes[StorageAttributes::ATTRIBUTE_PATH], $attributes[StorageAttributes::ATTRIBUTE_VISIBILITY] ?? null, $attributes[StorageAttributes::ATTRIBUTE_LAST_MODIFIED] ?? null, $attributes[StorageAttributes::ATTRIBUTE_EXTRA_METADATA] ?? [] ); } /** * @inheritDoc */ public function jsonSerialize(): array { return [ StorageAttributes::ATTRIBUTE_TYPE => $this->type, StorageAttributes::ATTRIBUTE_PATH => $this->path, StorageAttributes::ATTRIBUTE_VISIBILITY => $this->visibility, StorageAttributes::ATTRIBUTE_LAST_MODIFIED => $this->lastModified, StorageAttributes::ATTRIBUTE_EXTRA_METADATA => $this->extraMetadata, ]; } } flysystem/src/DirectoryListing.php 0000644 00000004036 15007532371 0013407 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use ArrayIterator; use Generator; use IteratorAggregate; use Traversable; /** * @template T */ class DirectoryListing implements IteratorAggregate { /** * @param iterable<T> $listing */ public function __construct(private iterable $listing) { } /** * @param callable(T): bool $filter * * @return DirectoryListing<T> */ public function filter(callable $filter): DirectoryListing { $generator = (static function (iterable $listing) use ($filter): Generator { foreach ($listing as $item) { if ($filter($item)) { yield $item; } } })($this->listing); return new DirectoryListing($generator); } /** * @template R * * @param callable(T): R $mapper * * @return DirectoryListing<R> */ public function map(callable $mapper): DirectoryListing { $generator = (static function (iterable $listing) use ($mapper): Generator { foreach ($listing as $item) { yield $mapper($item); } })($this->listing); return new DirectoryListing($generator); } /** * @return DirectoryListing<T> */ public function sortByPath(): DirectoryListing { $listing = $this->toArray(); usort($listing, function (StorageAttributes $a, StorageAttributes $b) { return $a->path() <=> $b->path(); }); return new DirectoryListing($listing); } /** * @return Traversable<T> */ public function getIterator(): Traversable { return $this->listing instanceof Traversable ? $this->listing : new ArrayIterator($this->listing); } /** * @return T[] */ public function toArray(): array { return $this->listing instanceof Traversable ? iterator_to_array($this->listing, false) : (array) $this->listing; } } flysystem/src/UnreadableFileEncountered.php 0000644 00000001054 15007532371 0015144 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; final class UnreadableFileEncountered extends RuntimeException implements FilesystemException { /** * @var string */ private $location; public function location(): string { return $this->location; } public static function atLocation(string $location): UnreadableFileEncountered { $e = new static("Unreadable file encountered at location {$location}."); $e->location = $location; return $e; } } flysystem/src/PathNormalizer.php 0000644 00000000224 15007532371 0013043 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; interface PathNormalizer { public function normalizePath(string $path): string; } flysystem/src/ProxyArrayAccessToProperties.php 0000644 00000002315 15007532371 0015731 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; /** * @internal */ trait ProxyArrayAccessToProperties { private function formatPropertyName(string $offset): string { return str_replace('_', '', lcfirst(ucwords($offset, '_'))); } /** * @param mixed $offset * * @return bool */ public function offsetExists($offset): bool { $property = $this->formatPropertyName((string) $offset); return isset($this->{$property}); } /** * @param mixed $offset * * @return mixed */ #[\ReturnTypeWillChange] public function offsetGet($offset) { $property = $this->formatPropertyName((string) $offset); return $this->{$property}; } /** * @param mixed $offset * @param mixed $value */ #[\ReturnTypeWillChange] public function offsetSet($offset, $value): void { throw new RuntimeException('Properties can not be manipulated'); } /** * @param mixed $offset */ #[\ReturnTypeWillChange] public function offsetUnset($offset): void { throw new RuntimeException('Properties can not be manipulated'); } } flysystem/src/UnableToCheckDirectoryExistence.php 0000644 00000000401 15007532371 0016305 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; class UnableToCheckDirectoryExistence extends UnableToCheckExistence { public function operation(): string { return FilesystemOperationFailed::OPERATION_DIRECTORY_EXISTS; } } flysystem/src/UnableToCopyFile.php 0000644 00000002762 15007532371 0013261 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; use RuntimeException; use Throwable; final class UnableToCopyFile extends RuntimeException implements FilesystemOperationFailed { /** * @var string */ private $source; /** * @var string */ private $destination; public function source(): string { return $this->source; } public function destination(): string { return $this->destination; } public static function fromLocationTo( string $sourcePath, string $destinationPath, ?Throwable $previous = null ): UnableToCopyFile { $e = new static("Unable to copy file from $sourcePath to $destinationPath", 0 , $previous); $e->source = $sourcePath; $e->destination = $destinationPath; return $e; } public static function sourceAndDestinationAreTheSame(string $source, string $destination): UnableToCopyFile { return UnableToCopyFile::because('Source and destination are the same', $source, $destination); } public static function because(string $reason, string $sourcePath, string $destinationPath): UnableToCopyFile { $e = new static("Unable to copy file from $sourcePath to $destinationPath, because $reason"); $e->source = $sourcePath; $e->destination = $destinationPath; return $e; } public function operation(): string { return FilesystemOperationFailed::OPERATION_COPY; } } flysystem/src/WhitespacePathNormalizer.php 0000644 00000002210 15007532371 0015055 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem; class WhitespacePathNormalizer implements PathNormalizer { public function normalizePath(string $path): string { $path = str_replace('\\', '/', $path); $this->rejectFunkyWhiteSpace($path); return $this->normalizeRelativePath($path); } private function rejectFunkyWhiteSpace(string $path): void { if (preg_match('#\p{C}+#u', $path)) { throw CorruptedPathDetected::forPath($path); } } private function normalizeRelativePath(string $path): string { $parts = []; foreach (explode('/', $path) as $part) { switch ($part) { case '': case '.': break; case '..': if (empty($parts)) { throw PathTraversalDetected::forPath($path); } array_pop($parts); break; default: $parts[] = $part; break; } } return implode('/', $parts); } } flysystem/INFO.md 0000644 00000000213 15007532371 0007657 0 ustar 00 View the docs at: https://flysystem.thephpleague.com/docs/ Changelog at: https://github.com/thephpleague/flysystem/blob/3.x/CHANGELOG.md flysystem/readme.md 0000644 00000007170 15007532371 0010372 0 ustar 00 # League\Flysystem [](https://twitter.com/frankdejonge) [](https://github.com/thephpleague/flysystem) [](https://github.com/thephpleague/flysystem/releases) [](https://github.com/thephpleague/flysystem/blob/master/LICENSE) [](https://github.com/thephpleague/flysystem/actions?query=workflow%3A%22Quality+Assurance%22) [](https://packagist.org/packages/league/flysystem)  ## About Flysystem Flysystem is a file storage library for PHP. It provides one interface to interact with many types of filesystems. When you use Flysystem, you're not only protected from vendor lock-in, you'll also have a consistent experience for which ever storage is right for you. ## Getting Started * **[New in V3](https://flysystem.thephpleague.com/docs/what-is-new/)**: What is new in Flysystem V2/V3? * **[Architecture](https://flysystem.thephpleague.com/docs/architecture/)**: Flysystem's internal architecture * **[Flysystem API](https://flysystem.thephpleague.com/docs/usage/filesystem-api/)**: How to interact with your Flysystem instance * **[Upgrade from 1x](https://flysystem.thephpleague.com/docs/upgrade-from-1.x/)**: How to upgrade from 1.x/2.x ### Officially supported adapters * **[Local](https://flysystem.thephpleague.com/docs/adapter/local/)** * **[FTP](https://flysystem.thephpleague.com/docs/adapter/ftp/)** * **[SFTP](https://flysystem.thephpleague.com/docs/adapter/sftp-v3/)** * **[Memory](https://flysystem.thephpleague.com/docs/adapter/in-memory/)** * **[AWS S3](https://flysystem.thephpleague.com/docs/adapter/aws-s3-v3/)** * **[AsyncAws S3](https://flysystem.thephpleague.com/docs/adapter/async-aws-s3/)** * **[Google Cloud Storage](https://flysystem.thephpleague.com/docs/adapter/google-cloud-storage/)** * **[Azure Blob Storage](https://flysystem.thephpleague.com/docs/adapter/azure-blob-storage/)** * **[WebDAV](https://flysystem.thephpleague.com/docs/adapter/webdav/)** * **[ZipArchive](https://flysystem.thephpleague.com/docs/adapter/zip-archive/)** ### Third party Adapters * **[Gitlab](https://github.com/RoyVoetman/flysystem-gitlab-storage)** * **[Google Drive (using regular paths)](https://github.com/masbug/flysystem-google-drive-ext)** * **[bunny.net / BunnyCDN](https://github.com/PlatformCommunity/flysystem-bunnycdn/tree/v3)** * **[Sharepoint 365 / One Drive (Using MS Graph)](https://github.com/shitware-ltd/flysystem-msgraph)** * **[OneDrive](https://github.com/doerffler/flysystem-onedrive)** * **[Dropbox](https://github.com/spatie/flysystem-dropbox)** * **[ReplicateAdapter](https://github.com/ajgarlag/flysystem-replicate)** * **[Uploadcare](https://github.com/vormkracht10/flysystem-uploadcare)** * **[Useful adapters (FallbackAdapter, LogAdapter, ReadWriteAdapter, RetryAdapter)](https://github.com/ElGigi/FlysystemUsefulAdapters)** You can always [create an adapter](https://flysystem.thephpleague.com/docs/advanced/creating-an-adapter/) yourself. ## Security If you discover any security related issues, please email info@frankdejonge.nl instead of using the issue tracker. ## Enjoy Oh, and if you've come down this far, you might as well follow me on [twitter](https://twitter.com/frankdejonge). event/composer.json 0000644 00000001374 15007532371 0010417 0 ustar 00 { "name": "league/event", "description": "Event package", "keywords": ["event", "emitter", "listener"], "license": "MIT", "authors": [ { "name": "Frank de Jonge", "email": "info@frenky.net" } ], "require": { "php": ">=5.4.0" }, "require-dev": { "henrikbjorn/phpspec-code-coverage": "~1.0.1", "phpspec/phpspec": "^2.2" }, "autoload": { "psr-4": { "League\\Event\\": "src/" } }, "autoload-dev": { "psr-4": { "League\\Event\\Stub\\": "stubs/" } }, "config": { "bin-dir": "bin" }, "extra": { "branch-alias": { "dev-master": "2.2-dev" } } } event/LICENSE 0000644 00000002042 15007532371 0006673 0 ustar 00 Copyright (c) 2014 Frank de Jonge 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. event/src/CallbackListener.php 0000644 00000002220 15007532371 0012366 0 ustar 00 <?php namespace League\Event; class CallbackListener implements ListenerInterface { /** * The callback. * * @var callable */ protected $callback; /** * Create a new callback listener instance. * * @param callable $callback */ public function __construct(callable $callback) { $this->callback = $callback; } /** * Get the callback. * * @return callable */ public function getCallback() { return $this->callback; } /** * @inheritdoc */ public function handle(EventInterface $event) { call_user_func_array($this->callback, func_get_args()); } /** * @inheritdoc */ public function isListener($listener) { if ($listener instanceof CallbackListener) { $listener = $listener->getCallback(); } return $this->callback === $listener; } /** * Named constructor * * @param callable $callable * * @return static */ public static function fromCallable(callable $callable) { return new static($callable); } } event/src/AbstractEvent.php 0000644 00000001734 15007532371 0011742 0 ustar 00 <?php namespace League\Event; abstract class AbstractEvent implements EventInterface { /** * Has propagation stopped? * * @var bool */ protected $propagationStopped = false; /** * The emitter instance. * * @var EmitterInterface|null */ protected $emitter; /** * @inheritdoc */ public function setEmitter(EmitterInterface $emitter) { $this->emitter = $emitter; return $this; } /** * @inheritdoc */ public function getEmitter() { return $this->emitter; } /** * @inheritdoc */ public function stopPropagation() { $this->propagationStopped = true; return $this; } /** * @inheritdoc */ public function isPropagationStopped() { return $this->propagationStopped; } /** * @inheritdoc */ public function getName() { return get_class($this); } } event/src/AbstractListener.php 0000644 00000000336 15007532371 0012443 0 ustar 00 <?php namespace League\Event; abstract class AbstractListener implements ListenerInterface { /** * @inheritdoc */ public function isListener($listener) { return $this === $listener; } } event/src/GeneratorTrait.php 0000644 00000001163 15007532371 0012123 0 ustar 00 <?php namespace League\Event; trait GeneratorTrait { /** * The registered events. * * @var EventInterface[] */ protected $events = []; /** * Add an event. * * @param EventInterface $event * * @return $this */ protected function addEvent(EventInterface $event) { $this->events[] = $event; return $this; } /** * Release all the added events. * * @return EventInterface[] */ public function releaseEvents() { $events = $this->events; $this->events = []; return $events; } } event/src/ListenerProviderInterface.php 0000644 00000000435 15007532371 0014313 0 ustar 00 <?php namespace League\Event; interface ListenerProviderInterface { /** * Provide event * * @param ListenerAcceptorInterface $listenerAcceptor * * @return $this */ public function provideListeners(ListenerAcceptorInterface $listenerAcceptor); } event/src/EventInterface.php 0000644 00000001276 15007532371 0012100 0 ustar 00 <?php namespace League\Event; interface EventInterface { /** * Set the Emitter. * * @param EmitterInterface $emitter * * @return $this */ public function setEmitter(EmitterInterface $emitter); /** * Get the Emitter. * * @return EmitterInterface */ public function getEmitter(); /** * Stop event propagation. * * @return $this */ public function stopPropagation(); /** * Check whether propagation was stopped. * * @return bool */ public function isPropagationStopped(); /** * Get the event name. * * @return string */ public function getName(); } event/src/OneTimeListener.php 0000644 00000002227 15007532371 0012241 0 ustar 00 <?php namespace League\Event; class OneTimeListener implements ListenerInterface { /** * The listener instance. * * @var ListenerInterface */ protected $listener; /** * Create a new one time listener instance. * * @param ListenerInterface $listener */ public function __construct(ListenerInterface $listener) { $this->listener = $listener; } /** * Get the wrapped listener. * * @return ListenerInterface */ public function getWrappedListener() { return $this->listener; } /** * @inheritdoc */ public function handle(EventInterface $event) { $name = $event->getName(); $emitter = $event->getEmitter(); $emitter->removeListener($name, $this->listener); return call_user_func_array([$this->listener, 'handle'], func_get_args()); } /** * @inheritdoc */ public function isListener($listener) { if ($listener instanceof OneTimeListener) { $listener = $listener->getWrappedListener(); } return $this->listener->isListener($listener); } } event/src/BufferedEmitter.php 0000644 00000001453 15007532371 0012247 0 ustar 00 <?php namespace League\Event; class BufferedEmitter extends Emitter { /** * @var EventInterface[] */ protected $bufferedEvents = []; /** * @inheritdoc */ public function emit($event) { $this->bufferedEvents[] = $event; return $event; } /** * @inheritdoc */ public function emitBatch(array $events) { foreach ($events as $event) { $this->bufferedEvents[] = $event; } return $events; } /** * Emit the buffered events. * * @return array */ public function emitBufferedEvents() { $result = []; while ($event = array_shift($this->bufferedEvents)) { $result[] = parent::emit($event); } return $result; } } event/src/ListenerAcceptor.php 0000644 00000001513 15007532371 0012436 0 ustar 00 <?php namespace League\Event; class ListenerAcceptor implements ListenerAcceptorInterface { /** * The emitter instance. * * @var EmitterInterface|null */ protected $emitter; /** * Constructor * * @param EmitterInterface $emitter */ public function __construct(EmitterInterface $emitter) { $this->emitter = $emitter; } /** * @inheritdoc */ public function addListener($event, $listener, $priority = self::P_NORMAL) { $this->emitter->addListener($event, $listener, $priority); return $this; } /** * @inheritdoc */ public function addOneTimeListener($event, $listener, $priority = self::P_NORMAL) { $this->emitter->addOneTimeListener($event, $listener, $priority); return $this; } } event/src/EmitterAwareInterface.php 0000644 00000000553 15007532371 0013405 0 ustar 00 <?php namespace League\Event; interface EmitterAwareInterface { /** * Set the Emitter. * * @param EmitterInterface $emitter * * @return $this */ public function setEmitter(EmitterInterface $emitter = null); /** * Get the Emitter. * * @return EmitterInterface */ public function getEmitter(); } event/src/EmitterTrait.php 0000644 00000005577 15007532371 0011623 0 ustar 00 <?php namespace League\Event; trait EmitterTrait { use EmitterAwareTrait; /** * Add a listener for an event. * * The first parameter should be the event name, and the second should be * the event listener. It may implement the League\Event\ListenerInterface * or simply be "callable". * * @param string $event * @param ListenerInterface|callable $listener * @param int $priority * * @return $this */ public function addListener($event, $listener, $priority = ListenerAcceptorInterface::P_NORMAL) { $this->getEmitter()->addListener($event, $listener, $priority); return $this; } /** * Add a one time listener for an event. * * The first parameter should be the event name, and the second should be * the event listener. It may implement the League\Event\ListenerInterface * or simply be "callable". * * @param string $event * @param ListenerInterface|callable $listener * @param int $priority * * @return $this */ public function addOneTimeListener($event, $listener, $priority = ListenerAcceptorInterface::P_NORMAL) { $this->getEmitter()->addOneTimeListener($event, $listener, $priority); return $this; } /** * Remove a specific listener for an event. * * The first parameter should be the event name, and the second should be * the event listener. It may implement the League\Event\ListenerInterface * or simply be "callable". * * @param string $event * @param ListenerInterface|callable $listener * * @return $this */ public function removeListener($event, $listener) { $this->getEmitter()->removeListener($event, $listener); return $this; } /** * Remove all listeners for an event. * * The first parameter should be the event name. All event listeners will * be removed. * * @param string $event * * @return $this */ public function removeAllListeners($event) { $this->getEmitter()->removeAllListeners($event); return $this; } /** * Add listeners from a provider. * * @param ListenerProviderInterface $provider * * @return $this */ public function useListenerProvider(ListenerProviderInterface $provider) { $this->getEmitter()->useListenerProvider($provider); return $this; } /** * Emit an event. * * @param string|EventInterface $event * * @return EventInterface */ public function emit($event) { $emitter = $this->getEmitter(); $arguments = [$event] + func_get_args(); return call_user_func_array([$emitter, 'emit'], $arguments); } } event/src/Event.php 0000644 00000001167 15007532371 0010256 0 ustar 00 <?php namespace League\Event; class Event extends AbstractEvent { /** * The event name. * * @var string */ protected $name; /** * Create a new event instance. * * @param string $name */ public function __construct($name) { $this->name = $name; } /** * @inheritdoc */ public function getName() { return $this->name; } /** * Create a new event instance. * * @param string $name * * @return static */ public static function named($name) { return new static($name); } } event/src/Emitter.php 0000644 00000014015 15007532371 0010602 0 ustar 00 <?php namespace League\Event; use InvalidArgumentException; class Emitter implements EmitterInterface { /** * The registered listeners. * * @var array */ protected $listeners = []; /** * The sorted listeners * * Listeners will get sorted and stored for re-use. * * @var ListenerInterface[] */ protected $sortedListeners = []; /** * @inheritdoc */ public function addListener($event, $listener, $priority = self::P_NORMAL) { $listener = $this->ensureListener($listener); $this->listeners[$event][$priority][] = $listener; $this->clearSortedListeners($event); return $this; } /** * @inheritdoc */ public function addOneTimeListener($event, $listener, $priority = self::P_NORMAL) { $listener = $this->ensureListener($listener); $listener = new OneTimeListener($listener); return $this->addListener($event, $listener, $priority); } /** * @inheritdoc */ public function useListenerProvider(ListenerProviderInterface $provider) { $acceptor = new ListenerAcceptor($this); $provider->provideListeners($acceptor); return $this; } /** * @inheritdoc */ public function removeListener($event, $listener) { $this->clearSortedListeners($event); $listeners = $this->hasListeners($event) ? $this->listeners[$event] : []; $filter = function ($registered) use ($listener) { return ! $registered->isListener($listener); }; foreach ($listeners as $priority => $collection) { $listeners[$priority] = array_filter($collection, $filter); } $this->listeners[$event] = $listeners; return $this; } /** * @inheritdoc */ public function removeAllListeners($event) { $this->clearSortedListeners($event); if ($this->hasListeners($event)) { unset($this->listeners[$event]); } return $this; } /** * Ensure the input is a listener. * * @param ListenerInterface|callable $listener * * @throws InvalidArgumentException * * @return ListenerInterface */ protected function ensureListener($listener) { if ($listener instanceof ListenerInterface) { return $listener; } if (is_callable($listener)) { return CallbackListener::fromCallable($listener); } throw new InvalidArgumentException('Listeners should be ListenerInterface, Closure or callable. Received type: '.gettype($listener)); } /** * @inheritdoc */ public function hasListeners($event) { if (! isset($this->listeners[$event]) || count($this->listeners[$event]) === 0) { return false; } return true; } /** * @inheritdoc */ public function getListeners($event) { if (array_key_exists($event, $this->sortedListeners)) { return $this->sortedListeners[$event]; } return $this->sortedListeners[$event] = $this->getSortedListeners($event); } /** * Get the listeners sorted by priority for a given event. * * @param string $event * * @return ListenerInterface[] */ protected function getSortedListeners($event) { if (! $this->hasListeners($event)) { return []; } $listeners = $this->listeners[$event]; krsort($listeners); return call_user_func_array('array_merge', $listeners); } /** * @inheritdoc */ public function emit($event) { list($name, $event) = $this->prepareEvent($event); $arguments = [$event] + func_get_args(); $this->invokeListeners($name, $event, $arguments); $this->invokeListeners('*', $event, $arguments); return $event; } /** * @inheritdoc */ public function emitBatch(array $events) { $results = []; foreach ($events as $event) { $results[] = $this->emit($event); } return $results; } /** * @inheritdoc */ public function emitGeneratedEvents(GeneratorInterface $generator) { $events = $generator->releaseEvents(); return $this->emitBatch($events); } /** * Invoke the listeners for an event. * * @param string $name * @param EventInterface $event * @param array $arguments * * @return void */ protected function invokeListeners($name, EventInterface $event, array $arguments) { $listeners = $this->getListeners($name); foreach ($listeners as $listener) { if ($event->isPropagationStopped()) { break; } call_user_func_array([$listener, 'handle'], $arguments); } } /** * Prepare an event for emitting. * * @param string|EventInterface $event * * @return array */ protected function prepareEvent($event) { $event = $this->ensureEvent($event); $name = $event->getName(); $event->setEmitter($this); return [$name, $event]; } /** * Ensure event input is of type EventInterface or convert it. * * @param string|EventInterface $event * * @throws InvalidArgumentException * * @return EventInterface */ protected function ensureEvent($event) { if (is_string($event)) { return Event::named($event); } if (! $event instanceof EventInterface) { throw new InvalidArgumentException('Events should be provides as Event instances or string, received type: '.gettype($event)); } return $event; } /** * Clear the sorted listeners for an event * * @param $event */ protected function clearSortedListeners($event) { unset($this->sortedListeners[$event]); } } event/src/ListenerInterface.php 0000644 00000000627 15007532371 0012603 0 ustar 00 <?php namespace League\Event; interface ListenerInterface { /** * Handle an event. * * @param EventInterface $event * * @return void */ public function handle(EventInterface $event); /** * Check whether the listener is the given parameter. * * @param mixed $listener * * @return bool */ public function isListener($listener); } event/src/EmitterInterface.php 0000644 00000004301 15007532371 0012420 0 ustar 00 <?php namespace League\Event; interface EmitterInterface extends ListenerAcceptorInterface { /** * Remove a specific listener for an event. * * The first parameter should be the event name, and the second should be * the event listener. It may implement the League\Event\ListenerInterface * or simply be "callable". * * @param string $event * @param ListenerInterface|callable $listener * * @return $this */ public function removeListener($event, $listener); /** * Use a provider to add listeners. * * @param ListenerProviderInterface $provider * * @return $this */ public function useListenerProvider(ListenerProviderInterface $provider); /** * Remove all listeners for an event. * * The first parameter should be the event name. All event listeners will * be removed. * * @param string $event * * @return $this */ public function removeAllListeners($event); /** * Check whether an event has listeners. * * The first parameter should be the event name. We'll return true if the * event has one or more registered even listeners, and false otherwise. * * @param string $event * * @return bool */ public function hasListeners($event); /** * Get all the listeners for an event. * * The first parameter should be the event name. We'll return an array of * all the registered even listeners, or an empty array if there are none. * * @param string $event * * @return array */ public function getListeners($event); /** * Emit an event. * * @param string|EventInterface $event * * @return EventInterface */ public function emit($event); /** * Emit a batch of events. * * @param array $events * * @return array */ public function emitBatch(array $events); /** * Release all events stored in a generator * * @param GeneratorInterface $generator * * @return EventInterface[] */ public function emitGeneratedEvents(GeneratorInterface $generator); } event/src/Generator.php 0000644 00000000215 15007532371 0011114 0 ustar 00 <?php namespace League\Event; class Generator implements GeneratorInterface { use GeneratorTrait { addEvent as public; } } event/src/EmitterAwareTrait.php 0000644 00000001235 15007532371 0012566 0 ustar 00 <?php namespace League\Event; trait EmitterAwareTrait { /** * The emitter instance. * * @var EmitterInterface|null */ protected $emitter; /** * Set the Emitter. * * @param EmitterInterface|null $emitter * * @return $this */ public function setEmitter(EmitterInterface $emitter = null) { $this->emitter = $emitter; return $this; } /** * Get the Emitter. * * @return EmitterInterface */ public function getEmitter() { if (! $this->emitter) { $this->emitter = new Emitter(); } return $this->emitter; } } event/src/GeneratorInterface.php 0000644 00000000302 15007532371 0012732 0 ustar 00 <?php namespace League\Event; interface GeneratorInterface { /** * Release all the added events. * * @return EventInterface[] */ public function releaseEvents(); } event/src/ListenerAcceptorInterface.php 0000644 00000003043 15007532371 0014257 0 ustar 00 <?php namespace League\Event; interface ListenerAcceptorInterface { /** * High priority. * * @const int */ const P_HIGH = 100; /** * Normal priority. * * @const int */ const P_NORMAL = 0; /** * Low priority. * * @const int */ const P_LOW = -100; /** * Add a listener for an event. * * The first parameter should be the event name, and the second should be * the event listener. It may implement the League\Event\ListenerInterface * or simply be "callable". In this case, the priority emitter also accepts * an optional third parameter specifying the priority as an integer. You * may use one of our predefined constants here if you want. * * @param string $event * @param ListenerInterface|callable $listener * @param int $priority * * @return $this */ public function addListener($event, $listener, $priority = self::P_NORMAL); /** * Add a one time listener for an event. * * The first parameter should be the event name, and the second should be * the event listener. It may implement the League\Event\ListenerInterface * or simply be "callable". * * @param string $event * @param ListenerInterface|callable $listener * @param int $priority * * @return $this */ public function addOneTimeListener($event, $listener, $priority = self::P_NORMAL); } flysystem-aws-s3-v3/composer.json 0000644 00000001346 15007532371 0012775 0 ustar 00 { "name": "league/flysystem-aws-s3-v3", "description": "AWS S3 filesystem adapter for Flysystem.", "keywords": ["aws", "s3", "flysystem", "filesystem", "storage", "file", "files"], "type": "library", "autoload": { "psr-4": { "League\\Flysystem\\AwsS3V3\\": "" } }, "require": { "php": "^8.0.2", "league/flysystem": "^3.10.0", "league/mime-type-detection": "^1.0.0", "aws/aws-sdk-php": "^3.295.10" }, "conflict": { "guzzlehttp/ringphp": "<1.1.1", "guzzlehttp/guzzle": "<7.0" }, "license": "MIT", "authors": [ { "name": "Frank de Jonge", "email": "info@frankdejonge.nl" } ] } flysystem-aws-s3-v3/LICENSE 0000644 00000002047 15007532371 0011257 0 ustar 00 Copyright (c) 2013-2024 Frank de Jonge 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. flysystem-aws-s3-v3/VisibilityConverter.php 0000644 00000000432 15007532371 0014776 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem\AwsS3V3; interface VisibilityConverter { public function visibilityToAcl(string $visibility): string; public function aclToVisibility(array $grants): string; public function defaultForDirectories(): string; } flysystem-aws-s3-v3/AwsS3V3Adapter.php 0000644 00000041516 15007532371 0013441 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem\AwsS3V3; use Aws\Api\DateTimeResult; use Aws\S3\S3ClientInterface; use DateTimeInterface; use Generator; use League\Flysystem\ChecksumAlgoIsNotSupported; use League\Flysystem\ChecksumProvider; use League\Flysystem\Config; use League\Flysystem\DirectoryAttributes; use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; use League\Flysystem\FilesystemOperationFailed; use League\Flysystem\PathPrefixer; use League\Flysystem\StorageAttributes; use League\Flysystem\UnableToCheckDirectoryExistence; use League\Flysystem\UnableToCheckFileExistence; use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToDeleteDirectory; use League\Flysystem\UnableToDeleteFile; use League\Flysystem\UnableToGeneratePublicUrl; use League\Flysystem\UnableToGenerateTemporaryUrl; use League\Flysystem\UnableToMoveFile; use League\Flysystem\UnableToProvideChecksum; use League\Flysystem\UnableToReadFile; use League\Flysystem\UnableToRetrieveMetadata; use League\Flysystem\UnableToSetVisibility; use League\Flysystem\UnableToWriteFile; use League\Flysystem\UrlGeneration\PublicUrlGenerator; use League\Flysystem\UrlGeneration\TemporaryUrlGenerator; use League\Flysystem\Visibility; use League\MimeTypeDetection\FinfoMimeTypeDetector; use League\MimeTypeDetection\MimeTypeDetector; use Psr\Http\Message\StreamInterface; use Throwable; use function trim; class AwsS3V3Adapter implements FilesystemAdapter, PublicUrlGenerator, ChecksumProvider, TemporaryUrlGenerator { /** * @var string[] */ public const AVAILABLE_OPTIONS = [ 'ACL', 'CacheControl', 'ContentDisposition', 'ContentEncoding', 'ContentLength', 'ContentType', 'Expires', 'GrantFullControl', 'GrantRead', 'GrantReadACP', 'GrantWriteACP', 'Metadata', 'MetadataDirective', 'RequestPayer', 'SSECustomerAlgorithm', 'SSECustomerKey', 'SSECustomerKeyMD5', 'SSEKMSKeyId', 'ServerSideEncryption', 'StorageClass', 'Tagging', 'WebsiteRedirectLocation', 'ChecksumAlgorithm', 'CopySourceSSECustomerAlgorithm', 'CopySourceSSECustomerKey', 'CopySourceSSECustomerKeyMD5', ]; /** * @var string[] */ public const MUP_AVAILABLE_OPTIONS = [ 'add_content_md5', 'before_upload', 'concurrency', 'mup_threshold', 'params', 'part_size', ]; /** * @var string[] */ private const EXTRA_METADATA_FIELDS = [ 'Metadata', 'StorageClass', 'ETag', 'VersionId', ]; private PathPrefixer $prefixer; private VisibilityConverter $visibility; private MimeTypeDetector $mimeTypeDetector; public function __construct( private S3ClientInterface $client, private string $bucket, string $prefix = '', ?VisibilityConverter $visibility = null, ?MimeTypeDetector $mimeTypeDetector = null, private array $options = [], private bool $streamReads = true, private array $forwardedOptions = self::AVAILABLE_OPTIONS, private array $metadataFields = self::EXTRA_METADATA_FIELDS, private array $multipartUploadOptions = self::MUP_AVAILABLE_OPTIONS, ) { $this->prefixer = new PathPrefixer($prefix); $this->visibility = $visibility ?? new PortableVisibilityConverter(); $this->mimeTypeDetector = $mimeTypeDetector ?? new FinfoMimeTypeDetector(); } public function fileExists(string $path): bool { try { return $this->client->doesObjectExistV2($this->bucket, $this->prefixer->prefixPath($path), false, $this->options); } catch (Throwable $exception) { throw UnableToCheckFileExistence::forLocation($path, $exception); } } public function directoryExists(string $path): bool { try { $prefix = $this->prefixer->prefixDirectoryPath($path); $options = ['Bucket' => $this->bucket, 'Prefix' => $prefix, 'MaxKeys' => 1, 'Delimiter' => '/']; $command = $this->client->getCommand('ListObjectsV2', $options); $result = $this->client->execute($command); return $result->hasKey('Contents') || $result->hasKey('CommonPrefixes'); } catch (Throwable $exception) { throw UnableToCheckDirectoryExistence::forLocation($path, $exception); } } public function write(string $path, string $contents, Config $config): void { $this->upload($path, $contents, $config); } /** * @param string $path * @param string|resource $body * @param Config $config */ private function upload(string $path, $body, Config $config): void { $key = $this->prefixer->prefixPath($path); $options = $this->createOptionsFromConfig($config); $acl = $options['params']['ACL'] ?? $this->determineAcl($config); $shouldDetermineMimetype = ! array_key_exists('ContentType', $options['params']); if ($shouldDetermineMimetype && $mimeType = $this->mimeTypeDetector->detectMimeType($key, $body)) { $options['params']['ContentType'] = $mimeType; } try { $this->client->upload($this->bucket, $key, $body, $acl, $options); } catch (Throwable $exception) { throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception); } } private function determineAcl(Config $config): string { $visibility = (string) $config->get(Config::OPTION_VISIBILITY, Visibility::PRIVATE); return $this->visibility->visibilityToAcl($visibility); } private function createOptionsFromConfig(Config $config): array { $config = $config->withDefaults($this->options); $options = ['params' => []]; if ($mimetype = $config->get('mimetype')) { $options['params']['ContentType'] = $mimetype; } foreach ($this->forwardedOptions as $option) { $value = $config->get($option, '__NOT_SET__'); if ($value !== '__NOT_SET__') { $options['params'][$option] = $value; } } foreach ($this->multipartUploadOptions as $option) { $value = $config->get($option, '__NOT_SET__'); if ($value !== '__NOT_SET__') { $options[$option] = $value; } } return $options; } public function writeStream(string $path, $contents, Config $config): void { $this->upload($path, $contents, $config); } public function read(string $path): string { $body = $this->readObject($path, false); return (string) $body->getContents(); } public function readStream(string $path) { /** @var resource $resource */ $resource = $this->readObject($path, true)->detach(); return $resource; } public function delete(string $path): void { $arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)]; $command = $this->client->getCommand('DeleteObject', $arguments); try { $this->client->execute($command); } catch (Throwable $exception) { throw UnableToDeleteFile::atLocation($path, '', $exception); } } public function deleteDirectory(string $path): void { $prefix = $this->prefixer->prefixPath($path); $prefix = ltrim(rtrim($prefix, '/') . '/', '/'); try { $this->client->deleteMatchingObjects($this->bucket, $prefix); } catch (Throwable $exception) { throw UnableToDeleteDirectory::atLocation($path, '', $exception); } } public function createDirectory(string $path, Config $config): void { $defaultVisibility = $config->get(Config::OPTION_DIRECTORY_VISIBILITY, $this->visibility->defaultForDirectories()); $config = $config->withDefaults([Config::OPTION_VISIBILITY => $defaultVisibility]); $this->upload(rtrim($path, '/') . '/', '', $config); } public function setVisibility(string $path, string $visibility): void { $arguments = [ 'Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path), 'ACL' => $this->visibility->visibilityToAcl($visibility), ]; $command = $this->client->getCommand('PutObjectAcl', $arguments); try { $this->client->execute($command); } catch (Throwable $exception) { throw UnableToSetVisibility::atLocation($path, '', $exception); } } public function visibility(string $path): FileAttributes { $arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)]; $command = $this->client->getCommand('GetObjectAcl', $arguments); try { $result = $this->client->execute($command); } catch (Throwable $exception) { throw UnableToRetrieveMetadata::visibility($path, '', $exception); } $visibility = $this->visibility->aclToVisibility((array) $result->get('Grants')); return new FileAttributes($path, null, $visibility); } private function fetchFileMetadata(string $path, string $type): FileAttributes { $arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)]; $command = $this->client->getCommand('HeadObject', $arguments); try { $result = $this->client->execute($command); } catch (Throwable $exception) { throw UnableToRetrieveMetadata::create($path, $type, '', $exception); } $attributes = $this->mapS3ObjectMetadata($result->toArray(), $path); if ( ! $attributes instanceof FileAttributes) { throw UnableToRetrieveMetadata::create($path, $type, ''); } return $attributes; } private function mapS3ObjectMetadata(array $metadata, string $path): StorageAttributes { if (substr($path, -1) === '/') { return new DirectoryAttributes(rtrim($path, '/')); } $mimetype = $metadata['ContentType'] ?? null; $fileSize = $metadata['ContentLength'] ?? $metadata['Size'] ?? null; $fileSize = $fileSize === null ? null : (int) $fileSize; $dateTime = $metadata['LastModified'] ?? null; $lastModified = $dateTime instanceof DateTimeResult ? $dateTime->getTimeStamp() : null; return new FileAttributes( $path, $fileSize, null, $lastModified, $mimetype, $this->extractExtraMetadata($metadata) ); } private function extractExtraMetadata(array $metadata): array { $extracted = []; foreach ($this->metadataFields as $field) { if (isset($metadata[$field]) && $metadata[$field] !== '') { $extracted[$field] = $metadata[$field]; } } return $extracted; } public function mimeType(string $path): FileAttributes { $attributes = $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_MIME_TYPE); if ($attributes->mimeType() === null) { throw UnableToRetrieveMetadata::mimeType($path); } return $attributes; } public function lastModified(string $path): FileAttributes { $attributes = $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_LAST_MODIFIED); if ($attributes->lastModified() === null) { throw UnableToRetrieveMetadata::lastModified($path); } return $attributes; } public function fileSize(string $path): FileAttributes { $attributes = $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_FILE_SIZE); if ($attributes->fileSize() === null) { throw UnableToRetrieveMetadata::fileSize($path); } return $attributes; } public function listContents(string $path, bool $deep): iterable { $prefix = trim($this->prefixer->prefixPath($path), '/'); $prefix = empty($prefix) ? '' : $prefix . '/'; $options = ['Bucket' => $this->bucket, 'Prefix' => $prefix]; if ($deep === false) { $options['Delimiter'] = '/'; } $listing = $this->retrievePaginatedListing($options); foreach ($listing as $item) { $key = $item['Key'] ?? $item['Prefix']; if ($key === $prefix) { continue; } yield $this->mapS3ObjectMetadata($item, $this->prefixer->stripPrefix($key)); } } private function retrievePaginatedListing(array $options): Generator { $resultPaginator = $this->client->getPaginator('ListObjectsV2', $options + $this->options); foreach ($resultPaginator as $result) { yield from ($result->get('CommonPrefixes') ?? []); yield from ($result->get('Contents') ?? []); } } public function move(string $source, string $destination, Config $config): void { try { $this->copy($source, $destination, $config); $this->delete($source); } catch (FilesystemOperationFailed $exception) { throw UnableToMoveFile::fromLocationTo($source, $destination, $exception); } } public function copy(string $source, string $destination, Config $config): void { try { $visibility = $config->get(Config::OPTION_VISIBILITY); if ($visibility === null && $config->get(Config::OPTION_RETAIN_VISIBILITY, true)) { $visibility = $this->visibility($source)->visibility(); } } catch (Throwable $exception) { throw UnableToCopyFile::fromLocationTo( $source, $destination, $exception ); } $options = $this->createOptionsFromConfig($config); $options['MetadataDirective'] = $config->get('MetadataDirective', 'COPY'); try { $this->client->copy( $this->bucket, $this->prefixer->prefixPath($source), $this->bucket, $this->prefixer->prefixPath($destination), $this->visibility->visibilityToAcl($visibility ?: 'private'), $options, ); } catch (Throwable $exception) { throw UnableToCopyFile::fromLocationTo($source, $destination, $exception); } } private function readObject(string $path, bool $wantsStream): StreamInterface { $options = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)]; if ($wantsStream && $this->streamReads && ! isset($this->options['@http']['stream'])) { $options['@http']['stream'] = true; } $command = $this->client->getCommand('GetObject', $options + $this->options); try { return $this->client->execute($command)->get('Body'); } catch (Throwable $exception) { throw UnableToReadFile::fromLocation($path, '', $exception); } } public function publicUrl(string $path, Config $config): string { $location = $this->prefixer->prefixPath($path); try { return $this->client->getObjectUrl($this->bucket, $location); } catch (Throwable $exception) { throw UnableToGeneratePublicUrl::dueToError($path, $exception); } } public function checksum(string $path, Config $config): string { $algo = $config->get('checksum_algo', 'etag'); if ($algo !== 'etag') { throw new ChecksumAlgoIsNotSupported(); } try { $metadata = $this->fetchFileMetadata($path, 'checksum')->extraMetadata(); } catch (UnableToRetrieveMetadata $exception) { throw new UnableToProvideChecksum($exception->reason(), $path, $exception); } if ( ! isset($metadata['ETag'])) { throw new UnableToProvideChecksum('ETag header not available.', $path); } return trim($metadata['ETag'], '"'); } public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string { try { $options = $config->get('get_object_options', []); $command = $this->client->getCommand('GetObject', [ 'Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path), ] + $options); $presignedRequestOptions = $config->get('presigned_request_options', []); $request = $this->client->createPresignedRequest($command, $expiresAt, $presignedRequestOptions); return (string) $request->getUri(); } catch (Throwable $exception) { throw UnableToGenerateTemporaryUrl::dueToError($path, $exception); } } } flysystem-aws-s3-v3/PortableVisibilityConverter.php 0000644 00000002366 15007532371 0016477 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem\AwsS3V3; use League\Flysystem\Visibility; class PortableVisibilityConverter implements VisibilityConverter { private const PUBLIC_GRANTEE_URI = 'http://acs.amazonaws.com/groups/global/AllUsers'; private const PUBLIC_GRANTS_PERMISSION = 'READ'; private const PUBLIC_ACL = 'public-read'; private const PRIVATE_ACL = 'private'; public function __construct(private string $defaultForDirectories = Visibility::PUBLIC) { } public function visibilityToAcl(string $visibility): string { if ($visibility === Visibility::PUBLIC) { return self::PUBLIC_ACL; } return self::PRIVATE_ACL; } public function aclToVisibility(array $grants): string { foreach ($grants as $grant) { $granteeUri = $grant['Grantee']['URI'] ?? null; $permission = $grant['Permission'] ?? null; if ($granteeUri === self::PUBLIC_GRANTEE_URI && $permission === self::PUBLIC_GRANTS_PERMISSION) { return Visibility::PUBLIC; } } return Visibility::PRIVATE; } public function defaultForDirectories(): string { return $this->defaultForDirectories; } } config/LICENSE.md 0000644 00000002751 15007532371 0007425 0 ustar 00 BSD 3-Clause License Copyright (c) 2022, Colin O'Dell. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. config/composer.json 0000644 00000003652 15007532371 0010544 0 ustar 00 { "name": "league/config", "type": "library", "description": "Define configuration arrays with strict schemas and access values with dot notation", "keywords": ["configuration","config","schema","array","nested","dot","dot-access"], "homepage": "https://config.thephpleague.com", "license": "BSD-3-Clause", "authors": [ { "name": "Colin O'Dell", "email": "colinodell@gmail.com", "homepage": "https://www.colinodell.com", "role": "Lead Developer" } ], "support": { "docs": "https://config.thephpleague.com/", "issues": "https://github.com/thephpleague/config/issues", "rss": "https://github.com/thephpleague/config/releases.atom", "source": "https://github.com/thephpleague/config" }, "require": { "php": "^7.4 || ^8.0", "dflydev/dot-access-data": "^3.0.1", "nette/schema": "^1.2" }, "require-dev": { "phpstan/phpstan": "^1.8.2", "phpunit/phpunit": "^9.5.5", "scrutinizer/ocular": "^1.8.1", "unleashedtech/php-coding-standard": "^3.1", "vimeo/psalm": "^4.7.3" }, "minimum-stability": "dev", "prefer-stable": true, "autoload": { "psr-4": { "League\\Config\\": "src" } }, "autoload-dev": { "psr-4": { "League\\Config\\Tests\\": "tests" } }, "scripts": { "phpcs": "phpcs", "phpstan": "phpstan analyse", "phpunit": "phpunit --no-coverage", "psalm": "psalm", "test": [ "@phpcs", "@phpstan", "@psalm", "@phpunit" ] }, "extra": { "branch-alias": { "dev-main": "1.2-dev" } }, "config": { "sort-packages": true, "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true } } } config/CHANGELOG.md 0000644 00000002403 15007532371 0007624 0 ustar 00 # Change Log All notable changes to this project will be documented in this file. Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) principles. ## [Unreleased][unreleased] ## [1.2.0] - 2022-12-11 ### Changed - Values can now be set prior to the corresponding schema being registered. - `exists()` and `get()` now only trigger validation for the relevant schema, not the entire config at once. ## [1.1.1] - 2021-08-14 ### Changed - Bumped the minimum version of dflydev/dot-access-data for PHP 8.1 support ## [1.1.0] - 2021-06-19 ### Changed - Bumped the minimum PHP version to 7.4+ - Bumped the minimum version of nette/schema to 1.2.0 ## [1.0.1] - 2021-05-31 ### Fixed - Fixed the `ConfigurationExceptionInterface` marker interface not extending `Throwable` (#2) ## [1.0.0] - 2021-05-31 Initial release! 🎉 [unreleased]: https://github.com/thephpleague/config/compare/v1.2.0...main [1.2.0]: https://github.com/thephpleague/config/compare/v1.1.1...v.1.2.0 [1.1.1]: https://github.com/thephpleague/config/compare/v1.1.0...v1.1.1 [1.1.0]: https://github.com/thephpleague/config/compare/v1.0.1...v1.1.0 [1.0.1]: https://github.com/thephpleague/config/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/thephpleague/config/releases/tag/v1.0.0 config/README.md 0000644 00000016002 15007532371 0007272 0 ustar 00 # league/config [](https://packagist.org/packages/league/config) [](https://packagist.org/packages/league/config) [](LICENSE) [](https://github.com/thephpleague/config/actions?query=workflow%3ATests+branch%3Amain) [](https://scrutinizer-ci.com/g/thephpleague/config/code-structure) [](https://scrutinizer-ci.com/g/thephpleague/config) [](https://www.colinodell.com/sponsor) **league/config** helps you define nested configuration arrays with strict schemas and access configuration values with dot notation. It was created by [Colin O'Dell][@colinodell]. ## 📦 Installation This project requires PHP 7.4 or higher. To install it via [Composer] simply run: ```bash composer require league/config ``` ## 🧰️ Basic Usage The `Configuration` class provides everything you need to define the configuration structure and fetch values: ```php use League\Config\Configuration; use Nette\Schema\Expect; // Define your configuration schema $config = new Configuration([ 'database' => Expect::structure([ 'driver' => Expect::anyOf('mysql', 'postgresql', 'sqlite')->required(), 'host' => Expect::string()->default('localhost'), 'port' => Expect::int()->min(1)->max(65535), 'ssl' => Expect::bool(), 'database' => Expect::string()->required(), 'username' => Expect::string()->required(), 'password' => Expect::string()->nullable(), ]), 'logging' => Expect::structure([ 'enabled' => Expect::bool()->default($_ENV['DEBUG'] == true), 'file' => Expect::string()->deprecated("use logging.path instead"), 'path' => Expect::string()->assert(function ($path) { return \is_writeable($path); })->required(), ]), ]); // Set the values, either all at once with `merge()`: $config->merge([ 'database' => [ 'driver' => 'mysql', 'port' => 3306, 'database' => 'mydb', 'username' => 'user', 'password' => 'secret', ], ]); // Or one-at-a-time with `set()`: $config->set('logging.path', '/var/log/myapp.log'); // You can now retrieve those values with `get()`. // Validation and defaults will be applied for you automatically $config->get('database'); // Fetches the entire "database" section as an array $config->get('database.driver'); // Fetch a specific nested value with dot notation $config->get('database/driver'); // Fetch a specific nested value with slash notation $config->get('database.host'); // Returns the default value "localhost" $config->get('logging.path'); // Guaranteed to be writeable thanks to the assertion in the schema // If validation fails an `InvalidConfigurationException` will be thrown: $config->set('database.driver', 'mongodb'); $config->get('database.driver'); // InvalidConfigurationException // Attempting to fetch a non-existent key will result in an `InvalidConfigurationException` $config->get('foo.bar'); // You could avoid this by checking whether that item exists: $config->exists('foo.bar'); // Returns `false` ``` ## 📓 Documentation Full documentation can be found at [config.thephpleague.com][docs]. ## 💭 Philosophy This library aims to provide a **simple yet opinionated** approach to configuration with the following goals: - The configuration should operate on **arrays with nested values** which are easily accessible - The configuration structure should be **defined with strict schemas** defining the overall structure, allowed types, and allowed values - Schemas should be defined using a **simple, fluent interface** - You should be able to **add and combine schemas but never modify existing ones** - Both the configuration values and the schema should be **defined and managed with PHP code** - Schemas should be **immutable**; they should never change once they are set - Configuration values should never define or influence the schemas As a result, this library will likely **never** support features like: - Loading and/or exporting configuration values or schemas using YAML, XML, or other files - Parsing configuration values from a command line or other user interface - Dynamically changing the schema, allowed values, or default values based on other configuration values If you need that functionality you should check out other libraries like: - [symfony/config] - [symfony/options-resolver] - [hassankhan/config] - [consolidation/config] - [laminas/laminas-config] ## 🏷️ Versioning [SemVer](http://semver.org/) is followed closely. Minor and patch releases should not introduce breaking changes to the codebase. Any classes or methods marked `@internal` are not intended for use outside this library and are subject to breaking changes at any time, so please avoid using them. ## 🛠️ Maintenance & Support When a new **minor** version (e.g. `1.0` -> `1.1`) is released, the previous one (`1.0`) will continue to receive security and critical bug fixes for *at least* 3 months. When a new **major** version is released (e.g. `1.1` -> `2.0`), the previous one (`1.1`) will receive critical bug fixes for *at least* 3 months and security updates for 6 months after that new release comes out. (This policy may change in the future and exceptions may be made on a case-by-case basis.) ## 👷️ Contributing Contributions to this library are **welcome**! We only ask that you adhere to our [contributor guidelines] and avoid making changes that conflict with our Philosophy above. ## 🧪 Testing ```bash composer test ``` ## 📄 License **league/config** is licensed under the BSD-3 license. See the [`LICENSE.md`][license] file for more details. ## 🗺️ Who Uses It? This project is used by [league/commonmark][league-commonmark]. [docs]: https://config.thephpleague.com/ [@colinodell]: https://www.twitter.com/colinodell [Composer]: https://getcomposer.org/ [PHP League]: https://thephpleague.com [symfony/config]: https://symfony.com/doc/current/components/config.html [symfony/options-resolver]: https://symfony.com/doc/current/components/options_resolver.html [hassankhan/config]: https://github.com/hassankhan/config [consolidation/config]: https://github.com/consolidation/config [laminas/laminas-config]: https://docs.laminas.dev/laminas-config/ [contributor guidelines]: https://github.com/thephpleague/config/blob/main/.github/CONTRIBUTING.md [license]: https://github.com/thephpleague/config/blob/main/LICENSE.md [league-commonmark]: https://commonmark.thephpleague.com config/src/ConfigurationProviderInterface.php 0000644 00000000745 15007532371 0015465 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/config package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\Config; /** * Interface for a service which provides a readable configuration object */ interface ConfigurationProviderInterface { public function getConfiguration(): ConfigurationInterface; } config/src/Exception/InvalidConfigurationException.php 0000644 00000002403 15007532371 0017246 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/config package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\Config\Exception; class InvalidConfigurationException extends \UnexpectedValueException implements ConfigurationExceptionInterface { /** * @param string $option Name/path of the option * @param mixed $valueGiven The invalid option that was provided * @param ?string $description Additional text describing the issue (optional) */ public static function forConfigOption(string $option, $valueGiven, ?string $description = null): self { $message = \sprintf('Invalid config option for "%s": %s', $option, self::getDebugValue($valueGiven)); if ($description !== null) { $message .= \sprintf(' (%s)', $description); } return new self($message); } /** * @param mixed $value * * @psalm-pure */ private static function getDebugValue($value): string { if (\is_object($value)) { return \get_class($value); } return \print_r($value, true); } } config/src/Exception/ConfigurationExceptionInterface.php 0000644 00000000673 15007532371 0017567 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/config package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\Config\Exception; /** * Marker interface for any/all exceptions thrown by this library */ interface ConfigurationExceptionInterface extends \Throwable { } config/src/Exception/ValidationException.php 0000644 00000001536 15007532371 0015230 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/config package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\Config\Exception; use Nette\Schema\ValidationException as NetteException; final class ValidationException extends InvalidConfigurationException { /** @var string[] */ private array $messages; public function __construct(NetteException $innerException) { parent::__construct($innerException->getMessage(), (int) $innerException->getCode(), $innerException); $this->messages = $innerException->getMessages(); } /** * @return string[] */ public function getMessages(): array { return $this->messages; } } config/src/Exception/UnknownOptionException.php 0000644 00000001354 15007532371 0015764 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/config package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\Config\Exception; use Throwable; final class UnknownOptionException extends \InvalidArgumentException implements ConfigurationExceptionInterface { private string $path; public function __construct(string $message, string $path, int $code = 0, ?Throwable $previous = null) { parent::__construct($message, $code, $previous); $this->path = $path; } public function getPath(): string { return $this->path; } } config/src/Configuration.php 0000644 00000014524 15007532371 0012131 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/config package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\Config; use Dflydev\DotAccessData\Data; use Dflydev\DotAccessData\DataInterface; use Dflydev\DotAccessData\Exception\DataException; use Dflydev\DotAccessData\Exception\InvalidPathException; use Dflydev\DotAccessData\Exception\MissingPathException; use League\Config\Exception\UnknownOptionException; use League\Config\Exception\ValidationException; use Nette\Schema\Expect; use Nette\Schema\Processor; use Nette\Schema\Schema; use Nette\Schema\ValidationException as NetteValidationException; final class Configuration implements ConfigurationBuilderInterface, ConfigurationInterface { /** @psalm-readonly */ private Data $userConfig; /** * @var array<string, Schema> * * @psalm-allow-private-mutation */ private array $configSchemas = []; /** @psalm-allow-private-mutation */ private Data $finalConfig; /** * @var array<string, mixed> * * @psalm-allow-private-mutation */ private array $cache = []; /** @psalm-readonly */ private ConfigurationInterface $reader; /** * @param array<string, Schema> $baseSchemas */ public function __construct(array $baseSchemas = []) { $this->configSchemas = $baseSchemas; $this->userConfig = new Data(); $this->finalConfig = new Data(); $this->reader = new ReadOnlyConfiguration($this); } /** * Registers a new configuration schema at the given top-level key * * @psalm-allow-private-mutation */ public function addSchema(string $key, Schema $schema): void { $this->invalidate(); $this->configSchemas[$key] = $schema; } /** * {@inheritDoc} * * @psalm-allow-private-mutation */ public function merge(array $config = []): void { $this->invalidate(); $this->userConfig->import($config, DataInterface::REPLACE); } /** * {@inheritDoc} * * @psalm-allow-private-mutation */ public function set(string $key, $value): void { $this->invalidate(); try { $this->userConfig->set($key, $value); } catch (DataException $ex) { throw new UnknownOptionException($ex->getMessage(), $key, (int) $ex->getCode(), $ex); } } /** * {@inheritDoc} * * @psalm-external-mutation-free */ public function get(string $key) { if (\array_key_exists($key, $this->cache)) { return $this->cache[$key]; } try { $this->build(self::getTopLevelKey($key)); return $this->cache[$key] = $this->finalConfig->get($key); } catch (InvalidPathException | MissingPathException $ex) { throw new UnknownOptionException($ex->getMessage(), $key, (int) $ex->getCode(), $ex); } } /** * {@inheritDoc} * * @psalm-external-mutation-free */ public function exists(string $key): bool { if (\array_key_exists($key, $this->cache)) { return true; } try { $this->build(self::getTopLevelKey($key)); return $this->finalConfig->has($key); } catch (InvalidPathException | UnknownOptionException $ex) { return false; } } /** * @psalm-mutation-free */ public function reader(): ConfigurationInterface { return $this->reader; } /** * @psalm-external-mutation-free */ private function invalidate(): void { $this->cache = []; $this->finalConfig = new Data(); } /** * Applies the schema against the configuration to return the final configuration * * @throws ValidationException|UnknownOptionException|InvalidPathException * * @psalm-allow-private-mutation */ private function build(string $topLevelKey): void { if ($this->finalConfig->has($topLevelKey)) { return; } if (! isset($this->configSchemas[$topLevelKey])) { throw new UnknownOptionException(\sprintf('Missing config schema for "%s"', $topLevelKey), $topLevelKey); } try { $userData = [$topLevelKey => $this->userConfig->get($topLevelKey)]; } catch (DataException $ex) { $userData = []; } try { $schema = $this->configSchemas[$topLevelKey]; $processor = new Processor(); $processed = $processor->process(Expect::structure([$topLevelKey => $schema]), $userData); $this->raiseAnyDeprecationNotices($processor->getWarnings()); $this->finalConfig->import((array) self::convertStdClassesToArrays($processed)); } catch (NetteValidationException $ex) { throw new ValidationException($ex); } } /** * Recursively converts stdClass instances to arrays * * @phpstan-template T * * @param T $data * * @return mixed * * @phpstan-return ($data is \stdClass ? array<string, mixed> : T) * * @psalm-pure */ private static function convertStdClassesToArrays($data) { if ($data instanceof \stdClass) { $data = (array) $data; } if (\is_array($data)) { foreach ($data as $k => $v) { $data[$k] = self::convertStdClassesToArrays($v); } } return $data; } /** * @param string[] $warnings */ private function raiseAnyDeprecationNotices(array $warnings): void { foreach ($warnings as $warning) { @\trigger_error($warning, \E_USER_DEPRECATED); } } /** * @throws InvalidPathException */ private static function getTopLevelKey(string $path): string { if (\strlen($path) === 0) { throw new InvalidPathException('Path cannot be an empty string'); } $path = \str_replace(['.', '/'], '.', $path); $firstDelimiter = \strpos($path, '.'); if ($firstDelimiter === false) { return $path; } return \substr($path, 0, $firstDelimiter); } } config/src/ConfigurationInterface.php 0000644 00000002226 15007532371 0013746 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/config package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\Config; use League\Config\Exception\UnknownOptionException; use League\Config\Exception\ValidationException; /** * Interface for reading configuration values */ interface ConfigurationInterface { /** * @param string $key Configuration option path/key * * @psalm-param non-empty-string $key * * @return mixed * * @throws ValidationException if the schema failed to validate the given input * @throws UnknownOptionException if the requested key does not exist or is malformed */ public function get(string $key); /** * @param string $key Configuration option path/key * * @psalm-param non-empty-string $key * * @return bool Whether the given option exists * * @throws ValidationException if the schema failed to validate the given input */ public function exists(string $key): bool; } config/src/ConfigurationAwareInterface.php 0000644 00000001004 15007532371 0014717 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/config package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\Config; /** * Implement this class to facilitate setter injection of the configuration where needed */ interface ConfigurationAwareInterface { public function setConfiguration(ConfigurationInterface $configuration): void; } config/src/ConfigurationBuilderInterface.php 0000644 00000000762 15007532371 0015260 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/config package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\Config; /** * An interface that provides the ability to set both the schema and configuration values */ interface ConfigurationBuilderInterface extends MutableConfigurationInterface, SchemaBuilderInterface { } config/src/MutableConfigurationInterface.php 0000644 00000001462 15007532371 0015261 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/config package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\Config; use League\Config\Exception\UnknownOptionException; /** * Interface for setting/merging user-defined configuration values into the configuration object */ interface MutableConfigurationInterface { /** * @param mixed $value * * @throws UnknownOptionException if $key contains a nested path which doesn't point to an array value */ public function set(string $key, $value): void; /** * @param array<string, mixed> $config */ public function merge(array $config = []): void; } config/src/ReadOnlyConfiguration.php 0000644 00000001442 15007532371 0013562 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/config package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\Config; /** * Provides read-only access to a given Configuration object */ final class ReadOnlyConfiguration implements ConfigurationInterface { private Configuration $config; public function __construct(Configuration $config) { $this->config = $config; } /** * {@inheritDoc} */ public function get(string $key) { return $this->config->get($key); } public function exists(string $key): bool { return $this->config->exists($key); } } config/src/SchemaBuilderInterface.php 0000644 00000001112 15007532371 0013637 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/config package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\Config; use Nette\Schema\Schema; /** * Interface that allows new schemas to be added to a configuration */ interface SchemaBuilderInterface { /** * Registers a new configuration schema at the given top-level key */ public function addSchema(string $key, Schema $schema): void; } commonmark/composer.json 0000644 00000010021 15007532371 0011426 0 ustar 00 { "name": "league/commonmark", "type": "library", "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", "keywords": ["markdown","parser","commonmark","gfm","github","flavored","github-flavored","md"], "homepage": "https://commonmark.thephpleague.com", "license": "BSD-3-Clause", "authors": [ { "name": "Colin O'Dell", "email": "colinodell@gmail.com", "homepage": "https://www.colinodell.com", "role": "Lead Developer" } ], "support": { "docs": "https://commonmark.thephpleague.com/", "forum": "https://github.com/thephpleague/commonmark/discussions", "issues": "https://github.com/thephpleague/commonmark/issues", "rss": "https://github.com/thephpleague/commonmark/releases.atom", "source": "https://github.com/thephpleague/commonmark" }, "require": { "php": "^7.4 || ^8.0", "ext-mbstring": "*", "league/config": "^1.1.1", "psr/event-dispatcher": "^1.0", "symfony/deprecation-contracts": "^2.1 || ^3.0", "symfony/polyfill-php80": "^1.16" }, "require-dev": { "ext-json": "*", "cebe/markdown": "^1.0", "commonmark/cmark": "0.30.3", "commonmark/commonmark.js": "0.30.0", "composer/package-versions-deprecated": "^1.8", "embed/embed": "^4.4", "erusev/parsedown": "^1.0", "github/gfm": "0.29.0", "michelf/php-markdown": "^1.4 || ^2.0", "nyholm/psr7": "^1.5", "phpstan/phpstan": "^1.8.2", "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", "scrutinizer/ocular": "^1.8.1", "symfony/finder": "^5.3 | ^6.0 || ^7.0", "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 || ^7.0", "unleashedtech/php-coding-standard": "^3.1.1", "vimeo/psalm": "^4.24.0 || ^5.0.0" }, "minimum-stability": "beta", "suggest": { "symfony/yaml": "v2.3+ required if using the Front Matter extension" }, "repositories": [ { "type": "package", "package": { "name": "commonmark/commonmark.js", "version": "0.30.0", "dist": { "url": "https://github.com/commonmark/commonmark.js/archive/0.30.0.zip", "type": "zip" } } }, { "type": "package", "package": { "name": "commonmark/cmark", "version": "0.30.3", "dist": { "url": "https://github.com/commonmark/cmark/archive/0.30.3.zip", "type": "zip" } } }, { "type": "package", "package": { "name": "github/gfm", "version": "0.29.0", "dist": { "url": "https://github.com/github/cmark-gfm/archive/0.29.0.gfm.13.zip", "type": "zip" } } } ], "autoload": { "psr-4": { "League\\CommonMark\\": "src" } }, "autoload-dev": { "psr-4": { "League\\CommonMark\\Tests\\Unit\\": "tests/unit", "League\\CommonMark\\Tests\\Functional\\": "tests/functional", "League\\CommonMark\\Tests\\PHPStan\\": "tests/phpstan" } }, "scripts": { "phpcs": "phpcs", "phpstan": "phpstan analyse", "phpunit": "phpunit --no-coverage", "psalm": "psalm --stats", "test": [ "@phpcs", "@phpstan", "@psalm", "@phpunit" ] }, "extra": { "branch-alias": { "dev-main": "2.5-dev" } }, "config": { "allow-plugins": { "composer/package-versions-deprecated": true, "dealerdirect/phpcodesniffer-composer-installer": true }, "sort-packages": true } } commonmark/LICENSE 0000644 00000003163 15007532371 0007722 0 ustar 00 BSD 3-Clause License Copyright (c) 2014-2022, Colin O'Dell. All rights reserved. Some code based on commonmark.js (copyright 2014-2018, John MacFarlane) and commonmark-java (copyright 2015-2016, Atlassian Pty Ltd) Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. commonmark/CHANGELOG.md 0000644 00000065525 15007532371 0010540 0 ustar 00 # Change Log All notable changes to this project will be documented in this file. Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) principles. **Upgrading from 1.x?** See <https://commonmark.thephpleague.com/2.0/upgrading/> for additional information. ## [Unreleased][unreleased] ## [2.4.2] - 2024-02-02 ### Fixed - Fixed declaration parser being too strict - `FencedCodeRenderer`: don't add `language-` to class if already prefixed ## [2.4.1] - 2023-08-30 ### Fixed - Fixed `ExternalLinkProcessor` not fully disabling the `rel` attribute when configured to do so (#992) ## [2.4.0] - 2023-03-24 ### Added - Added generic `CommonMarkException` marker interface for all exceptions thrown by the library - Added several new specific exception types implementing that marker interface: - `AlreadyInitializedException` - `InvalidArgumentException` - `IOException` - `LogicException` - `MissingDependencyException` - `NoMatchingRendererException` - `ParserLogicException` - Added more configuration options to the Heading Permalinks extension (#939): - `heading_permalink/apply_id_to_heading` - When `true`, the `id` attribute will be applied to the heading element itself instead of the `<a>` tag - `heading_permalink/heading_class` - class to apply to the heading element - `heading_permalink/insert` - now accepts `none` to prevent the creation of the `<a>` link - Added new `table/alignment_attributes` configuration option to control how table cell alignment is rendered (#959) ### Changed - Change several thrown exceptions from `RuntimeException` to `LogicException` (or something extending it), including: - `CallbackGenerator`s that fail to set a URL or return an expected value - `MarkdownParser` when deactivating the last block parser or attempting to get an active block parser when they've all been closed - Adding items to an already-initialized `Environment` - Rendering a `Node` when no renderer has been registered for it - `HeadingPermalinkProcessor` now throws `InvalidConfigurationException` instead of `RuntimeException` when invalid config values are given. - `HtmlElement::setAttribute()` no longer requires the second parameter for boolean attributes - Several small micro-optimizations - Changed Strikethrough to only allow 1 or 2 tildes per the updated GFM spec ### Fixed - Fixed inaccurate `@throws` docblocks throughout the codebase, including `ConverterInterface`, `MarkdownConverter`, and `MarkdownConverterInterface`. - These previously suggested that only `\RuntimeException`s were thrown, which was inaccurate as `\LogicException`s were also possible. ## [2.3.9] - 2023-02-15 ### Fixed - Fixed autolink extension not detecting some URIs with underscores (#956) ## [2.3.8] - 2022-12-10 ### Fixed - Fixed parsing issues when `mb_internal_encoding()` is set to something other than `UTF-8` (#951) ## [2.3.7] - 2022-11-03 ### Fixed - Fixed `TaskListItemMarkerRenderer` not including HTML attributes set on the node by other extensions (#947) ## [2.3.6] - 2022-10-30 ### Fixed - Fixed unquoted attribute parsing when closing curly brace is followed by certain characters (like a `.`) (#943) ## [2.3.5] - 2022-07-29 ### Fixed - Fixed error using `InlineParserEngine` when no inline parsers are registered in the `Environment` (#908) ## [2.3.4] - 2022-07-17 ### Changed - Made a number of small tweaks to the embed extension's parsing behavior to fix #898: - Changed `EmbedStartParser` to always capture embed-like lines in container blocks, regardless of parent block type - Changed `EmbedProcessor` to also remove `Embed` blocks that aren't direct children of the `Document` - Increased the priority of `EmbedProcessor` to `1010` ### Fixed - Fixed `EmbedExtension` not parsing embeds following a list block (#898) ## [2.3.3] - 2022-06-07 ### Fixed - Fixed `DomainFilteringAdapter` not reindexing the embed list (#884, #885) ## [2.3.2] - 2022-06-03 ### Fixed - Fixed FootnoteExtension stripping extra characters from tab-indented footnotes (#881) ## [2.2.5] - 2022-06-03 ### Fixed - Fixed FootnoteExtension stripping extra characters from tab-indented footnotes (#881) ## [2.3.1] - 2022-05-14 ### Fixed - Fixed AutolinkExtension not ignoring trailing strikethrough syntax (#867) ## [2.2.4] - 2022-05-14 ### Fixed - Fixed AutolinkExtension not ignoring trailing strikethrough syntax (#867) ## [2.3.0] - 2022-04-07 ### Added - Added new `EmbedExtension` (#805) - Added `DocumentRendererInterface` as a replacement for the now-deprecated `MarkdownRendererInterface` ### Deprecated - Deprecated `MarkdownRendererInterface`; use `DocumentRendererInterface` instead ## [2.2.3] - 2022-02-26 ### Fixed - Fixed front matter parsing with Windows line endings (#821) ## [2.1.3] - 2022-02-26 ### Fixed - Fixed front matter parsing with Windows line endings (#821) ## [2.0.4] - 2022-02-26 ### Fixed - Fixed front matter parsing with Windows line endings (#821) ## [2.2.2] - 2022-02-13 ### Fixed - Fixed double-escaping of image alt text (#806, #810) - Fixed Psalm typehints for event class names ## [2.2.1] - 2022-01-25 ### Fixed - Fixed `symfony/deprecation-contracts` constraint ### Removed - Removed deprecation trigger from `MarkdownConverterInterface` to reduce noise ## [2.2.0] - 2022-01-22 ### Added - Added new `ConverterInterface` - Added new `MarkdownToXmlConverter` class - Added new `HtmlDecorator` class which can wrap existing renderers with additional HTML tags - Added new `table/wrap` config to apply an optional wrapping/container element around a table (#780) ### Changed - `HtmlElement` contents can now consist of any `Stringable`, not just `HtmlElement` and `string` ### Deprecated - Deprecated `MarkdownConverterInterface` and its `convertToHtml()` method; use `ConverterInterface` and `convert()` instead ## [2.1.2] - 2022-02-13 ### Fixed - Fixed double-escaping of image alt text (#806, #810) - Fixed Psalm typehints for event class names ## [2.1.1] - 2022-01-02 ### Added - Added missing return type to `Environment::dispatch()` to fix deprecation warning (#778) ## [2.1.0] - 2021-12-05 ### Added - Added support for ext-yaml in FrontMatterExtension (#715) - Added support for symfony/yaml v6.0 in FrontMatterExtension (#739) - Added new `heading_permalink/aria_hidden` config option (#741) ### Fixed - Fixed PHP 8.1 deprecation warning (#759, #762) ## [2.0.3] - 2022-02-13 ### Fixed - Fixed double-escaping of image alt text (#806, #810) - Fixed Psalm typehints for event class names ## [2.0.2] - 2021-08-14 ### Changed - Bumped minimum version of league/config to support PHP 8.1 ### Fixed - Fixed ability to register block parsers that identify lines starting with letters (#706) ## [2.0.1] - 2021-07-31 ### Fixed - Fixed nested autolinks (#689) - Fixed description lists being parsed incorrectly (#692) - Fixed Table of Contents not respecting Heading Permalink prefixes (#690) ## [2.0.0] - 2021-07-24 No changes were introduced since the previous RC2 release. See all entries below for a list of changes between 1.x and 2.0. ## [2.0.0-rc2] - 2021-07-17 ### Fixed - Fixed Mentions inside of links creating nested links against the spec's rules (#688) ## [2.0.0-rc1] - 2021-07-10 No changes were introduced since the previous release. ## [2.0.0-beta3] - 2021-07-03 ### Changed - Any leading UTF-8 BOM will be stripped from the input - The `getEnvironment()` method of `CommonMarkConverter` and `GithubFlavoredMarkdownConverter` will always return the concrete, configurable `Environment` for upgrading convenience - Optimized AST iteration - Lots of small micro-optimizations ## [2.0.0-beta2] - 2021-06-27 ### Added - Added new `Node::iterator()` method and `NodeIterator` class for faster AST iteration (#683, #684) ### Changed - Made compatible with CommonMark spec 0.30.0 - Optimized link label parsing - Optimized AST iteration for a 50% performance boost in some event listeners (#683, #684) ### Fixed - Fixed processing instructions with EOLs - Fixed case-insensitive matching for HTML tag types - Fixed type 7 HTML blocks incorrectly interrupting lazy paragraphs - Fixed newlines in reference labels not collapsing into spaces - Fixed link label normalization with escaped newlines - Fixed unnecessary AST iteration when no default attributes are configured ## [2.0.0-beta1] - 2021-06-20 ### Added - **Added three new extensions:** - `FrontMatterExtension` ([see documentation](https://commonmark.thephpleague.com/extensions/front-matter/)) - `DescriptionListExtension` ([see documentation](https://commonmark.thephpleague.com/extensions/description-lists/)) - `DefaultAttributesExtension` ([see documentation](https://commonmark.thephpleague.com/extensions/default-attributes/)) - **Added new `XmlRenderer` to simplify AST debugging** ([see documentation](https://commonmark.thephpleague.com/xml/)) (#431) - **Added the ability to configure disallowed raw HTML tags** (#507) - **Added the ability for Mentions to use multiple characters for their symbol** (#514, #550) - **Added the ability to delegate event dispatching to PSR-14 compliant event dispatcher libraries** - **Added new configuration options:** - Added `heading_permalink/min_heading_level` and `heading_permalink/max_heading_level` options to control which headings get permalinks (#519) - Added `heading_permalink/fragment_prefix` to allow customizing the URL fragment prefix (#602) - Added `footnote/backref_symbol` option for customizing backreference link appearance (#522) - Added `slug_normalizer/max_length` option to control the maximum length of generated URL slugs - Added `slug_normalizer/unique` option to control whether unique slugs should be generated per-document or per-environment - **Added purity markers throughout the codebase** (verified with Psalm) - Added `Query` class to simplify Node traversal when looking to take action on certain Nodes - Added new `HtmlFilter` and `StringContainerHelper` utility classes - Added new `AbstractBlockContinueParser` class to simplify the creation of custom block parsers - Added several new classes and interfaces: - `BlockContinue` - `BlockContinueParserInterface` - `BlockContinueParserWithInlinesInterface` - `BlockStart` - `BlockStartParserInterface` - `ChildNodeRendererInterface` - `ConfigurableExtensionInterface` - `CursorState` - `DashParser` (extracted from `PunctuationParser`) - `DelimiterParser` - `DocumentBlockParser` - `DocumentPreRenderEvent` - `DocumentRenderedEvent` - `EllipsesParser` (extracted from `PunctuationParser`) - `ExpressionInterface` - `FallbackNodeXmlRenderer` - `InlineParserEngineInterface` - `InlineParserMatch` - `MarkdownParserState` - `MarkdownParserStateInterface` - `MarkdownRendererInterface` - `Query` - `RawMarkupContainerInterface` - `ReferenceableInterface` - `RenderedContent` - `RenderedContentInterface` - `ReplaceUnpairedQuotesListener` - `SpecReader` - `TableOfContentsRenderer` - `UniqueSlugNormalizer` - `UniqueSlugNormalizerInterface` - `XmlRenderer` - `XmlNodeRendererInterface` - Added several new methods: - `Cursor::getCurrentCharacter()` - `Environment::createDefaultConfiguration()` - `Environment::setEventDispatcher()` - `EnvironmentInterface::getExtensions()` - `EnvironmentInterface::getInlineParsers()` - `EnvironmentInterface::getSlugNormalizer()` - `FencedCode::setInfo()` - `Heading::setLevel()` - `HtmlRenderer::renderDocument()` - `InlineParserContext::getFullMatch()` - `InlineParserContext::getFullMatchLength()` - `InlineParserContext::getMatches()` - `InlineParserContext::getSubMatches()` - `LinkParserHelper::parsePartialLinkLabel()` - `LinkParserHelper::parsePartialLinkTitle()` - `Node::assertInstanceOf()` - `RegexHelper::isLetter()` - `StringContainerInterface::setLiteral()` - `TableCell::getType()` - `TableCell::setType()` - `TableCell::getAlign()` - `TableCell::setAlign()` ### Changed - **Changed the converter return type** - `CommonMarkConverter::convertToHtml()` now returns an instance of `RenderedContentInterface`. This can be cast to a string for backward compatibility with 1.x. - **Table of Contents items are no longer wrapped with `<p>` tags** (#613) - **Heading Permalinks now link to element IDs instead of using `name` attributes** (#602) - **Heading Permalink IDs and URL fragments now have a `content` prefix by default** (#602) - **Changes to configuration options:** - `enable_em` has been renamed to `commonmark/enable_em` - `enable_strong` has been renamed to `commonmark/enable_strong` - `use_asterisk` has been renamed to `commonmark/use_asterisk` - `use_underscore` has been renamed to `commonmark/use_underscore` - `unordered_list_markers` has been renamed to `commonmark/unordered_list_markers` - `mentions/*/symbol` has been renamed to `mentions/*/prefix` - `mentions/*/regex` has been renamed to `mentions/*/pattern` and requires partial regular expressions (without delimiters or flags) - `max_nesting_level` now defaults to `PHP_INT_MAX` and no longer supports floats - `heading_permalink/slug_normalizer` has been renamed to `slug_normalizer/instance` - **Event dispatching is now fully PSR-14 compliant** - **Moved and renamed several classes** - [see the full list here](https://commonmark.thephpleague.com/2.0/upgrading/#classesnamespaces-renamed) - The `HeadingPermalinkExtension` and `FootnoteExtension` were modified to ensure they never produce a slug which conflicts with slugs created by the other extension - `SlugNormalizer::normalizer()` now supports optional prefixes and max length options passed in via the `$context` argument - The `AbstractBlock::$data` and `AbstractInline::$data` arrays were replaced with a `Data` array-like object on the base `Node` class - **Implemented a new approach to block parsing.** This was a massive change, so here are the highlights: - Functionality previously found in block parsers and node elements has moved to block parser factories and block parsers, respectively ([more details](https://commonmark.thephpleague.com/2.0/upgrading/#new-block-parsing-approach)) - `ConfigurableEnvironmentInterface::addBlockParser()` is now `EnvironmentBuilderInterface::addBlockParserFactory()` - `ReferenceParser` was re-implemented and works completely different than before - The paragraph parser no longer needs to be added manually to the environment - **Implemented a new approach to inline parsing** where parsers can now specify longer strings or regular expressions they want to parse (instead of just single characters): - `InlineParserInterface::getCharacters()` is now `getMatchDefinition()` and returns an instance of `InlineParserMatch` - `InlineParserContext::__construct()` now requires the contents to be provided as a `Cursor` instead of a `string` - **Implemented delimiter parsing as a special type of inline parser** (via the new `DelimiterParser` class) - **Changed block and inline rendering to use common methods and interfaces** - `BlockRendererInterface` and `InlineRendererInterface` were replaced by `NodeRendererInterface` with slightly different parameters. All core renderers now implement this interface. - `ConfigurableEnvironmentInterface::addBlockRenderer()` and `addInlineRenderer()` were combined into `EnvironmentBuilderInterface::addRenderer()` - `EnvironmentInterface::getBlockRenderersForClass()` and `getInlineRenderersForClass()` are now just `getRenderersForClass()` - **Completely refactored the Configuration implementation** - All configuration-specific classes have been moved into a new `league/config` package with a new namespace - `Configuration` objects must now be configured with a schema and all options must match that schema - arbitrary keys are no longer permitted - `Configuration::__construct()` no longer accepts the default configuration values - use `Configuration::merge()` instead - `ConfigurationInterface` now only contains a `get(string $key)`; this method no longer allows arbitrary default values to be returned if the option is missing - `ConfigurableEnvironmentInterface` was renamed to `EnvironmentBuilderInterface` - `ExtensionInterface::register()` now requires an `EnvironmentBuilderInterface` param instead of `ConfigurableEnvironmentInterface` - **Added missing return types to virtually every class and interface method** - Re-implemented the GFM Autolink extension using the new inline parser approach instead of document processors - `EmailAutolinkProcessor` is now `EmailAutolinkParser` - `UrlAutolinkProcessor` is now `UrlAutolinkParser` - `HtmlElement` can now properly handle array (i.e. `class`) and boolean (i.e. `checked`) attribute values - `HtmlElement` automatically flattens any attributes with array values into space-separated strings, removing duplicate entries - Combined separate classes/interfaces into one: - `DisallowedRawHtmlRenderer` replaces `DisallowedRawHtmlBlockRenderer` and `DisallowedRawHtmlInlineRenderer` - `NodeRendererInterface` replaces `BlockRendererInterface` and `InlineRendererInterface` - Renamed the following methods: - `Environment` and `ConfigurableEnvironmentInterface`: - `addBlockParser()` is now `addBlockStartParser()` - `ReferenceMap` and `ReferenceMapInterface`: - `addReference()` is now `add()` - `getReference()` is now `get()` - `listReferences()` is now `getIterator()` - Various node (block/inline) classes: - `getContent()` is now `getLiteral()` - `setContent()` is now `setLiteral()` - Moved and renamed the following constants: - `EnvironmentInterface::HTML_INPUT_ALLOW` is now `HtmlFilter::ALLOW` - `EnvironmentInterface::HTML_INPUT_ESCAPE` is now `HtmlFilter::ESCAPE` - `EnvironmentInterface::HTML_INPUT_STRIP` is now `HtmlFilter::STRIP` - `TableCell::TYPE_HEAD` is now `TableCell::TYPE_HEADER` - `TableCell::TYPE_BODY` is now `TableCell::TYPE_DATA` - Changed the visibility of the following properties: - `AttributesInline::$attributes` is now `private` - `AttributesInline::$block` is now `private` - `TableCell::$align` is now `private` - `TableCell::$type` is now `private` - `TableSection::$type` is now `private` - Several methods which previously returned `$this` now return `void` - `Delimiter::setPrevious()` - `Node::replaceChildren()` - `Context::setTip()` - `Context::setContainer()` - `Context::setBlocksParsed()` - `AbstractStringContainer::setContent()` - `AbstractWebResource::setUrl()` - Several classes are now marked `final`: - `ArrayCollection` - `Emphasis` - `FencedCode` - `Heading` - `HtmlBlock` - `HtmlElement` - `HtmlInline` - `IndentedCode` - `Newline` - `Strikethrough` - `Strong` - `Text` - `Heading` nodes no longer directly contain a copy of their inner text - `StringContainerInterface` can now be used for inlines, not just blocks - `ArrayCollection` only supports integer keys - `HtmlElement` now implements `Stringable` - `Cursor::saveState()` and `Cursor::restoreState()` now use `CursorState` objects instead of arrays - `NodeWalker::next()` now enters, traverses any children, and leaves all elements which may have children (basically all blocks plus any inlines with children). Previously, it only did this for elements explicitly marked as "containers". - `InvalidOptionException` was removed - Anything with a `getReference(): ReferenceInterface` method now implements `ReferencableInterface` - The `SmartPunct` extension now replaces all unpaired `Quote` elements with `Text` elements towards the end of parsing, making the `QuoteRenderer` unnecessary - Several changes made to the Footnote extension: - Footnote identifiers can no longer contain spaces - Anonymous footnotes can now span subsequent lines - Footnotes can now contain multiple lines of content, including sub-blocks, by indenting them - Footnote event listeners now have numbered priorities (but still execute in the same order) - Footnotes must now be separated from previous content by a blank line - The line numbers (keys) returned via `MarkdownInput::getLines()` now start at 1 instead of 0 - `DelimiterProcessorCollectionInterface` now extends `Countable` - `RegexHelper::PARTIAL_` constants must always be used in case-insensitive contexts - `HeadingPermalinkProcessor` no longer accepts text normalizers via the constructor - these must be provided via configuration instead - Blocks which can't contain inlines will no longer be asked to render inlines - `AnonymousFootnoteRefParser` and `HeadingPermalinkProcessor` now implement `EnvironmentAwareInterface` instead of `ConfigurationAwareInterface` - The second argument to `TextNormalizerInterface::normalize()` must now be an array - The `title` attribute for `Link` and `Image` nodes is now stored using a dedicated property instead of stashing it in `$data` - `ListData::$delimiter` now returns either `ListBlock::DELIM_PERIOD` or `ListBlock::DELIM_PAREN` instead of the literal delimiter ### Fixed - **Fixed parsing of footnotes without content** - **Fixed rendering of orphaned footnotes and footnote refs** - **Fixed some URL autolinks breaking too early** (#492) - Fixed `AbstractStringContainer` not actually being `abstract` ### Removed - **Removed support for PHP 7.1, 7.2, and 7.3** (#625, #671) - **Removed all previously-deprecated functionality:** - Removed the ability to pass custom `Environment` instances into the `CommonMarkConverter` and `GithubFlavoredMarkdownConverter` constructors - Removed the `Converter` class and `ConverterInterface` - Removed the `bin/commonmark` script - Removed the `Html5Entities` utility class - Removed the `InlineMentionParser` (use `MentionParser` instead) - Removed `DefaultSlugGenerator` and `SlugGeneratorInterface` from the `Extension/HeadingPermalink/Slug` sub-namespace (use the new ones under `./SlugGenerator` instead) - Removed the following `ArrayCollection` methods: - `add()` - `set()` - `get()` - `remove()` - `isEmpty()` - `contains()` - `indexOf()` - `containsKey()` - `replaceWith()` - `removeGaps()` - Removed the `ConfigurableEnvironmentInterface::setConfig()` method - Removed the `ListBlock::TYPE_UNORDERED` constant - Removed the `CommonMarkConverter::VERSION` constant - Removed the `HeadingPermalinkRenderer::DEFAULT_INNER_CONTENTS` constant - Removed the `heading_permalink/inner_contents` configuration option - **Removed now-unused classes:** - `AbstractStringContainerBlock` - `BlockRendererInterface` - `Context` - `ContextInterface` - `Converter` - `ConverterInterface` - `InlineRendererInterface` - `PunctuationParser` (was split into two classes: `DashParser` and `EllipsesParser`) - `QuoteRenderer` - `UnmatchedBlockCloser` - Removed the following methods, properties, and constants: - `AbstractBlock::$open` - `AbstractBlock::$lastLineBlank` - `AbstractBlock::isContainer()` - `AbstractBlock::canContain()` - `AbstractBlock::isCode()` - `AbstractBlock::matchesNextLine()` - `AbstractBlock::endsWithBlankLine()` - `AbstractBlock::setLastLineBlank()` - `AbstractBlock::shouldLastLineBeBlank()` - `AbstractBlock::isOpen()` - `AbstractBlock::finalize()` - `AbstractBlock::getData()` - `AbstractInline::getData()` - `ConfigurableEnvironmentInterface::addBlockParser()` - `ConfigurableEnvironmentInterface::mergeConfig()` - `Delimiter::setCanClose()` - `EnvironmentInterface::getConfig()` - `EnvironmentInterface::getInlineParsersForCharacter()` - `EnvironmentInterface::getInlineParserCharacterRegex()` - `HtmlRenderer::renderBlock()` - `HtmlRenderer::renderBlocks()` - `HtmlRenderer::renderInline()` - `HtmlRenderer::renderInlines()` - `Node::isContainer()` - `RegexHelper::matchAll()` (use the new `matchFirst()` method instead) - `RegexHelper::REGEX_WHITESPACE` - Removed the second `$contents` argument from the `Heading` constructor ### Deprecated **The following things have been deprecated and will not be supported in v3.0:** - `Environment::mergeConfig()` (set configuration before instantiation instead) - `Environment::createCommonMarkEnvironment()` and `Environment::createGFMEnvironment()` - Alternative 1: Use `CommonMarkConverter` or `GithubFlavoredMarkdownConverter` if you don't need to customize the environment - Alternative 2: Instantiate a new `Environment` and add the necessary extensions yourself [unreleased]: https://github.com/thephpleague/commonmark/compare/2.4.2...main [2.4.2]: https://github.com/thephpleague/commonmark/compare/2.4.1...2.4.2 [2.4.1]: https://github.com/thephpleague/commonmark/compare/2.4.0...2.4.1 [2.4.0]: https://github.com/thephpleague/commonmark/compare/2.3.9...2.4.0 [2.3.9]: https://github.com/thephpleague/commonmark/compare/2.3.8...2.3.9 [2.3.8]: https://github.com/thephpleague/commonmark/compare/2.3.7...2.3.8 [2.3.7]: https://github.com/thephpleague/commonmark/compare/2.3.6...2.3.7 [2.3.6]: https://github.com/thephpleague/commonmark/compare/2.3.5...2.3.6 [2.3.5]: https://github.com/thephpleague/commonmark/compare/2.3.4...2.3.5 [2.3.4]: https://github.com/thephpleague/commonmark/compare/2.3.3...2.3.4 [2.3.3]: https://github.com/thephpleague/commonmark/compare/2.3.2...2.3.3 [2.3.2]: https://github.com/thephpleague/commonmark/compare/2.3.2...main [2.3.1]: https://github.com/thephpleague/commonmark/compare/2.3.0...2.3.1 [2.3.0]: https://github.com/thephpleague/commonmark/compare/2.2.3...2.3.0 [2.2.5]: https://github.com/thephpleague/commonmark/compare/2.2.4...2.2.5 [2.2.4]: https://github.com/thephpleague/commonmark/compare/2.2.3...2.2.4 [2.2.3]: https://github.com/thephpleague/commonmark/compare/2.2.2...2.2.3 [2.2.2]: https://github.com/thephpleague/commonmark/compare/2.2.1...2.2.2 [2.2.1]: https://github.com/thephpleague/commonmark/compare/2.2.0...2.2.1 [2.2.0]: https://github.com/thephpleague/commonmark/compare/2.1.1...2.2.0 [2.1.3]: https://github.com/thephpleague/commonmark/compare/2.1.2...2.1.3 [2.1.2]: https://github.com/thephpleague/commonmark/compare/2.1.1...2.1.2 [2.1.1]: https://github.com/thephpleague/commonmark/compare/2.0.2...2.1.1 [2.1.0]: https://github.com/thephpleague/commonmark/compare/2.0.2...2.1.0 [2.0.4]: https://github.com/thephpleague/commonmark/compare/2.0.3...2.0.4 [2.0.3]: https://github.com/thephpleague/commonmark/compare/2.0.2...2.0.3 [2.0.2]: https://github.com/thephpleague/commonmark/compare/2.0.1...2.0.2 [2.0.1]: https://github.com/thephpleague/commonmark/compare/2.0.0...2.0.1 [2.0.0]: https://github.com/thephpleague/commonmark/compare/2.0.0-rc2...2.0.0 [2.0.0-rc2]: https://github.com/thephpleague/commonmark/compare/2.0.0-rc1...2.0.0-rc2 [2.0.0-rc1]: https://github.com/thephpleague/commonmark/compare/2.0.0-beta3...2.0.0-rc1 [2.0.0-beta3]: https://github.com/thephpleague/commonmark/compare/2.0.0-beta2...2.0.0-beta3 [2.0.0-beta2]: https://github.com/thephpleague/commonmark/compare/2.0.0-beta1...2.0.0-beta2 [2.0.0-beta1]: https://github.com/thephpleague/commonmark/compare/1.6...2.0.0-beta1 commonmark/.phpstorm.meta.php 0000644 00000013755 15007532371 0012315 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace PHPSTORM_META { expectedArguments(\League\CommonMark\Util\HtmlElement::__construct(), 0, 'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kdb', 'keygen', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'menu', 'menuitem', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'pre', 'progress', 'q', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr'); expectedArguments(\League\CommonMark\Extension\CommonMark\Node\Block\Heading::__construct(), 0, 1, 2, 3, 4, 5, 6); expectedReturnValues(\League\CommonMark\Extension\CommonMark\Node\Block\Heading::getLevel(), 1, 2, 3, 4, 5, 6); registerArgumentsSet('league_commonmark_htmlblock_types', \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_1_CODE_CONTAINER, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_2_COMMENT, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_3, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_4, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_5_CDATA, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_6_BLOCK_ELEMENT, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_7_MISC_ELEMENT); expectedArguments(\League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::__construct(), 0, argumentsSet('league_commonmark_htmlblock_types')); expectedArguments(\League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::setType(), 0, argumentsSet('league_commonmark_htmlblock_types')); expectedReturnValues(\League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::getType(), argumentsSet('league_commonmark_htmlblock_types')); expectedArguments(\League\CommonMark\Util\RegexHelper::getHtmlBlockOpenRegex(), 0, argumentsSet('league_commonmark_htmlblock_types')); expectedArguments(\League\CommonMark\Util\RegexHelper::getHtmlBlockCloseRegex(), 0, argumentsSet('league_commonmark_htmlblock_types')); registerArgumentsSet('league_commonmark_newline_types', \League\CommonMark\Node\Inline\Newline::HARDBREAK, \League\CommonMark\Node\Inline\Newline::SOFTBREAK); expectedArguments(\League\CommonMark\Node\Inline\Newline::__construct(), 0, argumentsSet('league_commonmark_newline_types')); expectedReturnValues(\League\CommonMark\Node\Inline\Newline::getType(), argumentsSet('league_commonmark_newline_types')); registerArgumentsSet('league_commonmark_options', 'html_input', 'allow_unsafe_links', 'max_nesting_level', 'renderer', 'renderer/block_separator', 'renderer/inner_separator', 'renderer/soft_break', 'commonmark', 'commonmark/enable_em', 'commonmark/enable_strong', 'commonmark/use_asterisk', 'commonmark/use_underscore', 'commonmark/unordered_list_markers', 'disallowed_raw_html', 'disallowed_raw_html/disallowed_tags', 'external_link', 'external_link/html_class', 'external_link/internal_hosts', 'external_link/nofollow', 'external_link/noopener', 'external_link/noreferrer', 'external_link/open_in_new_window', 'footnote', 'footnote/backref_class', 'footnote/backref_symbol', 'footnote/container_add_hr', 'footnote/container_class', 'footnote/ref_class', 'footnote/ref_id_prefix', 'footnote/footnote_class', 'footnote/footnote_id_prefix', 'heading_permalink', 'heading_permalink/apply_id_to_heading', 'heading_permalink/heading_class', 'heading_permalink/html_class', 'heading_permalink/fragment_prefix', 'heading_permalink/id_prefix', 'heading_permalink/inner_contents', 'heading_permalink/insert', 'heading_permalink/max_heading_level', 'heading_permalink/min_heading_level', 'heading_permalink/symbol', 'heading_permalink/title', 'mentions', 'smartpunct/double_quote_closer', 'smartpunct/double_quote_opener', 'smartpunct/single_quote_closer', 'smartpunct/single_quote_opener', 'slug_normalizer', 'slug_normalizer/instance', 'slug_normalizer/max_length', 'slug_normalizer/unique', 'table', 'table/wrap', 'table/wrap/attributes', 'table/wrap/enabled', 'table/wrap/tag', 'table/alignment_attributes', 'table/alignment_attributes/left', 'table/alignment_attributes/center', 'table/alignment_attributes/right', 'table_of_contents', 'table_of_contents/html_class', 'table_of_contents/max_heading_level', 'table_of_contents/min_heading_level', 'table_of_contents/normalize', 'table_of_contents/placeholder', 'table_of_contents/position', 'table_of_contents/style', ); expectedArguments(\League\Config\ConfigurationInterface::get(), 0, argumentsSet('league_commonmark_options')); expectedArguments(\League\Config\ConfigurationInterface::exists(), 0, argumentsSet('league_commonmark_options')); expectedArguments(\League\Config\MutableConfigurationInterface::set(), 0, argumentsSet('league_commonmark_options')); } commonmark/README.md 0000644 00000027675 15007532371 0010212 0 ustar 00 # league/commonmark [](https://packagist.org/packages/league/commonmark) [](https://packagist.org/packages/league/commonmark) [](LICENSE) [](https://github.com/thephpleague/commonmark/actions?query=workflow%3ATests+branch%3Amain) [](https://scrutinizer-ci.com/g/thephpleague/commonmark/code-structure) [](https://scrutinizer-ci.com/g/thephpleague/commonmark) [](https://shepherd.dev/github/thephpleague/commonmark) [](https://bestpractices.coreinfrastructure.org/projects/126) [](https://www.colinodell.com/sponsor)  **league/commonmark** is a highly-extensible PHP Markdown parser created by [Colin O'Dell][@colinodell] which supports the full [CommonMark] spec and [GitHub-Flavored Markdown]. It is based on the [CommonMark JS reference implementation][commonmark.js] by [John MacFarlane] \([@jgm]\). ## 📦 Installation & Basic Usage This project requires PHP 7.4 or higher with the `mbstring` extension. To install it via [Composer] simply run: ``` bash $ composer require league/commonmark ``` The `CommonMarkConverter` class provides a simple wrapper for converting CommonMark to HTML: ```php use League\CommonMark\CommonMarkConverter; $converter = new CommonMarkConverter([ 'html_input' => 'strip', 'allow_unsafe_links' => false, ]); echo $converter->convert('# Hello World!'); // <h1>Hello World!</h1> ``` Or if you want GitHub-Flavored Markdown, use the `GithubFlavoredMarkdownConverter` class instead: ```php use League\CommonMark\GithubFlavoredMarkdownConverter; $converter = new GithubFlavoredMarkdownConverter([ 'html_input' => 'strip', 'allow_unsafe_links' => false, ]); echo $converter->convert('# Hello World!'); // <h1>Hello World!</h1> ``` Please note that only UTF-8 and ASCII encodings are supported. If your Markdown uses a different encoding please convert it to UTF-8 before running it through this library. 🔒 If you will be parsing untrusted input from users, please consider setting the `html_input` and `allow_unsafe_links` options per the example above. See <https://commonmark.thephpleague.com/security/> for more details. If you also do choose to allow raw HTML input from untrusted users, consider using a library (like [HTML Purifier](https://github.com/ezyang/htmlpurifier)) to provide additional HTML filtering. ## 📓 Documentation Full documentation on advanced usage, configuration, and customization can be found at [commonmark.thephpleague.com][docs]. ## ⏫ Upgrading Information on how to upgrade to newer versions of this library can be found at <https://commonmark.thephpleague.com/releases>. ## 💻 GitHub-Flavored Markdown The `GithubFlavoredMarkdownConverter` shown earlier is a drop-in replacement for the `CommonMarkConverter` which adds additional features found in the GFM spec: - Autolinks - Disallowed raw HTML - Strikethrough - Tables - Task Lists See the [Extensions documentation](https://commonmark.thephpleague.com/customization/extensions/) for more details on how to include only certain GFM features if you don't want them all. ## 🗃️ Related Packages ### Integrations - [CakePHP 3](https://github.com/gourmet/common-mark) - [Drupal](https://www.drupal.org/project/markdown) - [Laravel 4+](https://github.com/GrahamCampbell/Laravel-Markdown) - [Sculpin](https://github.com/bcremer/sculpin-commonmark-bundle) - [Symfony 2 & 3](https://github.com/webuni/commonmark-bundle) - [Symfony 4](https://github.com/avensome/commonmark-bundle) - [Twig Markdown extension](https://github.com/twigphp/markdown-extension) - [Twig filter and tag](https://github.com/aptoma/twig-markdown) - [Laravel CommonMark Blog](https://github.com/spekulatius/laravel-commonmark-blog) ### Included Extensions See [our extension documentation](https://commonmark.thephpleague.com/extensions/overview) for a full list of extensions bundled with this library. ### Community Extensions Custom parsers/renderers can be bundled into extensions which extend CommonMark. Here are some that you may find interesting: - [Emoji extension](https://github.com/ElGigi/CommonMarkEmoji) - UTF-8 emoji extension with Github tag. - [Sup Sub extensions](https://github.com/OWS/commonmark-sup-sub-extensions) - Adds support of superscript and subscript (`<sup>` and `<sub>` HTML tags) - [YouTube iframe extension](https://github.com/zoonru/commonmark-ext-youtube-iframe) - Replaces youtube link with iframe. - [Lazy Image extension](https://github.com/simonvomeyser/commonmark-ext-lazy-image) - Adds various options for lazy loading of images. - [Marker Extension](https://github.com/noah1400/commonmark-marker-extension) - Adds support of highlighted text (`<mark>` HTML tag) Others can be found on [Packagist under the `commonmark-extension` package type](https://packagist.org/packages/league/commonmark?type=commonmark-extension). If you build your own, feel free to submit a PR to add it to this list! ### Others Check out the other cool things people are doing with `league/commonmark`: <https://packagist.org/packages/league/commonmark/dependents> ## 🏷️ Versioning [SemVer](http://semver.org/) is followed closely. Minor and patch releases should not introduce breaking changes to the codebase; however, they might change the resulting AST or HTML output of parsed Markdown (due to bug fixes, spec changes, etc.) As a result, you might get slightly different HTML, but any custom code built onto this library should still function correctly. Any classes or methods marked `@internal` are not intended for use outside of this library and are subject to breaking changes at any time, so please avoid using them. ## 🛠️ Maintenance & Support When a new **minor** version (e.g. `2.0` -> `2.1`) is released, the previous one (`2.0`) will continue to receive security and critical bug fixes for *at least* 3 months. When a new **major** version is released (e.g. `1.6` -> `2.0`), the previous one (`1.6`) will receive critical bug fixes for *at least* 3 months and security updates for 6 months after that new release comes out. (This policy may change in the future and exceptions may be made on a case-by-case basis.) **Professional support, including notification of new releases and security updates, is available through a [Tidelift Subscription](https://tidelift.com/subscription/pkg/packagist-league-commonmark?utm_source=packagist-league-commonmark&utm_medium=referral&utm_campaign=readme).** ## 👷♀️ Contributing To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure with us. If you encounter a bug in the spec, please report it to the [CommonMark] project. Any resulting fix will eventually be implemented in this project as well. Contributions to this library are **welcome**, especially ones that: * Improve usability or flexibility without compromising our ability to adhere to the [CommonMark spec] * Mirror fixes made to the [reference implementation][commonmark.js] * Optimize performance * Fix issues with adhering to the [CommonMark spec] Major refactoring to core parsing logic should be avoided if possible so that we can easily follow updates made to [the reference implementation][commonmark.js]. That being said, we will absolutely consider changes which don't deviate too far from the reference spec or which are favored by other popular CommonMark implementations. Please see [CONTRIBUTING](https://github.com/thephpleague/commonmark/blob/main/.github/CONTRIBUTING.md) for additional details. ## 🧪 Testing ``` bash $ composer test ``` This will also test league/commonmark against the latest supported spec. ## 🚀 Performance Benchmarks You can compare the performance of **league/commonmark** to other popular parsers by running the included benchmark tool: ``` bash $ ./tests/benchmark/benchmark.php ``` ## 👥 Credits & Acknowledgements - [Colin O'Dell][@colinodell] - [John MacFarlane][@jgm] - [All Contributors] This code is partially based on the [CommonMark JS reference implementation][commonmark.js] which is written, maintained and copyrighted by [John MacFarlane]. This project simply wouldn't exist without his work. ### Sponsors We'd also like to extend our sincere thanks the following sponsors who support ongoing development of this project: - [Tidelift](https://tidelift.com/subscription/pkg/packagist-league-commonmark?utm_source=packagist-league-commonmark&utm_medium=referral&utm_campaign=readme) for offering support to both the maintainers and end-users through their [professional support](https://tidelift.com/subscription/pkg/packagist-league-commonmark?utm_source=packagist-league-commonmark&utm_medium=referral&utm_campaign=readme) program - [Blackfire](https://www.blackfire.io/) for providing an Open-Source Profiler subscription - [JetBrains](https://www.jetbrains.com/) for supporting this project with complimentary [PhpStorm](https://www.jetbrains.com/phpstorm/) licenses - [Taylor Otwell](https://twitter.com/taylorotwell) for sponsoring this project through GitHub sponsors Are you interested in sponsoring development of this project? See <https://www.colinodell.com/sponsor> for a list of ways to contribute. ## 📄 License **league/commonmark** is licensed under the BSD-3 license. See the [`LICENSE`](LICENSE) file for more details. ## 🏛️ Governance This project is primarily maintained by [Colin O'Dell][@colinodell]. Members of the [PHP League] Leadership Team may occasionally assist with some of these duties. ## 🗺️ Who Uses It? This project is used by [Drupal](https://www.drupal.org/project/markdown), [Laravel Framework](https://laravel.com/), [Cachet](https://cachethq.io/), [Firefly III](https://firefly-iii.org/), [Neos](https://www.neos.io/), [Daux.io](https://daux.io/), and [more](https://packagist.org/packages/league/commonmark/dependents)! --- <div align="center"> <b> <a href="https://tidelift.com/subscription/pkg/packagist-league-commonmark?utm_source=packagist-league-commonmark&utm_medium=referral&utm_campaign=readme">Get professional support for league/commonmark with a Tidelift subscription</a> </b> <br> <sub> Tidelift helps make open source sustainable for maintainers while giving companies<br>assurances about security, maintenance, and licensing for their dependencies. </sub> </div> [CommonMark]: http://commonmark.org/ [CommonMark spec]: http://spec.commonmark.org/ [commonmark.js]: https://github.com/jgm/commonmark.js [GitHub-Flavored Markdown]: https://github.github.com/gfm/ [John MacFarlane]: http://johnmacfarlane.net [docs]: https://commonmark.thephpleague.com/ [docs-examples]: https://commonmark.thephpleague.com/customization/overview/#examples [docs-example-twitter]: https://commonmark.thephpleague.com/customization/inline-parsing#example-1---twitter-handles [docs-example-smilies]: https://commonmark.thephpleague.com/customization/inline-parsing#example-2---emoticons [All Contributors]: https://github.com/thephpleague/commonmark/contributors [@colinodell]: https://www.twitter.com/colinodell [@jgm]: https://github.com/jgm [jgm/stmd]: https://github.com/jgm/stmd [Composer]: https://getcomposer.org/ [PHP League]: https://thephpleague.com commonmark/src/Environment/EnvironmentInterface.php 0000644 00000003124 15007532371 0016643 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Environment; use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection; use League\CommonMark\Extension\ExtensionInterface; use League\CommonMark\Node\Node; use League\CommonMark\Normalizer\TextNormalizerInterface; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\Config\ConfigurationProviderInterface; use Psr\EventDispatcher\EventDispatcherInterface; interface EnvironmentInterface extends ConfigurationProviderInterface, EventDispatcherInterface { /** * Get all registered extensions * * @return ExtensionInterface[] */ public function getExtensions(): iterable; /** * @return iterable<BlockStartParserInterface> */ public function getBlockStartParsers(): iterable; /** * @return iterable<InlineParserInterface> */ public function getInlineParsers(): iterable; public function getDelimiterProcessors(): DelimiterProcessorCollection; /** * @psalm-param class-string<Node> $nodeClass * * @return iterable<NodeRendererInterface> */ public function getRenderersForClass(string $nodeClass): iterable; public function getSlugNormalizer(): TextNormalizerInterface; } commonmark/src/Environment/Environment.php 0000644 00000035331 15007532371 0015027 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Environment; use League\CommonMark\Delimiter\DelimiterParser; use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection; use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Event\ListenerData; use League\CommonMark\Exception\AlreadyInitializedException; use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; use League\CommonMark\Extension\ConfigurableExtensionInterface; use League\CommonMark\Extension\ExtensionInterface; use League\CommonMark\Extension\GithubFlavoredMarkdownExtension; use League\CommonMark\Normalizer\SlugNormalizer; use League\CommonMark\Normalizer\TextNormalizerInterface; use League\CommonMark\Normalizer\UniqueSlugNormalizer; use League\CommonMark\Normalizer\UniqueSlugNormalizerInterface; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Block\SkipLinesStartingWithLettersParser; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlFilter; use League\CommonMark\Util\PrioritizedList; use League\Config\Configuration; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; use Nette\Schema\Expect; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\ListenerProviderInterface; use Psr\EventDispatcher\StoppableEventInterface; final class Environment implements EnvironmentInterface, EnvironmentBuilderInterface, ListenerProviderInterface { /** * @var ExtensionInterface[] * * @psalm-readonly-allow-private-mutation */ private array $extensions = []; /** * @var ExtensionInterface[] * * @psalm-readonly-allow-private-mutation */ private array $uninitializedExtensions = []; /** @psalm-readonly-allow-private-mutation */ private bool $extensionsInitialized = false; /** * @var PrioritizedList<BlockStartParserInterface> * * @psalm-readonly */ private PrioritizedList $blockStartParsers; /** * @var PrioritizedList<InlineParserInterface> * * @psalm-readonly */ private PrioritizedList $inlineParsers; /** @psalm-readonly */ private DelimiterProcessorCollection $delimiterProcessors; /** * @var array<string, PrioritizedList<NodeRendererInterface>> * * @psalm-readonly-allow-private-mutation */ private array $renderersByClass = []; /** * @var PrioritizedList<ListenerData> * * @psalm-readonly-allow-private-mutation */ private PrioritizedList $listenerData; private ?EventDispatcherInterface $eventDispatcher = null; /** @psalm-readonly */ private Configuration $config; private ?TextNormalizerInterface $slugNormalizer = null; /** * @param array<string, mixed> $config */ public function __construct(array $config = []) { $this->config = self::createDefaultConfiguration(); $this->config->merge($config); $this->blockStartParsers = new PrioritizedList(); $this->inlineParsers = new PrioritizedList(); $this->listenerData = new PrioritizedList(); $this->delimiterProcessors = new DelimiterProcessorCollection(); // Performance optimization: always include a block "parser" that aborts parsing if a line starts with a letter // and is therefore unlikely to match any lines as a block start. $this->addBlockStartParser(new SkipLinesStartingWithLettersParser(), 249); } public function getConfiguration(): ConfigurationInterface { return $this->config->reader(); } /** * @deprecated Environment::mergeConfig() is deprecated since league/commonmark v2.0 and will be removed in v3.0. Configuration should be set when instantiating the environment instead. * * @param array<string, mixed> $config */ public function mergeConfig(array $config): void { @\trigger_error('Environment::mergeConfig() is deprecated since league/commonmark v2.0 and will be removed in v3.0. Configuration should be set when instantiating the environment instead.', \E_USER_DEPRECATED); $this->assertUninitialized('Failed to modify configuration.'); $this->config->merge($config); } public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface { $this->assertUninitialized('Failed to add block start parser.'); $this->blockStartParsers->add($parser, $priority); $this->injectEnvironmentAndConfigurationIfNeeded($parser); return $this; } public function addInlineParser(InlineParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface { $this->assertUninitialized('Failed to add inline parser.'); $this->inlineParsers->add($parser, $priority); $this->injectEnvironmentAndConfigurationIfNeeded($parser); return $this; } public function addDelimiterProcessor(DelimiterProcessorInterface $processor): EnvironmentBuilderInterface { $this->assertUninitialized('Failed to add delimiter processor.'); $this->delimiterProcessors->add($processor); $this->injectEnvironmentAndConfigurationIfNeeded($processor); return $this; } public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): EnvironmentBuilderInterface { $this->assertUninitialized('Failed to add renderer.'); if (! isset($this->renderersByClass[$nodeClass])) { $this->renderersByClass[$nodeClass] = new PrioritizedList(); } $this->renderersByClass[$nodeClass]->add($renderer, $priority); $this->injectEnvironmentAndConfigurationIfNeeded($renderer); return $this; } /** * {@inheritDoc} */ public function getBlockStartParsers(): iterable { if (! $this->extensionsInitialized) { $this->initializeExtensions(); } return $this->blockStartParsers->getIterator(); } public function getDelimiterProcessors(): DelimiterProcessorCollection { if (! $this->extensionsInitialized) { $this->initializeExtensions(); } return $this->delimiterProcessors; } /** * {@inheritDoc} */ public function getRenderersForClass(string $nodeClass): iterable { if (! $this->extensionsInitialized) { $this->initializeExtensions(); } // If renderers are defined for this specific class, return them immediately if (isset($this->renderersByClass[$nodeClass])) { return $this->renderersByClass[$nodeClass]; } /** @psalm-suppress TypeDoesNotContainType -- Bug: https://github.com/vimeo/psalm/issues/3332 */ while (\class_exists($parent ??= $nodeClass) && $parent = \get_parent_class($parent)) { if (! isset($this->renderersByClass[$parent])) { continue; } // "Cache" this result to avoid future loops return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent]; } return []; } /** * {@inheritDoc} */ public function getExtensions(): iterable { return $this->extensions; } /** * Add a single extension * * @return $this */ public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface { $this->assertUninitialized('Failed to add extension.'); $this->extensions[] = $extension; $this->uninitializedExtensions[] = $extension; if ($extension instanceof ConfigurableExtensionInterface) { $extension->configureSchema($this->config); } return $this; } private function initializeExtensions(): void { // Initialize the slug normalizer $this->getSlugNormalizer(); // Ask all extensions to register their components while (\count($this->uninitializedExtensions) > 0) { foreach ($this->uninitializedExtensions as $i => $extension) { $extension->register($this); unset($this->uninitializedExtensions[$i]); } } $this->extensionsInitialized = true; // Create the special delimiter parser if any processors were registered if ($this->delimiterProcessors->count() > 0) { $this->inlineParsers->add(new DelimiterParser($this->delimiterProcessors), PHP_INT_MIN); } } private function injectEnvironmentAndConfigurationIfNeeded(object $object): void { if ($object instanceof EnvironmentAwareInterface) { $object->setEnvironment($this); } if ($object instanceof ConfigurationAwareInterface) { $object->setConfiguration($this->config->reader()); } } /** * @deprecated Instantiate the environment and add the extension yourself * * @param array<string, mixed> $config */ public static function createCommonMarkEnvironment(array $config = []): Environment { $environment = new self($config); $environment->addExtension(new CommonMarkCoreExtension()); return $environment; } /** * @deprecated Instantiate the environment and add the extension yourself * * @param array<string, mixed> $config */ public static function createGFMEnvironment(array $config = []): Environment { $environment = new self($config); $environment->addExtension(new CommonMarkCoreExtension()); $environment->addExtension(new GithubFlavoredMarkdownExtension()); return $environment; } public function addEventListener(string $eventClass, callable $listener, int $priority = 0): EnvironmentBuilderInterface { $this->assertUninitialized('Failed to add event listener.'); $this->listenerData->add(new ListenerData($eventClass, $listener), $priority); if (\is_object($listener)) { $this->injectEnvironmentAndConfigurationIfNeeded($listener); } elseif (\is_array($listener) && \is_object($listener[0])) { $this->injectEnvironmentAndConfigurationIfNeeded($listener[0]); } return $this; } public function dispatch(object $event): object { if (! $this->extensionsInitialized) { $this->initializeExtensions(); } if ($this->eventDispatcher !== null) { return $this->eventDispatcher->dispatch($event); } foreach ($this->getListenersForEvent($event) as $listener) { if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { return $event; } $listener($event); } return $event; } public function setEventDispatcher(EventDispatcherInterface $dispatcher): void { $this->eventDispatcher = $dispatcher; } /** * {@inheritDoc} * * @return iterable<callable> */ public function getListenersForEvent(object $event): iterable { foreach ($this->listenerData as $listenerData) { \assert($listenerData instanceof ListenerData); /** @psalm-suppress ArgumentTypeCoercion */ if (! \is_a($event, $listenerData->getEvent())) { continue; } yield function (object $event) use ($listenerData) { if (! $this->extensionsInitialized) { $this->initializeExtensions(); } return \call_user_func($listenerData->getListener(), $event); }; } } /** * @return iterable<InlineParserInterface> */ public function getInlineParsers(): iterable { if (! $this->extensionsInitialized) { $this->initializeExtensions(); } return $this->inlineParsers->getIterator(); } public function getSlugNormalizer(): TextNormalizerInterface { if ($this->slugNormalizer === null) { $normalizer = $this->config->get('slug_normalizer/instance'); \assert($normalizer instanceof TextNormalizerInterface); $this->injectEnvironmentAndConfigurationIfNeeded($normalizer); if ($this->config->get('slug_normalizer/unique') !== UniqueSlugNormalizerInterface::DISABLED && ! $normalizer instanceof UniqueSlugNormalizer) { $normalizer = new UniqueSlugNormalizer($normalizer); } if ($normalizer instanceof UniqueSlugNormalizer) { if ($this->config->get('slug_normalizer/unique') === UniqueSlugNormalizerInterface::PER_DOCUMENT) { $this->addEventListener(DocumentParsedEvent::class, [$normalizer, 'clearHistory'], -1000); } } $this->slugNormalizer = $normalizer; } return $this->slugNormalizer; } /** * @throws AlreadyInitializedException */ private function assertUninitialized(string $message): void { if ($this->extensionsInitialized) { throw new AlreadyInitializedException($message . ' Extensions have already been initialized.'); } } public static function createDefaultConfiguration(): Configuration { return new Configuration([ 'html_input' => Expect::anyOf(HtmlFilter::STRIP, HtmlFilter::ALLOW, HtmlFilter::ESCAPE)->default(HtmlFilter::ALLOW), 'allow_unsafe_links' => Expect::bool(true), 'max_nesting_level' => Expect::type('int')->default(PHP_INT_MAX), 'renderer' => Expect::structure([ 'block_separator' => Expect::string("\n"), 'inner_separator' => Expect::string("\n"), 'soft_break' => Expect::string("\n"), ]), 'slug_normalizer' => Expect::structure([ 'instance' => Expect::type(TextNormalizerInterface::class)->default(new SlugNormalizer()), 'max_length' => Expect::int()->min(0)->default(255), 'unique' => Expect::anyOf(UniqueSlugNormalizerInterface::DISABLED, UniqueSlugNormalizerInterface::PER_ENVIRONMENT, UniqueSlugNormalizerInterface::PER_DOCUMENT)->default(UniqueSlugNormalizerInterface::PER_DOCUMENT), ]), ]); } } commonmark/src/Environment/EnvironmentAwareInterface.php 0000644 00000000657 15007532371 0017633 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Environment; interface EnvironmentAwareInterface { public function setEnvironment(EnvironmentInterface $environment): void; } commonmark/src/Environment/EnvironmentBuilderInterface.php 0000644 00000007542 15007532371 0020162 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Environment; use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface; use League\CommonMark\Exception\AlreadyInitializedException; use League\CommonMark\Extension\ExtensionInterface; use League\CommonMark\Node\Node; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\Config\ConfigurationProviderInterface; /** * Interface for building the Environment with any extensions, parsers, listeners, etc. that it may need */ interface EnvironmentBuilderInterface extends ConfigurationProviderInterface { /** * Registers the given extension with the Environment * * @throws AlreadyInitializedException if the Environment has already been initialized */ public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface; /** * Registers the given block start parser with the Environment * * @param BlockStartParserInterface $parser Block parser instance * @param int $priority Priority (a higher number will be executed earlier) * * @return $this * * @throws AlreadyInitializedException if the Environment has already been initialized */ public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface; /** * Registers the given inline parser with the Environment * * @param InlineParserInterface $parser Inline parser instance * @param int $priority Priority (a higher number will be executed earlier) * * @return $this * * @throws AlreadyInitializedException if the Environment has already been initialized */ public function addInlineParser(InlineParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface; /** * Registers the given delimiter processor with the Environment * * @param DelimiterProcessorInterface $processor Delimiter processors instance * * @throws AlreadyInitializedException if the Environment has already been initialized */ public function addDelimiterProcessor(DelimiterProcessorInterface $processor): EnvironmentBuilderInterface; /** * Registers the given node renderer with the Environment * * @param string $nodeClass The fully-qualified node element class name the renderer below should handle * @param NodeRendererInterface $renderer The renderer responsible for rendering the type of element given above * @param int $priority Priority (a higher number will be executed earlier) * * @psalm-param class-string<Node> $nodeClass * * @return $this * * @throws AlreadyInitializedException if the Environment has already been initialized */ public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): EnvironmentBuilderInterface; /** * Registers the given event listener * * @param class-string $eventClass Fully-qualified class name of the event this listener should respond to * @param callable $listener Listener to be executed * @param int $priority Priority (a higher number will be executed earlier) * * @return $this * * @throws AlreadyInitializedException if the Environment has already been initialized */ public function addEventListener(string $eventClass, callable $listener, int $priority = 0): EnvironmentBuilderInterface; } commonmark/src/Exception/AlreadyInitializedException.php 0000644 00000000624 15007532371 0017600 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Exception; class AlreadyInitializedException extends LogicException implements CommonMarkException { } commonmark/src/Exception/IOException.php 0000644 00000000607 15007532371 0014341 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Exception; class IOException extends \RuntimeException implements CommonMarkException { } commonmark/src/Exception/MissingDependencyException.php 0000644 00000000626 15007532371 0017443 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Exception; class MissingDependencyException extends \RuntimeException implements CommonMarkException { } commonmark/src/Exception/LogicException.php 0000644 00000000610 15007532371 0015061 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Exception; class LogicException extends \LogicException implements CommonMarkException { } commonmark/src/Exception/CommonMarkException.php 0000644 00000000664 15007532371 0016100 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Exception; /** * Marker interface for all exceptions thrown by this library. */ interface CommonMarkException extends \Throwable { } commonmark/src/Exception/InvalidArgumentException.php 0000644 00000000634 15007532371 0017123 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Exception; class InvalidArgumentException extends \InvalidArgumentException implements CommonMarkException { } commonmark/src/Exception/UnexpectedEncodingException.php 0000644 00000000635 15007532371 0017606 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Exception; final class UnexpectedEncodingException extends \RuntimeException implements CommonMarkException { } commonmark/src/Delimiter/Delimiter.php 0000644 00000005740 15007532371 0014054 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Delimiter; use League\CommonMark\Node\Inline\AbstractStringContainer; final class Delimiter implements DelimiterInterface { /** @psalm-readonly */ private string $char; /** @psalm-readonly-allow-private-mutation */ private int $length; /** @psalm-readonly */ private int $originalLength; /** @psalm-readonly */ private AbstractStringContainer $inlineNode; /** @psalm-readonly-allow-private-mutation */ private ?DelimiterInterface $previous = null; /** @psalm-readonly-allow-private-mutation */ private ?DelimiterInterface $next = null; /** @psalm-readonly */ private bool $canOpen; /** @psalm-readonly */ private bool $canClose; /** @psalm-readonly-allow-private-mutation */ private bool $active; /** @psalm-readonly */ private ?int $index = null; public function __construct(string $char, int $numDelims, AbstractStringContainer $node, bool $canOpen, bool $canClose, ?int $index = null) { $this->char = $char; $this->length = $numDelims; $this->originalLength = $numDelims; $this->inlineNode = $node; $this->canOpen = $canOpen; $this->canClose = $canClose; $this->active = true; $this->index = $index; } public function canClose(): bool { return $this->canClose; } public function canOpen(): bool { return $this->canOpen; } public function isActive(): bool { return $this->active; } public function setActive(bool $active): void { $this->active = $active; } public function getChar(): string { return $this->char; } public function getIndex(): ?int { return $this->index; } public function getNext(): ?DelimiterInterface { return $this->next; } public function setNext(?DelimiterInterface $next): void { $this->next = $next; } public function getLength(): int { return $this->length; } public function setLength(int $length): void { $this->length = $length; } public function getOriginalLength(): int { return $this->originalLength; } public function getInlineNode(): AbstractStringContainer { return $this->inlineNode; } public function getPrevious(): ?DelimiterInterface { return $this->previous; } public function setPrevious(?DelimiterInterface $previous): void { $this->previous = $previous; } } commonmark/src/Delimiter/Processor/StaggeredDelimiterProcessor.php 0000644 00000007023 15007532371 0021555 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java) * - (c) Atlassian Pty Ltd * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Delimiter\Processor; use League\CommonMark\Delimiter\DelimiterInterface; use League\CommonMark\Exception\InvalidArgumentException; use League\CommonMark\Node\Inline\AbstractStringContainer; /** * An implementation of DelimiterProcessorInterface that dispatches all calls to two or more other DelimiterProcessors * depending on the length of the delimiter run. All child DelimiterProcessors must have different minimum * lengths. A given delimiter run is dispatched to the child with the largest acceptable minimum length. If no * child is applicable, the one with the largest minimum length is chosen. * * @internal */ final class StaggeredDelimiterProcessor implements DelimiterProcessorInterface { /** @psalm-readonly */ private string $delimiterChar; /** @psalm-readonly-allow-private-mutation */ private int $minLength = 0; /** * @var array<int, DelimiterProcessorInterface>|DelimiterProcessorInterface[] * * @psalm-readonly-allow-private-mutation */ private array $processors = []; // keyed by minLength in reverse order public function __construct(string $char, DelimiterProcessorInterface $processor) { $this->delimiterChar = $char; $this->add($processor); } public function getOpeningCharacter(): string { return $this->delimiterChar; } public function getClosingCharacter(): string { return $this->delimiterChar; } public function getMinLength(): int { return $this->minLength; } /** * Adds the given processor to this staggered delimiter processor * * @throws InvalidArgumentException if attempting to add another processors for the same character and minimum length */ public function add(DelimiterProcessorInterface $processor): void { $len = $processor->getMinLength(); if (isset($this->processors[$len])) { throw new InvalidArgumentException(\sprintf('Cannot add two delimiter processors for char "%s" and minimum length %d', $this->delimiterChar, $len)); } $this->processors[$len] = $processor; \krsort($this->processors); $this->minLength = \min($this->minLength, $len); } public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int { return $this->findProcessor($opener->getLength())->getDelimiterUse($opener, $closer); } public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void { $this->findProcessor($delimiterUse)->process($opener, $closer, $delimiterUse); } private function findProcessor(int $len): DelimiterProcessorInterface { // Find the "longest" processor which can handle this length foreach ($this->processors as $processor) { if ($processor->getMinLength() <= $len) { return $processor; } } // Just use the first one in our list $first = \reset($this->processors); \assert($first instanceof DelimiterProcessorInterface); return $first; } } commonmark/src/Delimiter/Processor/DelimiterProcessorCollection.php 0000644 00000005551 15007532371 0021747 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java) * - (c) Atlassian Pty Ltd * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Delimiter\Processor; use League\CommonMark\Exception\InvalidArgumentException; final class DelimiterProcessorCollection implements DelimiterProcessorCollectionInterface { /** * @var array<string,DelimiterProcessorInterface>|DelimiterProcessorInterface[] * * @psalm-readonly-allow-private-mutation */ private array $processorsByChar = []; public function add(DelimiterProcessorInterface $processor): void { $opening = $processor->getOpeningCharacter(); $closing = $processor->getClosingCharacter(); if ($opening === $closing) { $old = $this->processorsByChar[$opening] ?? null; if ($old !== null && $old->getOpeningCharacter() === $old->getClosingCharacter()) { $this->addStaggeredDelimiterProcessorForChar($opening, $old, $processor); } else { $this->addDelimiterProcessorForChar($opening, $processor); } } else { $this->addDelimiterProcessorForChar($opening, $processor); $this->addDelimiterProcessorForChar($closing, $processor); } } public function getDelimiterProcessor(string $char): ?DelimiterProcessorInterface { return $this->processorsByChar[$char] ?? null; } /** * @return string[] */ public function getDelimiterCharacters(): array { return \array_keys($this->processorsByChar); } private function addDelimiterProcessorForChar(string $delimiterChar, DelimiterProcessorInterface $processor): void { if (isset($this->processorsByChar[$delimiterChar])) { throw new InvalidArgumentException(\sprintf('Delim processor for character "%s" already exists', $processor->getOpeningCharacter())); } $this->processorsByChar[$delimiterChar] = $processor; } private function addStaggeredDelimiterProcessorForChar(string $opening, DelimiterProcessorInterface $old, DelimiterProcessorInterface $new): void { if ($old instanceof StaggeredDelimiterProcessor) { $s = $old; } else { $s = new StaggeredDelimiterProcessor($opening, $old); } $s->add($new); $this->processorsByChar[$opening] = $s; } public function count(): int { return \count($this->processorsByChar); } } commonmark/src/Delimiter/Processor/DelimiterProcessorInterface.php 0000644 00000005417 15007532371 0021555 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java) * - (c) Atlassian Pty Ltd * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Delimiter\Processor; use League\CommonMark\Delimiter\DelimiterInterface; use League\CommonMark\Node\Inline\AbstractStringContainer; /** * Interface for a delimiter processor */ interface DelimiterProcessorInterface { /** * Returns the character that marks the beginning of a delimited node. * * This must not clash with any other processors being added to the environment. */ public function getOpeningCharacter(): string; /** * Returns the character that marks the ending of a delimited node. * * This must not clash with any other processors being added to the environment. * * Note that for a symmetric delimiter such as "*", this is the same as the opening. */ public function getClosingCharacter(): string; /** * Minimum number of delimiter characters that are needed to active this. * * Must be at least 1. */ public function getMinLength(): int; /** * Determine how many (if any) of the delimiter characters should be used. * * This allows implementations to decide how many characters to be used * based on the properties of the delimiter runs. An implementation can also * return 0 when it doesn't want to allow this particular combination of * delimiter runs. * * @param DelimiterInterface $opener The opening delimiter run * @param DelimiterInterface $closer The closing delimiter run */ public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int; /** * Process the matched delimiters, e.g. by wrapping the nodes between opener * and closer in a new node, or appending a new node after the opener. * * Note that removal of the delimiter from the delimiter nodes and detaching * them is done by the caller. * * @param AbstractStringContainer $opener The node that contained the opening delimiter * @param AbstractStringContainer $closer The node that contained the closing delimiter * @param int $delimiterUse The number of delimiters that were used */ public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void; } commonmark/src/Delimiter/Processor/DelimiterProcessorCollectionInterface.php 0000644 00000002652 15007532371 0023567 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java) * - (c) Atlassian Pty Ltd * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Delimiter\Processor; use League\CommonMark\Exception\InvalidArgumentException; interface DelimiterProcessorCollectionInterface extends \Countable { /** * Add the given delim processor to the collection * * @param DelimiterProcessorInterface $processor The delim processor to add * * @throws InvalidArgumentException Exception will be thrown if attempting to add multiple processors for the same character */ public function add(DelimiterProcessorInterface $processor): void; /** * Returns the delim processor which handles the given character if one exists */ public function getDelimiterProcessor(string $char): ?DelimiterProcessorInterface; /** * Returns an array of delimiter characters who have associated processors * * @return string[] */ public function getDelimiterCharacters(): array; } commonmark/src/Delimiter/DelimiterInterface.php 0000644 00000002303 15007532371 0015665 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Delimiter; use League\CommonMark\Node\Inline\AbstractStringContainer; interface DelimiterInterface { public function canClose(): bool; public function canOpen(): bool; public function isActive(): bool; public function setActive(bool $active): void; public function getChar(): string; public function getIndex(): ?int; public function getNext(): ?DelimiterInterface; public function setNext(?DelimiterInterface $next): void; public function getLength(): int; public function setLength(int $length): void; public function getOriginalLength(): int; public function getInlineNode(): AbstractStringContainer; public function getPrevious(): ?DelimiterInterface; public function setPrevious(?DelimiterInterface $previous): void; } commonmark/src/Delimiter/DelimiterParser.php 0000644 00000007160 15007532371 0015227 0 ustar 00 <?php declare(strict_types=1); namespace League\CommonMark\Delimiter; use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection; use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; use League\CommonMark\Util\RegexHelper; /** * Delimiter parsing is implemented as an Inline Parser with the lowest-possible priority * * @internal */ final class DelimiterParser implements InlineParserInterface { private DelimiterProcessorCollection $collection; public function __construct(DelimiterProcessorCollection $collection) { $this->collection = $collection; } public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::oneOf(...$this->collection->getDelimiterCharacters()); } public function parse(InlineParserContext $inlineContext): bool { $character = $inlineContext->getFullMatch(); $numDelims = 0; $cursor = $inlineContext->getCursor(); $processor = $this->collection->getDelimiterProcessor($character); \assert($processor !== null); // Delimiter processor should never be null here $charBefore = $cursor->peek(-1); if ($charBefore === null) { $charBefore = "\n"; } while ($cursor->peek($numDelims) === $character) { ++$numDelims; } if ($numDelims < $processor->getMinLength()) { return false; } $cursor->advanceBy($numDelims); $charAfter = $cursor->getCurrentCharacter(); if ($charAfter === null) { $charAfter = "\n"; } [$canOpen, $canClose] = self::determineCanOpenOrClose($charBefore, $charAfter, $character, $processor); $node = new Text(\str_repeat($character, $numDelims), [ 'delim' => true, ]); $inlineContext->getContainer()->appendChild($node); // Add entry to stack to this opener if ($canOpen || $canClose) { $delimiter = new Delimiter($character, $numDelims, $node, $canOpen, $canClose); $inlineContext->getDelimiterStack()->push($delimiter); } return true; } /** * @return bool[] */ private static function determineCanOpenOrClose(string $charBefore, string $charAfter, string $character, DelimiterProcessorInterface $delimiterProcessor): array { $afterIsWhitespace = \preg_match(RegexHelper::REGEX_UNICODE_WHITESPACE_CHAR, $charAfter); $afterIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charAfter); $beforeIsWhitespace = \preg_match(RegexHelper::REGEX_UNICODE_WHITESPACE_CHAR, $charBefore); $beforeIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charBefore); $leftFlanking = ! $afterIsWhitespace && (! $afterIsPunctuation || $beforeIsWhitespace || $beforeIsPunctuation); $rightFlanking = ! $beforeIsWhitespace && (! $beforeIsPunctuation || $afterIsWhitespace || $afterIsPunctuation); if ($character === '_') { $canOpen = $leftFlanking && (! $rightFlanking || $beforeIsPunctuation); $canClose = $rightFlanking && (! $leftFlanking || $afterIsPunctuation); } else { $canOpen = $leftFlanking && $character === $delimiterProcessor->getOpeningCharacter(); $canClose = $rightFlanking && $character === $delimiterProcessor->getClosingCharacter(); } return [$canOpen, $canClose]; } } commonmark/src/Delimiter/DelimiterStack.php 0000644 00000016771 15007532371 0015050 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java) * - (c) Atlassian Pty Ltd * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Delimiter; use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection; use League\CommonMark\Node\Inline\AdjacentTextMerger; final class DelimiterStack { /** @psalm-readonly-allow-private-mutation */ private ?DelimiterInterface $top = null; public function push(DelimiterInterface $newDelimiter): void { $newDelimiter->setPrevious($this->top); if ($this->top !== null) { $this->top->setNext($newDelimiter); } $this->top = $newDelimiter; } private function findEarliest(?DelimiterInterface $stackBottom = null): ?DelimiterInterface { $delimiter = $this->top; while ($delimiter !== null && $delimiter->getPrevious() !== $stackBottom) { $delimiter = $delimiter->getPrevious(); } return $delimiter; } public function removeDelimiter(DelimiterInterface $delimiter): void { if ($delimiter->getPrevious() !== null) { /** @psalm-suppress PossiblyNullReference */ $delimiter->getPrevious()->setNext($delimiter->getNext()); } if ($delimiter->getNext() === null) { // top of stack $this->top = $delimiter->getPrevious(); } else { /** @psalm-suppress PossiblyNullReference */ $delimiter->getNext()->setPrevious($delimiter->getPrevious()); } } private function removeDelimiterAndNode(DelimiterInterface $delimiter): void { $delimiter->getInlineNode()->detach(); $this->removeDelimiter($delimiter); } private function removeDelimitersBetween(DelimiterInterface $opener, DelimiterInterface $closer): void { $delimiter = $closer->getPrevious(); while ($delimiter !== null && $delimiter !== $opener) { $previous = $delimiter->getPrevious(); $this->removeDelimiter($delimiter); $delimiter = $previous; } } public function removeAll(?DelimiterInterface $stackBottom = null): void { while ($this->top && $this->top !== $stackBottom) { $this->removeDelimiter($this->top); } } public function removeEarlierMatches(string $character): void { $opener = $this->top; while ($opener !== null) { if ($opener->getChar() === $character) { $opener->setActive(false); } $opener = $opener->getPrevious(); } } /** * @param string|string[] $characters */ public function searchByCharacter($characters): ?DelimiterInterface { if (! \is_array($characters)) { $characters = [$characters]; } $opener = $this->top; while ($opener !== null) { if (\in_array($opener->getChar(), $characters, true)) { break; } $opener = $opener->getPrevious(); } return $opener; } public function processDelimiters(?DelimiterInterface $stackBottom, DelimiterProcessorCollection $processors): void { $openersBottom = []; // Find first closer above stackBottom $closer = $this->findEarliest($stackBottom); // Move forward, looking for closers, and handling each while ($closer !== null) { $delimiterChar = $closer->getChar(); $delimiterProcessor = $processors->getDelimiterProcessor($delimiterChar); if (! $closer->canClose() || $delimiterProcessor === null) { $closer = $closer->getNext(); continue; } $openingDelimiterChar = $delimiterProcessor->getOpeningCharacter(); $useDelims = 0; $openerFound = false; $potentialOpenerFound = false; $opener = $closer->getPrevious(); while ($opener !== null && $opener !== $stackBottom && $opener !== ($openersBottom[$delimiterChar] ?? null)) { if ($opener->canOpen() && $opener->getChar() === $openingDelimiterChar) { $potentialOpenerFound = true; $useDelims = $delimiterProcessor->getDelimiterUse($opener, $closer); if ($useDelims > 0) { $openerFound = true; break; } } $opener = $opener->getPrevious(); } if (! $openerFound) { if (! $potentialOpenerFound) { // Only do this when we didn't even have a potential // opener (one that matches the character and can open). // If an opener was rejected because of the number of // delimiters (e.g. because of the "multiple of 3" // Set lower bound for future searches for openersrule), // we want to consider it next time because the number // of delimiters can change as we continue processing. $openersBottom[$delimiterChar] = $closer->getPrevious(); if (! $closer->canOpen()) { // We can remove a closer that can't be an opener, // once we've seen there's no matching opener. $this->removeDelimiter($closer); } } $closer = $closer->getNext(); continue; } \assert($opener !== null); $openerNode = $opener->getInlineNode(); $closerNode = $closer->getInlineNode(); // Remove number of used delimiters from stack and inline nodes. $opener->setLength($opener->getLength() - $useDelims); $closer->setLength($closer->getLength() - $useDelims); $openerNode->setLiteral(\substr($openerNode->getLiteral(), 0, -$useDelims)); $closerNode->setLiteral(\substr($closerNode->getLiteral(), 0, -$useDelims)); $this->removeDelimitersBetween($opener, $closer); // The delimiter processor can re-parent the nodes between opener and closer, // so make sure they're contiguous already. Exclusive because we want to keep opener/closer themselves. AdjacentTextMerger::mergeTextNodesBetweenExclusive($openerNode, $closerNode); $delimiterProcessor->process($openerNode, $closerNode, $useDelims); // No delimiter characters left to process, so we can remove delimiter and the now empty node. if ($opener->getLength() === 0) { $this->removeDelimiterAndNode($opener); } // phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed if ($closer->getLength() === 0) { $next = $closer->getNext(); $this->removeDelimiterAndNode($closer); $closer = $next; } } // Remove all delimiters $this->removeAll($stackBottom); } } commonmark/src/Util/HtmlElement.php 0000644 00000010054 15007532371 0013345 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; final class HtmlElement implements \Stringable { /** @psalm-readonly */ private string $tagName; /** @var array<string, string|bool> */ private array $attributes = []; /** @var \Stringable|\Stringable[]|string */ private $contents; /** @psalm-readonly */ private bool $selfClosing; /** * @param string $tagName Name of the HTML tag * @param array<string, string|string[]|bool> $attributes Array of attributes (values should be unescaped) * @param \Stringable|\Stringable[]|string|null $contents Inner contents, pre-escaped if needed * @param bool $selfClosing Whether the tag is self-closing */ public function __construct(string $tagName, array $attributes = [], $contents = '', bool $selfClosing = false) { $this->tagName = $tagName; $this->selfClosing = $selfClosing; foreach ($attributes as $name => $value) { $this->setAttribute($name, $value); } $this->setContents($contents ?? ''); } /** @psalm-immutable */ public function getTagName(): string { return $this->tagName; } /** * @return array<string, string|bool> * * @psalm-immutable */ public function getAllAttributes(): array { return $this->attributes; } /** * @return string|bool|null * * @psalm-immutable */ public function getAttribute(string $key) { return $this->attributes[$key] ?? null; } /** * @param string|string[]|bool $value */ public function setAttribute(string $key, $value = true): self { if (\is_array($value)) { $this->attributes[$key] = \implode(' ', \array_unique($value)); } else { $this->attributes[$key] = $value; } return $this; } /** * @return \Stringable|\Stringable[]|string * * @psalm-immutable */ public function getContents(bool $asString = true) { if (! $asString) { return $this->contents; } return $this->getContentsAsString(); } /** * Sets the inner contents of the tag (must be pre-escaped if needed) * * @param \Stringable|\Stringable[]|string $contents * * @return $this */ public function setContents($contents): self { $this->contents = $contents ?? ''; // @phpstan-ignore-line return $this; } /** @psalm-immutable */ public function __toString(): string { $result = '<' . $this->tagName; foreach ($this->attributes as $key => $value) { if ($value === true) { $result .= ' ' . $key; } elseif ($value === false) { continue; } else { $result .= ' ' . $key . '="' . Xml::escape($value) . '"'; } } if ($this->contents !== '') { $result .= '>' . $this->getContentsAsString() . '</' . $this->tagName . '>'; } elseif ($this->selfClosing && $this->tagName === 'input') { $result .= '>'; } elseif ($this->selfClosing) { $result .= ' />'; } else { $result .= '></' . $this->tagName . '>'; } return $result; } /** @psalm-immutable */ private function getContentsAsString(): string { if (\is_string($this->contents)) { return $this->contents; } if (\is_array($this->contents)) { return \implode('', $this->contents); } return (string) $this->contents; } } commonmark/src/Util/HtmlFilter.php 0000644 00000002674 15007532371 0013212 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; use League\CommonMark\Exception\InvalidArgumentException; /** * @psalm-immutable */ final class HtmlFilter { // Return the entire string as-is public const ALLOW = 'allow'; // Escape the entire string so any HTML/JS won't be interpreted as such public const ESCAPE = 'escape'; // Return an empty string public const STRIP = 'strip'; /** * Runs the given HTML through the given filter * * @param string $html HTML input to be filtered * @param string $filter One of the HtmlFilter constants * * @return string Filtered HTML * * @throws InvalidArgumentException when an invalid $filter is given * * @psalm-pure */ public static function filter(string $html, string $filter): string { switch ($filter) { case self::STRIP: return ''; case self::ESCAPE: return \htmlspecialchars($html, \ENT_NOQUOTES); case self::ALLOW: return $html; default: throw new InvalidArgumentException(\sprintf('Invalid filter provided: "%s"', $filter)); } } } commonmark/src/Util/SpecReader.php 0000644 00000004200 15007532371 0013140 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; use League\CommonMark\Exception\IOException; /** * Reads in a CommonMark spec document and extracts the input/output examples for testing against them */ final class SpecReader { private function __construct() { } /** * @return iterable<string, array{input: string, output: string, type: string, section: string, number: int}> */ public static function read(string $data): iterable { // Normalize newlines for platform independence $data = \preg_replace('/\r\n?/', "\n", $data); \assert($data !== null); $data = \preg_replace('/<!-- END TESTS -->.*$/', '', $data); \assert($data !== null); \preg_match_all('/^`{32} (example ?\w*)\n([\s\S]*?)^\.\n([\s\S]*?)^`{32}$|^#{1,6} *(.*)$/m', $data, $matches, PREG_SET_ORDER); $currentSection = 'Example'; $exampleNumber = 0; foreach ($matches as $match) { if (isset($match[4])) { $currentSection = $match[4]; continue; } yield \trim($currentSection . ' #' . $exampleNumber) => [ 'input' => \str_replace('→', "\t", $match[2]), 'output' => \str_replace('→', "\t", $match[3]), 'type' => $match[1], 'section' => $currentSection, 'number' => $exampleNumber++, ]; } } /** * @return iterable<string, array{input: string, output: string, type: string, section: string, number: int}> * * @throws IOException if the file cannot be loaded */ public static function readFile(string $filename): iterable { if (($data = \file_get_contents($filename)) === false) { throw new IOException(\sprintf('Failed to load spec from %s', $filename)); } return self::read($data); } } commonmark/src/Util/ArrayCollection.php 0000644 00000006624 15007532371 0014231 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; /** * Array collection * * Provides a wrapper around a standard PHP array. * * @internal * * @phpstan-template T * @phpstan-implements \IteratorAggregate<int, T> * @phpstan-implements \ArrayAccess<int, T> */ final class ArrayCollection implements \IteratorAggregate, \Countable, \ArrayAccess { /** * @var array<int, mixed> * @phpstan-var array<int, T> */ private array $elements; /** * Constructor * * @param array<int|string, mixed> $elements * * @phpstan-param array<int, T> $elements */ public function __construct(array $elements = []) { $this->elements = $elements; } /** * @return mixed|false * * @phpstan-return T|false */ public function first() { return \reset($this->elements); } /** * @return mixed|false * * @phpstan-return T|false */ public function last() { return \end($this->elements); } /** * Retrieve an external iterator * * @return \ArrayIterator<int, mixed> * * @phpstan-return \ArrayIterator<int, T> */ #[\ReturnTypeWillChange] public function getIterator(): \ArrayIterator { return new \ArrayIterator($this->elements); } /** * Count elements of an object * * @return int The count as an integer. */ public function count(): int { return \count($this->elements); } /** * Whether an offset exists * * {@inheritDoc} * * @phpstan-param int $offset */ public function offsetExists($offset): bool { return \array_key_exists($offset, $this->elements); } /** * Offset to retrieve * * {@inheritDoc} * * @phpstan-param int $offset * * @phpstan-return T|null */ #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->elements[$offset] ?? null; } /** * Offset to set * * {@inheritDoc} * * @phpstan-param int|null $offset * @phpstan-param T $value */ #[\ReturnTypeWillChange] public function offsetSet($offset, $value): void { if ($offset === null) { $this->elements[] = $value; } else { $this->elements[$offset] = $value; } } /** * Offset to unset * * {@inheritDoc} * * @phpstan-param int $offset */ #[\ReturnTypeWillChange] public function offsetUnset($offset): void { if (! \array_key_exists($offset, $this->elements)) { return; } unset($this->elements[$offset]); } /** * Returns a subset of the array * * @return array<int, mixed> * * @phpstan-return array<int, T> */ public function slice(int $offset, ?int $length = null): array { return \array_slice($this->elements, $offset, $length, true); } /** * @return array<int, mixed> * * @phpstan-return array<int, T> */ public function toArray(): array { return $this->elements; } } commonmark/src/Util/Xml.php 0000644 00000001342 15007532371 0011667 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; /** * Utility class for handling/generating XML and HTML * * @psalm-immutable */ final class Xml { /** * @psalm-pure */ public static function escape(string $string): string { return \str_replace(['&', '<', '>', '"'], ['&', '<', '>', '"'], $string); } } commonmark/src/Util/LinkParserHelper.php 0000644 00000007652 15007532371 0014353 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; use League\CommonMark\Parser\Cursor; /** * @psalm-immutable */ final class LinkParserHelper { /** * Attempt to parse link destination * * @return string|null The string, or null if no match */ public static function parseLinkDestination(Cursor $cursor): ?string { if ($res = $cursor->match(RegexHelper::REGEX_LINK_DESTINATION_BRACES)) { // Chop off surrounding <..>: return UrlEncoder::unescapeAndEncode( RegexHelper::unescape(\substr($res, 1, -1)) ); } if ($cursor->getCurrentCharacter() === '<') { return null; } $destination = self::manuallyParseLinkDestination($cursor); if ($destination === null) { return null; } return UrlEncoder::unescapeAndEncode( RegexHelper::unescape($destination) ); } public static function parseLinkLabel(Cursor $cursor): int { $match = $cursor->match('/^\[(?:[^\\\\\[\]]|\\\\.){0,1000}\]/'); if ($match === null) { return 0; } $length = \mb_strlen($match, 'UTF-8'); if ($length > 1001) { return 0; } return $length; } public static function parsePartialLinkLabel(Cursor $cursor): ?string { return $cursor->match('/^(?:[^\\\\\[\]]+|\\\\.?)*/'); } /** * Attempt to parse link title (sans quotes) * * @return string|null The string, or null if no match */ public static function parseLinkTitle(Cursor $cursor): ?string { if ($title = $cursor->match('/' . RegexHelper::PARTIAL_LINK_TITLE . '/')) { // Chop off quotes from title and unescape return RegexHelper::unescape(\substr($title, 1, -1)); } return null; } public static function parsePartialLinkTitle(Cursor $cursor, string $endDelimiter): ?string { $endDelimiter = \preg_quote($endDelimiter, '/'); $regex = \sprintf('/(%s|[^%s\x00])*(?:%s)?/', RegexHelper::PARTIAL_ESCAPED_CHAR, $endDelimiter, $endDelimiter); if (($partialTitle = $cursor->match($regex)) === null) { return null; } return RegexHelper::unescape($partialTitle); } private static function manuallyParseLinkDestination(Cursor $cursor): ?string { $oldPosition = $cursor->getPosition(); $oldState = $cursor->saveState(); $openParens = 0; while (($c = $cursor->getCurrentCharacter()) !== null) { if ($c === '\\' && ($peek = $cursor->peek()) !== null && RegexHelper::isEscapable($peek)) { $cursor->advanceBy(2); } elseif ($c === '(') { $cursor->advanceBy(1); $openParens++; } elseif ($c === ')') { if ($openParens < 1) { break; } $cursor->advanceBy(1); $openParens--; } elseif (\preg_match(RegexHelper::REGEX_WHITESPACE_CHAR, $c)) { break; } else { $cursor->advanceBy(1); } } if ($openParens !== 0) { return null; } if ($cursor->getPosition() === $oldPosition && (! isset($c) || $c !== ')')) { return null; } $newPos = $cursor->getPosition(); $cursor->restoreState($oldState); $cursor->advanceBy($newPos - $cursor->getPosition()); return $cursor->getPreviousText(); } } commonmark/src/Util/RegexHelper.php 0000644 00000024021 15007532371 0013340 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; use League\CommonMark\Exception\InvalidArgumentException; use League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock; /** * Provides regular expressions and utilities for parsing Markdown * * All of the PARTIAL_ regex constants assume that they'll be used in case-insensitive searches * All other complete regexes provided by this class (either via constants or methods) will have case-insensitivity enabled. * * @phpcs:disable Generic.Strings.UnnecessaryStringConcat.Found * * @psalm-immutable */ final class RegexHelper { // Partial regular expressions (wrap with `/` on each side and add the case-insensitive `i` flag before use) public const PARTIAL_ENTITY = '&(?:#x[a-f0-9]{1,6}|#[0-9]{1,7}|[a-z][a-z0-9]{1,31});'; public const PARTIAL_ESCAPABLE = '[!"#$%&\'()*+,.\/:;<=>?@[\\\\\]^_`{|}~-]'; public const PARTIAL_ESCAPED_CHAR = '\\\\' . self::PARTIAL_ESCAPABLE; public const PARTIAL_IN_DOUBLE_QUOTES = '"(' . self::PARTIAL_ESCAPED_CHAR . '|[^"\x00])*"'; public const PARTIAL_IN_SINGLE_QUOTES = '\'(' . self::PARTIAL_ESCAPED_CHAR . '|[^\'\x00])*\''; public const PARTIAL_IN_PARENS = '\\((' . self::PARTIAL_ESCAPED_CHAR . '|[^)\x00])*\\)'; public const PARTIAL_REG_CHAR = '[^\\\\()\x00-\x20]'; public const PARTIAL_IN_PARENS_NOSP = '\((' . self::PARTIAL_REG_CHAR . '|' . self::PARTIAL_ESCAPED_CHAR . '|\\\\)*\)'; public const PARTIAL_TAGNAME = '[a-z][a-z0-9-]*'; public const PARTIAL_BLOCKTAGNAME = '(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h1|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)'; public const PARTIAL_ATTRIBUTENAME = '[a-z_:][a-z0-9:._-]*'; public const PARTIAL_UNQUOTEDVALUE = '[^"\'=<>`\x00-\x20]+'; public const PARTIAL_SINGLEQUOTEDVALUE = '\'[^\']*\''; public const PARTIAL_DOUBLEQUOTEDVALUE = '"[^"]*"'; public const PARTIAL_ATTRIBUTEVALUE = '(?:' . self::PARTIAL_UNQUOTEDVALUE . '|' . self::PARTIAL_SINGLEQUOTEDVALUE . '|' . self::PARTIAL_DOUBLEQUOTEDVALUE . ')'; public const PARTIAL_ATTRIBUTEVALUESPEC = '(?:' . '\s*=' . '\s*' . self::PARTIAL_ATTRIBUTEVALUE . ')'; public const PARTIAL_ATTRIBUTE = '(?:' . '\s+' . self::PARTIAL_ATTRIBUTENAME . self::PARTIAL_ATTRIBUTEVALUESPEC . '?)'; public const PARTIAL_OPENTAG = '<' . self::PARTIAL_TAGNAME . self::PARTIAL_ATTRIBUTE . '*' . '\s*\/?>'; public const PARTIAL_CLOSETAG = '<\/' . self::PARTIAL_TAGNAME . '\s*[>]'; public const PARTIAL_OPENBLOCKTAG = '<' . self::PARTIAL_BLOCKTAGNAME . self::PARTIAL_ATTRIBUTE . '*' . '\s*\/?>'; public const PARTIAL_CLOSEBLOCKTAG = '<\/' . self::PARTIAL_BLOCKTAGNAME . '\s*[>]'; public const PARTIAL_HTMLCOMMENT = '<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->'; public const PARTIAL_PROCESSINGINSTRUCTION = '[<][?][\s\S]*?[?][>]'; public const PARTIAL_DECLARATION = '<![A-Z]+' . '[^>]*>'; public const PARTIAL_CDATA = '<!\[CDATA\[[\s\S]*?]\]>'; public const PARTIAL_HTMLTAG = '(?:' . self::PARTIAL_OPENTAG . '|' . self::PARTIAL_CLOSETAG . '|' . self::PARTIAL_HTMLCOMMENT . '|' . self::PARTIAL_PROCESSINGINSTRUCTION . '|' . self::PARTIAL_DECLARATION . '|' . self::PARTIAL_CDATA . ')'; public const PARTIAL_HTMLBLOCKOPEN = '<(?:' . self::PARTIAL_BLOCKTAGNAME . '(?:[\s\/>]|$)' . '|' . '\/' . self::PARTIAL_BLOCKTAGNAME . '(?:[\s>]|$)' . '|' . '[?!])'; public const PARTIAL_LINK_TITLE = '^(?:"(' . self::PARTIAL_ESCAPED_CHAR . '|[^"\x00])*"' . '|' . '\'(' . self::PARTIAL_ESCAPED_CHAR . '|[^\'\x00])*\'' . '|' . '\((' . self::PARTIAL_ESCAPED_CHAR . '|[^()\x00])*\))'; public const REGEX_PUNCTUATION = '/^[\x{2000}-\x{206F}\x{2E00}-\x{2E7F}\p{Pc}\p{Pd}\p{Pe}\p{Pf}\p{Pi}\p{Po}\p{Ps}\\\\\'!"#\$%&\(\)\*\+,\-\.\\/:;<=>\?@\[\]\^_`\{\|\}~]/u'; public const REGEX_UNSAFE_PROTOCOL = '/^javascript:|vbscript:|file:|data:/i'; public const REGEX_SAFE_DATA_PROTOCOL = '/^data:image\/(?:png|gif|jpeg|webp)/i'; public const REGEX_NON_SPACE = '/[^ \t\f\v\r\n]/'; public const REGEX_WHITESPACE_CHAR = '/^[ \t\n\x0b\x0c\x0d]/'; public const REGEX_UNICODE_WHITESPACE_CHAR = '/^\pZ|\s/u'; public const REGEX_THEMATIC_BREAK = '/^(?:\*[ \t]*){3,}$|^(?:_[ \t]*){3,}$|^(?:-[ \t]*){3,}$/'; public const REGEX_LINK_DESTINATION_BRACES = '/^(?:<(?:[^<>\\n\\\\\\x00]|\\\\.)*>)/'; /** * @psalm-pure */ public static function isEscapable(string $character): bool { return \preg_match('/' . self::PARTIAL_ESCAPABLE . '/', $character) === 1; } /** * @psalm-pure */ public static function isLetter(?string $character): bool { if ($character === null) { return false; } return \preg_match('/[\pL]/u', $character) === 1; } /** * Attempt to match a regex in string s at offset offset * * @psalm-param non-empty-string $regex * * @return int|null Index of match, or null * * @psalm-pure */ public static function matchAt(string $regex, string $string, int $offset = 0): ?int { $matches = []; $string = \mb_substr($string, $offset, null, 'UTF-8'); if (! \preg_match($regex, $string, $matches, \PREG_OFFSET_CAPTURE)) { return null; } // PREG_OFFSET_CAPTURE always returns the byte offset, not the char offset, which is annoying $charPos = \mb_strlen(\mb_strcut($string, 0, $matches[0][1], 'UTF-8'), 'UTF-8'); return $offset + $charPos; } /** * Functional wrapper around preg_match_all which only returns the first set of matches * * @psalm-param non-empty-string $pattern * * @return string[]|null * * @psalm-pure */ public static function matchFirst(string $pattern, string $subject, int $offset = 0): ?array { if ($offset !== 0) { $subject = \substr($subject, $offset); } \preg_match_all($pattern, $subject, $matches, \PREG_SET_ORDER); if ($matches === []) { return null; } return $matches[0] ?: null; } /** * Replace backslash escapes with literal characters * * @psalm-pure */ public static function unescape(string $string): string { $allEscapedChar = '/\\\\(' . self::PARTIAL_ESCAPABLE . ')/'; $escaped = \preg_replace($allEscapedChar, '$1', $string); \assert(\is_string($escaped)); return \preg_replace_callback('/' . self::PARTIAL_ENTITY . '/i', static fn ($e) => Html5EntityDecoder::decode($e[0]), $escaped); } /** * @internal * * @param int $type HTML block type * * @psalm-param HtmlBlock::TYPE_* $type * * @phpstan-param HtmlBlock::TYPE_* $type * * @psalm-return non-empty-string * * @throws InvalidArgumentException if an invalid type is given * * @psalm-pure */ public static function getHtmlBlockOpenRegex(int $type): string { switch ($type) { case HtmlBlock::TYPE_1_CODE_CONTAINER: return '/^<(?:script|pre|textarea|style)(?:\s|>|$)/i'; case HtmlBlock::TYPE_2_COMMENT: return '/^<!--/'; case HtmlBlock::TYPE_3: return '/^<[?]/'; case HtmlBlock::TYPE_4: return '/^<![A-Z]/i'; case HtmlBlock::TYPE_5_CDATA: return '/^<!\[CDATA\[/i'; case HtmlBlock::TYPE_6_BLOCK_ELEMENT: return '%^<[/]?(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[123456]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?:\s|[/]?[>]|$)%i'; case HtmlBlock::TYPE_7_MISC_ELEMENT: return '/^(?:' . self::PARTIAL_OPENTAG . '|' . self::PARTIAL_CLOSETAG . ')\\s*$/i'; default: throw new InvalidArgumentException('Invalid HTML block type'); } } /** * @internal * * @param int $type HTML block type * * @psalm-param HtmlBlock::TYPE_* $type * * @phpstan-param HtmlBlock::TYPE_* $type * * @psalm-return non-empty-string * * @throws InvalidArgumentException if an invalid type is given * * @psalm-pure */ public static function getHtmlBlockCloseRegex(int $type): string { switch ($type) { case HtmlBlock::TYPE_1_CODE_CONTAINER: return '%<\/(?:script|pre|textarea|style)>%i'; case HtmlBlock::TYPE_2_COMMENT: return '/-->/'; case HtmlBlock::TYPE_3: return '/\?>/'; case HtmlBlock::TYPE_4: return '/>/'; case HtmlBlock::TYPE_5_CDATA: return '/\]\]>/'; default: throw new InvalidArgumentException('Invalid HTML block type'); } } /** * @psalm-pure */ public static function isLinkPotentiallyUnsafe(string $url): bool { return \preg_match(self::REGEX_UNSAFE_PROTOCOL, $url) !== 0 && \preg_match(self::REGEX_SAFE_DATA_PROTOCOL, $url) === 0; } } commonmark/src/Util/Html5EntityDecoder.php 0000644 00000003427 15007532371 0014611 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; /** * @psalm-immutable */ final class Html5EntityDecoder { /** * @psalm-pure */ public static function decode(string $entity): string { if (\substr($entity, -1) !== ';') { return $entity; } if (\substr($entity, 0, 2) === '&#') { if (\strtolower(\substr($entity, 2, 1)) === 'x') { return self::fromHex(\substr($entity, 3, -1)); } return self::fromDecimal(\substr($entity, 2, -1)); } return \html_entity_decode($entity, \ENT_QUOTES | \ENT_HTML5, 'UTF-8'); } /** * @param mixed $number * * @psalm-pure */ private static function fromDecimal($number): string { // Only convert code points within planes 0-2, excluding NULL // phpcs:ignore Generic.PHP.ForbiddenFunctions.Found if (empty($number) || $number > 0x2FFFF) { return self::fromHex('fffd'); } $entity = '&#' . $number . ';'; $converted = \mb_decode_numericentity($entity, [0x0, 0x2FFFF, 0, 0xFFFF], 'UTF-8'); if ($converted === $entity) { return self::fromHex('fffd'); } return $converted; } /** * @psalm-pure */ private static function fromHex(string $hexChars): string { return self::fromDecimal(\hexdec($hexChars)); } } commonmark/src/Util/UrlEncoder.php 0000644 00000005073 15007532371 0013176 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; use League\CommonMark\Exception\UnexpectedEncodingException; /** * @psalm-immutable */ final class UrlEncoder { private const ENCODE_CACHE = ['%00', '%01', '%02', '%03', '%04', '%05', '%06', '%07', '%08', '%09', '%0A', '%0B', '%0C', '%0D', '%0E', '%0F', '%10', '%11', '%12', '%13', '%14', '%15', '%16', '%17', '%18', '%19', '%1A', '%1B', '%1C', '%1D', '%1E', '%1F', '%20', '!', '%22', '#', '$', '%25', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '%3C', '=', '%3E', '?', '@', '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', '%5B', '%5C', '%5D', '%5E', '_', '%60', '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', '%7B', '%7C', '%7D', '~', '%7F']; /** * @throws UnexpectedEncodingException if a non-UTF-8-compatible encoding is used * * @psalm-pure */ public static function unescapeAndEncode(string $uri): string { // Optimization: if the URL only includes characters we know will be kept as-is, then just return the URL as-is. if (\preg_match('/^[A-Za-z0-9~!@#$&*()\-_=+;:,.\/?]+$/', $uri)) { return $uri; } if (! \mb_check_encoding($uri, 'UTF-8')) { throw new UnexpectedEncodingException('Unexpected encoding - UTF-8 or ASCII was expected'); } $result = ''; $chars = \mb_str_split($uri, 1, 'UTF-8'); $l = \count($chars); for ($i = 0; $i < $l; $i++) { $code = $chars[$i]; if ($code === '%' && $i + 2 < $l) { if (\preg_match('/^[0-9a-f]{2}$/i', $chars[$i + 1] . $chars[$i + 2]) === 1) { $result .= '%' . $chars[$i + 1] . $chars[$i + 2]; $i += 2; continue; } } if (\ord($code) < 128) { $result .= self::ENCODE_CACHE[\ord($code)]; continue; } $result .= \rawurlencode($code); } return $result; } } commonmark/src/Util/PrioritizedList.php 0000644 00000003151 15007532371 0014267 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Util; /** * @internal * * @phpstan-template T * @phpstan-implements \IteratorAggregate<T> */ final class PrioritizedList implements \IteratorAggregate { /** * @var array<int, array<mixed>> * @phpstan-var array<int, array<T>> */ private array $list = []; /** * @var \Traversable<mixed>|null * @phpstan-var \Traversable<T>|null */ private ?\Traversable $optimized = null; /** * @param mixed $item * * @phpstan-param T $item */ public function add($item, int $priority): void { $this->list[$priority][] = $item; $this->optimized = null; } /** * @return \Traversable<int, mixed> * * @phpstan-return \Traversable<int, T> */ #[\ReturnTypeWillChange] public function getIterator(): \Traversable { if ($this->optimized === null) { \krsort($this->list); $sorted = []; foreach ($this->list as $group) { foreach ($group as $item) { $sorted[] = $item; } } $this->optimized = new \ArrayIterator($sorted); } return $this->optimized; } } commonmark/src/Node/NodeWalkerEvent.php 0000644 00000001575 15007532371 0014144 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node; final class NodeWalkerEvent { /** @psalm-readonly */ private Node $node; /** @psalm-readonly */ private bool $isEntering; public function __construct(Node $node, bool $isEntering = true) { $this->node = $node; $this->isEntering = $isEntering; } public function getNode(): Node { return $this->node; } public function isEntering(): bool { return $this->isEntering; } } commonmark/src/Node/Query/ExpressionInterface.php 0000644 00000000655 15007532371 0016172 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node\Query; use League\CommonMark\Node\Node; interface ExpressionInterface { public function __invoke(Node $node): bool; } commonmark/src/Node/Query/OrExpr.php 0000644 00000002153 15007532371 0013424 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node\Query; use League\CommonMark\Node\Node; /** * @internal */ final class OrExpr implements ExpressionInterface { /** * @var callable[] * @psalm-var list<callable(Node): bool> */ private array $conditions; /** * @psalm-param callable(Node): bool $expressions */ public function __construct(callable ...$expressions) { $this->conditions = \array_values($expressions); } /** * @param callable(Node): bool $expression */ public function add(callable $expression): void { $this->conditions[] = $expression; } public function __invoke(Node $node): bool { foreach ($this->conditions as $condition) { if ($condition($node)) { return true; } } return false; } } commonmark/src/Node/Query/AndExpr.php 0000644 00000002156 15007532371 0013551 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node\Query; use League\CommonMark\Node\Node; /** * @internal */ final class AndExpr implements ExpressionInterface { /** * @var callable[] * @psalm-var list<callable(Node): bool> */ private array $conditions; /** * @psalm-param callable(Node): bool $expressions */ public function __construct(callable ...$expressions) { $this->conditions = \array_values($expressions); } /** * @param callable(Node): bool $expression */ public function add(callable $expression): void { $this->conditions[] = $expression; } public function __invoke(Node $node): bool { foreach ($this->conditions as $condition) { if (! $condition($node)) { return false; } } return true; } } commonmark/src/Node/Query.php 0000644 00000006360 15007532371 0012211 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node; use League\CommonMark\Node\Query\AndExpr; use League\CommonMark\Node\Query\OrExpr; final class Query { /** @var callable(Node): bool $condition */ private $condition; public function __construct() { $this->condition = new AndExpr(); } public function where(callable ...$conditions): self { return $this->andWhere(...$conditions); } public function andWhere(callable ...$conditions): self { if ($this->condition instanceof AndExpr) { foreach ($conditions as $condition) { $this->condition->add($condition); } } else { $this->condition = new AndExpr($this->condition, ...$conditions); } return $this; } public function orWhere(callable ...$conditions): self { if ($this->condition instanceof OrExpr) { foreach ($conditions as $condition) { $this->condition->add($condition); } } else { $this->condition = new OrExpr($this->condition, ...$conditions); } return $this; } public function findOne(Node $node): ?Node { foreach ($node->iterator() as $n) { if (\call_user_func($this->condition, $n)) { return $n; } } return null; } /** * @return iterable<Node> */ public function findAll(Node $node, ?int $limit = PHP_INT_MAX): iterable { $resultCount = 0; foreach ($node->iterator() as $n) { if ($resultCount >= $limit) { break; } if (! \call_user_func($this->condition, $n)) { continue; } ++$resultCount; yield $n; } } /** * @return callable(Node): bool */ public static function type(string $class): callable { return static fn (Node $node): bool => $node instanceof $class; } /** * @psalm-param ?callable(Node): bool $condition * * @return callable(Node): bool */ public static function hasChild(?callable $condition = null): callable { return static function (Node $node) use ($condition): bool { foreach ($node->children() as $child) { if ($condition === null || $condition($child)) { return true; } } return false; }; } /** * @psalm-param ?callable(Node): bool $condition * * @return callable(Node): bool */ public static function hasParent(?callable $condition = null): callable { return static function (Node $node) use ($condition): bool { $parent = $node->parent(); if ($parent === null) { return false; } if ($condition === null) { return true; } return $condition($parent); }; } } commonmark/src/Node/Inline/Newline.php 0000644 00000001641 15007532371 0013720 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node\Inline; final class Newline extends AbstractInline { // Any changes to these constants should be reflected in .phpstorm.meta.php public const HARDBREAK = 0; public const SOFTBREAK = 1; /** @psalm-readonly */ private int $type; public function __construct(int $breakType = self::HARDBREAK) { parent::__construct(); $this->type = $breakType; } /** @psalm-immutable */ public function getType(): int { return $this->type; } } commonmark/src/Node/Inline/AbstractStringContainer.php 0000644 00000002127 15007532371 0017114 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node\Inline; use League\CommonMark\Node\StringContainerInterface; abstract class AbstractStringContainer extends AbstractInline implements StringContainerInterface { protected string $literal = ''; /** * @param array<string, mixed> $data */ public function __construct(string $contents = '', array $data = []) { parent::__construct(); $this->literal = $contents; if (\count($data) > 0) { $this->data->import($data); } } public function getLiteral(): string { return $this->literal; } public function setLiteral(string $literal): void { $this->literal = $literal; } } commonmark/src/Node/Inline/AbstractInline.php 0000644 00000001007 15007532371 0015215 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node\Inline; use League\CommonMark\Node\Node; abstract class AbstractInline extends Node { } commonmark/src/Node/Inline/Text.php 0000644 00000001115 15007532371 0013237 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node\Inline; final class Text extends AbstractStringContainer { public function append(string $literal): void { $this->literal .= $literal; } } commonmark/src/Node/Inline/AdjacentTextMerger.php 0000644 00000005604 15007532371 0016042 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java) * - (c) Atlassian Pty Ltd * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node\Inline; use League\CommonMark\Node\Node; /** * @internal */ final class AdjacentTextMerger { public static function mergeChildNodes(Node $node): void { // No children or just one child node, no need for merging if ($node->firstChild() === $node->lastChild() || $node->firstChild() === null || $node->lastChild() === null) { return; } /** @psalm-suppress PossiblyNullArgument */ self::mergeTextNodesInclusive($node->firstChild(), $node->lastChild()); } public static function mergeTextNodesBetweenExclusive(Node $fromNode, Node $toNode): void { // No nodes between them if ($fromNode === $toNode || $fromNode->next() === $toNode || $fromNode->next() === null || $toNode->previous() === null) { return; } /** @psalm-suppress PossiblyNullArgument */ self::mergeTextNodesInclusive($fromNode->next(), $toNode->previous()); } public static function mergeWithDirectlyAdjacentNodes(Text $node): void { $start = ($previous = $node->previous()) instanceof Text ? $previous : $node; $end = ($next = $node->next()) instanceof Text ? $next : $node; self::mergeIfNeeded($start, $end); } private static function mergeTextNodesInclusive(Node $fromNode, Node $toNode): void { $first = null; $last = null; $node = $fromNode; while ($node !== null) { if ($node instanceof Text) { if ($first === null) { $first = $node; } $last = $node; } else { self::mergeIfNeeded($first, $last); $first = null; $last = null; } if ($node === $toNode) { break; } $node = $node->next(); } self::mergeIfNeeded($first, $last); } private static function mergeIfNeeded(?Text $first, ?Text $last): void { if ($first === null || $last === null || $first === $last) { // No merging needed return; } $s = $first->getLiteral(); $node = $first->next(); $stop = $last->next(); while ($node !== $stop && $node instanceof Text) { $s .= $node->getLiteral(); $unlink = $node; $node = $node->next(); $unlink->detach(); } $first->setLiteral($s); } } commonmark/src/Node/Inline/DelimitedInterface.php 0000644 00000000702 15007532371 0016035 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node\Inline; interface DelimitedInterface { public function getOpeningDelimiter(): string; public function getClosingDelimiter(): string; } commonmark/src/Node/RawMarkupContainerInterface.php 0000644 00000000721 15007532371 0016474 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node; /** * Interface for a node which contains raw, unprocessed markup (like HTML) */ interface RawMarkupContainerInterface extends StringContainerInterface { } commonmark/src/Node/Block/TightBlockInterface.php 0000644 00000000662 15007532371 0016010 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node\Block; interface TightBlockInterface { public function isTight(): bool; public function setTight(bool $tight): void; } commonmark/src/Node/Block/AbstractBlock.php 0000644 00000002661 15007532371 0014654 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node\Block; use League\CommonMark\Exception\InvalidArgumentException; use League\CommonMark\Node\Node; /** * Block-level element * * @method parent() ?AbstractBlock */ abstract class AbstractBlock extends Node { protected ?int $startLine = null; protected ?int $endLine = null; protected function setParent(?Node $node = null): void { if ($node && ! $node instanceof self) { throw new InvalidArgumentException('Parent of block must also be block (cannot be inline)'); } parent::setParent($node); } public function setStartLine(?int $startLine): void { $this->startLine = $startLine; if ($this->endLine === null) { $this->endLine = $startLine; } } public function getStartLine(): ?int { return $this->startLine; } public function setEndLine(?int $endLine): void { $this->endLine = $endLine; } public function getEndLine(): ?int { return $this->endLine; } } commonmark/src/Node/Block/Document.php 0000644 00000002402 15007532371 0013705 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node\Block; use League\CommonMark\Parser\Cursor; use League\CommonMark\Reference\ReferenceMap; use League\CommonMark\Reference\ReferenceMapInterface; class Document extends AbstractBlock { /** @psalm-readonly */ protected ReferenceMapInterface $referenceMap; public function __construct(?ReferenceMapInterface $referenceMap = null) { parent::__construct(); $this->setStartLine(1); $this->referenceMap = $referenceMap ?? new ReferenceMap(); } public function getReferenceMap(): ReferenceMapInterface { return $this->referenceMap; } public function canContain(AbstractBlock $block): bool { return true; } public function isCode(): bool { return false; } public function matchesNextLine(Cursor $cursor): bool { return true; } } commonmark/src/Node/Block/Paragraph.php 0000644 00000000737 15007532371 0014045 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node\Block; class Paragraph extends AbstractBlock { } commonmark/src/Node/StringContainerInterface.php 0000644 00000001200 15007532371 0016022 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node; /** * Interface for a node which directly contains line(s) of text */ interface StringContainerInterface { public function setLiteral(string $literal): void; public function getLiteral(): string; } commonmark/src/Node/NodeIterator.php 0000644 00000002542 15007532371 0013501 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node; use League\CommonMark\Node\Block\AbstractBlock; /** * @implements \IteratorAggregate<int, Node> */ final class NodeIterator implements \IteratorAggregate { public const FLAG_BLOCKS_ONLY = 1; private Node $node; private bool $blocksOnly; public function __construct(Node $node, int $flags = 0) { $this->node = $node; $this->blocksOnly = ($flags & self::FLAG_BLOCKS_ONLY) === self::FLAG_BLOCKS_ONLY; } /** * @return \Generator<int, Node> */ public function getIterator(): \Generator { $stack = [$this->node]; $index = 0; while ($stack) { $node = \array_pop($stack); yield $index++ => $node; // Push all children onto the stack in reverse order $child = $node->lastChild(); while ($child !== null) { if (! $this->blocksOnly || $child instanceof AbstractBlock) { $stack[] = $child; } $child = $child->previous(); } } } } commonmark/src/Node/StringContainerHelper.php 0000644 00000002514 15007532371 0015352 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node; final class StringContainerHelper { /** * Extract text literals from all descendant nodes * * @param Node $node Parent node * @param array<string> $excludeTypes Optional list of node class types to exclude * * @return string Concatenated literals */ public static function getChildText(Node $node, array $excludeTypes = []): string { $text = ''; foreach ($node->iterator() as $child) { if ($child instanceof StringContainerInterface && ! self::isOneOf($child, $excludeTypes)) { $text .= $child->getLiteral(); } } return $text; } /** * @param string[] $classesOrInterfacesToCheck * * @psalm-pure */ private static function isOneOf(object $object, array $classesOrInterfacesToCheck): bool { foreach ($classesOrInterfacesToCheck as $type) { if ($object instanceof $type) { return true; } } return false; } } commonmark/src/Node/Node.php 0000644 00000014502 15007532371 0011766 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node; use Dflydev\DotAccessData\Data; use League\CommonMark\Exception\InvalidArgumentException; abstract class Node { /** @psalm-readonly */ public Data $data; /** @psalm-readonly-allow-private-mutation */ protected int $depth = 0; /** @psalm-readonly-allow-private-mutation */ protected ?Node $parent = null; /** @psalm-readonly-allow-private-mutation */ protected ?Node $previous = null; /** @psalm-readonly-allow-private-mutation */ protected ?Node $next = null; /** @psalm-readonly-allow-private-mutation */ protected ?Node $firstChild = null; /** @psalm-readonly-allow-private-mutation */ protected ?Node $lastChild = null; public function __construct() { $this->data = new Data([ 'attributes' => [], ]); } public function previous(): ?Node { return $this->previous; } public function next(): ?Node { return $this->next; } public function parent(): ?Node { return $this->parent; } protected function setParent(?Node $node = null): void { $this->parent = $node; $this->depth = $node === null ? 0 : $node->depth + 1; } /** * Inserts the $sibling node after $this */ public function insertAfter(Node $sibling): void { $sibling->detach(); $sibling->next = $this->next; if ($sibling->next) { $sibling->next->previous = $sibling; } $sibling->previous = $this; $this->next = $sibling; $sibling->setParent($this->parent); if (! $sibling->next && $sibling->parent) { $sibling->parent->lastChild = $sibling; } } /** * Inserts the $sibling node before $this */ public function insertBefore(Node $sibling): void { $sibling->detach(); $sibling->previous = $this->previous; if ($sibling->previous) { $sibling->previous->next = $sibling; } $sibling->next = $this; $this->previous = $sibling; $sibling->setParent($this->parent); if (! $sibling->previous && $sibling->parent) { $sibling->parent->firstChild = $sibling; } } public function replaceWith(Node $replacement): void { $replacement->detach(); $this->insertAfter($replacement); $this->detach(); } public function detach(): void { if ($this->previous) { $this->previous->next = $this->next; } elseif ($this->parent) { $this->parent->firstChild = $this->next; } if ($this->next) { $this->next->previous = $this->previous; } elseif ($this->parent) { $this->parent->lastChild = $this->previous; } $this->parent = null; $this->next = null; $this->previous = null; $this->depth = 0; } public function hasChildren(): bool { return $this->firstChild !== null; } public function firstChild(): ?Node { return $this->firstChild; } public function lastChild(): ?Node { return $this->lastChild; } /** * @return Node[] */ public function children(): iterable { $children = []; for ($current = $this->firstChild; $current !== null; $current = $current->next) { $children[] = $current; } return $children; } public function appendChild(Node $child): void { if ($this->lastChild) { $this->lastChild->insertAfter($child); } else { $child->detach(); $child->setParent($this); $this->lastChild = $this->firstChild = $child; } } /** * Adds $child as the very first child of $this */ public function prependChild(Node $child): void { if ($this->firstChild) { $this->firstChild->insertBefore($child); } else { $child->detach(); $child->setParent($this); $this->lastChild = $this->firstChild = $child; } } /** * Detaches all child nodes of given node */ public function detachChildren(): void { foreach ($this->children() as $children) { $children->setParent(null); } $this->firstChild = $this->lastChild = null; } /** * Replace all children of given node with collection of another * * @param iterable<Node> $children */ public function replaceChildren(iterable $children): void { $this->detachChildren(); foreach ($children as $item) { $this->appendChild($item); } } public function getDepth(): int { return $this->depth; } public function walker(): NodeWalker { return new NodeWalker($this); } public function iterator(int $flags = 0): NodeIterator { return new NodeIterator($this, $flags); } /** * Clone the current node and its children * * WARNING: This is a recursive function and should not be called on deeply-nested node trees! */ public function __clone() { // Cloned nodes are detached from their parents, siblings, and children $this->parent = null; $this->previous = null; $this->next = null; // But save a copy of the children since we'll need that in a moment $children = $this->children(); $this->detachChildren(); // The original children get cloned and re-added foreach ($children as $child) { $this->appendChild(clone $child); } } public static function assertInstanceOf(Node $node): void { if (! $node instanceof static) { throw new InvalidArgumentException(\sprintf('Incompatible node type: expected %s, got %s', static::class, \get_class($node))); } } } commonmark/src/Node/NodeWalker.php 0000644 00000004231 15007532371 0013132 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Node; use League\CommonMark\Node\Block\AbstractBlock; final class NodeWalker { /** @psalm-readonly */ private Node $root; /** @psalm-readonly-allow-private-mutation */ private ?Node $current = null; /** @psalm-readonly-allow-private-mutation */ private bool $entering; public function __construct(Node $root) { $this->root = $root; $this->current = $this->root; $this->entering = true; } /** * Returns an event which contains node and entering flag * (entering is true when we enter a Node from a parent or sibling, * and false when we reenter it from child) */ public function next(): ?NodeWalkerEvent { $current = $this->current; $entering = $this->entering; if ($current === null) { return null; } if ($entering && ($current instanceof AbstractBlock || $current->hasChildren())) { if ($current->firstChild()) { $this->current = $current->firstChild(); $this->entering = true; } else { $this->entering = false; } } elseif ($current === $this->root) { $this->current = null; } elseif ($current->next() === null) { $this->current = $current->parent(); $this->entering = false; } else { $this->current = $current->next(); $this->entering = true; } return new NodeWalkerEvent($current, $entering); } /** * Resets the iterator to resume at the specified node */ public function resumeAt(Node $node, bool $entering = true): void { $this->current = $node; $this->entering = $entering; } } commonmark/src/Output/RenderedContent.php 0000644 00000001752 15007532371 0014602 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Output; use League\CommonMark\Node\Block\Document; class RenderedContent implements RenderedContentInterface, \Stringable { /** @psalm-readonly */ private Document $document; /** @psalm-readonly */ private string $content; public function __construct(Document $document, string $content) { $this->document = $document; $this->content = $content; } public function getDocument(): Document { return $this->document; } public function getContent(): string { return $this->content; } /** * @psalm-mutation-free */ public function __toString(): string { return $this->content; } } commonmark/src/Output/RenderedContentInterface.php 0000644 00000001114 15007532371 0016413 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Output; use League\CommonMark\Node\Block\Document; interface RenderedContentInterface extends \Stringable { /** * @psalm-mutation-free */ public function getDocument(): Document; /** * @psalm-mutation-free */ public function getContent(): string; } commonmark/src/Reference/ReferenceMap.php 0000644 00000003515 15007532371 0014450 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Reference; use League\CommonMark\Normalizer\TextNormalizer; /** * A collection of references, indexed by label */ final class ReferenceMap implements ReferenceMapInterface { /** @psalm-readonly */ private TextNormalizer $normalizer; /** * @var array<string, ReferenceInterface> * * @psalm-readonly-allow-private-mutation */ private array $references = []; public function __construct() { $this->normalizer = new TextNormalizer(); } public function add(ReferenceInterface $reference): void { // Normalize the key $key = $this->normalizer->normalize($reference->getLabel()); // Store the reference $this->references[$key] = $reference; } public function contains(string $label): bool { $label = $this->normalizer->normalize($label); return isset($this->references[$label]); } public function get(string $label): ?ReferenceInterface { $label = $this->normalizer->normalize($label); return $this->references[$label] ?? null; } /** * @return \Traversable<string, ReferenceInterface> */ public function getIterator(): \Traversable { foreach ($this->references as $normalizedLabel => $reference) { yield $normalizedLabel => $reference; } } public function count(): int { return \count($this->references); } } commonmark/src/Reference/ReferenceMapInterface.php 0000644 00000001423 15007532371 0016265 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Reference; /** * A collection of references * * @phpstan-extends \IteratorAggregate<ReferenceInterface> */ interface ReferenceMapInterface extends \IteratorAggregate, \Countable { public function add(ReferenceInterface $reference): void; public function contains(string $label): bool; public function get(string $label): ?ReferenceInterface; } commonmark/src/Reference/ReferenceInterface.php 0000644 00000001157 15007532371 0015633 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Reference; /** * Link reference */ interface ReferenceInterface { public function getLabel(): string; public function getDestination(): string; public function getTitle(): string; } commonmark/src/Reference/Reference.php 0000644 00000002206 15007532371 0014006 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Reference; /** * @psalm-immutable */ final class Reference implements ReferenceInterface { /** @psalm-readonly */ private string $label; /** @psalm-readonly */ private string $destination; /** @psalm-readonly */ private string $title; public function __construct(string $label, string $destination, string $title) { $this->label = $label; $this->destination = $destination; $this->title = $title; } public function getLabel(): string { return $this->label; } public function getDestination(): string { return $this->destination; } public function getTitle(): string { return $this->title; } } commonmark/src/Reference/ReferenceParser.php 0000644 00000022026 15007532371 0015165 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Reference; use League\CommonMark\Parser\Cursor; use League\CommonMark\Util\LinkParserHelper; final class ReferenceParser { // Looking for the start of a definition, i.e. `[` private const START_DEFINITION = 0; // Looking for and parsing the label, i.e. `[foo]` within `[foo]` private const LABEL = 1; // Parsing the destination, i.e. `/url` in `[foo]: /url` private const DESTINATION = 2; // Looking for the start of a title, i.e. the first `"` in `[foo]: /url "title"` private const START_TITLE = 3; // Parsing the content of the title, i.e. `title` in `[foo]: /url "title"` private const TITLE = 4; // End state, no matter what kind of lines we add, they won't be references private const PARAGRAPH = 5; /** @psalm-readonly-allow-private-mutation */ private string $paragraph = ''; /** * @var array<int, ReferenceInterface> * * @psalm-readonly-allow-private-mutation */ private array $references = []; /** @psalm-readonly-allow-private-mutation */ private int $state = self::START_DEFINITION; /** @psalm-readonly-allow-private-mutation */ private ?string $label = null; /** @psalm-readonly-allow-private-mutation */ private ?string $destination = null; /** * @var string string * * @psalm-readonly-allow-private-mutation */ private string $title = ''; /** @psalm-readonly-allow-private-mutation */ private ?string $titleDelimiter = null; /** @psalm-readonly-allow-private-mutation */ private bool $referenceValid = false; public function getParagraphContent(): string { return $this->paragraph; } /** * @return ReferenceInterface[] */ public function getReferences(): iterable { $this->finishReference(); return $this->references; } public function hasReferences(): bool { return $this->references !== []; } public function parse(string $line): void { if ($this->paragraph !== '') { $this->paragraph .= "\n"; } $this->paragraph .= $line; $cursor = new Cursor($line); while (! $cursor->isAtEnd()) { $result = false; switch ($this->state) { case self::PARAGRAPH: // We're in a paragraph now. Link reference definitions can only appear at the beginning, so once // we're in a paragraph, there's no going back. return; case self::START_DEFINITION: $result = $this->parseStartDefinition($cursor); break; case self::LABEL: $result = $this->parseLabel($cursor); break; case self::DESTINATION: $result = $this->parseDestination($cursor); break; case self::START_TITLE: $result = $this->parseStartTitle($cursor); break; case self::TITLE: $result = $this->parseTitle($cursor); break; default: // this should never happen break; } if (! $result) { $this->state = self::PARAGRAPH; return; } } } private function parseStartDefinition(Cursor $cursor): bool { $cursor->advanceToNextNonSpaceOrTab(); if ($cursor->isAtEnd() || $cursor->getCurrentCharacter() !== '[') { return false; } $this->state = self::LABEL; $this->label = ''; $cursor->advance(); if ($cursor->isAtEnd()) { $this->label .= "\n"; } return true; } private function parseLabel(Cursor $cursor): bool { $cursor->advanceToNextNonSpaceOrTab(); $partialLabel = LinkParserHelper::parsePartialLinkLabel($cursor); if ($partialLabel === null) { return false; } \assert($this->label !== null); $this->label .= $partialLabel; if ($cursor->isAtEnd()) { // label might continue on next line $this->label .= "\n"; return true; } if ($cursor->getCurrentCharacter() !== ']') { return false; } $cursor->advance(); // end of label if ($cursor->getCurrentCharacter() !== ':') { return false; } $cursor->advance(); // spec: A link label can have at most 999 characters inside the square brackets if (\mb_strlen($this->label, 'UTF-8') > 999) { return false; } // spec: A link label must contain at least one non-whitespace character if (\trim($this->label) === '') { return false; } $cursor->advanceToNextNonSpaceOrTab(); $this->state = self::DESTINATION; return true; } private function parseDestination(Cursor $cursor): bool { $cursor->advanceToNextNonSpaceOrTab(); $destination = LinkParserHelper::parseLinkDestination($cursor); if ($destination === null) { return false; } $this->destination = $destination; $advanced = $cursor->advanceToNextNonSpaceOrTab(); if ($cursor->isAtEnd()) { // Destination was at end of line, so this is a valid reference for sure (and maybe a title). // If not at end of line, wait for title to be valid first. $this->referenceValid = true; $this->paragraph = ''; } elseif ($advanced === 0) { // spec: The title must be separated from the link destination by whitespace return false; } $this->state = self::START_TITLE; return true; } private function parseStartTitle(Cursor $cursor): bool { $cursor->advanceToNextNonSpaceOrTab(); if ($cursor->isAtEnd()) { $this->state = self::START_DEFINITION; return true; } $this->titleDelimiter = null; switch ($c = $cursor->getCurrentCharacter()) { case '"': case "'": $this->titleDelimiter = $c; break; case '(': $this->titleDelimiter = ')'; break; default: // no title delimter found break; } if ($this->titleDelimiter !== null) { $this->state = self::TITLE; $cursor->advance(); if ($cursor->isAtEnd()) { $this->title .= "\n"; } } else { $this->finishReference(); // There might be another reference instead, try that for the same character. $this->state = self::START_DEFINITION; } return true; } private function parseTitle(Cursor $cursor): bool { \assert($this->titleDelimiter !== null); $title = LinkParserHelper::parsePartialLinkTitle($cursor, $this->titleDelimiter); if ($title === null) { // Invalid title, stop return false; } // Did we find the end delimiter? $endDelimiterFound = false; if (\substr($title, -1) === $this->titleDelimiter) { $endDelimiterFound = true; // Chop it off $title = \substr($title, 0, -1); } $this->title .= $title; if (! $endDelimiterFound && $cursor->isAtEnd()) { // Title still going, continue on next line $this->title .= "\n"; return true; } // We either hit the end delimiter or some extra whitespace $cursor->advanceToNextNonSpaceOrTab(); if (! $cursor->isAtEnd()) { // spec: No further non-whitespace characters may occur on the line. return false; } $this->referenceValid = true; $this->finishReference(); $this->paragraph = ''; // See if there's another definition $this->state = self::START_DEFINITION; return true; } private function finishReference(): void { if (! $this->referenceValid) { return; } /** @psalm-suppress PossiblyNullArgument -- these can't possibly be null if we're in this state */ $this->references[] = new Reference($this->label, $this->destination, $this->title); $this->label = null; $this->referenceValid = false; $this->destination = null; $this->title = ''; $this->titleDelimiter = null; } } commonmark/src/Reference/ReferenceableInterface.php 0000644 00000000625 15007532371 0016456 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Reference; interface ReferenceableInterface { public function getReference(): ReferenceInterface; } commonmark/src/CommonMarkConverter.php 0000644 00000002236 15007532371 0014150 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark; use League\CommonMark\Environment\Environment; use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; /** * Converts CommonMark-compatible Markdown to HTML. */ final class CommonMarkConverter extends MarkdownConverter { /** * Create a new Markdown converter pre-configured for CommonMark * * @param array<string, mixed> $config */ public function __construct(array $config = []) { $environment = new Environment($config); $environment->addExtension(new CommonMarkCoreExtension()); parent::__construct($environment); } public function getEnvironment(): Environment { \assert($this->environment instanceof Environment); return $this->environment; } } commonmark/src/MarkdownConverterInterface.php 0000644 00000001516 15007532371 0015510 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark; use League\CommonMark\Exception\CommonMarkException; use League\CommonMark\Output\RenderedContentInterface; /** * Interface for a service which converts Markdown to HTML. * * @deprecated since 2.2; use {@link ConverterInterface} instead */ interface MarkdownConverterInterface { /** * Converts Markdown to HTML. * * @deprecated since 2.2; use {@link ConverterInterface::convert()} instead * * @throws CommonMarkException */ public function convertToHtml(string $markdown): RenderedContentInterface; } commonmark/src/Input/MarkdownInputInterface.php 0000644 00000001015 15007532371 0015731 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Input; interface MarkdownInputInterface { public function getContent(): string; /** * @return iterable<int, string> */ public function getLines(): iterable; public function getLineCount(): int; } commonmark/src/Input/MarkdownInput.php 0000644 00000005016 15007532371 0014115 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Input; use League\CommonMark\Exception\UnexpectedEncodingException; class MarkdownInput implements MarkdownInputInterface { /** * @var array<int, string>|null * * @psalm-readonly-allow-private-mutation */ private ?array $lines = null; /** @psalm-readonly-allow-private-mutation */ private string $content; /** @psalm-readonly-allow-private-mutation */ private ?int $lineCount = null; /** @psalm-readonly */ private int $lineOffset; public function __construct(string $content, int $lineOffset = 0) { if (! \mb_check_encoding($content, 'UTF-8')) { throw new UnexpectedEncodingException('Unexpected encoding - UTF-8 or ASCII was expected'); } // Strip any leading UTF-8 BOM if (\substr($content, 0, 3) === "\xEF\xBB\xBF") { $content = \substr($content, 3); } $this->content = $content; $this->lineOffset = $lineOffset; } public function getContent(): string { return $this->content; } /** * {@inheritDoc} */ public function getLines(): iterable { $this->splitLinesIfNeeded(); \assert($this->lines !== null); /** @psalm-suppress PossiblyNullIterator */ foreach ($this->lines as $i => $line) { yield $this->lineOffset + $i + 1 => $line; } } public function getLineCount(): int { $this->splitLinesIfNeeded(); \assert($this->lineCount !== null); return $this->lineCount; } private function splitLinesIfNeeded(): void { if ($this->lines !== null) { return; } $lines = \preg_split('/\r\n|\n|\r/', $this->content); if ($lines === false) { throw new UnexpectedEncodingException('Failed to split Markdown content by line'); } $this->lines = $lines; // Remove any newline which appears at the very end of the string. // We've already split the document by newlines, so we can simply drop // any empty element which appears on the end. if (\end($this->lines) === '') { \array_pop($this->lines); } $this->lineCount = \count($this->lines); } } commonmark/src/MarkdownConverter.php 0000644 00000005076 15007532371 0013674 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Exception\CommonMarkException; use League\CommonMark\Output\RenderedContentInterface; use League\CommonMark\Parser\MarkdownParser; use League\CommonMark\Parser\MarkdownParserInterface; use League\CommonMark\Renderer\HtmlRenderer; use League\CommonMark\Renderer\MarkdownRendererInterface; class MarkdownConverter implements ConverterInterface, MarkdownConverterInterface { /** @psalm-readonly */ protected EnvironmentInterface $environment; /** @psalm-readonly */ protected MarkdownParserInterface $markdownParser; /** @psalm-readonly */ protected MarkdownRendererInterface $htmlRenderer; public function __construct(EnvironmentInterface $environment) { $this->environment = $environment; $this->markdownParser = new MarkdownParser($environment); $this->htmlRenderer = new HtmlRenderer($environment); } public function getEnvironment(): EnvironmentInterface { return $this->environment; } /** * Converts Markdown to HTML. * * @param string $input The Markdown to convert * * @return RenderedContentInterface Rendered HTML * * @throws CommonMarkException */ public function convert(string $input): RenderedContentInterface { $documentAST = $this->markdownParser->parse($input); return $this->htmlRenderer->renderDocument($documentAST); } /** * Converts Markdown to HTML. * * @deprecated since 2.2; use {@link convert()} instead * * @param string $markdown The Markdown to convert * * @return RenderedContentInterface Rendered HTML * * @throws CommonMarkException */ public function convertToHtml(string $markdown): RenderedContentInterface { \trigger_deprecation('league/commonmark', '2.2.0', 'Calling "convertToHtml()" on a %s class is deprecated, use "convert()" instead.', self::class); return $this->convert($markdown); } /** * Converts CommonMark to HTML. * * @see MarkdownConverter::convert() * * @throws CommonMarkException */ public function __invoke(string $markdown): RenderedContentInterface { return $this->convert($markdown); } } commonmark/src/Parser/Cursor.php 0000644 00000033673 15007532371 0012737 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Exception\UnexpectedEncodingException; class Cursor { public const INDENT_LEVEL = 4; /** @psalm-readonly */ private string $line; /** @psalm-readonly */ private int $length; /** * @var int * * It's possible for this to be 1 char past the end, meaning we've parsed all chars and have * reached the end. In this state, any character-returning method MUST return null. */ private int $currentPosition = 0; private int $column = 0; private int $indent = 0; private int $previousPosition = 0; private ?int $nextNonSpaceCache = null; private bool $partiallyConsumedTab = false; /** * @var int|false * * @psalm-readonly */ private $lastTabPosition; /** @psalm-readonly */ private bool $isMultibyte; /** @var array<int, string> */ private array $charCache = []; /** * @param string $line The line being parsed (ASCII or UTF-8) */ public function __construct(string $line) { if (! \mb_check_encoding($line, 'UTF-8')) { throw new UnexpectedEncodingException('Unexpected encoding - UTF-8 or ASCII was expected'); } $this->line = $line; $this->length = \mb_strlen($line, 'UTF-8') ?: 0; $this->isMultibyte = $this->length !== \strlen($line); $this->lastTabPosition = $this->isMultibyte ? \mb_strrpos($line, "\t", 0, 'UTF-8') : \strrpos($line, "\t"); } /** * Returns the position of the next character which is not a space (or tab) */ public function getNextNonSpacePosition(): int { if ($this->nextNonSpaceCache !== null) { return $this->nextNonSpaceCache; } if ($this->currentPosition >= $this->length) { return $this->length; } $cols = $this->column; for ($i = $this->currentPosition; $i < $this->length; $i++) { // This if-else was copied out of getCharacter() for performance reasons if ($this->isMultibyte) { $c = $this->charCache[$i] ??= \mb_substr($this->line, $i, 1, 'UTF-8'); } else { $c = $this->line[$i]; } if ($c === ' ') { $cols++; } elseif ($c === "\t") { $cols += 4 - ($cols % 4); } else { break; } } $this->indent = $cols - $this->column; return $this->nextNonSpaceCache = $i; } /** * Returns the next character which isn't a space (or tab) */ public function getNextNonSpaceCharacter(): ?string { $index = $this->getNextNonSpacePosition(); if ($index >= $this->length) { return null; } if ($this->isMultibyte) { return $this->charCache[$index] ??= \mb_substr($this->line, $index, 1, 'UTF-8'); } return $this->line[$index]; } /** * Calculates the current indent (number of spaces after current position) */ public function getIndent(): int { if ($this->nextNonSpaceCache === null) { $this->getNextNonSpacePosition(); } return $this->indent; } /** * Whether the cursor is indented to INDENT_LEVEL */ public function isIndented(): bool { if ($this->nextNonSpaceCache === null) { $this->getNextNonSpacePosition(); } return $this->indent >= self::INDENT_LEVEL; } public function getCharacter(?int $index = null): ?string { if ($index === null) { $index = $this->currentPosition; } // Index out-of-bounds, or we're at the end if ($index < 0 || $index >= $this->length) { return null; } if ($this->isMultibyte) { return $this->charCache[$index] ??= \mb_substr($this->line, $index, 1, 'UTF-8'); } return $this->line[$index]; } /** * Slightly-optimized version of getCurrent(null) */ public function getCurrentCharacter(): ?string { if ($this->currentPosition >= $this->length) { return null; } if ($this->isMultibyte) { return $this->charCache[$this->currentPosition] ??= \mb_substr($this->line, $this->currentPosition, 1, 'UTF-8'); } return $this->line[$this->currentPosition]; } /** * Returns the next character (or null, if none) without advancing forwards */ public function peek(int $offset = 1): ?string { return $this->getCharacter($this->currentPosition + $offset); } /** * Whether the remainder is blank */ public function isBlank(): bool { return $this->nextNonSpaceCache === $this->length || $this->getNextNonSpacePosition() === $this->length; } /** * Move the cursor forwards */ public function advance(): void { $this->advanceBy(1); } /** * Move the cursor forwards * * @param int $characters Number of characters to advance by * @param bool $advanceByColumns Whether to advance by columns instead of spaces */ public function advanceBy(int $characters, bool $advanceByColumns = false): void { $this->previousPosition = $this->currentPosition; $this->nextNonSpaceCache = null; if ($this->currentPosition >= $this->length || $characters === 0) { return; } // Optimization to avoid tab handling logic if we have no tabs if ($this->lastTabPosition === false || $this->currentPosition > $this->lastTabPosition) { $length = \min($characters, $this->length - $this->currentPosition); $this->partiallyConsumedTab = false; $this->currentPosition += $length; $this->column += $length; return; } $nextFewChars = $this->isMultibyte ? \mb_substr($this->line, $this->currentPosition, $characters, 'UTF-8') : \substr($this->line, $this->currentPosition, $characters); if ($characters === 1) { $asArray = [$nextFewChars]; } elseif ($this->isMultibyte) { /** @var string[] $asArray */ $asArray = \mb_str_split($nextFewChars, 1, 'UTF-8'); } else { $asArray = \str_split($nextFewChars); } foreach ($asArray as $c) { if ($c === "\t") { $charsToTab = 4 - ($this->column % 4); if ($advanceByColumns) { $this->partiallyConsumedTab = $charsToTab > $characters; $charsToAdvance = $charsToTab > $characters ? $characters : $charsToTab; $this->column += $charsToAdvance; $this->currentPosition += $this->partiallyConsumedTab ? 0 : 1; $characters -= $charsToAdvance; } else { $this->partiallyConsumedTab = false; $this->column += $charsToTab; $this->currentPosition++; $characters--; } } else { $this->partiallyConsumedTab = false; $this->currentPosition++; $this->column++; $characters--; } if ($characters <= 0) { break; } } } /** * Advances the cursor by a single space or tab, if present */ public function advanceBySpaceOrTab(): bool { $character = $this->getCurrentCharacter(); if ($character === ' ' || $character === "\t") { $this->advanceBy(1, true); return true; } return false; } /** * Parse zero or more space/tab characters * * @return int Number of positions moved */ public function advanceToNextNonSpaceOrTab(): int { $newPosition = $this->nextNonSpaceCache ?? $this->getNextNonSpacePosition(); if ($newPosition === $this->currentPosition) { return 0; } $this->advanceBy($newPosition - $this->currentPosition); $this->partiallyConsumedTab = false; // We've just advanced to where that non-space is, // so any subsequent calls to find the next one will // always return the current position. $this->nextNonSpaceCache = $this->currentPosition; $this->indent = 0; return $this->currentPosition - $this->previousPosition; } /** * Parse zero or more space characters, including at most one newline. * * Tab characters are not parsed with this function. * * @return int Number of positions moved */ public function advanceToNextNonSpaceOrNewline(): int { $remainder = $this->getRemainder(); // Optimization: Avoid the regex if we know there are no spaces or newlines if ($remainder === '' || ($remainder[0] !== ' ' && $remainder[0] !== "\n")) { $this->previousPosition = $this->currentPosition; return 0; } $matches = []; \preg_match('/^ *(?:\n *)?/', $remainder, $matches, \PREG_OFFSET_CAPTURE); // [0][0] contains the matched text // [0][1] contains the index of that match $increment = $matches[0][1] + \strlen($matches[0][0]); $this->advanceBy($increment); return $this->currentPosition - $this->previousPosition; } /** * Move the position to the very end of the line * * @return int The number of characters moved */ public function advanceToEnd(): int { $this->previousPosition = $this->currentPosition; $this->nextNonSpaceCache = null; $this->currentPosition = $this->length; return $this->currentPosition - $this->previousPosition; } public function getRemainder(): string { if ($this->currentPosition >= $this->length) { return ''; } $prefix = ''; $position = $this->currentPosition; if ($this->partiallyConsumedTab) { $position++; $charsToTab = 4 - ($this->column % 4); $prefix = \str_repeat(' ', $charsToTab); } $subString = $this->isMultibyte ? \mb_substr($this->line, $position, null, 'UTF-8') : \substr($this->line, $position); return $prefix . $subString; } public function getLine(): string { return $this->line; } public function isAtEnd(): bool { return $this->currentPosition >= $this->length; } /** * Try to match a regular expression * * Returns the matching text and advances to the end of that match * * @psalm-param non-empty-string $regex */ public function match(string $regex): ?string { $subject = $this->getRemainder(); if (! \preg_match($regex, $subject, $matches, \PREG_OFFSET_CAPTURE)) { return null; } // $matches[0][0] contains the matched text // $matches[0][1] contains the index of that match if ($this->isMultibyte) { // PREG_OFFSET_CAPTURE always returns the byte offset, not the char offset, which is annoying $offset = \mb_strlen(\substr($subject, 0, $matches[0][1]), 'UTF-8'); $matchLength = \mb_strlen($matches[0][0], 'UTF-8'); } else { $offset = $matches[0][1]; $matchLength = \strlen($matches[0][0]); } // [0][0] contains the matched text // [0][1] contains the index of that match $this->advanceBy($offset + $matchLength); return $matches[0][0]; } /** * Encapsulates the current state of this cursor in case you need to rollback later. * * WARNING: Do not parse or use the return value for ANYTHING except for * passing it back into restoreState(), as the number of values and their * contents may change in any future release without warning. */ public function saveState(): CursorState { return new CursorState([ $this->currentPosition, $this->previousPosition, $this->nextNonSpaceCache, $this->indent, $this->column, $this->partiallyConsumedTab, ]); } /** * Restore the cursor to a previous state. * * Pass in the value previously obtained by calling saveState(). */ public function restoreState(CursorState $state): void { [ $this->currentPosition, $this->previousPosition, $this->nextNonSpaceCache, $this->indent, $this->column, $this->partiallyConsumedTab, ] = $state->toArray(); } public function getPosition(): int { return $this->currentPosition; } public function getPreviousText(): string { if ($this->isMultibyte) { return \mb_substr($this->line, $this->previousPosition, $this->currentPosition - $this->previousPosition, 'UTF-8'); } return \substr($this->line, $this->previousPosition, $this->currentPosition - $this->previousPosition); } public function getSubstring(int $start, ?int $length = null): string { if ($this->isMultibyte) { return \mb_substr($this->line, $start, $length, 'UTF-8'); } if ($length !== null) { return \substr($this->line, $start, $length); } return \substr($this->line, $start); } public function getColumn(): int { return $this->column; } } commonmark/src/Parser/Inline/NewlineParser.php 0000644 00000003054 15007532371 0015444 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Inline; use League\CommonMark\Node\Inline\Newline; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Parser\InlineParserContext; final class NewlineParser implements InlineParserInterface { public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::regex('\\n'); } public function parse(InlineParserContext $inlineContext): bool { $inlineContext->getCursor()->advanceBy(1); // Check previous inline for trailing spaces $spaces = 0; $lastInline = $inlineContext->getContainer()->lastChild(); if ($lastInline instanceof Text) { $trimmed = \rtrim($lastInline->getLiteral(), ' '); $spaces = \strlen($lastInline->getLiteral()) - \strlen($trimmed); if ($spaces) { $lastInline->setLiteral($trimmed); } } if ($spaces >= 2) { $inlineContext->getContainer()->appendChild(new Newline(Newline::HARDBREAK)); } else { $inlineContext->getContainer()->appendChild(new Newline(Newline::SOFTBREAK)); } return true; } } commonmark/src/Parser/Inline/InlineParserInterface.php 0000644 00000001026 15007532371 0017077 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Inline; use League\CommonMark\Parser\InlineParserContext; interface InlineParserInterface { public function getMatchDefinition(): InlineParserMatch; public function parse(InlineParserContext $inlineContext): bool; } commonmark/src/Parser/Inline/InlineParserMatch.php 0000644 00000004272 15007532371 0016241 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Inline; use League\CommonMark\Exception\InvalidArgumentException; final class InlineParserMatch { private string $regex; private bool $caseSensitive; private function __construct(string $regex, bool $caseSensitive = false) { $this->regex = $regex; $this->caseSensitive = $caseSensitive; } public function caseSensitive(): self { $this->caseSensitive = true; return $this; } /** * @internal * * @psalm-return non-empty-string */ public function getRegex(): string { return '/' . $this->regex . '/' . ($this->caseSensitive ? '' : 'i'); } /** * Match the given string (case-insensitive) */ public static function string(string $str): self { return new self(\preg_quote($str, '/')); } /** * Match any of the given strings (case-insensitive) */ public static function oneOf(string ...$str): self { return new self(\implode('|', \array_map(static fn (string $str): string => \preg_quote($str, '/'), $str))); } /** * Match a partial regular expression without starting/ending delimiters, anchors, or flags */ public static function regex(string $regex): self { return new self($regex); } public static function join(self ...$definitions): self { $regex = ''; $caseSensitive = null; foreach ($definitions as $definition) { $regex .= '(' . $definition->regex . ')'; if ($caseSensitive === null) { $caseSensitive = $definition->caseSensitive; } elseif ($caseSensitive !== $definition->caseSensitive) { throw new InvalidArgumentException('Case-sensitive and case-insensitive definitions cannot be combined'); } } return new self($regex, $caseSensitive ?? false); } } commonmark/src/Parser/ParserLogicException.php 0000644 00000000701 15007532371 0015535 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Exception\CommonMarkException; class ParserLogicException extends \LogicException implements CommonMarkException { } commonmark/src/Parser/InlineParserEngineInterface.php 0000644 00000001203 15007532371 0017004 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Node\Block\AbstractBlock; /** * Parser for inline content (text, links, emphasized text, etc). */ interface InlineParserEngineInterface { /** * Parse the given contents as inlines and insert them into the given block */ public function parse(string $contents, AbstractBlock $block): void; } commonmark/src/Parser/MarkdownParserState.php 0000644 00000003140 15007532371 0015404 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Block\ParagraphParser; /** * @internal You should rely on the interface instead */ final class MarkdownParserState implements MarkdownParserStateInterface { /** @psalm-readonly */ private BlockContinueParserInterface $activeBlockParser; /** @psalm-readonly */ private BlockContinueParserInterface $lastMatchedBlockParser; public function __construct(BlockContinueParserInterface $activeBlockParser, BlockContinueParserInterface $lastMatchedBlockParser) { $this->activeBlockParser = $activeBlockParser; $this->lastMatchedBlockParser = $lastMatchedBlockParser; } public function getActiveBlockParser(): BlockContinueParserInterface { return $this->activeBlockParser; } public function getLastMatchedBlockParser(): BlockContinueParserInterface { return $this->lastMatchedBlockParser; } public function getParagraphContent(): ?string { if (! $this->lastMatchedBlockParser instanceof ParagraphParser) { return null; } $paragraphParser = $this->lastMatchedBlockParser; $content = $paragraphParser->getContentString(); return $content === '' ? null : $content; } } commonmark/src/Parser/Block/ParagraphParser.php 0000644 00000004273 15007532371 0015570 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\InlineParserEngineInterface; use League\CommonMark\Reference\ReferenceInterface; use League\CommonMark\Reference\ReferenceParser; final class ParagraphParser extends AbstractBlockContinueParser implements BlockContinueParserWithInlinesInterface { /** @psalm-readonly */ private Paragraph $block; /** @psalm-readonly */ private ReferenceParser $referenceParser; public function __construct() { $this->block = new Paragraph(); $this->referenceParser = new ReferenceParser(); } public function canHaveLazyContinuationLines(): bool { return true; } public function getBlock(): Paragraph { return $this->block; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { if ($cursor->isBlank()) { return BlockContinue::none(); } return BlockContinue::at($cursor); } public function addLine(string $line): void { $this->referenceParser->parse($line); } public function closeBlock(): void { if ($this->referenceParser->hasReferences() && $this->referenceParser->getParagraphContent() === '') { $this->block->detach(); } } public function parseInlines(InlineParserEngineInterface $inlineParser): void { $content = $this->getContentString(); if ($content !== '') { $inlineParser->parse($content, $this->block); } } public function getContentString(): string { return $this->referenceParser->getParagraphContent(); } /** * @return ReferenceInterface[] */ public function getReferences(): iterable { return $this->referenceParser->getReferences(); } } commonmark/src/Parser/Block/BlockStartParserInterface.php 0000644 00000002061 15007532371 0017545 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; /** * Interface for a block parser which identifies block starts. */ interface BlockStartParserInterface { /** * Check whether we should handle the block at the current position * * @param Cursor $cursor A cloned copy of the cursor at the current parsing location * @param MarkdownParserStateInterface $parserState Additional information about the state of the Markdown parser * * @return BlockStart|null The BlockStart that has been identified, or null if the block doesn't match here */ public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart; } commonmark/src/Parser/Block/BlockContinue.php 0000644 00000003037 15007532371 0015242 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\CursorState; /** * Result object for continuing parsing of a block; see static methods for constructors. * * @psalm-immutable */ final class BlockContinue { /** @psalm-readonly */ private ?CursorState $cursorState = null; /** @psalm-readonly */ private bool $finalize; private function __construct(?CursorState $cursorState = null, bool $finalize = false) { $this->cursorState = $cursorState; $this->finalize = $finalize; } public function getCursorState(): ?CursorState { return $this->cursorState; } public function isFinalize(): bool { return $this->finalize; } /** * Signal that we cannot continue here * * @return null */ public static function none(): ?self { return null; } /** * Signal that we're continuing at the given position */ public static function at(Cursor $cursor): self { return new self($cursor->saveState(), false); } /** * Signal that we want to finalize and close the block */ public static function finished(): self { return new self(null, true); } } commonmark/src/Parser/Block/BlockStart.php 0000644 00000005241 15007532371 0014552 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\CursorState; /** * Result object for starting parsing of a block; see static methods for constructors */ final class BlockStart { /** * @var BlockContinueParserInterface[] * * @psalm-readonly */ private array $blockParsers; /** @psalm-readonly-allow-private-mutation */ private ?CursorState $cursorState = null; /** @psalm-readonly-allow-private-mutation */ private bool $replaceActiveBlockParser = false; private bool $isAborting = false; private function __construct(BlockContinueParserInterface ...$blockParsers) { $this->blockParsers = $blockParsers; } /** * @return BlockContinueParserInterface[] */ public function getBlockParsers(): iterable { return $this->blockParsers; } public function getCursorState(): ?CursorState { return $this->cursorState; } public function isReplaceActiveBlockParser(): bool { return $this->replaceActiveBlockParser; } /** * @internal */ public function isAborting(): bool { return $this->isAborting; } /** * Signal that we want to parse at the given cursor position * * @return $this */ public function at(Cursor $cursor): self { $this->cursorState = $cursor->saveState(); return $this; } /** * Signal that we want to replace the active block parser with this one * * @return $this */ public function replaceActiveBlockParser(): self { $this->replaceActiveBlockParser = true; return $this; } /** * Signal that we cannot parse whatever is here * * @return null */ public static function none(): ?self { return null; } /** * Signal that we'd like to register the given parser(s) so they can parse the current block */ public static function of(BlockContinueParserInterface ...$blockParsers): self { return new self(...$blockParsers); } /** * Signal that the block parsing process should be aborted (no other block starts should be checked) * * @internal */ public static function abort(): self { $ret = new self(); $ret->isAborting = true; return $ret; } } commonmark/src/Parser/Block/DocumentBlockParser.php 0000644 00000002410 15007532371 0016403 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Node\Block\Document; use League\CommonMark\Parser\Cursor; use League\CommonMark\Reference\ReferenceMapInterface; /** * Parser implementation which ensures everything is added to the root-level Document */ final class DocumentBlockParser extends AbstractBlockContinueParser { /** @psalm-readonly */ private Document $document; public function __construct(ReferenceMapInterface $referenceMap) { $this->document = new Document($referenceMap); } public function getBlock(): Document { return $this->document; } public function isContainer(): bool { return true; } public function canContain(AbstractBlock $childBlock): bool { return true; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { return BlockContinue::at($cursor); } } commonmark/src/Parser/Block/BlockContinueParserInterface.php 0000644 00000003500 15007532371 0020233 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Parser\Cursor; /** * Interface for a block continuation parser * * A block continue parser can only handle a single block instance. The current block being parsed is stored within this parser and * can be returned once parsing has completed. If you need to parse multiple block continuations, instantiate a new parser for each one. */ interface BlockContinueParserInterface { /** * Return the current block being parsed by this parser */ public function getBlock(): AbstractBlock; /** * Return whether we are parsing a container block */ public function isContainer(): bool; /** * Return whether we are interested in possibly lazily parsing any subsequent lines */ public function canHaveLazyContinuationLines(): bool; /** * Determine whether the current block being parsed can contain the given child block */ public function canContain(AbstractBlock $childBlock): bool; /** * Attempt to parse the given line */ public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue; /** * Add the given line of text to the current block */ public function addLine(string $line): void; /** * Close and finalize the current block */ public function closeBlock(): void; } commonmark/src/Parser/Block/SkipLinesStartingWithLettersParser.php 0000644 00000003216 15007532371 0021473 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; use League\CommonMark\Util\RegexHelper; /** * @internal * * This "parser" is actually a performance optimization. * * Most lines in a typical Markdown document probably won't match a block start. This is especially true for lines starting * with letters - nothing in the core CommonMark spec or our supported extensions will match those lines as blocks. Therefore, * if we can identify those lines and skip block start parsing, we can optimize performance by ~10%. * * Previously this optimization was hard-coded in the MarkdownParser but did not allow users to override this behavior. * By implementing this optimization as a block parser instead, users wanting custom blocks starting with letters * can instead register their block parser with a higher priority to ensure their parser is always called first. */ final class SkipLinesStartingWithLettersParser implements BlockStartParserInterface { public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart { if (! $cursor->isIndented() && RegexHelper::isLetter($cursor->getNextNonSpaceCharacter())) { $cursor->advanceToNextNonSpaceOrTab(); return BlockStart::abort(); } return BlockStart::none(); } } commonmark/src/Parser/Block/AbstractBlockContinueParser.php 0000644 00000001663 15007532371 0020106 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Node\Block\AbstractBlock; /** * Base class for a block parser * * Slightly more convenient to extend from vs. implementing the interface */ abstract class AbstractBlockContinueParser implements BlockContinueParserInterface { public function isContainer(): bool { return false; } public function canHaveLazyContinuationLines(): bool { return false; } public function canContain(AbstractBlock $childBlock): bool { return false; } public function addLine(string $line): void { } public function closeBlock(): void { } } commonmark/src/Parser/Block/BlockContinueParserWithInlinesInterface.php 0000644 00000001151 15007532371 0022411 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser\Block; use League\CommonMark\Parser\InlineParserEngineInterface; interface BlockContinueParserWithInlinesInterface extends BlockContinueParserInterface { /** * Parse any inlines inside of the current block */ public function parseInlines(InlineParserEngineInterface $inlineParser): void; } commonmark/src/Parser/MarkdownParser.php 0000644 00000027575 15007532371 0014425 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * Additional code based on commonmark-java (https://github.com/commonmark/commonmark-java) * - (c) Atlassian Pty Ltd * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Event\DocumentPreParsedEvent; use League\CommonMark\Exception\CommonMarkException; use League\CommonMark\Input\MarkdownInput; use League\CommonMark\Node\Block\Document; use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Block\BlockContinueParserWithInlinesInterface; use League\CommonMark\Parser\Block\BlockStart; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Block\DocumentBlockParser; use League\CommonMark\Parser\Block\ParagraphParser; use League\CommonMark\Reference\ReferenceInterface; use League\CommonMark\Reference\ReferenceMap; final class MarkdownParser implements MarkdownParserInterface { /** @psalm-readonly */ private EnvironmentInterface $environment; /** @psalm-readonly-allow-private-mutation */ private int $maxNestingLevel; /** @psalm-readonly-allow-private-mutation */ private ReferenceMap $referenceMap; /** @psalm-readonly-allow-private-mutation */ private int $lineNumber = 0; /** @psalm-readonly-allow-private-mutation */ private Cursor $cursor; /** * @var array<int, BlockContinueParserInterface> * * @psalm-readonly-allow-private-mutation */ private array $activeBlockParsers = []; /** * @var array<int, BlockContinueParserWithInlinesInterface> * * @psalm-readonly-allow-private-mutation */ private array $closedBlockParsers = []; public function __construct(EnvironmentInterface $environment) { $this->environment = $environment; } private function initialize(): void { $this->referenceMap = new ReferenceMap(); $this->lineNumber = 0; $this->activeBlockParsers = []; $this->closedBlockParsers = []; $this->maxNestingLevel = $this->environment->getConfiguration()->get('max_nesting_level'); } /** * @throws CommonMarkException */ public function parse(string $input): Document { $this->initialize(); $documentParser = new DocumentBlockParser($this->referenceMap); $this->activateBlockParser($documentParser); $preParsedEvent = new DocumentPreParsedEvent($documentParser->getBlock(), new MarkdownInput($input)); $this->environment->dispatch($preParsedEvent); $markdownInput = $preParsedEvent->getMarkdown(); foreach ($markdownInput->getLines() as $lineNumber => $line) { $this->lineNumber = $lineNumber; $this->parseLine($line); } // finalizeAndProcess $this->closeBlockParsers(\count($this->activeBlockParsers), $this->lineNumber); $this->processInlines(); $this->environment->dispatch(new DocumentParsedEvent($documentParser->getBlock())); return $documentParser->getBlock(); } /** * Analyze a line of text and update the document appropriately. We parse markdown text by calling this on each * line of input, then finalizing the document. */ private function parseLine(string $line): void { $this->cursor = new Cursor($line); $matches = $this->parseBlockContinuation(); if ($matches === null) { return; } $unmatchedBlocks = \count($this->activeBlockParsers) - $matches; $blockParser = $this->activeBlockParsers[$matches - 1]; $startedNewBlock = false; // Unless last matched container is a code block, try new container starts, // adding children to the last matched container: $tryBlockStarts = $blockParser->getBlock() instanceof Paragraph || $blockParser->isContainer(); while ($tryBlockStarts) { // this is a little performance optimization if ($this->cursor->isBlank()) { $this->cursor->advanceToEnd(); break; } if ($blockParser->getBlock()->getDepth() >= $this->maxNestingLevel) { break; } $blockStart = $this->findBlockStart($blockParser); if ($blockStart === null || $blockStart->isAborting()) { $this->cursor->advanceToNextNonSpaceOrTab(); break; } if (($state = $blockStart->getCursorState()) !== null) { $this->cursor->restoreState($state); } $startedNewBlock = true; // We're starting a new block. If we have any previous blocks that need to be closed, we need to do it now. if ($unmatchedBlocks > 0) { $this->closeBlockParsers($unmatchedBlocks, $this->lineNumber - 1); $unmatchedBlocks = 0; } if ($blockStart->isReplaceActiveBlockParser()) { $this->prepareActiveBlockParserForReplacement(); } foreach ($blockStart->getBlockParsers() as $newBlockParser) { $blockParser = $this->addChild($newBlockParser); $tryBlockStarts = $newBlockParser->isContainer(); } } // What remains at the offset is a text line. Add the text to the appropriate block. // First check for a lazy paragraph continuation: if (! $startedNewBlock && ! $this->cursor->isBlank() && $this->getActiveBlockParser()->canHaveLazyContinuationLines()) { $this->getActiveBlockParser()->addLine($this->cursor->getRemainder()); } else { // finalize any blocks not matched if ($unmatchedBlocks > 0) { $this->closeBlockParsers($unmatchedBlocks, $this->lineNumber); } if (! $blockParser->isContainer()) { $this->getActiveBlockParser()->addLine($this->cursor->getRemainder()); } elseif (! $this->cursor->isBlank()) { $this->addChild(new ParagraphParser()); $this->getActiveBlockParser()->addLine($this->cursor->getRemainder()); } } } private function parseBlockContinuation(): ?int { // For each containing block, try to parse the associated line start. // The document will always match, so we can skip the first block parser and start at 1 matches $matches = 1; for ($i = 1; $i < \count($this->activeBlockParsers); $i++) { $blockParser = $this->activeBlockParsers[$i]; $blockContinue = $blockParser->tryContinue(clone $this->cursor, $this->getActiveBlockParser()); if ($blockContinue === null) { break; } if ($blockContinue->isFinalize()) { $this->closeBlockParsers(\count($this->activeBlockParsers) - $i, $this->lineNumber); return null; } if (($state = $blockContinue->getCursorState()) !== null) { $this->cursor->restoreState($state); } $matches++; } return $matches; } private function findBlockStart(BlockContinueParserInterface $lastMatchedBlockParser): ?BlockStart { $matchedBlockParser = new MarkdownParserState($this->getActiveBlockParser(), $lastMatchedBlockParser); foreach ($this->environment->getBlockStartParsers() as $blockStartParser) { \assert($blockStartParser instanceof BlockStartParserInterface); if (($result = $blockStartParser->tryStart(clone $this->cursor, $matchedBlockParser)) !== null) { return $result; } } return null; } private function closeBlockParsers(int $count, int $endLineNumber): void { for ($i = 0; $i < $count; $i++) { $blockParser = $this->deactivateBlockParser(); $this->finalize($blockParser, $endLineNumber); // phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed if ($blockParser instanceof BlockContinueParserWithInlinesInterface) { // Remember for inline parsing $this->closedBlockParsers[] = $blockParser; } } } /** * Finalize a block. Close it and do any necessary postprocessing, e.g. creating string_content from strings, * setting the 'tight' or 'loose' status of a list, and parsing the beginnings of paragraphs for reference * definitions. */ private function finalize(BlockContinueParserInterface $blockParser, int $endLineNumber): void { if ($blockParser instanceof ParagraphParser) { $this->updateReferenceMap($blockParser->getReferences()); } $blockParser->getBlock()->setEndLine($endLineNumber); $blockParser->closeBlock(); } /** * Walk through a block & children recursively, parsing string content into inline content where appropriate. */ private function processInlines(): void { $p = new InlineParserEngine($this->environment, $this->referenceMap); foreach ($this->closedBlockParsers as $blockParser) { $blockParser->parseInlines($p); } } /** * Add block of type tag as a child of the tip. If the tip can't accept children, close and finalize it and try * its parent, and so on til we find a block that can accept children. */ private function addChild(BlockContinueParserInterface $blockParser): BlockContinueParserInterface { $blockParser->getBlock()->setStartLine($this->lineNumber); while (! $this->getActiveBlockParser()->canContain($blockParser->getBlock())) { $this->closeBlockParsers(1, $this->lineNumber - 1); } $this->getActiveBlockParser()->getBlock()->appendChild($blockParser->getBlock()); $this->activateBlockParser($blockParser); return $blockParser; } private function activateBlockParser(BlockContinueParserInterface $blockParser): void { $this->activeBlockParsers[] = $blockParser; } /** * @throws ParserLogicException */ private function deactivateBlockParser(): BlockContinueParserInterface { $popped = \array_pop($this->activeBlockParsers); if ($popped === null) { throw new ParserLogicException('The last block parser should not be deactivated'); } return $popped; } private function prepareActiveBlockParserForReplacement(): void { // Note that we don't want to parse inlines or finalize this block, as it's getting replaced. $old = $this->deactivateBlockParser(); if ($old instanceof ParagraphParser) { $this->updateReferenceMap($old->getReferences()); } $old->getBlock()->detach(); } /** * @param ReferenceInterface[] $references */ private function updateReferenceMap(iterable $references): void { foreach ($references as $reference) { if (! $this->referenceMap->contains($reference->getLabel())) { $this->referenceMap->add($reference); } } } /** * @throws ParserLogicException */ public function getActiveBlockParser(): BlockContinueParserInterface { $active = \end($this->activeBlockParsers); if ($active === false) { throw new ParserLogicException('No active block parsers are available'); } return $active; } } commonmark/src/Parser/InlineParserContext.php 0000644 00000005466 15007532371 0015421 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Delimiter\DelimiterStack; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Reference\ReferenceMapInterface; final class InlineParserContext { /** @psalm-readonly */ private AbstractBlock $container; /** @psalm-readonly */ private ReferenceMapInterface $referenceMap; /** @psalm-readonly */ private Cursor $cursor; /** @psalm-readonly */ private DelimiterStack $delimiterStack; /** * @var string[] * @psalm-var non-empty-array<string> * * @psalm-readonly-allow-private-mutation */ private array $matches; public function __construct(Cursor $contents, AbstractBlock $container, ReferenceMapInterface $referenceMap) { $this->referenceMap = $referenceMap; $this->container = $container; $this->cursor = $contents; $this->delimiterStack = new DelimiterStack(); } public function getContainer(): AbstractBlock { return $this->container; } public function getReferenceMap(): ReferenceMapInterface { return $this->referenceMap; } public function getCursor(): Cursor { return $this->cursor; } public function getDelimiterStack(): DelimiterStack { return $this->delimiterStack; } /** * @return string The full text that matched the InlineParserMatch definition */ public function getFullMatch(): string { return $this->matches[0]; } /** * @return int The length of the full match (in characters, not bytes) */ public function getFullMatchLength(): int { return \mb_strlen($this->matches[0], 'UTF-8'); } /** * @return string[] Similar to preg_match(), index 0 will contain the full match, and any other array elements will be captured sub-matches * * @psalm-return non-empty-array<string> */ public function getMatches(): array { return $this->matches; } /** * @return string[] */ public function getSubMatches(): array { return \array_slice($this->matches, 1); } /** * @param string[] $matches * * @psalm-param non-empty-array<string> $matches */ public function withMatches(array $matches): InlineParserContext { $ctx = clone $this; $ctx->matches = $matches; return $ctx; } } commonmark/src/Parser/MarkdownParserInterface.php 0000644 00000001043 15007532371 0016224 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Exception\CommonMarkException; use League\CommonMark\Node\Block\Document; interface MarkdownParserInterface { /** * @throws CommonMarkException */ public function parse(string $input): Document; } commonmark/src/Parser/InlineParserEngine.php 0000644 00000015005 15007532371 0015170 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Node\Inline\AdjacentTextMerger; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Reference\ReferenceMapInterface; /** * @internal */ final class InlineParserEngine implements InlineParserEngineInterface { /** @psalm-readonly */ private EnvironmentInterface $environment; /** @psalm-readonly */ private ReferenceMapInterface $referenceMap; /** * @var array<int, InlineParserInterface|string|bool> * @psalm-var list<array{0: InlineParserInterface, 1: non-empty-string, 2: bool}> * @phpstan-var array<int, array{0: InlineParserInterface, 1: non-empty-string, 2: bool}> */ private array $parsers = []; public function __construct(EnvironmentInterface $environment, ReferenceMapInterface $referenceMap) { $this->environment = $environment; $this->referenceMap = $referenceMap; foreach ($environment->getInlineParsers() as $parser) { \assert($parser instanceof InlineParserInterface); $regex = $parser->getMatchDefinition()->getRegex(); $this->parsers[] = [$parser, $regex, \strlen($regex) !== \mb_strlen($regex, 'UTF-8')]; } } public function parse(string $contents, AbstractBlock $block): void { $contents = \trim($contents); $cursor = new Cursor($contents); $inlineParserContext = new InlineParserContext($cursor, $block, $this->referenceMap); // Have all parsers look at the line to determine what they might want to parse and what positions they exist at foreach ($this->matchParsers($contents) as $matchPosition => $parsers) { $currentPosition = $cursor->getPosition(); // We've already gone past this point if ($currentPosition > $matchPosition) { continue; } // We've skipped over some uninteresting text that should be added as a plain text node if ($currentPosition < $matchPosition) { $cursor->advanceBy($matchPosition - $currentPosition); $this->addPlainText($cursor->getPreviousText(), $block); } // We're now at a potential start - see which of the current parsers can handle it $parsed = false; foreach ($parsers as [$parser, $matches]) { \assert($parser instanceof InlineParserInterface); if ($parser->parse($inlineParserContext->withMatches($matches))) { // A parser has successfully handled the text at the given position; don't consider any others at this position $parsed = true; break; } } if ($parsed) { continue; } // Despite potentially being interested, nothing actually parsed text here, so add the current character and continue onwards $this->addPlainText((string) $cursor->getCurrentCharacter(), $block); $cursor->advance(); } // Add any remaining text that wasn't parsed if (! $cursor->isAtEnd()) { $this->addPlainText($cursor->getRemainder(), $block); } // Process any delimiters that were found $delimiterStack = $inlineParserContext->getDelimiterStack(); $delimiterStack->processDelimiters(null, $this->environment->getDelimiterProcessors()); $delimiterStack->removeAll(); // Combine adjacent text notes into one AdjacentTextMerger::mergeChildNodes($block); } private function addPlainText(string $text, AbstractBlock $container): void { $lastInline = $container->lastChild(); if ($lastInline instanceof Text && ! $lastInline->data->has('delim')) { $lastInline->append($text); } else { $container->appendChild(new Text($text)); } } /** * Given the current line, ask all the parsers which parts of the text they would be interested in parsing. * * The resulting array provides a list of character positions, which parsers are interested in trying to parse * the text at those points, and (for convenience/optimization) what the matching text happened to be. * * @return array<array<int, InlineParserInterface|string>> * * @psalm-return array<int, list<array{0: InlineParserInterface, 1: non-empty-array<string>}>> * * @phpstan-return array<int, array<int, array{0: InlineParserInterface, 1: non-empty-array<string>}>> */ private function matchParsers(string $contents): array { $contents = \trim($contents); $isMultibyte = ! \mb_check_encoding($contents, 'ASCII'); $ret = []; foreach ($this->parsers as [$parser, $regex, $isRegexMultibyte]) { if ($isMultibyte || $isRegexMultibyte) { $regex .= 'u'; } // See if the parser's InlineParserMatch regex matched against any part of the string if (! \preg_match_all($regex, $contents, $matches, \PREG_OFFSET_CAPTURE | \PREG_SET_ORDER)) { continue; } // For each part that matched... foreach ($matches as $match) { if ($isMultibyte) { // PREG_OFFSET_CAPTURE always returns the byte offset, not the char offset, which is annoying $offset = \mb_strlen(\substr($contents, 0, $match[0][1]), 'UTF-8'); } else { $offset = \intval($match[0][1]); } // Remove the offsets, keeping only the matched text $m = \array_column($match, 0); if ($m === []) { continue; } // Add this match to the list of character positions to stop at $ret[$offset][] = [$parser, $m]; } } // Sort matches by position so we visit them in order \ksort($ret); return $ret; } } commonmark/src/Parser/CursorState.php 0000644 00000002150 15007532371 0013722 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; /** * Encapsulates the current state of a cursor in case you need to rollback later. * * WARNING: Do not attempt to use this class for ANYTHING except for * type hinting and passing this object back into restoreState(). * The constructor, methods, and inner contents may change in any * future release without warning! * * @internal * * @psalm-immutable */ final class CursorState { /** * @var array<int, mixed> * * @psalm-readonly */ private array $state; /** * @internal * * @param array<int, mixed> $state */ public function __construct(array $state) { $this->state = $state; } /** * @internal * * @return array<int, mixed> */ public function toArray(): array { return $this->state; } } commonmark/src/Parser/MarkdownParserStateInterface.php 0000644 00000002064 15007532371 0017231 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Parser; use League\CommonMark\Parser\Block\BlockContinueParserInterface; interface MarkdownParserStateInterface { /** * Returns the deepest open block parser */ public function getActiveBlockParser(): BlockContinueParserInterface; /** * Open block parser that was last matched during the continue phase. This is different from the currently active * block parser, as an unmatched block is only closed when a new block is started. */ public function getLastMatchedBlockParser(): BlockContinueParserInterface; /** * Returns the current content of the paragraph if the matched block is a paragraph. The content can be multiple * lines separated by newlines. */ public function getParagraphContent(): ?string; } commonmark/src/Renderer/NoMatchingRendererException.php 0000644 00000000645 15007532371 0017362 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Renderer; use League\CommonMark\Exception\LogicException; class NoMatchingRendererException extends LogicException { } commonmark/src/Renderer/Inline/NewlineRenderer.php 0000644 00000003734 15007532371 0016275 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Renderer\Inline; use League\CommonMark\Node\Inline\Newline; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Xml\XmlNodeRendererInterface; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class NewlineRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface { /** @psalm-readonly-allow-private-mutation */ private ConfigurationInterface $config; public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } /** * @param Newline $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): string { Newline::assertInstanceOf($node); if ($node->getType() === Newline::HARDBREAK) { return "<br />\n"; } return $this->config->get('renderer/soft_break'); } /** * @param Newline $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlTagName(Node $node): string { Newline::assertInstanceOf($node); return $node->getType() === Newline::SOFTBREAK ? 'softbreak' : 'linebreak'; } /** * {@inheritDoc} */ public function getXmlAttributes(Node $node): array { return []; } } commonmark/src/Renderer/Inline/TextRenderer.php 0000644 00000002473 15007532371 0015617 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Renderer\Inline; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\Xml; use League\CommonMark\Xml\XmlNodeRendererInterface; final class TextRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param Text $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): string { Text::assertInstanceOf($node); return Xml::escape($node->getLiteral()); } public function getXmlTagName(Node $node): string { return 'text'; } /** * {@inheritDoc} */ public function getXmlAttributes(Node $node): array { return []; } } commonmark/src/Renderer/ChildNodeRendererInterface.php 0000644 00000001275 15007532371 0017126 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Renderer; use League\CommonMark\Node\Node; /** * Renders multiple nodes by delegating to the individual node renderers and adding spacing where needed */ interface ChildNodeRendererInterface { /** * @param Node[] $nodes */ public function renderNodes(iterable $nodes): string; public function getBlockSeparator(): string; public function getInnerSeparator(): string; } commonmark/src/Renderer/DocumentRendererInterface.php 0000644 00000001257 15007532371 0017053 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Renderer; use League\CommonMark\Node\Block\Document; use League\CommonMark\Output\RenderedContentInterface; /** * Renders a parsed Document AST */ interface DocumentRendererInterface extends MarkdownRendererInterface { /** * Render the given Document node (and all of its children) */ public function renderDocument(Document $document): RenderedContentInterface; } commonmark/src/Renderer/NodeRendererInterface.php 0000644 00000001226 15007532371 0016156 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Renderer; use League\CommonMark\Exception\InvalidArgumentException; use League\CommonMark\Node\Node; interface NodeRendererInterface { /** * @return \Stringable|string|null * * @throws InvalidArgumentException if the wrong type of Node is provided */ public function render(Node $node, ChildNodeRendererInterface $childRenderer); } commonmark/src/Renderer/Block/DocumentRenderer.php 0000644 00000002673 15007532371 0016267 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Renderer\Block; use League\CommonMark\Node\Block\Document; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Xml\XmlNodeRendererInterface; final class DocumentRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param Document $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): string { Document::assertInstanceOf($node); $wholeDoc = $childRenderer->renderNodes($node->children()); return $wholeDoc === '' ? '' : $wholeDoc . "\n"; } public function getXmlTagName(Node $node): string { return 'document'; } /** * {@inheritDoc} */ public function getXmlAttributes(Node $node): array { return [ 'xmlns' => 'http://commonmark.org/xml/1.0', ]; } } commonmark/src/Renderer/Block/ParagraphRenderer.php 0000644 00000003672 15007532371 0016416 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Renderer\Block; use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Node\Block\TightBlockInterface; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; final class ParagraphRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param Paragraph $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer) { Paragraph::assertInstanceOf($node); if ($this->inTightList($node)) { return $childRenderer->renderNodes($node->children()); } $attrs = $node->data->get('attributes'); return new HtmlElement('p', $attrs, $childRenderer->renderNodes($node->children())); } public function getXmlTagName(Node $node): string { return 'paragraph'; } /** * {@inheritDoc} */ public function getXmlAttributes(Node $node): array { return []; } private function inTightList(Paragraph $node): bool { // Only check up to two (2) levels above this for tightness $i = 2; while (($node = $node->parent()) && $i--) { if ($node instanceof TightBlockInterface) { return $node->isTight(); } } return false; } } commonmark/src/Renderer/MarkdownRendererInterface.php 0000644 00000001330 15007532371 0017047 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Renderer; use League\CommonMark\Node\Block\Document; use League\CommonMark\Output\RenderedContentInterface; /** * Renders a parsed Document AST * * @deprecated since 2.3; use {@link DocumentRendererInterface} instead */ interface MarkdownRendererInterface { /** * Render the given Document node (and all of its children) */ public function renderDocument(Document $document): RenderedContentInterface; } commonmark/src/Renderer/HtmlRenderer.php 0000644 00000005465 15007532371 0014365 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Renderer; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Event\DocumentPreRenderEvent; use League\CommonMark\Event\DocumentRenderedEvent; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Node\Block\Document; use League\CommonMark\Node\Node; use League\CommonMark\Output\RenderedContent; use League\CommonMark\Output\RenderedContentInterface; final class HtmlRenderer implements DocumentRendererInterface, ChildNodeRendererInterface { /** @psalm-readonly */ private EnvironmentInterface $environment; public function __construct(EnvironmentInterface $environment) { $this->environment = $environment; } public function renderDocument(Document $document): RenderedContentInterface { $this->environment->dispatch(new DocumentPreRenderEvent($document, 'html')); $output = new RenderedContent($document, (string) $this->renderNode($document)); $event = new DocumentRenderedEvent($output); $this->environment->dispatch($event); return $event->getOutput(); } /** * {@inheritDoc} */ public function renderNodes(iterable $nodes): string { $output = ''; $isFirstItem = true; foreach ($nodes as $node) { if (! $isFirstItem && $node instanceof AbstractBlock) { $output .= $this->getBlockSeparator(); } $output .= $this->renderNode($node); $isFirstItem = false; } return $output; } /** * @return \Stringable|string * * @throws NoMatchingRendererException */ private function renderNode(Node $node) { $renderers = $this->environment->getRenderersForClass(\get_class($node)); foreach ($renderers as $renderer) { \assert($renderer instanceof NodeRendererInterface); if (($result = $renderer->render($node, $this)) !== null) { return $result; } } throw new NoMatchingRendererException('Unable to find corresponding renderer for node type ' . \get_class($node)); } public function getBlockSeparator(): string { return $this->environment->getConfiguration()->get('renderer/block_separator'); } public function getInnerSeparator(): string { return $this->environment->getConfiguration()->get('renderer/inner_separator'); } } commonmark/src/Renderer/HtmlDecorator.php 0000644 00000002357 15007532371 0014536 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Renderer; use League\CommonMark\Node\Node; use League\CommonMark\Util\HtmlElement; final class HtmlDecorator implements NodeRendererInterface { private NodeRendererInterface $inner; private string $tag; /** @var array<string, string|string[]|bool> */ private array $attributes; private bool $selfClosing; /** * @param array<string, string|string[]|bool> $attributes */ public function __construct(NodeRendererInterface $inner, string $tag, array $attributes = [], bool $selfClosing = false) { $this->inner = $inner; $this->tag = $tag; $this->attributes = $attributes; $this->selfClosing = $selfClosing; } /** * {@inheritDoc} */ public function render(Node $node, ChildNodeRendererInterface $childRenderer) { return new HtmlElement($this->tag, $this->attributes, $this->inner->render($node, $childRenderer), $this->selfClosing); } } commonmark/src/Event/DocumentRenderedEvent.php 0000644 00000001604 15007532371 0015525 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Event; use League\CommonMark\Output\RenderedContentInterface; final class DocumentRenderedEvent extends AbstractEvent { private RenderedContentInterface $output; public function __construct(RenderedContentInterface $output) { $this->output = $output; } /** * @psalm-mutation-free */ public function getOutput(): RenderedContentInterface { return $this->output; } /** * @psalm-external-mutation-free */ public function replaceOutput(RenderedContentInterface $output): void { $this->output = $output; } } commonmark/src/Event/AbstractEvent.php 0000644 00000002752 15007532371 0014046 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the Symfony EventDispatcher "Event" contract * - (c) 2018-2019 Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Event; use Psr\EventDispatcher\StoppableEventInterface; /** * Base class for classes containing event data. * * This class contains no event data. It is used by events that do not pass * state information to an event handler when an event is raised. * * You can call the method stopPropagation() to abort the execution of * further listeners in your event listener. */ abstract class AbstractEvent implements StoppableEventInterface { /** @psalm-readonly-allow-private-mutation */ private bool $propagationStopped = false; /** * Returns whether further event listeners should be triggered. */ final public function isPropagationStopped(): bool { return $this->propagationStopped; } /** * Stops the propagation of the event to further event listeners. * * If multiple event listeners are connected to the same event, no * further event listener will be triggered once any trigger calls * stopPropagation(). */ final public function stopPropagation(): void { $this->propagationStopped = true; } } commonmark/src/Event/DocumentPreParsedEvent.php 0000644 00000002164 15007532371 0015664 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Event; use League\CommonMark\Input\MarkdownInputInterface; use League\CommonMark\Node\Block\Document; /** * Event dispatched when the document is about to be parsed */ final class DocumentPreParsedEvent extends AbstractEvent { /** @psalm-readonly */ private Document $document; private MarkdownInputInterface $markdown; public function __construct(Document $document, MarkdownInputInterface $markdown) { $this->document = $document; $this->markdown = $markdown; } public function getDocument(): Document { return $this->document; } public function getMarkdown(): MarkdownInputInterface { return $this->markdown; } public function replaceMarkdown(MarkdownInputInterface $markdownInput): void { $this->markdown = $markdownInput; } } commonmark/src/Event/ListenerData.php 0000644 00000001605 15007532371 0013654 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Event; /** * @internal * * @psalm-immutable */ final class ListenerData { /** @var class-string */ private string $event; /** @var callable */ private $listener; /** * @param class-string $event */ public function __construct(string $event, callable $listener) { $this->event = $event; $this->listener = $listener; } /** * @return class-string */ public function getEvent(): string { return $this->event; } public function getListener(): callable { return $this->listener; } } commonmark/src/Event/DocumentPreRenderEvent.php 0000644 00000001616 15007532371 0015666 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Event; use League\CommonMark\Node\Block\Document; /** * Event dispatched just before rendering begins */ final class DocumentPreRenderEvent extends AbstractEvent { /** @psalm-readonly */ private Document $document; /** @psalm-readonly */ private string $format; public function __construct(Document $document, string $format) { $this->document = $document; $this->format = $format; } public function getDocument(): Document { return $this->document; } public function getFormat(): string { return $this->format; } } commonmark/src/Event/DocumentParsedEvent.php 0000644 00000001330 15007532371 0015207 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Event; use League\CommonMark\Node\Block\Document; /** * Event dispatched when the document has been fully parsed */ final class DocumentParsedEvent extends AbstractEvent { /** @psalm-readonly */ private Document $document; public function __construct(Document $document) { $this->document = $document; } public function getDocument(): Document { return $this->document; } } commonmark/src/GithubFlavoredMarkdownConverter.php 0000644 00000002255 15007532371 0016516 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark; use League\CommonMark\Environment\Environment; use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; use League\CommonMark\Extension\GithubFlavoredMarkdownExtension; /** * Converts GitHub Flavored Markdown to HTML. */ final class GithubFlavoredMarkdownConverter extends MarkdownConverter { /** * Create a new Markdown converter pre-configured for GFM * * @param array<string, mixed> $config */ public function __construct(array $config = []) { $environment = new Environment($config); $environment->addExtension(new CommonMarkCoreExtension()); $environment->addExtension(new GithubFlavoredMarkdownExtension()); parent::__construct($environment); } public function getEnvironment(): Environment { \assert($this->environment instanceof Environment); return $this->environment; } } commonmark/src/ConverterInterface.php 0000644 00000001423 15007532371 0014002 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark; use League\CommonMark\Exception\CommonMarkException; use League\CommonMark\Output\RenderedContentInterface; use League\Config\Exception\ConfigurationExceptionInterface; /** * Interface for a service which converts content from one format (like Markdown) to another (like HTML). */ interface ConverterInterface { /** * @throws CommonMarkException * @throws ConfigurationExceptionInterface */ public function convert(string $input): RenderedContentInterface; } commonmark/src/Normalizer/SlugNormalizer.php 0000644 00000003232 15007532371 0015311 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Normalizer; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; /** * Creates URL-friendly strings based on the given string input */ final class SlugNormalizer implements TextNormalizerInterface, ConfigurationAwareInterface { /** @psalm-allow-private-mutation */ private int $defaultMaxLength = 255; public function setConfiguration(ConfigurationInterface $configuration): void { $this->defaultMaxLength = $configuration->get('slug_normalizer/max_length'); } /** * {@inheritDoc} * * @psalm-immutable */ public function normalize(string $text, array $context = []): string { // Add any requested prefix $slug = ($context['prefix'] ?? '') . $text; // Trim whitespace $slug = \trim($slug); // Convert to lowercase $slug = \mb_strtolower($slug, 'UTF-8'); // Try replacing whitespace with a dash $slug = \preg_replace('/\s+/u', '-', $slug) ?? $slug; // Try removing characters other than letters, numbers, and marks. $slug = \preg_replace('/[^\p{L}\p{Nd}\p{Nl}\p{M}-]+/u', '', $slug) ?? $slug; // Trim to requested length if given if ($length = $context['length'] ?? $this->defaultMaxLength) { $slug = \mb_substr($slug, 0, $length, 'UTF-8'); } return $slug; } } commonmark/src/Normalizer/TextNormalizer.php 0000644 00000001716 15007532371 0015330 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Normalizer; /*** * Normalize text input using the steps given by the CommonMark spec to normalize labels * * @see https://spec.commonmark.org/0.29/#matches * * @psalm-immutable */ final class TextNormalizer implements TextNormalizerInterface { /** * {@inheritDoc} * * @psalm-pure */ public function normalize(string $text, array $context = []): string { // Collapse internal whitespace to single space and remove // leading/trailing whitespace $text = \preg_replace('/[ \t\r\n]+/', ' ', \trim($text)); \assert(\is_string($text)); return \mb_convert_case($text, \MB_CASE_FOLD, 'UTF-8'); } } commonmark/src/Normalizer/UniqueSlugNormalizer.php 0000644 00000002734 15007532371 0016506 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Normalizer; // phpcs:disable Squiz.Strings.DoubleQuoteUsage.ContainsVar final class UniqueSlugNormalizer implements UniqueSlugNormalizerInterface { private TextNormalizerInterface $innerNormalizer; /** @var array<string, bool> */ private array $alreadyUsed = []; public function __construct(TextNormalizerInterface $innerNormalizer) { $this->innerNormalizer = $innerNormalizer; } public function clearHistory(): void { $this->alreadyUsed = []; } /** * {@inheritDoc} * * @psalm-allow-private-mutation */ public function normalize(string $text, array $context = []): string { $normalized = $this->innerNormalizer->normalize($text, $context); // If it's not unique, add an incremental number to the end until we get a unique version if (\array_key_exists($normalized, $this->alreadyUsed)) { $suffix = 0; do { ++$suffix; } while (\array_key_exists("$normalized-$suffix", $this->alreadyUsed)); $normalized = "$normalized-$suffix"; } $this->alreadyUsed[$normalized] = true; return $normalized; } } commonmark/src/Normalizer/UniqueSlugNormalizerInterface.php 0000644 00000001322 15007532371 0020317 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Normalizer; interface UniqueSlugNormalizerInterface extends TextNormalizerInterface { public const DISABLED = false; public const PER_ENVIRONMENT = 'environment'; public const PER_DOCUMENT = 'document'; /** * Called by the Environment whenever the configured scope changes * * Currently, this will only be called PER_DOCUMENT. */ public function clearHistory(): void; } commonmark/src/Normalizer/TextNormalizerInterface.php 0000644 00000002017 15007532371 0017144 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Normalizer; /** * Creates a normalized version of the given input text */ interface TextNormalizerInterface { /** * @param string $text The text to normalize * @param array<string, mixed> $context Additional context about the text being normalized (optional) * * $context may include (but is not required to include) the following: * - `prefix` - A string prefix to prepend to each normalized result * - `length` - The requested maximum length * - `node` - The node we're normalizing text for * * Implementations do not have to use or respect any information within that $context */ public function normalize(string $text, array $context = []): string; } commonmark/src/Xml/MarkdownToXmlConverter.php 0000644 00000003104 15007532371 0015406 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Xml; use League\CommonMark\ConverterInterface; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Exception\CommonMarkException; use League\CommonMark\Output\RenderedContentInterface; use League\CommonMark\Parser\MarkdownParser; use League\CommonMark\Parser\MarkdownParserInterface; use League\CommonMark\Renderer\DocumentRendererInterface; final class MarkdownToXmlConverter implements ConverterInterface { /** @psalm-readonly */ private MarkdownParserInterface $parser; /** @psalm-readonly */ private DocumentRendererInterface $renderer; public function __construct(EnvironmentInterface $environment) { $this->parser = new MarkdownParser($environment); $this->renderer = new XmlRenderer($environment); } /** * Converts Markdown to XML * * @throws CommonMarkException */ public function convert(string $input): RenderedContentInterface { return $this->renderer->renderDocument($this->parser->parse($input)); } /** * Converts CommonMark to HTML. * * @see MarkdownToXmlConverter::convert() * * @throws CommonMarkException */ public function __invoke(string $input): RenderedContentInterface { return $this->convert($input); } } commonmark/src/Xml/FallbackNodeXmlRenderer.php 0000644 00000004256 15007532371 0015436 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Xml; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Node\Inline\AbstractInline; use League\CommonMark\Node\Node; /** * @internal */ final class FallbackNodeXmlRenderer implements XmlNodeRendererInterface { /** * @var array<string, string> * * @psalm-allow-private-mutation */ private array $classCache = []; /** * @psalm-allow-private-mutation */ public function getXmlTagName(Node $node): string { $className = \get_class($node); if (isset($this->classCache[$className])) { return $this->classCache[$className]; } $type = $node instanceof AbstractBlock ? 'block' : 'inline'; $shortName = \strtolower((new \ReflectionClass($node))->getShortName()); return $this->classCache[$className] = \sprintf('custom_%s_%s', $type, $shortName); } /** * {@inheritDoc} */ public function getXmlAttributes(Node $node): array { $attrs = []; foreach ($node->data->export() as $k => $v) { if (self::isValueUsable($v)) { $attrs[$k] = $v; } } $reflClass = new \ReflectionClass($node); foreach ($reflClass->getProperties() as $property) { if (\in_array($property->getDeclaringClass()->getName(), [Node::class, AbstractBlock::class, AbstractInline::class], true)) { continue; } $property->setAccessible(true); $value = $property->getValue($node); if (self::isValueUsable($value)) { $attrs[$property->getName()] = $value; } } return $attrs; } /** * @param mixed $var * * @psalm-pure */ private static function isValueUsable($var): bool { return \is_string($var) || \is_int($var) || \is_float($var) || \is_bool($var); } } commonmark/src/Xml/XmlNodeRendererInterface.php 0000644 00000001142 15007532371 0015626 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Xml; use League\CommonMark\Node\Node; interface XmlNodeRendererInterface { public function getXmlTagName(Node $node): string; /** * @return array<string, string|int|float|bool> * * @psalm-return array<string, scalar> */ public function getXmlAttributes(Node $node): array; } commonmark/src/Xml/XmlRenderer.php 0000644 00000010106 15007532371 0013177 0 ustar 00 <?php declare(strict_types=1); namespace League\CommonMark\Xml; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Event\DocumentPreRenderEvent; use League\CommonMark\Exception\InvalidArgumentException; use League\CommonMark\Node\Block\Document; use League\CommonMark\Node\Node; use League\CommonMark\Node\StringContainerInterface; use League\CommonMark\Output\RenderedContent; use League\CommonMark\Output\RenderedContentInterface; use League\CommonMark\Renderer\DocumentRendererInterface; use League\CommonMark\Util\Xml; final class XmlRenderer implements DocumentRendererInterface { private const INDENTATION = ' '; private EnvironmentInterface $environment; private XmlNodeRendererInterface $fallbackRenderer; /** @var array<class-string, XmlNodeRendererInterface> */ private array $rendererCache = []; public function __construct(EnvironmentInterface $environment) { $this->environment = $environment; $this->fallbackRenderer = new FallbackNodeXmlRenderer(); } public function renderDocument(Document $document): RenderedContentInterface { $this->environment->dispatch(new DocumentPreRenderEvent($document, 'xml')); $xml = '<?xml version="1.0" encoding="UTF-8"?>'; $indent = 0; $walker = $document->walker(); while ($event = $walker->next()) { $node = $event->getNode(); $closeImmediately = ! $node->hasChildren(); $selfClosing = $closeImmediately && ! $node instanceof StringContainerInterface; $renderer = $this->findXmlRenderer($node); $tagName = $renderer->getXmlTagName($node); if ($event->isEntering()) { $attrs = $renderer->getXmlAttributes($node); $xml .= "\n" . \str_repeat(self::INDENTATION, $indent); $xml .= self::tag($tagName, $attrs, $selfClosing); if ($node instanceof StringContainerInterface) { $xml .= Xml::escape($node->getLiteral()); } if ($closeImmediately && ! $selfClosing) { $xml .= self::tag('/' . $tagName); } if (! $closeImmediately) { $indent++; } } elseif (! $closeImmediately) { $indent--; $xml .= "\n" . \str_repeat(self::INDENTATION, $indent); $xml .= self::tag('/' . $tagName); } } return new RenderedContent($document, $xml . "\n"); } /** * @param array<string, string|int|float|bool> $attrs */ private static function tag(string $name, array $attrs = [], bool $selfClosing = \false): string { $result = '<' . $name; foreach ($attrs as $key => $value) { $result .= \sprintf(' %s="%s"', $key, self::convertAndEscape($value)); } if ($selfClosing) { $result .= ' /'; } $result .= '>'; return $result; } /** * @param string|int|float|bool $value */ private static function convertAndEscape($value): string { if (\is_string($value)) { return Xml::escape($value); } if (\is_int($value) || \is_float($value)) { return (string) $value; } if (\is_bool($value)) { return $value ? 'true' : 'false'; } // @phpstan-ignore-next-line throw new InvalidArgumentException('$value must be a string, int, float, or bool'); } private function findXmlRenderer(Node $node): XmlNodeRendererInterface { $class = \get_class($node); if (\array_key_exists($class, $this->rendererCache)) { return $this->rendererCache[$class]; } foreach ($this->environment->getRenderersForClass($class) as $renderer) { if ($renderer instanceof XmlNodeRendererInterface) { return $this->rendererCache[$class] = $renderer; } } return $this->rendererCache[$class] = $this->fallbackRenderer; } } commonmark/src/Extension/Embed/Bridge/OscaroteroEmbedAdapter.php 0000644 00000002636 15007532371 0020763 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Embed\Bridge; use Embed\Embed as EmbedLib; use League\CommonMark\Exception\MissingDependencyException; use League\CommonMark\Extension\Embed\Embed; use League\CommonMark\Extension\Embed\EmbedAdapterInterface; final class OscaroteroEmbedAdapter implements EmbedAdapterInterface { private EmbedLib $embedLib; public function __construct(?EmbedLib $embed = null) { if ($embed === null) { if (! \class_exists(EmbedLib::class)) { throw new MissingDependencyException('The embed/embed package is not installed. Please install it with Composer to use this adapter.'); } $embed = new EmbedLib(); } $this->embedLib = $embed; } /** * {@inheritDoc} */ public function updateEmbeds(array $embeds): void { $extractors = $this->embedLib->getMulti(...\array_map(static fn (Embed $embed) => $embed->getUrl(), $embeds)); foreach ($extractors as $i => $extractor) { if ($extractor->code !== null) { $embeds[$i]->setEmbedCode($extractor->code->html); } } } } commonmark/src/Extension/Embed/EmbedProcessor.php 0000644 00000004025 15007532371 0016117 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Embed; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\CommonMark\Node\Inline\Link; use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Node\NodeIterator; final class EmbedProcessor { public const FALLBACK_REMOVE = 'remove'; public const FALLBACK_LINK = 'link'; private EmbedAdapterInterface $adapter; private string $fallback; public function __construct(EmbedAdapterInterface $adapter, string $fallback = self::FALLBACK_REMOVE) { $this->adapter = $adapter; $this->fallback = $fallback; } public function __invoke(DocumentParsedEvent $event): void { $document = $event->getDocument(); $embeds = []; foreach (new NodeIterator($document) as $node) { if (! ($node instanceof Embed)) { continue; } if ($node->parent() !== $document) { $replacement = new Paragraph(); $replacement->appendChild(new Text($node->getUrl())); $node->replaceWith($replacement); } else { $embeds[] = $node; } } $this->adapter->updateEmbeds($embeds); foreach ($embeds as $embed) { if ($embed->getEmbedCode() !== null) { continue; } if ($this->fallback === self::FALLBACK_REMOVE) { $embed->detach(); } elseif ($this->fallback === self::FALLBACK_LINK) { $paragraph = new Paragraph(); $paragraph->appendChild(new Link($embed->getUrl(), $embed->getUrl())); $embed->replaceWith($paragraph); } } } } commonmark/src/Extension/Embed/EmbedRenderer.php 0000644 00000001502 15007532371 0015703 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Embed; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; class EmbedRenderer implements NodeRendererInterface { /** * @param Embed $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer) { Embed::assertInstanceOf($node); return $node->getEmbedCode() ?? ''; } } commonmark/src/Extension/Embed/EmbedParser.php 0000644 00000002470 15007532371 0015376 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Embed; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Cursor; class EmbedParser implements BlockContinueParserInterface { private Embed $embed; public function __construct(string $url) { $this->embed = new Embed($url); } public function getBlock(): AbstractBlock { return $this->embed; } public function isContainer(): bool { return false; } public function canHaveLazyContinuationLines(): bool { return false; } public function canContain(AbstractBlock $childBlock): bool { return false; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { return BlockContinue::none(); } public function addLine(string $line): void { } public function closeBlock(): void { } } commonmark/src/Extension/Embed/DomainFilteringAdapter.php 0000644 00000002564 15007532371 0017565 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Embed; class DomainFilteringAdapter implements EmbedAdapterInterface { private EmbedAdapterInterface $decorated; /** @psalm-var non-empty-string */ private string $regex; /** * @param string[] $allowedDomains */ public function __construct(EmbedAdapterInterface $decorated, array $allowedDomains) { $this->decorated = $decorated; $this->regex = self::createRegex($allowedDomains); } /** * {@inheritDoc} */ public function updateEmbeds(array $embeds): void { $this->decorated->updateEmbeds(\array_values(\array_filter($embeds, function (Embed $embed): bool { return \preg_match($this->regex, $embed->getUrl()) === 1; }))); } /** * @param string[] $allowedDomains * * @psalm-return non-empty-string */ private static function createRegex(array $allowedDomains): string { $allowedDomains = \array_map('preg_quote', $allowedDomains); return '/^(?:https?:\/\/)?(?:[^.]+\.)*(' . \implode('|', $allowedDomains) . ')/'; } } commonmark/src/Extension/Embed/EmbedExtension.php 0000644 00000003345 15007532371 0016120 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Embed; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\ConfigurableExtensionInterface; use League\Config\ConfigurationBuilderInterface; use Nette\Schema\Expect; final class EmbedExtension implements ConfigurableExtensionInterface { public function configureSchema(ConfigurationBuilderInterface $builder): void { $builder->addSchema('embed', Expect::structure([ 'adapter' => Expect::type(EmbedAdapterInterface::class), 'allowed_domains' => Expect::arrayOf('string')->default([]), 'fallback' => Expect::anyOf('link', 'remove')->default('link'), ])); } public function register(EnvironmentBuilderInterface $environment): void { $adapter = $environment->getConfiguration()->get('embed.adapter'); \assert($adapter instanceof EmbedAdapterInterface); $allowedDomains = $environment->getConfiguration()->get('embed.allowed_domains'); if ($allowedDomains !== []) { $adapter = new DomainFilteringAdapter($adapter, $allowedDomains); } $environment ->addBlockStartParser(new EmbedStartParser(), 300) ->addEventListener(DocumentParsedEvent::class, new EmbedProcessor($adapter, $environment->getConfiguration()->get('embed.fallback')), 1010) ->addRenderer(Embed::class, new EmbedRenderer()); } } commonmark/src/Extension/Embed/Embed.php 0000644 00000001775 15007532371 0014230 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Embed; use League\CommonMark\Node\Block\AbstractBlock; final class Embed extends AbstractBlock { private string $url; private ?string $embedCode; public function __construct(string $url, ?string $embedCode = null) { parent::__construct(); $this->url = $url; $this->embedCode = $embedCode; } public function getUrl(): string { return $this->url; } public function setUrl(string $url): void { $this->url = $url; } public function getEmbedCode(): ?string { return $this->embedCode; } public function setEmbedCode(?string $embedCode): void { $this->embedCode = $embedCode; } } commonmark/src/Extension/Embed/EmbedStartParser.php 0000644 00000003240 15007532371 0016410 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Embed; use League\CommonMark\Parser\Block\BlockStart; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; use League\CommonMark\Util\LinkParserHelper; class EmbedStartParser implements BlockStartParserInterface { public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart { if ($cursor->isIndented() || $parserState->getParagraphContent() !== null || ! ($parserState->getActiveBlockParser()->isContainer())) { return BlockStart::none(); } // 0-3 leading spaces are okay $cursor->advanceToNextNonSpaceOrTab(); // The line must begin with "https://" if (! str_starts_with($cursor->getRemainder(), 'https://')) { return BlockStart::none(); } // A valid link must be found next if (($dest = LinkParserHelper::parseLinkDestination($cursor)) === null) { return BlockStart::none(); } // Skip any trailing whitespace $cursor->advanceToNextNonSpaceOrTab(); // We must be at the end of the line; otherwise, this link was not by itself if (! $cursor->isAtEnd()) { return BlockStart::none(); } return BlockStart::of(new EmbedParser($dest))->at($cursor); } } commonmark/src/Extension/Embed/EmbedAdapterInterface.php 0000644 00000001050 15007532371 0017334 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Embed; /** * Interface for a service which updates the embed code(s) for the given array of embeds */ interface EmbedAdapterInterface { /** * @param Embed[] $embeds */ public function updateEmbeds(array $embeds): void; } commonmark/src/Extension/Attributes/Util/AttributesHelper.php 0000644 00000010553 15007532371 0020523 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) 2015 Martin Hasoň <martin.hason@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Attributes\Util; use League\CommonMark\Node\Node; use League\CommonMark\Parser\Cursor; use League\CommonMark\Util\RegexHelper; /** * @internal */ final class AttributesHelper { private const SINGLE_ATTRIBUTE = '\s*([.#][_a-z0-9-]+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . ')\s*'; private const ATTRIBUTE_LIST = '/^{:?(' . self::SINGLE_ATTRIBUTE . ')+}/i'; /** * @return array<string, mixed> */ public static function parseAttributes(Cursor $cursor): array { $state = $cursor->saveState(); $cursor->advanceToNextNonSpaceOrNewline(); // Quick check to see if we might have attributes if ($cursor->getCharacter() !== '{') { $cursor->restoreState($state); return []; } // Attempt to match the entire attribute list expression // While this is less performant than checking for '{' now and '}' later, it simplifies // matching individual attributes since they won't need to look ahead for the closing '}' // while dealing with the fact that attributes can technically contain curly braces. // So we'll just match the start and end braces up front. $attributeExpression = $cursor->match(self::ATTRIBUTE_LIST); if ($attributeExpression === null) { $cursor->restoreState($state); return []; } // Trim the leading '{' or '{:' and the trailing '}' $attributeExpression = \ltrim(\substr($attributeExpression, 1, -1), ':'); $attributeCursor = new Cursor($attributeExpression); /** @var array<string, mixed> $attributes */ $attributes = []; while ($attribute = \trim((string) $attributeCursor->match('/^' . self::SINGLE_ATTRIBUTE . '/i'))) { if ($attribute[0] === '#') { $attributes['id'] = \substr($attribute, 1); continue; } if ($attribute[0] === '.') { $attributes['class'][] = \substr($attribute, 1); continue; } /** @psalm-suppress PossiblyUndefinedArrayOffset */ [$name, $value] = \explode('=', $attribute, 2); $first = $value[0]; $last = \substr($value, -1); if (($first === '"' && $last === '"') || ($first === "'" && $last === "'") && \strlen($value) > 1) { $value = \substr($value, 1, -1); } if (\strtolower(\trim($name)) === 'class') { foreach (\array_filter(\explode(' ', \trim($value))) as $class) { $attributes['class'][] = $class; } } else { $attributes[\trim($name)] = \trim($value); } } if (isset($attributes['class'])) { $attributes['class'] = \implode(' ', (array) $attributes['class']); } return $attributes; } /** * @param Node|array<string, mixed> $attributes1 * @param Node|array<string, mixed> $attributes2 * * @return array<string, mixed> */ public static function mergeAttributes($attributes1, $attributes2): array { $attributes = []; foreach ([$attributes1, $attributes2] as $arg) { if ($arg instanceof Node) { $arg = $arg->data->get('attributes'); } /** @var array<string, mixed> $arg */ $arg = (array) $arg; if (isset($arg['class'])) { if (\is_string($arg['class'])) { $arg['class'] = \array_filter(\explode(' ', \trim($arg['class']))); } foreach ($arg['class'] as $class) { $attributes['class'][] = $class; } unset($arg['class']); } $attributes = \array_merge($attributes, $arg); } if (isset($attributes['class'])) { $attributes['class'] = \implode(' ', $attributes['class']); } return $attributes; } } commonmark/src/Extension/Attributes/Node/Attributes.php 0000644 00000002567 15007532371 0017341 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) 2015 Martin Hasoň <martin.hason@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Attributes\Node; use League\CommonMark\Node\Block\AbstractBlock; final class Attributes extends AbstractBlock { public const TARGET_PARENT = 0; public const TARGET_PREVIOUS = 1; public const TARGET_NEXT = 2; /** @var array<string, mixed> */ private array $attributes; private int $target = self::TARGET_NEXT; /** * @param array<string, mixed> $attributes */ public function __construct(array $attributes) { parent::__construct(); $this->attributes = $attributes; } /** * @return array<string, mixed> */ public function getAttributes(): array { return $this->attributes; } /** * @param array<string, mixed> $attributes */ public function setAttributes(array $attributes): void { $this->attributes = $attributes; } public function getTarget(): int { return $this->target; } public function setTarget(int $target): void { $this->target = $target; } } commonmark/src/Extension/Attributes/Node/AttributesInline.php 0000644 00000002311 15007532371 0020463 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) 2015 Martin Hasoň <martin.hason@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Attributes\Node; use League\CommonMark\Node\Inline\AbstractInline; final class AttributesInline extends AbstractInline { /** @var array<string, mixed> */ private array $attributes; private bool $block; /** * @param array<string, mixed> $attributes */ public function __construct(array $attributes, bool $block) { parent::__construct(); $this->attributes = $attributes; $this->block = $block; } /** * @return array<string, mixed> */ public function getAttributes(): array { return $this->attributes; } /** * @param array<string, mixed> $attributes */ public function setAttributes(array $attributes): void { $this->attributes = $attributes; } public function isBlock(): bool { return $this->block; } } commonmark/src/Extension/Attributes/AttributesExtension.php 0000644 00000002230 15007532371 0020334 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) 2015 Martin Hasoň <martin.hason@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Attributes; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\Attributes\Event\AttributesListener; use League\CommonMark\Extension\Attributes\Parser\AttributesBlockStartParser; use League\CommonMark\Extension\Attributes\Parser\AttributesInlineParser; use League\CommonMark\Extension\ExtensionInterface; final class AttributesExtension implements ExtensionInterface { public function register(EnvironmentBuilderInterface $environment): void { $environment->addBlockStartParser(new AttributesBlockStartParser()); $environment->addInlineParser(new AttributesInlineParser()); $environment->addEventListener(DocumentParsedEvent::class, [new AttributesListener(), 'processDocument']); } } commonmark/src/Extension/Attributes/Parser/AttributesBlockContinueParser.php 0000644 00000006271 15007532371 0023541 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) 2015 Martin Hasoň <martin.hason@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Attributes\Parser; use League\CommonMark\Extension\Attributes\Node\Attributes; use League\CommonMark\Extension\Attributes\Util\AttributesHelper; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Parser\Block\AbstractBlockContinueParser; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Cursor; final class AttributesBlockContinueParser extends AbstractBlockContinueParser { private Attributes $block; private AbstractBlock $container; private bool $hasSubsequentLine = false; /** * @param array<string, mixed> $attributes The attributes identified by the block start parser * @param AbstractBlock $container The node we were in when these attributes were discovered */ public function __construct(array $attributes, AbstractBlock $container) { $this->block = new Attributes($attributes); $this->container = $container; } public function getBlock(): AbstractBlock { return $this->block; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { $this->hasSubsequentLine = true; $cursor->advanceToNextNonSpaceOrTab(); // Does this next line also have attributes? $attributes = AttributesHelper::parseAttributes($cursor); $cursor->advanceToNextNonSpaceOrTab(); if ($cursor->isAtEnd() && $attributes !== []) { // It does! Merge them into what we parsed previously $this->block->setAttributes(AttributesHelper::mergeAttributes( $this->block->getAttributes(), $attributes )); // Tell the core parser we've consumed everything return BlockContinue::at($cursor); } // Okay, so there are no attributes on the next line // If this next line is blank we know we can't target the next node, it must be a previous one if ($cursor->isBlank()) { $this->block->setTarget(Attributes::TARGET_PREVIOUS); } return BlockContinue::none(); } public function closeBlock(): void { // Attributes appearing at the very end of the document won't have any last lines to check // so we can make that determination here if (! $this->hasSubsequentLine) { $this->block->setTarget(Attributes::TARGET_PREVIOUS); } // We know this block must apply to the "previous" block, but that could be a sibling or parent, // so we check the containing block to see which one it might be. if ($this->block->getTarget() === Attributes::TARGET_PREVIOUS && $this->block->parent() === $this->container) { $this->block->setTarget(Attributes::TARGET_PARENT); } } } commonmark/src/Extension/Attributes/Parser/AttributesInlineParser.php 0000644 00000003232 15007532371 0022212 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) 2015 Martin Hasoň <martin.hason@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Attributes\Parser; use League\CommonMark\Extension\Attributes\Node\AttributesInline; use League\CommonMark\Extension\Attributes\Util\AttributesHelper; use League\CommonMark\Node\StringContainerInterface; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; final class AttributesInlineParser implements InlineParserInterface { public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::string('{'); } public function parse(InlineParserContext $inlineContext): bool { $cursor = $inlineContext->getCursor(); $char = (string) $cursor->peek(-1); $attributes = AttributesHelper::parseAttributes($cursor); if ($attributes === []) { return false; } if ($char === ' ' && ($prev = $inlineContext->getContainer()->lastChild()) instanceof StringContainerInterface) { $prev->setLiteral(\rtrim($prev->getLiteral(), ' ')); } if ($char === '') { $cursor->advanceToNextNonSpaceOrNewline(); } $node = new AttributesInline($attributes, $char === ' ' || $char === ''); $inlineContext->getContainer()->appendChild($node); return true; } } commonmark/src/Extension/Attributes/Parser/AttributesBlockStartParser.php 0000644 00000002513 15007532371 0023045 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) 2015 Martin Hasoň <martin.hason@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Attributes\Parser; use League\CommonMark\Extension\Attributes\Util\AttributesHelper; use League\CommonMark\Parser\Block\BlockStart; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; final class AttributesBlockStartParser implements BlockStartParserInterface { public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart { $originalPosition = $cursor->getPosition(); $attributes = AttributesHelper::parseAttributes($cursor); if ($attributes === [] && $originalPosition === $cursor->getPosition()) { return BlockStart::none(); } if ($cursor->getNextNonSpaceCharacter() !== null) { return BlockStart::none(); } return BlockStart::of(new AttributesBlockContinueParser($attributes, $parserState->getActiveBlockParser()->getBlock()))->at($cursor); } } commonmark/src/Extension/Attributes/Event/AttributesListener.php 0000644 00000010503 15007532371 0021230 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) 2015 Martin Hasoň <martin.hason@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Attributes\Event; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\Attributes\Node\Attributes; use League\CommonMark\Extension\Attributes\Node\AttributesInline; use League\CommonMark\Extension\Attributes\Util\AttributesHelper; use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode; use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock; use League\CommonMark\Extension\CommonMark\Node\Block\ListItem; use League\CommonMark\Node\Inline\AbstractInline; use League\CommonMark\Node\Node; final class AttributesListener { private const DIRECTION_PREFIX = 'prefix'; private const DIRECTION_SUFFIX = 'suffix'; public function processDocument(DocumentParsedEvent $event): void { foreach ($event->getDocument()->iterator() as $node) { if (! ($node instanceof Attributes || $node instanceof AttributesInline)) { continue; } [$target, $direction] = self::findTargetAndDirection($node); if ($target instanceof Node) { $parent = $target->parent(); if ($parent instanceof ListItem && $parent->parent() instanceof ListBlock && $parent->parent()->isTight()) { $target = $parent; } if ($direction === self::DIRECTION_SUFFIX) { $attributes = AttributesHelper::mergeAttributes($target, $node->getAttributes()); } else { $attributes = AttributesHelper::mergeAttributes($node->getAttributes(), $target); } $target->data->set('attributes', $attributes); } $node->detach(); } } /** * @param Attributes|AttributesInline $node * * @return array<Node|string|null> */ private static function findTargetAndDirection($node): array { $target = null; $direction = null; $previous = $next = $node; while (true) { $previous = self::getPrevious($previous); $next = self::getNext($next); if ($previous === null && $next === null) { if (! $node->parent() instanceof FencedCode) { $target = $node->parent(); $direction = self::DIRECTION_SUFFIX; } break; } if ($node instanceof AttributesInline && ($previous === null || ($previous instanceof AbstractInline && $node->isBlock()))) { continue; } if ($previous !== null && ! self::isAttributesNode($previous)) { $target = $previous; $direction = self::DIRECTION_SUFFIX; break; } if ($next !== null && ! self::isAttributesNode($next)) { $target = $next; $direction = self::DIRECTION_PREFIX; break; } } return [$target, $direction]; } /** * Get any previous block (sibling or parent) this might apply to */ private static function getPrevious(?Node $node = null): ?Node { if ($node instanceof Attributes) { if ($node->getTarget() === Attributes::TARGET_NEXT) { return null; } if ($node->getTarget() === Attributes::TARGET_PARENT) { return $node->parent(); } } return $node instanceof Node ? $node->previous() : null; } /** * Get any previous block (sibling or parent) this might apply to */ private static function getNext(?Node $node = null): ?Node { if ($node instanceof Attributes && $node->getTarget() !== Attributes::TARGET_NEXT) { return null; } return $node instanceof Node ? $node->next() : null; } private static function isAttributesNode(Node $node): bool { return $node instanceof Attributes || $node instanceof AttributesInline; } } commonmark/src/Extension/InlinesOnly/ChildRenderer.php 0000644 00000001665 15007532371 0017153 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\InlinesOnly; use League\CommonMark\Node\Block\Document; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; /** * Simply renders child elements as-is, adding newlines as needed. */ final class ChildRenderer implements NodeRendererInterface { public function render(Node $node, ChildNodeRendererInterface $childRenderer): string { $out = $childRenderer->renderNodes($node->children()); if (! $node instanceof Document) { $out .= $childRenderer->getBlockSeparator(); } return $out; } } commonmark/src/Extension/InlinesOnly/InlinesOnlyExtension.php 0000644 00000007203 15007532371 0020573 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\InlinesOnly; use League\CommonMark as Core; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Extension\CommonMark; use League\CommonMark\Extension\CommonMark\Delimiter\Processor\EmphasisDelimiterProcessor; use League\CommonMark\Extension\ConfigurableExtensionInterface; use League\Config\ConfigurationBuilderInterface; use Nette\Schema\Expect; final class InlinesOnlyExtension implements ConfigurableExtensionInterface { public function configureSchema(ConfigurationBuilderInterface $builder): void { $builder->addSchema('commonmark', Expect::structure([ 'use_asterisk' => Expect::bool(true), 'use_underscore' => Expect::bool(true), 'enable_strong' => Expect::bool(true), 'enable_em' => Expect::bool(true), ])); } // phpcs:disable Generic.Functions.FunctionCallArgumentSpacing.TooMuchSpaceAfterComma,Squiz.WhiteSpace.SemicolonSpacing.Incorrect public function register(EnvironmentBuilderInterface $environment): void { $childRenderer = new ChildRenderer(); $environment ->addInlineParser(new Core\Parser\Inline\NewlineParser(), 200) ->addInlineParser(new CommonMark\Parser\Inline\BacktickParser(), 150) ->addInlineParser(new CommonMark\Parser\Inline\EscapableParser(), 80) ->addInlineParser(new CommonMark\Parser\Inline\EntityParser(), 70) ->addInlineParser(new CommonMark\Parser\Inline\AutolinkParser(), 50) ->addInlineParser(new CommonMark\Parser\Inline\HtmlInlineParser(), 40) ->addInlineParser(new CommonMark\Parser\Inline\CloseBracketParser(), 30) ->addInlineParser(new CommonMark\Parser\Inline\OpenBracketParser(), 20) ->addInlineParser(new CommonMark\Parser\Inline\BangParser(), 10) ->addRenderer(Core\Node\Block\Document::class, $childRenderer, 0) ->addRenderer(Core\Node\Block\Paragraph::class, $childRenderer, 0) ->addRenderer(CommonMark\Node\Inline\Code::class, new CommonMark\Renderer\Inline\CodeRenderer(), 0) ->addRenderer(CommonMark\Node\Inline\Emphasis::class, new CommonMark\Renderer\Inline\EmphasisRenderer(), 0) ->addRenderer(CommonMark\Node\Inline\HtmlInline::class, new CommonMark\Renderer\Inline\HtmlInlineRenderer(), 0) ->addRenderer(CommonMark\Node\Inline\Image::class, new CommonMark\Renderer\Inline\ImageRenderer(), 0) ->addRenderer(CommonMark\Node\Inline\Link::class, new CommonMark\Renderer\Inline\LinkRenderer(), 0) ->addRenderer(Core\Node\Inline\Newline::class, new Core\Renderer\Inline\NewlineRenderer(), 0) ->addRenderer(CommonMark\Node\Inline\Strong::class, new CommonMark\Renderer\Inline\StrongRenderer(), 0) ->addRenderer(Core\Node\Inline\Text::class, new Core\Renderer\Inline\TextRenderer(), 0) ; if ($environment->getConfiguration()->get('commonmark/use_asterisk')) { $environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('*')); } if ($environment->getConfiguration()->get('commonmark/use_underscore')) { $environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('_')); } } } commonmark/src/Extension/SmartPunct/SmartPunctExtension.php 0000644 00000005200 15007532371 0020260 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\SmartPunct; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\ConfigurableExtensionInterface; use League\CommonMark\Node\Block\Document; use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Renderer\Block as CoreBlockRenderer; use League\CommonMark\Renderer\Inline as CoreInlineRenderer; use League\Config\ConfigurationBuilderInterface; use Nette\Schema\Expect; final class SmartPunctExtension implements ConfigurableExtensionInterface { public function configureSchema(ConfigurationBuilderInterface $builder): void { $builder->addSchema('smartpunct', Expect::structure([ 'double_quote_opener' => Expect::string(Quote::DOUBLE_QUOTE_OPENER), 'double_quote_closer' => Expect::string(Quote::DOUBLE_QUOTE_CLOSER), 'single_quote_opener' => Expect::string(Quote::SINGLE_QUOTE_OPENER), 'single_quote_closer' => Expect::string(Quote::SINGLE_QUOTE_CLOSER), ])); } public function register(EnvironmentBuilderInterface $environment): void { $environment ->addInlineParser(new QuoteParser(), 10) ->addInlineParser(new DashParser(), 0) ->addInlineParser(new EllipsesParser(), 0) ->addDelimiterProcessor(QuoteProcessor::createDoubleQuoteProcessor( $environment->getConfiguration()->get('smartpunct/double_quote_opener'), $environment->getConfiguration()->get('smartpunct/double_quote_closer') )) ->addDelimiterProcessor(QuoteProcessor::createSingleQuoteProcessor( $environment->getConfiguration()->get('smartpunct/single_quote_opener'), $environment->getConfiguration()->get('smartpunct/single_quote_closer') )) ->addEventListener(DocumentParsedEvent::class, new ReplaceUnpairedQuotesListener()) ->addRenderer(Document::class, new CoreBlockRenderer\DocumentRenderer(), 0) ->addRenderer(Paragraph::class, new CoreBlockRenderer\ParagraphRenderer(), 0) ->addRenderer(Text::class, new CoreInlineRenderer\TextRenderer(), 0); } } commonmark/src/Extension/SmartPunct/QuoteParser.php 0000644 00000006674 15007532371 0016555 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\SmartPunct; use League\CommonMark\Delimiter\Delimiter; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; use League\CommonMark\Util\RegexHelper; final class QuoteParser implements InlineParserInterface { public const DOUBLE_QUOTES = [Quote::DOUBLE_QUOTE, Quote::DOUBLE_QUOTE_OPENER, Quote::DOUBLE_QUOTE_CLOSER]; public const SINGLE_QUOTES = [Quote::SINGLE_QUOTE, Quote::SINGLE_QUOTE_OPENER, Quote::SINGLE_QUOTE_CLOSER]; public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::oneOf(...[...self::DOUBLE_QUOTES, ...self::SINGLE_QUOTES]); } /** * Normalizes any quote characters found and manually adds them to the delimiter stack */ public function parse(InlineParserContext $inlineContext): bool { $char = $inlineContext->getFullMatch(); $cursor = $inlineContext->getCursor(); $normalizedCharacter = $this->getNormalizedQuoteCharacter($char); $charBefore = $cursor->peek(-1); if ($charBefore === null) { $charBefore = "\n"; } $cursor->advance(); $charAfter = $cursor->getCurrentCharacter(); if ($charAfter === null) { $charAfter = "\n"; } [$leftFlanking, $rightFlanking] = $this->determineFlanking($charBefore, $charAfter); $canOpen = $leftFlanking && ! $rightFlanking; $canClose = $rightFlanking; $node = new Quote($normalizedCharacter, ['delim' => true]); $inlineContext->getContainer()->appendChild($node); // Add entry to stack to this opener $inlineContext->getDelimiterStack()->push(new Delimiter($normalizedCharacter, 1, $node, $canOpen, $canClose)); return true; } private function getNormalizedQuoteCharacter(string $character): string { if (\in_array($character, self::DOUBLE_QUOTES, true)) { return Quote::DOUBLE_QUOTE; } if (\in_array($character, self::SINGLE_QUOTES, true)) { return Quote::SINGLE_QUOTE; } return $character; } /** * @return bool[] */ private function determineFlanking(string $charBefore, string $charAfter): array { $afterIsWhitespace = \preg_match('/\pZ|\s/u', $charAfter); $afterIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charAfter); $beforeIsWhitespace = \preg_match('/\pZ|\s/u', $charBefore); $beforeIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charBefore); $leftFlanking = ! $afterIsWhitespace && ! ($afterIsPunctuation && ! $beforeIsWhitespace && ! $beforeIsPunctuation); $rightFlanking = ! $beforeIsWhitespace && ! ($beforeIsPunctuation && ! $afterIsWhitespace && ! $afterIsPunctuation); return [$leftFlanking, $rightFlanking]; } } commonmark/src/Extension/SmartPunct/QuoteProcessor.php 0000644 00000004473 15007532371 0017273 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\SmartPunct; use League\CommonMark\Delimiter\DelimiterInterface; use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface; use League\CommonMark\Node\Inline\AbstractStringContainer; final class QuoteProcessor implements DelimiterProcessorInterface { /** @psalm-readonly */ private string $normalizedCharacter; /** @psalm-readonly */ private string $openerCharacter; /** @psalm-readonly */ private string $closerCharacter; private function __construct(string $char, string $opener, string $closer) { $this->normalizedCharacter = $char; $this->openerCharacter = $opener; $this->closerCharacter = $closer; } public function getOpeningCharacter(): string { return $this->normalizedCharacter; } public function getClosingCharacter(): string { return $this->normalizedCharacter; } public function getMinLength(): int { return 1; } public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int { return 1; } public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void { $opener->insertAfter(new Quote($this->openerCharacter)); $closer->insertBefore(new Quote($this->closerCharacter)); } /** * Create a double-quote processor */ public static function createDoubleQuoteProcessor(string $opener = Quote::DOUBLE_QUOTE_OPENER, string $closer = Quote::DOUBLE_QUOTE_CLOSER): self { return new self(Quote::DOUBLE_QUOTE, $opener, $closer); } /** * Create a single-quote processor */ public static function createSingleQuoteProcessor(string $opener = Quote::SINGLE_QUOTE_OPENER, string $closer = Quote::SINGLE_QUOTE_CLOSER): self { return new self(Quote::SINGLE_QUOTE, $opener, $closer); } } commonmark/src/Extension/SmartPunct/EllipsesParser.php 0000644 00000002117 15007532371 0017224 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\SmartPunct; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; final class EllipsesParser implements InlineParserInterface { public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::oneOf('...', '. . .'); } public function parse(InlineParserContext $inlineContext): bool { $inlineContext->getCursor()->advanceBy($inlineContext->getFullMatchLength()); $inlineContext->getContainer()->appendChild(new Text('…')); return true; } } commonmark/src/Extension/SmartPunct/ReplaceUnpairedQuotesListener.php 0000644 00000002462 15007532371 0022244 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\SmartPunct; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Node\Inline\AdjacentTextMerger; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Node\Query; /** * Identifies any lingering Quote nodes that were missing pairs and converts them into Text nodes */ final class ReplaceUnpairedQuotesListener { public function __invoke(DocumentParsedEvent $event): void { $query = (new Query())->where(Query::type(Quote::class)); foreach ($query->findAll($event->getDocument()) as $quote) { \assert($quote instanceof Quote); $literal = $quote->getLiteral(); if ($literal === Quote::SINGLE_QUOTE) { $literal = Quote::SINGLE_QUOTE_CLOSER; } elseif ($literal === Quote::DOUBLE_QUOTE) { $literal = Quote::DOUBLE_QUOTE_OPENER; } $quote->replaceWith($new = new Text($literal)); AdjacentTextMerger::mergeWithDirectlyAdjacentNodes($new); } } } commonmark/src/Extension/SmartPunct/DashParser.php 0000644 00000003562 15007532371 0016330 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\SmartPunct; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; final class DashParser implements InlineParserInterface { private const EN_DASH = '–'; private const EM_DASH = '—'; public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::regex('(?<!-)(-{2,})'); } public function parse(InlineParserContext $inlineContext): bool { $count = $inlineContext->getFullMatchLength(); $inlineContext->getCursor()->advanceBy($count); $enCount = 0; $emCount = 0; if ($count % 3 === 0) { // If divisible by 3, use all em dashes $emCount = (int) ($count / 3); } elseif ($count % 2 === 0) { // If divisible by 2, use all en dashes $enCount = (int) ($count / 2); } elseif ($count % 3 === 2) { // If 2 extra dashes, use en dash for last 2; em dashes for rest $emCount = (int) (($count - 2) / 3); $enCount = 1; } else { // Use en dashes for last 4 hyphens; em dashes for rest $emCount = (int) (($count - 4) / 3); $enCount = 2; } $inlineContext->getContainer()->appendChild(new Text( \str_repeat(self::EM_DASH, $emCount) . \str_repeat(self::EN_DASH, $enCount) )); return true; } } commonmark/src/Extension/SmartPunct/Quote.php 0000644 00000001501 15007532371 0015360 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\SmartPunct; use League\CommonMark\Node\Inline\AbstractStringContainer; final class Quote extends AbstractStringContainer { public const DOUBLE_QUOTE = '"'; public const DOUBLE_QUOTE_OPENER = '“'; public const DOUBLE_QUOTE_CLOSER = '”'; public const SINGLE_QUOTE = "'"; public const SINGLE_QUOTE_OPENER = '‘'; public const SINGLE_QUOTE_CLOSER = '’'; } commonmark/src/Extension/TableOfContents/Node/TableOfContents.php 0000644 00000000701 15007532371 0021135 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TableOfContents\Node; use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock; final class TableOfContents extends ListBlock { } commonmark/src/Extension/TableOfContents/Node/TableOfContentsPlaceholder.php 0000644 00000000677 15007532371 0023314 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TableOfContents\Node; use League\CommonMark\Node\Block\AbstractBlock; final class TableOfContentsPlaceholder extends AbstractBlock { } commonmark/src/Extension/TableOfContents/TableOfContentsPlaceholderParser.php 0000644 00000004747 15007532371 0023606 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TableOfContents; use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsPlaceholder; use League\CommonMark\Parser\Block\AbstractBlockContinueParser; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Block\BlockStart; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class TableOfContentsPlaceholderParser extends AbstractBlockContinueParser { /** @psalm-readonly */ private TableOfContentsPlaceholder $block; public function __construct() { $this->block = new TableOfContentsPlaceholder(); } public function getBlock(): TableOfContentsPlaceholder { return $this->block; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { return BlockContinue::none(); } public static function blockStartParser(): BlockStartParserInterface { return new class () implements BlockStartParserInterface, ConfigurationAwareInterface { /** @psalm-readonly-allow-private-mutation */ private ConfigurationInterface $config; public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart { $placeholder = $this->config->get('table_of_contents/placeholder'); if ($placeholder === null) { return BlockStart::none(); } // The placeholder must be the only thing on the line if ($cursor->match('/^' . \preg_quote($placeholder, '/') . '$/') === null) { return BlockStart::none(); } return BlockStart::of(new TableOfContentsPlaceholderParser())->at($cursor); } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } }; } } commonmark/src/Extension/TableOfContents/TableOfContentsRenderer.php 0000644 00000002763 15007532371 0021751 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TableOfContents; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Xml\XmlNodeRendererInterface; final class TableOfContentsRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** @var NodeRendererInterface&XmlNodeRendererInterface */ private $innerRenderer; /** * @psalm-param NodeRendererInterface&XmlNodeRendererInterface $innerRenderer * * @phpstan-param NodeRendererInterface&XmlNodeRendererInterface $innerRenderer */ public function __construct(NodeRendererInterface $innerRenderer) { $this->innerRenderer = $innerRenderer; } /** * {@inheritDoc} */ public function render(Node $node, ChildNodeRendererInterface $childRenderer) { return $this->innerRenderer->render($node, $childRenderer); } public function getXmlTagName(Node $node): string { return 'table_of_contents'; } /** * @return array<string, scalar> */ public function getXmlAttributes(Node $node): array { return $this->innerRenderer->getXmlAttributes($node); } } commonmark/src/Extension/TableOfContents/TableOfContentsPlaceholderRenderer.php 0000644 00000002020 15007532371 0024076 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TableOfContents; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Xml\XmlNodeRendererInterface; final class TableOfContentsPlaceholderRenderer implements NodeRendererInterface, XmlNodeRendererInterface { public function render(Node $node, ChildNodeRendererInterface $childRenderer): string { return '<!-- table of contents -->'; } public function getXmlTagName(Node $node): string { return 'table_of_contents_placeholder'; } /** * @return array<string, scalar> */ public function getXmlAttributes(Node $node): array { return []; } } commonmark/src/Extension/TableOfContents/TableOfContentsBuilder.php 0000644 00000007364 15007532371 0021573 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TableOfContents; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\CommonMark\Node\Block\Heading; use League\CommonMark\Extension\HeadingPermalink\HeadingPermalink; use League\CommonMark\Extension\TableOfContents\Node\TableOfContents; use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsPlaceholder; use League\CommonMark\Node\Block\Document; use League\CommonMark\Node\NodeIterator; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; use League\Config\Exception\InvalidConfigurationException; final class TableOfContentsBuilder implements ConfigurationAwareInterface { public const POSITION_TOP = 'top'; public const POSITION_BEFORE_HEADINGS = 'before-headings'; public const POSITION_PLACEHOLDER = 'placeholder'; /** @psalm-readonly-allow-private-mutation */ private ConfigurationInterface $config; public function onDocumentParsed(DocumentParsedEvent $event): void { $document = $event->getDocument(); $generator = new TableOfContentsGenerator( (string) $this->config->get('table_of_contents/style'), (string) $this->config->get('table_of_contents/normalize'), (int) $this->config->get('table_of_contents/min_heading_level'), (int) $this->config->get('table_of_contents/max_heading_level'), (string) $this->config->get('heading_permalink/fragment_prefix'), ); $toc = $generator->generate($document); if ($toc === null) { // No linkable headers exist, so no TOC could be generated return; } // Add custom CSS class(es), if defined $class = $this->config->get('table_of_contents/html_class'); if ($class !== null) { $toc->data->append('attributes/class', $class); } // Add the TOC to the Document $position = $this->config->get('table_of_contents/position'); if ($position === self::POSITION_TOP) { $document->prependChild($toc); } elseif ($position === self::POSITION_BEFORE_HEADINGS) { $this->insertBeforeFirstLinkedHeading($document, $toc); } elseif ($position === self::POSITION_PLACEHOLDER) { $this->replacePlaceholders($document, $toc); } else { throw InvalidConfigurationException::forConfigOption('table_of_contents/position', $position); } } private function insertBeforeFirstLinkedHeading(Document $document, TableOfContents $toc): void { foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) { if (! $node instanceof Heading) { continue; } foreach ($node->children() as $child) { if ($child instanceof HeadingPermalink) { $node->insertBefore($toc); return; } } } } private function replacePlaceholders(Document $document, TableOfContents $toc): void { foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) { // Add the block once we find a placeholder if (! $node instanceof TableOfContentsPlaceholder) { continue; } $node->replaceWith(clone $toc); } } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } } commonmark/src/Extension/TableOfContents/TableOfContentsGeneratorInterface.php 0000644 00000001056 15007532371 0023744 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TableOfContents; use League\CommonMark\Extension\TableOfContents\Node\TableOfContents; use League\CommonMark\Node\Block\Document; interface TableOfContentsGeneratorInterface { public function generate(Document $document): ?TableOfContents; } commonmark/src/Extension/TableOfContents/TableOfContentsExtension.php 0000644 00000005572 15007532371 0022160 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TableOfContents; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock; use League\CommonMark\Extension\CommonMark\Renderer\Block\ListBlockRenderer; use League\CommonMark\Extension\ConfigurableExtensionInterface; use League\CommonMark\Extension\TableOfContents\Node\TableOfContents; use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsPlaceholder; use League\Config\ConfigurationBuilderInterface; use Nette\Schema\Expect; final class TableOfContentsExtension implements ConfigurableExtensionInterface { public function configureSchema(ConfigurationBuilderInterface $builder): void { $builder->addSchema('table_of_contents', Expect::structure([ 'position' => Expect::anyOf(TableOfContentsBuilder::POSITION_BEFORE_HEADINGS, TableOfContentsBuilder::POSITION_PLACEHOLDER, TableOfContentsBuilder::POSITION_TOP)->default(TableOfContentsBuilder::POSITION_TOP), 'style' => Expect::anyOf(ListBlock::TYPE_BULLET, ListBlock::TYPE_ORDERED)->default(ListBlock::TYPE_BULLET), 'normalize' => Expect::anyOf(TableOfContentsGenerator::NORMALIZE_RELATIVE, TableOfContentsGenerator::NORMALIZE_FLAT, TableOfContentsGenerator::NORMALIZE_DISABLED)->default(TableOfContentsGenerator::NORMALIZE_RELATIVE), 'min_heading_level' => Expect::int()->min(1)->max(6)->default(1), 'max_heading_level' => Expect::int()->min(1)->max(6)->default(6), 'html_class' => Expect::string()->default('table-of-contents'), 'placeholder' => Expect::anyOf(Expect::string(), Expect::null())->default(null), ])); } public function register(EnvironmentBuilderInterface $environment): void { $environment->addRenderer(TableOfContents::class, new TableOfContentsRenderer(new ListBlockRenderer())); $environment->addEventListener(DocumentParsedEvent::class, [new TableOfContentsBuilder(), 'onDocumentParsed'], -150); // phpcs:ignore SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed if ($environment->getConfiguration()->get('table_of_contents/position') === TableOfContentsBuilder::POSITION_PLACEHOLDER) { $environment->addBlockStartParser(TableOfContentsPlaceholderParser::blockStartParser(), 200); // If a placeholder cannot be replaced with a TOC element this renderer will ensure the parser won't error out $environment->addRenderer(TableOfContentsPlaceholder::class, new TableOfContentsPlaceholderRenderer()); } } } commonmark/src/Extension/TableOfContents/Normalizer/RelativeNormalizerStrategy.php 0000644 00000004254 15007532371 0024710 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TableOfContents\Normalizer; use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock; use League\CommonMark\Extension\CommonMark\Node\Block\ListItem; use League\CommonMark\Extension\TableOfContents\Node\TableOfContents; final class RelativeNormalizerStrategy implements NormalizerStrategyInterface { /** @psalm-readonly */ private TableOfContents $toc; /** * @var array<int, ListItem> * * @psalm-readonly-allow-private-mutation */ private array $listItemStack = []; public function __construct(TableOfContents $toc) { $this->toc = $toc; } public function addItem(int $level, ListItem $listItemToAdd): void { $previousLevel = \array_key_last($this->listItemStack); // Pop the stack if we're too deep while ($previousLevel !== null && $level < $previousLevel) { \array_pop($this->listItemStack); $previousLevel = \array_key_last($this->listItemStack); } $lastListItem = \end($this->listItemStack); // Need to go one level deeper? Add that level if ($lastListItem !== false && $level > $previousLevel) { $targetListBlock = new ListBlock($lastListItem->getListData()); $targetListBlock->setStartLine($listItemToAdd->getStartLine()); $targetListBlock->setEndLine($listItemToAdd->getEndLine()); $lastListItem->appendChild($targetListBlock); // Otherwise we're at the right level // If there's no stack we're adding this item directly to the TOC element } elseif ($lastListItem === false) { $targetListBlock = $this->toc; // Otherwise add it to the last list item } else { $targetListBlock = $lastListItem->parent(); } $targetListBlock->appendChild($listItemToAdd); $this->listItemStack[$level] = $listItemToAdd; } } commonmark/src/Extension/TableOfContents/Normalizer/FlatNormalizerStrategy.php 0000644 00000001510 15007532371 0024013 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TableOfContents\Normalizer; use League\CommonMark\Extension\CommonMark\Node\Block\ListItem; use League\CommonMark\Extension\TableOfContents\Node\TableOfContents; final class FlatNormalizerStrategy implements NormalizerStrategyInterface { /** @psalm-readonly */ private TableOfContents $toc; public function __construct(TableOfContents $toc) { $this->toc = $toc; } public function addItem(int $level, ListItem $listItemToAdd): void { $this->toc->appendChild($listItemToAdd); } } commonmark/src/Extension/TableOfContents/Normalizer/AsIsNormalizerStrategy.php 0000644 00000004545 15007532371 0023777 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TableOfContents\Normalizer; use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock; use League\CommonMark\Extension\CommonMark\Node\Block\ListItem; use League\CommonMark\Extension\TableOfContents\Node\TableOfContents; final class AsIsNormalizerStrategy implements NormalizerStrategyInterface { /** @psalm-readonly-allow-private-mutation */ private ListBlock $parentListBlock; /** @psalm-readonly-allow-private-mutation */ private int $parentLevel = 1; /** @psalm-readonly-allow-private-mutation */ private ?ListItem $lastListItem = null; public function __construct(TableOfContents $toc) { $this->parentListBlock = $toc; } public function addItem(int $level, ListItem $listItemToAdd): void { while ($level > $this->parentLevel) { // Descend downwards, creating new ListBlocks if needed, until we reach the correct depth if ($this->lastListItem === null) { $this->lastListItem = new ListItem($this->parentListBlock->getListData()); $this->parentListBlock->appendChild($this->lastListItem); } $newListBlock = new ListBlock($this->parentListBlock->getListData()); $newListBlock->setStartLine($listItemToAdd->getStartLine()); $newListBlock->setEndLine($listItemToAdd->getEndLine()); $this->lastListItem->appendChild($newListBlock); $this->parentListBlock = $newListBlock; $this->lastListItem = null; $this->parentLevel++; } while ($level < $this->parentLevel) { // Search upwards for the previous parent list block $search = $this->parentListBlock; while ($search = $search->parent()) { if ($search instanceof ListBlock) { $this->parentListBlock = $search; break; } } $this->parentLevel--; } $this->parentListBlock->appendChild($listItemToAdd); $this->lastListItem = $listItemToAdd; } } commonmark/src/Extension/TableOfContents/Normalizer/NormalizerStrategyInterface.php 0000644 00000001006 15007532371 0025025 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TableOfContents\Normalizer; use League\CommonMark\Extension\CommonMark\Node\Block\ListItem; interface NormalizerStrategyInterface { public function addItem(int $level, ListItem $listItemToAdd): void; } commonmark/src/Extension/TableOfContents/TableOfContentsGenerator.php 0000644 00000013670 15007532371 0022130 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TableOfContents; use League\CommonMark\Extension\CommonMark\Node\Block\Heading; use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock; use League\CommonMark\Extension\CommonMark\Node\Block\ListData; use League\CommonMark\Extension\CommonMark\Node\Block\ListItem; use League\CommonMark\Extension\CommonMark\Node\Inline\Link; use League\CommonMark\Extension\HeadingPermalink\HeadingPermalink; use League\CommonMark\Extension\TableOfContents\Node\TableOfContents; use League\CommonMark\Extension\TableOfContents\Normalizer\AsIsNormalizerStrategy; use League\CommonMark\Extension\TableOfContents\Normalizer\FlatNormalizerStrategy; use League\CommonMark\Extension\TableOfContents\Normalizer\NormalizerStrategyInterface; use League\CommonMark\Extension\TableOfContents\Normalizer\RelativeNormalizerStrategy; use League\CommonMark\Node\Block\Document; use League\CommonMark\Node\NodeIterator; use League\CommonMark\Node\RawMarkupContainerInterface; use League\CommonMark\Node\StringContainerHelper; use League\Config\Exception\InvalidConfigurationException; final class TableOfContentsGenerator implements TableOfContentsGeneratorInterface { public const STYLE_BULLET = ListBlock::TYPE_BULLET; public const STYLE_ORDERED = ListBlock::TYPE_ORDERED; public const NORMALIZE_DISABLED = 'as-is'; public const NORMALIZE_RELATIVE = 'relative'; public const NORMALIZE_FLAT = 'flat'; /** @psalm-readonly */ private string $style; /** @psalm-readonly */ private string $normalizationStrategy; /** @psalm-readonly */ private int $minHeadingLevel; /** @psalm-readonly */ private int $maxHeadingLevel; /** @psalm-readonly */ private string $fragmentPrefix; public function __construct(string $style, string $normalizationStrategy, int $minHeadingLevel, int $maxHeadingLevel, string $fragmentPrefix) { $this->style = $style; $this->normalizationStrategy = $normalizationStrategy; $this->minHeadingLevel = $minHeadingLevel; $this->maxHeadingLevel = $maxHeadingLevel; $this->fragmentPrefix = $fragmentPrefix; if ($fragmentPrefix !== '') { $this->fragmentPrefix .= '-'; } } public function generate(Document $document): ?TableOfContents { $toc = $this->createToc($document); $normalizer = $this->getNormalizer($toc); $firstHeading = null; foreach ($this->getHeadingLinks($document) as $headingLink) { $heading = $headingLink->parent(); // Make sure this is actually tied to a heading if (! $heading instanceof Heading) { continue; } // Skip any headings outside the configured min/max levels if ($heading->getLevel() < $this->minHeadingLevel || $heading->getLevel() > $this->maxHeadingLevel) { continue; } // Keep track of the first heading we see - we might need this later $firstHeading ??= $heading; // Keep track of the start and end lines $toc->setStartLine($firstHeading->getStartLine()); $toc->setEndLine($heading->getEndLine()); // Create the new link $link = new Link('#' . $this->fragmentPrefix . $headingLink->getSlug(), StringContainerHelper::getChildText($heading, [RawMarkupContainerInterface::class])); $listItem = new ListItem($toc->getListData()); $listItem->setStartLine($heading->getStartLine()); $listItem->setEndLine($heading->getEndLine()); $listItem->appendChild($link); // Add it to the correct place $normalizer->addItem($heading->getLevel(), $listItem); } // Don't add the TOC if no headings were present if (! $toc->hasChildren() || $firstHeading === null) { return null; } return $toc; } private function createToc(Document $document): TableOfContents { $listData = new ListData(); if ($this->style === self::STYLE_BULLET) { $listData->type = ListBlock::TYPE_BULLET; } elseif ($this->style === self::STYLE_ORDERED) { $listData->type = ListBlock::TYPE_ORDERED; } else { throw new InvalidConfigurationException(\sprintf('Invalid table of contents list style: "%s"', $this->style)); } $toc = new TableOfContents($listData); $toc->setStartLine($document->getStartLine()); $toc->setEndLine($document->getEndLine()); return $toc; } /** * @return iterable<HeadingPermalink> */ private function getHeadingLinks(Document $document): iterable { foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) { if (! $node instanceof Heading) { continue; } foreach ($node->children() as $child) { if ($child instanceof HeadingPermalink) { yield $child; } } } } private function getNormalizer(TableOfContents $toc): NormalizerStrategyInterface { switch ($this->normalizationStrategy) { case self::NORMALIZE_DISABLED: return new AsIsNormalizerStrategy($toc); case self::NORMALIZE_RELATIVE: return new RelativeNormalizerStrategy($toc); case self::NORMALIZE_FLAT: return new FlatNormalizerStrategy($toc); default: throw new InvalidConfigurationException(\sprintf('Invalid table of contents normalization strategy: "%s"', $this->normalizationStrategy)); } } } commonmark/src/Extension/Mention/MentionExtension.php 0000644 00000005236 15007532371 0017113 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Mention; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Extension\ConfigurableExtensionInterface; use League\CommonMark\Extension\Mention\Generator\MentionGeneratorInterface; use League\Config\ConfigurationBuilderInterface; use League\Config\Exception\InvalidConfigurationException; use Nette\Schema\Expect; final class MentionExtension implements ConfigurableExtensionInterface { public function configureSchema(ConfigurationBuilderInterface $builder): void { $isAValidPartialRegex = static function (string $regex): bool { $regex = '/' . $regex . '/i'; return @\preg_match($regex, '') !== false; }; $builder->addSchema('mentions', Expect::arrayOf( Expect::structure([ 'prefix' => Expect::string()->required(), 'pattern' => Expect::string()->assert($isAValidPartialRegex, 'Pattern must not include starting/ending delimiters (like "/")')->required(), 'generator' => Expect::anyOf( Expect::type(MentionGeneratorInterface::class), Expect::string(), Expect::type('callable') )->required(), ]) )); } public function register(EnvironmentBuilderInterface $environment): void { $mentions = $environment->getConfiguration()->get('mentions'); foreach ($mentions as $name => $mention) { if ($mention['generator'] instanceof MentionGeneratorInterface) { $environment->addInlineParser(new MentionParser($name, $mention['prefix'], $mention['pattern'], $mention['generator'])); } elseif (\is_string($mention['generator'])) { $environment->addInlineParser(MentionParser::createWithStringTemplate($name, $mention['prefix'], $mention['pattern'], $mention['generator'])); } elseif (\is_callable($mention['generator'])) { $environment->addInlineParser(MentionParser::createWithCallback($name, $mention['prefix'], $mention['pattern'], $mention['generator'])); } else { throw new InvalidConfigurationException(\sprintf('The "generator" provided for the "%s" MentionParser configuration must be a string template, callable, or an object that implements %s.', $name, MentionGeneratorInterface::class)); } } } } commonmark/src/Extension/Mention/Generator/StringTemplateLinkGenerator.php 0000644 00000001516 15007532371 0023157 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Mention\Generator; use League\CommonMark\Extension\Mention\Mention; use League\CommonMark\Node\Inline\AbstractInline; final class StringTemplateLinkGenerator implements MentionGeneratorInterface { private string $urlTemplate; public function __construct(string $urlTemplate) { $this->urlTemplate = $urlTemplate; } public function generateMention(Mention $mention): ?AbstractInline { $mention->setUrl(\sprintf($this->urlTemplate, $mention->getIdentifier())); return $mention; } } commonmark/src/Extension/Mention/Generator/MentionGeneratorInterface.php 0000644 00000001036 15007532371 0022626 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Mention\Generator; use League\CommonMark\Extension\Mention\Mention; use League\CommonMark\Node\Inline\AbstractInline; interface MentionGeneratorInterface { public function generateMention(Mention $mention): ?AbstractInline; } commonmark/src/Extension/Mention/Generator/CallbackGenerator.php 0000644 00000003107 15007532371 0021071 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Mention\Generator; use League\CommonMark\Exception\LogicException; use League\CommonMark\Extension\Mention\Mention; use League\CommonMark\Node\Inline\AbstractInline; final class CallbackGenerator implements MentionGeneratorInterface { /** * A callback function which sets the URL on the passed mention and returns the mention, return a new AbstractInline based object or null if the mention is not a match * * @var callable(Mention): ?AbstractInline */ private $callback; public function __construct(callable $callback) { $this->callback = $callback; } /** * @throws LogicException */ public function generateMention(Mention $mention): ?AbstractInline { $result = \call_user_func($this->callback, $mention); if ($result === null) { return null; } if ($result instanceof AbstractInline && ! ($result instanceof Mention)) { return $result; } if ($result instanceof Mention && $result->hasUrl()) { return $mention; } throw new LogicException('CallbackGenerator callable must set the URL on the passed mention and return the mention, return a new AbstractInline based object or null if the mention is not a match'); } } commonmark/src/Extension/Mention/MentionParser.php 0000644 00000005606 15007532371 0016374 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Mention; use League\CommonMark\Extension\Mention\Generator\CallbackGenerator; use League\CommonMark\Extension\Mention\Generator\MentionGeneratorInterface; use League\CommonMark\Extension\Mention\Generator\StringTemplateLinkGenerator; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; final class MentionParser implements InlineParserInterface { /** @psalm-readonly */ private string $name; /** @psalm-readonly */ private string $prefix; /** @psalm-readonly */ private string $identifierPattern; /** @psalm-readonly */ private MentionGeneratorInterface $mentionGenerator; public function __construct(string $name, string $prefix, string $identifierPattern, MentionGeneratorInterface $mentionGenerator) { $this->name = $name; $this->prefix = $prefix; $this->identifierPattern = $identifierPattern; $this->mentionGenerator = $mentionGenerator; } public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::join( InlineParserMatch::string($this->prefix), InlineParserMatch::regex($this->identifierPattern) ); } public function parse(InlineParserContext $inlineContext): bool { $cursor = $inlineContext->getCursor(); // The prefix must not have any other characters immediately prior $previousChar = $cursor->peek(-1); if ($previousChar !== null && \preg_match('/\w/', $previousChar)) { // peek() doesn't modify the cursor, so no need to restore state first return false; } [$prefix, $identifier] = $inlineContext->getSubMatches(); $mention = $this->mentionGenerator->generateMention(new Mention($this->name, $prefix, $identifier)); if ($mention === null) { return false; } $cursor->advanceBy($inlineContext->getFullMatchLength()); $inlineContext->getContainer()->appendChild($mention); return true; } public static function createWithStringTemplate(string $name, string $prefix, string $mentionRegex, string $urlTemplate): MentionParser { return new self($name, $prefix, $mentionRegex, new StringTemplateLinkGenerator($urlTemplate)); } public static function createWithCallback(string $name, string $prefix, string $mentionRegex, callable $callback): MentionParser { return new self($name, $prefix, $mentionRegex, new CallbackGenerator($callback)); } } commonmark/src/Extension/Mention/Mention.php 0000644 00000004002 15007532371 0015204 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Mention; use League\CommonMark\Extension\CommonMark\Node\Inline\Link; use League\CommonMark\Node\Inline\Text; class Mention extends Link { private string $name; private string $prefix; private string $identifier; public function __construct(string $name, string $prefix, string $identifier, ?string $label = null) { $this->name = $name; $this->prefix = $prefix; $this->identifier = $identifier; parent::__construct('', $label ?? \sprintf('%s%s', $prefix, $identifier)); } public function getLabel(): ?string { if (($labelNode = $this->findLabelNode()) === null) { return null; } return $labelNode->getLiteral(); } public function getIdentifier(): string { return $this->identifier; } public function getName(): ?string { return $this->name; } public function getPrefix(): string { return $this->prefix; } public function hasUrl(): bool { return $this->url !== ''; } /** * @return $this */ public function setLabel(string $label): self { if (($labelNode = $this->findLabelNode()) === null) { $labelNode = new Text(); $this->prependChild($labelNode); } $labelNode->setLiteral($label); return $this; } private function findLabelNode(): ?Text { foreach ($this->children() as $child) { if ($child instanceof Text) { return $child; } } return null; } } commonmark/src/Extension/HeadingPermalink/HeadingPermalinkExtension.php 0000644 00000004123 15007532371 0022467 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\HeadingPermalink; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\ConfigurableExtensionInterface; use League\Config\ConfigurationBuilderInterface; use Nette\Schema\Expect; /** * Extension which automatically anchor links to heading elements */ final class HeadingPermalinkExtension implements ConfigurableExtensionInterface { public function configureSchema(ConfigurationBuilderInterface $builder): void { $builder->addSchema('heading_permalink', Expect::structure([ 'min_heading_level' => Expect::int()->min(1)->max(6)->default(1), 'max_heading_level' => Expect::int()->min(1)->max(6)->default(6), 'insert' => Expect::anyOf(HeadingPermalinkProcessor::INSERT_BEFORE, HeadingPermalinkProcessor::INSERT_AFTER, HeadingPermalinkProcessor::INSERT_NONE)->default(HeadingPermalinkProcessor::INSERT_BEFORE), 'id_prefix' => Expect::string()->default('content'), 'apply_id_to_heading' => Expect::bool()->default(false), 'heading_class' => Expect::string()->default(''), 'fragment_prefix' => Expect::string()->default('content'), 'html_class' => Expect::string()->default('heading-permalink'), 'title' => Expect::string()->default('Permalink'), 'symbol' => Expect::string()->default(HeadingPermalinkRenderer::DEFAULT_SYMBOL), 'aria_hidden' => Expect::bool()->default(true), ])); } public function register(EnvironmentBuilderInterface $environment): void { $environment->addEventListener(DocumentParsedEvent::class, new HeadingPermalinkProcessor(), -100); $environment->addRenderer(HeadingPermalink::class, new HeadingPermalinkRenderer()); } } commonmark/src/Extension/HeadingPermalink/HeadingPermalinkRenderer.php 0000644 00000005741 15007532371 0022270 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\HeadingPermalink; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; /** * Renders the HeadingPermalink elements */ final class HeadingPermalinkRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface { public const DEFAULT_SYMBOL = '¶'; /** @psalm-readonly-allow-private-mutation */ private ConfigurationInterface $config; public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } /** * @param HeadingPermalink $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { HeadingPermalink::assertInstanceOf($node); $slug = $node->getSlug(); $fragmentPrefix = (string) $this->config->get('heading_permalink/fragment_prefix'); if ($fragmentPrefix !== '') { $fragmentPrefix .= '-'; } $attrs = $node->data->getData('attributes'); $appendId = ! $this->config->get('heading_permalink/apply_id_to_heading'); if ($appendId) { $idPrefix = (string) $this->config->get('heading_permalink/id_prefix'); if ($idPrefix !== '') { $idPrefix .= '-'; } $attrs->set('id', $idPrefix . $slug); } $attrs->set('href', '#' . $fragmentPrefix . $slug); $attrs->append('class', $this->config->get('heading_permalink/html_class')); $hidden = $this->config->get('heading_permalink/aria_hidden'); if ($hidden) { $attrs->set('aria-hidden', 'true'); } $attrs->set('title', $this->config->get('heading_permalink/title')); $symbol = $this->config->get('heading_permalink/symbol'); \assert(\is_string($symbol)); return new HtmlElement('a', $attrs->export(), \htmlspecialchars($symbol), false); } public function getXmlTagName(Node $node): string { return 'heading_permalink'; } /** * @param HeadingPermalink $node * * @return array<string, scalar> * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlAttributes(Node $node): array { HeadingPermalink::assertInstanceOf($node); return [ 'slug' => $node->getSlug(), ]; } } commonmark/src/Extension/HeadingPermalink/HeadingPermalinkProcessor.php 0000644 00000007353 15007532371 0022502 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\HeadingPermalink; use League\CommonMark\Environment\EnvironmentAwareInterface; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\CommonMark\Node\Block\Heading; use League\CommonMark\Node\NodeIterator; use League\CommonMark\Node\RawMarkupContainerInterface; use League\CommonMark\Node\StringContainerHelper; use League\CommonMark\Normalizer\TextNormalizerInterface; use League\Config\ConfigurationInterface; use League\Config\Exception\InvalidConfigurationException; /** * Searches the Document for Heading elements and adds HeadingPermalinks to each one */ final class HeadingPermalinkProcessor implements EnvironmentAwareInterface { public const INSERT_BEFORE = 'before'; public const INSERT_AFTER = 'after'; public const INSERT_NONE = 'none'; /** @psalm-readonly-allow-private-mutation */ private TextNormalizerInterface $slugNormalizer; /** @psalm-readonly-allow-private-mutation */ private ConfigurationInterface $config; public function setEnvironment(EnvironmentInterface $environment): void { $this->config = $environment->getConfiguration(); $this->slugNormalizer = $environment->getSlugNormalizer(); } public function __invoke(DocumentParsedEvent $e): void { $min = (int) $this->config->get('heading_permalink/min_heading_level'); $max = (int) $this->config->get('heading_permalink/max_heading_level'); $applyToHeading = (bool) $this->config->get('heading_permalink/apply_id_to_heading'); $idPrefix = (string) $this->config->get('heading_permalink/id_prefix'); $slugLength = (int) $this->config->get('slug_normalizer/max_length'); $headingClass = (string) $this->config->get('heading_permalink/heading_class'); if ($idPrefix !== '') { $idPrefix .= '-'; } foreach ($e->getDocument()->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) { if ($node instanceof Heading && $node->getLevel() >= $min && $node->getLevel() <= $max) { $this->addHeadingLink($node, $slugLength, $idPrefix, $applyToHeading, $headingClass); } } } private function addHeadingLink(Heading $heading, int $slugLength, string $idPrefix, bool $applyToHeading, string $headingClass): void { $text = StringContainerHelper::getChildText($heading, [RawMarkupContainerInterface::class]); $slug = $this->slugNormalizer->normalize($text, [ 'node' => $heading, 'length' => $slugLength, ]); if ($applyToHeading) { $heading->data->set('attributes/id', $idPrefix . $slug); } if ($headingClass !== '') { $heading->data->append('attributes/class', $headingClass); } $headingLinkAnchor = new HeadingPermalink($slug); switch ($this->config->get('heading_permalink/insert')) { case self::INSERT_BEFORE: $heading->prependChild($headingLinkAnchor); return; case self::INSERT_AFTER: $heading->appendChild($headingLinkAnchor); return; case self::INSERT_NONE: return; default: throw new InvalidConfigurationException("Invalid configuration value for heading_permalink/insert; expected 'before', 'after', or 'none'"); } } } commonmark/src/Extension/HeadingPermalink/HeadingPermalink.php 0000644 00000001346 15007532371 0020576 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\HeadingPermalink; use League\CommonMark\Node\Inline\AbstractInline; /** * Represents an anchor link within a heading */ final class HeadingPermalink extends AbstractInline { /** @psalm-readonly */ private string $slug; public function __construct(string $slug) { parent::__construct(); $this->slug = $slug; } public function getSlug(): string { return $this->slug; } } commonmark/src/Extension/Footnote/Node/Footnote.php 0000644 00000001626 15007532371 0016452 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Node; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Reference\ReferenceInterface; use League\CommonMark\Reference\ReferenceableInterface; final class Footnote extends AbstractBlock implements ReferenceableInterface { /** @psalm-readonly */ private ReferenceInterface $reference; public function __construct(ReferenceInterface $reference) { parent::__construct(); $this->reference = $reference; } public function getReference(): ReferenceInterface { return $this->reference; } } commonmark/src/Extension/Footnote/Node/FootnoteRef.php 0000644 00000002547 15007532371 0017112 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Node; use League\CommonMark\Node\Inline\AbstractInline; use League\CommonMark\Reference\ReferenceInterface; use League\CommonMark\Reference\ReferenceableInterface; final class FootnoteRef extends AbstractInline implements ReferenceableInterface { private ReferenceInterface $reference; /** @psalm-readonly */ private ?string $content = null; /** * @param array<mixed> $data */ public function __construct(ReferenceInterface $reference, ?string $content = null, array $data = []) { parent::__construct(); $this->reference = $reference; $this->content = $content; if (\count($data) > 0) { $this->data->import($data); } } public function getReference(): ReferenceInterface { return $this->reference; } public function setReference(ReferenceInterface $reference): void { $this->reference = $reference; } public function getContent(): ?string { return $this->content; } } commonmark/src/Extension/Footnote/Node/FootnoteBackref.php 0000644 00000001766 15007532371 0017735 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Node; use League\CommonMark\Node\Inline\AbstractInline; use League\CommonMark\Reference\ReferenceInterface; use League\CommonMark\Reference\ReferenceableInterface; /** * Link from the footnote on the bottom of the document back to the reference */ final class FootnoteBackref extends AbstractInline implements ReferenceableInterface { /** @psalm-readonly */ private ReferenceInterface $reference; public function __construct(ReferenceInterface $reference) { parent::__construct(); $this->reference = $reference; } public function getReference(): ReferenceInterface { return $this->reference; } } commonmark/src/Extension/Footnote/Node/FootnoteContainer.php 0000644 00000000723 15007532371 0020312 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Node; use League\CommonMark\Node\Block\AbstractBlock; final class FootnoteContainer extends AbstractBlock { } commonmark/src/Extension/Footnote/FootnoteExtension.php 0000644 00000006725 15007532371 0017467 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\ConfigurableExtensionInterface; use League\CommonMark\Extension\Footnote\Event\AnonymousFootnotesListener; use League\CommonMark\Extension\Footnote\Event\FixOrphanedFootnotesAndRefsListener; use League\CommonMark\Extension\Footnote\Event\GatherFootnotesListener; use League\CommonMark\Extension\Footnote\Event\NumberFootnotesListener; use League\CommonMark\Extension\Footnote\Node\Footnote; use League\CommonMark\Extension\Footnote\Node\FootnoteBackref; use League\CommonMark\Extension\Footnote\Node\FootnoteContainer; use League\CommonMark\Extension\Footnote\Node\FootnoteRef; use League\CommonMark\Extension\Footnote\Parser\AnonymousFootnoteRefParser; use League\CommonMark\Extension\Footnote\Parser\FootnoteRefParser; use League\CommonMark\Extension\Footnote\Parser\FootnoteStartParser; use League\CommonMark\Extension\Footnote\Renderer\FootnoteBackrefRenderer; use League\CommonMark\Extension\Footnote\Renderer\FootnoteContainerRenderer; use League\CommonMark\Extension\Footnote\Renderer\FootnoteRefRenderer; use League\CommonMark\Extension\Footnote\Renderer\FootnoteRenderer; use League\Config\ConfigurationBuilderInterface; use Nette\Schema\Expect; final class FootnoteExtension implements ConfigurableExtensionInterface { public function configureSchema(ConfigurationBuilderInterface $builder): void { $builder->addSchema('footnote', Expect::structure([ 'backref_class' => Expect::string('footnote-backref'), 'backref_symbol' => Expect::string('↩'), 'container_add_hr' => Expect::bool(true), 'container_class' => Expect::string('footnotes'), 'ref_class' => Expect::string('footnote-ref'), 'ref_id_prefix' => Expect::string('fnref:'), 'footnote_class' => Expect::string('footnote'), 'footnote_id_prefix' => Expect::string('fn:'), ])); } public function register(EnvironmentBuilderInterface $environment): void { $environment->addBlockStartParser(new FootnoteStartParser(), 51); $environment->addInlineParser(new AnonymousFootnoteRefParser(), 35); $environment->addInlineParser(new FootnoteRefParser(), 51); $environment->addRenderer(FootnoteContainer::class, new FootnoteContainerRenderer()); $environment->addRenderer(Footnote::class, new FootnoteRenderer()); $environment->addRenderer(FootnoteRef::class, new FootnoteRefRenderer()); $environment->addRenderer(FootnoteBackref::class, new FootnoteBackrefRenderer()); $environment->addEventListener(DocumentParsedEvent::class, [new AnonymousFootnotesListener(), 'onDocumentParsed'], 40); $environment->addEventListener(DocumentParsedEvent::class, [new FixOrphanedFootnotesAndRefsListener(), 'onDocumentParsed'], 30); $environment->addEventListener(DocumentParsedEvent::class, [new NumberFootnotesListener(), 'onDocumentParsed'], 20); $environment->addEventListener(DocumentParsedEvent::class, [new GatherFootnotesListener(), 'onDocumentParsed'], 10); } } commonmark/src/Extension/Footnote/Parser/AnonymousFootnoteRefParser.php 0000644 00000004161 15007532371 0022541 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Parser; use League\CommonMark\Environment\EnvironmentAwareInterface; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Extension\Footnote\Node\FootnoteRef; use League\CommonMark\Normalizer\TextNormalizerInterface; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; use League\CommonMark\Reference\Reference; use League\Config\ConfigurationInterface; final class AnonymousFootnoteRefParser implements InlineParserInterface, EnvironmentAwareInterface { private ConfigurationInterface $config; /** @psalm-readonly-allow-private-mutation */ private TextNormalizerInterface $slugNormalizer; public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::regex('\^\[([^\]]+)\]'); } public function parse(InlineParserContext $inlineContext): bool { $inlineContext->getCursor()->advanceBy($inlineContext->getFullMatchLength()); [$label] = $inlineContext->getSubMatches(); $reference = $this->createReference($label); $inlineContext->getContainer()->appendChild(new FootnoteRef($reference, $label)); return true; } private function createReference(string $label): Reference { $refLabel = $this->slugNormalizer->normalize($label, ['length' => 20]); return new Reference( $refLabel, '#' . $this->config->get('footnote/footnote_id_prefix') . $refLabel, $label ); } public function setEnvironment(EnvironmentInterface $environment): void { $this->config = $environment->getConfiguration(); $this->slugNormalizer = $environment->getSlugNormalizer(); } } commonmark/src/Extension/Footnote/Parser/FootnoteRefParser.php 0000644 00000003253 15007532371 0020631 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Parser; use League\CommonMark\Extension\Footnote\Node\FootnoteRef; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; use League\CommonMark\Reference\Reference; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class FootnoteRefParser implements InlineParserInterface, ConfigurationAwareInterface { private ConfigurationInterface $config; public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::regex('\[\^([^\s\]]+)\]'); } public function parse(InlineParserContext $inlineContext): bool { $inlineContext->getCursor()->advanceBy($inlineContext->getFullMatchLength()); [$label] = $inlineContext->getSubMatches(); $inlineContext->getContainer()->appendChild(new FootnoteRef($this->createReference($label))); return true; } private function createReference(string $label): Reference { return new Reference( $label, '#' . $this->config->get('footnote/footnote_id_prefix') . $label, $label ); } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } } commonmark/src/Extension/Footnote/Parser/FootnoteStartParser.php 0000644 00000003405 15007532371 0021211 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Parser; use League\CommonMark\Parser\Block\BlockStart; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; use League\CommonMark\Reference\Reference; use League\CommonMark\Util\RegexHelper; final class FootnoteStartParser implements BlockStartParserInterface { public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart { if ($cursor->isIndented() || $parserState->getLastMatchedBlockParser()->canHaveLazyContinuationLines()) { return BlockStart::none(); } $match = RegexHelper::matchFirst( '/^\[\^([^\s^\]]+)\]\:(?:\s|$)/', $cursor->getLine(), $cursor->getNextNonSpacePosition() ); if (! $match) { return BlockStart::none(); } $cursor->advanceToNextNonSpaceOrTab(); $cursor->advanceBy(\strlen($match[0])); $str = $cursor->getRemainder(); \preg_replace('/^\[\^([^\s^\]]+)\]\:(?:\s|$)/', '', $str); if (\preg_match('/^\[\^([^\s^\]]+)\]\:(?:\s|$)/', $match[0], $matches) !== 1) { return BlockStart::none(); } $reference = new Reference($matches[1], $matches[1], $matches[1]); $footnoteParser = new FootnoteParser($reference); return BlockStart::of($footnoteParser)->at($cursor); } } commonmark/src/Extension/Footnote/Parser/FootnoteParser.php 0000644 00000003404 15007532371 0020172 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Parser; use League\CommonMark\Extension\Footnote\Node\Footnote; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Parser\Block\AbstractBlockContinueParser; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Reference\ReferenceInterface; final class FootnoteParser extends AbstractBlockContinueParser { /** @psalm-readonly */ private Footnote $block; /** @psalm-readonly-allow-private-mutation */ private ?int $indentation = null; public function __construct(ReferenceInterface $reference) { $this->block = new Footnote($reference); } public function getBlock(): Footnote { return $this->block; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { if ($cursor->isBlank()) { return BlockContinue::at($cursor); } if ($cursor->isIndented()) { $this->indentation ??= $cursor->getIndent(); $cursor->advanceBy($this->indentation, true); return BlockContinue::at($cursor); } return BlockContinue::none(); } public function isContainer(): bool { return true; } public function canContain(AbstractBlock $childBlock): bool { return true; } } commonmark/src/Extension/Footnote/Renderer/FootnoteRefRenderer.php 0000644 00000004701 15007532371 0021454 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Renderer; use League\CommonMark\Extension\Footnote\Node\FootnoteRef; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class FootnoteRefRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface { private ConfigurationInterface $config; /** * @param FootnoteRef $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { FootnoteRef::assertInstanceOf($node); $attrs = $node->data->getData('attributes'); $attrs->append('class', $this->config->get('footnote/ref_class')); $attrs->set('href', \mb_strtolower($node->getReference()->getDestination(), 'UTF-8')); $attrs->set('role', 'doc-noteref'); $idPrefix = $this->config->get('footnote/ref_id_prefix'); return new HtmlElement( 'sup', [ 'id' => $idPrefix . \mb_strtolower($node->getReference()->getLabel(), 'UTF-8'), ], new HtmlElement( 'a', $attrs->export(), $node->getReference()->getTitle() ), true ); } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } public function getXmlTagName(Node $node): string { return 'footnote_ref'; } /** * @param FootnoteRef $node * * @return array<string, scalar> * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlAttributes(Node $node): array { FootnoteRef::assertInstanceOf($node); return [ 'reference' => $node->getReference()->getLabel(), ]; } } commonmark/src/Extension/Footnote/Renderer/FootnoteRenderer.php 0000644 00000004341 15007532371 0021017 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Renderer; use League\CommonMark\Extension\Footnote\Node\Footnote; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class FootnoteRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface { private ConfigurationInterface $config; /** * @param Footnote $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { Footnote::assertInstanceOf($node); $attrs = $node->data->getData('attributes'); $attrs->append('class', $this->config->get('footnote/footnote_class')); $attrs->set('id', $this->config->get('footnote/footnote_id_prefix') . \mb_strtolower($node->getReference()->getLabel(), 'UTF-8')); $attrs->set('role', 'doc-endnote'); return new HtmlElement( 'li', $attrs->export(), $childRenderer->renderNodes($node->children()), true ); } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } public function getXmlTagName(Node $node): string { return 'footnote'; } /** * @param Footnote $node * * @return array<string, scalar> * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlAttributes(Node $node): array { Footnote::assertInstanceOf($node); return [ 'reference' => $node->getReference()->getLabel(), ]; } } commonmark/src/Extension/Footnote/Renderer/FootnoteContainerRenderer.php 0000644 00000004101 15007532371 0022654 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Renderer; use League\CommonMark\Extension\Footnote\Node\FootnoteContainer; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class FootnoteContainerRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface { private ConfigurationInterface $config; /** * @param FootnoteContainer $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { FootnoteContainer::assertInstanceOf($node); $attrs = $node->data->getData('attributes'); $attrs->append('class', $this->config->get('footnote/container_class')); $attrs->set('role', 'doc-endnotes'); $contents = new HtmlElement('ol', [], $childRenderer->renderNodes($node->children())); if ($this->config->get('footnote/container_add_hr')) { $contents = [new HtmlElement('hr', [], null, true), $contents]; } return new HtmlElement('div', $attrs->export(), $contents); } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } public function getXmlTagName(Node $node): string { return 'footnote_container'; } /** * @return array<string, scalar> */ public function getXmlAttributes(Node $node): array { return []; } } commonmark/src/Extension/Footnote/Renderer/FootnoteBackrefRenderer.php 0000644 00000004530 15007532371 0022275 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Renderer; use League\CommonMark\Extension\Footnote\Node\FootnoteBackref; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class FootnoteBackrefRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface { public const DEFAULT_SYMBOL = '↩'; private ConfigurationInterface $config; /** * @param FootnoteBackref $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): string { FootnoteBackref::assertInstanceOf($node); $attrs = $node->data->getData('attributes'); $attrs->append('class', $this->config->get('footnote/backref_class')); $attrs->set('rev', 'footnote'); $attrs->set('href', \mb_strtolower($node->getReference()->getDestination(), 'UTF-8')); $attrs->set('role', 'doc-backlink'); $symbol = $this->config->get('footnote/backref_symbol'); \assert(\is_string($symbol)); return ' ' . new HtmlElement('a', $attrs->export(), \htmlspecialchars($symbol), true); } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } public function getXmlTagName(Node $node): string { return 'footnote_backref'; } /** * @param FootnoteBackref $node * * @return array<string, scalar> * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlAttributes(Node $node): array { FootnoteBackref::assertInstanceOf($node); return [ 'reference' => $node->getReference()->getLabel(), ]; } } commonmark/src/Extension/Footnote/Event/GatherFootnotesListener.php 0000644 00000006727 15007532371 0021701 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Event; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\Footnote\Node\Footnote; use League\CommonMark\Extension\Footnote\Node\FootnoteBackref; use League\CommonMark\Extension\Footnote\Node\FootnoteContainer; use League\CommonMark\Node\Block\Document; use League\CommonMark\Node\NodeIterator; use League\CommonMark\Reference\Reference; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class GatherFootnotesListener implements ConfigurationAwareInterface { private ConfigurationInterface $config; public function onDocumentParsed(DocumentParsedEvent $event): void { $document = $event->getDocument(); $footnotes = []; foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) { if (! $node instanceof Footnote) { continue; } // Look for existing reference with footnote label $ref = $document->getReferenceMap()->get($node->getReference()->getLabel()); if ($ref !== null) { // Use numeric title to get footnotes order $footnotes[(int) $ref->getTitle()] = $node; } else { // Footnote call is missing, append footnote at the end $footnotes[\PHP_INT_MAX] = $node; } $key = '#' . $this->config->get('footnote/footnote_id_prefix') . $node->getReference()->getDestination(); if ($document->data->has($key)) { $this->createBackrefs($node, $document->data->get($key)); } } // Only add a footnote container if there are any if (\count($footnotes) === 0) { return; } $container = $this->getFootnotesContainer($document); \ksort($footnotes); foreach ($footnotes as $footnote) { $container->appendChild($footnote); } } private function getFootnotesContainer(Document $document): FootnoteContainer { $footnoteContainer = new FootnoteContainer(); $document->appendChild($footnoteContainer); return $footnoteContainer; } /** * Look for all footnote refs pointing to this footnote and create each footnote backrefs. * * @param Footnote $node The target footnote * @param Reference[] $backrefs References to create backrefs for */ private function createBackrefs(Footnote $node, array $backrefs): void { // Backrefs should be added to the child paragraph $target = $node->lastChild(); if ($target === null) { // This should never happen, but you never know $target = $node; } foreach ($backrefs as $backref) { $target->appendChild(new FootnoteBackref(new Reference( $backref->getLabel(), '#' . $this->config->get('footnote/ref_id_prefix') . $backref->getLabel(), $backref->getTitle() ))); } } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } } commonmark/src/Extension/Footnote/Event/FixOrphanedFootnotesAndRefsListener.php 0000644 00000004461 15007532371 0024132 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Event; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\Footnote\Node\Footnote; use League\CommonMark\Extension\Footnote\Node\FootnoteRef; use League\CommonMark\Node\Block\Document; use League\CommonMark\Node\Inline\Text; final class FixOrphanedFootnotesAndRefsListener { public function onDocumentParsed(DocumentParsedEvent $event): void { $document = $event->getDocument(); $map = $this->buildMapOfKnownFootnotesAndRefs($document); foreach ($map['_flat'] as $node) { if ($node instanceof FootnoteRef && ! isset($map[Footnote::class][$node->getReference()->getLabel()])) { // Found an orphaned FootnoteRef without a corresponding Footnote // Restore the original footnote ref text $node->replaceWith(new Text(\sprintf('[^%s]', $node->getReference()->getLabel()))); } // phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed if ($node instanceof Footnote && ! isset($map[FootnoteRef::class][$node->getReference()->getLabel()])) { // Found an orphaned Footnote without a corresponding FootnoteRef // Remove the footnote $node->detach(); } } } /** @phpstan-ignore-next-line */ private function buildMapOfKnownFootnotesAndRefs(Document $document): array // @phpcs:ignore { $map = [ Footnote::class => [], FootnoteRef::class => [], '_flat' => [], ]; foreach ($document->iterator() as $node) { if ($node instanceof Footnote) { $map[Footnote::class][$node->getReference()->getLabel()] = true; $map['_flat'][] = $node; } elseif ($node instanceof FootnoteRef) { $map[FootnoteRef::class][$node->getReference()->getLabel()] = true; $map['_flat'][] = $node; } } return $map; } } commonmark/src/Extension/Footnote/Event/NumberFootnotesListener.php 0000644 00000004446 15007532371 0021713 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Event; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\Footnote\Node\FootnoteRef; use League\CommonMark\Reference\Reference; final class NumberFootnotesListener { public function onDocumentParsed(DocumentParsedEvent $event): void { $document = $event->getDocument(); $nextCounter = 1; $usedLabels = []; $usedCounters = []; foreach ($document->iterator() as $node) { if (! $node instanceof FootnoteRef) { continue; } $existingReference = $node->getReference(); $label = $existingReference->getLabel(); $counter = $nextCounter; $canIncrementCounter = true; if (\array_key_exists($label, $usedLabels)) { /* * Reference is used again, we need to point * to the same footnote. But with a different ID */ $counter = $usedCounters[$label]; $label .= '__' . ++$usedLabels[$label]; $canIncrementCounter = false; } // rewrite reference title to use a numeric link $newReference = new Reference( $label, $existingReference->getDestination(), (string) $counter ); // Override reference with numeric link $node->setReference($newReference); $document->getReferenceMap()->add($newReference); /* * Store created references in document for * creating FootnoteBackrefs */ $document->data->append($existingReference->getDestination(), $newReference); $usedLabels[$label] = 1; $usedCounters[$label] = $nextCounter; if ($canIncrementCounter) { $nextCounter++; } } } } commonmark/src/Extension/Footnote/Event/AnonymousFootnotesListener.php 0000644 00000004066 15007532371 0022451 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * (c) Rezo Zero / Ambroise Maupate * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\Footnote\Event; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\Footnote\Node\Footnote; use League\CommonMark\Extension\Footnote\Node\FootnoteBackref; use League\CommonMark\Extension\Footnote\Node\FootnoteRef; use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Reference\Reference; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class AnonymousFootnotesListener implements ConfigurationAwareInterface { private ConfigurationInterface $config; public function onDocumentParsed(DocumentParsedEvent $event): void { $document = $event->getDocument(); foreach ($document->iterator() as $node) { if (! $node instanceof FootnoteRef || ($text = $node->getContent()) === null) { continue; } // Anonymous footnote needs to create a footnote from its content $existingReference = $node->getReference(); $newReference = new Reference( $existingReference->getLabel(), '#' . $this->config->get('footnote/ref_id_prefix') . $existingReference->getLabel(), $existingReference->getTitle() ); $paragraph = new Paragraph(); $paragraph->appendChild(new Text($text)); $paragraph->appendChild(new FootnoteBackref($newReference)); $footnote = new Footnote($newReference); $footnote->appendChild($paragraph); $document->appendChild($footnote); } } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } } commonmark/src/Extension/GithubFlavoredMarkdownExtension.php 0000644 00000002235 15007532371 0020475 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Extension\Autolink\AutolinkExtension; use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension; use League\CommonMark\Extension\Strikethrough\StrikethroughExtension; use League\CommonMark\Extension\Table\TableExtension; use League\CommonMark\Extension\TaskList\TaskListExtension; final class GithubFlavoredMarkdownExtension implements ExtensionInterface { public function register(EnvironmentBuilderInterface $environment): void { $environment->addExtension(new AutolinkExtension()); $environment->addExtension(new DisallowedRawHtmlExtension()); $environment->addExtension(new StrikethroughExtension()); $environment->addExtension(new TableExtension()); $environment->addExtension(new TaskListExtension()); } } commonmark/src/Extension/DisallowedRawHtml/DisallowedRawHtmlRenderer.php 0000644 00000003603 15007532371 0022633 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\DisallowedRawHtml; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class DisallowedRawHtmlRenderer implements NodeRendererInterface, ConfigurationAwareInterface { /** @psalm-readonly */ private NodeRendererInterface $innerRenderer; /** @psalm-readonly-allow-private-mutation */ private ConfigurationInterface $config; public function __construct(NodeRendererInterface $innerRenderer) { $this->innerRenderer = $innerRenderer; } public function render(Node $node, ChildNodeRendererInterface $childRenderer): ?string { $rendered = (string) $this->innerRenderer->render($node, $childRenderer); if ($rendered === '') { return ''; } $tags = (array) $this->config->get('disallowed_raw_html/disallowed_tags'); if (\count($tags) === 0) { return $rendered; } $regex = \sprintf('/<(\/?(?:%s)[ \/>])/i', \implode('|', \array_map('preg_quote', $tags))); // Match these types of tags: <title> </title> <title x="sdf"> <title/> <title /> return \preg_replace($regex, '<$1', $rendered); } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; if ($this->innerRenderer instanceof ConfigurationAwareInterface) { $this->innerRenderer->setConfiguration($configuration); } } } commonmark/src/Extension/DisallowedRawHtml/DisallowedRawHtmlExtension.php 0000644 00000003315 15007532371 0023041 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\DisallowedRawHtml; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock; use League\CommonMark\Extension\CommonMark\Node\Inline\HtmlInline; use League\CommonMark\Extension\CommonMark\Renderer\Block\HtmlBlockRenderer; use League\CommonMark\Extension\CommonMark\Renderer\Inline\HtmlInlineRenderer; use League\CommonMark\Extension\ConfigurableExtensionInterface; use League\Config\ConfigurationBuilderInterface; use Nette\Schema\Expect; final class DisallowedRawHtmlExtension implements ConfigurableExtensionInterface { private const DEFAULT_DISALLOWED_TAGS = [ 'title', 'textarea', 'style', 'xmp', 'iframe', 'noembed', 'noframes', 'script', 'plaintext', ]; public function configureSchema(ConfigurationBuilderInterface $builder): void { $builder->addSchema('disallowed_raw_html', Expect::structure([ 'disallowed_tags' => Expect::listOf('string')->default(self::DEFAULT_DISALLOWED_TAGS)->mergeDefaults(false), ])); } public function register(EnvironmentBuilderInterface $environment): void { $environment->addRenderer(HtmlBlock::class, new DisallowedRawHtmlRenderer(new HtmlBlockRenderer()), 50); $environment->addRenderer(HtmlInline::class, new DisallowedRawHtmlRenderer(new HtmlInlineRenderer()), 50); } } commonmark/src/Extension/CommonMark/Delimiter/Processor/EmphasisDelimiterProcessor.php 0000644 00000006225 15007532371 0025443 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java) * - (c) Atlassian Pty Ltd * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Delimiter\Processor; use League\CommonMark\Delimiter\DelimiterInterface; use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface; use League\CommonMark\Extension\CommonMark\Node\Inline\Emphasis; use League\CommonMark\Extension\CommonMark\Node\Inline\Strong; use League\CommonMark\Node\Inline\AbstractStringContainer; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class EmphasisDelimiterProcessor implements DelimiterProcessorInterface, ConfigurationAwareInterface { /** @psalm-readonly */ private string $char; /** @psalm-readonly-allow-private-mutation */ private ConfigurationInterface $config; /** * @param string $char The emphasis character to use (typically '*' or '_') */ public function __construct(string $char) { $this->char = $char; } public function getOpeningCharacter(): string { return $this->char; } public function getClosingCharacter(): string { return $this->char; } public function getMinLength(): int { return 1; } public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int { // "Multiple of 3" rule for internal delimiter runs if (($opener->canClose() || $closer->canOpen()) && $closer->getOriginalLength() % 3 !== 0 && ($opener->getOriginalLength() + $closer->getOriginalLength()) % 3 === 0) { return 0; } // Calculate actual number of delimiters used from this closer if ($opener->getLength() >= 2 && $closer->getLength() >= 2) { if ($this->config->get('commonmark/enable_strong')) { return 2; } return 0; } if ($this->config->get('commonmark/enable_em')) { return 1; } return 0; } public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void { if ($delimiterUse === 1) { $emphasis = new Emphasis($this->char); } elseif ($delimiterUse === 2) { $emphasis = new Strong($this->char . $this->char); } else { return; } $next = $opener->next(); while ($next !== null && $next !== $closer) { $tmp = $next->next(); $emphasis->appendChild($next); $next = $tmp; } $opener->insertAfter($emphasis); } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } } commonmark/src/Extension/CommonMark/Node/Inline/Emphasis.php 0000644 00000001756 15007532371 0020116 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Node\Inline; use League\CommonMark\Node\Inline\AbstractInline; use League\CommonMark\Node\Inline\DelimitedInterface; final class Emphasis extends AbstractInline implements DelimitedInterface { private string $delimiter; public function __construct(string $delimiter = '_') { parent::__construct(); $this->delimiter = $delimiter; } public function getOpeningDelimiter(): string { return $this->delimiter; } public function getClosingDelimiter(): string { return $this->delimiter; } } commonmark/src/Extension/CommonMark/Node/Inline/Image.php 0000644 00000002125 15007532371 0017356 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Node\Inline; use League\CommonMark\Node\Inline\Text; class Image extends AbstractWebResource { protected ?string $title = null; public function __construct(string $url, ?string $label = null, ?string $title = null) { parent::__construct($url); if ($label !== null && $label !== '') { $this->appendChild(new Text($label)); } $this->title = $title; } public function getTitle(): ?string { if ($this->title === '') { return null; } return $this->title; } public function setTitle(?string $title): void { $this->title = $title; } } commonmark/src/Extension/CommonMark/Node/Inline/Code.php 0000644 00000001066 15007532371 0017211 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Node\Inline; use League\CommonMark\Node\Inline\AbstractStringContainer; class Code extends AbstractStringContainer { } commonmark/src/Extension/CommonMark/Node/Inline/Link.php 0000644 00000002124 15007532371 0017230 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Node\Inline; use League\CommonMark\Node\Inline\Text; class Link extends AbstractWebResource { protected ?string $title = null; public function __construct(string $url, ?string $label = null, ?string $title = null) { parent::__construct($url); if ($label !== null && $label !== '') { $this->appendChild(new Text($label)); } $this->title = $title; } public function getTitle(): ?string { if ($this->title === '') { return null; } return $this->title; } public function setTitle(?string $title): void { $this->title = $title; } } commonmark/src/Extension/CommonMark/Node/Inline/AbstractWebResource.php 0000644 00000001557 15007532371 0022255 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Node\Inline; use League\CommonMark\Node\Inline\AbstractInline; abstract class AbstractWebResource extends AbstractInline { protected string $url; public function __construct(string $url) { parent::__construct(); $this->url = $url; } public function getUrl(): string { return $this->url; } public function setUrl(string $url): void { $this->url = $url; } } commonmark/src/Extension/CommonMark/Node/Inline/Strong.php 0000644 00000001755 15007532371 0017620 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Node\Inline; use League\CommonMark\Node\Inline\AbstractInline; use League\CommonMark\Node\Inline\DelimitedInterface; final class Strong extends AbstractInline implements DelimitedInterface { private string $delimiter; public function __construct(string $delimiter = '**') { parent::__construct(); $this->delimiter = $delimiter; } public function getOpeningDelimiter(): string { return $this->delimiter; } public function getClosingDelimiter(): string { return $this->delimiter; } } commonmark/src/Extension/CommonMark/Node/Inline/HtmlInline.php 0000644 00000001241 15007532371 0020375 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Node\Inline; use League\CommonMark\Node\Inline\AbstractStringContainer; use League\CommonMark\Node\RawMarkupContainerInterface; final class HtmlInline extends AbstractStringContainer implements RawMarkupContainerInterface { } commonmark/src/Extension/CommonMark/Node/Block/HtmlBlock.php 0000644 00000003447 15007532371 0020037 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Node\Block; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Node\RawMarkupContainerInterface; final class HtmlBlock extends AbstractBlock implements RawMarkupContainerInterface { // Any changes to these constants should be reflected in .phpstorm.meta.php public const TYPE_1_CODE_CONTAINER = 1; public const TYPE_2_COMMENT = 2; public const TYPE_3 = 3; public const TYPE_4 = 4; public const TYPE_5_CDATA = 5; public const TYPE_6_BLOCK_ELEMENT = 6; public const TYPE_7_MISC_ELEMENT = 7; /** * @psalm-var self::TYPE_* $type * @phpstan-var self::TYPE_* $type */ private int $type; private string $literal = ''; /** * @psalm-param self::TYPE_* $type * * @phpstan-param self::TYPE_* $type */ public function __construct(int $type) { parent::__construct(); $this->type = $type; } /** * @psalm-return self::TYPE_* * * @phpstan-return self::TYPE_* */ public function getType(): int { return $this->type; } /** * @psalm-param self::TYPE_* $type * * @phpstan-param self::TYPE_* $type */ public function setType(int $type): void { $this->type = $type; } public function getLiteral(): string { return $this->literal; } public function setLiteral(string $literal): void { $this->literal = $literal; } } commonmark/src/Extension/CommonMark/Node/Block/ListBlock.php 0000644 00000002416 15007532371 0020041 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Node\Block; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Node\Block\TightBlockInterface; class ListBlock extends AbstractBlock implements TightBlockInterface { public const TYPE_BULLET = 'bullet'; public const TYPE_ORDERED = 'ordered'; public const DELIM_PERIOD = 'period'; public const DELIM_PAREN = 'paren'; protected bool $tight = false; /** @psalm-readonly */ protected ListData $listData; public function __construct(ListData $listData) { parent::__construct(); $this->listData = $listData; } public function getListData(): ListData { return $this->listData; } public function isTight(): bool { return $this->tight; } public function setTight(bool $tight): void { $this->tight = $tight; } } commonmark/src/Extension/CommonMark/Node/Block/Heading.php 0000644 00000001542 15007532371 0017511 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Node\Block; use League\CommonMark\Node\Block\AbstractBlock; final class Heading extends AbstractBlock { private int $level; public function __construct(int $level) { parent::__construct(); $this->level = $level; } public function getLevel(): int { return $this->level; } public function setLevel(int $level): void { $this->level = $level; } } commonmark/src/Extension/CommonMark/Node/Block/ListData.php 0000644 00000002070 15007532371 0017654 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Node\Block; class ListData { public ?int $start = null; public int $padding = 0; /** * @psalm-var ListBlock::TYPE_* * @phpstan-var ListBlock::TYPE_* */ public string $type; /** * @psalm-var ListBlock::DELIM_*|null * @phpstan-var ListBlock::DELIM_*|null */ public ?string $delimiter = null; public ?string $bulletChar = null; public int $markerOffset; public function equals(ListData $data): bool { return $this->type === $data->type && $this->delimiter === $data->delimiter && $this->bulletChar === $data->bulletChar; } } commonmark/src/Extension/CommonMark/Node/Block/ListItem.php 0000644 00000001500 15007532371 0017676 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Node\Block; use League\CommonMark\Node\Block\AbstractBlock; class ListItem extends AbstractBlock { /** @psalm-readonly */ protected ListData $listData; public function __construct(ListData $listData) { parent::__construct(); $this->listData = $listData; } public function getListData(): ListData { return $this->listData; } } commonmark/src/Extension/CommonMark/Node/Block/IndentedCode.php 0000644 00000001350 15007532371 0020474 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Node\Block; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Node\StringContainerInterface; final class IndentedCode extends AbstractBlock implements StringContainerInterface { private string $literal = ''; public function getLiteral(): string { return $this->literal; } public function setLiteral(string $literal): void { $this->literal = $literal; } } commonmark/src/Extension/CommonMark/Node/Block/BlockQuote.php 0000644 00000000652 15007532371 0020223 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Node\Block; use League\CommonMark\Node\Block\AbstractBlock; class BlockQuote extends AbstractBlock { } commonmark/src/Extension/CommonMark/Node/Block/ThematicBreak.php 0000644 00000000655 15007532371 0020661 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Node\Block; use League\CommonMark\Node\Block\AbstractBlock; class ThematicBreak extends AbstractBlock { } commonmark/src/Extension/CommonMark/Node/Block/FencedCode.php 0000644 00000003770 15007532371 0020136 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Node\Block; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Node\StringContainerInterface; final class FencedCode extends AbstractBlock implements StringContainerInterface { private ?string $info = null; private string $literal = ''; private int $length; private string $char; private int $offset; public function __construct(int $length, string $char, int $offset) { parent::__construct(); $this->length = $length; $this->char = $char; $this->offset = $offset; } public function getInfo(): ?string { return $this->info; } /** * @return string[] */ public function getInfoWords(): array { return \preg_split('/\s+/', $this->info ?? '') ?: []; } public function setInfo(string $info): void { $this->info = $info; } public function getLiteral(): string { return $this->literal; } public function setLiteral(string $literal): void { $this->literal = $literal; } public function getChar(): string { return $this->char; } public function setChar(string $char): void { $this->char = $char; } public function getLength(): int { return $this->length; } public function setLength(int $length): void { $this->length = $length; } public function getOffset(): int { return $this->offset; } public function setOffset(int $offset): void { $this->offset = $offset; } } commonmark/src/Extension/CommonMark/CommonMarkCoreExtension.php 0000644 00000012125 15007532371 0021003 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Extension\CommonMark\Delimiter\Processor\EmphasisDelimiterProcessor; use League\CommonMark\Extension\ConfigurableExtensionInterface; use League\CommonMark\Node as CoreNode; use League\CommonMark\Parser as CoreParser; use League\CommonMark\Renderer as CoreRenderer; use League\Config\ConfigurationBuilderInterface; use Nette\Schema\Expect; final class CommonMarkCoreExtension implements ConfigurableExtensionInterface { public function configureSchema(ConfigurationBuilderInterface $builder): void { $builder->addSchema('commonmark', Expect::structure([ 'use_asterisk' => Expect::bool(true), 'use_underscore' => Expect::bool(true), 'enable_strong' => Expect::bool(true), 'enable_em' => Expect::bool(true), 'unordered_list_markers' => Expect::listOf('string')->min(1)->default(['*', '+', '-'])->mergeDefaults(false), ])); } // phpcs:disable Generic.Functions.FunctionCallArgumentSpacing.TooMuchSpaceAfterComma,Squiz.WhiteSpace.SemicolonSpacing.Incorrect public function register(EnvironmentBuilderInterface $environment): void { $environment ->addBlockStartParser(new Parser\Block\BlockQuoteStartParser(), 70) ->addBlockStartParser(new Parser\Block\HeadingStartParser(), 60) ->addBlockStartParser(new Parser\Block\FencedCodeStartParser(), 50) ->addBlockStartParser(new Parser\Block\HtmlBlockStartParser(), 40) ->addBlockStartParser(new Parser\Block\ThematicBreakStartParser(), 20) ->addBlockStartParser(new Parser\Block\ListBlockStartParser(), 10) ->addBlockStartParser(new Parser\Block\IndentedCodeStartParser(), -100) ->addInlineParser(new CoreParser\Inline\NewlineParser(), 200) ->addInlineParser(new Parser\Inline\BacktickParser(), 150) ->addInlineParser(new Parser\Inline\EscapableParser(), 80) ->addInlineParser(new Parser\Inline\EntityParser(), 70) ->addInlineParser(new Parser\Inline\AutolinkParser(), 50) ->addInlineParser(new Parser\Inline\HtmlInlineParser(), 40) ->addInlineParser(new Parser\Inline\CloseBracketParser(), 30) ->addInlineParser(new Parser\Inline\OpenBracketParser(), 20) ->addInlineParser(new Parser\Inline\BangParser(), 10) ->addRenderer(Node\Block\BlockQuote::class, new Renderer\Block\BlockQuoteRenderer(), 0) ->addRenderer(CoreNode\Block\Document::class, new CoreRenderer\Block\DocumentRenderer(), 0) ->addRenderer(Node\Block\FencedCode::class, new Renderer\Block\FencedCodeRenderer(), 0) ->addRenderer(Node\Block\Heading::class, new Renderer\Block\HeadingRenderer(), 0) ->addRenderer(Node\Block\HtmlBlock::class, new Renderer\Block\HtmlBlockRenderer(), 0) ->addRenderer(Node\Block\IndentedCode::class, new Renderer\Block\IndentedCodeRenderer(), 0) ->addRenderer(Node\Block\ListBlock::class, new Renderer\Block\ListBlockRenderer(), 0) ->addRenderer(Node\Block\ListItem::class, new Renderer\Block\ListItemRenderer(), 0) ->addRenderer(CoreNode\Block\Paragraph::class, new CoreRenderer\Block\ParagraphRenderer(), 0) ->addRenderer(Node\Block\ThematicBreak::class, new Renderer\Block\ThematicBreakRenderer(), 0) ->addRenderer(Node\Inline\Code::class, new Renderer\Inline\CodeRenderer(), 0) ->addRenderer(Node\Inline\Emphasis::class, new Renderer\Inline\EmphasisRenderer(), 0) ->addRenderer(Node\Inline\HtmlInline::class, new Renderer\Inline\HtmlInlineRenderer(), 0) ->addRenderer(Node\Inline\Image::class, new Renderer\Inline\ImageRenderer(), 0) ->addRenderer(Node\Inline\Link::class, new Renderer\Inline\LinkRenderer(), 0) ->addRenderer(CoreNode\Inline\Newline::class, new CoreRenderer\Inline\NewlineRenderer(), 0) ->addRenderer(Node\Inline\Strong::class, new Renderer\Inline\StrongRenderer(), 0) ->addRenderer(CoreNode\Inline\Text::class, new CoreRenderer\Inline\TextRenderer(), 0) ; if ($environment->getConfiguration()->get('commonmark/use_asterisk')) { $environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('*')); } if ($environment->getConfiguration()->get('commonmark/use_underscore')) { $environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('_')); } } } commonmark/src/Extension/CommonMark/Parser/Inline/HtmlInlineParser.php 0000644 00000002375 15007532371 0022132 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Inline; use League\CommonMark\Extension\CommonMark\Node\Inline\HtmlInline; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; use League\CommonMark\Util\RegexHelper; final class HtmlInlineParser implements InlineParserInterface { public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::regex(RegexHelper::PARTIAL_HTMLTAG)->caseSensitive(); } public function parse(InlineParserContext $inlineContext): bool { $inline = $inlineContext->getFullMatch(); $inlineContext->getCursor()->advanceBy($inlineContext->getFullMatchLength()); $inlineContext->getContainer()->appendChild(new HtmlInline($inline)); return true; } } commonmark/src/Extension/CommonMark/Parser/Inline/OpenBracketParser.php 0000644 00000002541 15007532371 0022257 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Inline; use League\CommonMark\Delimiter\Delimiter; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; final class OpenBracketParser implements InlineParserInterface { public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::string('['); } public function parse(InlineParserContext $inlineContext): bool { $inlineContext->getCursor()->advanceBy(1); $node = new Text('[', ['delim' => true]); $inlineContext->getContainer()->appendChild($node); // Add entry to stack for this opener $delimiter = new Delimiter('[', 1, $node, true, false, $inlineContext->getCursor()->getPosition()); $inlineContext->getDelimiterStack()->push($delimiter); return true; } } commonmark/src/Extension/CommonMark/Parser/Inline/EntityParser.php 0000644 00000002421 15007532371 0021333 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Inline; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; use League\CommonMark\Util\Html5EntityDecoder; use League\CommonMark\Util\RegexHelper; final class EntityParser implements InlineParserInterface { public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::regex(RegexHelper::PARTIAL_ENTITY); } public function parse(InlineParserContext $inlineContext): bool { $entity = $inlineContext->getFullMatch(); $inlineContext->getCursor()->advanceBy($inlineContext->getFullMatchLength()); $inlineContext->getContainer()->appendChild(new Text(Html5EntityDecoder::decode($entity))); return true; } } commonmark/src/Extension/CommonMark/Parser/Inline/BacktickParser.php 0000644 00000004136 15007532371 0021577 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Inline; use League\CommonMark\Extension\CommonMark\Node\Inline\Code; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; final class BacktickParser implements InlineParserInterface { public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::regex('`+'); } public function parse(InlineParserContext $inlineContext): bool { $ticks = $inlineContext->getFullMatch(); $cursor = $inlineContext->getCursor(); $cursor->advanceBy($inlineContext->getFullMatchLength()); $currentPosition = $cursor->getPosition(); $previousState = $cursor->saveState(); while ($matchingTicks = $cursor->match('/`+/m')) { if ($matchingTicks !== $ticks) { continue; } $code = $cursor->getSubstring($currentPosition, $cursor->getPosition() - $currentPosition - \strlen($ticks)); $c = \preg_replace('/\n/m', ' ', $code) ?? ''; if ( $c !== '' && $c[0] === ' ' && \substr($c, -1, 1) === ' ' && \preg_match('/[^ ]/', $c) ) { $c = \substr($c, 1, -1); } $inlineContext->getContainer()->appendChild(new Code($c)); return true; } // If we got here, we didn't match a closing backtick sequence $cursor->restoreState($previousState); $inlineContext->getContainer()->appendChild(new Text($ticks)); return true; } } commonmark/src/Extension/CommonMark/Parser/Inline/BangParser.php 0000644 00000002544 15007532371 0020734 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Inline; use League\CommonMark\Delimiter\Delimiter; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; final class BangParser implements InlineParserInterface { public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::string('!['); } public function parse(InlineParserContext $inlineContext): bool { $cursor = $inlineContext->getCursor(); $cursor->advanceBy(2); $node = new Text('![', ['delim' => true]); $inlineContext->getContainer()->appendChild($node); // Add entry to stack for this opener $delimiter = new Delimiter('!', 1, $node, true, false, $cursor->getPosition()); $inlineContext->getDelimiterStack()->push($delimiter); return true; } } commonmark/src/Extension/CommonMark/Parser/Inline/CloseBracketParser.php 0000644 00000016323 15007532371 0022426 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Inline; use League\CommonMark\Environment\EnvironmentAwareInterface; use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Extension\CommonMark\Node\Inline\AbstractWebResource; use League\CommonMark\Extension\CommonMark\Node\Inline\Image; use League\CommonMark\Extension\CommonMark\Node\Inline\Link; use League\CommonMark\Extension\Mention\Mention; use League\CommonMark\Node\Inline\AdjacentTextMerger; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; use League\CommonMark\Reference\ReferenceInterface; use League\CommonMark\Reference\ReferenceMapInterface; use League\CommonMark\Util\LinkParserHelper; use League\CommonMark\Util\RegexHelper; final class CloseBracketParser implements InlineParserInterface, EnvironmentAwareInterface { /** @psalm-readonly-allow-private-mutation */ private EnvironmentInterface $environment; public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::string(']'); } public function parse(InlineParserContext $inlineContext): bool { // Look through stack of delimiters for a [ or ! $opener = $inlineContext->getDelimiterStack()->searchByCharacter(['[', '!']); if ($opener === null) { return false; } if (! $opener->isActive()) { // no matched opener; remove from emphasis stack $inlineContext->getDelimiterStack()->removeDelimiter($opener); return false; } $cursor = $inlineContext->getCursor(); $startPos = $cursor->getPosition(); $previousState = $cursor->saveState(); $cursor->advanceBy(1); // Check to see if we have a link/image // Inline link? if ($result = $this->tryParseInlineLinkAndTitle($cursor)) { $link = $result; } elseif ($link = $this->tryParseReference($cursor, $inlineContext->getReferenceMap(), $opener->getIndex(), $startPos)) { $reference = $link; $link = ['url' => $link->getDestination(), 'title' => $link->getTitle()]; } else { // No match $inlineContext->getDelimiterStack()->removeDelimiter($opener); // Remove this opener from stack $cursor->restoreState($previousState); return false; } $isImage = $opener->getChar() === '!'; $inline = $this->createInline($link['url'], $link['title'], $isImage, $reference ?? null); $opener->getInlineNode()->replaceWith($inline); while (($label = $inline->next()) !== null) { // Is there a Mention or Link contained within this link? // CommonMark does not allow nested links, so we'll restore the original text. if ($label instanceof Mention) { $label->replaceWith($replacement = new Text($label->getPrefix() . $label->getIdentifier())); $inline->appendChild($replacement); } elseif ($label instanceof Link) { foreach ($label->children() as $child) { $label->insertBefore($child); } $label->detach(); } else { $inline->appendChild($label); } } // Process delimiters such as emphasis inside link/image $delimiterStack = $inlineContext->getDelimiterStack(); $stackBottom = $opener->getPrevious(); $delimiterStack->processDelimiters($stackBottom, $this->environment->getDelimiterProcessors()); $delimiterStack->removeAll($stackBottom); // Merge any adjacent Text nodes together AdjacentTextMerger::mergeChildNodes($inline); // processEmphasis will remove this and later delimiters. // Now, for a link, we also remove earlier link openers (no links in links) if (! $isImage) { $inlineContext->getDelimiterStack()->removeEarlierMatches('['); } return true; } public function setEnvironment(EnvironmentInterface $environment): void { $this->environment = $environment; } /** * @return array<string, string>|null */ private function tryParseInlineLinkAndTitle(Cursor $cursor): ?array { if ($cursor->getCurrentCharacter() !== '(') { return null; } $previousState = $cursor->saveState(); $cursor->advanceBy(1); $cursor->advanceToNextNonSpaceOrNewline(); if (($dest = LinkParserHelper::parseLinkDestination($cursor)) === null) { $cursor->restoreState($previousState); return null; } $cursor->advanceToNextNonSpaceOrNewline(); $previousCharacter = $cursor->peek(-1); // We know from previous lines that we've advanced at least one space so far, so this next call should never be null \assert(\is_string($previousCharacter)); $title = ''; // make sure there's a space before the title: if (\preg_match(RegexHelper::REGEX_WHITESPACE_CHAR, $previousCharacter)) { $title = LinkParserHelper::parseLinkTitle($cursor) ?? ''; } $cursor->advanceToNextNonSpaceOrNewline(); if ($cursor->getCurrentCharacter() !== ')') { $cursor->restoreState($previousState); return null; } $cursor->advanceBy(1); return ['url' => $dest, 'title' => $title]; } private function tryParseReference(Cursor $cursor, ReferenceMapInterface $referenceMap, ?int $openerIndex, int $startPos): ?ReferenceInterface { if ($openerIndex === null) { return null; } $savePos = $cursor->saveState(); $beforeLabel = $cursor->getPosition(); $n = LinkParserHelper::parseLinkLabel($cursor); if ($n === 0 || $n === 2) { $start = $openerIndex; $length = $startPos - $openerIndex; } else { $start = $beforeLabel + 1; $length = $n - 2; } $referenceLabel = $cursor->getSubstring($start, $length); if ($n === 0) { // If shortcut reference link, rewind before spaces we skipped $cursor->restoreState($savePos); } return $referenceMap->get($referenceLabel); } private function createInline(string $url, string $title, bool $isImage, ?ReferenceInterface $reference = null): AbstractWebResource { if ($isImage) { $inline = new Image($url, null, $title); } else { $inline = new Link($url, null, $title); } if ($reference) { $inline->data->set('reference', $reference); } return $inline; } } commonmark/src/Extension/CommonMark/Parser/Inline/AutolinkParser.php 0000644 00000003542 15007532371 0021652 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Inline; use League\CommonMark\Extension\CommonMark\Node\Inline\Link; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; use League\CommonMark\Util\UrlEncoder; final class AutolinkParser implements InlineParserInterface { private const EMAIL_REGEX = '<([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])?)*)>'; private const OTHER_LINK_REGEX = '<([A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\x00-\x20]*)>'; public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::regex(self::EMAIL_REGEX . '|' . self::OTHER_LINK_REGEX); } public function parse(InlineParserContext $inlineContext): bool { $inlineContext->getCursor()->advanceBy($inlineContext->getFullMatchLength()); $matches = $inlineContext->getMatches(); if ($matches[1] !== '') { $inlineContext->getContainer()->appendChild(new Link('mailto:' . UrlEncoder::unescapeAndEncode($matches[1]), $matches[1])); return true; } if ($matches[2] !== '') { $inlineContext->getContainer()->appendChild(new Link(UrlEncoder::unescapeAndEncode($matches[2]), $matches[2])); return true; } return false; // This should never happen } } commonmark/src/Extension/CommonMark/Parser/Inline/EscapableParser.php 0000644 00000003153 15007532371 0021741 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Inline; use League\CommonMark\Node\Inline\Newline; use League\CommonMark\Node\Inline\Text; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; use League\CommonMark\Util\RegexHelper; final class EscapableParser implements InlineParserInterface { public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::string('\\'); } public function parse(InlineParserContext $inlineContext): bool { $cursor = $inlineContext->getCursor(); $nextChar = $cursor->peek(); if ($nextChar === "\n") { $cursor->advanceBy(2); $inlineContext->getContainer()->appendChild(new Newline(Newline::HARDBREAK)); return true; } if ($nextChar !== null && RegexHelper::isEscapable($nextChar)) { $cursor->advanceBy(2); $inlineContext->getContainer()->appendChild(new Text($nextChar)); return true; } $cursor->advanceBy(1); $inlineContext->getContainer()->appendChild(new Text('\\')); return true; } } commonmark/src/Extension/CommonMark/Parser/Block/ListBlockStartParser.php 0000644 00000012553 15007532371 0022606 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Block; use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock; use League\CommonMark\Extension\CommonMark\Node\Block\ListData; use League\CommonMark\Parser\Block\BlockStart; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; use League\CommonMark\Util\RegexHelper; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class ListBlockStartParser implements BlockStartParserInterface, ConfigurationAwareInterface { /** @psalm-readonly-allow-private-mutation */ private ?ConfigurationInterface $config = null; /** * @psalm-var non-empty-string|null * * @psalm-readonly-allow-private-mutation */ private ?string $listMarkerRegex = null; public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart { if ($cursor->isIndented()) { return BlockStart::none(); } $listData = $this->parseList($cursor, $parserState->getParagraphContent() !== null); if ($listData === null) { return BlockStart::none(); } $listItemParser = new ListItemParser($listData); // prepend the list block if needed $matched = $parserState->getLastMatchedBlockParser(); if (! ($matched instanceof ListBlockParser) || ! $listData->equals($matched->getBlock()->getListData())) { $listBlockParser = new ListBlockParser($listData); // We start out with assuming a list is tight. If we find a blank line, we set it to loose later. $listBlockParser->getBlock()->setTight(true); return BlockStart::of($listBlockParser, $listItemParser)->at($cursor); } return BlockStart::of($listItemParser)->at($cursor); } private function parseList(Cursor $cursor, bool $inParagraph): ?ListData { $indent = $cursor->getIndent(); $tmpCursor = clone $cursor; $tmpCursor->advanceToNextNonSpaceOrTab(); $rest = $tmpCursor->getRemainder(); if (\preg_match($this->listMarkerRegex ?? $this->generateListMarkerRegex(), $rest) === 1) { $data = new ListData(); $data->markerOffset = $indent; $data->type = ListBlock::TYPE_BULLET; $data->delimiter = null; $data->bulletChar = $rest[0]; $markerLength = 1; } elseif (($matches = RegexHelper::matchFirst('/^(\d{1,9})([.)])/', $rest)) && (! $inParagraph || $matches[1] === '1')) { $data = new ListData(); $data->markerOffset = $indent; $data->type = ListBlock::TYPE_ORDERED; $data->start = (int) $matches[1]; $data->delimiter = $matches[2] === '.' ? ListBlock::DELIM_PERIOD : ListBlock::DELIM_PAREN; $data->bulletChar = null; $markerLength = \strlen($matches[0]); } else { return null; } // Make sure we have spaces after $nextChar = $tmpCursor->peek($markerLength); if (! ($nextChar === null || $nextChar === "\t" || $nextChar === ' ')) { return null; } // If it interrupts paragraph, make sure first line isn't blank if ($inParagraph && ! RegexHelper::matchAt(RegexHelper::REGEX_NON_SPACE, $rest, $markerLength)) { return null; } $cursor->advanceToNextNonSpaceOrTab(); // to start of marker $cursor->advanceBy($markerLength, true); // to end of marker $data->padding = self::calculateListMarkerPadding($cursor, $markerLength); return $data; } private static function calculateListMarkerPadding(Cursor $cursor, int $markerLength): int { $start = $cursor->saveState(); $spacesStartCol = $cursor->getColumn(); while ($cursor->getColumn() - $spacesStartCol < 5) { if (! $cursor->advanceBySpaceOrTab()) { break; } } $blankItem = $cursor->peek() === null; $spacesAfterMarker = $cursor->getColumn() - $spacesStartCol; if ($spacesAfterMarker >= 5 || $spacesAfterMarker < 1 || $blankItem) { $cursor->restoreState($start); $cursor->advanceBySpaceOrTab(); return $markerLength + 1; } return $markerLength + $spacesAfterMarker; } /** * @psalm-return non-empty-string */ private function generateListMarkerRegex(): string { // No configuration given - use the defaults if ($this->config === null) { return $this->listMarkerRegex = '/^[*+-]/'; } $markers = $this->config->get('commonmark/unordered_list_markers'); \assert(\is_array($markers)); return $this->listMarkerRegex = '/^[' . \preg_quote(\implode('', $markers), '/') . ']/'; } } commonmark/src/Extension/CommonMark/Parser/Block/ThematicBreakStartParser.php 0000644 00000002367 15007532371 0023425 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Block; use League\CommonMark\Parser\Block\BlockStart; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; use League\CommonMark\Util\RegexHelper; final class ThematicBreakStartParser implements BlockStartParserInterface { public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart { if ($cursor->isIndented()) { return BlockStart::none(); } $match = RegexHelper::matchAt(RegexHelper::REGEX_THEMATIC_BREAK, $cursor->getLine(), $cursor->getNextNonSpacePosition()); if ($match === null) { return BlockStart::none(); } // Advance to the end of the string, consuming the entire line (of the thematic break) $cursor->advanceToEnd(); return BlockStart::of(new ThematicBreakParser())->at($cursor); } } commonmark/src/Extension/CommonMark/Parser/Block/FencedCodeStartParser.php 0000644 00000002342 15007532371 0022672 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Block; use League\CommonMark\Parser\Block\BlockStart; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; final class FencedCodeStartParser implements BlockStartParserInterface { public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart { if ($cursor->isIndented() || ! \in_array($cursor->getNextNonSpaceCharacter(), ['`', '~'], true)) { return BlockStart::none(); } $indent = $cursor->getIndent(); $fence = $cursor->match('/^[ \t]*(?:`{3,}(?!.*`)|~{3,})/'); if ($fence === null) { return BlockStart::none(); } // fenced code block $fence = \ltrim($fence, " \t"); return BlockStart::of(new FencedCodeParser(\strlen($fence), $fence[0], $indent))->at($cursor); } } commonmark/src/Extension/CommonMark/Parser/Block/ThematicBreakParser.php 0000644 00000002226 15007532371 0022401 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Block; use League\CommonMark\Extension\CommonMark\Node\Block\ThematicBreak; use League\CommonMark\Parser\Block\AbstractBlockContinueParser; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Cursor; final class ThematicBreakParser extends AbstractBlockContinueParser { /** @psalm-readonly */ private ThematicBreak $block; public function __construct() { $this->block = new ThematicBreak(); } public function getBlock(): ThematicBreak { return $this->block; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { // a horizontal rule can never container > 1 line, so fail to match return BlockContinue::none(); } } commonmark/src/Extension/CommonMark/Parser/Block/HtmlBlockStartParser.php 0000644 00000004126 15007532371 0022574 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Block; use League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock; use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Parser\Block\BlockStart; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; use League\CommonMark\Util\RegexHelper; final class HtmlBlockStartParser implements BlockStartParserInterface { public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart { if ($cursor->isIndented() || $cursor->getNextNonSpaceCharacter() !== '<') { return BlockStart::none(); } $tmpCursor = clone $cursor; $tmpCursor->advanceToNextNonSpaceOrTab(); $line = $tmpCursor->getRemainder(); for ($blockType = 1; $blockType <= 7; $blockType++) { /** @psalm-var HtmlBlock::TYPE_* $blockType */ /** @phpstan-var HtmlBlock::TYPE_* $blockType */ $match = RegexHelper::matchAt( RegexHelper::getHtmlBlockOpenRegex($blockType), $line ); if ($match !== null && ($blockType < 7 || $this->isType7BlockAllowed($cursor, $parserState))) { return BlockStart::of(new HtmlBlockParser($blockType))->at($cursor); } } return BlockStart::none(); } private function isType7BlockAllowed(Cursor $cursor, MarkdownParserStateInterface $parserState): bool { // Type 7 blocks can't interrupt paragraphs if ($parserState->getLastMatchedBlockParser()->getBlock() instanceof Paragraph) { return false; } // Even lazy ones return ! $parserState->getActiveBlockParser()->canHaveLazyContinuationLines(); } } commonmark/src/Extension/CommonMark/Parser/Block/IndentedCodeStartParser.php 0000644 00000002273 15007532371 0023243 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Block; use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Parser\Block\BlockStart; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; final class IndentedCodeStartParser implements BlockStartParserInterface { public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart { if (! $cursor->isIndented()) { return BlockStart::none(); } if ($parserState->getActiveBlockParser()->getBlock() instanceof Paragraph) { return BlockStart::none(); } if ($cursor->isBlank()) { return BlockStart::none(); } $cursor->advanceBy(Cursor::INDENT_LEVEL, true); return BlockStart::of(new IndentedCodeParser())->at($cursor); } } commonmark/src/Extension/CommonMark/Parser/Block/ListBlockParser.php 0000644 00000004504 15007532371 0021565 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Block; use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock; use League\CommonMark\Extension\CommonMark\Node\Block\ListData; use League\CommonMark\Extension\CommonMark\Node\Block\ListItem; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Parser\Block\AbstractBlockContinueParser; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Cursor; final class ListBlockParser extends AbstractBlockContinueParser { /** @psalm-readonly */ private ListBlock $block; private bool $hadBlankLine = false; private int $linesAfterBlank = 0; public function __construct(ListData $listData) { $this->block = new ListBlock($listData); } public function getBlock(): ListBlock { return $this->block; } public function isContainer(): bool { return true; } public function canContain(AbstractBlock $childBlock): bool { if (! $childBlock instanceof ListItem) { return false; } // Another list item is being added to this list block. // If the previous line was blank, that means this list // block is "loose" (not tight). if ($this->hadBlankLine && $this->linesAfterBlank === 1) { $this->block->setTight(false); $this->hadBlankLine = false; } return true; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { if ($cursor->isBlank()) { $this->hadBlankLine = true; $this->linesAfterBlank = 0; } elseif ($this->hadBlankLine) { $this->linesAfterBlank++; } // List blocks themselves don't have any markers, only list items. So try to stay in the list. // If there is a block start other than list item, canContain makes sure that this list is closed. return BlockContinue::at($cursor); } } commonmark/src/Extension/CommonMark/Parser/Block/HtmlBlockParser.php 0000644 00000004372 15007532371 0021561 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Block; use League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock; use League\CommonMark\Parser\Block\AbstractBlockContinueParser; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Util\RegexHelper; final class HtmlBlockParser extends AbstractBlockContinueParser { /** @psalm-readonly */ private HtmlBlock $block; private string $content = ''; private bool $finished = false; /** * @psalm-param HtmlBlock::TYPE_* $blockType * * @phpstan-param HtmlBlock::TYPE_* $blockType */ public function __construct(int $blockType) { $this->block = new HtmlBlock($blockType); } public function getBlock(): HtmlBlock { return $this->block; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { if ($this->finished) { return BlockContinue::none(); } if ($cursor->isBlank() && \in_array($this->block->getType(), [HtmlBlock::TYPE_6_BLOCK_ELEMENT, HtmlBlock::TYPE_7_MISC_ELEMENT], true)) { return BlockContinue::none(); } return BlockContinue::at($cursor); } public function addLine(string $line): void { if ($this->content !== '') { $this->content .= "\n"; } $this->content .= $line; // Check for end condition // phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed if ($this->block->getType() <= HtmlBlock::TYPE_5_CDATA) { if (\preg_match(RegexHelper::getHtmlBlockCloseRegex($this->block->getType()), $line) === 1) { $this->finished = true; } } } public function closeBlock(): void { $this->block->setLiteral($this->content); $this->content = ''; } } commonmark/src/Extension/CommonMark/Parser/Block/FencedCodeParser.php 0000644 00000005340 15007532371 0021655 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Block; use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode; use League\CommonMark\Parser\Block\AbstractBlockContinueParser; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Util\ArrayCollection; use League\CommonMark\Util\RegexHelper; final class FencedCodeParser extends AbstractBlockContinueParser { /** @psalm-readonly */ private FencedCode $block; /** @var ArrayCollection<string> */ private ArrayCollection $strings; public function __construct(int $fenceLength, string $fenceChar, int $fenceOffset) { $this->block = new FencedCode($fenceLength, $fenceChar, $fenceOffset); $this->strings = new ArrayCollection(); } public function getBlock(): FencedCode { return $this->block; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { // Check for closing code fence if (! $cursor->isIndented() && $cursor->getNextNonSpaceCharacter() === $this->block->getChar()) { $match = RegexHelper::matchFirst('/^(?:`{3,}|~{3,})(?= *$)/', $cursor->getLine(), $cursor->getNextNonSpacePosition()); if ($match !== null && \strlen($match[0]) >= $this->block->getLength()) { // closing fence - we're at end of line, so we can finalize now return BlockContinue::finished(); } } // Skip optional spaces of fence offset // Optimization: don't attempt to match if we're at a non-space position if ($cursor->getNextNonSpacePosition() > $cursor->getPosition()) { $cursor->match('/^ {0,' . $this->block->getOffset() . '}/'); } return BlockContinue::at($cursor); } public function addLine(string $line): void { $this->strings[] = $line; } public function closeBlock(): void { // first line becomes info string $firstLine = $this->strings->first(); if ($firstLine === false) { $firstLine = ''; } $this->block->setInfo(RegexHelper::unescape(\trim($firstLine))); if ($this->strings->count() === 1) { $this->block->setLiteral(''); } else { $this->block->setLiteral(\implode("\n", $this->strings->slice(1)) . "\n"); } } } commonmark/src/Extension/CommonMark/Parser/Block/ListItemParser.php 0000644 00000005573 15007532371 0021440 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Block; use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock; use League\CommonMark\Extension\CommonMark\Node\Block\ListData; use League\CommonMark\Extension\CommonMark\Node\Block\ListItem; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Parser\Block\AbstractBlockContinueParser; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Cursor; final class ListItemParser extends AbstractBlockContinueParser { /** @psalm-readonly */ private ListItem $block; private bool $hadBlankLine = false; public function __construct(ListData $listData) { $this->block = new ListItem($listData); } public function getBlock(): ListItem { return $this->block; } public function isContainer(): bool { return true; } public function canContain(AbstractBlock $childBlock): bool { if ($this->hadBlankLine) { // We saw a blank line in this list item, that means the list block is loose. // // spec: if any of its constituent list items directly contain two block-level elements with a blank line // between them $parent = $this->block->parent(); if ($parent instanceof ListBlock) { $parent->setTight(false); } } return true; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { if ($cursor->isBlank()) { if ($this->block->firstChild() === null) { // Blank line after empty list item return BlockContinue::none(); } $activeBlock = $activeBlockParser->getBlock(); // If the active block is a code block, blank lines in it should not affect if the list is tight. $this->hadBlankLine = $activeBlock instanceof Paragraph || $activeBlock instanceof ListItem; $cursor->advanceToNextNonSpaceOrTab(); return BlockContinue::at($cursor); } $contentIndent = $this->block->getListData()->markerOffset + $this->getBlock()->getListData()->padding; if ($cursor->getIndent() >= $contentIndent) { $cursor->advanceBy($contentIndent, true); return BlockContinue::at($cursor); } // Note: We'll hit this case for lazy continuation lines, they will get added later. return BlockContinue::none(); } } commonmark/src/Extension/CommonMark/Parser/Block/IndentedCodeParser.php 0000644 00000004242 15007532371 0022223 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Block; use League\CommonMark\Extension\CommonMark\Node\Block\IndentedCode; use League\CommonMark\Parser\Block\AbstractBlockContinueParser; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Util\ArrayCollection; final class IndentedCodeParser extends AbstractBlockContinueParser { /** @psalm-readonly */ private IndentedCode $block; /** @var ArrayCollection<string> */ private ArrayCollection $strings; public function __construct() { $this->block = new IndentedCode(); $this->strings = new ArrayCollection(); } public function getBlock(): IndentedCode { return $this->block; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { if ($cursor->isIndented()) { $cursor->advanceBy(Cursor::INDENT_LEVEL, true); return BlockContinue::at($cursor); } if ($cursor->isBlank()) { $cursor->advanceToNextNonSpaceOrTab(); return BlockContinue::at($cursor); } return BlockContinue::none(); } public function addLine(string $line): void { $this->strings[] = $line; } public function closeBlock(): void { $reversed = \array_reverse($this->strings->toArray(), true); foreach ($reversed as $index => $line) { if ($line !== '' && $line !== "\n" && ! \preg_match('/^(\n *)$/', $line)) { break; } unset($reversed[$index]); } $fixed = \array_reverse($reversed); $tmp = \implode("\n", $fixed); if (\substr($tmp, -1) !== "\n") { $tmp .= "\n"; } $this->block->setLiteral($tmp); } } commonmark/src/Extension/CommonMark/Parser/Block/BlockQuoteParser.php 0000644 00000003052 15007532371 0021744 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Block; use League\CommonMark\Extension\CommonMark\Node\Block\BlockQuote; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Parser\Block\AbstractBlockContinueParser; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Cursor; final class BlockQuoteParser extends AbstractBlockContinueParser { /** @psalm-readonly */ private BlockQuote $block; public function __construct() { $this->block = new BlockQuote(); } public function getBlock(): BlockQuote { return $this->block; } public function isContainer(): bool { return true; } public function canContain(AbstractBlock $childBlock): bool { return true; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { if (! $cursor->isIndented() && $cursor->getNextNonSpaceCharacter() === '>') { $cursor->advanceToNextNonSpaceOrTab(); $cursor->advanceBy(1); $cursor->advanceBySpaceOrTab(); return BlockContinue::at($cursor); } return BlockContinue::none(); } } commonmark/src/Extension/CommonMark/Parser/Block/HeadingStartParser.php 0000644 00000004735 15007532371 0022262 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Block; use League\CommonMark\Parser\Block\BlockStart; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; use League\CommonMark\Util\RegexHelper; class HeadingStartParser implements BlockStartParserInterface { public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart { if ($cursor->isIndented() || ! \in_array($cursor->getNextNonSpaceCharacter(), ['#', '-', '='], true)) { return BlockStart::none(); } $cursor->advanceToNextNonSpaceOrTab(); if ($atxHeading = self::getAtxHeader($cursor)) { return BlockStart::of($atxHeading)->at($cursor); } $setextHeadingLevel = self::getSetextHeadingLevel($cursor); if ($setextHeadingLevel > 0) { $content = $parserState->getParagraphContent(); if ($content !== null) { $cursor->advanceToEnd(); return BlockStart::of(new HeadingParser($setextHeadingLevel, $content)) ->at($cursor) ->replaceActiveBlockParser(); } } return BlockStart::none(); } private static function getAtxHeader(Cursor $cursor): ?HeadingParser { $match = RegexHelper::matchFirst('/^#{1,6}(?:[ \t]+|$)/', $cursor->getRemainder()); if (! $match) { return null; } $cursor->advanceToNextNonSpaceOrTab(); $cursor->advanceBy(\strlen($match[0])); $level = \strlen(\trim($match[0])); $str = $cursor->getRemainder(); $str = \preg_replace('/^[ \t]*#+[ \t]*$/', '', $str); \assert(\is_string($str)); $str = \preg_replace('/[ \t]+#+[ \t]*$/', '', $str); \assert(\is_string($str)); return new HeadingParser($level, $str); } private static function getSetextHeadingLevel(Cursor $cursor): int { $match = RegexHelper::matchFirst('/^(?:=+|-+)[ \t]*$/', $cursor->getRemainder()); if ($match === null) { return 0; } return $match[0][0] === '=' ? 1 : 2; } } commonmark/src/Extension/CommonMark/Parser/Block/HeadingParser.php 0000644 00000002744 15007532371 0021242 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Block; use League\CommonMark\Extension\CommonMark\Node\Block\Heading; use League\CommonMark\Parser\Block\AbstractBlockContinueParser; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Block\BlockContinueParserWithInlinesInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\InlineParserEngineInterface; final class HeadingParser extends AbstractBlockContinueParser implements BlockContinueParserWithInlinesInterface { /** @psalm-readonly */ private Heading $block; private string $content; public function __construct(int $level, string $content) { $this->block = new Heading($level); $this->content = $content; } public function getBlock(): Heading { return $this->block; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { return BlockContinue::none(); } public function parseInlines(InlineParserEngineInterface $inlineParser): void { $inlineParser->parse($this->content, $this->block); } } commonmark/src/Extension/CommonMark/Parser/Block/BlockQuoteStartParser.php 0000644 00000002131 15007532371 0022757 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Parser\Block; use League\CommonMark\Parser\Block\BlockStart; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; final class BlockQuoteStartParser implements BlockStartParserInterface { public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart { if ($cursor->isIndented()) { return BlockStart::none(); } if ($cursor->getNextNonSpaceCharacter() !== '>') { return BlockStart::none(); } $cursor->advanceToNextNonSpaceOrTab(); $cursor->advanceBy(1); $cursor->advanceBySpaceOrTab(); return BlockStart::of(new BlockQuoteParser())->at($cursor); } } commonmark/src/Extension/CommonMark/Renderer/Inline/CodeRenderer.php 0000644 00000002745 15007532371 0021566 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Renderer\Inline; use League\CommonMark\Extension\CommonMark\Node\Inline\Code; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Util\Xml; use League\CommonMark\Xml\XmlNodeRendererInterface; final class CodeRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param Code $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { Code::assertInstanceOf($node); $attrs = $node->data->get('attributes'); return new HtmlElement('code', $attrs, Xml::escape($node->getLiteral())); } public function getXmlTagName(Node $node): string { return 'code'; } /** * {@inheritDoc} */ public function getXmlAttributes(Node $node): array { return []; } } commonmark/src/Extension/CommonMark/Renderer/Inline/EmphasisRenderer.php 0000644 00000002741 15007532371 0022461 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Renderer\Inline; use League\CommonMark\Extension\CommonMark\Node\Inline\Emphasis; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; final class EmphasisRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param Emphasis $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { Emphasis::assertInstanceOf($node); $attrs = $node->data->get('attributes'); return new HtmlElement('em', $attrs, $childRenderer->renderNodes($node->children())); } public function getXmlTagName(Node $node): string { return 'emph'; } /** * {@inheritDoc} */ public function getXmlAttributes(Node $node): array { return []; } } commonmark/src/Extension/CommonMark/Renderer/Inline/StrongRenderer.php 0000644 00000002737 15007532371 0022171 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Renderer\Inline; use League\CommonMark\Extension\CommonMark\Node\Inline\Strong; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; final class StrongRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param Strong $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { Strong::assertInstanceOf($node); $attrs = $node->data->get('attributes'); return new HtmlElement('strong', $attrs, $childRenderer->renderNodes($node->children())); } public function getXmlTagName(Node $node): string { return 'strong'; } /** * {@inheritDoc} */ public function getXmlAttributes(Node $node): array { return []; } } commonmark/src/Extension/CommonMark/Renderer/Inline/LinkRenderer.php 0000644 00000005071 15007532371 0021604 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Renderer\Inline; use League\CommonMark\Extension\CommonMark\Node\Inline\Link; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Util\RegexHelper; use League\CommonMark\Xml\XmlNodeRendererInterface; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class LinkRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface { /** @psalm-readonly-allow-private-mutation */ private ConfigurationInterface $config; /** * @param Link $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { Link::assertInstanceOf($node); $attrs = $node->data->get('attributes'); $forbidUnsafeLinks = ! $this->config->get('allow_unsafe_links'); if (! ($forbidUnsafeLinks && RegexHelper::isLinkPotentiallyUnsafe($node->getUrl()))) { $attrs['href'] = $node->getUrl(); } if (($title = $node->getTitle()) !== null) { $attrs['title'] = $title; } if (isset($attrs['target']) && $attrs['target'] === '_blank' && ! isset($attrs['rel'])) { $attrs['rel'] = 'noopener noreferrer'; } return new HtmlElement('a', $attrs, $childRenderer->renderNodes($node->children())); } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } public function getXmlTagName(Node $node): string { return 'link'; } /** * @param Link $node * * @return array<string, scalar> * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlAttributes(Node $node): array { Link::assertInstanceOf($node); return [ 'destination' => $node->getUrl(), 'title' => $node->getTitle() ?? '', ]; } } commonmark/src/Extension/CommonMark/Renderer/Inline/HtmlInlineRenderer.php 0000644 00000003462 15007532371 0022754 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Renderer\Inline; use League\CommonMark\Extension\CommonMark\Node\Inline\HtmlInline; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlFilter; use League\CommonMark\Xml\XmlNodeRendererInterface; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class HtmlInlineRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface { /** @psalm-readonly-allow-private-mutation */ private ConfigurationInterface $config; /** * @param HtmlInline $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): string { HtmlInline::assertInstanceOf($node); $htmlInput = $this->config->get('html_input'); return HtmlFilter::filter($node->getLiteral(), $htmlInput); } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } public function getXmlTagName(Node $node): string { return 'html_inline'; } /** * {@inheritDoc} */ public function getXmlAttributes(Node $node): array { return []; } } commonmark/src/Extension/CommonMark/Renderer/Inline/ImageRenderer.php 0000644 00000005735 15007532371 0021740 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Renderer\Inline; use League\CommonMark\Extension\CommonMark\Node\Inline\Image; use League\CommonMark\Node\Inline\Newline; use League\CommonMark\Node\Node; use League\CommonMark\Node\NodeIterator; use League\CommonMark\Node\StringContainerInterface; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Util\RegexHelper; use League\CommonMark\Xml\XmlNodeRendererInterface; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class ImageRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface { /** @psalm-readonly-allow-private-mutation */ private ConfigurationInterface $config; /** * @param Image $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { Image::assertInstanceOf($node); $attrs = $node->data->get('attributes'); $forbidUnsafeLinks = ! $this->config->get('allow_unsafe_links'); if ($forbidUnsafeLinks && RegexHelper::isLinkPotentiallyUnsafe($node->getUrl())) { $attrs['src'] = ''; } else { $attrs['src'] = $node->getUrl(); } $attrs['alt'] = $this->getAltText($node); if (($title = $node->getTitle()) !== null) { $attrs['title'] = $title; } return new HtmlElement('img', $attrs, '', true); } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } public function getXmlTagName(Node $node): string { return 'image'; } /** * @param Image $node * * @return array<string, scalar> * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlAttributes(Node $node): array { Image::assertInstanceOf($node); return [ 'destination' => $node->getUrl(), 'title' => $node->getTitle() ?? '', ]; } private function getAltText(Image $node): string { $altText = ''; foreach ((new NodeIterator($node)) as $n) { if ($n instanceof StringContainerInterface) { $altText .= $n->getLiteral(); } elseif ($n instanceof Newline) { $altText .= "\n"; } } return $altText; } } commonmark/src/Extension/CommonMark/Renderer/Block/HtmlBlockRenderer.php 0000644 00000003453 15007532371 0022404 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Renderer\Block; use League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlFilter; use League\CommonMark\Xml\XmlNodeRendererInterface; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class HtmlBlockRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface { /** @psalm-readonly-allow-private-mutation */ private ConfigurationInterface $config; /** * @param HtmlBlock $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): string { HtmlBlock::assertInstanceOf($node); $htmlInput = $this->config->get('html_input'); return HtmlFilter::filter($node->getLiteral(), $htmlInput); } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } public function getXmlTagName(Node $node): string { return 'html_block'; } /** * {@inheritDoc} */ public function getXmlAttributes(Node $node): array { return []; } } commonmark/src/Extension/CommonMark/Renderer/Block/ListBlockRenderer.php 0000644 00000004636 15007532371 0022417 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Renderer\Block; use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; final class ListBlockRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param ListBlock $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { ListBlock::assertInstanceOf($node); $listData = $node->getListData(); $tag = $listData->type === ListBlock::TYPE_BULLET ? 'ul' : 'ol'; $attrs = $node->data->get('attributes'); if ($listData->start !== null && $listData->start !== 1) { $attrs['start'] = (string) $listData->start; } $innerSeparator = $childRenderer->getInnerSeparator(); return new HtmlElement($tag, $attrs, $innerSeparator . $childRenderer->renderNodes($node->children()) . $innerSeparator); } public function getXmlTagName(Node $node): string { return 'list'; } /** * @param ListBlock $node * * @return array<string, scalar> * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlAttributes(Node $node): array { ListBlock::assertInstanceOf($node); $data = $node->getListData(); if ($data->type === ListBlock::TYPE_BULLET) { return [ 'type' => $data->type, 'tight' => $node->isTight() ? 'true' : 'false', ]; } return [ 'type' => $data->type, 'start' => $data->start ?? 1, 'tight' => $node->isTight(), 'delimiter' => $data->delimiter ?? ListBlock::DELIM_PERIOD, ]; } } commonmark/src/Extension/CommonMark/Renderer/Block/HeadingRenderer.php 0000644 00000003300 15007532371 0022053 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Renderer\Block; use League\CommonMark\Extension\CommonMark\Node\Block\Heading; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; final class HeadingRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param Heading $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { Heading::assertInstanceOf($node); $tag = 'h' . $node->getLevel(); $attrs = $node->data->get('attributes'); return new HtmlElement($tag, $attrs, $childRenderer->renderNodes($node->children())); } public function getXmlTagName(Node $node): string { return 'heading'; } /** * @param Heading $node * * @return array<string, scalar> * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlAttributes(Node $node): array { Heading::assertInstanceOf($node); return ['level' => $node->getLevel()]; } } commonmark/src/Extension/CommonMark/Renderer/Block/BlockQuoteRenderer.php 0000644 00000003625 15007532371 0022576 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Renderer\Block; use League\CommonMark\Extension\CommonMark\Node\Block\BlockQuote; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; final class BlockQuoteRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param BlockQuote $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { BlockQuote::assertInstanceOf($node); $attrs = $node->data->get('attributes'); $filling = $childRenderer->renderNodes($node->children()); $innerSeparator = $childRenderer->getInnerSeparator(); if ($filling === '') { return new HtmlElement('blockquote', $attrs, $innerSeparator); } return new HtmlElement( 'blockquote', $attrs, $innerSeparator . $filling . $innerSeparator ); } public function getXmlTagName(Node $node): string { return 'block_quote'; } /** * @param BlockQuote $node * * @return array<string, scalar> * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlAttributes(Node $node): array { return []; } } commonmark/src/Extension/CommonMark/Renderer/Block/FencedCodeRenderer.php 0000644 00000004261 15007532371 0022502 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Renderer\Block; use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Util\Xml; use League\CommonMark\Xml\XmlNodeRendererInterface; final class FencedCodeRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param FencedCode $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { FencedCode::assertInstanceOf($node); $attrs = $node->data->getData('attributes'); $infoWords = $node->getInfoWords(); if (\count($infoWords) !== 0 && $infoWords[0] !== '') { $class = $infoWords[0]; if (! \str_starts_with($class, 'language-')) { $class = 'language-' . $class; } $attrs->append('class', $class); } return new HtmlElement( 'pre', [], new HtmlElement('code', $attrs->export(), Xml::escape($node->getLiteral())) ); } public function getXmlTagName(Node $node): string { return 'code_block'; } /** * @param FencedCode $node * * @return array<string, scalar> * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlAttributes(Node $node): array { FencedCode::assertInstanceOf($node); if (($info = $node->getInfo()) === null || $info === '') { return []; } return ['info' => $info]; } } commonmark/src/Extension/CommonMark/Renderer/Block/IndentedCodeRenderer.php 0000644 00000003143 15007532371 0023046 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Renderer\Block; use League\CommonMark\Extension\CommonMark\Node\Block\IndentedCode; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Util\Xml; use League\CommonMark\Xml\XmlNodeRendererInterface; final class IndentedCodeRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param IndentedCode $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { IndentedCode::assertInstanceOf($node); $attrs = $node->data->get('attributes'); return new HtmlElement( 'pre', [], new HtmlElement('code', $attrs, Xml::escape($node->getLiteral())) ); } public function getXmlTagName(Node $node): string { return 'code_block'; } /** * @return array<string, scalar> */ public function getXmlAttributes(Node $node): array { return []; } } commonmark/src/Extension/CommonMark/Renderer/Block/ThematicBreakRenderer.php 0000644 00000002727 15007532371 0023233 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Renderer\Block; use League\CommonMark\Extension\CommonMark\Node\Block\ThematicBreak; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; final class ThematicBreakRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param ThematicBreak $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { ThematicBreak::assertInstanceOf($node); $attrs = $node->data->get('attributes'); return new HtmlElement('hr', $attrs, '', true); } public function getXmlTagName(Node $node): string { return 'thematic_break'; } /** * {@inheritDoc} */ public function getXmlAttributes(Node $node): array { return []; } } commonmark/src/Extension/CommonMark/Renderer/Block/ListItemRenderer.php 0000644 00000004062 15007532371 0022254 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\CommonMark\Renderer\Block; use League\CommonMark\Extension\CommonMark\Node\Block\ListItem; use League\CommonMark\Extension\TaskList\TaskListItemMarker; use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; final class ListItemRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param ListItem $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { ListItem::assertInstanceOf($node); $contents = $childRenderer->renderNodes($node->children()); if (\substr($contents, 0, 1) === '<' && ! $this->startsTaskListItem($node)) { $contents = "\n" . $contents; } if (\substr($contents, -1, 1) === '>') { $contents .= "\n"; } $attrs = $node->data->get('attributes'); return new HtmlElement('li', $attrs, $contents); } public function getXmlTagName(Node $node): string { return 'item'; } /** * {@inheritDoc} */ public function getXmlAttributes(Node $node): array { return []; } private function startsTaskListItem(ListItem $block): bool { $firstChild = $block->firstChild(); return $firstChild instanceof Paragraph && $firstChild->firstChild() instanceof TaskListItemMarker; } } commonmark/src/Extension/ExtensionInterface.php 0000644 00000001143 15007532371 0015762 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js) * - (c) John MacFarlane * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension; use League\CommonMark\Environment\EnvironmentBuilderInterface; interface ExtensionInterface { public function register(EnvironmentBuilderInterface $environment): void; } commonmark/src/Extension/DescriptionList/Node/DescriptionTerm.php 0000644 00000000656 15007532371 0021314 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\DescriptionList\Node; use League\CommonMark\Node\Block\AbstractBlock; class DescriptionTerm extends AbstractBlock { } commonmark/src/Extension/DescriptionList/Node/Description.php 0000644 00000001503 15007532371 0020454 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\DescriptionList\Node; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Node\Block\TightBlockInterface; class Description extends AbstractBlock implements TightBlockInterface { private bool $tight; public function __construct(bool $tight = false) { parent::__construct(); $this->tight = $tight; } public function isTight(): bool { return $this->tight; } public function setTight(bool $tight): void { $this->tight = $tight; } } commonmark/src/Extension/DescriptionList/Node/DescriptionList.php 0000644 00000000656 15007532371 0021320 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\DescriptionList\Node; use League\CommonMark\Node\Block\AbstractBlock; class DescriptionList extends AbstractBlock { } commonmark/src/Extension/DescriptionList/DescriptionListExtension.php 0000644 00000003557 15007532371 0022333 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\DescriptionList; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\DescriptionList\Event\ConsecutiveDescriptionListMerger; use League\CommonMark\Extension\DescriptionList\Event\LooseDescriptionHandler; use League\CommonMark\Extension\DescriptionList\Node\Description; use League\CommonMark\Extension\DescriptionList\Node\DescriptionList; use League\CommonMark\Extension\DescriptionList\Node\DescriptionTerm; use League\CommonMark\Extension\DescriptionList\Parser\DescriptionStartParser; use League\CommonMark\Extension\DescriptionList\Renderer\DescriptionListRenderer; use League\CommonMark\Extension\DescriptionList\Renderer\DescriptionRenderer; use League\CommonMark\Extension\DescriptionList\Renderer\DescriptionTermRenderer; use League\CommonMark\Extension\ExtensionInterface; final class DescriptionListExtension implements ExtensionInterface { public function register(EnvironmentBuilderInterface $environment): void { $environment->addBlockStartParser(new DescriptionStartParser()); $environment->addEventListener(DocumentParsedEvent::class, new LooseDescriptionHandler(), 1001); $environment->addEventListener(DocumentParsedEvent::class, new ConsecutiveDescriptionListMerger(), 1000); $environment->addRenderer(DescriptionList::class, new DescriptionListRenderer()); $environment->addRenderer(DescriptionTerm::class, new DescriptionTermRenderer()); $environment->addRenderer(Description::class, new DescriptionRenderer()); } } commonmark/src/Extension/DescriptionList/Parser/DescriptionStartParser.php 0000644 00000004773 15007532371 0023232 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\DescriptionList\Parser; use League\CommonMark\Extension\DescriptionList\Node\Description; use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Parser\Block\BlockStart; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; final class DescriptionStartParser implements BlockStartParserInterface { public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart { if ($cursor->isIndented()) { return BlockStart::none(); } $cursor->advanceToNextNonSpaceOrTab(); if ($cursor->match('/^:[ \t]+/') === null) { return BlockStart::none(); } $terms = $parserState->getParagraphContent(); $activeBlock = $parserState->getActiveBlockParser()->getBlock(); if ($terms !== null && $terms !== '') { // New description; tight; term(s) sitting in pending block that we will replace return BlockStart::of(...[new DescriptionListContinueParser()], ...self::splitTerms($terms), ...[new DescriptionContinueParser(true, $cursor->getPosition())]) ->at($cursor) ->replaceActiveBlockParser(); } if ($activeBlock instanceof Paragraph && $activeBlock->parent() instanceof Description) { // Additional description in the same list as the parent description return BlockStart::of(new DescriptionContinueParser(true, $cursor->getPosition()))->at($cursor); } if ($activeBlock->lastChild() instanceof Paragraph) { // New description; loose; term(s) sitting in previous closed paragraph block return BlockStart::of(new DescriptionContinueParser(false, $cursor->getPosition()))->at($cursor); } // No preceding terms return BlockStart::none(); } /** * @return array<int, DescriptionTermContinueParser> */ private static function splitTerms(string $terms): array { $ret = []; foreach (\explode("\n", $terms) as $term) { $ret[] = new DescriptionTermContinueParser($term); } return $ret; } } commonmark/src/Extension/DescriptionList/Parser/DescriptionListContinueParser.php 0000644 00000002760 15007532371 0024547 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\DescriptionList\Parser; use League\CommonMark\Extension\DescriptionList\Node\Description; use League\CommonMark\Extension\DescriptionList\Node\DescriptionList; use League\CommonMark\Extension\DescriptionList\Node\DescriptionTerm; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Parser\Block\AbstractBlockContinueParser; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Cursor; final class DescriptionListContinueParser extends AbstractBlockContinueParser { private DescriptionList $block; public function __construct() { $this->block = new DescriptionList(); } public function getBlock(): DescriptionList { return $this->block; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { return BlockContinue::at($cursor); } public function isContainer(): bool { return true; } public function canContain(AbstractBlock $childBlock): bool { return $childBlock instanceof DescriptionTerm || $childBlock instanceof Description; } } commonmark/src/Extension/DescriptionList/Parser/DescriptionTermContinueParser.php 0000644 00000003011 15007532371 0024531 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\DescriptionList\Parser; use League\CommonMark\Extension\DescriptionList\Node\DescriptionTerm; use League\CommonMark\Parser\Block\AbstractBlockContinueParser; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Block\BlockContinueParserWithInlinesInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\InlineParserEngineInterface; final class DescriptionTermContinueParser extends AbstractBlockContinueParser implements BlockContinueParserWithInlinesInterface { private DescriptionTerm $block; private string $term; public function __construct(string $term) { $this->block = new DescriptionTerm(); $this->term = $term; } public function getBlock(): DescriptionTerm { return $this->block; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { return BlockContinue::finished(); } public function parseInlines(InlineParserEngineInterface $inlineParser): void { if ($this->term !== '') { $inlineParser->parse($this->term, $this->block); } } } commonmark/src/Extension/DescriptionList/Parser/DescriptionContinueParser.php 0000644 00000003534 15007532371 0023713 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\DescriptionList\Parser; use League\CommonMark\Extension\DescriptionList\Node\Description; use League\CommonMark\Node\Block\AbstractBlock; use League\CommonMark\Parser\Block\AbstractBlockContinueParser; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Cursor; final class DescriptionContinueParser extends AbstractBlockContinueParser { private Description $block; private int $indentation; public function __construct(bool $tight, int $indentation) { $this->block = new Description($tight); $this->indentation = $indentation; } public function getBlock(): Description { return $this->block; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { if ($cursor->isBlank()) { if ($this->block->firstChild() === null) { // Blank line after empty item return BlockContinue::none(); } $cursor->advanceToNextNonSpaceOrTab(); return BlockContinue::at($cursor); } if ($cursor->getIndent() >= $this->indentation) { $cursor->advanceBy($this->indentation, true); return BlockContinue::at($cursor); } return BlockContinue::none(); } public function isContainer(): bool { return true; } public function canContain(AbstractBlock $childBlock): bool { return true; } } commonmark/src/Extension/DescriptionList/Renderer/DescriptionTermRenderer.php 0000644 00000002042 15007532371 0023653 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\DescriptionList\Renderer; use League\CommonMark\Extension\DescriptionList\Node\DescriptionTerm; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; final class DescriptionTermRenderer implements NodeRendererInterface { /** * @param DescriptionTerm $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { DescriptionTerm::assertInstanceOf($node); return new HtmlElement('dt', [], $childRenderer->renderNodes($node->children())); } } commonmark/src/Extension/DescriptionList/Renderer/DescriptionListRenderer.php 0000644 00000002167 15007532371 0023667 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\DescriptionList\Renderer; use League\CommonMark\Extension\DescriptionList\Node\DescriptionList; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; final class DescriptionListRenderer implements NodeRendererInterface { /** * @param DescriptionList $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): HtmlElement { DescriptionList::assertInstanceOf($node); $separator = $childRenderer->getBlockSeparator(); return new HtmlElement('dl', [], $separator . $childRenderer->renderNodes($node->children()) . $separator); } } commonmark/src/Extension/DescriptionList/Renderer/DescriptionRenderer.php 0000644 00000002022 15007532371 0023021 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\DescriptionList\Renderer; use League\CommonMark\Extension\DescriptionList\Node\Description; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; final class DescriptionRenderer implements NodeRendererInterface { /** * @param Description $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { Description::assertInstanceOf($node); return new HtmlElement('dd', [], $childRenderer->renderNodes($node->children())); } } commonmark/src/Extension/DescriptionList/Event/ConsecutiveDescriptionListMerger.php 0000644 00000002223 15007532371 0025056 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\DescriptionList\Event; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\DescriptionList\Node\DescriptionList; use League\CommonMark\Node\NodeIterator; final class ConsecutiveDescriptionListMerger { public function __invoke(DocumentParsedEvent $event): void { foreach ($event->getDocument()->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) { if (! $node instanceof DescriptionList) { continue; } if (! ($prev = $node->previous()) instanceof DescriptionList) { continue; } // There's another description list behind this one; merge the current one into that foreach ($node->children() as $child) { $prev->appendChild($child); } $node->detach(); } } } commonmark/src/Extension/DescriptionList/Event/LooseDescriptionHandler.php 0000644 00000004336 15007532371 0023157 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\DescriptionList\Event; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\DescriptionList\Node\Description; use League\CommonMark\Extension\DescriptionList\Node\DescriptionList; use League\CommonMark\Extension\DescriptionList\Node\DescriptionTerm; use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Node\Inline\Newline; use League\CommonMark\Node\NodeIterator; final class LooseDescriptionHandler { public function __invoke(DocumentParsedEvent $event): void { foreach ($event->getDocument()->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $description) { if (! $description instanceof Description) { continue; } // Does this description need to be added to a list? if (! $description->parent() instanceof DescriptionList) { $list = new DescriptionList(); // Taking any preceding paragraphs with it if (($paragraph = $description->previous()) instanceof Paragraph) { $list->appendChild($paragraph); } $description->replaceWith($list); $list->appendChild($description); } // Is this description preceded by a paragraph that should really be a term? if (! (($paragraph = $description->previous()) instanceof Paragraph)) { continue; } // Convert the paragraph into one or more terms $term = new DescriptionTerm(); $paragraph->replaceWith($term); foreach ($paragraph->children() as $child) { if ($child instanceof Newline) { $newTerm = new DescriptionTerm(); $term->insertAfter($newTerm); $term = $newTerm; continue; } $term->appendChild($child); } } } } commonmark/src/Extension/TaskList/TaskListItemMarker.php 0000644 00000001470 15007532371 0017445 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TaskList; use League\CommonMark\Node\Inline\AbstractInline; final class TaskListItemMarker extends AbstractInline { /** @psalm-readonly-allow-private-mutation */ private bool $checked; public function __construct(bool $isCompleted) { parent::__construct(); $this->checked = $isCompleted; } public function isChecked(): bool { return $this->checked; } public function setChecked(bool $checked): void { $this->checked = $checked; } } commonmark/src/Extension/TaskList/TaskListItemMarkerParser.php 0000644 00000003131 15007532371 0020616 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TaskList; use League\CommonMark\Extension\CommonMark\Node\Block\ListItem; use League\CommonMark\Node\Block\Paragraph; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; final class TaskListItemMarkerParser implements InlineParserInterface { public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::oneOf('[ ]', '[x]'); } public function parse(InlineParserContext $inlineContext): bool { $container = $inlineContext->getContainer(); // Checkbox must come at the beginning of the first paragraph of the list item if ($container->hasChildren() || ! ($container instanceof Paragraph && $container->parent() && $container->parent() instanceof ListItem)) { return false; } $cursor = $inlineContext->getCursor(); $oldState = $cursor->saveState(); $cursor->advanceBy(3); if ($cursor->getNextNonSpaceCharacter() === null) { $cursor->restoreState($oldState); return false; } $isChecked = $inlineContext->getFullMatch() !== '[ ]'; $container->appendChild(new TaskListItemMarker($isChecked)); return true; } } commonmark/src/Extension/TaskList/TaskListExtension.php 0000644 00000001371 15007532371 0017361 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TaskList; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Extension\ExtensionInterface; final class TaskListExtension implements ExtensionInterface { public function register(EnvironmentBuilderInterface $environment): void { $environment->addInlineParser(new TaskListItemMarkerParser(), 35); $environment->addRenderer(TaskListItemMarker::class, new TaskListItemMarkerRenderer()); } } commonmark/src/Extension/TaskList/TaskListItemMarkerRenderer.php 0000644 00000003425 15007532371 0021136 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\TaskList; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; final class TaskListItemMarkerRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param TaskListItemMarker $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { TaskListItemMarker::assertInstanceOf($node); $attrs = $node->data->get('attributes'); $checkbox = new HtmlElement('input', $attrs, '', true); if ($node->isChecked()) { $checkbox->setAttribute('checked', ''); } $checkbox->setAttribute('disabled', ''); $checkbox->setAttribute('type', 'checkbox'); return $checkbox; } public function getXmlTagName(Node $node): string { return 'task_list_item_marker'; } /** * @param TaskListItemMarker $node * * @return array<string, scalar> * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlAttributes(Node $node): array { TaskListItemMarker::assertInstanceOf($node); if ($node->isChecked()) { return ['checked' => 'checked']; } return []; } } commonmark/src/Extension/FrontMatter/Exception/InvalidFrontMatterException.php 0000644 00000001220 15007532371 0024017 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\FrontMatter\Exception; use League\CommonMark\Exception\CommonMarkException; class InvalidFrontMatterException extends \RuntimeException implements CommonMarkException { public static function wrap(\Throwable $t): self { return new InvalidFrontMatterException('Failed to parse front matter: ' . $t->getMessage(), 0, $t); } } commonmark/src/Extension/FrontMatter/Output/RenderedContentWithFrontMatter.php 0000644 00000002320 15007532371 0024035 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\FrontMatter\Output; use League\CommonMark\Extension\FrontMatter\FrontMatterProviderInterface; use League\CommonMark\Node\Block\Document; use League\CommonMark\Output\RenderedContent; /** * @psalm-immutable */ final class RenderedContentWithFrontMatter extends RenderedContent implements FrontMatterProviderInterface { /** * @var mixed * * @psalm-readonly */ private $frontMatter; /** * @param Document $document The parsed Document object * @param string $content The final HTML * @param mixed|null $frontMatter Any parsed front matter */ public function __construct(Document $document, string $content, $frontMatter) { parent::__construct($document, $content); $this->frontMatter = $frontMatter; } /** * {@inheritDoc} */ public function getFrontMatter() { return $this->frontMatter; } } commonmark/src/Extension/FrontMatter/Listener/FrontMatterPostRenderListener.php 0000644 00000001747 15007532371 0024212 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\FrontMatter\Listener; use League\CommonMark\Event\DocumentRenderedEvent; use League\CommonMark\Extension\FrontMatter\Output\RenderedContentWithFrontMatter; final class FrontMatterPostRenderListener { public function __invoke(DocumentRenderedEvent $event): void { if ($event->getOutput()->getDocument()->data->get('front_matter', null) === null) { return; } $frontMatter = $event->getOutput()->getDocument()->data->get('front_matter'); $event->replaceOutput(new RenderedContentWithFrontMatter( $event->getOutput()->getDocument(), $event->getOutput()->getContent(), $frontMatter )); } } commonmark/src/Extension/FrontMatter/Listener/FrontMatterPreParser.php 0000644 00000001713 15007532371 0022313 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\FrontMatter\Listener; use League\CommonMark\Event\DocumentPreParsedEvent; use League\CommonMark\Extension\FrontMatter\FrontMatterParserInterface; final class FrontMatterPreParser { private FrontMatterParserInterface $parser; public function __construct(FrontMatterParserInterface $parser) { $this->parser = $parser; } public function __invoke(DocumentPreParsedEvent $event): void { $content = $event->getMarkdown()->getContent(); $parsed = $this->parser->parse($content); $event->getDocument()->data->set('front_matter', $parsed->getFrontMatter()); $event->replaceMarkdown($parsed); } } commonmark/src/Extension/FrontMatter/FrontMatterParser.php 0000644 00000004447 15007532371 0020066 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\FrontMatter; use League\CommonMark\Extension\FrontMatter\Data\FrontMatterDataParserInterface; use League\CommonMark\Extension\FrontMatter\Exception\InvalidFrontMatterException; use League\CommonMark\Extension\FrontMatter\Input\MarkdownInputWithFrontMatter; use League\CommonMark\Parser\Cursor; final class FrontMatterParser implements FrontMatterParserInterface { /** @psalm-readonly */ private FrontMatterDataParserInterface $frontMatterParser; private const REGEX_FRONT_MATTER = '/^---\\R.*?\\R---\\R/s'; public function __construct(FrontMatterDataParserInterface $frontMatterParser) { $this->frontMatterParser = $frontMatterParser; } /** * @throws InvalidFrontMatterException if the front matter cannot be parsed */ public function parse(string $markdownContent): MarkdownInputWithFrontMatter { $cursor = new Cursor($markdownContent); // Locate the front matter $frontMatter = $cursor->match(self::REGEX_FRONT_MATTER); if ($frontMatter === null) { return new MarkdownInputWithFrontMatter($markdownContent); } // Trim the last line (ending ---s and newline) $frontMatter = \preg_replace('/---\R$/', '', $frontMatter); if ($frontMatter === null) { return new MarkdownInputWithFrontMatter($markdownContent); } // Parse the resulting YAML data $data = $this->frontMatterParser->parse($frontMatter); // Advance through any remaining newlines which separated the front matter from the Markdown text $trailingNewlines = $cursor->match('/^\R+/'); // Calculate how many lines the Markdown is offset from the front matter by counting the number of newlines // Don't forget to add 1 because we stripped one out when trimming the trailing delims $lineOffset = \preg_match_all('/\R/', $frontMatter . $trailingNewlines) + 1; return new MarkdownInputWithFrontMatter($cursor->getRemainder(), $lineOffset, $data); } } commonmark/src/Extension/FrontMatter/FrontMatterProviderInterface.php 0000644 00000000677 15007532371 0022246 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\FrontMatter; interface FrontMatterProviderInterface { /** * @return mixed|null */ public function getFrontMatter(); } commonmark/src/Extension/FrontMatter/Input/MarkdownInputWithFrontMatter.php 0000644 00000002242 15007532371 0023356 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\FrontMatter\Input; use League\CommonMark\Extension\FrontMatter\FrontMatterProviderInterface; use League\CommonMark\Input\MarkdownInput; final class MarkdownInputWithFrontMatter extends MarkdownInput implements FrontMatterProviderInterface { /** @var mixed|null */ private $frontMatter; /** * @param string $content Markdown content without the raw front matter * @param int $lineOffset Line offset (based on number of front matter lines removed) * @param mixed|null $frontMatter Parsed front matter */ public function __construct(string $content, int $lineOffset = 0, $frontMatter = null) { parent::__construct($content, $lineOffset); $this->frontMatter = $frontMatter; } /** * {@inheritDoc} */ public function getFrontMatter() { return $this->frontMatter; } } commonmark/src/Extension/FrontMatter/FrontMatterParserInterface.php 0000644 00000001020 15007532371 0021667 0 ustar 00 <?php /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\CommonMark\Extension\FrontMatter; use League\CommonMark\Extension\FrontMatter\Input\MarkdownInputWithFrontMatter; interface FrontMatterParserInterface { public function parse(string $markdownContent): MarkdownInputWithFrontMatter; } commonmark/src/Extension/FrontMatter/Data/SymfonyYamlFrontMatterParser.php 0000644 00000002141 15007532371 0023134 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\FrontMatter\Data; use League\CommonMark\Exception\MissingDependencyException; use League\CommonMark\Extension\FrontMatter\Exception\InvalidFrontMatterException; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Yaml; final class SymfonyYamlFrontMatterParser implements FrontMatterDataParserInterface { /** * {@inheritDoc} */ public function parse(string $frontMatter) { if (! \class_exists(Yaml::class)) { throw new MissingDependencyException('Failed to parse yaml: "symfony/yaml" library is missing'); } try { /** @psalm-suppress ReservedWord */ return Yaml::parse($frontMatter); } catch (ParseException $ex) { throw InvalidFrontMatterException::wrap($ex); } } } commonmark/src/Extension/FrontMatter/Data/FrontMatterDataParserInterface.php 0000644 00000001261 15007532371 0023341 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\FrontMatter\Data; use League\CommonMark\Extension\FrontMatter\Exception\InvalidFrontMatterException; interface FrontMatterDataParserInterface { /** * @return mixed|null The parsed data (which may be null, if the input represents a null value) * * @throws InvalidFrontMatterException if parsing fails */ public function parse(string $frontMatter); } commonmark/src/Extension/FrontMatter/Data/LibYamlFrontMatterParser.php 0000644 00000002276 15007532371 0022207 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\FrontMatter\Data; use League\CommonMark\Exception\MissingDependencyException; use League\CommonMark\Extension\FrontMatter\Exception\InvalidFrontMatterException; final class LibYamlFrontMatterParser implements FrontMatterDataParserInterface { public static function capable(): ?LibYamlFrontMatterParser { if (! \extension_loaded('yaml')) { return null; } return new LibYamlFrontMatterParser(); } /** * {@inheritDoc} */ public function parse(string $frontMatter) { if (! \extension_loaded('yaml')) { throw new MissingDependencyException('Failed to parse yaml: "ext-yaml" extension is missing'); } $result = @\yaml_parse($frontMatter); if ($result === false) { throw new InvalidFrontMatterException('Failed to parse front matter'); } return $result; } } commonmark/src/Extension/FrontMatter/FrontMatterExtension.php 0000644 00000003372 15007532371 0020602 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\FrontMatter; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Event\DocumentPreParsedEvent; use League\CommonMark\Event\DocumentRenderedEvent; use League\CommonMark\Extension\ExtensionInterface; use League\CommonMark\Extension\FrontMatter\Data\FrontMatterDataParserInterface; use League\CommonMark\Extension\FrontMatter\Data\LibYamlFrontMatterParser; use League\CommonMark\Extension\FrontMatter\Data\SymfonyYamlFrontMatterParser; use League\CommonMark\Extension\FrontMatter\Listener\FrontMatterPostRenderListener; use League\CommonMark\Extension\FrontMatter\Listener\FrontMatterPreParser; final class FrontMatterExtension implements ExtensionInterface { /** @psalm-readonly */ private FrontMatterParserInterface $frontMatterParser; public function __construct(?FrontMatterDataParserInterface $dataParser = null) { $this->frontMatterParser = new FrontMatterParser($dataParser ?? LibYamlFrontMatterParser::capable() ?? new SymfonyYamlFrontMatterParser()); } public function getFrontMatterParser(): FrontMatterParserInterface { return $this->frontMatterParser; } public function register(EnvironmentBuilderInterface $environment): void { $environment->addEventListener(DocumentPreParsedEvent::class, new FrontMatterPreParser($this->frontMatterParser)); $environment->addEventListener(DocumentRenderedEvent::class, new FrontMatterPostRenderListener(), -500); } } commonmark/src/Extension/ExternalLink/ExternalLinkExtension.php 0000644 00000003371 15007532371 0021067 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\ExternalLink; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\ConfigurableExtensionInterface; use League\Config\ConfigurationBuilderInterface; use Nette\Schema\Expect; final class ExternalLinkExtension implements ConfigurableExtensionInterface { public function configureSchema(ConfigurationBuilderInterface $builder): void { $applyOptions = [ ExternalLinkProcessor::APPLY_NONE, ExternalLinkProcessor::APPLY_ALL, ExternalLinkProcessor::APPLY_INTERNAL, ExternalLinkProcessor::APPLY_EXTERNAL, ]; $builder->addSchema('external_link', Expect::structure([ 'internal_hosts' => Expect::type('string|string[]'), 'open_in_new_window' => Expect::bool(false), 'html_class' => Expect::string()->default(''), 'nofollow' => Expect::anyOf(...$applyOptions)->default(ExternalLinkProcessor::APPLY_NONE), 'noopener' => Expect::anyOf(...$applyOptions)->default(ExternalLinkProcessor::APPLY_EXTERNAL), 'noreferrer' => Expect::anyOf(...$applyOptions)->default(ExternalLinkProcessor::APPLY_EXTERNAL), ])); } public function register(EnvironmentBuilderInterface $environment): void { $environment->addEventListener(DocumentParsedEvent::class, new ExternalLinkProcessor($environment->getConfiguration()), -50); } } commonmark/src/Extension/ExternalLink/ExternalLinkProcessor.php 0000644 00000007303 15007532371 0021071 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\ExternalLink; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\CommonMark\Node\Inline\Link; use League\Config\ConfigurationInterface; final class ExternalLinkProcessor { public const APPLY_NONE = ''; public const APPLY_ALL = 'all'; public const APPLY_EXTERNAL = 'external'; public const APPLY_INTERNAL = 'internal'; /** @psalm-readonly */ private ConfigurationInterface $config; public function __construct(ConfigurationInterface $config) { $this->config = $config; } public function __invoke(DocumentParsedEvent $e): void { $internalHosts = $this->config->get('external_link/internal_hosts'); $openInNewWindow = $this->config->get('external_link/open_in_new_window'); $classes = $this->config->get('external_link/html_class'); foreach ($e->getDocument()->iterator() as $link) { if (! ($link instanceof Link)) { continue; } $host = \parse_url($link->getUrl(), PHP_URL_HOST); if (! \is_string($host)) { // Something is terribly wrong with this URL continue; } if (self::hostMatches($host, $internalHosts)) { $link->data->set('external', false); $this->applyRelAttribute($link, false); continue; } // Host does not match our list $this->markLinkAsExternal($link, $openInNewWindow, $classes); } } private function markLinkAsExternal(Link $link, bool $openInNewWindow, string $classes): void { $link->data->set('external', true); $this->applyRelAttribute($link, true); if ($openInNewWindow) { $link->data->set('attributes/target', '_blank'); } if ($classes !== '') { $link->data->append('attributes/class', $classes); } } private function applyRelAttribute(Link $link, bool $isExternal): void { $options = [ 'nofollow' => $this->config->get('external_link/nofollow'), 'noopener' => $this->config->get('external_link/noopener'), 'noreferrer' => $this->config->get('external_link/noreferrer'), ]; foreach ($options as $type => $option) { switch (true) { case $option === self::APPLY_ALL: case $isExternal && $option === self::APPLY_EXTERNAL: case ! $isExternal && $option === self::APPLY_INTERNAL: $link->data->append('attributes/rel', $type); } } // No rel attributes? Mark the attribute as 'false' so LinkRenderer doesn't add defaults if (! $link->data->has('attributes/rel')) { $link->data->set('attributes/rel', false); } } /** * @internal This method is only public so we can easily test it. DO NOT USE THIS OUTSIDE OF THIS EXTENSION! * * @param non-empty-string|list<non-empty-string> $compareTo */ public static function hostMatches(string $host, $compareTo): bool { foreach ((array) $compareTo as $c) { if (\strpos($c, '/') === 0) { if (\preg_match($c, $host)) { return true; } } elseif ($c === $host) { return true; } } return false; } } commonmark/src/Extension/ConfigurableExtensionInterface.php 0000644 00000001005 15007532371 0020300 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension; use League\Config\ConfigurationBuilderInterface; interface ConfigurableExtensionInterface extends ExtensionInterface { public function configureSchema(ConfigurationBuilderInterface $builder): void; } commonmark/src/Extension/DefaultAttributes/DefaultAttributesExtension.php 0000644 00000002375 15007532371 0023160 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\DefaultAttributes; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\ConfigurableExtensionInterface; use League\Config\ConfigurationBuilderInterface; use Nette\Schema\Expect; final class DefaultAttributesExtension implements ConfigurableExtensionInterface { public function configureSchema(ConfigurationBuilderInterface $builder): void { $builder->addSchema('default_attributes', Expect::arrayOf( Expect::arrayOf( Expect::type('string|string[]|bool|callable'), // attribute value(s) 'string' // attribute name ), 'string' // node FQCN )->default([])); } public function register(EnvironmentBuilderInterface $environment): void { $environment->addEventListener(DocumentParsedEvent::class, [new ApplyDefaultAttributesProcessor(), 'onDocumentParsed']); } } commonmark/src/Extension/DefaultAttributes/ApplyDefaultAttributesProcessor.php 0000644 00000004117 15007532371 0024165 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\DefaultAttributes; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\Attributes\Util\AttributesHelper; use League\Config\ConfigurationAwareInterface; use League\Config\ConfigurationInterface; final class ApplyDefaultAttributesProcessor implements ConfigurationAwareInterface { private ConfigurationInterface $config; public function onDocumentParsed(DocumentParsedEvent $event): void { /** @var array<string, array<string, mixed>> $map */ $map = $this->config->get('default_attributes'); // Don't bother iterating if no default attributes are configured if (! $map) { return; } foreach ($event->getDocument()->iterator() as $node) { // Check to see if any default attributes were defined if (($attributesToApply = $map[\get_class($node)] ?? []) === []) { continue; } $newAttributes = []; foreach ($attributesToApply as $name => $value) { if (\is_callable($value)) { $value = $value($node); // Callables are allowed to return `null` indicating that no changes should be made if ($value !== null) { $newAttributes[$name] = $value; } } else { $newAttributes[$name] = $value; } } // Merge these attributes into the node if (\count($newAttributes) > 0) { $node->data->set('attributes', AttributesHelper::mergeAttributes($node, $newAttributes)); } } } public function setConfiguration(ConfigurationInterface $configuration): void { $this->config = $configuration; } } commonmark/src/Extension/Table/TableCell.php 0000644 00000004243 15007532371 0015047 0 ustar 00 <?php declare(strict_types=1); /* * This is part of the league/commonmark package. * * (c) Martin Hasoň <martin.hason@gmail.com> * (c) Webuni s.r.o. <info@webuni.cz> * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Table; use League\CommonMark\Node\Block\AbstractBlock; final class TableCell extends AbstractBlock { public const TYPE_HEADER = 'header'; public const TYPE_DATA = 'data'; public const ALIGN_LEFT = 'left'; public const ALIGN_RIGHT = 'right'; public const ALIGN_CENTER = 'center'; /** * @psalm-var self::TYPE_* * @phpstan-var self::TYPE_* * * @psalm-readonly-allow-private-mutation */ private string $type = self::TYPE_DATA; /** * @psalm-var self::ALIGN_*|null * @phpstan-var self::ALIGN_*|null * * @psalm-readonly-allow-private-mutation */ private ?string $align = null; /** * @psalm-param self::TYPE_* $type * @psalm-param self::ALIGN_*|null $align * * @phpstan-param self::TYPE_* $type * @phpstan-param self::ALIGN_*|null $align */ public function __construct(string $type = self::TYPE_DATA, ?string $align = null) { parent::__construct(); $this->type = $type; $this->align = $align; } /** * @psalm-return self::TYPE_* * * @phpstan-return self::TYPE_* */ public function getType(): string { return $this->type; } /** * @psalm-param self::TYPE_* $type * * @phpstan-param self::TYPE_* $type */ public function setType(string $type): void { $this->type = $type; } /** * @psalm-return self::ALIGN_*|null * * @phpstan-return self::ALIGN_*|null */ public function getAlign(): ?string { return $this->align; } /** * @psalm-param self::ALIGN_*|null $align * * @phpstan-param self::ALIGN_*|null $align */ public function setAlign(?string $align): void { $this->align = $align; } } commonmark/src/Extension/Table/TableParser.php 0000644 00000013470 15007532371 0015426 0 ustar 00 <?php declare(strict_types=1); /* * This is part of the league/commonmark package. * * (c) Martin Hasoň <martin.hason@gmail.com> * (c) Webuni s.r.o. <info@webuni.cz> * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Table; use League\CommonMark\Parser\Block\AbstractBlockContinueParser; use League\CommonMark\Parser\Block\BlockContinue; use League\CommonMark\Parser\Block\BlockContinueParserInterface; use League\CommonMark\Parser\Block\BlockContinueParserWithInlinesInterface; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\InlineParserEngineInterface; use League\CommonMark\Util\ArrayCollection; final class TableParser extends AbstractBlockContinueParser implements BlockContinueParserWithInlinesInterface { /** @psalm-readonly */ private Table $block; /** * @var ArrayCollection<string> * * @psalm-readonly-allow-private-mutation */ private ArrayCollection $bodyLines; /** * @var array<int, string|null> * @psalm-var array<int, TableCell::ALIGN_*|null> * @phpstan-var array<int, TableCell::ALIGN_*|null> * * @psalm-readonly */ private array $columns; /** * @var array<int, string> * * @psalm-readonly-allow-private-mutation */ private array $headerCells; /** @psalm-readonly-allow-private-mutation */ private bool $nextIsSeparatorLine = true; /** * @param array<int, string|null> $columns * @param array<int, string> $headerCells * * @psalm-param array<int, TableCell::ALIGN_*|null> $columns * * @phpstan-param array<int, TableCell::ALIGN_*|null> $columns */ public function __construct(array $columns, array $headerCells) { $this->block = new Table(); $this->bodyLines = new ArrayCollection(); $this->columns = $columns; $this->headerCells = $headerCells; } public function canHaveLazyContinuationLines(): bool { return true; } public function getBlock(): Table { return $this->block; } public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue { if (\strpos($cursor->getLine(), '|') === false) { return BlockContinue::none(); } return BlockContinue::at($cursor); } public function addLine(string $line): void { if ($this->nextIsSeparatorLine) { $this->nextIsSeparatorLine = false; } else { $this->bodyLines[] = $line; } } public function parseInlines(InlineParserEngineInterface $inlineParser): void { $headerColumns = \count($this->headerCells); $head = new TableSection(TableSection::TYPE_HEAD); $this->block->appendChild($head); $headerRow = new TableRow(); $head->appendChild($headerRow); for ($i = 0; $i < $headerColumns; $i++) { $cell = $this->headerCells[$i]; $tableCell = $this->parseCell($cell, $i, $inlineParser); $tableCell->setType(TableCell::TYPE_HEADER); $headerRow->appendChild($tableCell); } $body = null; foreach ($this->bodyLines as $rowLine) { $cells = self::split($rowLine); $row = new TableRow(); // Body can not have more columns than head for ($i = 0; $i < $headerColumns; $i++) { $cell = $cells[$i] ?? ''; $tableCell = $this->parseCell($cell, $i, $inlineParser); $row->appendChild($tableCell); } if ($body === null) { // It's valid to have a table without body. In that case, don't add an empty TableBody node. $body = new TableSection(); $this->block->appendChild($body); } $body->appendChild($row); } } private function parseCell(string $cell, int $column, InlineParserEngineInterface $inlineParser): TableCell { $tableCell = new TableCell(); if ($column < \count($this->columns)) { $tableCell->setAlign($this->columns[$column]); } $inlineParser->parse(\trim($cell), $tableCell); return $tableCell; } /** * @internal * * @return array<int, string> */ public static function split(string $line): array { $cursor = new Cursor(\trim($line)); if ($cursor->getCurrentCharacter() === '|') { $cursor->advanceBy(1); } $cells = []; $sb = ''; while (! $cursor->isAtEnd()) { switch ($c = $cursor->getCurrentCharacter()) { case '\\': if ($cursor->peek() === '|') { // Pipe is special for table parsing. An escaped pipe doesn't result in a new cell, but is // passed down to inline parsing as an unescaped pipe. Note that that applies even for the `\|` // in an input like `\\|` - in other words, table parsing doesn't support escaping backslashes. $sb .= '|'; $cursor->advanceBy(1); } else { // Preserve backslash before other characters or at end of line. $sb .= '\\'; } break; case '|': $cells[] = $sb; $sb = ''; break; default: $sb .= $c; } $cursor->advanceBy(1); } if ($sb !== '') { $cells[] = $sb; } return $cells; } } commonmark/src/Extension/Table/TableSection.php 0000644 00000002441 15007532371 0015572 0 ustar 00 <?php declare(strict_types=1); /* * This is part of the league/commonmark package. * * (c) Martin Hasoň <martin.hason@gmail.com> * (c) Webuni s.r.o. <info@webuni.cz> * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Table; use League\CommonMark\Node\Block\AbstractBlock; final class TableSection extends AbstractBlock { public const TYPE_HEAD = 'head'; public const TYPE_BODY = 'body'; /** * @psalm-var self::TYPE_* * @phpstan-var self::TYPE_* * * @psalm-readonly */ private string $type; /** * @psalm-param self::TYPE_* $type * * @phpstan-param self::TYPE_* $type */ public function __construct(string $type = self::TYPE_BODY) { parent::__construct(); $this->type = $type; } /** * @psalm-return self::TYPE_* * * @phpstan-return self::TYPE_* */ public function getType(): string { return $this->type; } public function isHead(): bool { return $this->type === self::TYPE_HEAD; } public function isBody(): bool { return $this->type === self::TYPE_BODY; } } commonmark/src/Extension/Table/TableCellRenderer.php 0000644 00000005071 15007532371 0016536 0 ustar 00 <?php declare(strict_types=1); /* * This is part of the league/commonmark package. * * (c) Martin Hasoň <martin.hason@gmail.com> * (c) Webuni s.r.o. <info@webuni.cz> * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Table; use League\CommonMark\Extension\Attributes\Util\AttributesHelper; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; final class TableCellRenderer implements NodeRendererInterface, XmlNodeRendererInterface { private const DEFAULT_ATTRIBUTES = [ TableCell::ALIGN_LEFT => ['align' => 'left'], TableCell::ALIGN_CENTER => ['align' => 'center'], TableCell::ALIGN_RIGHT => ['align' => 'right'], ]; /** @var array<TableCell::ALIGN_*, array<string, string|string[]|bool>> */ private array $alignmentAttributes; /** * @param array<TableCell::ALIGN_*, array<string, string|string[]|bool>> $alignmentAttributes */ public function __construct(array $alignmentAttributes = self::DEFAULT_ATTRIBUTES) { $this->alignmentAttributes = $alignmentAttributes; } /** * @param TableCell $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { TableCell::assertInstanceOf($node); $attrs = $node->data->get('attributes'); if (($alignment = $node->getAlign()) !== null) { $attrs = AttributesHelper::mergeAttributes($attrs, $this->alignmentAttributes[$alignment]); } $tag = $node->getType() === TableCell::TYPE_HEADER ? 'th' : 'td'; return new HtmlElement($tag, $attrs, $childRenderer->renderNodes($node->children())); } public function getXmlTagName(Node $node): string { return 'table_cell'; } /** * @param TableCell $node * * @return array<string, scalar> * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlAttributes(Node $node): array { TableCell::assertInstanceOf($node); $ret = ['type' => $node->getType()]; if (($align = $node->getAlign()) !== null) { $ret['align'] = $align; } return $ret; } } commonmark/src/Extension/Table/TableExtension.php 0000644 00000004664 15007532371 0016153 0 ustar 00 <?php declare(strict_types=1); /* * This is part of the league/commonmark package. * * (c) Martin Hasoň <martin.hason@gmail.com> * (c) Webuni s.r.o. <info@webuni.cz> * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Table; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Extension\ConfigurableExtensionInterface; use League\CommonMark\Renderer\HtmlDecorator; use League\Config\ConfigurationBuilderInterface; use Nette\Schema\Expect; final class TableExtension implements ConfigurableExtensionInterface { public function configureSchema(ConfigurationBuilderInterface $builder): void { $attributeArraySchema = Expect::arrayOf( Expect::type('string|string[]|bool'), // attribute value(s) 'string' // attribute name )->mergeDefaults(false); $builder->addSchema('table', Expect::structure([ 'wrap' => Expect::structure([ 'enabled' => Expect::bool()->default(false), 'tag' => Expect::string()->default('div'), 'attributes' => Expect::arrayOf(Expect::string()), ]), 'alignment_attributes' => Expect::structure([ 'left' => (clone $attributeArraySchema)->default(['align' => 'left']), 'center' => (clone $attributeArraySchema)->default(['align' => 'center']), 'right' => (clone $attributeArraySchema)->default(['align' => 'right']), ]), ])); } public function register(EnvironmentBuilderInterface $environment): void { $tableRenderer = new TableRenderer(); if ($environment->getConfiguration()->get('table/wrap/enabled')) { $tableRenderer = new HtmlDecorator($tableRenderer, $environment->getConfiguration()->get('table/wrap/tag'), $environment->getConfiguration()->get('table/wrap/attributes')); } $environment ->addBlockStartParser(new TableStartParser()) ->addRenderer(Table::class, $tableRenderer) ->addRenderer(TableSection::class, new TableSectionRenderer()) ->addRenderer(TableRow::class, new TableRowRenderer()) ->addRenderer(TableCell::class, new TableCellRenderer($environment->getConfiguration()->get('table/alignment_attributes'))); } } commonmark/src/Extension/Table/Table.php 0000644 00000000752 15007532371 0014250 0 ustar 00 <?php declare(strict_types=1); /* * This is part of the league/commonmark package. * * (c) Martin Hasoň <martin.hason@gmail.com> * (c) Webuni s.r.o. <info@webuni.cz> * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Table; use League\CommonMark\Node\Block\AbstractBlock; final class Table extends AbstractBlock { } commonmark/src/Extension/Table/TableStartParser.php 0000644 00000010725 15007532371 0016444 0 ustar 00 <?php declare(strict_types=1); /* * This is part of the league/commonmark package. * * (c) Martin Hasoň <martin.hason@gmail.com> * (c) Webuni s.r.o. <info@webuni.cz> * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Table; use League\CommonMark\Parser\Block\BlockStart; use League\CommonMark\Parser\Block\BlockStartParserInterface; use League\CommonMark\Parser\Block\ParagraphParser; use League\CommonMark\Parser\Cursor; use League\CommonMark\Parser\MarkdownParserStateInterface; final class TableStartParser implements BlockStartParserInterface { public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart { $paragraph = $parserState->getParagraphContent(); if ($paragraph === null || \strpos($paragraph, '|') === false) { return BlockStart::none(); } $columns = self::parseSeparator($cursor); if (\count($columns) === 0) { return BlockStart::none(); } $lines = \explode("\n", $paragraph); $lastLine = \array_pop($lines); $headerCells = TableParser::split($lastLine); if (\count($headerCells) > \count($columns)) { return BlockStart::none(); } $cursor->advanceToEnd(); $parsers = []; if (\count($lines) > 0) { $p = new ParagraphParser(); $p->addLine(\implode("\n", $lines)); $parsers[] = $p; } $parsers[] = new TableParser($columns, $headerCells); return BlockStart::of(...$parsers) ->at($cursor) ->replaceActiveBlockParser(); } /** * @return array<int, string|null> * * @psalm-return array<int, TableCell::ALIGN_*|null> * * @phpstan-return array<int, TableCell::ALIGN_*|null> */ private static function parseSeparator(Cursor $cursor): array { $columns = []; $pipes = 0; $valid = false; while (! $cursor->isAtEnd()) { switch ($c = $cursor->getCurrentCharacter()) { case '|': $cursor->advanceBy(1); $pipes++; if ($pipes > 1) { // More than one adjacent pipe not allowed return []; } // Need at least one pipe, even for a one-column table $valid = true; break; case '-': case ':': if ($pipes === 0 && \count($columns) > 0) { // Need a pipe after the first column (first column doesn't need to start with one) return []; } $left = false; $right = false; if ($c === ':') { $left = true; $cursor->advanceBy(1); } if ($cursor->match('/^-+/') === null) { // Need at least one dash return []; } if ($cursor->getCurrentCharacter() === ':') { $right = true; $cursor->advanceBy(1); } $columns[] = self::getAlignment($left, $right); // Next, need another pipe $pipes = 0; break; case ' ': case "\t": // White space is allowed between pipes and columns $cursor->advanceToNextNonSpaceOrTab(); break; default: // Any other character is invalid return []; } } if (! $valid) { return []; } return $columns; } /** * @psalm-return TableCell::ALIGN_*|null * * @phpstan-return TableCell::ALIGN_*|null * * @psalm-pure */ private static function getAlignment(bool $left, bool $right): ?string { if ($left && $right) { return TableCell::ALIGN_CENTER; } if ($left) { return TableCell::ALIGN_LEFT; } if ($right) { return TableCell::ALIGN_RIGHT; } return null; } } commonmark/src/Extension/Table/TableSectionRenderer.php 0000644 00000003455 15007532371 0017267 0 ustar 00 <?php declare(strict_types=1); /* * This is part of the league/commonmark package. * * (c) Martin Hasoň <martin.hason@gmail.com> * (c) Webuni s.r.o. <info@webuni.cz> * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Table; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; final class TableSectionRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param TableSection $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer) { TableSection::assertInstanceOf($node); if (! $node->hasChildren()) { return ''; } $attrs = $node->data->get('attributes'); $separator = $childRenderer->getInnerSeparator(); $tag = $node->getType() === TableSection::TYPE_HEAD ? 'thead' : 'tbody'; return new HtmlElement($tag, $attrs, $separator . $childRenderer->renderNodes($node->children()) . $separator); } public function getXmlTagName(Node $node): string { return 'table_section'; } /** * @param TableSection $node * * @return array<string, scalar> * * @psalm-suppress MoreSpecificImplementedParamType */ public function getXmlAttributes(Node $node): array { TableSection::assertInstanceOf($node); return [ 'type' => $node->getType(), ]; } } commonmark/src/Extension/Table/TableRenderer.php 0000644 00000002725 15007532371 0015741 0 ustar 00 <?php declare(strict_types=1); /* * This is part of the league/commonmark package. * * (c) Martin Hasoň <martin.hason@gmail.com> * (c) Webuni s.r.o. <info@webuni.cz> * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Table; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; final class TableRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param Table $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { Table::assertInstanceOf($node); $attrs = $node->data->get('attributes'); $separator = $childRenderer->getInnerSeparator(); $children = $childRenderer->renderNodes($node->children()); return new HtmlElement('table', $attrs, $separator . \trim($children) . $separator); } public function getXmlTagName(Node $node): string { return 'table'; } /** * {@inheritDoc} */ public function getXmlAttributes(Node $node): array { return []; } } commonmark/src/Extension/Table/TableRow.php 0000644 00000000755 15007532371 0014743 0 ustar 00 <?php declare(strict_types=1); /* * This is part of the league/commonmark package. * * (c) Martin Hasoň <martin.hason@gmail.com> * (c) Webuni s.r.o. <info@webuni.cz> * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Table; use League\CommonMark\Node\Block\AbstractBlock; final class TableRow extends AbstractBlock { } commonmark/src/Extension/Table/TableRowRenderer.php 0000644 00000002670 15007532371 0016430 0 ustar 00 <?php declare(strict_types=1); /* * This is part of the league/commonmark package. * * (c) Martin Hasoň <martin.hason@gmail.com> * (c) Webuni s.r.o. <info@webuni.cz> * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Table; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; final class TableRowRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param TableRow $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { TableRow::assertInstanceOf($node); $attrs = $node->data->get('attributes'); $separator = $childRenderer->getInnerSeparator(); return new HtmlElement('tr', $attrs, $separator . $childRenderer->renderNodes($node->children()) . $separator); } public function getXmlTagName(Node $node): string { return 'table_row'; } /** * {@inheritDoc} */ public function getXmlAttributes(Node $node): array { return []; } } commonmark/src/Extension/Strikethrough/StrikethroughExtension.php 0000644 00000001447 15007532371 0021575 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com) * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Strikethrough; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Extension\ExtensionInterface; final class StrikethroughExtension implements ExtensionInterface { public function register(EnvironmentBuilderInterface $environment): void { $environment->addDelimiterProcessor(new StrikethroughDelimiterProcessor()); $environment->addRenderer(Strikethrough::class, new StrikethroughRenderer()); } } commonmark/src/Extension/Strikethrough/Strikethrough.php 0000644 00000001624 15007532371 0017675 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com) * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Strikethrough; use League\CommonMark\Node\Inline\AbstractInline; use League\CommonMark\Node\Inline\DelimitedInterface; final class Strikethrough extends AbstractInline implements DelimitedInterface { private string $delimiter; public function __construct(string $delimiter = '~~') { parent::__construct(); $this->delimiter = $delimiter; } public function getOpeningDelimiter(): string { return $this->delimiter; } public function getClosingDelimiter(): string { return $this->delimiter; } } commonmark/src/Extension/Strikethrough/StrikethroughDelimiterProcessor.php 0000644 00000003241 15007532371 0023431 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com) * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Strikethrough; use League\CommonMark\Delimiter\DelimiterInterface; use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface; use League\CommonMark\Node\Inline\AbstractStringContainer; final class StrikethroughDelimiterProcessor implements DelimiterProcessorInterface { public function getOpeningCharacter(): string { return '~'; } public function getClosingCharacter(): string { return '~'; } public function getMinLength(): int { return 1; } public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int { if ($opener->getLength() > 2 && $closer->getLength() > 2) { return 0; } if ($opener->getLength() !== $closer->getLength()) { return 0; } return \min($opener->getLength(), $closer->getLength()); } public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void { $strikethrough = new Strikethrough(\str_repeat('~', $delimiterUse)); $tmp = $opener->next(); while ($tmp !== null && $tmp !== $closer) { $next = $tmp->next(); $strikethrough->appendChild($tmp); $tmp = $next; } $opener->insertAfter($strikethrough); } } commonmark/src/Extension/Strikethrough/StrikethroughRenderer.php 0000644 00000002473 15007532371 0021367 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com) * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Strikethrough; use League\CommonMark\Node\Node; use League\CommonMark\Renderer\ChildNodeRendererInterface; use League\CommonMark\Renderer\NodeRendererInterface; use League\CommonMark\Util\HtmlElement; use League\CommonMark\Xml\XmlNodeRendererInterface; final class StrikethroughRenderer implements NodeRendererInterface, XmlNodeRendererInterface { /** * @param Strikethrough $node * * {@inheritDoc} * * @psalm-suppress MoreSpecificImplementedParamType */ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable { Strikethrough::assertInstanceOf($node); return new HtmlElement('del', $node->data->get('attributes'), $childRenderer->renderNodes($node->children())); } public function getXmlTagName(Node $node): string { return 'strikethrough'; } /** * {@inheritDoc} */ public function getXmlAttributes(Node $node): array { return []; } } commonmark/src/Extension/Autolink/AutolinkExtension.php 0000644 00000001320 15007532371 0017433 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Autolink; use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Extension\ExtensionInterface; final class AutolinkExtension implements ExtensionInterface { public function register(EnvironmentBuilderInterface $environment): void { $environment->addInlineParser(new EmailAutolinkParser()); $environment->addInlineParser(new UrlAutolinkParser()); } } commonmark/src/Extension/Autolink/UrlAutolinkParser.php 0000644 00000015752 15007532371 0017414 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Autolink; use League\CommonMark\Extension\CommonMark\Node\Inline\Link; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; final class UrlAutolinkParser implements InlineParserInterface { private const ALLOWED_AFTER = [null, ' ', "\t", "\n", "\x0b", "\x0c", "\x0d", '*', '_', '~', '(']; // RegEx adapted from https://github.com/symfony/symfony/blob/6.3/src/Symfony/Component/Validator/Constraints/UrlValidator.php private const REGEX = '~ ( # Must start with a supported scheme + auth, or "www" (?: (?:%s):// # protocol (?:(?:(?:[\_\.\pL\pN-]|%%[0-9A-Fa-f]{2})+:)?((?:[\_\.\pL\pN-]|%%[0-9A-Fa-f]{2})+)@)? # basic auth |www\.) (?: (?: (?:xn--[a-z0-9-]++\.)*+xn--[a-z0-9-]++ # a domain name using punycode | (?:[\pL\pN\pS\pM\-\_]++\.)+[\pL\pN\pM]++ # a multi-level domain name | [a-z0-9\-\_]++ # a single-level domain name )\.? | # or \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} # an IP address | # or \[ (?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::)))) \] # an IPv6 address ) (?::[0-9]+)? # a port (optional) (?:/ (?:[\pL\pN\-._\~!$&\'()*+,;=:@]|%%[0-9A-Fa-f]{2})* )* # a path (?:\? (?:[\pL\pN\-._\~!$&\'\[\]()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )? # a query (optional) (?:\# (?:[\pL\pN\-._\~!$&\'()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )? # a fragment (optional) )~ixu'; /** * @var string[] * * @psalm-readonly */ private array $prefixes = ['www']; /** * @psalm-var non-empty-string * * @psalm-readonly */ private string $finalRegex; /** * @param array<int, string> $allowedProtocols */ public function __construct(array $allowedProtocols = ['http', 'https', 'ftp']) { /** * @psalm-suppress PropertyTypeCoercion */ $this->finalRegex = \sprintf(self::REGEX, \implode('|', $allowedProtocols)); foreach ($allowedProtocols as $protocol) { $this->prefixes[] = $protocol . '://'; } } public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::oneOf(...$this->prefixes); } public function parse(InlineParserContext $inlineContext): bool { $cursor = $inlineContext->getCursor(); // Autolinks can only come at the beginning of a line, after whitespace, or certain delimiting characters $previousChar = $cursor->peek(-1); if (! \in_array($previousChar, self::ALLOWED_AFTER, true)) { return false; } // Check if we have a valid URL if (! \preg_match($this->finalRegex, $cursor->getRemainder(), $matches)) { return false; } $url = $matches[0]; // Does the URL end with punctuation that should be stripped? if (\preg_match('/(.+?)([?!.,:*_~]+)$/', $url, $matches)) { // Add the punctuation later $url = $matches[1]; } // Does the URL end with something that looks like an entity reference? if (\preg_match('/(.+)(&[A-Za-z0-9]+;)$/', $url, $matches)) { $url = $matches[1]; } // Does the URL need unmatched parens chopped off? if (\substr($url, -1) === ')' && ($diff = self::diffParens($url)) > 0) { $url = \substr($url, 0, -$diff); } $cursor->advanceBy(\mb_strlen($url, 'UTF-8')); // Auto-prefix 'http://' onto 'www' URLs if (\substr($url, 0, 4) === 'www.') { $inlineContext->getContainer()->appendChild(new Link('http://' . $url, $url)); return true; } $inlineContext->getContainer()->appendChild(new Link($url, $url)); return true; } /** * @psalm-pure */ private static function diffParens(string $content): int { // Scan the entire autolink for the total number of parentheses. // If there is a greater number of closing parentheses than opening ones, // we don’t consider ANY of the last characters as part of the autolink, // in order to facilitate including an autolink inside a parenthesis. \preg_match_all('/[()]/', $content, $matches); $charCount = ['(' => 0, ')' => 0]; foreach ($matches[0] as $char) { $charCount[$char]++; } return $charCount[')'] - $charCount['(']; } } commonmark/src/Extension/Autolink/EmailAutolinkParser.php 0000644 00000002651 15007532371 0017673 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the league/commonmark package. * * (c) Colin O'Dell <colinodell@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\CommonMark\Extension\Autolink; use League\CommonMark\Extension\CommonMark\Node\Inline\Link; use League\CommonMark\Parser\Inline\InlineParserInterface; use League\CommonMark\Parser\Inline\InlineParserMatch; use League\CommonMark\Parser\InlineParserContext; final class EmailAutolinkParser implements InlineParserInterface { private const REGEX = '[A-Za-z0-9.\-_+]+@[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_.]+'; public function getMatchDefinition(): InlineParserMatch { return InlineParserMatch::regex(self::REGEX); } public function parse(InlineParserContext $inlineContext): bool { $email = $inlineContext->getFullMatch(); // The last character cannot be - or _ if (\in_array(\substr($email, -1), ['-', '_'], true)) { return false; } // Does the URL end with punctuation that should be stripped? if (\substr($email, -1) === '.') { $email = \substr($email, 0, -1); } $inlineContext->getCursor()->advanceBy(\strlen($email)); $inlineContext->getContainer()->appendChild(new Link('mailto:' . $email, $email)); return true; } } uri-interfaces/composer.json 0000644 00000003432 15007532371 0012213 0 ustar 00 { "name": "league/uri-interfaces", "type": "library", "description" : "Common interfaces and classes for URI representation and interaction", "keywords": [ "url", "uri", "rfc3986", "rfc3987", "rfc6570", "psr-7", "parse_url", "http", "https", "ws", "ftp", "data-uri", "file-uri", "parse_str", "query-string", "querystring", "hostname" ], "license": "MIT", "homepage": "https://uri.thephpleague.com", "authors": [ { "name" : "Ignace Nyamagana Butera", "email" : "nyamsprod@gmail.com", "homepage" : "https://nyamsprod.com" } ], "funding": [ { "type": "github", "url": "https://github.com/sponsors/nyamsprod" } ], "require": { "php" : "^8.1", "ext-filter": "*", "psr/http-message": "^1.1 || ^2.0", "psr/http-factory": "^1" }, "autoload": { "psr-4": { "League\\Uri\\": "" } }, "suggest": { "ext-bcmath": "to improve IPV4 host parsing", "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "extra": { "branch-alias": { "dev-master": "7.x-dev" } }, "support": { "forum": "https://thephpleague.slack.com", "docs": "https://uri.thephpleague.com", "issues": "https://github.com/thephpleague/uri-src/issues" }, "config": { "sort-packages": true } } uri-interfaces/LICENSE 0000644 00000002102 15007532371 0010467 0 ustar 00 The MIT License (MIT) Copyright (c) 2015 ignace nyamagana butera 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. uri-interfaces/QueryString.php 0000644 00000022056 15007532371 0012501 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri; use League\Uri\Exceptions\SyntaxError; use League\Uri\KeyValuePair\Converter; use Stringable; use function array_key_exists; use function array_keys; use function is_array; use function rawurldecode; use function strpos; use function substr; use const PHP_QUERY_RFC3986; /** * A class to parse the URI query string. * * @see https://tools.ietf.org/html/rfc3986#section-3.4 */ final class QueryString { private const PAIR_VALUE_DECODED = 1; private const PAIR_VALUE_PRESERVED = 2; /** * @codeCoverageIgnore */ private function __construct() { } /** * Build a query string from a list of pairs. * * @see QueryString::buildFromPairs() * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 * * @param iterable<array{0:string, 1:string|float|int|bool|null}> $pairs * @param non-empty-string $separator * * @throws SyntaxError If the encoding type is invalid * @throws SyntaxError If a pair is invalid */ public static function build(iterable $pairs, string $separator = '&', int $encType = PHP_QUERY_RFC3986): ?string { return self::buildFromPairs($pairs, Converter::fromEncodingType($encType)->withSeparator($separator)); } /** * Build a query string from a list of pairs. * * The method expects the return value from Query::parse to build * a valid query string. This method differs from PHP http_build_query as * it does not modify parameters keys. * * If a reserved character is found in a URI component and * no delimiting role is known for that character, then it must be * interpreted as representing the data octet corresponding to that * character's encoding in US-ASCII. * * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 * * @param iterable<array{0:string, 1:string|float|int|bool|null}> $pairs * * @throws SyntaxError If the encoding type is invalid * @throws SyntaxError If a pair is invalid */ public static function buildFromPairs(iterable $pairs, ?Converter $converter = null): ?string { $keyValuePairs = []; foreach ($pairs as $pair) { if (!is_array($pair) || [0, 1] !== array_keys($pair)) { throw new SyntaxError('A pair must be a sequential array starting at `0` and containing two elements.'); } $keyValuePairs[] = [(string) Encoder::encodeQueryKeyValue($pair[0]), match(null) { $pair[1] => null, default => Encoder::encodeQueryKeyValue($pair[1]), }]; } return ($converter ?? Converter::fromRFC3986())->toValue($keyValuePairs); } /** * Parses the query string like parse_str without mangling the results. * * @see QueryString::extractFromValue() * @see http://php.net/parse_str * @see https://wiki.php.net/rfc/on_demand_name_mangling * * @param non-empty-string $separator * * @throws SyntaxError */ public static function extract(Stringable|string|bool|null $query, string $separator = '&', int $encType = PHP_QUERY_RFC3986): array { return self::extractFromValue($query, Converter::fromEncodingType($encType)->withSeparator($separator)); } /** * Parses the query string like parse_str without mangling the results. * * The result is similar as PHP parse_str when used with its * second argument with the difference that variable names are * not mangled. * * @see http://php.net/parse_str * @see https://wiki.php.net/rfc/on_demand_name_mangling * * @throws SyntaxError */ public static function extractFromValue(Stringable|string|bool|null $query, ?Converter $converter = null): array { return self::convert(self::decodePairs( ($converter ?? Converter::fromRFC3986())->toPairs($query), self::PAIR_VALUE_PRESERVED )); } /** * Parses a query string into a collection of key/value pairs. * * @param non-empty-string $separator * * @throws SyntaxError * * @return array<int, array{0:string, 1:string|null}> */ public static function parse(Stringable|string|bool|null $query, string $separator = '&', int $encType = PHP_QUERY_RFC3986): array { return self::parseFromValue($query, Converter::fromEncodingType($encType)->withSeparator($separator)); } /** * Parses a query string into a collection of key/value pairs. * * @throws SyntaxError * * @return array<int, array{0:string, 1:string|null}> */ public static function parseFromValue(Stringable|string|bool|null $query, ?Converter $converter = null): array { return self::decodePairs( ($converter ?? Converter::fromRFC3986())->toPairs($query), self::PAIR_VALUE_DECODED ); } /** * @param array<non-empty-list<string|null>> $pairs * * @return array<int, array{0:string, 1:string|null}> */ private static function decodePairs(array $pairs, int $pairValueState): array { $decodePair = static function (array $pair, int $pairValueState): array { [$key, $value] = $pair; return match ($pairValueState) { self::PAIR_VALUE_PRESERVED => [(string) Encoder::decodeAll($key), $value], default => [(string) Encoder::decodeAll($key), Encoder::decodeAll($value)], }; }; return array_reduce( $pairs, fn (array $carry, array $pair) => [...$carry, $decodePair($pair, $pairValueState)], [] ); } /** * Converts a collection of key/value pairs and returns * the store PHP variables as elements of an array. */ public static function convert(iterable $pairs): array { $returnedValue = []; foreach ($pairs as $pair) { $returnedValue = self::extractPhpVariable($returnedValue, $pair); } return $returnedValue; } /** * Parses a query pair like parse_str without mangling the results array keys. * * <ul> * <li>empty name are not saved</li> * <li>If the value from name is duplicated its corresponding value will be overwritten</li> * <li>if no "[" is detected the value is added to the return array with the name as index</li> * <li>if no "]" is detected after detecting a "[" the value is added to the return array with the name as index</li> * <li>if there's a mismatch in bracket usage the remaining part is dropped</li> * <li>“.” and “ ” are not converted to “_”</li> * <li>If there is no “]”, then the first “[” is not converted to becomes an “_”</li> * <li>no whitespace trimming is done on the key value</li> * </ul> * * @see https://php.net/parse_str * @see https://wiki.php.net/rfc/on_demand_name_mangling * @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic1.phpt * @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic2.phpt * @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic3.phpt * @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic4.phpt * * @param array $data the submitted array * @param array|string $name the pair key * @param string $value the pair value */ private static function extractPhpVariable(array $data, array|string $name, string $value = ''): array { if (is_array($name)) { [$name, $value] = $name; $value = rawurldecode((string) $value); } if ('' === $name) { return $data; } $leftBracketPosition = strpos($name, '['); if (false === $leftBracketPosition) { $data[$name] = $value; return $data; } $rightBracketPosition = strpos($name, ']', $leftBracketPosition); if (false === $rightBracketPosition) { $data[$name] = $value; return $data; } $key = substr($name, 0, $leftBracketPosition); if (!array_key_exists($key, $data) || !is_array($data[$key])) { $data[$key] = []; } $index = substr($name, $leftBracketPosition + 1, $rightBracketPosition - $leftBracketPosition - 1); if ('' === $index) { $data[$key][] = $value; return $data; } $remaining = substr($name, $rightBracketPosition + 1); if (!str_starts_with($remaining, '[') || false === strpos($remaining, ']', 1)) { $remaining = ''; } $data[$key] = self::extractPhpVariable($data[$key], $index.$remaining, $value); return $data; } } uri-interfaces/Encoder.php 0000644 00000014120 15007532371 0011555 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri; use Closure; use League\Uri\Contracts\UriComponentInterface; use League\Uri\Exceptions\SyntaxError; use Stringable; use function preg_match; use function preg_replace_callback; use function rawurldecode; use function rawurlencode; use function strtoupper; final class Encoder { private const REGEXP_CHARS_INVALID = '/[\x00-\x1f\x7f]/'; private const REGEXP_CHARS_ENCODED = ',%[A-Fa-f0-9]{2},'; private const REGEXP_CHARS_PREVENTS_DECODING = ',% 2[A-F|1-2|4-9]| 3[0-9|B|D]| 4[1-9|A-F]| 5[0-9|A|F]| 6[1-9|A-F]| 7[0-9|E] ,ix'; private const REGEXP_PART_SUBDELIM = "\!\$&'\(\)\*\+,;\=%"; private const REGEXP_PART_UNRESERVED = 'A-Za-z\d_\-.~'; private const REGEXP_PART_ENCODED = '%(?![A-Fa-f\d]{2})'; /** * Encode User. * * All generic delimiters MUST be encoded */ public static function encodeUser(Stringable|string|null $component): ?string { static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.']+|'.self::REGEXP_PART_ENCODED.'/'; return self::encode($component, $pattern); } /** * Encode Password. * * Generic delimiters ":" MUST NOT be encoded */ public static function encodePassword(Stringable|string|null $component): ?string { static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':]+|'.self::REGEXP_PART_ENCODED.'/'; return self::encode($component, $pattern); } /** * Encode Path. * * Generic delimiters ":", "@", and "/" MUST NOT be encoded */ public static function encodePath(Stringable|string|null $component): string { static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/]+|'.self::REGEXP_PART_ENCODED.'/'; return (string) self::encode($component, $pattern); } /** * Encode Query or Fragment. * * Generic delimiters ":", "@", "?", and "/" MUST NOT be encoded */ public static function encodeQueryOrFragment(Stringable|string|null $component): ?string { static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/?]+|'.self::REGEXP_PART_ENCODED.'/'; return self::encode($component, $pattern); } public static function encodeQueryKeyValue(mixed $component): ?string { static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.']+|'.self::REGEXP_PART_ENCODED.'/'; $encodeMatches = static fn (array $matches): string => match (1) { preg_match('/[^'.self::REGEXP_PART_UNRESERVED.']/', rawurldecode($matches[0])) => rawurlencode($matches[0]), default => $matches[0], }; $component = self::filterComponent($component); return match (true) { !is_scalar($component) => throw new SyntaxError(sprintf('A pair key/value must be a scalar value `%s` given.', gettype($component))), 1 === preg_match(self::REGEXP_CHARS_INVALID, $component) => rawurlencode($component), 1 === preg_match($pattern, $component) => (string) preg_replace_callback($pattern, $encodeMatches(...), $component), default => $component, }; } /** * Decodes the URI component without decoding the unreserved characters which are already encoded. */ public static function decodePartial(Stringable|string|int|null $component): ?string { $decodeMatches = static fn (array $matches): string => match (1) { preg_match(self::REGEXP_CHARS_PREVENTS_DECODING, $matches[0]) => strtoupper($matches[0]), default => rawurldecode($matches[0]), }; return self::decode($component, $decodeMatches); } /** * Decodes all the URI component characters. */ public static function decodeAll(Stringable|string|int|null $component): ?string { $decodeMatches = static fn (array $matches): string => rawurldecode($matches[0]); return self::decode($component, $decodeMatches); } private static function filterComponent(mixed $component): ?string { return match (true) { true === $component => '1', false === $component => '0', $component instanceof UriComponentInterface => $component->value(), $component instanceof Stringable, is_scalar($component) => (string) $component, null === $component => null, default => throw new SyntaxError(sprintf('The component must be a scalar value `%s` given.', gettype($component))), }; } private static function encode(Stringable|string|int|bool|null $component, string $pattern): ?string { $component = self::filterComponent($component); $encodeMatches = static fn (array $matches): string => match (1) { preg_match('/[^'.self::REGEXP_PART_UNRESERVED.']/', rawurldecode($matches[0])) => rawurlencode($matches[0]), default => $matches[0], }; return match (true) { null === $component, '' === $component => $component, default => (string) preg_replace_callback($pattern, $encodeMatches(...), $component), }; } /** * Decodes all the URI component characters. */ private static function decode(Stringable|string|int|null $component, Closure $decodeMatches): ?string { $component = self::filterComponent($component); return match (true) { null === $component => null, 1 === preg_match(self::REGEXP_CHARS_INVALID, $component) => throw new SyntaxError('Invalid component string: '.$component.'.'), 1 === preg_match(self::REGEXP_CHARS_ENCODED, $component) => preg_replace_callback(self::REGEXP_CHARS_ENCODED, $decodeMatches(...), $component), default => $component, }; } } uri-interfaces/IPv4/GMPCalculator.php 0000644 00000002741 15007532371 0013423 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\IPv4; use GMP; use function gmp_add; use function gmp_cmp; use function gmp_div_q; use function gmp_init; use function gmp_mod; use function gmp_mul; use function gmp_pow; use function gmp_sub; use const GMP_ROUND_MINUSINF; final class GMPCalculator implements Calculator { public function baseConvert(mixed $value, int $base): GMP { return gmp_init($value, $base); } public function pow(mixed $value, int $exponent): GMP { return gmp_pow($value, $exponent); } public function compare(mixed $value1, mixed $value2): int { return gmp_cmp($value1, $value2); } public function multiply(mixed $value1, mixed $value2): GMP { return gmp_mul($value1, $value2); } public function div(mixed $value, mixed $base): GMP { return gmp_div_q($value, $base, GMP_ROUND_MINUSINF); } public function mod(mixed $value, mixed $base): GMP { return gmp_mod($value, $base); } public function add(mixed $value1, mixed $value2): GMP { return gmp_add($value1, $value2); } public function sub(mixed $value1, mixed $value2): GMP { return gmp_sub($value1, $value2); } } uri-interfaces/IPv4/BCMathCalculator.php 0000644 00000004321 15007532371 0014072 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\IPv4; use function bcadd; use function bccomp; use function bcdiv; use function bcmod; use function bcmul; use function bcpow; use function bcsub; use function str_split; final class BCMathCalculator implements Calculator { private const SCALE = 0; private const CONVERSION_TABLE = [ '0' => '0', '1' => '1', '2' => '2', '3' => '3', '4' => '4', '5' => '5', '6' => '6', '7' => '7', '8' => '8', '9' => '9', 'a' => '10', 'b' => '11', 'c' => '12', 'd' => '13', 'e' => '14', 'f' => '15', ]; public function baseConvert(mixed $value, int $base): string { $value = (string) $value; if (10 === $base) { return $value; } $base = (string) $base; $decimal = '0'; foreach (str_split($value) as $char) { $decimal = bcadd($this->multiply($decimal, $base), self::CONVERSION_TABLE[$char], self::SCALE); } return $decimal; } public function pow(mixed $value, int $exponent): string { return bcpow((string) $value, (string) $exponent, self::SCALE); } public function compare(mixed $value1, $value2): int { return bccomp((string) $value1, (string) $value2, self::SCALE); } public function multiply(mixed $value1, $value2): string { return bcmul((string) $value1, (string) $value2, self::SCALE); } public function div(mixed $value, mixed $base): string { return bcdiv((string) $value, (string) $base, self::SCALE); } public function mod(mixed $value, mixed $base): string { return bcmod((string) $value, (string) $base, self::SCALE); } public function add(mixed $value1, mixed $value2): string { return bcadd((string) $value1, (string) $value2, self::SCALE); } public function sub(mixed $value1, mixed $value2): string { return bcsub((string) $value1, (string) $value2, self::SCALE); } } uri-interfaces/IPv4/Converter.php 0000644 00000014041 15007532371 0012731 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\IPv4; use League\Uri\Exceptions\MissingFeature; use League\Uri\FeatureDetection; use Stringable; use function array_pop; use function count; use function explode; use function extension_loaded; use function ltrim; use function preg_match; use function str_ends_with; use function substr; final class Converter { private const REGEXP_IPV4_HOST = '/ (?(DEFINE) # . is missing as it is used to separate labels (?<hexadecimal>0x[[:xdigit:]]*) (?<octal>0[0-7]*) (?<decimal>\d+) (?<ipv4_part>(?:(?&hexadecimal)|(?&octal)|(?&decimal))*) ) ^(?:(?&ipv4_part)\.){0,3}(?&ipv4_part)\.?$ /x'; private const REGEXP_IPV4_NUMBER_PER_BASE = [ '/^0x(?<number>[[:xdigit:]]*)$/' => 16, '/^0(?<number>[0-7]*)$/' => 8, '/^(?<number>\d+)$/' => 10, ]; private readonly mixed $maxIPv4Number; public function __construct( private readonly Calculator $calculator ) { $this->maxIPv4Number = $calculator->sub($calculator->pow(2, 32), 1); } /** * Returns an instance using a GMP calculator. */ public static function fromGMP(): self { return new self(new GMPCalculator()); } /** * Returns an instance using a Bcmath calculator. */ public static function fromBCMath(): self { return new self(new BCMathCalculator()); } /** * Returns an instance using a PHP native calculator (requires 64bits PHP). */ public static function fromNative(): self { return new self(new NativeCalculator()); } /** * Returns an instance using a detected calculator depending on the PHP environment. * * @throws MissingFeature If no Calculator implementing object can be used on the platform * * @codeCoverageIgnore */ public static function fromEnvironment(): self { FeatureDetection::supportsIPv4Conversion(); return match (true) { extension_loaded('gmp') => self::fromGMP(), extension_loaded('bcmath') => self::fromBCMath(), default => self::fromNative(), }; } public function isIpv4(Stringable|string|null $host): bool { return null !== $this->toDecimal($host); } public function toOctal(Stringable|string|null $host): ?string { $host = $this->toDecimal($host); return match (null) { $host => null, default => implode('.', array_map( fn ($value) => str_pad(decoct((int) $value), 4, '0', STR_PAD_LEFT), explode('.', $host) )), }; } public function toHexadecimal(Stringable|string|null $host): ?string { $host = $this->toDecimal($host); return match (null) { $host => null, default => '0x'.implode('', array_map( fn ($value) => dechex((int) $value), explode('.', $host) )), }; } /** * Tries to convert a IPv4 hexadecimal or a IPv4 octal notation into a IPv4 dot-decimal notation if possible * otherwise returns null. * * @see https://url.spec.whatwg.org/#concept-ipv4-parser */ public function toDecimal(Stringable|string|null $host): ?string { $host = (string) $host; if (1 !== preg_match(self::REGEXP_IPV4_HOST, $host)) { return null; } if (str_ends_with($host, '.')) { $host = substr($host, 0, -1); } $numbers = []; foreach (explode('.', $host) as $label) { $number = $this->labelToNumber($label); if (null === $number) { return null; } $numbers[] = $number; } $ipv4 = array_pop($numbers); $max = $this->calculator->pow(256, 6 - count($numbers)); if ($this->calculator->compare($ipv4, $max) > 0) { return null; } foreach ($numbers as $offset => $number) { if ($this->calculator->compare($number, 255) > 0) { return null; } $ipv4 = $this->calculator->add($ipv4, $this->calculator->multiply( $number, $this->calculator->pow(256, 3 - $offset) )); } return $this->long2Ip($ipv4); } /** * Converts a domain label into a IPv4 integer part. * * @see https://url.spec.whatwg.org/#ipv4-number-parser * * @return mixed returns null if it cannot correctly convert the label */ private function labelToNumber(string $label): mixed { foreach (self::REGEXP_IPV4_NUMBER_PER_BASE as $regexp => $base) { if (1 !== preg_match($regexp, $label, $matches)) { continue; } $number = ltrim($matches['number'], '0'); if ('' === $number) { return 0; } $number = $this->calculator->baseConvert($number, $base); if (0 <= $this->calculator->compare($number, 0) && 0 >= $this->calculator->compare($number, $this->maxIPv4Number)) { return $number; } } return null; } /** * Generates the dot-decimal notation for IPv4. * * @see https://url.spec.whatwg.org/#concept-ipv4-parser * * @param mixed $ipAddress the number representation of the IPV4address */ private function long2Ip(mixed $ipAddress): string { $output = ''; for ($offset = 0; $offset < 4; $offset++) { $output = $this->calculator->mod($ipAddress, 256).$output; if ($offset < 3) { $output = '.'.$output; } $ipAddress = $this->calculator->div($ipAddress, 256); } return $output; } } uri-interfaces/IPv4/Calculator.php 0000644 00000005070 15007532371 0013055 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\IPv4; interface Calculator { /** * Add numbers. * * @param mixed $value1 a number that will be added to $value2 * @param mixed $value2 a number that will be added to $value1 * * @return mixed the addition result */ public function add(mixed $value1, mixed $value2); /** * Subtract one number from another. * * @param mixed $value1 a number that will be subtracted of $value2 * @param mixed $value2 a number that will be subtracted to $value1 * * @return mixed the subtraction result */ public function sub(mixed $value1, mixed $value2); /** * Multiply numbers. * * @param mixed $value1 a number that will be multiplied by $value2 * @param mixed $value2 a number that will be multiplied by $value1 * * @return mixed the multiplication result */ public function multiply(mixed $value1, mixed $value2); /** * Divide numbers. * * @param mixed $value The number being divided. * @param mixed $base The number that $value is being divided by. * * @return mixed the result of the division */ public function div(mixed $value, mixed $base); /** * Raise an number to the power of exponent. * * @param mixed $value scalar, the base to use * * @return mixed the value raised to the power of exp. */ public function pow(mixed $value, int $exponent); /** * Returns the int point remainder (modulo) of the division of the arguments. * * @param mixed $value The dividend * @param mixed $base The divisor * * @return mixed the remainder */ public function mod(mixed $value, mixed $base); /** * Number comparison. * * @param mixed $value1 the first value * @param mixed $value2 the second value * * @return int Returns < 0 if value1 is less than value2; > 0 if value1 is greater than value2, and 0 if they are equal. */ public function compare(mixed $value1, mixed $value2): int; /** * Get the decimal integer value of a variable. * * @param mixed $value The scalar value being converted to an integer * * @return mixed the integer value */ public function baseConvert(mixed $value, int $base); } uri-interfaces/IPv4/NativeCalculator.php 0000644 00000002365 15007532371 0014230 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\IPv4; use function floor; use function intval; final class NativeCalculator implements Calculator { public function baseConvert(mixed $value, int $base): int { return intval((string) $value, $base); } public function pow(mixed $value, int $exponent) { return $value ** $exponent; } public function compare(mixed $value1, mixed $value2): int { return $value1 <=> $value2; } public function multiply(mixed $value1, mixed $value2): int { return $value1 * $value2; } public function div(mixed $value, mixed $base): int { return (int) floor($value / $base); } public function mod(mixed $value, mixed $base): int { return $value % $base; } public function add(mixed $value1, mixed $value2): int { return $value1 + $value2; } public function sub(mixed $value1, mixed $value2): int { return $value1 - $value2; } } uri-interfaces/KeyValuePair/Converter.php 0000644 00000014422 15007532371 0014513 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\KeyValuePair; use League\Uri\Contracts\UriComponentInterface; use League\Uri\Exceptions\SyntaxError; use Stringable; use function array_combine; use function explode; use function implode; use function is_float; use function is_int; use function is_string; use function json_encode; use function preg_match; use function str_replace; use const JSON_PRESERVE_ZERO_FRACTION; use const PHP_QUERY_RFC1738; use const PHP_QUERY_RFC3986; final class Converter { private const REGEXP_INVALID_CHARS = '/[\x00-\x1f\x7f]/'; /** @var non-empty-string */ private readonly string $separator; /** * @param array<string> $fromRfc3986 contains all the RFC3986 encoded characters to be converted * @param array<string> $toEncoding contains all the expected encoded characters */ private function __construct( string $separator, private readonly array $fromRfc3986 = [], private readonly array $toEncoding = [], ) { if ('' === $separator) { throw new SyntaxError('The separator character must be a non empty string.'); } $this->separator = $separator; } /** * @param non-empty-string $separator */ public static function new(string $separator): self { return new self($separator); } /** * @param non-empty-string $separator */ public static function fromRFC3986(string $separator = '&'): self { return self::new($separator); } /** * @param non-empty-string $separator */ public static function fromRFC1738(string $separator = '&'): self { return self::new($separator) ->withEncodingMap(['%20' => '+']); } /** * @param non-empty-string $separator * * @see https://url.spec.whatwg.org/#application/x-www-form-urlencoded */ public static function fromFormData(string $separator = '&'): self { return self::new($separator) ->withEncodingMap(['%20' => '+', '%2A' => '*']); } public static function fromEncodingType(int $encType): self { return match ($encType) { PHP_QUERY_RFC3986 => self::fromRFC3986(), PHP_QUERY_RFC1738 => self::fromRFC1738(), default => throw new SyntaxError('Unknown or Unsupported encoding.'), }; } /** * @return non-empty-string */ public function separator(): string { return $this->separator; } /** * @return array<string, string> */ public function encodingMap(): array { return array_combine($this->fromRfc3986, $this->toEncoding); } /** * @return array<non-empty-list<string|null>> */ public function toPairs(Stringable|string|int|float|bool|null $value): array { $value = match (true) { $value instanceof UriComponentInterface => $value->value(), $value instanceof Stringable, is_int($value) => (string) $value, false === $value => '0', true === $value => '1', default => $value, }; if (null === $value) { return []; } $value = match (1) { preg_match(self::REGEXP_INVALID_CHARS, (string) $value) => throw new SyntaxError('Invalid query string: `'.$value.'`.'), default => str_replace($this->toEncoding, $this->fromRfc3986, (string) $value), }; return array_map( fn (string $pair): array => explode('=', $pair, 2) + [1 => null], explode($this->separator, $value) ); } private static function vString(Stringable|string|bool|int|float|null $value): ?string { return match (true) { $value => '1', false === $value => '0', null === $value => null, is_float($value) => (string) json_encode($value, JSON_PRESERVE_ZERO_FRACTION), default => (string) $value, }; } /** * @param iterable<array{0:string|null, 1:Stringable|string|bool|int|float|null}> $pairs */ public function toValue(iterable $pairs): ?string { $filteredPairs = []; foreach ($pairs as $pair) { $filteredPairs[] = match (true) { !is_string($pair[0]) => throw new SyntaxError('the pair key MUST be a string;, `'.gettype($pair[0]).'` given.'), null === $pair[1] => self::vString($pair[0]), default => self::vString($pair[0]).'='.self::vString($pair[1]), }; } return match ([]) { $filteredPairs => null, default => str_replace($this->fromRfc3986, $this->toEncoding, implode($this->separator, $filteredPairs)), }; } /** * @param non-empty-string $separator */ public function withSeparator(string $separator): self { return match ($this->separator) { $separator => $this, default => new self($separator, $this->fromRfc3986, $this->toEncoding), }; } /** * Sets the conversion map. * * Each key from the iterable structure represents the RFC3986 encoded characters as string, * while each value represents the expected output encoded characters */ public function withEncodingMap(iterable $encodingMap): self { $fromRfc3986 = []; $toEncoding = []; foreach ($encodingMap as $from => $to) { [$fromRfc3986[], $toEncoding[]] = match (true) { !is_string($from) => throw new SyntaxError('The encoding output must be a string; `'.gettype($from).'` given.'), $to instanceof Stringable, is_string($to) => [$from, (string) $to], default => throw new SyntaxError('The encoding output must be a string; `'.gettype($to).'` given.'), }; } return match (true) { $fromRfc3986 !== $this->fromRfc3986, $toEncoding !== $this->toEncoding => new self($this->separator, $fromRfc3986, $toEncoding), default => $this, }; } } uri-interfaces/Exceptions/SyntaxError.php 0000644 00000000710 15007532371 0014617 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Exceptions; use InvalidArgumentException; use League\Uri\Contracts\UriException; class SyntaxError extends InvalidArgumentException implements UriException { } uri-interfaces/Exceptions/ConversionFailed.php 0000644 00000002113 15007532371 0015550 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Exceptions; use League\Uri\Idna\Error; use League\Uri\Idna\Result; use Stringable; final class ConversionFailed extends SyntaxError { private function __construct( string $message, private readonly string $host, private readonly Result $result ) { parent::__construct($message); } public static function dueToIdnError(Stringable|string $host, Result $result): self { $reasons = array_map(fn (Error $error): string => $error->description(), $result->errors()); return new self('Host `'.$host.'` is invalid: '.implode('; ', $reasons).'.', (string) $host, $result); } public function getHost(): string { return $this->host; } public function getResult(): Result { return $this->result; } } uri-interfaces/Exceptions/OffsetOutOfBounds.php 0000644 00000000543 15007532371 0015701 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Exceptions; class OffsetOutOfBounds extends SyntaxError { } uri-interfaces/Exceptions/MissingFeature.php 0000644 00000000673 15007532371 0015254 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Exceptions; use League\Uri\Contracts\UriException; use RuntimeException; class MissingFeature extends RuntimeException implements UriException { } uri-interfaces/UriString.php 0000644 00000035125 15007532371 0012134 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri; use League\Uri\Exceptions\ConversionFailed; use League\Uri\Exceptions\MissingFeature; use League\Uri\Exceptions\SyntaxError; use League\Uri\Idna\Converter; use Stringable; use function array_merge; use function explode; use function filter_var; use function inet_pton; use function preg_match; use function rawurldecode; use function sprintf; use function strpos; use function substr; use const FILTER_FLAG_IPV6; use const FILTER_VALIDATE_IP; /** * A class to parse a URI string according to RFC3986. * * @link https://tools.ietf.org/html/rfc3986 * @package League\Uri * @author Ignace Nyamagana Butera <nyamsprod@gmail.com> * @since 6.0.0 * * @phpstan-type AuthorityMap array{user:?string, pass:?string, host:?string, port:?int} * @phpstan-type ComponentMap array{scheme:?string, user:?string, pass:?string, host:?string, port:?int, path:string, query:?string, fragment:?string} * @phpstan-type InputComponentMap array{scheme? : ?string, user? : ?string, pass? : ?string, host? : ?string, port? : ?int, path? : ?string, query? : ?string, fragment? : ?string} */ final class UriString { /** * Default URI component values. */ private const URI_COMPONENTS = [ 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null, 'port' => null, 'path' => '', 'query' => null, 'fragment' => null, ]; /** * Simple URI which do not need any parsing. */ private const URI_SCHORTCUTS = [ '' => [], '#' => ['fragment' => ''], '?' => ['query' => ''], '?#' => ['query' => '', 'fragment' => ''], '/' => ['path' => '/'], '//' => ['host' => ''], ]; /** * Range of invalid characters in URI string. */ private const REGEXP_INVALID_URI_CHARS = '/[\x00-\x1f\x7f]/'; /** * RFC3986 regular expression URI splitter. * * @link https://tools.ietf.org/html/rfc3986#appendix-B */ private const REGEXP_URI_PARTS = ',^ (?<scheme>(?<scontent>[^:/?\#]+):)? # URI scheme component (?<authority>//(?<acontent>[^/?\#]*))? # URI authority part (?<path>[^?\#]*) # URI path component (?<query>\?(?<qcontent>[^\#]*))? # URI query component (?<fragment>\#(?<fcontent>.*))? # URI fragment component ,x'; /** * URI scheme regular expresssion. * * @link https://tools.ietf.org/html/rfc3986#section-3.1 */ private const REGEXP_URI_SCHEME = '/^([a-z][a-z\d+.-]*)?$/i'; /** * IPvFuture regular expression. * * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 */ private const REGEXP_IP_FUTURE = '/^ v(?<version>[A-F0-9])+\. (?: (?<unreserved>[a-z0-9_~\-\.])| (?<sub_delims>[!$&\'()*+,;=:]) # also include the : character )+ $/ix'; /** * General registered name regular expression. * * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 */ private const REGEXP_REGISTERED_NAME = '/(?(DEFINE) (?<unreserved>[a-z0-9_~\-]) # . is missing as it is used to separate labels (?<sub_delims>[!$&\'()*+,;=]) (?<encoded>%[A-F0-9]{2}) (?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded))*) ) ^(?:(?®_name)\.)*(?®_name)\.?$/ix'; /** * Invalid characters in host regular expression. * * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 */ private const REGEXP_INVALID_HOST_CHARS = '/ [:\/?#\[\]@ ] # gen-delims characters as well as the space character /ix'; /** * Invalid path for URI without scheme and authority regular expression. * * @link https://tools.ietf.org/html/rfc3986#section-3.3 */ private const REGEXP_INVALID_PATH = ',^(([^/]*):)(.*)?/,'; /** * Host and Port splitter regular expression. */ private const REGEXP_HOST_PORT = ',^(?<host>\[.*\]|[^:]*)(:(?<port>.*))?$,'; /** * IDN Host detector regular expression. */ private const REGEXP_IDN_PATTERN = '/[^\x20-\x7f]/'; /** * Only the address block fe80::/10 can have a Zone ID attach to * let's detect the link local significant 10 bits. */ private const ZONE_ID_ADDRESS_BLOCK = "\xfe\x80"; /** * Generate a URI string representation from its parsed representation * returned by League\UriString::parse() or PHP's parse_url. * * If you supply your own array, you are responsible for providing * valid components without their URI delimiters. * * @link https://tools.ietf.org/html/rfc3986#section-5.3 * @link https://tools.ietf.org/html/rfc3986#section-7.5 * * @param InputComponentMap $components */ public static function build(array $components): string { $uri = $components['path'] ?? ''; if (isset($components['query'])) { $uri .= '?'.$components['query']; } if (isset($components['fragment'])) { $uri .= '#'.$components['fragment']; } $scheme = null; if (isset($components['scheme'])) { $scheme = $components['scheme'].':'; } $authority = self::buildAuthority($components); if (null !== $authority) { $authority = '//'.$authority; } return $scheme.$authority.$uri; } /** * Generate a URI authority representation from its parsed representation. * * @param InputComponentMap $components */ public static function buildAuthority(array $components): ?string { if (!isset($components['host'])) { return null; } $authority = $components['host']; if (isset($components['port'])) { $authority .= ':'.$components['port']; } if (!isset($components['user'])) { return $authority; } $authority = '@'.$authority; if (!isset($components['pass'])) { return $components['user'].$authority; } return $components['user'].':'.$components['pass'].$authority; } /** * Parse a URI string into its components. * * This method parses a URI and returns an associative array containing any * of the various components of the URI that are present. * * <code> * $components = UriString::parse('http://foo@test.example.com:42?query#'); * var_export($components); * //will display * array( * 'scheme' => 'http', // the URI scheme component * 'user' => 'foo', // the URI user component * 'pass' => null, // the URI pass component * 'host' => 'test.example.com', // the URI host component * 'port' => 42, // the URI port component * 'path' => '', // the URI path component * 'query' => 'query', // the URI query component * 'fragment' => '', // the URI fragment component * ); * </code> * * The returned array is similar to PHP's parse_url return value with the following * differences: * * <ul> * <li>All components are always present in the returned array</li> * <li>Empty and undefined component are treated differently. And empty component is * set to the empty string while an undefined component is set to the `null` value.</li> * <li>The path component is never undefined</li> * <li>The method parses the URI following the RFC3986 rules, but you are still * required to validate the returned components against its related scheme specific rules.</li> * </ul> * * @link https://tools.ietf.org/html/rfc3986 * * @throws SyntaxError if the URI contains invalid characters * @throws SyntaxError if the URI contains an invalid scheme * @throws SyntaxError if the URI contains an invalid path * * @return ComponentMap */ public static function parse(Stringable|string|int $uri): array { $uri = (string) $uri; if (isset(self::URI_SCHORTCUTS[$uri])) { /** @var ComponentMap $components */ $components = array_merge(self::URI_COMPONENTS, self::URI_SCHORTCUTS[$uri]); return $components; } if (1 === preg_match(self::REGEXP_INVALID_URI_CHARS, $uri)) { throw new SyntaxError(sprintf('The uri `%s` contains invalid characters', $uri)); } //if the first character is a known URI delimiter parsing can be simplified $first_char = $uri[0]; //The URI is made of the fragment only if ('#' === $first_char) { [, $fragment] = explode('#', $uri, 2); $components = self::URI_COMPONENTS; $components['fragment'] = $fragment; return $components; } //The URI is made of the query and fragment if ('?' === $first_char) { [, $partial] = explode('?', $uri, 2); [$query, $fragment] = explode('#', $partial, 2) + [1 => null]; $components = self::URI_COMPONENTS; $components['query'] = $query; $components['fragment'] = $fragment; return $components; } //use RFC3986 URI regexp to split the URI preg_match(self::REGEXP_URI_PARTS, $uri, $parts); $parts += ['query' => '', 'fragment' => '']; if (':' === $parts['scheme'] || 1 !== preg_match(self::REGEXP_URI_SCHEME, $parts['scontent'])) { throw new SyntaxError(sprintf('The uri `%s` contains an invalid scheme', $uri)); } if ('' === $parts['scheme'].$parts['authority'] && 1 === preg_match(self::REGEXP_INVALID_PATH, $parts['path'])) { throw new SyntaxError(sprintf('The uri `%s` contains an invalid path.', $uri)); } /** @var ComponentMap $components */ $components = array_merge( self::URI_COMPONENTS, '' === $parts['authority'] ? [] : self::parseAuthority($parts['acontent']), [ 'path' => $parts['path'], 'scheme' => '' === $parts['scheme'] ? null : $parts['scontent'], 'query' => '' === $parts['query'] ? null : $parts['qcontent'], 'fragment' => '' === $parts['fragment'] ? null : $parts['fcontent'], ] ); return $components; } /** * Parses the URI authority part. * * @link https://tools.ietf.org/html/rfc3986#section-3.2 * * @throws SyntaxError If the port component is invalid * * @return AuthorityMap */ public static function parseAuthority(?string $authority): array { $components = ['user' => null, 'pass' => null, 'host' => null, 'port' => null]; if (null === $authority) { return $components; } $components['host'] = ''; if ('' === $authority) { return $components; } $parts = explode('@', $authority, 2); if (isset($parts[1])) { [$components['user'], $components['pass']] = explode(':', $parts[0], 2) + [1 => null]; } preg_match(self::REGEXP_HOST_PORT, $parts[1] ?? $parts[0], $matches); $matches += ['port' => '']; $components['port'] = self::filterPort($matches['port']); $components['host'] = self::filterHost($matches['host']); return $components; } /** * Filter and format the port component. * * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 * * @throws SyntaxError if the registered name is invalid */ private static function filterPort(string $port): ?int { return match (true) { '' === $port => null, 1 === preg_match('/^\d*$/', $port) => (int) $port, default => throw new SyntaxError(sprintf('The port `%s` is invalid', $port)), }; } /** * Returns whether a hostname is valid. * * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 * * @throws SyntaxError if the registered name is invalid */ private static function filterHost(string $host): string { return match (true) { '' === $host => '', '[' !== $host[0] || !str_ends_with($host, ']') => self::filterRegisteredName($host), !self::isIpHost(substr($host, 1, -1)) => throw new SyntaxError(sprintf('Host `%s` is invalid : the IP host is malformed', $host)), default => $host, }; } /** * Returns whether the host is an IPv4 or a registered named. * * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 * * @throws SyntaxError if the registered name is invalid * @throws MissingFeature if IDN support or ICU requirement are not available or met. * @throws ConversionFailed if the submitted IDN host cannot be converted to a valid ascii form */ private static function filterRegisteredName(string $host): string { $formattedHost = rawurldecode($host); if (1 === preg_match(self::REGEXP_REGISTERED_NAME, $formattedHost)) { return $host; } //to test IDN host non-ascii characters must be present in the host if (1 !== preg_match(self::REGEXP_IDN_PATTERN, $formattedHost)) { throw new SyntaxError(sprintf('Host `%s` is invalid: the host is not a valid registered name', $host)); } Converter::toAsciiOrFail($host); return $host; } /** * Validates a IPv6/IPfuture host. * * @link https://tools.ietf.org/html/rfc3986#section-3.2.2 * @link https://tools.ietf.org/html/rfc6874#section-2 * @link https://tools.ietf.org/html/rfc6874#section-4 */ private static function isIpHost(string $ipHost): bool { if (false !== filter_var($ipHost, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { return true; } if (1 === preg_match(self::REGEXP_IP_FUTURE, $ipHost, $matches)) { return !in_array($matches['version'], ['4', '6'], true); } $pos = strpos($ipHost, '%'); if (false === $pos || 1 === preg_match(self::REGEXP_INVALID_HOST_CHARS, rawurldecode(substr($ipHost, $pos)))) { return false; } $ipHost = substr($ipHost, 0, $pos); return false !== filter_var($ipHost, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && str_starts_with((string)inet_pton($ipHost), self::ZONE_ID_ADDRESS_BLOCK); } } uri-interfaces/Contracts/UriAccess.php 0000644 00000000754 15007532371 0014027 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Contracts; use Psr\Http\Message\UriInterface as Psr7UriInterface; interface UriAccess { public function getUri(): UriInterface|Psr7UriInterface; public function getUriString(): string; } uri-interfaces/Contracts/IpHostInterface.php 0000644 00000002273 15007532371 0015173 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Contracts; interface IpHostInterface extends HostInterface { /** * Tells whether the host is an IPv4 address. */ public function isIpv4(): bool; /** * Tells whether the host is an IPv6 address. */ public function isIpv6(): bool; /** * Tells whether the host is an IPv6 address. */ public function isIpFuture(): bool; /** * Tells whether the host has a ZoneIdentifier. * * @see http://tools.ietf.org/html/rfc6874#section-4 */ public function hasZoneIdentifier(): bool; /** * Returns a host without its zone identifier according to RFC6874. * * This method MUST retain the state of the current instance, and return * an instance without the host zone identifier according to RFC6874 * * @see http://tools.ietf.org/html/rfc6874#section-4 */ public function withoutZoneIdentifier(): self; } uri-interfaces/Contracts/SegmentedPathInterface.php 0000644 00000010615 15007532371 0016514 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Contracts; use Countable; use Iterator; use IteratorAggregate; use League\Uri\Exceptions\SyntaxError; use Stringable; /** * @extends IteratorAggregate<string> */ interface SegmentedPathInterface extends Countable, IteratorAggregate, PathInterface { /** * Returns the total number of segments in the path. */ public function count(): int; /** * Iterate over the path segment. * * @return Iterator<string> */ public function getIterator(): Iterator; /** * Returns parent directory's path. */ public function getDirname(): string; /** * Returns the path basename. */ public function getBasename(): string; /** * Returns the basename extension. */ public function getExtension(): string; /** * Retrieves a single path segment. * * If the segment offset has not been set, returns null. */ public function get(int $offset): ?string; /** * Returns the associated key for a specific segment. * * If a value is specified only the keys associated with * the given value will be returned * * @return array<int> */ public function keys(Stringable|string|null $segment = null): array; /** * Appends a segment to the path. */ public function append(Stringable|string $segment): self; /** * Extracts a slice of $length elements starting at position $offset from the host. * * This method MUST retain the state of the current instance, and return * an instance that contains the selected slice. * * If $length is null it returns all elements from $offset to the end of the Path. */ public function slice(int $offset, ?int $length = null): self; /** * Prepends a segment to the path. */ public function prepend(Stringable|string $segment): self; /** * Returns an instance with the modified segment. * * This method MUST retain the state of the current instance, and return * an instance that contains the new segment * * If $key is non-negative, the added segment will be the segment at $key position from the start. * If $key is negative, the added segment will be the segment at $key position from the end. * * @throws SyntaxError If the key is invalid */ public function withSegment(int $key, Stringable|string $segment): self; /** * Returns an instance without the specified segment. * * This method MUST retain the state of the current instance, and return * an instance that contains the modified component * * If $key is non-negative, the removed segment will be the segment at $key position from the start. * If $key is negative, the removed segment will be the segment at $key position from the end. * * @throws SyntaxError If the key is invalid */ public function withoutSegment(int ...$keys): self; /** * Returns an instance without duplicate delimiters. * * This method MUST retain the state of the current instance, and return * an instance that contains the path component normalized by removing * multiple consecutive empty segment */ public function withoutEmptySegments(): self; /** * Returns an instance with the specified parent directory's path. * * This method MUST retain the state of the current instance, and return * an instance that contains the extension basename modified. */ public function withDirname(Stringable|string $path): self; /** * Returns an instance with the specified basename. * * This method MUST retain the state of the current instance, and return * an instance that contains the extension basename modified. */ public function withBasename(Stringable|string $basename): self; /** * Returns an instance with the specified basename extension. * * This method MUST retain the state of the current instance, and return * an instance that contains the extension basename modified. */ public function withExtension(Stringable|string $extension): self; } uri-interfaces/Contracts/UriException.php 0000644 00000000557 15007532371 0014565 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Contracts; use Throwable; interface UriException extends Throwable { } uri-interfaces/Contracts/PortInterface.php 0000644 00000000726 15007532371 0014712 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Contracts; interface PortInterface extends UriComponentInterface { /** * Returns the integer representation of the Port. */ public function toInt(): ?int; } uri-interfaces/Contracts/UserInfoInterface.php 0000644 00000003412 15007532371 0015513 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Contracts; use Stringable; interface UserInfoInterface extends UriComponentInterface { /** * Returns the user component part. */ public function getUser(): ?string; /** * Returns the pass component part. */ public function getPass(): ?string; /** * Returns an associative array containing all the User Info components. * * The returned a hashmap similar to PHP's parse_url return value * * @link https://tools.ietf.org/html/rfc3986 * * @return array{user: ?string, pass : ?string} */ public function components(): array; /** * Returns an instance with the specified user and/or pass. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified new username * otherwise it returns the same instance unchanged. * * A variable equal to null is equivalent to removing the complete user information. */ public function withUser(Stringable|string|null $username): self; /** * Returns an instance with the specified user and/or pass. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified password if the user is specified * otherwise it returns the same instance unchanged. * * An empty user is equivalent to removing the user information. */ public function withPass(Stringable|string|null $password): self; } uri-interfaces/Contracts/DomainHostInterface.php 0000644 00000006314 15007532371 0016032 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Contracts; use Countable; use Iterator; use IteratorAggregate; use League\Uri\Exceptions\SyntaxError; use Stringable; /** * @extends IteratorAggregate<string> */ interface DomainHostInterface extends Countable, HostInterface, IteratorAggregate { /** * Returns the labels total number. */ public function count(): int; /** * Iterate over the Domain labels. * * @return Iterator<string> */ public function getIterator(): Iterator; /** * Retrieves a single host label. * * If the label offset has not been set, returns the null value. */ public function get(int $offset): ?string; /** * Returns the associated key for a specific label or all the keys. * * @return int[] */ public function keys(?string $label = null): array; /** * Tells whether the domain is absolute. */ public function isAbsolute(): bool; /** * Prepends a label to the host. */ public function prepend(Stringable|string $label): self; /** * Appends a label to the host. */ public function append(Stringable|string $label): self; /** * Extracts a slice of $length elements starting at position $offset from the host. * * This method MUST retain the state of the current instance, and return * an instance that contains the selected slice. * * If $length is null it returns all elements from $offset to the end of the Domain. */ public function slice(int $offset, ?int $length = null): self; /** * Returns an instance with its Root label. * * @see https://tools.ietf.org/html/rfc3986#section-3.2.2 */ public function withRootLabel(): self; /** * Returns an instance without its Root label. * * @see https://tools.ietf.org/html/rfc3986#section-3.2.2 */ public function withoutRootLabel(): self; /** * Returns an instance with the modified label. * * This method MUST retain the state of the current instance, and return * an instance that contains the new label * * If $key is non-negative, the added label will be the label at $key position from the start. * If $key is negative, the added label will be the label at $key position from the end. * * @throws SyntaxError If the key is invalid */ public function withLabel(int $key, Stringable|string $label): self; /** * Returns an instance without the specified label. * * This method MUST retain the state of the current instance, and return * an instance that contains the modified component * * If $key is non-negative, the removed label will be the label at $key position from the start. * If $key is negative, the removed label will be the label at $key position from the end. * * @throws SyntaxError If the key is invalid */ public function withoutLabel(int ...$keys): self; } uri-interfaces/Contracts/QueryInterface.php 0000644 00000021106 15007532371 0015066 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Contracts; use Countable; use Iterator; use IteratorAggregate; use Stringable; /** * @extends IteratorAggregate<array{0:string, 1:string|null}> * * @method self withoutPairByKey(string ...$keys) Returns an instance without pairs with the specified keys. * @method self withoutPairByValue(Stringable|string|int|bool|null ...$values) Returns an instance without pairs with the specified values. * @method self withoutPairByKeyValue(string $key, Stringable|string|int|bool|null $value) Returns an instance without pairs with the specified key/value pair * @method bool hasPair(string $key, ?string $value) Tells whether the pair exists in the query. * @method ?string toFormData() Returns the string representation using the applicat/www-form-urlencoded rules * @method ?string toRFC3986() Returns the string representation using RFC3986 rules */ interface QueryInterface extends Countable, IteratorAggregate, UriComponentInterface { /** * Returns the query separator. * * @return non-empty-string */ public function getSeparator(): string; /** * Returns the number of key/value pairs present in the object. */ public function count(): int; /** * Returns an iterator allowing to go through all key/value pairs contained in this object. * * The pair is represented as an array where the first value is the pair key * and the second value the pair value. * * The key of each pair is a string * The value of each pair is a scalar or the null value * * @return Iterator<int, array{0:string, 1:string|null}> */ public function getIterator(): Iterator; /** * Returns an iterator allowing to go through all key/value pairs contained in this object. * * The return type is as an Iterator where its offset is the pair key and its value the pair value. * * The key of each pair is a string * The value of each pair is a scalar or the null value * * @return iterable<string, string|null> */ public function pairs(): iterable; /** * Tells whether a list of pair with a specific key exists. * * @see https://url.spec.whatwg.org/#dom-urlsearchparams-has */ public function has(string ...$keys): bool; /** * Returns the first value associated to the given pair name. * * If no value is found null is returned * * @see https://url.spec.whatwg.org/#dom-urlsearchparams-get */ public function get(string $key): ?string; /** * Returns all the values associated to the given pair name as an array or all * the instance pairs. * * If no value is found an empty array is returned * * @see https://url.spec.whatwg.org/#dom-urlsearchparams-getall * * @return array<int, string|null> */ public function getAll(string $key): array; /** * Returns the store PHP variables as elements of an array. * * The result is similar as PHP parse_str when used with its * second argument with the difference that variable names are * not mangled. * * @see http://php.net/parse_str * @see https://wiki.php.net/rfc/on_demand_name_mangling * * @return array the collection of stored PHP variables or the empty array if no input is given, */ public function parameters(): array; /** * Returns the value attached to the specific key. * * The result is similar to PHP parse_str with the difference that variable * names are not mangled. * * If a key is submitted it will return the value attached to it or null * * @see http://php.net/parse_str * @see https://wiki.php.net/rfc/on_demand_name_mangling * * @return mixed the collection of stored PHP variables or the empty array if no input is given, * the single value of a stored PHP variable or null if the variable is not present in the collection */ public function parameter(string $name): mixed; /** * Tells whether a list of variable with specific names exists. * * @see https://url.spec.whatwg.org/#dom-urlsearchparams-has */ public function hasParameter(string ...$names): bool; /** * Returns the RFC1738 encoded query. */ public function toRFC1738(): ?string; /** * Returns an instance with a different separator. * * This method MUST retain the state of the current instance, and return * an instance that contains the query component with a different separator */ public function withSeparator(string $separator): self; /** * Returns an instance with the new pairs set to it. * * This method MUST retain the state of the current instance, and return * an instance that contains the modified query * * @see ::withPair */ public function merge(Stringable|string $query): self; /** * Returns an instance with the new pairs appended to it. * * This method MUST retain the state of the current instance, and return * an instance that contains the modified query * * If the pair already exists the value will be added to it. */ public function append(Stringable|string $query): self; /** * Returns a new instance with a specified key/value pair appended as a new pair. * * This method MUST retain the state of the current instance, and return * an instance that contains the modified query */ public function appendTo(string $key, Stringable|string|int|bool|null $value): self; /** * Sorts the query string by offset, maintaining offset to data correlations. * * This method MUST retain the state of the current instance, and return * an instance that contains the modified query * * @see https://url.spec.whatwg.org/#dom-urlsearchparams-sort */ public function sort(): self; /** * Returns an instance without duplicate key/value pair. * * This method MUST retain the state of the current instance, and return * an instance that contains the query component normalized by removing * duplicate pairs whose key/value are the same. */ public function withoutDuplicates(): self; /** * Returns an instance without empty key/value where the value is the null value. * * This method MUST retain the state of the current instance, and return * an instance that contains the query component normalized by removing * empty pairs. * * A pair is considered empty if its value is equal to the null value */ public function withoutEmptyPairs(): self; /** * Returns an instance where numeric indices associated to PHP's array like key are removed. * * This method MUST retain the state of the current instance, and return * an instance that contains the query component normalized so that numeric indexes * are removed from the pair key value. * * i.e.: toto[3]=bar[3]&foo=bar becomes toto[]=bar[3]&foo=bar */ public function withoutNumericIndices(): self; /** * Returns an instance with a new key/value pair added to it. * * This method MUST retain the state of the current instance, and return * an instance that contains the modified query * * If the pair already exists the value will replace the existing value. * * @see https://url.spec.whatwg.org/#dom-urlsearchparams-set */ public function withPair(string $key, Stringable|string|int|float|bool|null $value): self; /** * DEPRECATION WARNING! This method will be removed in the next major point release. * * @deprecated Since version 7.2.0 * @codeCoverageIgnore * @see Modifier::removeQueryPairsByKey() * * Returns an instance without the specified keys. * * This method MUST retain the state of the current instance, and return * an instance that contains the modified component */ public function withoutPair(string ...$keys): self; /** * Returns an instance without the specified params. * * This method MUST retain the state of the current instance, and return * an instance that contains the modified component without PHP's value. * PHP's mangled is not taken into account. */ public function withoutParameters(string ...$names): self; } uri-interfaces/Contracts/DataPathInterface.php 0000644 00000005307 15007532371 0015454 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Contracts; use SplFileObject; use Stringable; interface DataPathInterface extends PathInterface { /** * Retrieve the data mime type associated to the URI. * * If no mimetype is present, this method MUST return the default mimetype 'text/plain'. * * @see http://tools.ietf.org/html/rfc2397#section-2 */ public function getMimeType(): string; /** * Retrieve the parameters associated with the Mime Type of the URI. * * If no parameters is present, this method MUST return the default parameter 'charset=US-ASCII'. * * @see http://tools.ietf.org/html/rfc2397#section-2 */ public function getParameters(): string; /** * Retrieve the mediatype associated with the URI. * * If no mediatype is present, this method MUST return the default parameter 'text/plain;charset=US-ASCII'. * * @see http://tools.ietf.org/html/rfc2397#section-3 * * @return string The URI scheme. */ public function getMediaType(): string; /** * Retrieves the data string. * * Retrieves the data part of the path. If no data part is provided return * an empty string */ public function getData(): string; /** * Tells whether the data is binary safe encoded. */ public function isBinaryData(): bool; /** * Save the data to a specific file. */ public function save(string $path, string $mode = 'w'): SplFileObject; /** * Returns an instance where the data part is base64 encoded. * * This method MUST retain the state of the current instance, and return * an instance where the data part is base64 encoded */ public function toBinary(): self; /** * Returns an instance where the data part is url encoded following RFC3986 rules. * * This method MUST retain the state of the current instance, and return * an instance where the data part is url encoded */ public function toAscii(): self; /** * Return an instance with the specified mediatype parameters. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified mediatype parameters. * * Users must provide encoded characters. * * An empty parameters value is equivalent to removing the parameter. */ public function withParameters(Stringable|string $parameters): self; } uri-interfaces/Contracts/UriInterface.php 0000644 00000026064 15007532371 0014530 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Contracts; use JsonSerializable; use League\Uri\Exceptions\MissingFeature; use League\Uri\Exceptions\SyntaxError; use League\Uri\UriString; use Stringable; /** * @phpstan-import-type ComponentMap from UriString */ interface UriInterface extends JsonSerializable, Stringable { /** * Returns the string representation as a URI reference. * * @see http://tools.ietf.org/html/rfc3986#section-4.1 */ public function __toString(): string; /** * Returns the string representation as a URI reference. * * @see http://tools.ietf.org/html/rfc3986#section-4.1 */ public function toString(): string; /** * Returns the string representation as a URI reference. * * @see http://tools.ietf.org/html/rfc3986#section-4.1 * @see ::__toString */ public function jsonSerialize(): string; /** * Retrieve the scheme component of the URI. * * If no scheme is present, this method MUST return a null value. * * The value returned MUST be normalized to lowercase, per RFC 3986 * Section 3.1. * * The trailing ":" character is not part of the scheme and MUST NOT be * added. * * @see https://tools.ietf.org/html/rfc3986#section-3.1 */ public function getScheme(): ?string; /** * Retrieve the authority component of the URI. * * If no scheme is present, this method MUST return a null value. * * If the port component is not set or is the standard port for the current * scheme, it SHOULD NOT be included. * * @see https://tools.ietf.org/html/rfc3986#section-3.2 */ public function getAuthority(): ?string; /** * Retrieve the user information component of the URI. * * If no scheme is present, this method MUST return a null value. * * If a user is present in the URI, this will return that value; * additionally, if the password is also present, it will be appended to the * user value, with a colon (":") separating the values. * * The trailing "@" character is not part of the user information and MUST * NOT be added. */ public function getUserInfo(): ?string; /** * Retrieve the host component of the URI. * * If no host is present this method MUST return a null value. * * The value returned MUST be normalized to lowercase, per RFC 3986 * Section 3.2.2. * * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 */ public function getHost(): ?string; /** * Retrieve the port component of the URI. * * If a port is present, and it is non-standard for the current scheme, * this method MUST return it as an integer. If the port is the standard port * used with the current scheme, this method SHOULD return null. * * If no port is present, and no scheme is present, this method MUST return * a null value. * * If no port is present, but a scheme is present, this method MAY return * the standard port for that scheme, but SHOULD return null. */ public function getPort(): ?int; /** * Retrieve the path component of the URI. * * The path can either be empty or absolute (starting with a slash) or * rootless (not starting with a slash). Implementations MUST support all * three syntaxes. * * Normally, the empty path "" and absolute path "/" are considered equal as * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically * do this normalization because in contexts with a trimmed base path, e.g. * the front controller, this difference becomes significant. It's the task * of the user to handle both "" and "/". * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.3. * * As an example, if the value should include a slash ("/") not intended as * delimiter between path segments, that value MUST be passed in encoded * form (e.g., "%2F") to the instance. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.3 */ public function getPath(): string; /** * Retrieve the query string of the URI. * * If no host is present this method MUST return a null value. * * The leading "?" character is not part of the query and MUST NOT be * added. * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.4. * * As an example, if a value in a key/value pair of the query string should * include an ampersand ("&") not intended as a delimiter between values, * that value MUST be passed in encoded form (e.g., "%26") to the instance. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.4 */ public function getQuery(): ?string; /** * Retrieve the fragment component of the URI. * * If no host is present this method MUST return a null value. * * The leading "#" character is not part of the fragment and MUST NOT be * added. * * The value returned MUST be percent-encoded, but MUST NOT double-encode * any characters. To determine what characters to encode, please refer to * RFC 3986, Sections 2 and 3.5. * * @see https://tools.ietf.org/html/rfc3986#section-2 * @see https://tools.ietf.org/html/rfc3986#section-3.5 */ public function getFragment(): ?string; /** * Returns an associative array containing all the URI components. * * The returned array is similar to PHP's parse_url return value with the following * differences: * * <ul> * <li>All components are present in the returned array</li> * <li>Empty and undefined component are treated differently. And empty component is * set to the empty string while an undefined component is set to the `null` value.</li> * </ul> * * @link https://tools.ietf.org/html/rfc3986 * * @return ComponentMap */ public function getComponents(): array; /** * Return an instance with the specified scheme. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified scheme. * * A null value provided for the scheme is equivalent to removing the scheme * information. * * @throws SyntaxError for invalid component or transformations * that would result in an object in invalid state. */ public function withScheme(Stringable|string|null $scheme): self; /** * Return an instance with the specified user information. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified user information. * * Password is optional, but the user information MUST include the * user; a null value for the user is equivalent to removing user * information. * * @throws SyntaxError for invalid component or transformations * that would result in an object in invalid state. */ public function withUserInfo(Stringable|string|null $user, Stringable|string|null $password = null): self; /** * Return an instance with the specified host. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified host. * * A null value provided for the host is equivalent to removing the host * information. * * @throws SyntaxError for invalid component or transformations * that would result in an object in invalid state. * @throws MissingFeature for component or transformations * requiring IDN support when IDN support is not present * or misconfigured. */ public function withHost(Stringable|string|null $host): self; /** * Return an instance with the specified port. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified port. * * A null value provided for the port is equivalent to removing the port * information. * * @throws SyntaxError for invalid component or transformations * that would result in an object in invalid state. */ public function withPort(?int $port): self; /** * Return an instance with the specified path. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified path. * * The path can either be empty or absolute (starting with a slash) or * rootless (not starting with a slash). Implementations MUST support all * three syntaxes. * * Users can provide both encoded and decoded path characters. * Implementations ensure the correct encoding as outlined in getPath(). * * @throws SyntaxError for invalid component or transformations * that would result in an object in invalid state. */ public function withPath(Stringable|string $path): self; /** * Return an instance with the specified query string. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified query string. * * Users can provide both encoded and decoded query characters. * Implementations ensure the correct encoding as outlined in getQuery(). * * A null value provided for the query is equivalent to removing the query * information. * * @throws SyntaxError for invalid component or transformations * that would result in an object in invalid state. */ public function withQuery(Stringable|string|null $query): self; /** * Return an instance with the specified URI fragment. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified URI fragment. * * Users can provide both encoded and decoded fragment characters. * Implementations ensure the correct encoding as outlined in getFragment(). * * A null value provided for the fragment is equivalent to removing the fragment * information. * * @throws SyntaxError for invalid component or transformations * that would result in an object in invalid state. */ public function withFragment(Stringable|string|null $fragment): self; } uri-interfaces/Contracts/AuthorityInterface.php 0000644 00000005750 15007532371 0015760 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Contracts; use League\Uri\Exceptions\MissingFeature; use League\Uri\Exceptions\SyntaxError; use Stringable; interface AuthorityInterface extends UriComponentInterface { /** * Returns the host component of the authority. */ public function getHost(): ?string; /** * Returns the port component of the authority. */ public function getPort(): ?int; /** * Returns the user information component of the authority. */ public function getUserInfo(): ?string; /** * Returns an associative array containing all the Authority components. * * The returned a hashmap similar to PHP's parse_url return value * * @link https://tools.ietf.org/html/rfc3986 * * @return array{user: ?string, pass : ?string, host: ?string, port: ?int} */ public function components(): array; /** * Return an instance with the specified host. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified host. * * A null value provided for the host is equivalent to removing the host * information. * * @throws SyntaxError for invalid component or transformations * that would result in an object in invalid state. * @throws MissingFeature for component or transformations * requiring IDN support when IDN support is not present * or misconfigured. */ public function withHost(Stringable|string|null $host): self; /** * Return an instance with the specified port. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified port. * * A null value provided for the port is equivalent to removing the port * information. * * @throws SyntaxError for invalid component or transformations * that would result in an object in invalid state. */ public function withPort(?int $port): self; /** * Return an instance with the specified user information. * * This method MUST retain the state of the current instance, and return * an instance that contains the specified user information. * * Password is optional, but the user information MUST include the * user; a null value for the user is equivalent to removing user * information. * * @throws SyntaxError for invalid component or transformations * that would result in an object in invalid state. */ public function withUserInfo(Stringable|string|null $user, Stringable|string|null $password = null): self; } uri-interfaces/Contracts/HostInterface.php 0000644 00000002316 15007532371 0014700 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Contracts; interface HostInterface extends UriComponentInterface { /** * Returns the ascii representation. */ public function toAscii(): ?string; /** * Returns the unicode representation. */ public function toUnicode(): ?string; /** * Returns the IP version. * * If the host is a not an IP this method will return null */ public function getIpVersion(): ?string; /** * Returns the IP component If the Host is an IP address. * * If the host is a not an IP this method will return null */ public function getIp(): ?string; /** * Tells whether the host is a domain name. */ public function isDomain(): bool; /** * Tells whether the host is an IP Address. */ public function isIp(): bool; /** * Tells whether the host is a registered name. */ public function isRegisteredName(): bool; } uri-interfaces/Contracts/PathInterface.php 0000644 00000005462 15007532371 0014664 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Contracts; use League\Uri\Exceptions\SyntaxError; interface PathInterface extends UriComponentInterface { /** * Returns the decoded path. */ public function decoded(): string; /** * Tells whether the path is absolute or relative. */ public function isAbsolute(): bool; /** * Tells whether the path has a trailing slash. */ public function hasTrailingSlash(): bool; /** * Returns an instance without dot segments. * * This method MUST retain the state of the current instance, and return * an instance that contains the path component normalized by removing * the dot segment. * * @throws SyntaxError for invalid component or transformations * that would result in a object in invalid state. */ public function withoutDotSegments(): self; /** * Returns an instance with a leading slash. * * This method MUST retain the state of the current instance, and return * an instance that contains the path component with a leading slash * * @throws SyntaxError for invalid component or transformations * that would result in a object in invalid state. */ public function withLeadingSlash(): self; /** * Returns an instance without a leading slash. * * This method MUST retain the state of the current instance, and return * an instance that contains the path component without a leading slash * * @throws SyntaxError for invalid component or transformations * that would result in a object in invalid state. */ public function withoutLeadingSlash(): self; /** * Returns an instance with a trailing slash. * * This method MUST retain the state of the current instance, and return * an instance that contains the path component with a trailing slash * * @throws SyntaxError for invalid component or transformations * that would result in a object in invalid state. */ public function withTrailingSlash(): self; /** * Returns an instance without a trailing slash. * * This method MUST retain the state of the current instance, and return * an instance that contains the path component without a trailing slash * * @throws SyntaxError for invalid component or transformations * that would result in a object in invalid state. */ public function withoutTrailingSlash(): self; } uri-interfaces/Contracts/UriComponentInterface.php 0000644 00000004615 15007532371 0016411 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Contracts; use JsonSerializable; use Stringable; interface UriComponentInterface extends JsonSerializable, Stringable { /** * Returns the instance string representation. * * If the instance is defined, the value returned MUST be percent-encoded, * but MUST NOT double-encode any characters. To determine what characters * to encode, please refer to RFC 3986, Sections 2 and 3. * * If the instance is not defined null is returned */ public function value(): ?string; /** * Returns the instance string representation. * * If the instance is defined, the value returned MUST be percent-encoded, * but MUST NOT double-encode any characters. To determine what characters * to encode, please refer to RFC 3986, Sections 2 and 3. * * If the instance is not defined an empty string is returned */ public function toString(): string; /** * Returns the instance string representation. * * If the instance is defined, the value returned MUST be percent-encoded, * but MUST NOT double-encode any characters. To determine what characters * to encode, please refer to RFC 3986, Sections 2 and 3. * * If the instance is not defined an empty string is returned */ public function __toString(): string; /** * Returns the instance json representation. * * If the instance is defined, the value returned MUST be percent-encoded, * but MUST NOT double-encode any characters. To determine what characters * to encode, please refer to RFC 3986 or RFC 1738. * * If the instance is not defined null is returned */ public function jsonSerialize(): ?string; /** * Returns the instance string representation with its optional URI delimiters. * * The value returned MUST be percent-encoded, but MUST NOT double-encode any * characters. To determine what characters to encode, please refer to RFC 3986, * Sections 2 and 3. * * If the instance is not defined an empty string is returned */ public function getUriComponent(): string; } uri-interfaces/Contracts/FragmentInterface.php 0000644 00000000715 15007532371 0015527 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Contracts; interface FragmentInterface extends UriComponentInterface { /** * Returns the decoded fragment. */ public function decoded(): ?string; } uri-interfaces/Idna/Option.php 0000644 00000010621 15007532371 0012323 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Idna; use ReflectionClass; use ReflectionClassConstant; /** * @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uidna_8h.html */ final class Option { private const DEFAULT = 0; private const ALLOW_UNASSIGNED = 1; private const USE_STD3_RULES = 2; private const CHECK_BIDI = 4; private const CHECK_CONTEXTJ = 8; private const NONTRANSITIONAL_TO_ASCII = 0x10; private const NONTRANSITIONAL_TO_UNICODE = 0x20; private const CHECK_CONTEXTO = 0x40; private function __construct(private readonly int $value) { } private static function cases(): array { static $assoc; if (null === $assoc) { $assoc = []; $fooClass = new ReflectionClass(self::class); foreach ($fooClass->getConstants(ReflectionClassConstant::IS_PRIVATE) as $name => $value) { $assoc[$name] = $value; } } return $assoc; } public static function new(int $bytes = self::DEFAULT): self { return new self(array_reduce( self::cases(), fn (int $value, int $option) => 0 !== ($option & $bytes) ? ($value | $option) : $value, self::DEFAULT )); } public static function forIDNA2008Ascii(): self { return self::new() ->nonTransitionalToAscii() ->checkBidi() ->useSTD3Rules() ->checkContextJ(); } public static function forIDNA2008Unicode(): self { return self::new() ->nonTransitionalToUnicode() ->checkBidi() ->useSTD3Rules() ->checkContextJ(); } public function toBytes(): int { return $this->value; } /** array<string, int> */ public function list(): array { return array_keys(array_filter( self::cases(), fn (int $value) => 0 !== ($value & $this->value) )); } public function allowUnassigned(): self { return $this->add(self::ALLOW_UNASSIGNED); } public function disallowUnassigned(): self { return $this->remove(self::ALLOW_UNASSIGNED); } public function useSTD3Rules(): self { return $this->add(self::USE_STD3_RULES); } public function prohibitSTD3Rules(): self { return $this->remove(self::USE_STD3_RULES); } public function checkBidi(): self { return $this->add(self::CHECK_BIDI); } public function ignoreBidi(): self { return $this->remove(self::CHECK_BIDI); } public function checkContextJ(): self { return $this->add(self::CHECK_CONTEXTJ); } public function ignoreContextJ(): self { return $this->remove(self::CHECK_CONTEXTJ); } public function checkContextO(): self { return $this->add(self::CHECK_CONTEXTO); } public function ignoreContextO(): self { return $this->remove(self::CHECK_CONTEXTO); } public function nonTransitionalToAscii(): self { return $this->add(self::NONTRANSITIONAL_TO_ASCII); } public function transitionalToAscii(): self { return $this->remove(self::NONTRANSITIONAL_TO_ASCII); } public function nonTransitionalToUnicode(): self { return $this->add(self::NONTRANSITIONAL_TO_UNICODE); } public function transitionalToUnicode(): self { return $this->remove(self::NONTRANSITIONAL_TO_UNICODE); } public function add(Option|int|null $option = null): self { return match (true) { null === $option => $this, $option instanceof self => self::new($this->value | $option->value), default => self::new($this->value | $option), }; } public function remove(Option|int|null $option = null): self { return match (true) { null === $option => $this, $option instanceof self => self::new($this->value & ~$option->value), default => self::new($this->value & ~$option), }; } } uri-interfaces/Idna/Converter.php 0000644 00000015747 15007532371 0013040 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Idna; use League\Uri\Exceptions\ConversionFailed; use League\Uri\Exceptions\SyntaxError; use League\Uri\FeatureDetection; use Stringable; use function idn_to_ascii; use function idn_to_utf8; use function rawurldecode; use const INTL_IDNA_VARIANT_UTS46; /** * @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uidna_8h.html */ final class Converter { private const REGEXP_IDNA_PATTERN = '/[^\x20-\x7f]/'; private const MAX_DOMAIN_LENGTH = 253; private const MAX_LABEL_LENGTH = 63; /** * General registered name regular expression. * * @see https://tools.ietf.org/html/rfc3986#section-3.2.2 * @see https://regex101.com/r/fptU8V/1 */ private const REGEXP_REGISTERED_NAME = '/ (?(DEFINE) (?<unreserved>[a-z0-9_~\-]) # . is missing as it is used to separate labels (?<sub_delims>[!$&\'()*+,;=]) (?<encoded>%[A-F0-9]{2}) (?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded))*) ) ^(?:(?®_name)\.)*(?®_name)\.?$ /ix'; /** * Converts the input to its IDNA ASCII form or throw on failure. * * @see Converter::toAscii() * * @throws SyntaxError if the string cannot be converted to UNICODE using IDN UTS46 algorithm * @throws ConversionFailed if the conversion returns error */ public static function toAsciiOrFail(Stringable|string $domain, Option|int|null $options = null): string { $result = self::toAscii($domain, $options); return match (true) { $result->hasErrors() => throw ConversionFailed::dueToIdnError($domain, $result), default => $result->domain(), }; } /** * Converts the input to its IDNA ASCII form. * * This method returns the string converted to IDN ASCII form * * @throws SyntaxError if the string cannot be converted to ASCII using IDN UTS46 algorithm */ public static function toAscii(Stringable|string $domain, Option|int|null $options = null): Result { $domain = rawurldecode((string) $domain); if (1 === preg_match(self::REGEXP_IDNA_PATTERN, $domain)) { FeatureDetection::supportsIdn(); $flags = match (true) { null === $options => Option::forIDNA2008Ascii(), $options instanceof Option => $options, default => Option::new($options), }; idn_to_ascii($domain, $flags->toBytes(), INTL_IDNA_VARIANT_UTS46, $idnaInfo); if ([] === $idnaInfo) { return Result::fromIntl([ 'result' => strtolower($domain), 'isTransitionalDifferent' => false, 'errors' => self::validateDomainAndLabelLength($domain), ]); } return Result::fromIntl($idnaInfo); } $error = Error::NONE->value; if (1 !== preg_match(self::REGEXP_REGISTERED_NAME, $domain)) { $error |= Error::DISALLOWED->value; } return Result::fromIntl([ 'result' => strtolower($domain), 'isTransitionalDifferent' => false, 'errors' => self::validateDomainAndLabelLength($domain) | $error, ]); } /** * Converts the input to its IDNA UNICODE form or throw on failure. * * @see Converter::toUnicode() * * @throws ConversionFailed if the conversion returns error */ public static function toUnicodeOrFail(Stringable|string $domain, Option|int|null $options = null): string { $result = self::toUnicode($domain, $options); return match (true) { $result->hasErrors() => throw ConversionFailed::dueToIdnError($domain, $result), default => $result->domain(), }; } /** * Converts the input to its IDNA UNICODE form. * * This method returns the string converted to IDN UNICODE form * * @throws SyntaxError if the string cannot be converted to UNICODE using IDN UTS46 algorithm */ public static function toUnicode(Stringable|string $domain, Option|int|null $options = null): Result { $domain = rawurldecode((string) $domain); if (false === stripos($domain, 'xn--')) { return Result::fromIntl(['result' => $domain, 'isTransitionalDifferent' => false, 'errors' => Error::NONE->value]); } FeatureDetection::supportsIdn(); $flags = match (true) { null === $options => Option::forIDNA2008Unicode(), $options instanceof Option => $options, default => Option::new($options), }; idn_to_utf8($domain, $flags->toBytes(), INTL_IDNA_VARIANT_UTS46, $idnaInfo); if ([] === $idnaInfo) { return Result::fromIntl(['result' => $domain, 'isTransitionalDifferent' => false, 'errors' => Error::NONE->value]); } return Result::fromIntl($idnaInfo); } /** * Tells whether the submitted host is a valid IDN regardless of its format. * * Returns false if the host is invalid or if its conversion yield the same result */ public static function isIdn(Stringable|string|null $domain): bool { $domain = strtolower(rawurldecode((string) $domain)); $result = match (1) { preg_match(self::REGEXP_IDNA_PATTERN, $domain) => self::toAscii($domain), default => self::toUnicode($domain), }; return match (true) { $result->hasErrors() => false, default => $result->domain() !== $domain, }; } /** * Adapted from https://github.com/TRowbotham/idna. * * @see https://github.com/TRowbotham/idna/blob/master/src/Idna.php#L236 */ private static function validateDomainAndLabelLength(string $domain): int { $error = Error::NONE->value; $labels = explode('.', $domain); $maxDomainSize = self::MAX_DOMAIN_LENGTH; $length = count($labels); // If the last label is empty, and it is not the first label, then it is the root label. // Increase the max size by 1, making it 254, to account for the root label's "." // delimiter. This also means we don't need to check the last label's length for being too // long. if ($length > 1 && '' === $labels[$length - 1]) { ++$maxDomainSize; array_pop($labels); } if (strlen($domain) > $maxDomainSize) { $error |= Error::DOMAIN_NAME_TOO_LONG->value; } foreach ($labels as $label) { if (strlen($label) > self::MAX_LABEL_LENGTH) { $error |= Error::LABEL_TOO_LONG->value; break; } } return $error; } } uri-interfaces/Idna/Result.php 0000644 00000002643 15007532371 0012336 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri\Idna; /** * @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uidna_8h.html */ final class Result { private function __construct( private readonly string $domain, private readonly bool $isTransitionalDifferent, /** @var array<Error> */ private readonly array $errors ) { } /** * @param array{result:string, isTransitionalDifferent:bool, errors:int} $infos */ public static function fromIntl(array $infos): self { return new self($infos['result'], $infos['isTransitionalDifferent'], Error::filterByErrorBytes($infos['errors'])); } public function domain(): string { return $this->domain; } public function isTransitionalDifferent(): bool { return $this->isTransitionalDifferent; } /** * @return array<Error> */ public function errors(): array { return $this->errors; } public function hasErrors(): bool { return [] !== $this->errors; } public function hasError(Error $error): bool { return in_array($error, $this->errors, true); } } uri-interfaces/Idna/Error.php 0000644 00000005450 15007532371 0012150 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\Uri\Idna; enum Error: int { case NONE = 0; case EMPTY_LABEL = 1; case LABEL_TOO_LONG = 2; case DOMAIN_NAME_TOO_LONG = 4; case LEADING_HYPHEN = 8; case TRAILING_HYPHEN = 0x10; case HYPHEN_3_4 = 0x20; case LEADING_COMBINING_MARK = 0x40; case DISALLOWED = 0x80; case PUNYCODE = 0x100; case LABEL_HAS_DOT = 0x200; case INVALID_ACE_LABEL = 0x400; case BIDI = 0x800; case CONTEXTJ = 0x1000; case CONTEXTO_PUNCTUATION = 0x2000; case CONTEXTO_DIGITS = 0x4000; public function description(): string { return match ($this) { self::NONE => 'No error has occurred', self::EMPTY_LABEL => 'a non-final domain name label (or the whole domain name) is empty', self::LABEL_TOO_LONG => 'a domain name label is longer than 63 bytes', self::DOMAIN_NAME_TOO_LONG => 'a domain name is longer than 255 bytes in its storage form', self::LEADING_HYPHEN => 'a label starts with a hyphen-minus ("-")', self::TRAILING_HYPHEN => 'a label ends with a hyphen-minus ("-")', self::HYPHEN_3_4 => 'a label contains hyphen-minus ("-") in the third and fourth positions', self::LEADING_COMBINING_MARK => 'a label starts with a combining mark', self::DISALLOWED => 'a label or domain name contains disallowed characters', self::PUNYCODE => 'a label starts with "xn--" but does not contain valid Punycode', self::LABEL_HAS_DOT => 'a label contains a dot=full stop', self::INVALID_ACE_LABEL => 'An ACE label does not contain a valid label string', self::BIDI => 'a label does not meet the IDNA BiDi requirements (for right-to-left characters)', self::CONTEXTJ => 'a label does not meet the IDNA CONTEXTJ requirements', self::CONTEXTO_DIGITS => 'a label does not meet the IDNA CONTEXTO requirements for digits', self::CONTEXTO_PUNCTUATION => 'a label does not meet the IDNA CONTEXTO requirements for punctuation characters. Some punctuation characters "Would otherwise have been DISALLOWED" but are allowed in certain contexts', }; } public static function filterByErrorBytes(int $errors): array { return array_values( array_filter( self::cases(), fn (self $error): bool => 0 !== ($error->value & $errors) ) ); } } uri-interfaces/FeatureDetection.php 0000644 00000003403 15007532371 0013432 0 ustar 00 <?php /** * League.Uri (https://uri.thephpleague.com) * * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ declare(strict_types=1); namespace League\Uri; use finfo; use League\Uri\Exceptions\MissingFeature; use League\Uri\IPv4\Calculator; use const PHP_INT_SIZE; /** * Allow detecting features needed to make the packages work. */ final class FeatureDetection { public static function supportsFileDetection(): void { static $isSupported = null; $isSupported = $isSupported ?? class_exists(finfo::class); if (!$isSupported) { throw new MissingFeature('Support for file type detection requires the `fileinfo` extension.'); } } public static function supportsIdn(): void { static $isSupported = null; $isSupported = $isSupported ?? (function_exists('\idn_to_ascii') && defined('\INTL_IDNA_VARIANT_UTS46')); if (!$isSupported) { throw new MissingFeature('Support for IDN host requires the `intl` extension for best performance or run "composer require symfony/polyfill-intl-idn" to install a polyfill.'); } } public static function supportsIPv4Conversion(): void { static $isSupported = null; $isSupported = $isSupported ?? (extension_loaded('gmp') || extension_loaded('bcmath') || (4 < PHP_INT_SIZE)); if (!$isSupported) { throw new MissingFeature('A '.Calculator::class.' implementation could not be automatically loaded. To perform IPv4 conversion use a x.64 PHP build or install one of the following extension GMP or BCMath. You can also ship your own implmentation.'); } } } flysystem-local/composer.json 0000644 00000001172 15007532371 0012421 0 ustar 00 { "name": "league/flysystem-local", "description": "Local filesystem adapter for Flysystem.", "keywords": ["flysystem", "filesystem", "local", "file", "files"], "type": "library", "prefer-stable": true, "autoload": { "psr-4": { "League\\Flysystem\\Local\\": "" } }, "require": { "php": "^8.0.2", "ext-fileinfo": "*", "league/flysystem": "^3.0.0", "league/mime-type-detection": "^1.0.0" }, "license": "MIT", "authors": [ { "name": "Frank de Jonge", "email": "info@frankdejonge.nl" } ] } flysystem-local/LICENSE 0000644 00000002047 15007532371 0010706 0 ustar 00 Copyright (c) 2013-2024 Frank de Jonge 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. flysystem-local/FallbackMimeTypeDetector.php 0000644 00000002700 15007532371 0015251 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem\Local; use League\MimeTypeDetection\MimeTypeDetector; use function in_array; class FallbackMimeTypeDetector implements MimeTypeDetector { private const INCONCLUSIVE_MIME_TYPES = [ 'application/x-empty', 'text/plain', 'text/x-asm', 'application/octet-stream', 'inode/x-empty', ]; public function __construct( private MimeTypeDetector $detector, private array $inconclusiveMimetypes = self::INCONCLUSIVE_MIME_TYPES, private bool $useInconclusiveMimeTypeFallback = false, ) { } public function detectMimeType(string $path, $contents): ?string { return $this->detector->detectMimeType($path, $contents); } public function detectMimeTypeFromBuffer(string $contents): ?string { return $this->detector->detectMimeTypeFromBuffer($contents); } public function detectMimeTypeFromPath(string $path): ?string { return $this->detector->detectMimeTypeFromPath($path); } public function detectMimeTypeFromFile(string $path): ?string { $mimeType = $this->detector->detectMimeTypeFromFile($path); if ($mimeType !== null && ! in_array($mimeType, $this->inconclusiveMimetypes)) { return $mimeType; } return $this->detector->detectMimeTypeFromPath($path) ?? ($this->useInconclusiveMimeTypeFallback ? $mimeType : null); } } flysystem-local/LocalFilesystemAdapter.php 0000644 00000036101 15007532371 0015010 0 ustar 00 <?php declare(strict_types=1); namespace League\Flysystem\Local; use const DIRECTORY_SEPARATOR; use const LOCK_EX; use DirectoryIterator; use FilesystemIterator; use Generator; use League\Flysystem\ChecksumProvider; use League\Flysystem\Config; use League\Flysystem\DirectoryAttributes; use League\Flysystem\FileAttributes; use League\Flysystem\FilesystemAdapter; use League\Flysystem\PathPrefixer; use League\Flysystem\SymbolicLinkEncountered; use League\Flysystem\UnableToCopyFile; use League\Flysystem\UnableToCreateDirectory; use League\Flysystem\UnableToDeleteDirectory; use League\Flysystem\UnableToDeleteFile; use League\Flysystem\UnableToMoveFile; use League\Flysystem\UnableToProvideChecksum; use League\Flysystem\UnableToReadFile; use League\Flysystem\UnableToRetrieveMetadata; use League\Flysystem\UnableToSetVisibility; use League\Flysystem\UnableToWriteFile; use League\Flysystem\UnixVisibility\PortableVisibilityConverter; use League\Flysystem\UnixVisibility\VisibilityConverter; use League\MimeTypeDetection\FinfoMimeTypeDetector; use League\MimeTypeDetection\MimeTypeDetector; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use SplFileInfo; use Throwable; use function chmod; use function clearstatcache; use function dirname; use function error_clear_last; use function error_get_last; use function file_exists; use function file_put_contents; use function hash_file; use function is_dir; use function is_file; use function mkdir; use function rename; class LocalFilesystemAdapter implements FilesystemAdapter, ChecksumProvider { /** * @var int */ public const SKIP_LINKS = 0001; /** * @var int */ public const DISALLOW_LINKS = 0002; private PathPrefixer $prefixer; private VisibilityConverter $visibility; private MimeTypeDetector $mimeTypeDetector; private string $rootLocation; /** * @var bool */ private $rootLocationIsSetup = false; public function __construct( string $location, ?VisibilityConverter $visibility = null, private int $writeFlags = LOCK_EX, private int $linkHandling = self::DISALLOW_LINKS, ?MimeTypeDetector $mimeTypeDetector = null, bool $lazyRootCreation = false, bool $useInconclusiveMimeTypeFallback = false, ) { $this->prefixer = new PathPrefixer($location, DIRECTORY_SEPARATOR); $visibility ??= new PortableVisibilityConverter(); $this->visibility = $visibility; $this->rootLocation = $location; $this->mimeTypeDetector = $mimeTypeDetector ?? new FallbackMimeTypeDetector( detector: new FinfoMimeTypeDetector(), useInconclusiveMimeTypeFallback: $useInconclusiveMimeTypeFallback, ); if ( ! $lazyRootCreation) { $this->ensureRootDirectoryExists(); } } private function ensureRootDirectoryExists(): void { if ($this->rootLocationIsSetup) { return; } $this->ensureDirectoryExists($this->rootLocation, $this->visibility->defaultForDirectories()); $this->rootLocationIsSetup = true; } public function write(string $path, string $contents, Config $config): void { $this->writeToFile($path, $contents, $config); } public function writeStream(string $path, $contents, Config $config): void { $this->writeToFile($path, $contents, $config); } /** * @param resource|string $contents */ private function writeToFile(string $path, $contents, Config $config): void { $prefixedLocation = $this->prefixer->prefixPath($path); $this->ensureRootDirectoryExists(); $this->ensureDirectoryExists( dirname($prefixedLocation), $this->resolveDirectoryVisibility($config->get(Config::OPTION_DIRECTORY_VISIBILITY)) ); error_clear_last(); if (@file_put_contents($prefixedLocation, $contents, $this->writeFlags) === false) { throw UnableToWriteFile::atLocation($path, error_get_last()['message'] ?? ''); } if ($visibility = $config->get(Config::OPTION_VISIBILITY)) { $this->setVisibility($path, (string) $visibility); } } public function delete(string $path): void { $location = $this->prefixer->prefixPath($path); if ( ! file_exists($location)) { return; } error_clear_last(); if ( ! @unlink($location)) { throw UnableToDeleteFile::atLocation($location, error_get_last()['message'] ?? ''); } } public function deleteDirectory(string $prefix): void { $location = $this->prefixer->prefixPath($prefix); if ( ! is_dir($location)) { return; } $contents = $this->listDirectoryRecursively($location, RecursiveIteratorIterator::CHILD_FIRST); /** @var SplFileInfo $file */ foreach ($contents as $file) { if ( ! $this->deleteFileInfoObject($file)) { throw UnableToDeleteDirectory::atLocation($prefix, "Unable to delete file at " . $file->getPathname()); } } unset($contents); if ( ! @rmdir($location)) { throw UnableToDeleteDirectory::atLocation($prefix, error_get_last()['message'] ?? ''); } } private function listDirectoryRecursively( string $path, int $mode = RecursiveIteratorIterator::SELF_FIRST ): Generator { if ( ! is_dir($path)) { return; } yield from new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), $mode ); } protected function deleteFileInfoObject(SplFileInfo $file): bool { switch ($file->getType()) { case 'dir': return @rmdir((string) $file->getRealPath()); case 'link': return @unlink((string) $file->getPathname()); default: return @unlink((string) $file->getRealPath()); } } public function listContents(string $path, bool $deep): iterable { $location = $this->prefixer->prefixPath($path); if ( ! is_dir($location)) { return; } /** @var SplFileInfo[] $iterator */ $iterator = $deep ? $this->listDirectoryRecursively($location) : $this->listDirectory($location); foreach ($iterator as $fileInfo) { $pathName = $fileInfo->getPathname(); try { if ($fileInfo->isLink()) { if ($this->linkHandling & self::SKIP_LINKS) { continue; } throw SymbolicLinkEncountered::atLocation($pathName); } $path = $this->prefixer->stripPrefix($pathName); $lastModified = $fileInfo->getMTime(); $isDirectory = $fileInfo->isDir(); $permissions = octdec(substr(sprintf('%o', $fileInfo->getPerms()), -4)); $visibility = $isDirectory ? $this->visibility->inverseForDirectory($permissions) : $this->visibility->inverseForFile($permissions); yield $isDirectory ? new DirectoryAttributes(str_replace('\\', '/', $path), $visibility, $lastModified) : new FileAttributes( str_replace('\\', '/', $path), $fileInfo->getSize(), $visibility, $lastModified ); } catch (Throwable $exception) { if (file_exists($pathName)) { throw $exception; } } } } public function move(string $source, string $destination, Config $config): void { $sourcePath = $this->prefixer->prefixPath($source); $destinationPath = $this->prefixer->prefixPath($destination); $this->ensureRootDirectoryExists(); $this->ensureDirectoryExists( dirname($destinationPath), $this->resolveDirectoryVisibility($config->get(Config::OPTION_DIRECTORY_VISIBILITY)) ); if ( ! @rename($sourcePath, $destinationPath)) { throw UnableToMoveFile::because(error_get_last()['message'] ?? 'unknown reason', $source, $destination); } if ($visibility = $config->get(Config::OPTION_VISIBILITY)) { $this->setVisibility($destination, (string) $visibility); } } public function copy(string $source, string $destination, Config $config): void { $sourcePath = $this->prefixer->prefixPath($source); $destinationPath = $this->prefixer->prefixPath($destination); $this->ensureRootDirectoryExists(); $this->ensureDirectoryExists( dirname($destinationPath), $this->resolveDirectoryVisibility($config->get(Config::OPTION_DIRECTORY_VISIBILITY)) ); if ( ! @copy($sourcePath, $destinationPath)) { throw UnableToCopyFile::because(error_get_last()['message'] ?? 'unknown', $source, $destination); } $visibility = $config->get( Config::OPTION_VISIBILITY, $config->get(Config::OPTION_RETAIN_VISIBILITY, true) ? $this->visibility($source)->visibility() : null, ); if ($visibility) { $this->setVisibility($destination, (string) $visibility); } } public function read(string $path): string { $location = $this->prefixer->prefixPath($path); error_clear_last(); $contents = @file_get_contents($location); if ($contents === false) { throw UnableToReadFile::fromLocation($path, error_get_last()['message'] ?? ''); } return $contents; } public function readStream(string $path) { $location = $this->prefixer->prefixPath($path); error_clear_last(); $contents = @fopen($location, 'rb'); if ($contents === false) { throw UnableToReadFile::fromLocation($path, error_get_last()['message'] ?? ''); } return $contents; } protected function ensureDirectoryExists(string $dirname, int $visibility): void { if (is_dir($dirname)) { return; } error_clear_last(); if ( ! @mkdir($dirname, $visibility, true)) { $mkdirError = error_get_last(); } clearstatcache(true, $dirname); if ( ! is_dir($dirname)) { $errorMessage = isset($mkdirError['message']) ? $mkdirError['message'] : ''; throw UnableToCreateDirectory::atLocation($dirname, $errorMessage); } } public function fileExists(string $location): bool { $location = $this->prefixer->prefixPath($location); return is_file($location); } public function directoryExists(string $location): bool { $location = $this->prefixer->prefixPath($location); return is_dir($location); } public function createDirectory(string $path, Config $config): void { $this->ensureRootDirectoryExists(); $location = $this->prefixer->prefixPath($path); $visibility = $config->get(Config::OPTION_VISIBILITY, $config->get(Config::OPTION_DIRECTORY_VISIBILITY)); $permissions = $this->resolveDirectoryVisibility($visibility); if (is_dir($location)) { $this->setPermissions($location, $permissions); return; } error_clear_last(); if ( ! @mkdir($location, $permissions, true)) { throw UnableToCreateDirectory::atLocation($path, error_get_last()['message'] ?? ''); } } public function setVisibility(string $path, string $visibility): void { $path = $this->prefixer->prefixPath($path); $visibility = is_dir($path) ? $this->visibility->forDirectory($visibility) : $this->visibility->forFile( $visibility ); $this->setPermissions($path, $visibility); } public function visibility(string $path): FileAttributes { $location = $this->prefixer->prefixPath($path); clearstatcache(false, $location); error_clear_last(); $fileperms = @fileperms($location); if ($fileperms === false) { throw UnableToRetrieveMetadata::visibility($path, error_get_last()['message'] ?? ''); } $permissions = $fileperms & 0777; $visibility = $this->visibility->inverseForFile($permissions); return new FileAttributes($path, null, $visibility); } private function resolveDirectoryVisibility(?string $visibility): int { return $visibility === null ? $this->visibility->defaultForDirectories() : $this->visibility->forDirectory( $visibility ); } public function mimeType(string $path): FileAttributes { $location = $this->prefixer->prefixPath($path); error_clear_last(); if ( ! is_file($location)) { throw UnableToRetrieveMetadata::mimeType($location, 'No such file exists.'); } $mimeType = $this->mimeTypeDetector->detectMimeTypeFromFile($location); if ($mimeType === null) { throw UnableToRetrieveMetadata::mimeType($path, error_get_last()['message'] ?? ''); } return new FileAttributes($path, null, null, null, $mimeType); } public function lastModified(string $path): FileAttributes { $location = $this->prefixer->prefixPath($path); error_clear_last(); $lastModified = @filemtime($location); if ($lastModified === false) { throw UnableToRetrieveMetadata::lastModified($path, error_get_last()['message'] ?? ''); } return new FileAttributes($path, null, null, $lastModified); } public function fileSize(string $path): FileAttributes { $location = $this->prefixer->prefixPath($path); error_clear_last(); if (is_file($location) && ($fileSize = @filesize($location)) !== false) { return new FileAttributes($path, $fileSize); } throw UnableToRetrieveMetadata::fileSize($path, error_get_last()['message'] ?? ''); } public function checksum(string $path, Config $config): string { $algo = $config->get('checksum_algo', 'md5'); $location = $this->prefixer->prefixPath($path); error_clear_last(); $checksum = @hash_file($algo, $location); if ($checksum === false) { throw new UnableToProvideChecksum(error_get_last()['message'] ?? '', $path); } return $checksum; } private function listDirectory(string $location): Generator { $iterator = new DirectoryIterator($location); foreach ($iterator as $item) { if ($item->isDot()) { continue; } yield $item; } } private function setPermissions(string $location, int $visibility): void { error_clear_last(); if ( ! @chmod($location, $visibility)) { $extraMessage = error_get_last()['message'] ?? ''; throw UnableToSetVisibility::atLocation($this->prefixer->stripPrefix($location), $extraMessage); } } } oauth2-server/composer.json 0000644 00000004100 15007532371 0011772 0 ustar 00 { "name": "league/oauth2-server", "description": "A lightweight and powerful OAuth 2.0 authorization and resource server library with support for all the core specification grants. This library will allow you to secure your API with OAuth and allow your applications users to approve apps that want to access their data from your API.", "homepage": "https://oauth2.thephpleague.com/", "license": "MIT", "require": { "php": "^8.0", "ext-openssl": "*", "league/event": "^2.2", "league/uri": "^6.7 || ^7.0", "lcobucci/jwt": "^4.3 || ^5.0", "psr/http-message": "^1.0.1 || ^2.0", "defuse/php-encryption": "^2.3", "lcobucci/clock": "^2.2 || ^3.0" }, "require-dev": { "phpunit/phpunit": "^9.6.6", "laminas/laminas-diactoros": "^3.0.0", "phpstan/phpstan": "^0.12.57", "phpstan/phpstan-phpunit": "^0.12.16", "roave/security-advisories": "dev-master" }, "repositories": [ { "type": "git", "url": "https://github.com/thephpleague/oauth2-server.git" } ], "keywords": [ "oauth", "oauth2", "oauth 2", "oauth 2.0", "server", "auth", "authorization", "authorisation", "authentication", "resource", "api", "auth", "protect", "secure" ], "authors": [ { "name": "Alex Bilbie", "email": "hello@alexbilbie.com", "homepage": "http://www.alexbilbie.com", "role": "Developer" }, { "name": "Andy Millington", "email": "andrew@noexceptions.io", "homepage": "https://www.noexceptions.io", "role": "Developer" } ], "replace": { "lncd/oauth2": "*", "league/oauth2server": "*" }, "autoload": { "psr-4": { "League\\OAuth2\\Server\\": "src/" } }, "autoload-dev": { "psr-4": { "LeagueTests\\": "tests/" } } } oauth2-server/LICENSE 0000644 00000002046 15007532371 0010264 0 ustar 00 MIT License Copyright (C) Alex Bilbie 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. oauth2-server/src/Repositories/AccessTokenRepositoryInterface.php 0000644 00000003170 15007532371 0021370 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Repositories; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; /** * Access token interface. */ interface AccessTokenRepositoryInterface extends RepositoryInterface { /** * Create a new access token * * @param ClientEntityInterface $clientEntity * @param ScopeEntityInterface[] $scopes * @param mixed $userIdentifier * * @return AccessTokenEntityInterface */ public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null); /** * Persists a new access token to permanent storage. * * @param AccessTokenEntityInterface $accessTokenEntity * * @throws UniqueTokenIdentifierConstraintViolationException */ public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity); /** * Revoke an access token. * * @param string $tokenId */ public function revokeAccessToken($tokenId); /** * Check if the access token has been revoked. * * @param string $tokenId * * @return bool Return true if this token has been revoked */ public function isAccessTokenRevoked($tokenId); } oauth2-server/src/Repositories/RepositoryInterface.php 0000644 00000000506 15007532371 0017245 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Repositories; /** * Repository interface. */ interface RepositoryInterface { } oauth2-server/src/Repositories/ScopeRepositoryInterface.php 0000644 00000002434 15007532371 0020241 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Repositories; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; /** * Scope interface. */ interface ScopeRepositoryInterface extends RepositoryInterface { /** * Return information about a scope. * * @param string $identifier The scope identifier * * @return ScopeEntityInterface|null */ public function getScopeEntityByIdentifier($identifier); /** * Given a client, grant type and optional user identifier validate the set of scopes requested are valid and optionally * append additional scopes or remove requested scopes. * * @param ScopeEntityInterface[] $scopes * @param string $grantType * @param ClientEntityInterface $clientEntity * @param null|string $userIdentifier * * @return ScopeEntityInterface[] */ public function finalizeScopes( array $scopes, $grantType, ClientEntityInterface $clientEntity, $userIdentifier = null ); } oauth2-server/src/Repositories/ClientRepositoryInterface.php 0000644 00000002014 15007532371 0020400 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Repositories; use League\OAuth2\Server\Entities\ClientEntityInterface; /** * Client storage interface. */ interface ClientRepositoryInterface extends RepositoryInterface { /** * Get a client. * * @param string $clientIdentifier The client's identifier * * @return ClientEntityInterface|null */ public function getClientEntity($clientIdentifier); /** * Validate a client's secret. * * @param string $clientIdentifier The client's identifier * @param null|string $clientSecret The client's secret (if sent) * @param null|string $grantType The type of grant the client is using (if sent) * * @return bool */ public function validateClient($clientIdentifier, $clientSecret, $grantType); } oauth2-server/src/Repositories/UserRepositoryInterface.php 0000644 00000001613 15007532371 0020104 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Repositories; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\UserEntityInterface; interface UserRepositoryInterface extends RepositoryInterface { /** * Get a user entity. * * @param string $username * @param string $password * @param string $grantType The grant type used * @param ClientEntityInterface $clientEntity * * @return UserEntityInterface|null */ public function getUserEntityByUserCredentials( $username, $password, $grantType, ClientEntityInterface $clientEntity ); } oauth2-server/src/Repositories/RefreshTokenRepositoryInterface.php 0000644 00000002454 15007532371 0021571 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Repositories; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; /** * Refresh token interface. */ interface RefreshTokenRepositoryInterface extends RepositoryInterface { /** * Creates a new refresh token * * @return RefreshTokenEntityInterface|null */ public function getNewRefreshToken(); /** * Create a new refresh token_name. * * @param RefreshTokenEntityInterface $refreshTokenEntity * * @throws UniqueTokenIdentifierConstraintViolationException */ public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity); /** * Revoke the refresh token. * * @param string $tokenId */ public function revokeRefreshToken($tokenId); /** * Check if the refresh token has been revoked. * * @param string $tokenId * * @return bool Return true if this token has been revoked */ public function isRefreshTokenRevoked($tokenId); } oauth2-server/src/Repositories/AuthCodeRepositoryInterface.php 0000644 00000002372 15007532371 0020665 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Repositories; use League\OAuth2\Server\Entities\AuthCodeEntityInterface; use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; /** * Auth code storage interface. */ interface AuthCodeRepositoryInterface extends RepositoryInterface { /** * Creates a new AuthCode * * @return AuthCodeEntityInterface */ public function getNewAuthCode(); /** * Persists a new auth code to permanent storage. * * @param AuthCodeEntityInterface $authCodeEntity * * @throws UniqueTokenIdentifierConstraintViolationException */ public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity); /** * Revoke an auth code. * * @param string $codeId */ public function revokeAuthCode($codeId); /** * Check if the auth code has been revoked. * * @param string $codeId * * @return bool Return true if this code has been revoked */ public function isAuthCodeRevoked($codeId); } oauth2-server/src/Exception/OAuthServerException.php 0000644 00000026757 15007532371 0016622 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Exception; use Exception; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Throwable; class OAuthServerException extends Exception { /** * @var int */ private $httpStatusCode; /** * @var string */ private $errorType; /** * @var null|string */ private $hint; /** * @var null|string */ private $redirectUri; /** * @var array */ private $payload; /** * @var ServerRequestInterface */ private $serverRequest; /** * Throw a new exception. * * @param string $message Error message * @param int $code Error code * @param string $errorType Error type * @param int $httpStatusCode HTTP status code to send (default = 400) * @param null|string $hint A helper hint * @param null|string $redirectUri A HTTP URI to redirect the user back to * @param Throwable $previous Previous exception */ public function __construct($message, $code, $errorType, $httpStatusCode = 400, $hint = null, $redirectUri = null, Throwable $previous = null) { parent::__construct($message, $code, $previous); $this->httpStatusCode = $httpStatusCode; $this->errorType = $errorType; $this->hint = $hint; $this->redirectUri = $redirectUri; $this->payload = [ 'error' => $errorType, 'error_description' => $message, ]; if ($hint !== null) { $this->payload['hint'] = $hint; } } /** * Returns the current payload. * * @return array */ public function getPayload() { $payload = $this->payload; // The "message" property is deprecated and replaced by "error_description" // TODO: remove "message" property if (isset($payload['error_description']) && !isset($payload['message'])) { $payload['message'] = $payload['error_description']; } return $payload; } /** * Updates the current payload. * * @param array $payload */ public function setPayload(array $payload) { $this->payload = $payload; } /** * Set the server request that is responsible for generating the exception * * @param ServerRequestInterface $serverRequest */ public function setServerRequest(ServerRequestInterface $serverRequest) { $this->serverRequest = $serverRequest; } /** * Unsupported grant type error. * * @return static */ public static function unsupportedGrantType() { $errorMessage = 'The authorization grant type is not supported by the authorization server.'; $hint = 'Check that all required parameters have been provided'; return new static($errorMessage, 2, 'unsupported_grant_type', 400, $hint); } /** * Invalid request error. * * @param string $parameter The invalid parameter * @param null|string $hint * @param Throwable $previous Previous exception * * @return static */ public static function invalidRequest($parameter, $hint = null, Throwable $previous = null) { $errorMessage = 'The request is missing a required parameter, includes an invalid parameter value, ' . 'includes a parameter more than once, or is otherwise malformed.'; $hint = ($hint === null) ? \sprintf('Check the `%s` parameter', $parameter) : $hint; return new static($errorMessage, 3, 'invalid_request', 400, $hint, null, $previous); } /** * Invalid client error. * * @param ServerRequestInterface $serverRequest * * @return static */ public static function invalidClient(ServerRequestInterface $serverRequest) { $exception = new static('Client authentication failed', 4, 'invalid_client', 401); $exception->setServerRequest($serverRequest); return $exception; } /** * Invalid scope error. * * @param string $scope The bad scope * @param null|string $redirectUri A HTTP URI to redirect the user back to * * @return static */ public static function invalidScope($scope, $redirectUri = null) { $errorMessage = 'The requested scope is invalid, unknown, or malformed'; if (empty($scope)) { $hint = 'Specify a scope in the request or set a default scope'; } else { $hint = \sprintf( 'Check the `%s` scope', \htmlspecialchars($scope, ENT_QUOTES, 'UTF-8', false) ); } return new static($errorMessage, 5, 'invalid_scope', 400, $hint, $redirectUri); } /** * Invalid credentials error. * * @return static */ public static function invalidCredentials() { return new static('The user credentials were incorrect.', 6, 'invalid_grant', 400); } /** * Server error. * * @param string $hint * @param Throwable $previous * * @return static * * @codeCoverageIgnore */ public static function serverError($hint, Throwable $previous = null) { return new static( 'The authorization server encountered an unexpected condition which prevented it from fulfilling' . ' the request: ' . $hint, 7, 'server_error', 500, null, null, $previous ); } /** * Invalid refresh token. * * @param null|string $hint * @param Throwable $previous * * @return static */ public static function invalidRefreshToken($hint = null, Throwable $previous = null) { return new static('The refresh token is invalid.', 8, 'invalid_request', 401, $hint, null, $previous); } /** * Access denied. * * @param null|string $hint * @param null|string $redirectUri * @param Throwable $previous * * @return static */ public static function accessDenied($hint = null, $redirectUri = null, Throwable $previous = null) { return new static( 'The resource owner or authorization server denied the request.', 9, 'access_denied', 401, $hint, $redirectUri, $previous ); } /** * Invalid grant. * * @param string $hint * * @return static */ public static function invalidGrant($hint = '') { return new static( 'The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token ' . 'is invalid, expired, revoked, does not match the redirection URI used in the authorization request, ' . 'or was issued to another client.', 10, 'invalid_grant', 400, $hint ); } /** * @return string */ public function getErrorType() { return $this->errorType; } /** * Generate a HTTP response. * * @param ResponseInterface $response * @param bool $useFragment True if errors should be in the URI fragment instead of query string * @param int $jsonOptions options passed to json_encode * * @return ResponseInterface */ public function generateHttpResponse(ResponseInterface $response, $useFragment = false, $jsonOptions = 0) { $headers = $this->getHttpHeaders(); $payload = $this->getPayload(); if ($this->redirectUri !== null) { if ($useFragment === true) { $this->redirectUri .= (\strstr($this->redirectUri, '#') === false) ? '#' : '&'; } else { $this->redirectUri .= (\strstr($this->redirectUri, '?') === false) ? '?' : '&'; } return $response->withStatus(302)->withHeader('Location', $this->redirectUri . \http_build_query($payload)); } foreach ($headers as $header => $content) { $response = $response->withHeader($header, $content); } $responseBody = \json_encode($payload, $jsonOptions) ?: 'JSON encoding of payload failed'; $response->getBody()->write($responseBody); return $response->withStatus($this->getHttpStatusCode()); } /** * Get all headers that have to be send with the error response. * * @return array Array with header values */ public function getHttpHeaders() { $headers = [ 'Content-type' => 'application/json', ]; // Add "WWW-Authenticate" header // // RFC 6749, section 5.2.: // "If the client attempted to authenticate via the 'Authorization' // request header field, the authorization server MUST // respond with an HTTP 401 (Unauthorized) status code and // include the "WWW-Authenticate" response header field // matching the authentication scheme used by the client. if ($this->errorType === 'invalid_client' && $this->requestHasAuthorizationHeader()) { $authScheme = \strpos($this->serverRequest->getHeader('Authorization')[0], 'Bearer') === 0 ? 'Bearer' : 'Basic'; $headers['WWW-Authenticate'] = $authScheme . ' realm="OAuth"'; } return $headers; } /** * Check if the exception has an associated redirect URI. * * Returns whether the exception includes a redirect, since * getHttpStatusCode() doesn't return a 302 when there's a * redirect enabled. This helps when you want to override local * error pages but want to let redirects through. * * @return bool */ public function hasRedirect() { return $this->redirectUri !== null; } /** * Returns the Redirect URI used for redirecting. * * @return string|null */ public function getRedirectUri() { return $this->redirectUri; } /** * Returns the HTTP status code to send when the exceptions is output. * * @return int */ public function getHttpStatusCode() { return $this->httpStatusCode; } /** * @return null|string */ public function getHint() { return $this->hint; } /** * Check if the request has a non-empty 'Authorization' header value. * * Returns true if the header is present and not an empty string, false * otherwise. * * @return bool */ private function requestHasAuthorizationHeader() { if (!$this->serverRequest->hasHeader('Authorization')) { return false; } $authorizationHeader = $this->serverRequest->getHeader('Authorization'); // Common .htaccess configurations yield an empty string for the // 'Authorization' header when one is not provided by the client. // For practical purposes that case should be treated as though the // header isn't present. // See https://github.com/thephpleague/oauth2-server/issues/1162 if (empty($authorizationHeader) || empty($authorizationHeader[0])) { return false; } return true; } } oauth2-server/src/Exception/UniqueTokenIdentifierConstraintViolationException.php 0000644 00000001163 15007532371 0024577 0 ustar 00 <?php /** * @author Ivan Kurnosov <zerkms@zerkms.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Exception; class UniqueTokenIdentifierConstraintViolationException extends OAuthServerException { /** * @return UniqueTokenIdentifierConstraintViolationException */ public static function create() { $errorMessage = 'Could not create unique access token identifier'; return new static($errorMessage, 100, 'access_token_duplicate', 500); } } oauth2-server/src/RequestAccessTokenEvent.php 0000644 00000001733 15007532371 0015336 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use Psr\Http\Message\ServerRequestInterface; class RequestAccessTokenEvent extends RequestEvent { /** * @var AccessTokenEntityInterface */ private $accessToken; /** * @param string $name * @param ServerRequestInterface $request */ public function __construct($name, ServerRequestInterface $request, AccessTokenEntityInterface $accessToken) { parent::__construct($name, $request); $this->accessToken = $accessToken; } /** * @return AccessTokenEntityInterface * * @codeCoverageIgnore */ public function getAccessToken() { return $this->accessToken; } } oauth2-server/src/RequestEvent.php 0000644 00000002303 15007532371 0013205 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server; use League\Event\Event; use Psr\Http\Message\ServerRequestInterface; class RequestEvent extends Event { const CLIENT_AUTHENTICATION_FAILED = 'client.authentication.failed'; const USER_AUTHENTICATION_FAILED = 'user.authentication.failed'; const REFRESH_TOKEN_CLIENT_FAILED = 'refresh_token.client.failed'; const REFRESH_TOKEN_ISSUED = 'refresh_token.issued'; const ACCESS_TOKEN_ISSUED = 'access_token.issued'; /** * @var ServerRequestInterface */ private $request; /** * RequestEvent constructor. * * @param string $name * @param ServerRequestInterface $request */ public function __construct($name, ServerRequestInterface $request) { parent::__construct($name); $this->request = $request; } /** * @return ServerRequestInterface * * @codeCoverageIgnore */ public function getRequest() { return $this->request; } } oauth2-server/src/Grant/AbstractGrant.php 0000644 00000045000 15007532371 0014366 0 ustar 00 <?php /** * OAuth 2.0 Abstract grant. * * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Grant; use DateInterval; use DateTimeImmutable; use Error; use Exception; use League\Event\EmitterAwareTrait; use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\CryptTrait; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\AuthCodeEntityInterface; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; use League\OAuth2\Server\RedirectUriValidators\RedirectUriValidator; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; use League\OAuth2\Server\Repositories\UserRepositoryInterface; use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use LogicException; use Psr\Http\Message\ServerRequestInterface; use TypeError; /** * Abstract grant class. */ abstract class AbstractGrant implements GrantTypeInterface { use EmitterAwareTrait, CryptTrait; const SCOPE_DELIMITER_STRING = ' '; const MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS = 10; /** * @var ClientRepositoryInterface */ protected $clientRepository; /** * @var AccessTokenRepositoryInterface */ protected $accessTokenRepository; /** * @var ScopeRepositoryInterface */ protected $scopeRepository; /** * @var AuthCodeRepositoryInterface */ protected $authCodeRepository; /** * @var RefreshTokenRepositoryInterface */ protected $refreshTokenRepository; /** * @var UserRepositoryInterface */ protected $userRepository; /** * @var DateInterval */ protected $refreshTokenTTL; /** * @var CryptKey */ protected $privateKey; /** * @var string */ protected $defaultScope; /** * @var bool */ protected $revokeRefreshTokens; /** * @param ClientRepositoryInterface $clientRepository */ public function setClientRepository(ClientRepositoryInterface $clientRepository) { $this->clientRepository = $clientRepository; } /** * @param AccessTokenRepositoryInterface $accessTokenRepository */ public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository) { $this->accessTokenRepository = $accessTokenRepository; } /** * @param ScopeRepositoryInterface $scopeRepository */ public function setScopeRepository(ScopeRepositoryInterface $scopeRepository) { $this->scopeRepository = $scopeRepository; } /** * @param RefreshTokenRepositoryInterface $refreshTokenRepository */ public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository) { $this->refreshTokenRepository = $refreshTokenRepository; } /** * @param AuthCodeRepositoryInterface $authCodeRepository */ public function setAuthCodeRepository(AuthCodeRepositoryInterface $authCodeRepository) { $this->authCodeRepository = $authCodeRepository; } /** * @param UserRepositoryInterface $userRepository */ public function setUserRepository(UserRepositoryInterface $userRepository) { $this->userRepository = $userRepository; } /** * {@inheritdoc} */ public function setRefreshTokenTTL(DateInterval $refreshTokenTTL) { $this->refreshTokenTTL = $refreshTokenTTL; } /** * Set the private key * * @param CryptKey $key */ public function setPrivateKey(CryptKey $key) { $this->privateKey = $key; } /** * @param string $scope */ public function setDefaultScope($scope) { $this->defaultScope = $scope; } /** * @param bool $revokeRefreshTokens */ public function revokeRefreshTokens(bool $revokeRefreshTokens) { $this->revokeRefreshTokens = $revokeRefreshTokens; } /** * Validate the client. * * @param ServerRequestInterface $request * * @throws OAuthServerException * * @return ClientEntityInterface */ protected function validateClient(ServerRequestInterface $request) { [$clientId, $clientSecret] = $this->getClientCredentials($request); if ($this->clientRepository->validateClient($clientId, $clientSecret, $this->getIdentifier()) === false) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient($request); } $client = $this->getClientEntityOrFail($clientId, $request); // If a redirect URI is provided ensure it matches what is pre-registered $redirectUri = $this->getRequestParameter('redirect_uri', $request, null); if ($redirectUri !== null) { if (!\is_string($redirectUri)) { throw OAuthServerException::invalidRequest('redirect_uri'); } $this->validateRedirectUri($redirectUri, $client, $request); } return $client; } /** * Wrapper around ClientRepository::getClientEntity() that ensures we emit * an event and throw an exception if the repo doesn't return a client * entity. * * This is a bit of defensive coding because the interface contract * doesn't actually enforce non-null returns/exception-on-no-client so * getClientEntity might return null. By contrast, this method will * always either return a ClientEntityInterface or throw. * * @param string $clientId * @param ServerRequestInterface $request * * @return ClientEntityInterface */ protected function getClientEntityOrFail($clientId, ServerRequestInterface $request) { $client = $this->clientRepository->getClientEntity($clientId); if ($client instanceof ClientEntityInterface === false) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient($request); } return $client; } /** * Gets the client credentials from the request from the request body or * the Http Basic Authorization header * * @param ServerRequestInterface $request * * @return array */ protected function getClientCredentials(ServerRequestInterface $request) { [$basicAuthUser, $basicAuthPassword] = $this->getBasicAuthCredentials($request); $clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser); if (\is_null($clientId)) { throw OAuthServerException::invalidRequest('client_id'); } $clientSecret = $this->getRequestParameter('client_secret', $request, $basicAuthPassword); if ($clientSecret !== null && !\is_string($clientSecret)) { throw OAuthServerException::invalidRequest('client_secret'); } return [$clientId, $clientSecret]; } /** * Validate redirectUri from the request. * If a redirect URI is provided ensure it matches what is pre-registered * * @param string $redirectUri * @param ClientEntityInterface $client * @param ServerRequestInterface $request * * @throws OAuthServerException */ protected function validateRedirectUri( string $redirectUri, ClientEntityInterface $client, ServerRequestInterface $request ) { $validator = new RedirectUriValidator($client->getRedirectUri()); if (!$validator->validateRedirectUri($redirectUri)) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient($request); } } /** * Validate scopes in the request. * * @param string|array|null $scopes * @param string $redirectUri * * @throws OAuthServerException * * @return ScopeEntityInterface[] */ public function validateScopes($scopes, $redirectUri = null) { if ($scopes === null) { $scopes = []; } elseif (\is_string($scopes)) { $scopes = $this->convertScopesQueryStringToArray($scopes); } if (!\is_array($scopes)) { throw OAuthServerException::invalidRequest('scope'); } $validScopes = []; foreach ($scopes as $scopeItem) { $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem); if ($scope instanceof ScopeEntityInterface === false) { throw OAuthServerException::invalidScope($scopeItem, $redirectUri); } $validScopes[] = $scope; } return $validScopes; } /** * Converts a scopes query string to an array to easily iterate for validation. * * @param string $scopes * * @return array */ private function convertScopesQueryStringToArray(string $scopes) { return \array_filter(\explode(self::SCOPE_DELIMITER_STRING, \trim($scopes)), function ($scope) { return $scope !== ''; }); } /** * Retrieve request parameter. * * @param string $parameter * @param ServerRequestInterface $request * @param mixed $default * * @return null|string */ protected function getRequestParameter($parameter, ServerRequestInterface $request, $default = null) { $requestParameters = (array) $request->getParsedBody(); return $requestParameters[$parameter] ?? $default; } /** * Retrieve HTTP Basic Auth credentials with the Authorization header * of a request. First index of the returned array is the username, * second is the password (so list() will work). If the header does * not exist, or is otherwise an invalid HTTP Basic header, return * [null, null]. * * @param ServerRequestInterface $request * * @return string[]|null[] */ protected function getBasicAuthCredentials(ServerRequestInterface $request) { if (!$request->hasHeader('Authorization')) { return [null, null]; } $header = $request->getHeader('Authorization')[0]; if (\strpos($header, 'Basic ') !== 0) { return [null, null]; } if (!($decoded = \base64_decode(\substr($header, 6)))) { return [null, null]; } if (\strpos($decoded, ':') === false) { return [null, null]; // HTTP Basic header without colon isn't valid } return \explode(':', $decoded, 2); } /** * Retrieve query string parameter. * * @param string $parameter * @param ServerRequestInterface $request * @param mixed $default * * @return null|string */ protected function getQueryStringParameter($parameter, ServerRequestInterface $request, $default = null) { return isset($request->getQueryParams()[$parameter]) ? $request->getQueryParams()[$parameter] : $default; } /** * Retrieve cookie parameter. * * @param string $parameter * @param ServerRequestInterface $request * @param mixed $default * * @return null|string */ protected function getCookieParameter($parameter, ServerRequestInterface $request, $default = null) { return isset($request->getCookieParams()[$parameter]) ? $request->getCookieParams()[$parameter] : $default; } /** * Retrieve server parameter. * * @param string $parameter * @param ServerRequestInterface $request * @param mixed $default * * @return null|string */ protected function getServerParameter($parameter, ServerRequestInterface $request, $default = null) { return isset($request->getServerParams()[$parameter]) ? $request->getServerParams()[$parameter] : $default; } /** * Issue an access token. * * @param DateInterval $accessTokenTTL * @param ClientEntityInterface $client * @param string|null $userIdentifier * @param ScopeEntityInterface[] $scopes * * @throws OAuthServerException * @throws UniqueTokenIdentifierConstraintViolationException * * @return AccessTokenEntityInterface */ protected function issueAccessToken( DateInterval $accessTokenTTL, ClientEntityInterface $client, $userIdentifier, array $scopes = [] ) { $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS; $accessToken = $this->accessTokenRepository->getNewToken($client, $scopes, $userIdentifier); $accessToken->setExpiryDateTime((new DateTimeImmutable())->add($accessTokenTTL)); $accessToken->setPrivateKey($this->privateKey); while ($maxGenerationAttempts-- > 0) { $accessToken->setIdentifier($this->generateUniqueIdentifier()); try { $this->accessTokenRepository->persistNewAccessToken($accessToken); return $accessToken; } catch (UniqueTokenIdentifierConstraintViolationException $e) { if ($maxGenerationAttempts === 0) { throw $e; } } } } /** * Issue an auth code. * * @param DateInterval $authCodeTTL * @param ClientEntityInterface $client * @param string $userIdentifier * @param string|null $redirectUri * @param ScopeEntityInterface[] $scopes * * @throws OAuthServerException * @throws UniqueTokenIdentifierConstraintViolationException * * @return AuthCodeEntityInterface */ protected function issueAuthCode( DateInterval $authCodeTTL, ClientEntityInterface $client, $userIdentifier, $redirectUri, array $scopes = [] ) { $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS; $authCode = $this->authCodeRepository->getNewAuthCode(); $authCode->setExpiryDateTime((new DateTimeImmutable())->add($authCodeTTL)); $authCode->setClient($client); $authCode->setUserIdentifier($userIdentifier); if ($redirectUri !== null) { $authCode->setRedirectUri($redirectUri); } foreach ($scopes as $scope) { $authCode->addScope($scope); } while ($maxGenerationAttempts-- > 0) { $authCode->setIdentifier($this->generateUniqueIdentifier()); try { $this->authCodeRepository->persistNewAuthCode($authCode); return $authCode; } catch (UniqueTokenIdentifierConstraintViolationException $e) { if ($maxGenerationAttempts === 0) { throw $e; } } } } /** * @param AccessTokenEntityInterface $accessToken * * @throws OAuthServerException * @throws UniqueTokenIdentifierConstraintViolationException * * @return RefreshTokenEntityInterface|null */ protected function issueRefreshToken(AccessTokenEntityInterface $accessToken) { $refreshToken = $this->refreshTokenRepository->getNewRefreshToken(); if ($refreshToken === null) { return null; } $refreshToken->setExpiryDateTime((new DateTimeImmutable())->add($this->refreshTokenTTL)); $refreshToken->setAccessToken($accessToken); $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS; while ($maxGenerationAttempts-- > 0) { $refreshToken->setIdentifier($this->generateUniqueIdentifier()); try { $this->refreshTokenRepository->persistNewRefreshToken($refreshToken); return $refreshToken; } catch (UniqueTokenIdentifierConstraintViolationException $e) { if ($maxGenerationAttempts === 0) { throw $e; } } } } /** * Generate a new unique identifier. * * @param int $length * * @throws OAuthServerException * * @return string */ protected function generateUniqueIdentifier($length = 40) { try { return \bin2hex(\random_bytes($length)); // @codeCoverageIgnoreStart } catch (TypeError $e) { throw OAuthServerException::serverError('An unexpected error has occurred', $e); } catch (Error $e) { throw OAuthServerException::serverError('An unexpected error has occurred', $e); } catch (Exception $e) { // If you get this message, the CSPRNG failed hard. throw OAuthServerException::serverError('Could not generate a random string', $e); } // @codeCoverageIgnoreEnd } /** * {@inheritdoc} */ public function canRespondToAccessTokenRequest(ServerRequestInterface $request) { $requestParameters = (array) $request->getParsedBody(); return ( \array_key_exists('grant_type', $requestParameters) && $requestParameters['grant_type'] === $this->getIdentifier() ); } /** * {@inheritdoc} */ public function canRespondToAuthorizationRequest(ServerRequestInterface $request) { return false; } /** * {@inheritdoc} */ public function validateAuthorizationRequest(ServerRequestInterface $request) { throw new LogicException('This grant cannot validate an authorization request'); } /** * {@inheritdoc} */ public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest) { throw new LogicException('This grant cannot complete an authorization request'); } } oauth2-server/src/Grant/PasswordGrant.php 0000644 00000007703 15007532371 0014435 0 ustar 00 <?php /** * OAuth 2.0 Password grant. * * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Grant; use DateInterval; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\Repositories\UserRepositoryInterface; use League\OAuth2\Server\RequestAccessTokenEvent; use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\RequestRefreshTokenEvent; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Psr\Http\Message\ServerRequestInterface; /** * Password grant class. */ class PasswordGrant extends AbstractGrant { /** * @param UserRepositoryInterface $userRepository * @param RefreshTokenRepositoryInterface $refreshTokenRepository */ public function __construct( UserRepositoryInterface $userRepository, RefreshTokenRepositoryInterface $refreshTokenRepository ) { $this->setUserRepository($userRepository); $this->setRefreshTokenRepository($refreshTokenRepository); $this->refreshTokenTTL = new DateInterval('P1M'); } /** * {@inheritdoc} */ public function respondToAccessTokenRequest( ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ) { // Validate request $client = $this->validateClient($request); $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope)); $user = $this->validateUser($request, $client); // Finalize the requested scopes $finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier()); // Issue and persist new access token $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $finalizedScopes); $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); $responseType->setAccessToken($accessToken); // Issue and persist new refresh token if given $refreshToken = $this->issueRefreshToken($accessToken); if ($refreshToken !== null) { $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken)); $responseType->setRefreshToken($refreshToken); } return $responseType; } /** * @param ServerRequestInterface $request * @param ClientEntityInterface $client * * @throws OAuthServerException * * @return UserEntityInterface */ protected function validateUser(ServerRequestInterface $request, ClientEntityInterface $client) { $username = $this->getRequestParameter('username', $request); if (!\is_string($username)) { throw OAuthServerException::invalidRequest('username'); } $password = $this->getRequestParameter('password', $request); if (!\is_string($password)) { throw OAuthServerException::invalidRequest('password'); } $user = $this->userRepository->getUserEntityByUserCredentials( $username, $password, $this->getIdentifier(), $client ); if ($user instanceof UserEntityInterface === false) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidCredentials(); } return $user; } /** * {@inheritdoc} */ public function getIdentifier() { return 'password'; } } oauth2-server/src/Grant/AbstractAuthorizeGrant.php 0000644 00000001321 15007532371 0016257 0 ustar 00 <?php /** * Abstract authorization grant. * * @author Julián Gutiérrez <juliangut@gmail.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Grant; abstract class AbstractAuthorizeGrant extends AbstractGrant { /** * @param string $uri * @param array $params * @param string $queryDelimiter * * @return string */ public function makeRedirectUri($uri, $params = [], $queryDelimiter = '?') { $uri .= (\strstr($uri, $queryDelimiter) === false) ? $queryDelimiter : '&'; return $uri . \http_build_query($params); } } oauth2-server/src/Grant/GrantTypeInterface.php 0000644 00000010541 15007532371 0015367 0 ustar 00 <?php /** * OAuth 2.0 Grant type interface. * * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Grant; use DateInterval; use Defuse\Crypto\Key; use League\Event\EmitterAwareInterface; use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Psr\Http\Message\ServerRequestInterface; /** * Grant type interface. */ interface GrantTypeInterface extends EmitterAwareInterface { /** * Set refresh token TTL. * * @param DateInterval $refreshTokenTTL */ public function setRefreshTokenTTL(DateInterval $refreshTokenTTL); /** * Return the grant identifier that can be used in matching up requests. * * @return string */ public function getIdentifier(); /** * Respond to an incoming request. * * @param ServerRequestInterface $request * @param ResponseTypeInterface $responseType * @param DateInterval $accessTokenTTL * * @return ResponseTypeInterface */ public function respondToAccessTokenRequest( ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ); /** * The grant type should return true if it is able to response to an authorization request * * @param ServerRequestInterface $request * * @return bool */ public function canRespondToAuthorizationRequest(ServerRequestInterface $request); /** * If the grant can respond to an authorization request this method should be called to validate the parameters of * the request. * * If the validation is successful an AuthorizationRequest object will be returned. This object can be safely * serialized in a user's session, and can be used during user authentication and authorization. * * @param ServerRequestInterface $request * * @return AuthorizationRequest */ public function validateAuthorizationRequest(ServerRequestInterface $request); /** * Once a user has authenticated and authorized the client the grant can complete the authorization request. * The AuthorizationRequest object's $userId property must be set to the authenticated user and the * $authorizationApproved property must reflect their desire to authorize or deny the client. * * @param AuthorizationRequest $authorizationRequest * * @return ResponseTypeInterface */ public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest); /** * The grant type should return true if it is able to respond to this request. * * For example most grant types will check that the $_POST['grant_type'] property matches it's identifier property. * * @param ServerRequestInterface $request * * @return bool */ public function canRespondToAccessTokenRequest(ServerRequestInterface $request); /** * Set the client repository. * * @param ClientRepositoryInterface $clientRepository */ public function setClientRepository(ClientRepositoryInterface $clientRepository); /** * Set the access token repository. * * @param AccessTokenRepositoryInterface $accessTokenRepository */ public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository); /** * Set the scope repository. * * @param ScopeRepositoryInterface $scopeRepository */ public function setScopeRepository(ScopeRepositoryInterface $scopeRepository); /** * Set the default scope. * * @param string $scope */ public function setDefaultScope($scope); /** * Set the path to the private key. * * @param CryptKey $privateKey */ public function setPrivateKey(CryptKey $privateKey); /** * Set the encryption key * * @param string|Key|null $key */ public function setEncryptionKey($key = null); } oauth2-server/src/Grant/RefreshTokenGrant.php 0000644 00000011343 15007532371 0015225 0 ustar 00 <?php /** * OAuth 2.0 Refresh token grant. * * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Grant; use DateInterval; use Exception; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\RequestAccessTokenEvent; use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\RequestRefreshTokenEvent; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Psr\Http\Message\ServerRequestInterface; /** * Refresh token grant. */ class RefreshTokenGrant extends AbstractGrant { /** * @param RefreshTokenRepositoryInterface $refreshTokenRepository */ public function __construct(RefreshTokenRepositoryInterface $refreshTokenRepository) { $this->setRefreshTokenRepository($refreshTokenRepository); $this->refreshTokenTTL = new DateInterval('P1M'); } /** * {@inheritdoc} */ public function respondToAccessTokenRequest( ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ) { // Validate request $client = $this->validateClient($request); $oldRefreshToken = $this->validateOldRefreshToken($request, $client->getIdentifier()); $scopes = $this->validateScopes( $this->getRequestParameter( 'scope', $request, \implode(self::SCOPE_DELIMITER_STRING, $oldRefreshToken['scopes']) ) ); // The OAuth spec says that a refreshed access token can have the original scopes or fewer so ensure // the request doesn't include any new scopes foreach ($scopes as $scope) { if (\in_array($scope->getIdentifier(), $oldRefreshToken['scopes'], true) === false) { throw OAuthServerException::invalidScope($scope->getIdentifier()); } } // Expire old tokens $this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']); if ($this->revokeRefreshTokens) { $this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_token_id']); } // Issue and persist new access token $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $oldRefreshToken['user_id'], $scopes); $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); $responseType->setAccessToken($accessToken); // Issue and persist new refresh token if given if ($this->revokeRefreshTokens) { $refreshToken = $this->issueRefreshToken($accessToken); if ($refreshToken !== null) { $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken)); $responseType->setRefreshToken($refreshToken); } } return $responseType; } /** * @param ServerRequestInterface $request * @param string $clientId * * @throws OAuthServerException * * @return array */ protected function validateOldRefreshToken(ServerRequestInterface $request, $clientId) { $encryptedRefreshToken = $this->getRequestParameter('refresh_token', $request); if (!\is_string($encryptedRefreshToken)) { throw OAuthServerException::invalidRequest('refresh_token'); } // Validate refresh token try { $refreshToken = $this->decrypt($encryptedRefreshToken); } catch (Exception $e) { throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token', $e); } $refreshTokenData = \json_decode($refreshToken, true); if ($refreshTokenData['client_id'] !== $clientId) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request)); throw OAuthServerException::invalidRefreshToken('Token is not linked to client'); } if ($refreshTokenData['expire_time'] < \time()) { throw OAuthServerException::invalidRefreshToken('Token has expired'); } if ($this->refreshTokenRepository->isRefreshTokenRevoked($refreshTokenData['refresh_token_id']) === true) { throw OAuthServerException::invalidRefreshToken('Token has been revoked'); } return $refreshTokenData; } /** * {@inheritdoc} */ public function getIdentifier() { return 'refresh_token'; } } oauth2-server/src/Grant/ClientCredentialsGrant.php 0000644 00000004133 15007532371 0016221 0 ustar 00 <?php /** * OAuth 2.0 Client credentials grant. * * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Grant; use DateInterval; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\RequestAccessTokenEvent; use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Psr\Http\Message\ServerRequestInterface; /** * Client credentials grant class. */ class ClientCredentialsGrant extends AbstractGrant { /** * {@inheritdoc} */ public function respondToAccessTokenRequest( ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ) { list($clientId) = $this->getClientCredentials($request); $client = $this->getClientEntityOrFail($clientId, $request); if (!$client->isConfidential()) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient($request); } // Validate request $this->validateClient($request); $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope)); // Finalize the requested scopes $finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client); // Issue and persist access token $accessToken = $this->issueAccessToken($accessTokenTTL, $client, null, $finalizedScopes); // Send event to emitter $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); // Inject access token into response type $responseType->setAccessToken($accessToken); return $responseType; } /** * {@inheritdoc} */ public function getIdentifier() { return 'client_credentials'; } } oauth2-server/src/Grant/AuthCodeGrant.php 0000644 00000037567 15007532371 0014342 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Grant; use DateInterval; use DateTimeImmutable; use Exception; use League\OAuth2\Server\CodeChallengeVerifiers\CodeChallengeVerifierInterface; use League\OAuth2\Server\CodeChallengeVerifiers\PlainVerifier; use League\OAuth2\Server\CodeChallengeVerifiers\S256Verifier; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\RequestAccessTokenEvent; use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\RequestRefreshTokenEvent; use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use League\OAuth2\Server\ResponseTypes\RedirectResponse; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use LogicException; use Psr\Http\Message\ServerRequestInterface; use stdClass; class AuthCodeGrant extends AbstractAuthorizeGrant { /** * @var DateInterval */ private $authCodeTTL; /** * @var bool */ private $requireCodeChallengeForPublicClients = true; /** * @var CodeChallengeVerifierInterface[] */ private $codeChallengeVerifiers = []; /** * @param AuthCodeRepositoryInterface $authCodeRepository * @param RefreshTokenRepositoryInterface $refreshTokenRepository * @param DateInterval $authCodeTTL * * @throws Exception */ public function __construct( AuthCodeRepositoryInterface $authCodeRepository, RefreshTokenRepositoryInterface $refreshTokenRepository, DateInterval $authCodeTTL ) { $this->setAuthCodeRepository($authCodeRepository); $this->setRefreshTokenRepository($refreshTokenRepository); $this->authCodeTTL = $authCodeTTL; $this->refreshTokenTTL = new DateInterval('P1M'); if (\in_array('sha256', \hash_algos(), true)) { $s256Verifier = new S256Verifier(); $this->codeChallengeVerifiers[$s256Verifier->getMethod()] = $s256Verifier; } $plainVerifier = new PlainVerifier(); $this->codeChallengeVerifiers[$plainVerifier->getMethod()] = $plainVerifier; } /** * Disable the requirement for a code challenge for public clients. */ public function disableRequireCodeChallengeForPublicClients() { $this->requireCodeChallengeForPublicClients = false; } /** * Respond to an access token request. * * @param ServerRequestInterface $request * @param ResponseTypeInterface $responseType * @param DateInterval $accessTokenTTL * * @throws OAuthServerException * * @return ResponseTypeInterface */ public function respondToAccessTokenRequest( ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ) { list($clientId) = $this->getClientCredentials($request); $client = $this->getClientEntityOrFail($clientId, $request); // Only validate the client if it is confidential if ($client->isConfidential()) { $this->validateClient($request); } $encryptedAuthCode = $this->getRequestParameter('code', $request, null); if (!\is_string($encryptedAuthCode)) { throw OAuthServerException::invalidRequest('code'); } try { $authCodePayload = \json_decode($this->decrypt($encryptedAuthCode)); $this->validateAuthorizationCode($authCodePayload, $client, $request); $scopes = $this->scopeRepository->finalizeScopes( $this->validateScopes($authCodePayload->scopes), $this->getIdentifier(), $client, $authCodePayload->user_id ); } catch (LogicException $e) { throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code', $e); } $codeVerifier = $this->getRequestParameter('code_verifier', $request, null); // If a code challenge isn't present but a code verifier is, reject the request to block PKCE downgrade attack if (empty($authCodePayload->code_challenge) && $codeVerifier !== null) { throw OAuthServerException::invalidRequest( 'code_challenge', 'code_verifier received when no code_challenge is present' ); } if (!empty($authCodePayload->code_challenge)) { $this->validateCodeChallenge($authCodePayload, $codeVerifier); } // Issue and persist new access token $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes); $this->getEmitter()->emit(new RequestAccessTokenEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request, $accessToken)); $responseType->setAccessToken($accessToken); // Issue and persist new refresh token if given $refreshToken = $this->issueRefreshToken($accessToken); if ($refreshToken !== null) { $this->getEmitter()->emit(new RequestRefreshTokenEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request, $refreshToken)); $responseType->setRefreshToken($refreshToken); } // Revoke used auth code $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id); return $responseType; } private function validateCodeChallenge($authCodePayload, $codeVerifier) { if ($codeVerifier === null) { throw OAuthServerException::invalidRequest('code_verifier'); } // Validate code_verifier according to RFC-7636 // @see: https://tools.ietf.org/html/rfc7636#section-4.1 if (\preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeVerifier) !== 1) { throw OAuthServerException::invalidRequest( 'code_verifier', 'Code Verifier must follow the specifications of RFC-7636.' ); } if (\property_exists($authCodePayload, 'code_challenge_method')) { if (isset($this->codeChallengeVerifiers[$authCodePayload->code_challenge_method])) { $codeChallengeVerifier = $this->codeChallengeVerifiers[$authCodePayload->code_challenge_method]; if ($codeChallengeVerifier->verifyCodeChallenge($codeVerifier, $authCodePayload->code_challenge) === false) { throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.'); } } else { throw OAuthServerException::serverError( \sprintf( 'Unsupported code challenge method `%s`', $authCodePayload->code_challenge_method ) ); } } } /** * Validate the authorization code. * * @param stdClass $authCodePayload * @param ClientEntityInterface $client * @param ServerRequestInterface $request */ private function validateAuthorizationCode( $authCodePayload, ClientEntityInterface $client, ServerRequestInterface $request ) { if (!\property_exists($authCodePayload, 'auth_code_id')) { throw OAuthServerException::invalidRequest('code', 'Authorization code malformed'); } if (\time() > $authCodePayload->expire_time) { throw OAuthServerException::invalidRequest('code', 'Authorization code has expired'); } if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) { throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked'); } if ($authCodePayload->client_id !== $client->getIdentifier()) { throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client'); } // The redirect URI is required in this request $redirectUri = $this->getRequestParameter('redirect_uri', $request, null); if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) { throw OAuthServerException::invalidRequest('redirect_uri'); } if ($authCodePayload->redirect_uri !== $redirectUri) { throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI'); } } /** * Return the grant identifier that can be used in matching up requests. * * @return string */ public function getIdentifier() { return 'authorization_code'; } /** * {@inheritdoc} */ public function canRespondToAuthorizationRequest(ServerRequestInterface $request) { return ( \array_key_exists('response_type', $request->getQueryParams()) && $request->getQueryParams()['response_type'] === 'code' && isset($request->getQueryParams()['client_id']) ); } /** * {@inheritdoc} */ public function validateAuthorizationRequest(ServerRequestInterface $request) { $clientId = $this->getQueryStringParameter( 'client_id', $request, $this->getServerParameter('PHP_AUTH_USER', $request) ); if ($clientId === null) { throw OAuthServerException::invalidRequest('client_id'); } $client = $this->getClientEntityOrFail($clientId, $request); $redirectUri = $this->getQueryStringParameter('redirect_uri', $request); if ($redirectUri !== null) { if (!\is_string($redirectUri)) { throw OAuthServerException::invalidRequest('redirect_uri'); } $this->validateRedirectUri($redirectUri, $client, $request); } elseif (empty($client->getRedirectUri()) || (\is_array($client->getRedirectUri()) && \count($client->getRedirectUri()) !== 1)) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient($request); } $defaultClientRedirectUri = \is_array($client->getRedirectUri()) ? $client->getRedirectUri()[0] : $client->getRedirectUri(); $scopes = $this->validateScopes( $this->getQueryStringParameter('scope', $request, $this->defaultScope), $redirectUri ?? $defaultClientRedirectUri ); $stateParameter = $this->getQueryStringParameter('state', $request); $authorizationRequest = new AuthorizationRequest(); $authorizationRequest->setGrantTypeId($this->getIdentifier()); $authorizationRequest->setClient($client); $authorizationRequest->setRedirectUri($redirectUri); if ($stateParameter !== null) { $authorizationRequest->setState($stateParameter); } $authorizationRequest->setScopes($scopes); $codeChallenge = $this->getQueryStringParameter('code_challenge', $request); if ($codeChallenge !== null) { $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain'); if (\array_key_exists($codeChallengeMethod, $this->codeChallengeVerifiers) === false) { throw OAuthServerException::invalidRequest( 'code_challenge_method', 'Code challenge method must be one of ' . \implode(', ', \array_map( function ($method) { return '`' . $method . '`'; }, \array_keys($this->codeChallengeVerifiers) )) ); } // Validate code_challenge according to RFC-7636 // @see: https://tools.ietf.org/html/rfc7636#section-4.2 if (\preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) { throw OAuthServerException::invalidRequest( 'code_challenge', 'Code challenge must follow the specifications of RFC-7636.' ); } $authorizationRequest->setCodeChallenge($codeChallenge); $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod); } elseif ($this->requireCodeChallengeForPublicClients && !$client->isConfidential()) { throw OAuthServerException::invalidRequest('code_challenge', 'Code challenge must be provided for public clients'); } return $authorizationRequest; } /** * {@inheritdoc} */ public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest) { if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) { throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest'); } $finalRedirectUri = $authorizationRequest->getRedirectUri() ?? $this->getClientRedirectUri($authorizationRequest); // The user approved the client, redirect them back with an auth code if ($authorizationRequest->isAuthorizationApproved() === true) { $authCode = $this->issueAuthCode( $this->authCodeTTL, $authorizationRequest->getClient(), $authorizationRequest->getUser()->getIdentifier(), $authorizationRequest->getRedirectUri(), $authorizationRequest->getScopes() ); $payload = [ 'client_id' => $authCode->getClient()->getIdentifier(), 'redirect_uri' => $authCode->getRedirectUri(), 'auth_code_id' => $authCode->getIdentifier(), 'scopes' => $authCode->getScopes(), 'user_id' => $authCode->getUserIdentifier(), 'expire_time' => (new DateTimeImmutable())->add($this->authCodeTTL)->getTimestamp(), 'code_challenge' => $authorizationRequest->getCodeChallenge(), 'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(), ]; $jsonPayload = \json_encode($payload); if ($jsonPayload === false) { throw new LogicException('An error was encountered when JSON encoding the authorization request response'); } $response = new RedirectResponse(); $response->setRedirectUri( $this->makeRedirectUri( $finalRedirectUri, [ 'code' => $this->encrypt($jsonPayload), 'state' => $authorizationRequest->getState(), ] ) ); return $response; } // The user denied the client, redirect them back with an error throw OAuthServerException::accessDenied( 'The user denied the request', $this->makeRedirectUri( $finalRedirectUri, [ 'state' => $authorizationRequest->getState(), ] ) ); } /** * Get the client redirect URI if not set in the request. * * @param AuthorizationRequest $authorizationRequest * * @return string */ private function getClientRedirectUri(AuthorizationRequest $authorizationRequest) { return \is_array($authorizationRequest->getClient()->getRedirectUri()) ? $authorizationRequest->getClient()->getRedirectUri()[0] : $authorizationRequest->getClient()->getRedirectUri(); } } oauth2-server/src/Grant/ImplicitGrant.php 0000644 00000016772 15007532371 0014413 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Grant; use DateInterval; use League\OAuth2\Server\Entities\UserEntityInterface; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\RequestEvent; use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use League\OAuth2\Server\ResponseTypes\RedirectResponse; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use LogicException; use Psr\Http\Message\ServerRequestInterface; class ImplicitGrant extends AbstractAuthorizeGrant { /** * @var DateInterval */ private $accessTokenTTL; /** * @var string */ private $queryDelimiter; /** * @param DateInterval $accessTokenTTL * @param string $queryDelimiter */ public function __construct(DateInterval $accessTokenTTL, $queryDelimiter = '#') { $this->accessTokenTTL = $accessTokenTTL; $this->queryDelimiter = $queryDelimiter; } /** * @param DateInterval $refreshTokenTTL * * @throw LogicException */ public function setRefreshTokenTTL(DateInterval $refreshTokenTTL) { throw new LogicException('The Implicit Grant does not return refresh tokens'); } /** * @param RefreshTokenRepositoryInterface $refreshTokenRepository * * @throw LogicException */ public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository) { throw new LogicException('The Implicit Grant does not return refresh tokens'); } /** * {@inheritdoc} */ public function canRespondToAccessTokenRequest(ServerRequestInterface $request) { return false; } /** * Return the grant identifier that can be used in matching up requests. * * @return string */ public function getIdentifier() { return 'implicit'; } /** * Respond to an incoming request. * * @param ServerRequestInterface $request * @param ResponseTypeInterface $responseType * @param DateInterval $accessTokenTTL * * @return ResponseTypeInterface */ public function respondToAccessTokenRequest( ServerRequestInterface $request, ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ) { throw new LogicException('This grant does not used this method'); } /** * {@inheritdoc} */ public function canRespondToAuthorizationRequest(ServerRequestInterface $request) { return ( isset($request->getQueryParams()['response_type']) && $request->getQueryParams()['response_type'] === 'token' && isset($request->getQueryParams()['client_id']) ); } /** * {@inheritdoc} */ public function validateAuthorizationRequest(ServerRequestInterface $request) { $clientId = $this->getQueryStringParameter( 'client_id', $request, $this->getServerParameter('PHP_AUTH_USER', $request) ); if (\is_null($clientId)) { throw OAuthServerException::invalidRequest('client_id'); } $client = $this->getClientEntityOrFail($clientId, $request); $redirectUri = $this->getQueryStringParameter('redirect_uri', $request); if ($redirectUri !== null) { if (!\is_string($redirectUri)) { throw OAuthServerException::invalidRequest('redirect_uri'); } $this->validateRedirectUri($redirectUri, $client, $request); } elseif (\is_array($client->getRedirectUri()) && \count($client->getRedirectUri()) !== 1 || empty($client->getRedirectUri())) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); throw OAuthServerException::invalidClient($request); } else { $redirectUri = \is_array($client->getRedirectUri()) ? $client->getRedirectUri()[0] : $client->getRedirectUri(); } $scopes = $this->validateScopes( $this->getQueryStringParameter('scope', $request, $this->defaultScope), $redirectUri ); $stateParameter = $this->getQueryStringParameter('state', $request); if ($stateParameter !== null && !\is_string($stateParameter)) { throw OAuthServerException::invalidRequest('state'); } $authorizationRequest = new AuthorizationRequest(); $authorizationRequest->setGrantTypeId($this->getIdentifier()); $authorizationRequest->setClient($client); $authorizationRequest->setRedirectUri($redirectUri); if ($stateParameter !== null) { $authorizationRequest->setState($stateParameter); } $authorizationRequest->setScopes($scopes); return $authorizationRequest; } /** * {@inheritdoc} */ public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest) { if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) { throw new LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest'); } $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null) ? \is_array($authorizationRequest->getClient()->getRedirectUri()) ? $authorizationRequest->getClient()->getRedirectUri()[0] : $authorizationRequest->getClient()->getRedirectUri() : $authorizationRequest->getRedirectUri(); // The user approved the client, redirect them back with an access token if ($authorizationRequest->isAuthorizationApproved() === true) { // Finalize the requested scopes $finalizedScopes = $this->scopeRepository->finalizeScopes( $authorizationRequest->getScopes(), $this->getIdentifier(), $authorizationRequest->getClient(), $authorizationRequest->getUser()->getIdentifier() ); $accessToken = $this->issueAccessToken( $this->accessTokenTTL, $authorizationRequest->getClient(), $authorizationRequest->getUser()->getIdentifier(), $finalizedScopes ); $response = new RedirectResponse(); $response->setRedirectUri( $this->makeRedirectUri( $finalRedirectUri, [ 'access_token' => (string) $accessToken, 'token_type' => 'Bearer', 'expires_in' => $accessToken->getExpiryDateTime()->getTimestamp() - \time(), 'state' => $authorizationRequest->getState(), ], $this->queryDelimiter ) ); return $response; } // The user denied the client, redirect them back with an error throw OAuthServerException::accessDenied( 'The user denied the request', $this->makeRedirectUri( $finalRedirectUri, [ 'state' => $authorizationRequest->getState(), ] ) ); } } oauth2-server/src/ResponseTypes/RedirectResponse.php 0000644 00000001554 15007532371 0016665 0 ustar 00 <?php /** * OAuth 2.0 Redirect Response. * * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\ResponseTypes; use Psr\Http\Message\ResponseInterface; class RedirectResponse extends AbstractResponseType { /** * @var string */ private $redirectUri; /** * @param string $redirectUri */ public function setRedirectUri($redirectUri) { $this->redirectUri = $redirectUri; } /** * @param ResponseInterface $response * * @return ResponseInterface */ public function generateHttpResponse(ResponseInterface $response) { return $response->withStatus(302)->withHeader('Location', $this->redirectUri); } } oauth2-server/src/ResponseTypes/BearerTokenResponse.php 0000644 00000005404 15007532371 0017323 0 ustar 00 <?php /** * OAuth 2.0 Bearer Token Response. * * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\ResponseTypes; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use LogicException; use Psr\Http\Message\ResponseInterface; class BearerTokenResponse extends AbstractResponseType { /** * {@inheritdoc} */ public function generateHttpResponse(ResponseInterface $response) { $expireDateTime = $this->accessToken->getExpiryDateTime()->getTimestamp(); $responseParams = [ 'token_type' => 'Bearer', 'expires_in' => $expireDateTime - \time(), 'access_token' => (string) $this->accessToken, ]; if ($this->refreshToken instanceof RefreshTokenEntityInterface) { $refreshTokenPayload = \json_encode([ 'client_id' => $this->accessToken->getClient()->getIdentifier(), 'refresh_token_id' => $this->refreshToken->getIdentifier(), 'access_token_id' => $this->accessToken->getIdentifier(), 'scopes' => $this->accessToken->getScopes(), 'user_id' => $this->accessToken->getUserIdentifier(), 'expire_time' => $this->refreshToken->getExpiryDateTime()->getTimestamp(), ]); if ($refreshTokenPayload === false) { throw new LogicException('Error encountered JSON encoding the refresh token payload'); } $responseParams['refresh_token'] = $this->encrypt($refreshTokenPayload); } $responseParams = \json_encode(\array_merge($this->getExtraParams($this->accessToken), $responseParams)); if ($responseParams === false) { throw new LogicException('Error encountered JSON encoding response parameters'); } $response = $response ->withStatus(200) ->withHeader('pragma', 'no-cache') ->withHeader('cache-control', 'no-store') ->withHeader('content-type', 'application/json; charset=UTF-8'); $response->getBody()->write($responseParams); return $response; } /** * Add custom fields to your Bearer Token response here, then override * AuthorizationServer::getResponseType() to pull in your version of * this class rather than the default. * * @param AccessTokenEntityInterface $accessToken * * @return array */ protected function getExtraParams(AccessTokenEntityInterface $accessToken) { return []; } } oauth2-server/src/ResponseTypes/AbstractResponseType.php 0000644 00000002477 15007532371 0017536 0 ustar 00 <?php /** * OAuth 2.0 Abstract Response Type. * * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\ResponseTypes; use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\CryptTrait; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; abstract class AbstractResponseType implements ResponseTypeInterface { use CryptTrait; /** * @var AccessTokenEntityInterface */ protected $accessToken; /** * @var RefreshTokenEntityInterface */ protected $refreshToken; /** * @var CryptKey */ protected $privateKey; /** * {@inheritdoc} */ public function setAccessToken(AccessTokenEntityInterface $accessToken) { $this->accessToken = $accessToken; } /** * {@inheritdoc} */ public function setRefreshToken(RefreshTokenEntityInterface $refreshToken) { $this->refreshToken = $refreshToken; } /** * Set the private key * * @param CryptKey $key */ public function setPrivateKey(CryptKey $key) { $this->privateKey = $key; } } oauth2-server/src/ResponseTypes/ResponseTypeInterface.php 0000644 00000002160 15007532371 0017660 0 ustar 00 <?php /** * OAuth 2.0 Response Type Interface. * * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\ResponseTypes; use Defuse\Crypto\Key; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use Psr\Http\Message\ResponseInterface; interface ResponseTypeInterface { /** * @param AccessTokenEntityInterface $accessToken */ public function setAccessToken(AccessTokenEntityInterface $accessToken); /** * @param RefreshTokenEntityInterface $refreshToken */ public function setRefreshToken(RefreshTokenEntityInterface $refreshToken); /** * @param ResponseInterface $response * * @return ResponseInterface */ public function generateHttpResponse(ResponseInterface $response); /** * Set the encryption key * * @param string|Key|null $key */ public function setEncryptionKey($key = null); } oauth2-server/src/CryptTrait.php 0000644 00000004135 15007532371 0012665 0 ustar 00 <?php /** * Encrypt/decrypt with encryptionKey. * * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server; use Defuse\Crypto\Crypto; use Defuse\Crypto\Key; use Exception; use LogicException; trait CryptTrait { /** * @var string|Key|null */ protected $encryptionKey; /** * Encrypt data with encryptionKey. * * @param string $unencryptedData * * @throws LogicException * * @return string */ protected function encrypt($unencryptedData) { try { if ($this->encryptionKey instanceof Key) { return Crypto::encrypt($unencryptedData, $this->encryptionKey); } if (\is_string($this->encryptionKey)) { return Crypto::encryptWithPassword($unencryptedData, $this->encryptionKey); } throw new LogicException('Encryption key not set when attempting to encrypt'); } catch (Exception $e) { throw new LogicException($e->getMessage(), 0, $e); } } /** * Decrypt data with encryptionKey. * * @param string $encryptedData * * @throws LogicException * * @return string */ protected function decrypt($encryptedData) { try { if ($this->encryptionKey instanceof Key) { return Crypto::decrypt($encryptedData, $this->encryptionKey); } if (\is_string($this->encryptionKey)) { return Crypto::decryptWithPassword($encryptedData, $this->encryptionKey); } throw new LogicException('Encryption key not set when attempting to decrypt'); } catch (Exception $e) { throw new LogicException($e->getMessage(), 0, $e); } } /** * Set the encryption key * * @param string|Key $key */ public function setEncryptionKey($key = null) { $this->encryptionKey = $key; } } oauth2-server/src/RequestRefreshTokenEvent.php 0000644 00000001746 15007532371 0015537 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server; use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; use Psr\Http\Message\ServerRequestInterface; class RequestRefreshTokenEvent extends RequestEvent { /** * @var RefreshTokenEntityInterface */ private $refreshToken; /** * @param string $name * @param ServerRequestInterface $request */ public function __construct($name, ServerRequestInterface $request, RefreshTokenEntityInterface $refreshToken) { parent::__construct($name, $request); $this->refreshToken = $refreshToken; } /** * @return RefreshTokenEntityInterface * * @codeCoverageIgnore */ public function getRefreshToken() { return $this->refreshToken; } } oauth2-server/src/CryptKey.php 0000644 00000007313 15007532371 0012333 0 ustar 00 <?php /** * Cryptography key holder. * * @author Julián Gutiérrez <juliangut@gmail.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server; use LogicException; class CryptKey { /** @deprecated left for backward compatibility check */ const RSA_KEY_PATTERN = '/^(-----BEGIN (RSA )?(PUBLIC|PRIVATE) KEY-----)\R.*(-----END (RSA )?(PUBLIC|PRIVATE) KEY-----)\R?$/s'; private const FILE_PREFIX = 'file://'; /** * @var string Key contents */ protected $keyContents; /** * @var string */ protected $keyPath; /** * @var null|string */ protected $passPhrase; /** * @param string $keyPath * @param null|string $passPhrase * @param bool $keyPermissionsCheck */ public function __construct($keyPath, $passPhrase = null, $keyPermissionsCheck = true) { $this->passPhrase = $passPhrase; if (\strpos($keyPath, self::FILE_PREFIX) !== 0 && $this->isValidKey($keyPath, $this->passPhrase ?? '')) { $this->keyContents = $keyPath; $this->keyPath = ''; // There's no file, so no need for permission check. $keyPermissionsCheck = false; } elseif (\is_file($keyPath)) { if (\strpos($keyPath, self::FILE_PREFIX) !== 0) { $keyPath = self::FILE_PREFIX . $keyPath; } if (!\is_readable($keyPath)) { throw new LogicException(\sprintf('Key path "%s" does not exist or is not readable', $keyPath)); } $this->keyContents = \file_get_contents($keyPath); $this->keyPath = $keyPath; if (!$this->isValidKey($this->keyContents, $this->passPhrase ?? '')) { throw new LogicException('Unable to read key from file ' . $keyPath); } } else { throw new LogicException('Invalid key supplied'); } if ($keyPermissionsCheck === true) { // Verify the permissions of the key $keyPathPerms = \decoct(\fileperms($this->keyPath) & 0777); if (\in_array($keyPathPerms, ['400', '440', '600', '640', '660'], true) === false) { \trigger_error( \sprintf( 'Key file "%s" permissions are not correct, recommend changing to 600 or 660 instead of %s', $this->keyPath, $keyPathPerms ), E_USER_NOTICE ); } } } /** * Get key contents * * @return string Key contents */ public function getKeyContents(): string { return $this->keyContents; } /** * Validate key contents. * * @param string $contents * @param string $passPhrase * * @return bool */ private function isValidKey($contents, $passPhrase) { $pkey = \openssl_pkey_get_private($contents, $passPhrase) ?: \openssl_pkey_get_public($contents); if ($pkey === false) { return false; } $details = \openssl_pkey_get_details($pkey); return $details !== false && \in_array( $details['type'] ?? -1, [OPENSSL_KEYTYPE_RSA, OPENSSL_KEYTYPE_EC], true ); } /** * Retrieve key path. * * @return string */ public function getKeyPath() { return $this->keyPath; } /** * Retrieve key pass phrase. * * @return null|string */ public function getPassPhrase() { return $this->passPhrase; } } oauth2-server/src/AuthorizationValidators/BearerTokenValidator.php 0000644 00000011065 15007532371 0021520 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\AuthorizationValidators; use DateTimeZone; use Lcobucci\Clock\SystemClock; use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Rsa\Sha256; use Lcobucci\JWT\Validation\Constraint\LooseValidAt; use Lcobucci\JWT\Validation\Constraint\SignedWith; use Lcobucci\JWT\Validation\RequiredConstraintsViolated; use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\CryptTrait; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use Psr\Http\Message\ServerRequestInterface; class BearerTokenValidator implements AuthorizationValidatorInterface { use CryptTrait; /** * @var AccessTokenRepositoryInterface */ private $accessTokenRepository; /** * @var CryptKey */ protected $publicKey; /** * @var Configuration */ private $jwtConfiguration; /** * @var \DateInterval|null */ private $jwtValidAtDateLeeway; /** * @param AccessTokenRepositoryInterface $accessTokenRepository * @param \DateInterval|null $jwtValidAtDateLeeway */ public function __construct(AccessTokenRepositoryInterface $accessTokenRepository, \DateInterval $jwtValidAtDateLeeway = null) { $this->accessTokenRepository = $accessTokenRepository; $this->jwtValidAtDateLeeway = $jwtValidAtDateLeeway; } /** * Set the public key * * @param CryptKey $key */ public function setPublicKey(CryptKey $key) { $this->publicKey = $key; $this->initJwtConfiguration(); } /** * Initialise the JWT configuration. */ private function initJwtConfiguration() { $this->jwtConfiguration = Configuration::forSymmetricSigner( new Sha256(), InMemory::plainText('empty', 'empty') ); $clock = new SystemClock(new DateTimeZone(\date_default_timezone_get())); $this->jwtConfiguration->setValidationConstraints( new LooseValidAt($clock, $this->jwtValidAtDateLeeway), new SignedWith( new Sha256(), InMemory::plainText($this->publicKey->getKeyContents(), $this->publicKey->getPassPhrase() ?? '') ) ); } /** * {@inheritdoc} */ public function validateAuthorization(ServerRequestInterface $request) { if ($request->hasHeader('authorization') === false) { throw OAuthServerException::accessDenied('Missing "Authorization" header'); } $header = $request->getHeader('authorization'); $jwt = \trim((string) \preg_replace('/^\s*Bearer\s/', '', $header[0])); try { // Attempt to parse the JWT $token = $this->jwtConfiguration->parser()->parse($jwt); } catch (\Lcobucci\JWT\Exception $exception) { throw OAuthServerException::accessDenied($exception->getMessage(), null, $exception); } try { // Attempt to validate the JWT $constraints = $this->jwtConfiguration->validationConstraints(); $this->jwtConfiguration->validator()->assert($token, ...$constraints); } catch (RequiredConstraintsViolated $exception) { throw OAuthServerException::accessDenied('Access token could not be verified', null, $exception); } $claims = $token->claims(); // Check if token has been revoked if ($this->accessTokenRepository->isAccessTokenRevoked($claims->get('jti'))) { throw OAuthServerException::accessDenied('Access token has been revoked'); } // Return the request with additional attributes return $request ->withAttribute('oauth_access_token_id', $claims->get('jti')) ->withAttribute('oauth_client_id', $this->convertSingleRecordAudToString($claims->get('aud'))) ->withAttribute('oauth_user_id', $claims->get('sub')) ->withAttribute('oauth_scopes', $claims->get('scopes')); } /** * Convert single record arrays into strings to ensure backwards compatibility between v4 and v3.x of lcobucci/jwt * * @param mixed $aud * * @return array|string */ private function convertSingleRecordAudToString($aud) { return \is_array($aud) && \count($aud) === 1 ? $aud[0] : $aud; } } oauth2-server/src/AuthorizationValidators/AuthorizationValidatorInterface.php 0000644 00000001250 15007532371 0023773 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\AuthorizationValidators; use Psr\Http\Message\ServerRequestInterface; interface AuthorizationValidatorInterface { /** * Determine the access token in the authorization header and append OAUth properties to the request * as attributes. * * @param ServerRequestInterface $request * * @return ServerRequestInterface */ public function validateAuthorization(ServerRequestInterface $request); } oauth2-server/src/RedirectUriValidators/RedirectUriValidatorInterface.php 0000644 00000001017 15007532371 0022736 0 ustar 00 <?php /** * @author Sebastiano Degan <sebdeg87@gmail.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\RedirectUriValidators; interface RedirectUriValidatorInterface { /** * Validates the redirect uri. * * @param string $redirectUri * * @return bool Return true if valid, false otherwise */ public function validateRedirectUri($redirectUri); } oauth2-server/src/RedirectUriValidators/RedirectUriValidator.php 0000644 00000006043 15007532371 0021121 0 ustar 00 <?php /** * @author Sebastiano Degan <sebdeg87@gmail.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\RedirectUriValidators; use League\Uri\Exceptions\SyntaxError; use League\Uri\Uri; class RedirectUriValidator implements RedirectUriValidatorInterface { /** * @var array */ private $allowedRedirectUris; /** * New validator instance for the given uri * * @param string|array $allowedRedirectUris */ public function __construct($allowedRedirectUri) { if (\is_string($allowedRedirectUri)) { $this->allowedRedirectUris = [$allowedRedirectUri]; } elseif (\is_array($allowedRedirectUri)) { $this->allowedRedirectUris = $allowedRedirectUri; } else { $this->allowedRedirectUris = []; } } /** * Validates the redirect uri. * * @param string $redirectUri * * @return bool Return true if valid, false otherwise */ public function validateRedirectUri($redirectUri) { if ($this->isLoopbackUri($redirectUri)) { return $this->matchUriExcludingPort($redirectUri); } return $this->matchExactUri($redirectUri); } /** * According to section 7.3 of rfc8252, loopback uris are: * - "http://127.0.0.1:{port}/{path}" for IPv4 * - "http://[::1]:{port}/{path}" for IPv6 * * @param string $redirectUri * * @return bool */ private function isLoopbackUri($redirectUri) { try { $uri = Uri::createFromString($redirectUri); } catch (SyntaxError $e) { return false; } return $uri->getScheme() === 'http' && (\in_array($uri->getHost(), ['127.0.0.1', '[::1]'], true)); } /** * Find an exact match among allowed uris * * @param string $redirectUri * * @return bool Return true if an exact match is found, false otherwise */ private function matchExactUri($redirectUri) { return \in_array($redirectUri, $this->allowedRedirectUris, true); } /** * Find a match among allowed uris, allowing for different port numbers * * @param string $redirectUri * * @return bool Return true if a match is found, false otherwise */ private function matchUriExcludingPort($redirectUri) { $parsedUrl = $this->parseUrlAndRemovePort($redirectUri); foreach ($this->allowedRedirectUris as $allowedRedirectUri) { if ($parsedUrl === $this->parseUrlAndRemovePort($allowedRedirectUri)) { return true; } } return false; } /** * Parse an url like \parse_url, excluding the port * * @param string $url * * @return array */ private function parseUrlAndRemovePort($url) { $uri = Uri::createFromString($url); return (string) $uri->withPort(null); } } oauth2-server/src/RequestTypes/AuthorizationRequest.php 0000644 00000010101 15007532371 0017434 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\RequestTypes; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\Entities\UserEntityInterface; class AuthorizationRequest { /** * The grant type identifier * * @var string */ protected $grantTypeId; /** * The client identifier * * @var ClientEntityInterface */ protected $client; /** * The user identifier * * @var UserEntityInterface */ protected $user; /** * An array of scope identifiers * * @var ScopeEntityInterface[] */ protected $scopes = []; /** * Has the user authorized the authorization request * * @var bool */ protected $authorizationApproved = false; /** * The redirect URI used in the request * * @var string|null */ protected $redirectUri; /** * The state parameter on the authorization request * * @var string|null */ protected $state; /** * The code challenge (if provided) * * @var string */ protected $codeChallenge; /** * The code challenge method (if provided) * * @var string */ protected $codeChallengeMethod; /** * @return string */ public function getGrantTypeId() { return $this->grantTypeId; } /** * @param string $grantTypeId */ public function setGrantTypeId($grantTypeId) { $this->grantTypeId = $grantTypeId; } /** * @return ClientEntityInterface */ public function getClient() { return $this->client; } /** * @param ClientEntityInterface $client */ public function setClient(ClientEntityInterface $client) { $this->client = $client; } /** * @return UserEntityInterface|null */ public function getUser() { return $this->user; } /** * @param UserEntityInterface $user */ public function setUser(UserEntityInterface $user) { $this->user = $user; } /** * @return ScopeEntityInterface[] */ public function getScopes() { return $this->scopes; } /** * @param ScopeEntityInterface[] $scopes */ public function setScopes(array $scopes) { $this->scopes = $scopes; } /** * @return bool */ public function isAuthorizationApproved() { return $this->authorizationApproved; } /** * @param bool $authorizationApproved */ public function setAuthorizationApproved($authorizationApproved) { $this->authorizationApproved = $authorizationApproved; } /** * @return string|null */ public function getRedirectUri() { return $this->redirectUri; } /** * @param string|null $redirectUri */ public function setRedirectUri($redirectUri) { $this->redirectUri = $redirectUri; } /** * @return string|null */ public function getState() { return $this->state; } /** * @param string $state */ public function setState($state) { $this->state = $state; } /** * @return string */ public function getCodeChallenge() { return $this->codeChallenge; } /** * @param string $codeChallenge */ public function setCodeChallenge($codeChallenge) { $this->codeChallenge = $codeChallenge; } /** * @return string */ public function getCodeChallengeMethod() { return $this->codeChallengeMethod; } /** * @param string $codeChallengeMethod */ public function setCodeChallengeMethod($codeChallengeMethod) { $this->codeChallengeMethod = $codeChallengeMethod; } } oauth2-server/src/Entities/AuthCodeEntityInterface.php 0000644 00000000742 15007532371 0017056 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Entities; interface AuthCodeEntityInterface extends TokenInterface { /** * @return string|null */ public function getRedirectUri(); /** * @param string $uri */ public function setRedirectUri($uri); } oauth2-server/src/Entities/TokenInterface.php 0000644 00000003531 15007532371 0015244 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Entities; use DateTimeImmutable; interface TokenInterface { /** * Get the token's identifier. * * @return string */ public function getIdentifier(); /** * Set the token's identifier. * * @param mixed $identifier */ public function setIdentifier($identifier); /** * Get the token's expiry date time. * * @return DateTimeImmutable */ public function getExpiryDateTime(); /** * Set the date time when the token expires. * * @param DateTimeImmutable $dateTime */ public function setExpiryDateTime(DateTimeImmutable $dateTime); /** * Set the identifier of the user associated with the token. * * @param string|int|null $identifier The identifier of the user */ public function setUserIdentifier($identifier); /** * Get the token user's identifier. * * @return string|int|null */ public function getUserIdentifier(); /** * Get the client that the token was issued to. * * @return ClientEntityInterface */ public function getClient(); /** * Set the client that the token was issued to. * * @param ClientEntityInterface $client */ public function setClient(ClientEntityInterface $client); /** * Associate a scope with the token. * * @param ScopeEntityInterface $scope */ public function addScope(ScopeEntityInterface $scope); /** * Return an array of scopes associated with the token. * * @return ScopeEntityInterface[] */ public function getScopes(); } oauth2-server/src/Entities/UserEntityInterface.php 0000644 00000000627 15007532371 0016302 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Entities; interface UserEntityInterface { /** * Return the user's identifier. * * @return mixed */ public function getIdentifier(); } oauth2-server/src/Entities/ClientEntityInterface.php 0000644 00000001546 15007532371 0016603 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Entities; interface ClientEntityInterface { /** * Get the client's identifier. * * @return string */ public function getIdentifier(); /** * Get the client's name. * * @return string */ public function getName(); /** * Returns the registered redirect URI (as a string). * * Alternatively return an indexed array of redirect URIs. * * @return string|string[] */ public function getRedirectUri(); /** * Returns true if the client is confidential. * * @return bool */ public function isConfidential(); } oauth2-server/src/Entities/ScopeEntityInterface.php 0000644 00000000707 15007532371 0016434 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Entities; use JsonSerializable; interface ScopeEntityInterface extends JsonSerializable { /** * Get the scope's identifier. * * @return string */ public function getIdentifier(); } oauth2-server/src/Entities/Traits/ClientTrait.php 0000644 00000002133 15007532371 0016030 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Entities\Traits; trait ClientTrait { /** * @var string */ protected $name; /** * @var string|string[] */ protected $redirectUri; /** * @var bool */ protected $isConfidential = false; /** * Get the client's name. * * @return string * * @codeCoverageIgnore */ public function getName() { return $this->name; } /** * Returns the registered redirect URI (as a string). * * Alternatively return an indexed array of redirect URIs. * * @return string|string[] */ public function getRedirectUri() { return $this->redirectUri; } /** * Returns true if the client is confidential. * * @return bool */ public function isConfidential() { return $this->isConfidential; } } oauth2-server/src/Entities/Traits/AuthCodeTrait.php 0000644 00000001145 15007532371 0016310 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Entities\Traits; trait AuthCodeTrait { /** * @var null|string */ protected $redirectUri; /** * @return string|null */ public function getRedirectUri() { return $this->redirectUri; } /** * @param string $uri */ public function setRedirectUri($uri) { $this->redirectUri = $uri; } } oauth2-server/src/Entities/Traits/EntityTrait.php 0000644 00000001147 15007532371 0016072 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Entities\Traits; trait EntityTrait { /** * @var string */ protected $identifier; /** * @return mixed */ public function getIdentifier() { return $this->identifier; } /** * @param mixed $identifier */ public function setIdentifier($identifier) { $this->identifier = $identifier; } } oauth2-server/src/Entities/Traits/TokenEntityTrait.php 0000644 00000004643 15007532371 0017077 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Entities\Traits; use DateTimeImmutable; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; trait TokenEntityTrait { /** * @var ScopeEntityInterface[] */ protected $scopes = []; /** * @var DateTimeImmutable */ protected $expiryDateTime; /** * @var string|int|null */ protected $userIdentifier; /** * @var ClientEntityInterface */ protected $client; /** * Associate a scope with the token. * * @param ScopeEntityInterface $scope */ public function addScope(ScopeEntityInterface $scope) { $this->scopes[$scope->getIdentifier()] = $scope; } /** * Return an array of scopes associated with the token. * * @return ScopeEntityInterface[] */ public function getScopes() { return \array_values($this->scopes); } /** * Get the token's expiry date time. * * @return DateTimeImmutable */ public function getExpiryDateTime() { return $this->expiryDateTime; } /** * Set the date time when the token expires. * * @param DateTimeImmutable $dateTime */ public function setExpiryDateTime(DateTimeImmutable $dateTime) { $this->expiryDateTime = $dateTime; } /** * Set the identifier of the user associated with the token. * * @param string|int|null $identifier The identifier of the user */ public function setUserIdentifier($identifier) { $this->userIdentifier = $identifier; } /** * Get the token user's identifier. * * @return string|int|null */ public function getUserIdentifier() { return $this->userIdentifier; } /** * Get the client that the token was issued to. * * @return ClientEntityInterface */ public function getClient() { return $this->client; } /** * Set the client that the token was issued to. * * @param ClientEntityInterface $client */ public function setClient(ClientEntityInterface $client) { $this->client = $client; } } oauth2-server/src/Entities/Traits/ScopeTrait.php 0000644 00000001154 15007532371 0015665 0 ustar 00 <?php /** * @author Andrew Millington <andrew@noexceptions.io> * @copyright Copyright (c) Andrew Millington * @license http://mit-license.org * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Entities\Traits; trait ScopeTrait { /** * Serialize the object to the scopes string identifier when using json_encode(). * * @return string */ #[\ReturnTypeWillChange] public function jsonSerialize() { return $this->getIdentifier(); } /** * @return string */ abstract public function getIdentifier(); } oauth2-server/src/Entities/Traits/RefreshTokenTrait.php 0000644 00000002345 15007532371 0017216 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Entities\Traits; use DateTimeImmutable; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; trait RefreshTokenTrait { /** * @var AccessTokenEntityInterface */ protected $accessToken; /** * @var DateTimeImmutable */ protected $expiryDateTime; /** * {@inheritdoc} */ public function setAccessToken(AccessTokenEntityInterface $accessToken) { $this->accessToken = $accessToken; } /** * {@inheritdoc} */ public function getAccessToken() { return $this->accessToken; } /** * Get the token's expiry date time. * * @return DateTimeImmutable */ public function getExpiryDateTime() { return $this->expiryDateTime; } /** * Set the date time when the token expires. * * @param DateTimeImmutable $dateTime */ public function setExpiryDateTime(DateTimeImmutable $dateTime) { $this->expiryDateTime = $dateTime; } } oauth2-server/src/Entities/Traits/AccessTokenTrait.php 0000644 00000005137 15007532371 0017023 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Entities\Traits; use DateTimeImmutable; use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Rsa\Sha256; use Lcobucci\JWT\Token; use League\OAuth2\Server\CryptKey; use League\OAuth2\Server\Entities\ClientEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; trait AccessTokenTrait { /** * @var CryptKey */ private $privateKey; /** * @var Configuration */ private $jwtConfiguration; /** * Set the private key used to encrypt this access token. */ public function setPrivateKey(CryptKey $privateKey) { $this->privateKey = $privateKey; } /** * Initialise the JWT Configuration. */ public function initJwtConfiguration() { $this->jwtConfiguration = Configuration::forAsymmetricSigner( new Sha256(), InMemory::plainText($this->privateKey->getKeyContents(), $this->privateKey->getPassPhrase() ?? ''), InMemory::plainText('empty', 'empty') ); } /** * Generate a JWT from the access token * * @return Token */ private function convertToJWT() { $this->initJwtConfiguration(); return $this->jwtConfiguration->builder() ->permittedFor($this->getClient()->getIdentifier()) ->identifiedBy($this->getIdentifier()) ->issuedAt(new DateTimeImmutable()) ->canOnlyBeUsedAfter(new DateTimeImmutable()) ->expiresAt($this->getExpiryDateTime()) ->relatedTo((string) $this->getUserIdentifier()) ->withClaim('scopes', $this->getScopes()) ->getToken($this->jwtConfiguration->signer(), $this->jwtConfiguration->signingKey()); } /** * Generate a string representation from the access token */ public function __toString() { return $this->convertToJWT()->toString(); } /** * @return ClientEntityInterface */ abstract public function getClient(); /** * @return DateTimeImmutable */ abstract public function getExpiryDateTime(); /** * @return string|int */ abstract public function getUserIdentifier(); /** * @return ScopeEntityInterface[] */ abstract public function getScopes(); /** * @return string */ abstract public function getIdentifier(); } oauth2-server/src/Entities/RefreshTokenEntityInterface.php 0000644 00000002441 15007532371 0017757 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Entities; use DateTimeImmutable; interface RefreshTokenEntityInterface { /** * Get the token's identifier. * * @return string */ public function getIdentifier(); /** * Set the token's identifier. * * @param mixed $identifier */ public function setIdentifier($identifier); /** * Get the token's expiry date time. * * @return DateTimeImmutable */ public function getExpiryDateTime(); /** * Set the date time when the token expires. * * @param DateTimeImmutable $dateTime */ public function setExpiryDateTime(DateTimeImmutable $dateTime); /** * Set the access token that the refresh token was associated with. * * @param AccessTokenEntityInterface $accessToken */ public function setAccessToken(AccessTokenEntityInterface $accessToken); /** * Get the access token that the refresh token was originally associated with. * * @return AccessTokenEntityInterface */ public function getAccessToken(); } oauth2-server/src/Entities/AccessTokenEntityInterface.php 0000644 00000001127 15007532371 0017562 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Entities; use League\OAuth2\Server\CryptKey; interface AccessTokenEntityInterface extends TokenInterface { /** * Set a private key used to encrypt the access token. */ public function setPrivateKey(CryptKey $privateKey); /** * Generate a string representation of the access token. */ public function __toString(); } oauth2-server/src/ResourceServer.php 0000644 00000004755 15007532371 0013546 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server; use League\OAuth2\Server\AuthorizationValidators\AuthorizationValidatorInterface; use League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use Psr\Http\Message\ServerRequestInterface; class ResourceServer { /** * @var AccessTokenRepositoryInterface */ private $accessTokenRepository; /** * @var CryptKey */ private $publicKey; /** * @var null|AuthorizationValidatorInterface */ private $authorizationValidator; /** * New server instance. * * @param AccessTokenRepositoryInterface $accessTokenRepository * @param CryptKey|string $publicKey * @param null|AuthorizationValidatorInterface $authorizationValidator */ public function __construct( AccessTokenRepositoryInterface $accessTokenRepository, $publicKey, AuthorizationValidatorInterface $authorizationValidator = null ) { $this->accessTokenRepository = $accessTokenRepository; if ($publicKey instanceof CryptKey === false) { $publicKey = new CryptKey($publicKey); } $this->publicKey = $publicKey; $this->authorizationValidator = $authorizationValidator; } /** * @return AuthorizationValidatorInterface */ protected function getAuthorizationValidator() { if ($this->authorizationValidator instanceof AuthorizationValidatorInterface === false) { $this->authorizationValidator = new BearerTokenValidator($this->accessTokenRepository); } if ($this->authorizationValidator instanceof BearerTokenValidator === true) { $this->authorizationValidator->setPublicKey($this->publicKey); } return $this->authorizationValidator; } /** * Determine the access token validity. * * @param ServerRequestInterface $request * * @throws OAuthServerException * * @return ServerRequestInterface */ public function validateAuthenticatedRequest(ServerRequestInterface $request) { return $this->getAuthorizationValidator()->validateAuthorization($request); } } oauth2-server/src/CodeChallengeVerifiers/S256Verifier.php 0000644 00000001572 15007532371 0017251 0 ustar 00 <?php /** * @author Lukáš Unger <lookymsc@gmail.com> * @copyright Copyright (c) Lukáš Unger * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\CodeChallengeVerifiers; class S256Verifier implements CodeChallengeVerifierInterface { /** * Return code challenge method. * * @return string */ public function getMethod() { return 'S256'; } /** * Verify the code challenge. * * @param string $codeVerifier * @param string $codeChallenge * * @return bool */ public function verifyCodeChallenge($codeVerifier, $codeChallenge) { return \hash_equals( \strtr(\rtrim(\base64_encode(\hash('sha256', $codeVerifier, true)), '='), '+/', '-_'), $codeChallenge ); } } oauth2-server/src/CodeChallengeVerifiers/PlainVerifier.php 0000644 00000001422 15007532371 0017647 0 ustar 00 <?php /** * @author Lukáš Unger <lookymsc@gmail.com> * @copyright Copyright (c) Lukáš Unger * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\CodeChallengeVerifiers; class PlainVerifier implements CodeChallengeVerifierInterface { /** * Return code challenge method. * * @return string */ public function getMethod() { return 'plain'; } /** * Verify the code challenge. * * @param string $codeVerifier * @param string $codeChallenge * * @return bool */ public function verifyCodeChallenge($codeVerifier, $codeChallenge) { return \hash_equals($codeVerifier, $codeChallenge); } } oauth2-server/src/CodeChallengeVerifiers/CodeChallengeVerifierInterface.php 0000644 00000001223 15007532371 0023101 0 ustar 00 <?php /** * @author Lukáš Unger <lookymsc@gmail.com> * @copyright Copyright (c) Lukáš Unger * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\CodeChallengeVerifiers; interface CodeChallengeVerifierInterface { /** * Return code challenge method. * * @return string */ public function getMethod(); /** * Verify the code challenge. * * @param string $codeVerifier * @param string $codeChallenge * * @return bool */ public function verifyCodeChallenge($codeVerifier, $codeChallenge); } oauth2-server/src/AuthorizationServer.php 0000644 00000016310 15007532371 0014605 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server; use DateInterval; use Defuse\Crypto\Key; use League\Event\EmitterAwareInterface; use League\Event\EmitterAwareTrait; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Grant\GrantTypeInterface; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; use League\OAuth2\Server\RequestTypes\AuthorizationRequest; use League\OAuth2\Server\ResponseTypes\AbstractResponseType; use League\OAuth2\Server\ResponseTypes\BearerTokenResponse; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; class AuthorizationServer implements EmitterAwareInterface { use EmitterAwareTrait; /** * @var GrantTypeInterface[] */ protected $enabledGrantTypes = []; /** * @var DateInterval[] */ protected $grantTypeAccessTokenTTL = []; /** * @var CryptKey */ protected $privateKey; /** * @var CryptKey */ protected $publicKey; /** * @var ResponseTypeInterface */ protected $responseType; /** * @var ClientRepositoryInterface */ private $clientRepository; /** * @var AccessTokenRepositoryInterface */ private $accessTokenRepository; /** * @var ScopeRepositoryInterface */ private $scopeRepository; /** * @var string|Key */ private $encryptionKey; /** * @var string */ private $defaultScope = ''; /** * @var bool */ private $revokeRefreshTokens = true; /** * New server instance. * * @param ClientRepositoryInterface $clientRepository * @param AccessTokenRepositoryInterface $accessTokenRepository * @param ScopeRepositoryInterface $scopeRepository * @param CryptKey|string $privateKey * @param string|Key $encryptionKey * @param null|ResponseTypeInterface $responseType */ public function __construct( ClientRepositoryInterface $clientRepository, AccessTokenRepositoryInterface $accessTokenRepository, ScopeRepositoryInterface $scopeRepository, $privateKey, $encryptionKey, ResponseTypeInterface $responseType = null ) { $this->clientRepository = $clientRepository; $this->accessTokenRepository = $accessTokenRepository; $this->scopeRepository = $scopeRepository; if ($privateKey instanceof CryptKey === false) { $privateKey = new CryptKey($privateKey); } $this->privateKey = $privateKey; $this->encryptionKey = $encryptionKey; if ($responseType === null) { $responseType = new BearerTokenResponse(); } else { $responseType = clone $responseType; } $this->responseType = $responseType; } /** * Enable a grant type on the server. * * @param GrantTypeInterface $grantType * @param null|DateInterval $accessTokenTTL */ public function enableGrantType(GrantTypeInterface $grantType, DateInterval $accessTokenTTL = null) { if ($accessTokenTTL === null) { $accessTokenTTL = new DateInterval('PT1H'); } $grantType->setAccessTokenRepository($this->accessTokenRepository); $grantType->setClientRepository($this->clientRepository); $grantType->setScopeRepository($this->scopeRepository); $grantType->setDefaultScope($this->defaultScope); $grantType->setPrivateKey($this->privateKey); $grantType->setEmitter($this->getEmitter()); $grantType->setEncryptionKey($this->encryptionKey); $grantType->revokeRefreshTokens($this->revokeRefreshTokens); $this->enabledGrantTypes[$grantType->getIdentifier()] = $grantType; $this->grantTypeAccessTokenTTL[$grantType->getIdentifier()] = $accessTokenTTL; } /** * Validate an authorization request * * @param ServerRequestInterface $request * * @throws OAuthServerException * * @return AuthorizationRequest */ public function validateAuthorizationRequest(ServerRequestInterface $request) { foreach ($this->enabledGrantTypes as $grantType) { if ($grantType->canRespondToAuthorizationRequest($request)) { return $grantType->validateAuthorizationRequest($request); } } throw OAuthServerException::unsupportedGrantType(); } /** * Complete an authorization request * * @param AuthorizationRequest $authRequest * @param ResponseInterface $response * * @return ResponseInterface */ public function completeAuthorizationRequest(AuthorizationRequest $authRequest, ResponseInterface $response) { return $this->enabledGrantTypes[$authRequest->getGrantTypeId()] ->completeAuthorizationRequest($authRequest) ->generateHttpResponse($response); } /** * Return an access token response. * * @param ServerRequestInterface $request * @param ResponseInterface $response * * @throws OAuthServerException * * @return ResponseInterface */ public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseInterface $response) { foreach ($this->enabledGrantTypes as $grantType) { if (!$grantType->canRespondToAccessTokenRequest($request)) { continue; } $tokenResponse = $grantType->respondToAccessTokenRequest( $request, $this->getResponseType(), $this->grantTypeAccessTokenTTL[$grantType->getIdentifier()] ); if ($tokenResponse instanceof ResponseTypeInterface) { return $tokenResponse->generateHttpResponse($response); } } throw OAuthServerException::unsupportedGrantType(); } /** * Get the token type that grants will return in the HTTP response. * * @return ResponseTypeInterface */ protected function getResponseType() { $responseType = clone $this->responseType; if ($responseType instanceof AbstractResponseType) { $responseType->setPrivateKey($this->privateKey); } $responseType->setEncryptionKey($this->encryptionKey); return $responseType; } /** * Set the default scope for the authorization server. * * @param string $defaultScope */ public function setDefaultScope($defaultScope) { $this->defaultScope = $defaultScope; } /** * Sets whether to revoke refresh tokens or not (for all grant types). * * @param bool $revokeRefreshTokens */ public function revokeRefreshTokens(bool $revokeRefreshTokens): void { $this->revokeRefreshTokens = $revokeRefreshTokens; } } oauth2-server/src/Middleware/AuthorizationServerMiddleware.php 0000644 00000003202 15007532371 0020654 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Middleware; use Exception; use League\OAuth2\Server\AuthorizationServer; use League\OAuth2\Server\Exception\OAuthServerException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; class AuthorizationServerMiddleware { /** * @var AuthorizationServer */ private $server; /** * @param AuthorizationServer $server */ public function __construct(AuthorizationServer $server) { $this->server = $server; } /** * @param ServerRequestInterface $request * @param ResponseInterface $response * @param callable $next * * @return ResponseInterface */ public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next) { try { $response = $this->server->respondToAccessTokenRequest($request, $response); } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); // @codeCoverageIgnoreStart } catch (Exception $exception) { return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) ->generateHttpResponse($response); // @codeCoverageIgnoreEnd } // Pass the request and response on to the next responder in the chain return $next($request, $response); } } oauth2-server/src/Middleware/ResourceServerMiddleware.php 0000644 00000003136 15007532371 0017611 0 ustar 00 <?php /** * @author Alex Bilbie <hello@alexbilbie.com> * @copyright Copyright (c) Alex Bilbie * @license http://mit-license.org/ * * @link https://github.com/thephpleague/oauth2-server */ namespace League\OAuth2\Server\Middleware; use Exception; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\ResourceServer; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; class ResourceServerMiddleware { /** * @var ResourceServer */ private $server; /** * @param ResourceServer $server */ public function __construct(ResourceServer $server) { $this->server = $server; } /** * @param ServerRequestInterface $request * @param ResponseInterface $response * @param callable $next * * @return ResponseInterface */ public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next) { try { $request = $this->server->validateAuthenticatedRequest($request); } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); // @codeCoverageIgnoreStart } catch (Exception $exception) { return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) ->generateHttpResponse($response); // @codeCoverageIgnoreEnd } // Pass the request and response on to the next responder in the chain return $next($request, $response); } }
Free Space : Byte