From 57b5b68f7e5693236ed838f35a8ce5209949633a Mon Sep 17 00:00:00 2001 From: Oliver Hader <oliver@typo3.org> Date: Tue, 16 Mar 2021 10:04:20 +0100 Subject: [PATCH] [SECURITY] Mitigate directly accessible file upload in form framework File handling implementation in `UploadedFileReferenceConverter` of `ext:form` creates files in `/fileadmin/user_uploads/` whenever some Extbase controller is (implicitly) dealing with `FileReference` models, unless particular implementations assign specific type converters or register type converters having a higher processing priority. As a side-effect this could lead to by-passing mime-type validators, allowing to plant cross-site scripting and other malicious binaries to public accessible `/fileadmin/` storage. PHP files and similar are blocked since `fileDenyPattern` rule is active in any case. This change makes the usage of `UploadedFileReferenceConverter` more specific in the scope of processing contact forms with `ext:form` * use random folder names for files, `.../form_abcde12345/image.png` * removes `UploadedFileReferenceConverter` from being used implicitly by other Extbase implementations dealing with `FileReference` models `PseudoFileReference` has been introduced to limit properties being serialized to `uid` (in case it's a real file reference) or `uidLocal` (in case it's a transient reference, pointing to a file). Direct URLs to uploaded files are substituted by `fileDump` eID script now, enforcing corresponding FAL mime-type and denying the web server from guessing/interpreting a different mime-type based on file suffix. A unique form `__session` value has been introduce, serving as seed to derive for instance mentioned folder names for uploaded files. In addition to that, form `__state` is only parsed when having been submitted via expected `FormFrontendController::performAction`. Resolves: #92136 Releases: master, 11.1, 10.4, 9.5 Change-Id: I7c33803443a68d6b3c895ec74da802a70bd390c1 Security-Bulletin: TYPO3-CORE-SA-2021-002 Security-References: CVE-2021-21355 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/68435 Tested-by: Oliver Hader <oliver.hader@typo3.org> Reviewed-by: Oliver Hader <oliver.hader@typo3.org> --- Build/Scripts/generateMimeTypes.php | 82 ++ Build/package.json | 1 + Build/yarn.lock | 5 + .../core/Classes/Resource/FileReference.php | 6 +- .../Classes/Resource/MimeTypeCollection.php | 993 ++++++++++++++++++ .../Classes/Resource/MimeTypeDetector.php | 60 ++ .../Finishers/DeleteUploadsFinisher.php | 39 + .../Classes/Domain/Runtime/FormRuntime.php | 117 ++- .../Runtime/FormRuntime/FormSession.php | 94 ++ .../AfterFormStateInitializedInterface.php | 34 + .../Property/PropertyMappingConfiguration.php | 64 +- .../Mvc/Property/TypeConverter/PseudoFile.php | 106 ++ .../TypeConverter/PseudoFileReference.php | 86 ++ .../UploadedFileReferenceConverter.php | 112 +- .../Mvc/Validation/FileSizeValidator.php | 12 +- .../Mvc/Validation/MimeTypeValidator.php | 38 +- .../Classes/Slot/ResourcePublicationSlot.php | 83 ++ .../Classes/ViewHelpers/FormViewHelper.php | 42 +- typo3/sysext/form/Configuration/Services.yaml | 7 + .../Resources/Private/Language/locallang.xlf | 3 + .../Mvc/Validation/MimeTypeValidatorTest.php | 143 +++ .../PropertyMappingConfigurationTest.php | 21 + .../Mvc/Validation/MimeTypeValidatorTest.php | 39 + typo3/sysext/form/ext_localconf.php | 5 +- 24 files changed, 2139 insertions(+), 53 deletions(-) create mode 100755 Build/Scripts/generateMimeTypes.php create mode 100644 typo3/sysext/core/Classes/Resource/MimeTypeCollection.php create mode 100644 typo3/sysext/core/Classes/Resource/MimeTypeDetector.php create mode 100644 typo3/sysext/form/Classes/Domain/Runtime/FormRuntime/FormSession.php create mode 100644 typo3/sysext/form/Classes/Domain/Runtime/FormRuntime/Lifecycle/AfterFormStateInitializedInterface.php create mode 100644 typo3/sysext/form/Classes/Mvc/Property/TypeConverter/PseudoFile.php create mode 100644 typo3/sysext/form/Classes/Mvc/Property/TypeConverter/PseudoFileReference.php create mode 100644 typo3/sysext/form/Classes/Slot/ResourcePublicationSlot.php create mode 100644 typo3/sysext/form/Tests/Functional/Mvc/Validation/MimeTypeValidatorTest.php diff --git a/Build/Scripts/generateMimeTypes.php b/Build/Scripts/generateMimeTypes.php new file mode 100755 index 000000000000..fa0530785b79 --- /dev/null +++ b/Build/Scripts/generateMimeTypes.php @@ -0,0 +1,82 @@ +#!/usr/bin/env php +<?php +declare(strict_types=1); +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +$dbJson = file_get_contents(dirname(__DIR__) . '/node_modules/mime-db/db.json'); +$dbJson = json_decode($dbJson, true); + +$mimeTypeMapping = []; +$mimeTypeString = ''; +foreach ($dbJson as $mimeType => $mimeTypeInfo) { + if (isset($mimeTypeInfo['extensions'])) { + $mimeTypeMapping[$mimeType] = $mimeTypeInfo['extensions']; + } +} + +// @todo: add our own file extensions here + +foreach ($mimeTypeMapping as $mimeType => $extensionInfo) { + $mimeTypeString .= " '" . $mimeType . "' => ['" . implode("', '", $extensionInfo) . "'], +"; +} + +$classTemplate = '<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Core\Resource; + +/** + * This class contains a list of all available / known mimetypes and file extensions, + * and is automatically generated by TYPO3 via Core/Build/Scripts/generateMimeTypes.php + */ +final class MimeTypeCollection +{ + private $map = [ +' . rtrim($mimeTypeString, ',') . +' ]; + + /** + * @return array<string, List<string>> + */ + public function getMap(): array + { + return $this->map; + } + + /** + * @return List<string> + */ + public function getMimeTypes(): array + { + return array_keys($this->map); + } +} +'; + +file_put_contents(dirname(dirname(__DIR__)) . '/typo3/sysext/core/Classes/Resource/MimeTypeCollection.php', $classTemplate); diff --git a/Build/package.json b/Build/package.json index fa1dd286b40f..12cfdb12419a 100644 --- a/Build/package.json +++ b/Build/package.json @@ -56,6 +56,7 @@ "karma-opera-launcher": "^1.0.0", "karma-requirejs": "^1.1.0", "karma-safari-launcher": "^1.0.0", + "mime-db": "^1.46.0", "node-sass": "^4.14.1", "patch-package": "^6.2.2", "postcss-banner": "^3.0.2", diff --git a/Build/yarn.lock b/Build/yarn.lock index 57f550aadb7c..a15a0fc45842 100644 --- a/Build/yarn.lock +++ b/Build/yarn.lock @@ -5528,6 +5528,11 @@ mime-db@^1.28.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== +mime-db@^1.46.0: + version "1.46.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee" + integrity sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ== + mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" diff --git a/typo3/sysext/core/Classes/Resource/FileReference.php b/typo3/sysext/core/Classes/Resource/FileReference.php index 68c8dfc8da72..c25d9d260b30 100644 --- a/typo3/sysext/core/Classes/Resource/FileReference.php +++ b/typo3/sysext/core/Classes/Resource/FileReference.php @@ -516,14 +516,16 @@ class FileReference implements FileInterface public function __sleep(): array { $keys = get_object_vars($this); - unset($keys['originalFile']); + unset($keys['originalFile'], $keys['mergedProperties']); return array_keys($keys); } public function __wakeup(): void { + $factory = GeneralUtility::makeInstance(ResourceFactory::class); $this->originalFile = $this->getFileObject( - (int)$this->propertiesOfFileReference['uid_local'] + (int)$this->propertiesOfFileReference['uid_local'], + $factory ); } } diff --git a/typo3/sysext/core/Classes/Resource/MimeTypeCollection.php b/typo3/sysext/core/Classes/Resource/MimeTypeCollection.php new file mode 100644 index 000000000000..36293f392cfd --- /dev/null +++ b/typo3/sysext/core/Classes/Resource/MimeTypeCollection.php @@ -0,0 +1,993 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Core\Resource; + +/** + * This class contains a list of all available / known mimetypes and file extensions, + * and is automatically generated by TYPO3 via Core/Build/Scripts/generateMimeTypes.php + */ +final class MimeTypeCollection +{ + private $map = [ + 'application/andrew-inset' => ['ez'], + 'application/applixware' => ['aw'], + '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/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/cu-seeme' => ['cu'], + 'application/dash+xml' => ['mpd'], + 'application/davmount+xml' => ['davmount'], + 'application/docbook+xml' => ['dbk'], + 'application/dssc+der' => ['dssc'], + 'application/dssc+xml' => ['xdssc'], + 'application/ecmascript' => ['ecma', 'es'], + 'application/emma+xml' => ['emma'], + 'application/emotionml+xml' => ['emotionml'], + 'application/epub+zip' => ['epub'], + 'application/exi' => ['exi'], + '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'], + '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', 'mjs'], + '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/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' => ['mp4s', 'm4p'], + 'application/mrb-consumer+xml' => ['xdf'], + 'application/mrb-publish+xml' => ['xdf'], + 'application/msword' => ['doc', 'dot'], + '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'], + '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'], + 'application/pgp-encrypted' => ['pgp'], + 'application/pgp-signature' => ['asc', 'sig'], + '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/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/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/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.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.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'], + '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.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'], + '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/widget' => ['wgt'], + 'application/winhlp' => ['hlp'], + 'application/wsdl+xml' => ['wsdl'], + 'application/wspolicy+xml' => ['wspolicy'], + 'application/x-7z-compressed' => ['7z'], + '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'], + 'application/x-install-instructions' => ['install'], + 'application/x-iso9660-image' => ['iso'], + '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'], + '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-error+xml' => ['xer'], + 'application/xcap-ns+xml' => ['xns'], + 'application/xenc+xml' => ['xenc'], + '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/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'], + 'audio/x-pn-realaudio-plugin' => ['rmp'], + '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/avif' => ['avif'], + 'image/bmp' => ['bmp'], + 'image/cgm' => ['cgm'], + 'image/dicom-rle' => ['drle'], + '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'], + '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'], + '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/mesh' => ['msh', 'mesh', 'silo'], + 'model/mtl' => ['mtl'], + 'model/obj' => ['obj'], + 'model/stl' => ['stl'], + '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.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/jsx' => ['jsx'], + 'text/less' => ['less'], + 'text/markdown' => ['markdown', 'md'], + 'text/mathml' => ['mml'], + 'text/mdx' => ['mdx'], + 'text/n3' => ['n3'], + 'text/plain' => ['txt', 'text', 'conf', 'def', 'list', 'log', 'in', 'ini'], + '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.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/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'], + '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'], + ]; + + /** + * @return array<string, List<string>> + */ + public function getMap(): array + { + return $this->map; + } + + /** + * @return List<string> + */ + public function getMimeTypes(): array + { + return array_keys($this->map); + } +} diff --git a/typo3/sysext/core/Classes/Resource/MimeTypeDetector.php b/typo3/sysext/core/Classes/Resource/MimeTypeDetector.php new file mode 100644 index 000000000000..0e124883a878 --- /dev/null +++ b/typo3/sysext/core/Classes/Resource/MimeTypeDetector.php @@ -0,0 +1,60 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Core\Resource; + +/** + * This class contains a list of all available / known mimetypes and file extensions, + * and is automatically generated by TYPO3 via Core/Build/Scripts/generateMimeTypes.php + */ +final class MimeTypeDetector +{ + /** + * @var MimeTypeCollection + */ + private $collection; + + public function __construct() + { + $this->collection = new MimeTypeCollection(); + } + + /** + * @param string $fileExtension + * @return array<int, string> + */ + public function getMimeTypesForFileExtension(string $fileExtension): array + { + $mimeTypes = []; + $fileExtension = strtolower($fileExtension); + foreach ($this->collection->getMap() as $mimeType => $availableExtensions) { + if (in_array($fileExtension, $availableExtensions, true)) { + $mimeTypes[] = $mimeType; + } + } + return $mimeTypes; + } + + /** + * @param string $mimeType + * @return array<int, string> + */ + public function getFileExtensionsForMimeType(string $mimeType): array + { + return $this->collection->getMap()[strtolower($mimeType)] ?? []; + } +} diff --git a/typo3/sysext/form/Classes/Domain/Finishers/DeleteUploadsFinisher.php b/typo3/sysext/form/Classes/Domain/Finishers/DeleteUploadsFinisher.php index 5be9be344732..62865d2c0ee4 100644 --- a/typo3/sysext/form/Classes/Domain/Finishers/DeleteUploadsFinisher.php +++ b/typo3/sysext/form/Classes/Domain/Finishers/DeleteUploadsFinisher.php @@ -17,6 +17,7 @@ declare(strict_types=1); namespace TYPO3\CMS\Form\Domain\Finishers; +use TYPO3\CMS\Core\Resource\Folder; use TYPO3\CMS\Extbase\Domain\Model\FileReference; use TYPO3\CMS\Form\Domain\Model\FormElements\FileUpload; @@ -38,6 +39,7 @@ class DeleteUploadsFinisher extends AbstractFinisher { $formRuntime = $this->finisherContext->getFormRuntime(); + $uploadFolders = []; $elements = $formRuntime->getFormDefinition()->getRenderablesRecursively(); foreach ($elements as $element) { if (!$element instanceof FileUpload) { @@ -51,7 +53,44 @@ class DeleteUploadsFinisher extends AbstractFinisher if ($file instanceof FileReference) { $file = $file->getOriginalResource(); } + + $folder = $file->getParentFolder(); + $uploadFolders[$folder->getCombinedIdentifier()] = $folder; + $file->getStorage()->deleteFile($file->getOriginalFile()); } + + $this->deleteEmptyUploadFolders($uploadFolders); + } + + /** + * note: + * TYPO3\CMS\Form\Mvc\Property\TypeConverter\UploadedFileReferenceConverter::importUploadedResource() + * creates a sub-folder for file uploads (e.g. .../form_<40-chars-hash>/actual.file) + * @param Folder[] $folders + */ + protected function deleteEmptyUploadFolders(array $folders): void + { + foreach ($folders as $folder) { + $parentFolder = $folder->getParentFolder(); + + if ($this->isEmptyFolder($folder)) { + $folder->delete(); + } + + if ($this->isEmptyFolder($parentFolder)) { + $parentFolder->delete(); + } + } + } + + /** + * @param Folder $folder + * @return bool + */ + protected function isEmptyFolder(Folder $folder): bool + { + return $folder->getFileCount() === 0 + && $folder->getStorage()->countFoldersInFolder($folder) === 0; } } diff --git a/typo3/sysext/form/Classes/Domain/Runtime/FormRuntime.php b/typo3/sysext/form/Classes/Domain/Runtime/FormRuntime.php index 42258c7fc2f3..ca76dc6caa29 100644 --- a/typo3/sysext/form/Classes/Domain/Runtime/FormRuntime.php +++ b/typo3/sysext/form/Classes/Domain/Runtime/FormRuntime.php @@ -33,6 +33,7 @@ use TYPO3\CMS\Core\Site\Entity\SiteLanguage; use TYPO3\CMS\Core\Utility\ArrayUtility; use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface; use TYPO3\CMS\Extbase\Error\Result; use TYPO3\CMS\Extbase\Mvc\Controller\Arguments; use TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext; @@ -53,9 +54,12 @@ use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface; use TYPO3\CMS\Form\Domain\Model\Renderable\VariableRenderableInterface; use TYPO3\CMS\Form\Domain\Renderer\RendererInterface; use TYPO3\CMS\Form\Domain\Runtime\Exception\PropertyMappingException; +use TYPO3\CMS\Form\Domain\Runtime\FormRuntime\FormSession; +use TYPO3\CMS\Form\Domain\Runtime\FormRuntime\Lifecycle\AfterFormStateInitializedInterface; use TYPO3\CMS\Form\Exception as FormException; use TYPO3\CMS\Form\Mvc\Validation\EmptyValidator; use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication; +use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController; /** @@ -125,6 +129,14 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess */ protected $formState; + /** + * Individual unique random form session identifier valid + * for current user session. This value is not persisted server-side. + * + * @var FormSession|null + */ + protected $formSession; + /** * The current page is the page which will be displayed to the user * during rendering. @@ -164,6 +176,11 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess */ protected $currentFinisher; + /** + * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface + */ + protected $configurationManager; + /** * @param \TYPO3\CMS\Extbase\Security\Cryptography\HashService $hashService * @internal @@ -182,6 +199,14 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess $this->objectManager = $objectManager; } + /** + * @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager + */ + public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager) + { + $this->configurationManager = $configurationManager; + } + /** * @param FormDefinition $formDefinition * @param Request $request @@ -206,43 +231,79 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess public function initializeObject() { $this->initializeCurrentSiteLanguage(); + $this->initializeFormSessionFromRequest(); $this->initializeFormStateFromRequest(); + $this->triggerAfterFormStateInitialized(); $this->processVariants(); $this->initializeCurrentPageFromRequest(); $this->initializeHoneypotFromRequest(); - if (!$this->isFirstRequest() && $this->getRequest()->getMethod() === 'POST') { + // Only validate and set form values within the form state + // if the current request is not the very first request + // and the current request can be processed (POST request and uncached). + if (!$this->isFirstRequest() && $this->canProcessFormSubmission()) { $this->processSubmittedFormValues(); } $this->renderHoneypot(); } + /** + * @todo `FormRuntime::$formSession` is still vulnerable to session fixation unless a real cookie-based process is used + */ + protected function initializeFormSessionFromRequest(): void + { + // Initialize the form session only if the current request can be processed + // (POST request and uncached) to ensure unique sessions for each form submitter. + if (!$this->canProcessFormSubmission()) { + return; + } + + $sessionIdentifierFromRequest = $this->request->getInternalArgument('__session'); + $this->formSession = GeneralUtility::makeInstance(FormSession::class, $sessionIdentifierFromRequest); + } + /** * Initializes the current state of the form, based on the request * @throws BadRequestException */ protected function initializeFormStateFromRequest() { + // Only try to reconstitute the form state if the current request + // is not the very first request and if the current request can + // be processed (POST request and uncached). $serializedFormStateWithHmac = $this->request->getInternalArgument('__state'); - if ($serializedFormStateWithHmac === null) { + if ($serializedFormStateWithHmac === null || !$this->canProcessFormSubmission()) { $this->formState = GeneralUtility::makeInstance(FormState::class); } else { try { $serializedFormState = $this->hashService->validateAndStripHmac($serializedFormStateWithHmac); } catch (InvalidHashException | InvalidArgumentForHashGenerationException $e) { - throw new BadRequestException('The HMAC of the form could not be validated.', 1581862823); + throw new BadRequestException('The HMAC of the form state could not be validated.', 1581862823); } $this->formState = unserialize(base64_decode($serializedFormState)); } } + protected function triggerAfterFormStateInitialized(): void + { + foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterFormStateInitialized'] ?? [] as $className) { + $hookObj = GeneralUtility::makeInstance($className); + if ($hookObj instanceof AfterFormStateInitializedInterface) { + $hookObj->afterFormStateInitialized($this); + } + } + } + /** * Initializes the current page data based on the current request, also modifiable by a hook */ protected function initializeCurrentPageFromRequest() { - if (!$this->formState->isFormSubmitted()) { + // If there was no previous form submissions or if the current request + // can't be processed (no POST request and/or cached) then display the first + // form step + if (!$this->formState->isFormSubmitted() || !$this->canProcessFormSubmission()) { $this->currentPage = $this->formDefinition->getPageByIndex(0); if (!$this->currentPage->isEnabled()) { @@ -478,6 +539,31 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess return $this->lastDisplayedPage === null; } + /** + * @return bool + */ + protected function isPostRequest(): bool + { + return $this->getRequest()->getMethod() === 'POST'; + } + + /** + * Determine whether the surrounding content object is cached. + * If no surrounding content object can be found (which would be strange) + * we assume a cached request for safety which means that an empty form + * will be rendered. + * + * @return bool + */ + protected function isRenderedCached(): bool + { + $contentObject = $this->configurationManager->getContentObject(); + return $contentObject === null + ? true + // @todo this does not work when rendering a cached `FLUIDTEMPLATE` (not nested in `COA_INT`) + : $contentObject->getUserObjectType() === ContentObjectRenderer::OBJECTTYPE_USER; + } + /** * Runs through all validations */ @@ -709,6 +795,29 @@ class FormRuntime implements RootRenderableInterface, \ArrayAccess return $this->response; } + /** + * Only process values if there is a post request and if the + * surrounding content object is uncached. + * Is this not the case, all possible submitted values will be discarded + * and the first form step will be shown with an empty form state. + * + * @return bool + * @internal + */ + public function canProcessFormSubmission(): bool + { + return $this->isPostRequest() && !$this->isRenderedCached(); + } + + /** + * @return FormSession|null + * @internal + */ + public function getFormSession(): ?FormSession + { + return $this->formSession; + } + /** * Returns the currently selected page * diff --git a/typo3/sysext/form/Classes/Domain/Runtime/FormRuntime/FormSession.php b/typo3/sysext/form/Classes/Domain/Runtime/FormRuntime/FormSession.php new file mode 100644 index 000000000000..73fe89bfbdc0 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Runtime/FormRuntime/FormSession.php @@ -0,0 +1,94 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Form\Domain\Runtime\FormRuntime; + +use TYPO3\CMS\Core\Crypto\Random; +use TYPO3\CMS\Core\Error\Http\BadRequestException; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Security\Cryptography\HashService; +use TYPO3\CMS\Extbase\Security\Exception\InvalidArgumentForHashGenerationException; +use TYPO3\CMS\Extbase\Security\Exception\InvalidHashException; + +/** + * @internal + */ +class FormSession +{ + protected $identifier; + + /** + * Factory to create the form session from the current state + * + * @param string|null $authenticatedIdentifier + * @throws BadRequestException + */ + public function __construct(string $authenticatedIdentifier = null) + { + if ($authenticatedIdentifier === null) { + $this->identifier = $this->generateIdentifier(); + } else { + $this->identifier = $this->validateIdentifier($authenticatedIdentifier); + } + } + + /** + * @return string + * @internal + */ + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * Consumed by TYPO3\CMS\Form\ViewHelpers\FormViewHelper + * + * @return string + * @internal + */ + public function getAuthenticatedIdentifier(): string + { + return GeneralUtility::makeInstance(HashService::class) + // restrict string expansion by adding some char ('|') + ->appendHmac($this->identifier . '|'); + } + + /** + * @return string + */ + protected function generateIdentifier(): string + { + return GeneralUtility::makeInstance(Random::class)->generateRandomHexString(40); + } + + /** + * @param string $authenticatedIdentifier + * @return string + * @throws BadRequestException + */ + protected function validateIdentifier(string $authenticatedIdentifier): string + { + try { + $identifier = GeneralUtility::makeInstance(HashService::class) + ->validateAndStripHmac($authenticatedIdentifier); + return rtrim($identifier, '|'); + } catch (InvalidHashException | InvalidArgumentForHashGenerationException $e) { + throw new BadRequestException('The HMAC of the form session could not be validated.', 1613300274); + } + } +} diff --git a/typo3/sysext/form/Classes/Domain/Runtime/FormRuntime/Lifecycle/AfterFormStateInitializedInterface.php b/typo3/sysext/form/Classes/Domain/Runtime/FormRuntime/Lifecycle/AfterFormStateInitializedInterface.php new file mode 100644 index 000000000000..78972e584221 --- /dev/null +++ b/typo3/sysext/form/Classes/Domain/Runtime/FormRuntime/Lifecycle/AfterFormStateInitializedInterface.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Form\Domain\Runtime\FormRuntime\Lifecycle; + +use TYPO3\CMS\Form\Domain\Runtime\FormRuntime; + +/** + * Event is triggered with current form state and form session, which is + * not the case with e.g. `afterBuildingFinished`. Can be used to further + * enrich components with runtime state. + * @internal + */ +interface AfterFormStateInitializedInterface +{ + /** + * @param FormRuntime $formRuntime holding current form state and static form definition + */ + public function afterFormStateInitialized(FormRuntime $formRuntime): void; +} diff --git a/typo3/sysext/form/Classes/Mvc/Property/PropertyMappingConfiguration.php b/typo3/sysext/form/Classes/Mvc/Property/PropertyMappingConfiguration.php index 9b5cc59cc756..571a8ae20c80 100644 --- a/typo3/sysext/form/Classes/Mvc/Property/PropertyMappingConfiguration.php +++ b/typo3/sysext/form/Classes/Mvc/Property/PropertyMappingConfiguration.php @@ -25,6 +25,8 @@ use TYPO3\CMS\Extbase\Property\TypeConverter\DateTimeConverter; use TYPO3\CMS\Extbase\Validation\Validator\NotEmptyValidator; use TYPO3\CMS\Form\Domain\Model\FormElements\FileUpload; use TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface; +use TYPO3\CMS\Form\Domain\Runtime\FormRuntime; +use TYPO3\CMS\Form\Domain\Runtime\FormRuntime\Lifecycle\AfterFormStateInitializedInterface; use TYPO3\CMS\Form\Mvc\Property\TypeConverter\UploadedFileReferenceConverter; use TYPO3\CMS\Form\Mvc\Validation\MimeTypeValidator; @@ -32,13 +34,16 @@ use TYPO3\CMS\Form\Mvc\Validation\MimeTypeValidator; * Scope: frontend * @internal */ -class PropertyMappingConfiguration +class PropertyMappingConfiguration implements AfterFormStateInitializedInterface { /** * This hook is called for each form element after the class * TYPO3\CMS\Form\Domain\Factory\ArrayFormFactory has built the entire form. * + * It is invoked after the static form definition is ready, but without knowing + * about the individual state organized in `FormRuntime` and `FormState`. + * * @param RenderableInterface $renderable * @internal */ @@ -53,12 +58,17 @@ class PropertyMappingConfiguration // * Setup the storage: // If the property "saveToFileMount" exist for this element it will be used. // If this file mount or the property "saveToFileMount" does not exist - // the folder in which the form definition lies (persistence identifier) will be used. - // If the form is generated programmatically and therefore no - // persistence identifier exist the default storage "1:/user_upload/" will be used. + // the default storage "1:/user_uploads/" will be used. Uploads are placed + // in a dedicated sub-folder (e.g. ".../form_<40-chars-hash>/actual.file"). + /** @var UploadedFileReferenceConverter $typeConverter */ + $typeConverter = GeneralUtility::makeInstance(ObjectManager::class) + ->get(UploadedFileReferenceConverter::class); /** @var \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration $propertyMappingConfiguration */ - $propertyMappingConfiguration = $renderable->getRootForm()->getProcessingRule($renderable->getIdentifier())->getPropertyMappingConfiguration(); + $propertyMappingConfiguration = $renderable->getRootForm() + ->getProcessingRule($renderable->getIdentifier()) + ->getPropertyMappingConfiguration() + ->setTypeConverter($typeConverter); $allowedMimeTypes = []; $validators = []; @@ -88,6 +98,7 @@ class PropertyMappingConfiguration if ($this->checkSaveFileMountAccess($saveToFileMountIdentifier)) { $uploadConfiguration[UploadedFileReferenceConverter::CONFIGURATION_UPLOAD_FOLDER] = $saveToFileMountIdentifier; } else { + // @todo Why should uploaded files be stored to the same directory as the *.form.yaml definitions? $persistenceIdentifier = $renderable->getRootForm()->getPersistenceIdentifier(); if (!empty($persistenceIdentifier)) { $pathinfo = PathUtility::pathinfo($persistenceIdentifier); @@ -97,7 +108,6 @@ class PropertyMappingConfiguration } } } - $propertyMappingConfiguration->setTypeConverterOptions(UploadedFileReferenceConverter::class, $uploadConfiguration); return; } @@ -138,4 +148,46 @@ class PropertyMappingConfiguration return false; } } + + /** + * @param FormRuntime $formRuntime holding current form state and static form definition + */ + public function afterFormStateInitialized(FormRuntime $formRuntime): void + { + foreach ($formRuntime->getFormDefinition()->getRenderablesRecursively() as $renderable) { + $this->adjustPropertyMappingForFileUploadsAtRuntime($formRuntime, $renderable); + } + } + + /** + * If the form runtime is able to process form submissions + * (determined by $formRuntime->canProcessFormSubmission()) then a + * 'form session' is available. + * This form session identifier will be used to deriving storage sub-folders + * for the file uploads. + * This is done by setting `UploadedFileReferenceConverter::CONFIGURATION_UPLOAD_SEED` + * type converter option. + * + * @param FormRuntime $formRuntime + * @param RenderableInterface $renderable + */ + protected function adjustPropertyMappingForFileUploadsAtRuntime( + FormRuntime $formRuntime, + RenderableInterface $renderable + ): void { + if (!$renderable instanceof FileUpload + || $formRuntime->getFormSession() === null + || !$formRuntime->canProcessFormSubmission() + ) { + return; + } + $renderable->getRootForm() + ->getProcessingRule($renderable->getIdentifier()) + ->getPropertyMappingConfiguration() + ->setTypeConverterOption( + UploadedFileReferenceConverter::class, + UploadedFileReferenceConverter::CONFIGURATION_UPLOAD_SEED, + $formRuntime->getFormSession()->getIdentifier() + ); + } } diff --git a/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/PseudoFile.php b/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/PseudoFile.php new file mode 100644 index 000000000000..89284cd6417e --- /dev/null +++ b/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/PseudoFile.php @@ -0,0 +1,106 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Form\Mvc\Property\TypeConverter; + +use TYPO3\CMS\Core\Type\File\FileInfo; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Form\Mvc\Property\Exception\TypeConverterException; + +/** + * Use in `UploadedFileReferenceConverter` handling file uploads. + * `PseudoFile` and `PseudoFileReference` are independent and not associated. + * @internal + */ +class PseudoFile +{ + /** + * @var \SplFileInfo + */ + protected $nameFileInfo; + + /** + * @var FileInfo + */ + protected $payloadFileInfo; + + /** + * @var string + */ + protected $payloadFilePath; + + /** + * see https://www.php.net/manual/en/features.file-upload.post-method.php + * + * @param array $uploadInfo as in $_FILES + * @throws TypeConverterException + */ + public function __construct(array $uploadInfo) + { + if (!isset($uploadInfo['tmp_name']) || !isset($uploadInfo['name'])) { + throw new TypeConverterException( + 'Could not determine uploaded file', + 1602103603 + ); + } + $this->nameFileInfo = new \SplFileInfo($uploadInfo['name']); + $this->payloadFilePath = $uploadInfo['tmp_name']; + $this->payloadFileInfo = GeneralUtility::makeInstance(FileInfo::class, $uploadInfo['tmp_name']); + } + + public function getName(): string + { + return $this->nameFileInfo->getBasename(); + } + + public function getNameWithoutExtension(): string + { + // `image...png` + return rtrim( + $this->nameFileInfo->getBasename($this->getExtension()), + '.' + ); + } + + public function getExtension(): string + { + return $this->nameFileInfo->getExtension(); + } + + public function getSize(): ?int + { + // returns `null` in case size is empty (includes `0`) + // @see \TYPO3\CMS\Core\Resource\AbstractFile::getSize() + return $this->payloadFileInfo->getSize() ?: null; + } + + public function getMimeType(): ?string + { + $mimeType = $this->payloadFileInfo->getMimeType(); + return is_string($mimeType) ? $mimeType : null; + } + + public function getContents(): string + { + return file_get_contents($this->payloadFilePath); + } + + public function getSha1(): string + { + return sha1_file($this->payloadFilePath); + } +} diff --git a/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/PseudoFileReference.php b/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/PseudoFileReference.php new file mode 100644 index 000000000000..bdcdb61bb181 --- /dev/null +++ b/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/PseudoFileReference.php @@ -0,0 +1,86 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Form\Mvc\Property\TypeConverter; + +use TYPO3\CMS\Core\Resource\ResourceFactory; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Domain\Model\FileReference; + +/** + * Use in `UploadedFileReferenceConverter` handling file uploads. + * `PseudoFile` and `PseudoFileReference` are independent and not associated. + * + * This facade hides (potential) internal properties from being exposed to the + * public during serialization. `sys_file_reference.uid` or `sys_file.uid` are + * the only aspects used externally. In case of missing integrity checks during + * deserialization, both properties would allow direct object reference (IDOR). + * + * @internal + */ +class PseudoFileReference extends FileReference +{ + /** + * @var int|null + */ + private $_uid; + + /** + * @var int|null + */ + private $_uidLocal; + + public function __sleep(): array + { + // in case this is a persisted file reference, use it directly as reference + // as a consequence, in-memory changes are lost and have to be persisted first + // (it seems that in ext:form a `FileReference` was never persisted) + if ($this->uid > 0) { + $this->_uid = (int)$this->uid; + return ['_uid']; + } + if ($this->getOriginalResource()->getUid() > 0) { + $this->_uid = (int)$this->getOriginalResource()->getUid(); + return ['_uid']; + } + // in case this is a transient file reference, just expose the associated `sys_file.uid` + // (based on previous comments, this is the most probably case in ext:form) + $this->_uidLocal = (int)$this->getOriginalResource()->getOriginalFile()->getUid(); + return ['_uidLocal']; + } + + public function __wakeup(): void + { + $factory = GeneralUtility::makeInstance(ResourceFactory::class); + if ($this->_uid > 0) { + $this->originalResource = $factory->getFileReferenceObject($this->_uid); + } elseif ($this->_uidLocal > 0) { + $this->originalResource = $factory->createFileReferenceObject([ + 'uid_local' => $this->_uidLocal, + 'uid_foreign' => 0, + 'uid' => 0, + 'crop' => null, + ]); + } else { + throw new \LogicException( + sprintf('Cannot unserialize %s', static::class), + 1613216548 + ); + } + unset($this->_uid, $this->_uidLocal); + } +} diff --git a/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/UploadedFileReferenceConverter.php b/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/UploadedFileReferenceConverter.php index 16f64c58d9fe..2539b7109949 100644 --- a/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/UploadedFileReferenceConverter.php +++ b/typo3/sysext/form/Classes/Mvc/Property/TypeConverter/UploadedFileReferenceConverter.php @@ -17,15 +17,17 @@ declare(strict_types=1); namespace TYPO3\CMS\Form\Mvc\Property\TypeConverter; +use TYPO3\CMS\Core\Crypto\Random; use TYPO3\CMS\Core\Log\LogManager; +use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException; use TYPO3\CMS\Core\Resource\File as File; use TYPO3\CMS\Core\Resource\FileReference as CoreFileReference; +use TYPO3\CMS\Core\Resource\Folder; use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Resource\Security\FileNameValidator; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\StringUtility; use TYPO3\CMS\Extbase\Domain\Model\AbstractFileFolder; -use TYPO3\CMS\Extbase\Domain\Model\FileReference as ExtbaseFileReference; use TYPO3\CMS\Extbase\Error\Error; use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface; use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface; @@ -34,6 +36,7 @@ use TYPO3\CMS\Extbase\Security\Cryptography\HashService; use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator; use TYPO3\CMS\Form\Mvc\Property\Exception\TypeConverterException; use TYPO3\CMS\Form\Service\TranslationService; +use TYPO3\CMS\Form\Slot\ResourcePublicationSlot; /** * Class UploadedFileReferenceConverter @@ -54,6 +57,11 @@ class UploadedFileReferenceConverter extends AbstractTypeConverter */ const CONFIGURATION_UPLOAD_CONFLICT_MODE = 2; + /** + * Random seed to be used for deriving storage sub-folders. + */ + const CONFIGURATION_UPLOAD_SEED = 3; + /** * Validator for file types */ @@ -79,7 +87,7 @@ class UploadedFileReferenceConverter extends AbstractTypeConverter /** * @var string */ - protected $targetType = ExtbaseFileReference::class; + protected $targetType = PseudoFileReference::class; /** * Take precedence over the available FileReferenceConverter @@ -89,7 +97,7 @@ class UploadedFileReferenceConverter extends AbstractTypeConverter protected $priority = 12; /** - * @var \TYPO3\CMS\Core\Resource\FileInterface[] + * @var PseudoFileReference[] */ protected $convertedResources = []; @@ -148,6 +156,8 @@ class UploadedFileReferenceConverter extends AbstractTypeConverter */ public function convertFrom($source, $targetType, array $convertedChildProperties = [], PropertyMappingConfigurationInterface $configuration = null) { + // slot/listener using `FileDumpController` instead of direct public URL in (later) rendering process + $resourcePublicationSlot = GeneralUtility::makeInstance(ResourcePublicationSlot::class); if (!isset($source['error']) || $source['error'] === \UPLOAD_ERR_NO_FILE) { if (isset($source['submittedFile']['resourcePointer'])) { try { @@ -156,12 +166,15 @@ class UploadedFileReferenceConverter extends AbstractTypeConverter $resourcePointer = $this->hashService->validateAndStripHmac($source['submittedFile']['resourcePointer']); if (strpos($resourcePointer, 'file:') === 0) { $fileUid = (int)substr($resourcePointer, 5); - return $this->createFileReferenceFromFalFileObject($this->resourceFactory->getFileObject($fileUid)); + $resource = $this->createFileReferenceFromFalFileObject($this->resourceFactory->getFileObject($fileUid)); + } else { + $resource = $this->createFileReferenceFromFalFileReferenceObject( + $this->resourceFactory->getFileReferenceObject($resourcePointer), + (int)$resourcePointer + ); } - return $this->createFileReferenceFromFalFileReferenceObject( - $this->resourceFactory->getFileReferenceObject($resourcePointer), - (int)$resourcePointer - ); + $resourcePublicationSlot->add($resource->getOriginalResource()->getOriginalFile()); + return $resource; } catch (\InvalidArgumentException $e) { // Nothing to do. No file is uploaded and resource pointer is invalid. Discard! } @@ -183,6 +196,7 @@ class UploadedFileReferenceConverter extends AbstractTypeConverter try { $resource = $this->importUploadedResource($source, $configuration); + $resourcePublicationSlot->add($resource->getOriginalResource()->getOriginalFile()); } catch (TypeConverterException $e) { return $e->getError(); } catch (\Exception $e) { @@ -198,35 +212,42 @@ class UploadedFileReferenceConverter extends AbstractTypeConverter * * @param array $uploadInfo * @param PropertyMappingConfigurationInterface $configuration - * @return ExtbaseFileReference + * @return PseudoFileReference */ protected function importUploadedResource( array $uploadInfo, PropertyMappingConfigurationInterface $configuration - ): ExtbaseFileReference { + ): PseudoFileReference { if (!GeneralUtility::makeInstance(FileNameValidator::class)->isValid($uploadInfo['name'])) { throw new TypeConverterException('Uploading files with PHP file extensions is not allowed!', 1471710357); } - + // `CONFIGURATION_UPLOAD_SEED` is expected to be defined + // if it's not given any random seed is generated, instead of throwing an exception + $seed = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_UPLOAD_SEED) + ?: GeneralUtility::makeInstance(Random::class)->generateRandomHexString(40); $uploadFolderId = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_UPLOAD_FOLDER) ?: $this->defaultUploadFolder; $conflictMode = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_UPLOAD_CONFLICT_MODE) ?: $this->defaultConflictMode; - - $uploadFolder = $this->resourceFactory->retrieveFileOrFolderObject($uploadFolderId); - $uploadedFile = $uploadFolder->addUploadedFile($uploadInfo, $conflictMode); + $pseudoFile = GeneralUtility::makeInstance(PseudoFile::class, $uploadInfo); $validators = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_FILE_VALIDATORS); if (is_array($validators)) { foreach ($validators as $validator) { if ($validator instanceof AbstractValidator) { - $validationResult = $validator->validate($uploadedFile); + $validationResult = $validator->validate($pseudoFile); if ($validationResult->hasErrors()) { - $uploadedFile->getStorage()->deleteFile($uploadedFile); throw TypeConverterException::fromError($validationResult->getErrors()[0]); } } } } + $uploadFolder = $this->provideUploadFolder($uploadFolderId); + // current folder name, derived from public random seed (`formSession`) + $currentName = 'form_' . GeneralUtility::hmac($seed, self::class); + $uploadFolder = $this->provideTargetFolder($uploadFolder, $currentName); + // sub-folder in $uploadFolder with 160 bit of derived entropy (.../form_<40-chars-hash>/actual.file) + $uploadedFile = $uploadFolder->addUploadedFile($uploadInfo, $conflictMode); + $resourcePointer = isset($uploadInfo['submittedFile']['resourcePointer']) && strpos($uploadInfo['submittedFile']['resourcePointer'], 'file:') === false ? (int)$this->hashService->validateAndStripHmac($uploadInfo['submittedFile']['resourcePointer']) : null; @@ -239,12 +260,12 @@ class UploadedFileReferenceConverter extends AbstractTypeConverter /** * @param File $file * @param int $resourcePointer - * @return ExtbaseFileReference + * @return PseudoFileReference */ protected function createFileReferenceFromFalFileObject( File $file, int $resourcePointer = null - ): ExtbaseFileReference { + ): PseudoFileReference { $fileReference = $this->resourceFactory->createFileReferenceObject( [ 'uid_local' => $file->getUid(), @@ -263,16 +284,16 @@ class UploadedFileReferenceConverter extends AbstractTypeConverter * * @param CoreFileReference $falFileReference * @param int $resourcePointer - * @return ExtbaseFileReference + * @return PseudoFileReference */ protected function createFileReferenceFromFalFileReferenceObject( CoreFileReference $falFileReference, int $resourcePointer = null - ): ExtbaseFileReference { + ): PseudoFileReference { if ($resourcePointer === null) { - $fileReference = $this->objectManager->get(ExtbaseFileReference::class); + $fileReference = $this->objectManager->get(PseudoFileReference::class); } else { - $fileReference = $this->persistenceManager->getObjectByIdentifier($resourcePointer, ExtbaseFileReference::class, false); + $fileReference = $this->persistenceManager->getObjectByIdentifier($resourcePointer, PseudoFileReference::class, false); } $fileReference->setOriginalResource($falFileReference); @@ -316,4 +337,51 @@ class UploadedFileReferenceConverter extends AbstractTypeConverter return TranslationService::getInstance()->translate('upload.error.150530348', null, 'EXT:form/Resources/Private/Language/locallang.xlf'); } } + + /** + * Ensures that upload folder exists, creates it if it does not. + * + * @param string $uploadFolderIdentifier + * @return Folder + */ + protected function provideUploadFolder(string $uploadFolderIdentifier): Folder + { + try { + return $this->resourceFactory->getFolderObjectFromCombinedIdentifier($uploadFolderIdentifier); + } catch (FolderDoesNotExistException $exception) { + [$storageId, $storagePath] = explode(':', $uploadFolderIdentifier, 2); + $storage = $this->resourceFactory->getStorageObject($storageId); + $folderNames = GeneralUtility::trimExplode('/', $storagePath, true); + $uploadFolder = $this->provideTargetFolder($storage->getRootLevelFolder(), ...$folderNames); + $this->provideFolderInitialization($uploadFolder); + return $uploadFolder; + } + } + + /** + * Ensures that particular target folder exists, creates it if it does not. + * + * @param Folder $parentFolder + * @param string $folderName + * @return Folder + */ + protected function provideTargetFolder(Folder $parentFolder, string $folderName): Folder + { + return $parentFolder->hasFolder($folderName) + ? $parentFolder->getSubfolder($folderName) + : $parentFolder->createFolder($folderName); + } + + /** + * Creates empty index.html file to avoid directory indexing, + * in case it does not exist yet. + * + * @param Folder $parentFolder + */ + protected function provideFolderInitialization(Folder $parentFolder): void + { + if (!$parentFolder->hasFile('index.html')) { + $parentFolder->createFile('index.html'); + } + } } diff --git a/typo3/sysext/form/Classes/Mvc/Validation/FileSizeValidator.php b/typo3/sysext/form/Classes/Mvc/Validation/FileSizeValidator.php index b05db8897e4d..1b248a1f72d0 100644 --- a/typo3/sysext/form/Classes/Mvc/Validation/FileSizeValidator.php +++ b/typo3/sysext/form/Classes/Mvc/Validation/FileSizeValidator.php @@ -21,6 +21,7 @@ use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Domain\Model\FileReference; use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator; +use TYPO3\CMS\Form\Mvc\Property\TypeConverter\PseudoFile; use TYPO3\CMS\Form\Mvc\Validation\Exception\InvalidValidationOptionsException; /** @@ -42,14 +43,18 @@ class FileSizeValidator extends AbstractValidator /** * The given value is valid * - * @param FileReference|File $resource + * @param FileReference|File|PseudoFile $resource */ public function isValid($resource) { $this->validateOptions(); if ($resource instanceof FileReference) { - $resource = $resource->getOriginalResource(); - } elseif (!$resource instanceof File) { + $fileSize = $resource->getOriginalResource()->getSize(); + } elseif ($resource instanceof File) { + $fileSize = $resource->getSize(); + } elseif ($resource instanceof PseudoFile) { + $fileSize = $resource->getSize(); + } else { $this->addError( $this->translateErrorMessage( 'validation.error.1505303626', @@ -60,7 +65,6 @@ class FileSizeValidator extends AbstractValidator return; } - $fileSize = $resource->getSize(); $minFileSize = GeneralUtility::getBytesFromSizeMeasurement($this->options['minimum']); $maxFileSize = GeneralUtility::getBytesFromSizeMeasurement($this->options['maximum']); diff --git a/typo3/sysext/form/Classes/Mvc/Validation/MimeTypeValidator.php b/typo3/sysext/form/Classes/Mvc/Validation/MimeTypeValidator.php index e2a5496dbcfa..4bd34b257de1 100644 --- a/typo3/sysext/form/Classes/Mvc/Validation/MimeTypeValidator.php +++ b/typo3/sysext/form/Classes/Mvc/Validation/MimeTypeValidator.php @@ -18,8 +18,10 @@ declare(strict_types=1); namespace TYPO3\CMS\Form\Mvc\Validation; use TYPO3\CMS\Core\Resource\File; +use TYPO3\CMS\Core\Resource\MimeTypeDetector; use TYPO3\CMS\Extbase\Domain\Model\FileReference; use TYPO3\CMS\Extbase\Validation\Validator\AbstractValidator; +use TYPO3\CMS\Form\Mvc\Property\TypeConverter\PseudoFile; use TYPO3\CMS\Form\Mvc\Validation\Exception\InvalidValidationOptionsException; /** @@ -42,15 +44,22 @@ class MimeTypeValidator extends AbstractValidator * * Note: a value of NULL or empty string ('') is considered valid * - * @param FileReference|File $resource The resource that should be validated + * @param FileReference|File|PseudoFile $resource The resource that should be validated */ public function isValid($resource) { $this->validateOptions(); if ($resource instanceof FileReference) { - $resource = $resource->getOriginalResource(); - } elseif (!$resource instanceof File) { + $mimeType = $resource->getOriginalResource()->getMimeType(); + $fileExtension = $resource->getOriginalResource()->getExtension(); + } elseif ($resource instanceof File) { + $mimeType = $resource->getMimeType(); + $fileExtension = $resource->getExtension(); + } elseif ($resource instanceof PseudoFile) { + $mimeType = $resource->getMimeType(); + $fileExtension = $resource->getExtension(); + } else { $this->addError( $this->translateErrorMessage( 'validation.error.1471708997', @@ -62,16 +71,33 @@ class MimeTypeValidator extends AbstractValidator } $allowedMimeTypes = $this->options['allowedMimeTypes']; - if (!in_array($resource->getMimeType(), $allowedMimeTypes, true)) { + if (!in_array($mimeType, $allowedMimeTypes, true)) { $this->addError( $this->translateErrorMessage( 'validation.error.1471708998', 'form', - [$resource->getMimeType()] + [$mimeType] ) ?? '', 1471708998, - [$resource->getMimeType()] + [$mimeType] ); + } else { + // The mime-type which was detected by FAL matches, but the file name does not match. + // Example: myfile.txt is actually a PDF file (defined by mime-type), but .txt is not associated + // for application/pdf, so this is not valid. The file extension of the uploaded file must match + // the mime-type for this file. + $assumedMimesTypeOfFileExtension = (new MimeTypeDetector())->getMimeTypesForFileExtension($fileExtension); + if (empty(array_intersect($allowedMimeTypes, $assumedMimesTypeOfFileExtension))) { + $this->addError( + $this->translateErrorMessage( + 'validation.error.1613126216', + 'form', + [$fileExtension] + ) ?? '', + 1613126216, + [$fileExtension] + ); + } } } diff --git a/typo3/sysext/form/Classes/Slot/ResourcePublicationSlot.php b/typo3/sysext/form/Classes/Slot/ResourcePublicationSlot.php new file mode 100644 index 000000000000..748133913083 --- /dev/null +++ b/typo3/sysext/form/Classes/Slot/ResourcePublicationSlot.php @@ -0,0 +1,83 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Form\Slot; + +use TYPO3\CMS\Core\Core\Environment; +use TYPO3\CMS\Core\Resource\Event\GeneratePublicUrlForResourceEvent; +use TYPO3\CMS\Core\Resource\File; +use TYPO3\CMS\Core\Resource\FileInterface; +use TYPO3\CMS\Core\Resource\ProcessedFile; +use TYPO3\CMS\Core\Resource\ResourceInterface; +use TYPO3\CMS\Core\SingletonInterface; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\PathUtility; + +/** + * A PSR-14 event listener for using FAL resources in public (frontend) + * + * @internal will be renamed at some point. + */ +class ResourcePublicationSlot implements SingletonInterface +{ + /** + * @var list<string> + */ + protected $fileIdentifiers = []; + + public function getPublicUrl(GeneratePublicUrlForResourceEvent $event): void + { + $resource = $event->getResource(); + if (!$resource instanceof FileInterface + || !$this->has($resource) + || $event->getStorage()->getDriverType() !== 'Local' + ) { + return; + } + $event->setPublicUrl($this->getStreamUrl($event->getResource())); + } + + public function add(FileInterface $resource): void + { + if ($this->has($resource)) { + return; + } + $this->fileIdentifiers[] = $resource->getIdentifier(); + } + + public function has(FileInterface $resource): bool + { + return in_array($resource->getIdentifier(), $this->fileIdentifiers, true); + } + + protected function getStreamUrl(ResourceInterface $resource): string + { + $queryParameterArray = ['eID' => 'dumpFile', 't' => '']; + if ($resource instanceof File) { + $queryParameterArray['f'] = $resource->getUid(); + $queryParameterArray['t'] = 'f'; + } elseif ($resource instanceof ProcessedFile) { + $queryParameterArray['p'] = $resource->getUid(); + $queryParameterArray['t'] = 'p'; + } + + $queryParameterArray['token'] = GeneralUtility::hmac(implode('|', $queryParameterArray), 'resourceStorageDumpFile'); + $publicUrl = GeneralUtility::locationHeaderUrl(PathUtility::getAbsoluteWebPath(Environment::getPublicPath() . '/index.php')); + $publicUrl .= '?' . http_build_query($queryParameterArray, '', '&', PHP_QUERY_RFC3986); + return $publicUrl; + } +} diff --git a/typo3/sysext/form/Classes/ViewHelpers/FormViewHelper.php b/typo3/sysext/form/Classes/ViewHelpers/FormViewHelper.php index 5edd62a41cf4..b92488c419d6 100644 --- a/typo3/sysext/form/Classes/ViewHelpers/FormViewHelper.php +++ b/typo3/sysext/form/Classes/ViewHelpers/FormViewHelper.php @@ -23,6 +23,7 @@ namespace TYPO3\CMS\Form\ViewHelpers; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper as FluidFormViewHelper; +use TYPO3\CMS\Form\Domain\Runtime\FormRuntime; use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder; /** @@ -40,14 +41,38 @@ class FormViewHelper extends FluidFormViewHelper * @return string Hidden fields with referrer information */ protected function renderHiddenReferrerFields() + { + $formRuntime = $this->getFormRuntime(); + $prefix = $this->prefixFieldName($this->getFormObjectName()); + + $markup = $this->createHiddenInputElement( + $prefix . '[__state]', + $this->hashService->appendHmac( + base64_encode(serialize($formRuntime->getFormState())) + ) + ); + + // ONLY assign `__session` if form is performing (uncached) + if ($formRuntime->canProcessFormSubmission() && $formRuntime->getFormSession() !== null) { + $markup .= $this->createHiddenInputElement( + $prefix . '[__session]', + $formRuntime->getFormSession()->getAuthenticatedIdentifier() + ); + } + return $markup; + } + + /** + * @param string $name + * @param string $value + * @return string + */ + protected function createHiddenInputElement(string $name, string $value): string { $tagBuilder = GeneralUtility::makeInstance(TagBuilder::class, 'input'); $tagBuilder->addAttribute('type', 'hidden'); - $stateName = $this->prefixFieldName($this->arguments['object']->getFormDefinition()->getIdentifier()) . '[__state]'; - $tagBuilder->addAttribute('name', $stateName); - - $serializedFormState = base64_encode(serialize($this->arguments['object']->getFormState())); - $tagBuilder->addAttribute('value', $this->hashService->appendHmac($serializedFormState)); + $tagBuilder->addAttribute('name', $name); + $tagBuilder->addAttribute('value', $value); return $tagBuilder->render(); } @@ -59,6 +84,11 @@ class FormViewHelper extends FluidFormViewHelper */ protected function getFormObjectName() { - return $this->arguments['object']->getFormDefinition()->getIdentifier(); + return $this->getFormRuntime()->getFormDefinition()->getIdentifier(); + } + + protected function getFormRuntime(): FormRuntime + { + return $this->arguments['object']; } } diff --git a/typo3/sysext/form/Configuration/Services.yaml b/typo3/sysext/form/Configuration/Services.yaml index f90443d7f76e..1cdce1dfd1cb 100644 --- a/typo3/sysext/form/Configuration/Services.yaml +++ b/typo3/sysext/form/Configuration/Services.yaml @@ -8,6 +8,13 @@ services: resource: '../Classes/*' exclude: '../Classes/{Domain/Model}' + TYPO3\CMS\Form\Slot\ResourcePublicationSlot: + tags: + - name: event.listener + identifier: 'form-framework/resource-getPublicUrl' + method: 'getPublicUrl' + event: TYPO3\CMS\Core\Resource\Event\GeneratePublicUrlForResourceEvent + TYPO3\CMS\Form\Slot\FilePersistenceSlot: tags: - name: event.listener diff --git a/typo3/sysext/form/Resources/Private/Language/locallang.xlf b/typo3/sysext/form/Resources/Private/Language/locallang.xlf index 8b6a25fdb31b..562e13238d60 100644 --- a/typo3/sysext/form/Resources/Private/Language/locallang.xlf +++ b/typo3/sysext/form/Resources/Private/Language/locallang.xlf @@ -79,6 +79,9 @@ <trans-unit id="validation.error.1471708998" resname="validation.error.1471708998" xml:space="preserve"> <source>You entered an incorrect media type, "%s" is not allowed for this file. Please refer to the description of this field.</source> </trans-unit> + <trans-unit id="validation.error.1613126216" resname="validation.error.1613126216" xml:space="preserve"> + <source>The file extension provided "%s" does not match to expected media types. Please refer to the description of this field.</source> + </trans-unit> <trans-unit id="validation.error.1476396435" resname="validation.error.1476396435" xml:space="preserve"> <source>You must NOT fill this field.</source> </trans-unit> diff --git a/typo3/sysext/form/Tests/Functional/Mvc/Validation/MimeTypeValidatorTest.php b/typo3/sysext/form/Tests/Functional/Mvc/Validation/MimeTypeValidatorTest.php new file mode 100644 index 000000000000..aeac8ea83cbc --- /dev/null +++ b/typo3/sysext/form/Tests/Functional/Mvc/Validation/MimeTypeValidatorTest.php @@ -0,0 +1,143 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Form\Tests\Functional\Mvc\Validation; + +use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; +use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Error\Error; +use TYPO3\CMS\Form\Mvc\Property\TypeConverter\PseudoFile; +use TYPO3\CMS\Form\Mvc\Validation\MimeTypeValidator; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; + +class MimeTypeValidatorTest extends FunctionalTestCase +{ + /** + * @var string[] + */ + protected $coreExtensionsToLoad = [ + 'form', + ]; + + /** + * @var array<string, string> + */ + private $files = [ + 'file.exe' => "MZ\x90\x00\x03\x00", + 'file.zip' => "PK\x03\x04", + 'file.jpg' => "\xFF\xD8\xFF\xDB", + 'file.gif' => 'GIF87a', + 'file.pdf' => '%PDF-', + ]; + + /** + * @var vfsStreamDirectory + */ + private $tmp; + + protected function setUp(): void + { + parent::setUp(); + $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageService::class); + $this->tmp = vfsStream::setup('tmp', null, $this->files); + } + + protected function tearDown(): void + { + unset($GLOBALS['LANG'], $this->tmp); + parent::tearDown(); + } + + public function dataProvider(): array + { + // error-codes + // + 1471708998: mime-type not allowed + // + 1613126216: mime-type to file-extension mismatch + return [ + 'submitted gif as upload.gif' => [ + [ + 'tmp_name' => 'vfs://tmp/file.gif', + 'name' => 'upload.gif', + 'type' => 'does/not-matter', + ], + ['image/gif'], + ], + 'submitted jpg as upload.jpg' => [ + [ + 'tmp_name' => 'vfs://tmp/file.jpg', + 'name' => 'upload.jpg', + 'type' => 'does/not-matter', + ], + ['image/jpeg'], + ], + 'submitted pdf as upload.pdf' => [ + [ + 'tmp_name' => 'vfs://tmp/file.pdf', + 'name' => 'upload.pdf', + 'type' => 'does/not-matter', + ], + ['application/pdf'], + ], + 'submitted exe as upload.exe' => [ + [ + 'tmp_name' => 'vfs://tmp/file.exe', + 'name' => 'upload.exe', + 'type' => 'does/not-matter', + ], // upload data (as in $_FILES) + ['image/gif'], // allowed mime-types + [1471708998], // expected error-codes + ], + 'submitted gif as upload.exe' => [ + [ + 'tmp_name' => 'vfs://tmp/file.gif', + 'name' => 'upload.exe', + 'type' => 'does/not-matter', + ], // upload data (as in $_FILES) + ['image/gif'], // allowed mime-types + [1613126216], // expected error-codes + ], + ]; + } + + /** + * @param array<string, int|string> $uploadData + * @param List<string> $allowedMimeTypes + * @param List<int> $expectedErrorCodes + * + * @test + * @dataProvider dataProvider + */ + public function someTest(array $uploadData, array $allowedMimeTypes, array $expectedErrorCodes = []): void + { + $uploadData['error'] = \UPLOAD_ERR_OK; + $uploadData['size'] = filesize($uploadData['tmp_name']); + + $validator = new MimeTypeValidator(['allowedMimeTypes' => $allowedMimeTypes]); + + $resource = new PseudoFile($uploadData); + $result = $validator->validate($resource); + $errorCodes = array_map([$this, 'resolveErrorCode'], $result->getErrors()); + self::assertSame($expectedErrorCodes, $errorCodes); + } + + private function resolveErrorCode(Error $error) + { + return $error->getCode(); + } +} diff --git a/typo3/sysext/form/Tests/Unit/Mvc/Property/PropertyMappingConfigurationTest.php b/typo3/sysext/form/Tests/Unit/Mvc/Property/PropertyMappingConfigurationTest.php index 94c3dcbd8f14..be20ccd9cae6 100644 --- a/typo3/sysext/form/Tests/Unit/Mvc/Property/PropertyMappingConfigurationTest.php +++ b/typo3/sysext/form/Tests/Unit/Mvc/Property/PropertyMappingConfigurationTest.php @@ -120,9 +120,12 @@ class PropertyMappingConfigurationTest extends UnitTestCase // Resource Factory /** @var \PHPUnit\Framework\MockObject\MockObject|ResourceFactory $resourceFactory */ $resourceFactory = $this->createMock(ResourceFactory::class); + /** @var \PHPUnit\Framework\MockObject\MockObject|UploadedFileReferenceConverter $typeConverter */ + $typeConverter = $this->createMock(UploadedFileReferenceConverter::class); // Object Manager (in order to return mocked Resource Factory) $objectManager = $this->prophesize(ObjectManager::class); + $objectManager->get(UploadedFileReferenceConverter::class)->willReturn($typeConverter); $objectManager->get(MimeTypeValidator::class)->willReturn($mimeTypeValidator); $objectManager->get(ResourceFactory::class)->willReturn($resourceFactory); GeneralUtility::setSingletonInstance(ObjectManager::class, $objectManager->reveal()); @@ -161,9 +164,12 @@ class PropertyMappingConfigurationTest extends UnitTestCase ->setMethods(['__construct']) ->disableOriginalConstructor() ->getMock(); + /** @var \PHPUnit\Framework\MockObject\MockObject|UploadedFileReferenceConverter $typeConverter */ + $typeConverter = $this->createMock(UploadedFileReferenceConverter::class); // Object Manager to return the MimeTypeValidator $objectManager = $this->prophesize(ObjectManager::class); + $objectManager->get(UploadedFileReferenceConverter::class)->willReturn($typeConverter); $objectManager->get(MimeTypeValidator::class, $mimeTypes)->willReturn($mimeTypeValidator); GeneralUtility::setSingletonInstance(ObjectManager::class, $objectManager->reveal()); @@ -205,6 +211,8 @@ class PropertyMappingConfigurationTest extends UnitTestCase // Resource Factory /** @var \PHPUnit\Framework\MockObject\MockObject|ResourceFactory $resourceFactory */ $resourceFactory = $this->createMock(ResourceFactory::class); + /** @var \PHPUnit\Framework\MockObject\MockObject|UploadedFileReferenceConverter $typeConverter */ + $typeConverter = $this->createMock(UploadedFileReferenceConverter::class); // Object Manager (in order to return mocked Resource Factory) /** @var \PHPUnit\Framework\MockObject\MockObject|ObjectManager $objectManager */ @@ -217,6 +225,7 @@ class PropertyMappingConfigurationTest extends UnitTestCase ->expects(self::any()) ->method('get') ->willReturnMap([ + [UploadedFileReferenceConverter::class, $typeConverter], [MimeTypeValidator::class, $mimeTypeValidator], [ResourceFactory::class, $resourceFactory] ]); @@ -261,6 +270,8 @@ class PropertyMappingConfigurationTest extends UnitTestCase // Resource Factory /** @var \PHPUnit\Framework\MockObject\MockObject|ResourceFactory $resourceFactory */ $resourceFactory = $this->createMock(ResourceFactory::class); + /** @var \PHPUnit\Framework\MockObject\MockObject|UploadedFileReferenceConverter $typeConverter */ + $typeConverter = $this->createMock(UploadedFileReferenceConverter::class); // Object Manager (in order to return mocked Resource Factory) /** @var \PHPUnit\Framework\MockObject\MockObject|ObjectManager $objectManager */ @@ -273,6 +284,7 @@ class PropertyMappingConfigurationTest extends UnitTestCase ->expects(self::any()) ->method('get') ->willReturnMap([ + [UploadedFileReferenceConverter::class, $typeConverter], [MimeTypeValidator::class, $mimeTypeValidator], [ResourceFactory::class, $resourceFactory] ]); @@ -322,6 +334,8 @@ class PropertyMappingConfigurationTest extends UnitTestCase // Resource Factory /** @var \PHPUnit\Framework\MockObject\MockObject|ResourceFactory $resourceFactory */ $resourceFactory = $this->createMock(ResourceFactory::class); + /** @var \PHPUnit\Framework\MockObject\MockObject|UploadedFileReferenceConverter $typeConverter */ + $typeConverter = $this->createMock(UploadedFileReferenceConverter::class); // Object Manager (in order to return mocked Resource Factory) /** @var \PHPUnit\Framework\MockObject\MockObject|ObjectManager $objectManager */ @@ -334,6 +348,7 @@ class PropertyMappingConfigurationTest extends UnitTestCase ->expects(self::any()) ->method('get') ->willReturnMap([ + [UploadedFileReferenceConverter::class, $typeConverter], [MimeTypeValidator::class, $mimeTypeValidator], [ResourceFactory::class, $resourceFactory] ]); @@ -383,6 +398,8 @@ class PropertyMappingConfigurationTest extends UnitTestCase // Resource Factory /** @var \PHPUnit\Framework\MockObject\MockObject|ResourceFactory $resourceFactory */ $resourceFactory = $this->createMock(ResourceFactory::class); + /** @var \PHPUnit\Framework\MockObject\MockObject|UploadedFileReferenceConverter $typeConverter */ + $typeConverter = $this->createMock(UploadedFileReferenceConverter::class); // Object Manager (in order to return mocked Resource Factory) /** @var \PHPUnit\Framework\MockObject\MockObject|ObjectManager $objectManager */ @@ -395,6 +412,7 @@ class PropertyMappingConfigurationTest extends UnitTestCase ->expects(self::any()) ->method('get') ->willReturnMap([ + [UploadedFileReferenceConverter::class, $typeConverter], [MimeTypeValidator::class, $mimeTypeValidator], [ResourceFactory::class, $resourceFactory] ]); @@ -439,6 +457,8 @@ class PropertyMappingConfigurationTest extends UnitTestCase // Resource Factory /** @var \PHPUnit\Framework\MockObject\MockObject|ResourceFactory $resourceFactory */ $resourceFactory = $this->createMock(ResourceFactory::class); + /** @var \PHPUnit\Framework\MockObject\MockObject|UploadedFileReferenceConverter $typeConverter */ + $typeConverter = $this->createMock(UploadedFileReferenceConverter::class); // Object Manager (in order to return mocked Resource Factory) /** @var \PHPUnit\Framework\MockObject\MockObject|ObjectManager $objectManager */ @@ -451,6 +471,7 @@ class PropertyMappingConfigurationTest extends UnitTestCase ->expects(self::any()) ->method('get') ->willReturnMap([ + [UploadedFileReferenceConverter::class, $typeConverter], [MimeTypeValidator::class, $mimeTypeValidator], [ResourceFactory::class, $resourceFactory] ]); diff --git a/typo3/sysext/form/Tests/Unit/Mvc/Validation/MimeTypeValidatorTest.php b/typo3/sysext/form/Tests/Unit/Mvc/Validation/MimeTypeValidatorTest.php index 5899a0d3fe9b..47f532044ed7 100644 --- a/typo3/sysext/form/Tests/Unit/Mvc/Validation/MimeTypeValidatorTest.php +++ b/typo3/sysext/form/Tests/Unit/Mvc/Validation/MimeTypeValidatorTest.php @@ -108,4 +108,43 @@ class MimeTypeValidatorTest extends UnitTestCase self::assertTrue($validator->validate('string')->hasErrors()); } + + public function fileExtensionMatchesMimeTypesDataProvider(): array + { + $allowedMimeTypes = ['application/pdf', 'application/vnd.oasis.opendocument.text']; + return [ + // filename, file mime-type, allowed types, is valid (is allowed) + ['something.pdf', 'application/pdf', $allowedMimeTypes, true], + ['something.txt', 'application/pdf', $allowedMimeTypes, false], + ['something.pdf', 'application/pdf', [false], false], + ['something.pdf', 'false', $allowedMimeTypes, false], + ]; + } + + /** + * @param string $fileName + * @param string $fileMimeType + * @param array $allowedMimeTypes + * @param bool $isValid + * @test + * @dataProvider fileExtensionMatchesMimeTypesDataProvider + */ + public function fileExtensionMatchesMimeTypes(string $fileName, string $fileMimeType, array $allowedMimeTypes, bool $isValid): void + { + $options = ['allowedMimeTypes' => $allowedMimeTypes]; + $validator = $this->getMockBuilder(MimeTypeValidator::class) + ->setMethods(['translateErrorMessage']) + ->setConstructorArgs(['options' => $options]) + ->getMock(); + $mockedStorage = $this->getMockBuilder(ResourceStorage::class) + ->disableOriginalConstructor() + ->getMock(); + $file = new File([ + 'name' => $fileName, + 'identifier' => '/folder/' . $fileName, + 'mime_type' => $fileMimeType + ], $mockedStorage); + $result = $validator->validate($file); + self::assertSame($isValid, !$result->hasErrors()); + } } diff --git a/typo3/sysext/form/ext_localconf.php b/typo3/sysext/form/ext_localconf.php index 74dad0e5a1bd..ee27f80aded4 100644 --- a/typo3/sysext/form/ext_localconf.php +++ b/typo3/sysext/form/ext_localconf.php @@ -61,13 +61,12 @@ call_user_func(function () { // FE file upload processing $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterBuildingFinished'][1489772699] = \TYPO3\CMS\Form\Mvc\Property\PropertyMappingConfiguration::class; + $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterFormStateInitialized'][1613296803] + = \TYPO3\CMS\Form\Mvc\Property\PropertyMappingConfiguration::class; \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerTypeConverter( \TYPO3\CMS\Form\Mvc\Property\TypeConverter\FormDefinitionArrayConverter::class ); - \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerTypeConverter( - \TYPO3\CMS\Form\Mvc\Property\TypeConverter\UploadedFileReferenceConverter::class - ); // Register "formvh:" namespace $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces']['formvh'][] = 'TYPO3\\CMS\\Form\\ViewHelpers'; -- GitLab