ouidade 3 years ago
parent
commit
66f14b6c0e
50 changed files with 2848 additions and 212 deletions
  1. 2 1
      system/blueprints/config/scheduler.yaml
  2. 1 1
      system/blueprints/flex/pages.yaml
  3. 6 2
      system/config/media.yaml
  4. 1986 0
      system/config/mime.yaml
  5. 2 2
      system/config/system.yaml
  6. 1 1
      system/defines.php
  7. 21 7
      system/src/Grav/Common/Assets.php
  8. 17 12
      system/src/Grav/Common/Assets/Pipeline.php
  9. 4 0
      system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php
  10. 13 3
      system/src/Grav/Common/Config/Setup.php
  11. 21 6
      system/src/Grav/Common/Data/Validation.php
  12. 2 1
      system/src/Grav/Common/Flex/FlexObject.php
  13. 8 0
      system/src/Grav/Common/Flex/Types/Pages/PageCollection.php
  14. 3 3
      system/src/Grav/Common/Flex/Types/Pages/PageIndex.php
  15. 32 4
      system/src/Grav/Common/Flex/Types/Pages/PageObject.php
  16. 8 0
      system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php
  17. 33 3
      system/src/Grav/Common/Flex/Types/Users/UserObject.php
  18. 99 37
      system/src/Grav/Common/GPM/GPM.php
  19. 8 0
      system/src/Grav/Common/Grav.php
  20. 22 16
      system/src/Grav/Common/Media/Traits/MediaUploadTrait.php
  21. 12 0
      system/src/Grav/Common/Page/Collection.php
  22. 3 2
      system/src/Grav/Common/Page/Media.php
  23. 3 3
      system/src/Grav/Common/Processors/InitializeProcessor.php
  24. 20 3
      system/src/Grav/Common/Processors/PagesProcessor.php
  25. 3 1
      system/src/Grav/Common/Scheduler/Job.php
  26. 10 2
      system/src/Grav/Common/Security.php
  27. 14 0
      system/src/Grav/Common/Service/ConfigServiceProvider.php
  28. 11 2
      system/src/Grav/Common/Service/TaskServiceProvider.php
  29. 8 4
      system/src/Grav/Common/Session.php
  30. 19 0
      system/src/Grav/Common/Twig/Exception/TwigException.php
  31. 13 1
      system/src/Grav/Common/Twig/Extension/GravExtension.php
  32. 1 1
      system/src/Grav/Common/Twig/Node/TwigNodeThrow.php
  33. 7 8
      system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php
  34. 53 26
      system/src/Grav/Common/Twig/Twig.php
  35. 10 5
      system/src/Grav/Common/Uri.php
  36. 41 5
      system/src/Grav/Framework/Flex/FlexDirectory.php
  37. 7 3
      system/src/Grav/Framework/Flex/FlexDirectoryForm.php
  38. 28 7
      system/src/Grav/Framework/Flex/FlexForm.php
  39. 43 6
      system/src/Grav/Framework/Flex/FlexObject.php
  40. 1 1
      system/src/Grav/Framework/Flex/Interfaces/FlexDirectoryInterface.php
  41. 2 2
      system/src/Grav/Framework/Flex/Interfaces/FlexFormInterface.php
  42. 9 3
      system/src/Grav/Framework/Flex/Storage/FolderStorage.php
  43. 1 1
      system/src/Grav/Framework/Flex/Storage/SimpleStorage.php
  44. 1 1
      system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php
  45. 13 7
      system/src/Grav/Framework/Form/FormFlash.php
  46. 107 0
      system/src/Grav/Framework/Mime/MimeTypes.php
  47. 0 1
      system/src/Grav/Framework/Object/ObjectCollection.php
  48. 33 0
      system/src/Grav/Framework/Psr7/UploadedFile.php
  49. 16 18
      system/src/Grav/Framework/Session/Session.php
  50. 70 0
      system/src/Phive/Twig/Extensions/Deferred/DeferredExtension.php

+ 2 - 1
system/blueprints/config/scheduler.yaml

@@ -47,7 +47,8 @@ form:
               label: PLUGIN_ADMIN.EXTRA_ARGUMENTS
               placeholder: '-lah'
             .at:
-              type: cron
+              type: text
+              wrapper_classes: cron-selector
               label: PLUGIN_ADMIN.SCHEDULER_RUNAT
               help: PLUGIN_ADMIN.SCHEDULER_RUNAT_HELP
               placeholder: '* * * * *'

+ 1 - 1
system/blueprints/flex/pages.yaml

@@ -184,9 +184,9 @@ config:
       # Fields to be searched
       fields:
         - key
+        - slug
         - menu
         - title
-        - name
 
 blueprints:
   configure:

+ 6 - 2
system/config/media.yaml

@@ -28,6 +28,10 @@ types:
     type: image
     thumb: media/thumb-webp.png
     mime: image/webp
+  avif:
+    type: image
+    thumb: media/thumb.png
+    mime: image/avif
   gif:
     type: animated
     thumb: media/thumb-gif.png
@@ -91,7 +95,7 @@ types:
   aif:
     type: audio
     thumb: media/thumb-aif.png
-    mime: audio/aif
+    mime: audio/aiff
   txt:
     type: file
     thumb: media/thumb-txt.png
@@ -207,7 +211,7 @@ types:
   js:
     type: file
     thumb: media/thumb-js.png
-    mime: application/javascript
+    mime: text/javascript
   json:
     type: file
     thumb: media/thumb-json.png

+ 1986 - 0
system/config/mime.yaml

@@ -0,0 +1,1986 @@
+types:
+  '123':
+  - application/vnd.lotus-1-2-3
+  wof:
+  - application/font-woff
+  php:
+  - application/php
+  - application/x-httpd-php
+  - application/x-httpd-php-source
+  - application/x-php
+  - text/php
+  - text/x-php
+  otf:
+  - application/x-font-otf
+  - font/otf
+  ttf:
+  - application/x-font-ttf
+  - font/ttf
+  ttc:
+  - application/x-font-ttf
+  - font/collection
+  zip:
+  - application/x-gzip
+  - application/zip
+  - application/x-zip-compressed
+  amr:
+  - audio/amr
+  mp3:
+  - audio/mpeg
+  mpga:
+  - audio/mpeg
+  mp2:
+  - audio/mpeg
+  mp2a:
+  - audio/mpeg
+  m2a:
+  - audio/mpeg
+  m3a:
+  - audio/mpeg
+  jpg:
+  - image/jpeg
+  jpeg:
+  - image/jpeg
+  jpe:
+  - image/jpeg
+  bmp:
+  - image/x-ms-bmp
+  - image/bmp
+  ez:
+  - application/andrew-inset
+  aw:
+  - application/applixware
+  atom:
+  - application/atom+xml
+  atomcat:
+  - application/atomcat+xml
+  atomsvc:
+  - application/atomsvc+xml
+  ccxml:
+  - application/ccxml+xml
+  cdmia:
+  - application/cdmi-capability
+  cdmic:
+  - application/cdmi-container
+  cdmid:
+  - application/cdmi-domain
+  cdmio:
+  - application/cdmi-object
+  cdmiq:
+  - application/cdmi-queue
+  cu:
+  - application/cu-seeme
+  davmount:
+  - application/davmount+xml
+  dbk:
+  - application/docbook+xml
+  dssc:
+  - application/dssc+der
+  xdssc:
+  - application/dssc+xml
+  ecma:
+  - application/ecmascript
+  emma:
+  - application/emma+xml
+  epub:
+  - application/epub+zip
+  exi:
+  - application/exi
+  pfr:
+  - application/font-tdpfr
+  gml:
+  - application/gml+xml
+  gpx:
+  - application/gpx+xml
+  gxf:
+  - application/gxf
+  stk:
+  - application/hyperstudio
+  ink:
+  - application/inkml+xml
+  inkml:
+  - application/inkml+xml
+  ipfix:
+  - application/ipfix
+  jar:
+  - application/java-archive
+  ser:
+  - application/java-serialized-object
+  class:
+  - application/java-vm
+  js:
+  - application/javascript
+  json:
+  - application/json
+  jsonml:
+  - application/jsonml+json
+  lostxml:
+  - application/lost+xml
+  hqx:
+  - application/mac-binhex40
+  cpt:
+  - application/mac-compactpro
+  mads:
+  - application/mads+xml
+  mrc:
+  - application/marc
+  mrcx:
+  - application/marcxml+xml
+  ma:
+  - application/mathematica
+  nb:
+  - application/mathematica
+  mb:
+  - application/mathematica
+  mathml:
+  - application/mathml+xml
+  mbox:
+  - application/mbox
+  mscml:
+  - application/mediaservercontrol+xml
+  metalink:
+  - application/metalink+xml
+  meta4:
+  - application/metalink4+xml
+  mets:
+  - application/mets+xml
+  mods:
+  - application/mods+xml
+  m21:
+  - application/mp21
+  mp21:
+  - application/mp21
+  mp4s:
+  - application/mp4
+  doc:
+  - application/msword
+  dot:
+  - application/msword
+  mxf:
+  - application/mxf
+  bin:
+  - application/octet-stream
+  dms:
+  - application/octet-stream
+  lrf:
+  - application/octet-stream
+  mar:
+  - application/octet-stream
+  so:
+  - application/octet-stream
+  dist:
+  - application/octet-stream
+  distz:
+  - application/octet-stream
+  pkg:
+  - application/octet-stream
+  bpk:
+  - application/octet-stream
+  dump:
+  - application/octet-stream
+  elc:
+  - application/octet-stream
+  deploy:
+  - application/octet-stream
+  oda:
+  - application/oda
+  opf:
+  - application/oebps-package+xml
+  ogx:
+  - application/ogg
+  omdoc:
+  - application/omdoc+xml
+  onetoc:
+  - application/onenote
+  onetoc2:
+  - application/onenote
+  onetmp:
+  - application/onenote
+  onepkg:
+  - application/onenote
+  oxps:
+  - application/oxps
+  xer:
+  - application/patch-ops-error+xml
+  pdf:
+  - application/pdf
+  pgp:
+  - application/pgp-encrypted
+  asc:
+  - application/pgp-signature
+  sig:
+  - application/pgp-signature
+  prf:
+  - application/pics-rules
+  p10:
+  - application/pkcs10
+  p7m:
+  - application/pkcs7-mime
+  p7c:
+  - application/pkcs7-mime
+  p7s:
+  - application/pkcs7-signature
+  p8:
+  - application/pkcs8
+  ac:
+  - application/pkix-attr-cert
+  cer:
+  - application/pkix-cert
+  crl:
+  - application/pkix-crl
+  pkipath:
+  - application/pkix-pkipath
+  pki:
+  - application/pkixcmp
+  pls:
+  - application/pls+xml
+  ai:
+  - application/postscript
+  eps:
+  - application/postscript
+  ps:
+  - application/postscript
+  cww:
+  - application/prs.cww
+  pskcxml:
+  - application/pskc+xml
+  rdf:
+  - application/rdf+xml
+  rif:
+  - application/reginfo+xml
+  rnc:
+  - application/relax-ng-compact-syntax
+  rl:
+  - application/resource-lists+xml
+  rld:
+  - application/resource-lists-diff+xml
+  rs:
+  - application/rls-services+xml
+  gbr:
+  - application/rpki-ghostbusters
+  mft:
+  - application/rpki-manifest
+  roa:
+  - application/rpki-roa
+  rsd:
+  - application/rsd+xml
+  rss:
+  - application/rss+xml
+  rtf:
+  - application/rtf
+  sbml:
+  - application/sbml+xml
+  scq:
+  - application/scvp-cv-request
+  scs:
+  - application/scvp-cv-response
+  spq:
+  - application/scvp-vp-request
+  spp:
+  - application/scvp-vp-response
+  sdp:
+  - application/sdp
+  setpay:
+  - application/set-payment-initiation
+  setreg:
+  - application/set-registration-initiation
+  shf:
+  - application/shf+xml
+  smi:
+  - application/smil+xml
+  smil:
+  - application/smil+xml
+  rq:
+  - application/sparql-query
+  srx:
+  - application/sparql-results+xml
+  gram:
+  - application/srgs
+  grxml:
+  - application/srgs+xml
+  sru:
+  - application/sru+xml
+  ssdl:
+  - application/ssdl+xml
+  ssml:
+  - application/ssml+xml
+  tei:
+  - application/tei+xml
+  teicorpus:
+  - application/tei+xml
+  tfi:
+  - application/thraud+xml
+  tsd:
+  - application/timestamped-data
+  plb:
+  - application/vnd.3gpp.pic-bw-large
+  psb:
+  - application/vnd.3gpp.pic-bw-small
+  pvb:
+  - application/vnd.3gpp.pic-bw-var
+  tcap:
+  - application/vnd.3gpp2.tcap
+  pwn:
+  - application/vnd.3m.post-it-notes
+  aso:
+  - application/vnd.accpac.simply.aso
+  imp:
+  - application/vnd.accpac.simply.imp
+  acu:
+  - application/vnd.acucobol
+  atc:
+  - application/vnd.acucorp
+  acutc:
+  - application/vnd.acucorp
+  air:
+  - application/vnd.adobe.air-application-installer-package+zip
+  fcdt:
+  - application/vnd.adobe.formscentral.fcdt
+  fxp:
+  - application/vnd.adobe.fxp
+  fxpl:
+  - application/vnd.adobe.fxp
+  xdp:
+  - application/vnd.adobe.xdp+xml
+  xfdf:
+  - application/vnd.adobe.xfdf
+  ahead:
+  - application/vnd.ahead.space
+  azf:
+  - application/vnd.airzip.filesecure.azf
+  azs:
+  - application/vnd.airzip.filesecure.azs
+  azw:
+  - application/vnd.amazon.ebook
+  acc:
+  - application/vnd.americandynamics.acc
+  ami:
+  - application/vnd.amiga.ami
+  apk:
+  - application/vnd.android.package-archive
+  cii:
+  - application/vnd.anser-web-certificate-issue-initiation
+  fti:
+  - application/vnd.anser-web-funds-transfer-initiation
+  atx:
+  - application/vnd.antix.game-component
+  mpkg:
+  - application/vnd.apple.installer+xml
+  m3u8:
+  - application/vnd.apple.mpegurl
+  swi:
+  - application/vnd.aristanetworks.swi
+  iota:
+  - application/vnd.astraea-software.iota
+  aep:
+  - application/vnd.audiograph
+  mpm:
+  - application/vnd.blueice.multipass
+  bmi:
+  - application/vnd.bmi
+  rep:
+  - application/vnd.businessobjects
+  cdxml:
+  - application/vnd.chemdraw+xml
+  mmd:
+  - application/vnd.chipnuts.karaoke-mmd
+  cdy:
+  - application/vnd.cinderella
+  cla:
+  - application/vnd.claymore
+  rp9:
+  - application/vnd.cloanto.rp9
+  c4g:
+  - application/vnd.clonk.c4group
+  c4d:
+  - application/vnd.clonk.c4group
+  c4f:
+  - application/vnd.clonk.c4group
+  c4p:
+  - application/vnd.clonk.c4group
+  c4u:
+  - application/vnd.clonk.c4group
+  c11amc:
+  - application/vnd.cluetrust.cartomobile-config
+  c11amz:
+  - application/vnd.cluetrust.cartomobile-config-pkg
+  csp:
+  - application/vnd.commonspace
+  cdbcmsg:
+  - application/vnd.contact.cmsg
+  cmc:
+  - application/vnd.cosmocaller
+  clkx:
+  - application/vnd.crick.clicker
+  clkk:
+  - application/vnd.crick.clicker.keyboard
+  clkp:
+  - application/vnd.crick.clicker.palette
+  clkt:
+  - application/vnd.crick.clicker.template
+  clkw:
+  - application/vnd.crick.clicker.wordbank
+  wbs:
+  - application/vnd.criticaltools.wbs+xml
+  pml:
+  - application/vnd.ctc-posml
+  ppd:
+  - application/vnd.cups-ppd
+  car:
+  - application/vnd.curl.car
+  pcurl:
+  - application/vnd.curl.pcurl
+  dart:
+  - application/vnd.dart
+  rdz:
+  - application/vnd.data-vision.rdz
+  uvf:
+  - application/vnd.dece.data
+  uvvf:
+  - application/vnd.dece.data
+  uvd:
+  - application/vnd.dece.data
+  uvvd:
+  - application/vnd.dece.data
+  uvt:
+  - application/vnd.dece.ttml+xml
+  uvvt:
+  - application/vnd.dece.ttml+xml
+  uvx:
+  - application/vnd.dece.unspecified
+  uvvx:
+  - application/vnd.dece.unspecified
+  uvz:
+  - application/vnd.dece.zip
+  uvvz:
+  - application/vnd.dece.zip
+  fe_launch:
+  - application/vnd.denovo.fcselayout-link
+  dna:
+  - application/vnd.dna
+  mlp:
+  - application/vnd.dolby.mlp
+  dpg:
+  - application/vnd.dpgraph
+  dfac:
+  - application/vnd.dreamfactory
+  kpxx:
+  - application/vnd.ds-keypoint
+  ait:
+  - application/vnd.dvb.ait
+  svc:
+  - application/vnd.dvb.service
+  geo:
+  - application/vnd.dynageo
+  mag:
+  - application/vnd.ecowin.chart
+  nml:
+  - application/vnd.enliven
+  esf:
+  - application/vnd.epson.esf
+  msf:
+  - application/vnd.epson.msf
+  qam:
+  - application/vnd.epson.quickanime
+  slt:
+  - application/vnd.epson.salt
+  ssf:
+  - application/vnd.epson.ssf
+  es3:
+  - application/vnd.eszigno3+xml
+  et3:
+  - application/vnd.eszigno3+xml
+  ez2:
+  - application/vnd.ezpix-album
+  ez3:
+  - application/vnd.ezpix-package
+  fdf:
+  - application/vnd.fdf
+  mseed:
+  - application/vnd.fdsn.mseed
+  seed:
+  - application/vnd.fdsn.seed
+  dataless:
+  - application/vnd.fdsn.seed
+  gph:
+  - application/vnd.flographit
+  ftc:
+  - application/vnd.fluxtime.clip
+  fm:
+  - application/vnd.framemaker
+  frame:
+  - application/vnd.framemaker
+  maker:
+  - application/vnd.framemaker
+  book:
+  - application/vnd.framemaker
+  fnc:
+  - application/vnd.frogans.fnc
+  ltf:
+  - application/vnd.frogans.ltf
+  fsc:
+  - application/vnd.fsc.weblaunch
+  oas:
+  - application/vnd.fujitsu.oasys
+  oa2:
+  - application/vnd.fujitsu.oasys2
+  oa3:
+  - application/vnd.fujitsu.oasys3
+  fg5:
+  - application/vnd.fujitsu.oasysgp
+  bh2:
+  - application/vnd.fujitsu.oasysprs
+  ddd:
+  - application/vnd.fujixerox.ddd
+  xdw:
+  - application/vnd.fujixerox.docuworks
+  xbd:
+  - application/vnd.fujixerox.docuworks.binder
+  fzs:
+  - application/vnd.fuzzysheet
+  txd:
+  - application/vnd.genomatix.tuxedo
+  ggb:
+  - application/vnd.geogebra.file
+  ggt:
+  - application/vnd.geogebra.tool
+  gex:
+  - application/vnd.geometry-explorer
+  gre:
+  - application/vnd.geometry-explorer
+  gxt:
+  - application/vnd.geonext
+  g2w:
+  - application/vnd.geoplan
+  g3w:
+  - application/vnd.geospace
+  gmx:
+  - application/vnd.gmx
+  kml:
+  - application/vnd.google-earth.kml+xml
+  kmz:
+  - application/vnd.google-earth.kmz
+  gqf:
+  - application/vnd.grafeq
+  gqs:
+  - application/vnd.grafeq
+  gac:
+  - application/vnd.groove-account
+  ghf:
+  - application/vnd.groove-help
+  gim:
+  - application/vnd.groove-identity-message
+  grv:
+  - application/vnd.groove-injector
+  gtm:
+  - application/vnd.groove-tool-message
+  tpl:
+  - application/vnd.groove-tool-template
+  vcg:
+  - application/vnd.groove-vcard
+  hal:
+  - application/vnd.hal+xml
+  zmm:
+  - application/vnd.handheld-entertainment+xml
+  hbci:
+  - application/vnd.hbci
+  les:
+  - application/vnd.hhe.lesson-player
+  hpgl:
+  - application/vnd.hp-hpgl
+  hpid:
+  - application/vnd.hp-hpid
+  hps:
+  - application/vnd.hp-hps
+  jlt:
+  - application/vnd.hp-jlyt
+  pcl:
+  - application/vnd.hp-pcl
+  pclxl:
+  - application/vnd.hp-pclxl
+  sfd-hdstx:
+  - application/vnd.hydrostatix.sof-data
+  mpy:
+  - application/vnd.ibm.minipay
+  afp:
+  - application/vnd.ibm.modcap
+  listafp:
+  - application/vnd.ibm.modcap
+  list3820:
+  - application/vnd.ibm.modcap
+  irm:
+  - application/vnd.ibm.rights-management
+  sc:
+  - application/vnd.ibm.secure-container
+  icc:
+  - application/vnd.iccprofile
+  icm:
+  - application/vnd.iccprofile
+  igl:
+  - application/vnd.igloader
+  ivp:
+  - application/vnd.immervision-ivp
+  ivu:
+  - application/vnd.immervision-ivu
+  igm:
+  - application/vnd.insors.igm
+  xpw:
+  - application/vnd.intercon.formnet
+  xpx:
+  - application/vnd.intercon.formnet
+  i2g:
+  - application/vnd.intergeo
+  qbo:
+  - application/vnd.intu.qbo
+  qfx:
+  - application/vnd.intu.qfx
+  rcprofile:
+  - application/vnd.ipunplugged.rcprofile
+  irp:
+  - application/vnd.irepository.package+xml
+  xpr:
+  - application/vnd.is-xpr
+  fcs:
+  - application/vnd.isac.fcs
+  jam:
+  - application/vnd.jam
+  rms:
+  - application/vnd.jcp.javame.midlet-rms
+  jisp:
+  - application/vnd.jisp
+  joda:
+  - application/vnd.joost.joda-archive
+  ktz:
+  - application/vnd.kahootz
+  ktr:
+  - application/vnd.kahootz
+  karbon:
+  - application/vnd.kde.karbon
+  chrt:
+  - application/vnd.kde.kchart
+  kfo:
+  - application/vnd.kde.kformula
+  flw:
+  - application/vnd.kde.kivio
+  kon:
+  - application/vnd.kde.kontour
+  kpr:
+  - application/vnd.kde.kpresenter
+  kpt:
+  - application/vnd.kde.kpresenter
+  ksp:
+  - application/vnd.kde.kspread
+  kwd:
+  - application/vnd.kde.kword
+  kwt:
+  - application/vnd.kde.kword
+  htke:
+  - application/vnd.kenameaapp
+  kia:
+  - application/vnd.kidspiration
+  kne:
+  - application/vnd.kinar
+  knp:
+  - application/vnd.kinar
+  skp:
+  - application/vnd.koan
+  skd:
+  - application/vnd.koan
+  skt:
+  - application/vnd.koan
+  skm:
+  - application/vnd.koan
+  sse:
+  - application/vnd.kodak-descriptor
+  lasxml:
+  - application/vnd.las.las+xml
+  lbd:
+  - application/vnd.llamagraphics.life-balance.desktop
+  lbe:
+  - application/vnd.llamagraphics.life-balance.exchange+xml
+  apr:
+  - application/vnd.lotus-approach
+  pre:
+  - application/vnd.lotus-freelance
+  nsf:
+  - application/vnd.lotus-notes
+  org:
+  - application/vnd.lotus-organizer
+  scm:
+  - application/vnd.lotus-screencam
+  lwp:
+  - application/vnd.lotus-wordpro
+  portpkg:
+  - application/vnd.macports.portpkg
+  mcd:
+  - application/vnd.mcd
+  mc1:
+  - application/vnd.medcalcdata
+  cdkey:
+  - application/vnd.mediastation.cdkey
+  mwf:
+  - application/vnd.mfer
+  mfm:
+  - application/vnd.mfmp
+  flo:
+  - application/vnd.micrografx.flo
+  igx:
+  - application/vnd.micrografx.igx
+  mif:
+  - application/vnd.mif
+  daf:
+  - application/vnd.mobius.daf
+  dis:
+  - application/vnd.mobius.dis
+  mbk:
+  - application/vnd.mobius.mbk
+  mqy:
+  - application/vnd.mobius.mqy
+  msl:
+  - application/vnd.mobius.msl
+  plc:
+  - application/vnd.mobius.plc
+  txf:
+  - application/vnd.mobius.txf
+  mpn:
+  - application/vnd.mophun.application
+  mpc:
+  - application/vnd.mophun.certificate
+  xul:
+  - application/vnd.mozilla.xul+xml
+  cil:
+  - application/vnd.ms-artgalry
+  cab:
+  - application/vnd.ms-cab-compressed
+  xls:
+  - application/vnd.ms-excel
+  xlm:
+  - application/vnd.ms-excel
+  xla:
+  - application/vnd.ms-excel
+  xlc:
+  - application/vnd.ms-excel
+  xlt:
+  - application/vnd.ms-excel
+  xlw:
+  - application/vnd.ms-excel
+  xlam:
+  - application/vnd.ms-excel.addin.macroenabled.12
+  xlsb:
+  - application/vnd.ms-excel.sheet.binary.macroenabled.12
+  xlsm:
+  - application/vnd.ms-excel.sheet.macroenabled.12
+  xltm:
+  - application/vnd.ms-excel.template.macroenabled.12
+  eot:
+  - application/vnd.ms-fontobject
+  chm:
+  - application/vnd.ms-htmlhelp
+  ims:
+  - application/vnd.ms-ims
+  lrm:
+  - application/vnd.ms-lrm
+  thmx:
+  - application/vnd.ms-officetheme
+  cat:
+  - application/vnd.ms-pki.seccat
+  stl:
+  - application/vnd.ms-pki.stl
+  ppt:
+  - application/vnd.ms-powerpoint
+  pps:
+  - application/vnd.ms-powerpoint
+  pot:
+  - application/vnd.ms-powerpoint
+  ppam:
+  - application/vnd.ms-powerpoint.addin.macroenabled.12
+  pptm:
+  - application/vnd.ms-powerpoint.presentation.macroenabled.12
+  sldm:
+  - application/vnd.ms-powerpoint.slide.macroenabled.12
+  ppsm:
+  - application/vnd.ms-powerpoint.slideshow.macroenabled.12
+  potm:
+  - application/vnd.ms-powerpoint.template.macroenabled.12
+  mpp:
+  - application/vnd.ms-project
+  mpt:
+  - application/vnd.ms-project
+  docm:
+  - application/vnd.ms-word.document.macroenabled.12
+  dotm:
+  - application/vnd.ms-word.template.macroenabled.12
+  wps:
+  - application/vnd.ms-works
+  wks:
+  - application/vnd.ms-works
+  wcm:
+  - application/vnd.ms-works
+  wdb:
+  - application/vnd.ms-works
+  wpl:
+  - application/vnd.ms-wpl
+  xps:
+  - application/vnd.ms-xpsdocument
+  mseq:
+  - application/vnd.mseq
+  mus:
+  - application/vnd.musician
+  msty:
+  - application/vnd.muvee.style
+  taglet:
+  - application/vnd.mynfc
+  nlu:
+  - application/vnd.neurolanguage.nlu
+  ntf:
+  - application/vnd.nitf
+  nitf:
+  - application/vnd.nitf
+  nnd:
+  - application/vnd.noblenet-directory
+  nns:
+  - application/vnd.noblenet-sealer
+  nnw:
+  - application/vnd.noblenet-web
+  ngdat:
+  - application/vnd.nokia.n-gage.data
+  n-gage:
+  - application/vnd.nokia.n-gage.symbian.install
+  rpst:
+  - application/vnd.nokia.radio-preset
+  rpss:
+  - application/vnd.nokia.radio-presets
+  edm:
+  - application/vnd.novadigm.edm
+  edx:
+  - application/vnd.novadigm.edx
+  ext:
+  - application/vnd.novadigm.ext
+  odc:
+  - application/vnd.oasis.opendocument.chart
+  otc:
+  - application/vnd.oasis.opendocument.chart-template
+  odb:
+  - application/vnd.oasis.opendocument.database
+  odf:
+  - application/vnd.oasis.opendocument.formula
+  odft:
+  - application/vnd.oasis.opendocument.formula-template
+  odg:
+  - application/vnd.oasis.opendocument.graphics
+  otg:
+  - application/vnd.oasis.opendocument.graphics-template
+  odi:
+  - application/vnd.oasis.opendocument.image
+  oti:
+  - application/vnd.oasis.opendocument.image-template
+  odp:
+  - application/vnd.oasis.opendocument.presentation
+  otp:
+  - application/vnd.oasis.opendocument.presentation-template
+  ods:
+  - application/vnd.oasis.opendocument.spreadsheet
+  ots:
+  - application/vnd.oasis.opendocument.spreadsheet-template
+  odt:
+  - application/vnd.oasis.opendocument.text
+  odm:
+  - application/vnd.oasis.opendocument.text-master
+  ott:
+  - application/vnd.oasis.opendocument.text-template
+  oth:
+  - application/vnd.oasis.opendocument.text-web
+  xo:
+  - application/vnd.olpc-sugar
+  dd2:
+  - application/vnd.oma.dd2+xml
+  oxt:
+  - application/vnd.openofficeorg.extension
+  pptx:
+  - application/vnd.openxmlformats-officedocument.presentationml.presentation
+  sldx:
+  - application/vnd.openxmlformats-officedocument.presentationml.slide
+  ppsx:
+  - application/vnd.openxmlformats-officedocument.presentationml.slideshow
+  potx:
+  - application/vnd.openxmlformats-officedocument.presentationml.template
+  xlsx:
+  - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
+  xltx:
+  - application/vnd.openxmlformats-officedocument.spreadsheetml.template
+  docx:
+  - application/vnd.openxmlformats-officedocument.wordprocessingml.document
+  dotx:
+  - application/vnd.openxmlformats-officedocument.wordprocessingml.template
+  mgp:
+  - application/vnd.osgeo.mapguide.package
+  dp:
+  - application/vnd.osgi.dp
+  esa:
+  - application/vnd.osgi.subsystem
+  pdb:
+  - application/vnd.palm
+  pqa:
+  - application/vnd.palm
+  oprc:
+  - application/vnd.palm
+  paw:
+  - application/vnd.pawaafile
+  str:
+  - application/vnd.pg.format
+  ei6:
+  - application/vnd.pg.osasli
+  efif:
+  - application/vnd.picsel
+  wg:
+  - application/vnd.pmi.widget
+  plf:
+  - application/vnd.pocketlearn
+  pbd:
+  - application/vnd.powerbuilder6
+  box:
+  - application/vnd.previewsystems.box
+  mgz:
+  - application/vnd.proteus.magazine
+  qps:
+  - application/vnd.publishare-delta-tree
+  ptid:
+  - application/vnd.pvi.ptid1
+  qxd:
+  - application/vnd.quark.quarkxpress
+  qxt:
+  - application/vnd.quark.quarkxpress
+  qwd:
+  - application/vnd.quark.quarkxpress
+  qwt:
+  - application/vnd.quark.quarkxpress
+  qxl:
+  - application/vnd.quark.quarkxpress
+  qxb:
+  - application/vnd.quark.quarkxpress
+  bed:
+  - application/vnd.realvnc.bed
+  mxl:
+  - application/vnd.recordare.musicxml
+  musicxml:
+  - application/vnd.recordare.musicxml+xml
+  cryptonote:
+  - application/vnd.rig.cryptonote
+  cod:
+  - application/vnd.rim.cod
+  rm:
+  - application/vnd.rn-realmedia
+  rmvb:
+  - application/vnd.rn-realmedia-vbr
+  link66:
+  - application/vnd.route66.link66+xml
+  st:
+  - application/vnd.sailingtracker.track
+  see:
+  - application/vnd.seemail
+  sema:
+  - application/vnd.sema
+  semd:
+  - application/vnd.semd
+  semf:
+  - application/vnd.semf
+  ifm:
+  - application/vnd.shana.informed.formdata
+  itp:
+  - application/vnd.shana.informed.formtemplate
+  iif:
+  - application/vnd.shana.informed.interchange
+  ipk:
+  - application/vnd.shana.informed.package
+  twd:
+  - application/vnd.simtech-mindmapper
+  twds:
+  - application/vnd.simtech-mindmapper
+  mmf:
+  - application/vnd.smaf
+  teacher:
+  - application/vnd.smart.teacher
+  sdkm:
+  - application/vnd.solent.sdkm+xml
+  sdkd:
+  - application/vnd.solent.sdkm+xml
+  dxp:
+  - application/vnd.spotfire.dxp
+  sfs:
+  - application/vnd.spotfire.sfs
+  sdc:
+  - application/vnd.stardivision.calc
+  sda:
+  - application/vnd.stardivision.draw
+  sdd:
+  - application/vnd.stardivision.impress
+  smf:
+  - application/vnd.stardivision.math
+  sdw:
+  - application/vnd.stardivision.writer
+  vor:
+  - application/vnd.stardivision.writer
+  sgl:
+  - application/vnd.stardivision.writer-global
+  smzip:
+  - application/vnd.stepmania.package
+  sm:
+  - application/vnd.stepmania.stepchart
+  sxc:
+  - application/vnd.sun.xml.calc
+  stc:
+  - application/vnd.sun.xml.calc.template
+  sxd:
+  - application/vnd.sun.xml.draw
+  std:
+  - application/vnd.sun.xml.draw.template
+  sxi:
+  - application/vnd.sun.xml.impress
+  sti:
+  - application/vnd.sun.xml.impress.template
+  sxm:
+  - application/vnd.sun.xml.math
+  sxw:
+  - application/vnd.sun.xml.writer
+  sxg:
+  - application/vnd.sun.xml.writer.global
+  stw:
+  - application/vnd.sun.xml.writer.template
+  sus:
+  - application/vnd.sus-calendar
+  susp:
+  - application/vnd.sus-calendar
+  svd:
+  - application/vnd.svd
+  sis:
+  - application/vnd.symbian.install
+  sisx:
+  - application/vnd.symbian.install
+  xsm:
+  - application/vnd.syncml+xml
+  bdm:
+  - application/vnd.syncml.dm+wbxml
+  xdm:
+  - application/vnd.syncml.dm+xml
+  tao:
+  - application/vnd.tao.intent-module-archive
+  pcap:
+  - application/vnd.tcpdump.pcap
+  cap:
+  - application/vnd.tcpdump.pcap
+  dmp:
+  - application/vnd.tcpdump.pcap
+  tmo:
+  - application/vnd.tmobile-livetv
+  tpt:
+  - application/vnd.trid.tpt
+  mxs:
+  - application/vnd.triscape.mxs
+  tra:
+  - application/vnd.trueapp
+  ufd:
+  - application/vnd.ufdl
+  ufdl:
+  - application/vnd.ufdl
+  utz:
+  - application/vnd.uiq.theme
+  umj:
+  - application/vnd.umajin
+  unityweb:
+  - application/vnd.unity
+  uoml:
+  - application/vnd.uoml+xml
+  vcx:
+  - application/vnd.vcx
+  vsd:
+  - application/vnd.visio
+  vst:
+  - application/vnd.visio
+  vss:
+  - application/vnd.visio
+  vsw:
+  - application/vnd.visio
+  vis:
+  - application/vnd.visionary
+  vsf:
+  - application/vnd.vsf
+  wbxml:
+  - application/vnd.wap.wbxml
+  wmlc:
+  - application/vnd.wap.wmlc
+  wmlsc:
+  - application/vnd.wap.wmlscriptc
+  wtb:
+  - application/vnd.webturbo
+  nbp:
+  - application/vnd.wolfram.player
+  wpd:
+  - application/vnd.wordperfect
+  wqd:
+  - application/vnd.wqd
+  stf:
+  - application/vnd.wt.stf
+  xar:
+  - application/vnd.xara
+  xfdl:
+  - application/vnd.xfdl
+  hvd:
+  - application/vnd.yamaha.hv-dic
+  hvs:
+  - application/vnd.yamaha.hv-script
+  hvp:
+  - application/vnd.yamaha.hv-voice
+  osf:
+  - application/vnd.yamaha.openscoreformat
+  osfpvg:
+  - application/vnd.yamaha.openscoreformat.osfpvg+xml
+  saf:
+  - application/vnd.yamaha.smaf-audio
+  spf:
+  - application/vnd.yamaha.smaf-phrase
+  cmp:
+  - application/vnd.yellowriver-custom-menu
+  zir:
+  - application/vnd.zul
+  zirz:
+  - application/vnd.zul
+  zaz:
+  - application/vnd.zzazz.deck+xml
+  vxml:
+  - application/voicexml+xml
+  wgt:
+  - application/widget
+  hlp:
+  - application/winhlp
+  wsdl:
+  - application/wsdl+xml
+  wspolicy:
+  - application/wspolicy+xml
+  7z:
+  - application/x-7z-compressed
+  abw:
+  - application/x-abiword
+  ace:
+  - application/x-ace-compressed
+  dmg:
+  - application/x-apple-diskimage
+  aab:
+  - application/x-authorware-bin
+  x32:
+  - application/x-authorware-bin
+  u32:
+  - application/x-authorware-bin
+  vox:
+  - application/x-authorware-bin
+  aam:
+  - application/x-authorware-map
+  aas:
+  - application/x-authorware-seg
+  bcpio:
+  - application/x-bcpio
+  torrent:
+  - application/x-bittorrent
+  blb:
+  - application/x-blorb
+  blorb:
+  - application/x-blorb
+  bz:
+  - application/x-bzip
+  bz2:
+  - application/x-bzip2
+  boz:
+  - application/x-bzip2
+  cbr:
+  - application/x-cbr
+  cba:
+  - application/x-cbr
+  cbt:
+  - application/x-cbr
+  cbz:
+  - application/x-cbr
+  cb7:
+  - application/x-cbr
+  vcd:
+  - application/x-cdlink
+  cfs:
+  - application/x-cfs-compressed
+  chat:
+  - application/x-chat
+  pgn:
+  - application/x-chess-pgn
+  nsc:
+  - application/x-conference
+  cpio:
+  - application/x-cpio
+  csh:
+  - application/x-csh
+  deb:
+  - application/x-debian-package
+  udeb:
+  - application/x-debian-package
+  dgc:
+  - application/x-dgc-compressed
+  dir:
+  - application/x-director
+  dcr:
+  - application/x-director
+  dxr:
+  - application/x-director
+  cst:
+  - application/x-director
+  cct:
+  - application/x-director
+  cxt:
+  - application/x-director
+  w3d:
+  - application/x-director
+  fgd:
+  - application/x-director
+  swa:
+  - application/x-director
+  wad:
+  - application/x-doom
+  ncx:
+  - application/x-dtbncx+xml
+  dtb:
+  - application/x-dtbook+xml
+  res:
+  - application/x-dtbresource+xml
+  dvi:
+  - application/x-dvi
+  evy:
+  - application/x-envoy
+  eva:
+  - application/x-eva
+  bdf:
+  - application/x-font-bdf
+  gsf:
+  - application/x-font-ghostscript
+  psf:
+  - application/x-font-linux-psf
+  pcf:
+  - application/x-font-pcf
+  snf:
+  - application/x-font-snf
+  pfa:
+  - application/x-font-type1
+  pfb:
+  - application/x-font-type1
+  pfm:
+  - application/x-font-type1
+  afm:
+  - application/x-font-type1
+  arc:
+  - application/x-freearc
+  spl:
+  - application/x-futuresplash
+  gca:
+  - application/x-gca-compressed
+  ulx:
+  - application/x-glulx
+  gnumeric:
+  - application/x-gnumeric
+  gramps:
+  - application/x-gramps-xml
+  gtar:
+  - application/x-gtar
+  hdf:
+  - application/x-hdf
+  install:
+  - application/x-install-instructions
+  iso:
+  - application/x-iso9660-image
+  jnlp:
+  - application/x-java-jnlp-file
+  latex:
+  - application/x-latex
+  lzh:
+  - application/x-lzh-compressed
+  lha:
+  - application/x-lzh-compressed
+  mie:
+  - application/x-mie
+  prc:
+  - application/x-mobipocket-ebook
+  mobi:
+  - application/x-mobipocket-ebook
+  application:
+  - application/x-ms-application
+  lnk:
+  - application/x-ms-shortcut
+  wmd:
+  - application/x-ms-wmd
+  wmz:
+  - application/x-ms-wmz
+  - application/x-msmetafile
+  xbap:
+  - application/x-ms-xbap
+  mdb:
+  - application/x-msaccess
+  obd:
+  - application/x-msbinder
+  crd:
+  - application/x-mscardfile
+  clp:
+  - application/x-msclip
+  exe:
+  - application/x-msdownload
+  dll:
+  - application/x-msdownload
+  com:
+  - application/x-msdownload
+  bat:
+  - application/x-msdownload
+  msi:
+  - application/x-msdownload
+  mvb:
+  - application/x-msmediaview
+  m13:
+  - application/x-msmediaview
+  m14:
+  - application/x-msmediaview
+  wmf:
+  - application/x-msmetafile
+  emf:
+  - application/x-msmetafile
+  emz:
+  - application/x-msmetafile
+  mny:
+  - application/x-msmoney
+  pub:
+  - application/x-mspublisher
+  scd:
+  - application/x-msschedule
+  trm:
+  - application/x-msterminal
+  wri:
+  - application/x-mswrite
+  nc:
+  - application/x-netcdf
+  cdf:
+  - application/x-netcdf
+  nzb:
+  - application/x-nzb
+  p12:
+  - application/x-pkcs12
+  pfx:
+  - application/x-pkcs12
+  p7b:
+  - application/x-pkcs7-certificates
+  spc:
+  - application/x-pkcs7-certificates
+  p7r:
+  - application/x-pkcs7-certreqresp
+  rar:
+  - application/x-rar-compressed
+  ris:
+  - application/x-research-info-systems
+  sh:
+  - application/x-sh
+  shar:
+  - application/x-shar
+  swf:
+  - application/x-shockwave-flash
+  xap:
+  - application/x-silverlight-app
+  sql:
+  - application/x-sql
+  sit:
+  - application/x-stuffit
+  sitx:
+  - application/x-stuffitx
+  srt:
+  - application/x-subrip
+  sv4cpio:
+  - application/x-sv4cpio
+  sv4crc:
+  - application/x-sv4crc
+  t3:
+  - application/x-t3vm-image
+  gam:
+  - application/x-tads
+  tar:
+  - application/x-tar
+  tcl:
+  - application/x-tcl
+  tex:
+  - application/x-tex
+  tfm:
+  - application/x-tex-tfm
+  texinfo:
+  - application/x-texinfo
+  texi:
+  - application/x-texinfo
+  obj:
+  - application/x-tgif
+  ustar:
+  - application/x-ustar
+  src:
+  - application/x-wais-source
+  der:
+  - application/x-x509-ca-cert
+  crt:
+  - application/x-x509-ca-cert
+  fig:
+  - application/x-xfig
+  xlf:
+  - application/x-xliff+xml
+  xpi:
+  - application/x-xpinstall
+  xz:
+  - application/x-xz
+  z1:
+  - application/x-zmachine
+  z2:
+  - application/x-zmachine
+  z3:
+  - application/x-zmachine
+  z4:
+  - application/x-zmachine
+  z5:
+  - application/x-zmachine
+  z6:
+  - application/x-zmachine
+  z7:
+  - application/x-zmachine
+  z8:
+  - application/x-zmachine
+  xaml:
+  - application/xaml+xml
+  xdf:
+  - application/xcap-diff+xml
+  xenc:
+  - application/xenc+xml
+  xhtml:
+  - application/xhtml+xml
+  xht:
+  - application/xhtml+xml
+  xml:
+  - application/xml
+  xsl:
+  - application/xml
+  dtd:
+  - application/xml-dtd
+  xop:
+  - application/xop+xml
+  xpl:
+  - application/xproc+xml
+  xslt:
+  - application/xslt+xml
+  xspf:
+  - application/xspf+xml
+  mxml:
+  - application/xv+xml
+  xhvml:
+  - application/xv+xml
+  xvml:
+  - application/xv+xml
+  xvm:
+  - application/xv+xml
+  yang:
+  - application/yang
+  yin:
+  - application/yin+xml
+  adp:
+  - audio/adpcm
+  au:
+  - audio/basic
+  snd:
+  - audio/basic
+  mid:
+  - audio/midi
+  midi:
+  - audio/midi
+  kar:
+  - audio/midi
+  rmi:
+  - audio/midi
+  m4a:
+  - audio/mp4
+  mp4a:
+  - audio/mp4
+  oga:
+  - audio/ogg
+  ogg:
+  - audio/ogg
+  spx:
+  - audio/ogg
+  s3m:
+  - audio/s3m
+  sil:
+  - audio/silk
+  uva:
+  - audio/vnd.dece.audio
+  uvva:
+  - audio/vnd.dece.audio
+  eol:
+  - audio/vnd.digital-winds
+  dra:
+  - audio/vnd.dra
+  dts:
+  - audio/vnd.dts
+  dtshd:
+  - audio/vnd.dts.hd
+  lvp:
+  - audio/vnd.lucent.voice
+  pya:
+  - audio/vnd.ms-playready.media.pya
+  ecelp4800:
+  - audio/vnd.nuera.ecelp4800
+  ecelp7470:
+  - audio/vnd.nuera.ecelp7470
+  ecelp9600:
+  - audio/vnd.nuera.ecelp9600
+  rip:
+  - audio/vnd.rip
+  weba:
+  - audio/webm
+  aac:
+  - audio/x-aac
+  aif:
+  - audio/x-aiff
+  aiff:
+  - audio/x-aiff
+  aifc:
+  - audio/x-aiff
+  caf:
+  - audio/x-caf
+  flac:
+  - audio/x-flac
+  mka:
+  - audio/x-matroska
+  m3u:
+  - audio/x-mpegurl
+  wax:
+  - audio/x-ms-wax
+  wma:
+  - audio/x-ms-wma
+  ram:
+  - audio/x-pn-realaudio
+  ra:
+  - audio/x-pn-realaudio
+  rmp:
+  - audio/x-pn-realaudio-plugin
+  wav:
+  - audio/x-wav
+  xm:
+  - audio/xm
+  cdx:
+  - chemical/x-cdx
+  cif:
+  - chemical/x-cif
+  cmdf:
+  - chemical/x-cmdf
+  cml:
+  - chemical/x-cml
+  csml:
+  - chemical/x-csml
+  xyz:
+  - chemical/x-xyz
+  woff:
+  - font/woff
+  woff2:
+  - font/woff2
+  cgm:
+  - image/cgm
+  g3:
+  - image/g3fax
+  gif:
+  - image/gif
+  ief:
+  - image/ief
+  ktx:
+  - image/ktx
+  png:
+  - image/png
+  btif:
+  - image/prs.btif
+  sgi:
+  - image/sgi
+  svg:
+  - image/svg+xml
+  svgz:
+  - image/svg+xml
+  tiff:
+  - image/tiff
+  tif:
+  - image/tiff
+  psd:
+  - image/vnd.adobe.photoshop
+  uvi:
+  - image/vnd.dece.graphic
+  uvvi:
+  - image/vnd.dece.graphic
+  uvg:
+  - image/vnd.dece.graphic
+  uvvg:
+  - image/vnd.dece.graphic
+  djvu:
+  - image/vnd.djvu
+  djv:
+  - image/vnd.djvu
+  sub:
+  - image/vnd.dvb.subtitle
+  - text/vnd.dvb.subtitle
+  dwg:
+  - image/vnd.dwg
+  dxf:
+  - image/vnd.dxf
+  fbs:
+  - image/vnd.fastbidsheet
+  fpx:
+  - image/vnd.fpx
+  fst:
+  - image/vnd.fst
+  mmr:
+  - image/vnd.fujixerox.edmics-mmr
+  rlc:
+  - image/vnd.fujixerox.edmics-rlc
+  mdi:
+  - image/vnd.ms-modi
+  wdp:
+  - image/vnd.ms-photo
+  npx:
+  - image/vnd.net-fpx
+  wbmp:
+  - image/vnd.wap.wbmp
+  xif:
+  - image/vnd.xiff
+  webp:
+  - image/webp
+  3ds:
+  - image/x-3ds
+  ras:
+  - image/x-cmu-raster
+  cmx:
+  - image/x-cmx
+  fh:
+  - image/x-freehand
+  fhc:
+  - image/x-freehand
+  fh4:
+  - image/x-freehand
+  fh5:
+  - image/x-freehand
+  fh7:
+  - image/x-freehand
+  ico:
+  - image/x-icon
+  sid:
+  - image/x-mrsid-image
+  pcx:
+  - image/x-pcx
+  pic:
+  - image/x-pict
+  pct:
+  - image/x-pict
+  pnm:
+  - image/x-portable-anymap
+  pbm:
+  - image/x-portable-bitmap
+  pgm:
+  - image/x-portable-graymap
+  ppm:
+  - image/x-portable-pixmap
+  rgb:
+  - image/x-rgb
+  tga:
+  - image/x-tga
+  xbm:
+  - image/x-xbitmap
+  xpm:
+  - image/x-xpixmap
+  xwd:
+  - image/x-xwindowdump
+  eml:
+  - message/rfc822
+  mime:
+  - message/rfc822
+  igs:
+  - model/iges
+  iges:
+  - model/iges
+  msh:
+  - model/mesh
+  mesh:
+  - model/mesh
+  silo:
+  - model/mesh
+  dae:
+  - model/vnd.collada+xml
+  dwf:
+  - model/vnd.dwf
+  gdl:
+  - model/vnd.gdl
+  gtw:
+  - model/vnd.gtw
+  mts:
+  - model/vnd.mts
+  vtu:
+  - model/vnd.vtu
+  wrl:
+  - model/vrml
+  vrml:
+  - model/vrml
+  x3db:
+  - model/x3d+binary
+  x3dbz:
+  - model/x3d+binary
+  x3dv:
+  - model/x3d+vrml
+  x3dvz:
+  - model/x3d+vrml
+  x3d:
+  - model/x3d+xml
+  x3dz:
+  - model/x3d+xml
+  appcache:
+  - text/cache-manifest
+  ics:
+  - text/calendar
+  ifb:
+  - text/calendar
+  css:
+  - text/css
+  csv:
+  - text/csv
+  html:
+  - text/html
+  htm:
+  - text/html
+  n3:
+  - text/n3
+  txt:
+  - text/plain
+  text:
+  - text/plain
+  conf:
+  - text/plain
+  def:
+  - text/plain
+  list:
+  - text/plain
+  log:
+  - text/plain
+  in:
+  - text/plain
+  dsc:
+  - text/prs.lines.tag
+  rtx:
+  - text/richtext
+  sgml:
+  - text/sgml
+  sgm:
+  - text/sgml
+  tsv:
+  - text/tab-separated-values
+  t:
+  - text/troff
+  tr:
+  - text/troff
+  roff:
+  - text/troff
+  man:
+  - text/troff
+  me:
+  - text/troff
+  ms:
+  - text/troff
+  ttl:
+  - text/turtle
+  uri:
+  - text/uri-list
+  uris:
+  - text/uri-list
+  urls:
+  - text/uri-list
+  vcard:
+  - text/vcard
+  curl:
+  - text/vnd.curl
+  dcurl:
+  - text/vnd.curl.dcurl
+  mcurl:
+  - text/vnd.curl.mcurl
+  scurl:
+  - text/vnd.curl.scurl
+  fly:
+  - text/vnd.fly
+  flx:
+  - text/vnd.fmi.flexstor
+  gv:
+  - text/vnd.graphviz
+  3dml:
+  - text/vnd.in3d.3dml
+  spot:
+  - text/vnd.in3d.spot
+  jad:
+  - text/vnd.sun.j2me.app-descriptor
+  wml:
+  - text/vnd.wap.wml
+  wmls:
+  - text/vnd.wap.wmlscript
+  s:
+  - text/x-asm
+  asm:
+  - text/x-asm
+  c:
+  - text/x-c
+  cc:
+  - text/x-c
+  cxx:
+  - text/x-c
+  cpp:
+  - text/x-c
+  h:
+  - text/x-c
+  hh:
+  - text/x-c
+  dic:
+  - text/x-c
+  f:
+  - text/x-fortran
+  for:
+  - text/x-fortran
+  f77:
+  - text/x-fortran
+  f90:
+  - text/x-fortran
+  java:
+  - text/x-java-source
+  nfo:
+  - text/x-nfo
+  opml:
+  - text/x-opml
+  p:
+  - text/x-pascal
+  pas:
+  - text/x-pascal
+  etx:
+  - text/x-setext
+  sfv:
+  - text/x-sfv
+  uu:
+  - text/x-uuencode
+  vcs:
+  - text/x-vcalendar
+  vcf:
+  - text/x-vcard
+  3gp:
+  - video/3gpp
+  3g2:
+  - video/3gpp2
+  h261:
+  - video/h261
+  h263:
+  - video/h263
+  h264:
+  - video/h264
+  jpgv:
+  - video/jpeg
+  jpm:
+  - video/jpm
+  jpgm:
+  - video/jpm
+  mj2:
+  - video/mj2
+  mjp2:
+  - video/mj2
+  mp4:
+  - video/mp4
+  mp4v:
+  - video/mp4
+  mpg4:
+  - video/mp4
+  mpeg:
+  - video/mpeg
+  mpg:
+  - video/mpeg
+  mpe:
+  - video/mpeg
+  m1v:
+  - video/mpeg
+  m2v:
+  - video/mpeg
+  ogv:
+  - video/ogg
+  qt:
+  - video/quicktime
+  mov:
+  - video/quicktime
+  uvh:
+  - video/vnd.dece.hd
+  uvvh:
+  - video/vnd.dece.hd
+  uvm:
+  - video/vnd.dece.mobile
+  uvvm:
+  - video/vnd.dece.mobile
+  uvp:
+  - video/vnd.dece.pd
+  uvvp:
+  - video/vnd.dece.pd
+  uvs:
+  - video/vnd.dece.sd
+  uvvs:
+  - video/vnd.dece.sd
+  uvv:
+  - video/vnd.dece.video
+  uvvv:
+  - video/vnd.dece.video
+  dvb:
+  - video/vnd.dvb.file
+  fvt:
+  - video/vnd.fvt
+  mxu:
+  - video/vnd.mpegurl
+  m4u:
+  - video/vnd.mpegurl
+  pyv:
+  - video/vnd.ms-playready.media.pyv
+  uvu:
+  - video/vnd.uvvu.mp4
+  uvvu:
+  - video/vnd.uvvu.mp4
+  viv:
+  - video/vnd.vivo
+  webm:
+  - video/webm
+  f4v:
+  - video/x-f4v
+  fli:
+  - video/x-fli
+  flv:
+  - video/x-flv
+  m4v:
+  - video/x-m4v
+  mkv:
+  - video/x-matroska
+  mk3d:
+  - video/x-matroska
+  mks:
+  - video/x-matroska
+  mng:
+  - video/x-mng
+  asf:
+  - video/x-ms-asf
+  asx:
+  - video/x-ms-asf
+  vob:
+  - video/x-ms-vob
+  wm:
+  - video/x-ms-wm
+  wmv:
+  - video/x-ms-wmv
+  wmx:
+  - video/x-ms-wmx
+  wvx:
+  - video/x-ms-wvx
+  avi:
+  - video/x-msvideo
+  movie:
+  - video/x-sgi-movie
+  smv:
+  - video/x-smv
+  ice:
+  - x-conference/x-cooltalk

+ 2 - 2
system/config/system.yaml

@@ -96,7 +96,7 @@ cache:
   purge_at: '0 4 * * *'                          # How often to purge old file cache (using new scheduler)
   clear_at: '0 3 * * *'                           # How often to clear cache (using new scheduler)
   clear_job_type: 'standard'                     # Type to clear when processing the scheduled clear job `standard`|`all`
-  clear_images_by_default: false                  # By default grav will include processed images in cache clear, this can be disabled
+  clear_images_by_default: false                  # By default grav does not include processed images in cache clear, this can be enabled
   cli_compatibility: false                       # Ensures only non-volatile drivers are used (file, redis, memcache, etc.)
   lifetime: 604800                               # Lifetime of cached data in seconds (0 = infinite)
   gzip: false                                    # GZip compress the page output
@@ -131,7 +131,7 @@ assets:                                          # Configuration for Assets Mana
   enable_asset_timestamp: false                  # Enable asset timestamps
   enable_asset_sri: false                        # Enable asset SRI
   collections:
-    jquery: system://assets/jquery/jquery-2.x.min.js
+    jquery: system://assets/jquery/jquery-3.x.min.js
 
 errors:
   display: 0                                     # Display either (1) Full backtrace | (0) Simple Error | (-1) System Error

+ 1 - 1
system/defines.php

@@ -9,7 +9,7 @@
 
 // Some standard defines
 define('GRAV', true);
-define('GRAV_VERSION', '1.7.16');
+define('GRAV_VERSION', '1.7.21');
 define('GRAV_SCHEMA', '1.7.0_2020-11-20_1');
 define('GRAV_TESTING', false);
 

+ 21 - 7
system/src/Grav/Common/Assets.php

@@ -110,7 +110,7 @@ class Assets extends PropertyObject
 
         /** @var UniformResourceLocator $locator */
         $locator = $grav['locator'];
-        $this->assets_dir = $locator->findResource('asset://') . DS;
+        $this->assets_dir = $locator->findResource('asset://');
         $this->assets_url = $locator->findResource('asset://', false);
 
         $this->config($asset_config);
@@ -164,10 +164,19 @@ class Assets extends PropertyObject
 
         // More than one asset
         if (is_array($asset)) {
-            foreach ($asset as $a) {
-                array_shift($args);
-                $args = array_merge([$a], $args);
-                call_user_func_array([$this, 'add'], $args);
+            foreach ($asset as $index => $location) {
+                $params = array_slice($args, 1);
+                if (is_array($location)) {
+                    $params = array_shift($params);
+                    if (is_numeric($params)) {
+                        $params = [ 'priority' => $params ];
+                    }
+                    $params = [array_replace_recursive([], $location, $params)];
+                    $location = $index;
+                }
+
+                $params = array_merge([$location], $params);
+                call_user_func_array([$this, 'add'], $params);
             }
         } elseif (isset($this->collections[$asset])) {
             array_shift($args);
@@ -201,8 +210,13 @@ class Assets extends PropertyObject
     protected function addType($collection, $type, $asset, $options)
     {
         if (is_array($asset)) {
-            foreach ($asset as $a) {
-                $this->addType($collection, $type, $a, $options);
+            foreach ($asset as $index => $location) {
+                $assetOptions = $options;
+                if (is_array($location)) {
+                    $assetOptions = array_replace_recursive([], $options, $location);
+                    $location = $index;
+                }
+                $this->addType($collection, $type, $location, $assetOptions);
             }
 
             return $this;

+ 17 - 12
system/src/Grav/Common/Assets/Pipeline.php

@@ -9,9 +9,9 @@
 
 namespace Grav\Common\Assets;
 
-use Grav\Common\Assets\BaseAsset;
 use Grav\Common\Assets\Traits\AssetUtilsTrait;
 use Grav\Common\Config\Config;
+use Grav\Common\Filesystem\Folder;
 use Grav\Common\Grav;
 use Grav\Common\Uri;
 use Grav\Common\Utils;
@@ -88,7 +88,14 @@ class Pipeline extends PropertyObject
         $uri = Grav::instance()['uri'];
 
         $this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/';
-        $this->assets_dir = $locator->findResource('asset://') . DS;
+        $this->assets_dir = $locator->findResource('asset://');
+        if (!$this->assets_dir) {
+            // Attempt to create assets folder if it doesn't exist yet.
+            $this->assets_dir = $locator->findResource('asset://', true, true);
+            Folder::mkdir($this->assets_dir);
+            $locator->clearCache();
+        }
+
         $this->assets_url = $locator->findResource('asset://', false);
     }
 
@@ -119,10 +126,9 @@ class Pipeline extends PropertyObject
         $file = $uid . '.css';
         $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
 
-        $buffer = null;
-
-        if (file_exists($this->assets_dir . $file)) {
-            $buffer = file_get_contents($this->assets_dir . $file) . "\n";
+        $filepath = "{$this->assets_dir}/{$file}";
+        if (file_exists($filepath)) {
+            $buffer = file_get_contents($filepath) . "\n";
         } else {
             //if nothing found get out of here!
             if (empty($assets)) {
@@ -141,7 +147,7 @@ class Pipeline extends PropertyObject
 
             // Write file
             if (trim($buffer) !== '') {
-                file_put_contents($this->assets_dir . $file, $buffer);
+                file_put_contents($filepath, $buffer);
             }
         }
 
@@ -182,10 +188,9 @@ class Pipeline extends PropertyObject
         $file = $uid . '.js';
         $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
 
-        $buffer = null;
-
-        if (file_exists($this->assets_dir . $file)) {
-            $buffer = file_get_contents($this->assets_dir . $file) . "\n";
+        $filepath = "{$this->assets_dir}/{$file}";
+        if (file_exists($filepath)) {
+            $buffer = file_get_contents($filepath) . "\n";
         } else {
             //if nothing found get out of here!
             if (empty($assets)) {
@@ -204,7 +209,7 @@ class Pipeline extends PropertyObject
 
             // Write file
             if (trim($buffer) !== '') {
-                file_put_contents($this->assets_dir . $file, $buffer);
+                file_put_contents($filepath, $buffer);
             }
         }
 

+ 4 - 0
system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php

@@ -156,6 +156,10 @@ trait AssetUtilsTrait
         $no_key = ['loading'];
 
         foreach ($this->attributes as $key => $value) {
+            if ($value === null) {
+                continue;
+            }
+
             if (is_numeric($key)) {
                 $key = $value;
             }

+ 13 - 3
system/src/Grav/Common/Config/Setup.php

@@ -41,6 +41,9 @@ class Setup extends Data
      */
     public static $environment;
 
+    /** @var string */
+    public static $securityFile = 'config://security.yaml';
+
     /** @var array */
     protected $streams = [
         'user' => [
@@ -390,12 +393,19 @@ class Setup extends Data
 
             if (!$locator->findResource('environment://config', true)) {
                 // If environment does not have its own directory, remove it from the lookup.
-                $this->set('streams.schemes.environment.prefixes', ['config' => []]);
+                $prefixes = $this->get('streams.schemes.environment.prefixes');
+                $prefixes['config'] = [];
+
+                $this->set('streams.schemes.environment.prefixes', $prefixes);
                 $this->initializeLocator($locator);
             }
 
-            // Create security.yaml if it doesn't exist.
-            $filename = $locator->findResource('config://security.yaml', true, true);
+            // Create security.yaml salt if it doesn't exist into existing configuration environment if possible.
+            $securityFile = basename(static::$securityFile);
+            $securityFolder = substr(static::$securityFile, 0, -\strlen($securityFile));
+            $securityFolder = $locator->findResource($securityFolder, true) ?: $locator->findResource($securityFolder, true, true);
+            $filename = "{$securityFolder}/{$securityFile}";
+
             $security_file = CompiledYamlFile::instance($filename);
             $security_content = (array)$security_file->content();
 

+ 21 - 6
system/src/Grav/Common/Data/Validation.php

@@ -519,17 +519,32 @@ class Validation
             return false;
         }
 
-        if (isset($params['min']) && $value < $params['min']) {
-            return false;
+        $value = (float)$value;
+
+        $min = 0;
+        if (isset($params['min'])) {
+            $min = (float)$params['min'];
+            if ($value < $min) {
+                return false;
+            }
         }
 
-        if (isset($params['max']) && $value > $params['max']) {
-            return false;
+        if (isset($params['max'])) {
+            $max = (float)$params['max'];
+            if ($value > $max) {
+                return false;
+            }
         }
 
-        $min = $params['min'] ?? 0;
+        if (isset($params['step'])) {
+            $step = (float)$params['step'];
+            // Count of how many steps we are above/below the minimum value.
+            $pos = ($value - $min) / $step;
 
-        return !(isset($params['step']) && fmod($value - $min, $params['step']) === 0);
+            return is_int(static::filterNumber($pos, $params, $field));
+        }
+
+        return true;
     }
 
     /**

+ 2 - 1
system/src/Grav/Common/Flex/FlexObject.php

@@ -13,6 +13,7 @@ namespace Grav\Common\Flex;
 
 use Grav\Common\Flex\Traits\FlexGravTrait;
 use Grav\Common\Flex\Traits\FlexObjectTrait;
+use Grav\Common\Media\Interfaces\MediaInterface;
 use Grav\Framework\Flex\Traits\FlexMediaTrait;
 use function is_array;
 
@@ -21,7 +22,7 @@ use function is_array;
  *
  * @package Grav\Common\Flex
  */
-abstract class FlexObject extends \Grav\Framework\Flex\FlexObject
+abstract class FlexObject extends \Grav\Framework\Flex\FlexObject implements MediaInterface
 {
     use FlexGravTrait;
     use FlexObjectTrait;

+ 8 - 0
system/src/Grav/Common/Flex/Types/Pages/PageCollection.php

@@ -192,6 +192,14 @@ class PageCollection extends FlexPageCollection implements PageCollectionInterfa
         throw new RuntimeException(__METHOD__ . '(): Not Implemented');
     }
 
+    /**
+     * Set current page.
+     */
+    public function setCurrent(string $path): void
+    {
+        throw new RuntimeException(__METHOD__ . '(): Not Implemented');
+    }
+
     /**
      * Return previous item.
      *

+ 3 - 3
system/src/Grav/Common/Flex/Types/Pages/PageIndex.php

@@ -674,12 +674,12 @@ class PageIndex extends FlexPageIndex implements PageCollectionInterface
                     $count = $filters ? $tmp->filterBy($filters, true)->count() : null;
                     $route = $child->getRoute();
                     $payload = [
-                        'item-key' => basename($child->rawRoute() ?? $child->getKey()),
+                        'item-key' => htmlspecialchars(basename($child->rawRoute() ?? $child->getKey())),
                         'icon' => $icon,
                         'title' => htmlspecialchars($child->menu()),
                         'route' => [
-                            'display' => ($route ? ($route->toString(false) ?: '/') : null) ?? '',
-                            'raw' => $child->rawRoute(),
+                            'display' => htmlspecialchars(($route ? ($route->toString(false) ?: '/') : null) ?? ''),
+                            'raw' => htmlspecialchars($child->rawRoute()),
                         ],
                         'modified' => $this->jsDate($child->modified()),
                         'child_count' => $child_count ?: null,

+ 32 - 4
system/src/Grav/Common/Flex/Types/Pages/PageObject.php

@@ -262,6 +262,24 @@ class PageObject extends FlexPageObject
         $this->getFlexDirectory()->reloadIndex();
     }
 
+    /**
+     * @param UserInterface|null $user
+     */
+    public function check(UserInterface $user = null): void
+    {
+        parent::check($user);
+
+        if ($user && $this->isMoved()) {
+            $parentKey = $this->getProperty('parent_key');
+
+            /** @var PageObject|null $parent */
+            $parent = $this->getFlexDirectory()->getObject($parentKey, 'storage_key');
+            if (!$parent || !$parent->isAuthorized('create', null, $user)) {
+                throw new \RuntimeException('Forbidden', 403);
+            }
+        }
+    }
+
     /**
      * @param array|bool $reorder
      * @return FlexObject|FlexObjectInterface
@@ -358,16 +376,26 @@ class PageObject extends FlexPageObject
     }
 
     /**
-     * @param array $ordering
-     * @return PageCollection|null
+     * @return bool
      */
-    protected function reorderSiblings(array $ordering)
+    protected function isMoved(): bool
     {
         $storageKey = $this->getMasterKey();
         $filesystem = Filesystem::getInstance(false);
         $oldParentKey = ltrim($filesystem->dirname("/{$storageKey}"), '/');
         $newParentKey = $this->getProperty('parent_key');
-        $isMoved = $this->exists() && $oldParentKey !== $newParentKey;
+
+        return $this->exists() && $oldParentKey !== $newParentKey;
+    }
+
+    /**
+     * @param array $ordering
+     * @return PageCollection|null
+     */
+    protected function reorderSiblings(array $ordering)
+    {
+        $storageKey = $this->getMasterKey();
+        $isMoved = $this->isMoved();
         $order = !$isMoved ? $this->order() : false;
         if ($order !== false) {
             $order = (int)$order;

+ 8 - 0
system/src/Grav/Common/Flex/Types/UserGroups/UserGroupObject.php

@@ -41,6 +41,14 @@ class UserGroupObject extends FlexObject implements UserGroupInterface
         ] + parent::getCachedMethods();
     }
 
+    /**
+     * @return string
+     */
+    public function getTitle(): string
+    {
+        return $this->getProperty('readableName');
+    }
+
     /**
      * Checks user authorization to the action.
      *

+ 33 - 3
system/src/Grav/Common/Flex/Types/Users/UserObject.php

@@ -11,6 +11,7 @@ declare(strict_types=1);
 
 namespace Grav\Common\Flex\Types\Users;
 
+use Closure;
 use Countable;
 use Grav\Common\Config\Config;
 use Grav\Common\Data\Blueprint;
@@ -31,6 +32,7 @@ use Grav\Common\User\Interfaces\UserInterface;
 use Grav\Common\User\Traits\UserTrait;
 use Grav\Framework\File\Formatter\JsonFormatter;
 use Grav\Framework\File\Formatter\YamlFormatter;
+use Grav\Framework\Filesystem\Filesystem;
 use Grav\Framework\Flex\Flex;
 use Grav\Framework\Flex\FlexDirectory;
 use Grav\Framework\Flex\Storage\FileStorage;
@@ -75,6 +77,9 @@ class UserObject extends FlexObject implements UserInterface, Countable
     use UserTrait;
     use UserObjectLegacyTrait;
 
+    /** @var Closure|null */
+    static public $authorizeCallable;
+
     /** @var array|null */
     protected $_uploads_original;
     /** @var FileInterface|null */
@@ -259,6 +264,15 @@ class UserObject extends FlexObject implements UserInterface, Countable
             }
         }
 
+        $authorizeCallable = static::$authorizeCallable;
+        if ($authorizeCallable instanceof Closure) {
+            $authorizeCallable->bindTo($this);
+            $authorized = $authorizeCallable($action, $scope);
+            if (is_bool($authorized)) {
+                return $authorized;
+            }
+        }
+
         // Check user access.
         $access = $this->getAccess();
         $authorized = $access->authorize($action, $scope);
@@ -292,6 +306,14 @@ class UserObject extends FlexObject implements UserInterface, Countable
         return $value;
     }
 
+    /**
+     * @return UserGroupIndex
+     */
+    public function getRoles(): UserGroupIndex
+    {
+        return $this->getGroups();
+    }
+
     /**
      * Convert object into an array.
      *
@@ -689,6 +711,7 @@ class UserObject extends FlexObject implements UserInterface, Countable
 
     /**
      * @param array $files
+     * @return void
      */
     protected function setUpdatedMedia(array $files): void
     {
@@ -700,9 +723,12 @@ class UserObject extends FlexObject implements UserInterface, Countable
             return;
         }
 
+        $filesystem = Filesystem::getInstance(false);
+
         $list = [];
         $list_original = [];
         foreach ($files as $field => $group) {
+            // Ignore files without a field.
             if ($field === '') {
                 continue;
             }
@@ -724,7 +750,7 @@ class UserObject extends FlexObject implements UserInterface, Countable
                 }
 
                 if ($file) {
-                    // Check file upload against media limits.
+                    // Check file upload against media limits (except for max size).
                     $filename = $media->checkUploadedFile($file, $filename, ['filesize' => 0] + $settings);
                 }
 
@@ -748,15 +774,19 @@ class UserObject extends FlexObject implements UserInterface, Countable
                     continue;
                 }
 
+                // Calculate path without the retina scaling factor.
+                $realpath = $filesystem->pathname($filepath) . str_replace(['@3x', '@2x'], '', basename($filepath));
+
                 $list[$filename] = [$file, $settings];
 
+                $path = str_replace('.', "\n", $field);
                 if (null !== $data) {
                     $data['name'] = $filename;
                     $data['path'] = $filepath;
 
-                    $this->setNestedProperty("{$field}\n{$filepath}", $data, "\n");
+                    $this->setNestedProperty("{$path}\n{$realpath}", $data, "\n");
                 } else {
-                    $this->unsetNestedProperty("{$field}\n{$filepath}", "\n");
+                    $this->unsetNestedProperty("{$path}\n{$realpath}", "\n");
                 }
             }
         }

+ 99 - 37
system/src/Grav/Common/GPM/GPM.php

@@ -35,7 +35,11 @@ class GPM extends Iterator
     /** @var Remote\Packages|null Remote available Packages */
     private $repository;
     /** @var Remote\GravCore|null Remove Grav Packages */
-    public $grav;
+    private $grav;
+    /** @var bool */
+    private $refresh;
+    /** @var callable|null */
+    private $callback;
 
     /** @var array Internal cache */
     protected $cache;
@@ -55,13 +59,45 @@ class GPM extends Iterator
     public function __construct($refresh = false, $callback = null)
     {
         parent::__construct();
+
+        Folder::create(CACHE_DIR . '/gpm');
+
         $this->cache = [];
         $this->installed = new Local\Packages();
-        try {
-            $this->repository = new Remote\Packages($refresh, $callback);
-            $this->grav = new Remote\GravCore($refresh, $callback);
-        } catch (Exception $e) {
+        $this->refresh = $refresh;
+        $this->callback = $callback;
+    }
+
+    /**
+     * Magic getter method
+     *
+     * @param string $offset Asset name value
+     * @return mixed Asset value
+     */
+    public function __get($offset)
+    {
+        switch ($offset) {
+            case 'grav':
+                return $this->getGrav();
+        }
+
+        return parent::__get($offset);
+    }
+
+    /**
+     * Magic method to determine if the attribute is set
+     *
+     * @param string $offset Asset name value
+     * @return bool True if the value is set
+     */
+    public function __isset($offset)
+    {
+        switch ($offset) {
+            case 'grav':
+                return $this->getGrav() !== null;
         }
+
+        return parent::__isset($offset);
     }
 
     /**
@@ -266,11 +302,12 @@ class GPM extends Iterator
     {
         $items = [];
 
-        if (null === $this->repository) {
+        $repository = $this->getRepository();
+        if (null === $repository) {
             return $items;
         }
 
-        $repository = $this->repository['plugins'];
+        $plugins = $repository['plugins'];
 
         // local cache to speed things up
         if (isset($this->cache[__METHOD__])) {
@@ -278,18 +315,18 @@ class GPM extends Iterator
         }
 
         foreach ($this->installed['plugins'] as $slug => $plugin) {
-            if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
+            if (!isset($plugins[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
                 continue;
             }
 
             $local_version = $plugin->version ?? 'Unknown';
-            $remote_version = $repository[$slug]->version;
+            $remote_version = $plugins[$slug]->version;
 
             if (version_compare($local_version, $remote_version) < 0) {
-                $repository[$slug]->available = $remote_version;
-                $repository[$slug]->version = $local_version;
-                $repository[$slug]->type = $repository[$slug]->release_type;
-                $items[$slug] = $repository[$slug];
+                $plugins[$slug]->available = $remote_version;
+                $plugins[$slug]->version = $local_version;
+                $plugins[$slug]->type = $plugins[$slug]->release_type;
+                $items[$slug] = $plugins[$slug];
             }
         }
 
@@ -306,19 +343,20 @@ class GPM extends Iterator
      */
     public function getLatestVersionOfPackage($package_name)
     {
-        if (null === $this->repository) {
+        $repository = $this->getRepository();
+        if (null === $repository) {
             return null;
         }
 
-        $repository = $this->repository['plugins'];
-        if (isset($repository[$package_name])) {
-            return $repository[$package_name]->available ?: $repository[$package_name]->version;
+        $plugins = $repository['plugins'];
+        if (isset($plugins[$package_name])) {
+            return $plugins[$package_name]->available ?: $plugins[$package_name]->version;
         }
 
         //Not a plugin, it's a theme?
-        $repository = $this->repository['themes'];
-        if (isset($repository[$package_name])) {
-            return $repository[$package_name]->available ?: $repository[$package_name]->version;
+        $themes = $repository['themes'];
+        if (isset($themes[$package_name])) {
+            return $themes[$package_name]->available ?: $themes[$package_name]->version;
         }
 
         return null;
@@ -356,11 +394,12 @@ class GPM extends Iterator
     {
         $items = [];
 
-        if (null === $this->repository) {
+        $repository = $this->getRepository();
+        if (null === $repository) {
             return $items;
         }
 
-        $repository = $this->repository['themes'];
+        $themes = $repository['themes'];
 
         // local cache to speed things up
         if (isset($this->cache[__METHOD__])) {
@@ -368,18 +407,18 @@ class GPM extends Iterator
         }
 
         foreach ($this->installed['themes'] as $slug => $plugin) {
-            if (!isset($repository[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
+            if (!isset($themes[$slug]) || $plugin->symlink || !$plugin->version || $plugin->gpm === false) {
                 continue;
             }
 
             $local_version = $plugin->version ?? 'Unknown';
-            $remote_version = $repository[$slug]->version;
+            $remote_version = $themes[$slug]->version;
 
             if (version_compare($local_version, $remote_version) < 0) {
-                $repository[$slug]->available = $remote_version;
-                $repository[$slug]->version = $local_version;
-                $repository[$slug]->type = $repository[$slug]->release_type;
-                $items[$slug] = $repository[$slug];
+                $themes[$slug]->available = $remote_version;
+                $themes[$slug]->version = $local_version;
+                $themes[$slug]->type = $themes[$slug]->release_type;
+                $items[$slug] = $themes[$slug];
             }
         }
 
@@ -407,19 +446,20 @@ class GPM extends Iterator
      */
     public function getReleaseType($package_name)
     {
-        if (null === $this->repository) {
+        $repository = $this->getRepository();
+        if (null === $repository) {
             return null;
         }
 
-        $repository = $this->repository['plugins'];
-        if (isset($repository[$package_name])) {
-            return $repository[$package_name]->release_type;
+        $plugins = $repository['plugins'];
+        if (isset($plugins[$package_name])) {
+            return $plugins[$package_name]->release_type;
         }
 
         //Not a plugin, it's a theme?
-        $repository = $this->repository['themes'];
-        if (isset($repository[$package_name])) {
-            return $repository[$package_name]->release_type;
+        $themes = $repository['themes'];
+        if (isset($themes[$package_name])) {
+            return $themes[$package_name]->release_type;
         }
 
         return null;
@@ -470,7 +510,7 @@ class GPM extends Iterator
      */
     public function getRepositoryPlugins()
     {
-        return $this->repository['plugins'] ?? null;
+        return $this->getRepository()['plugins'] ?? null;
     }
 
     /**
@@ -493,7 +533,7 @@ class GPM extends Iterator
      */
     public function getRepositoryThemes()
     {
-        return $this->repository['themes'] ?? null;
+        return $this->getRepository()['themes'] ?? null;
     }
 
     /**
@@ -504,9 +544,31 @@ class GPM extends Iterator
      */
     public function getRepository()
     {
+        if (null === $this->repository) {
+            try {
+                $this->repository = new Remote\Packages($this->refresh, $this->callback);
+            } catch (Exception $e) {}
+        }
+
         return $this->repository;
     }
 
+    /**
+     * Returns Grav version available in the repository
+     *
+     * @return Remote\GravCore|null
+     */
+    public function getGrav()
+    {
+        if (null === $this->grav) {
+            try {
+                $this->grav = new Remote\GravCore($this->refresh, $this->callback);
+            } catch (Exception $e) {}
+        }
+
+        return $this->grav;
+    }
+
     /**
      * Searches for a Package in the repository
      *

+ 8 - 0
system/src/Grav/Common/Grav.php

@@ -9,6 +9,7 @@
 
 namespace Grav\Common;
 
+use Composer\Autoload\ClassLoader;
 use Grav\Common\Config\Config;
 use Grav\Common\Config\Setup;
 use Grav\Common\Helpers\Exif;
@@ -152,6 +153,13 @@ class Grav extends Container
     {
         if (null === self::$instance) {
             self::$instance = static::load($values);
+
+            /** @var ClassLoader|null $loader */
+            $loader = self::$instance['loader'] ?? null;
+            if ($loader) {
+                // Load fix for Deferred Twig Extension
+                $loader->addPsr4('Phive\\Twig\\Extensions\\Deferred\\', LIB_DIR . 'Phive/Twig/Extensions/Deferred/', true);
+            }
         } elseif ($values) {
             $instance = self::$instance;
             foreach ($values as $key => $value) {

+ 22 - 16
system/src/Grav/Common/Media/Traits/MediaUploadTrait.php

@@ -20,11 +20,13 @@ use Grav\Common\Security;
 use Grav\Common\Utils;
 use Grav\Framework\Filesystem\Filesystem;
 use Grav\Framework\Form\FormFlashFile;
+use Grav\Framework\Mime\MimeTypes;
 use Psr\Http\Message\UploadedFileInterface;
 use RocketTheme\Toolbox\File\YamlFile;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 use RuntimeException;
 use function dirname;
+use function in_array;
 
 /**
  * Implements media upload and delete functionality.
@@ -179,16 +181,20 @@ trait MediaUploadTrait
             }
         }
 
+        $grav = Grav::instance();
+        /** @var MimeTypes $mimeChecker */
+        $mimeChecker = $grav['mime'];
+
         // Handle Accepted file types. Accept can only be mime types (image/png | image/*) or file extensions (.pdf | .jpg)
-        $accepted = false;
-        $errors = [];
         // Do not trust mime type sent by the browser.
-        $mime = Utils::getMimeByFilename($filename);
-        $mimeTest = $metadata['mime'] ?? $mime;
-        if ($mime !== $mimeTest) {
+        $mime = $metadata['mime'] ?? $mimeChecker->getMimeType($extension);
+        $validExtensions = $mimeChecker->getExtensions($mime);
+        if (!in_array($extension, $validExtensions, true)) {
             throw new RuntimeException('The mime type does not match to file extension', 400);
         }
 
+        $accepted = false;
+        $errors = [];
         foreach ((array)$settings['accept'] as $type) {
             // Force acceptance of any file when star notation
             if ($type === '*') {
@@ -418,6 +424,17 @@ trait MediaUploadTrait
         $uploadedFile->moveTo($filepath);
     }
 
+    /**
+     * Get upload settings.
+     *
+     * @param array|null $settings Form field specific settings (override).
+     * @return array
+     */
+    public function getUploadSettings(?array $settings = null): array
+    {
+        return null !== $settings ? $settings + $this->_upload_defaults : $this->_upload_defaults;
+    }
+
     /**
      * Internal logic to copy file.
      *
@@ -604,17 +621,6 @@ trait MediaUploadTrait
         }
     }
 
-    /**
-     * Get upload settings.
-     *
-     * @param array|null $settings Form field specific settings (override).
-     * @return array
-     */
-    protected function getUploadSettings(?array $settings = null): array
-    {
-        return null !== $settings ? $settings + $this->_upload_defaults : $this->_upload_defaults;
-    }
-
     /**
      * @param string $filename
      * @param string $path

+ 12 - 0
system/src/Grav/Common/Page/Collection.php

@@ -145,6 +145,18 @@ class Collection extends Iterator implements PageCollectionInterface
         return $this;
     }
 
+    /**
+     * Set current page.
+     */
+    public function setCurrent(string $path): void
+    {
+        reset($this->items);
+
+        while (($key = key($this->items)) !== null && $key !== $path) {
+            next($this->items);
+        }
+    }
+
     /**
      * Returns current page.
      *

+ 3 - 2
system/src/Grav/Common/Page/Media.php

@@ -102,12 +102,13 @@ class Media extends AbstractMedia
 
         foreach ($iterator as $file => $info) {
             // Ignore folders and Markdown files.
-            if (!$info->isFile() || $info->getExtension() === 'md' || strpos($info->getFilename(), '.') === 0) {
+            $filename = $info->getFilename();
+            if (!$info->isFile() || $info->getExtension() === 'md' || $filename === 'frontmatter.yaml' || strpos($filename, '.') === 0) {
                 continue;
             }
 
             // Find out what type we're dealing with
-            [$basename, $ext, $type, $extra] = $this->getFileParts($info->getFilename());
+            [$basename, $ext, $type, $extra] = $this->getFileParts($filename);
 
             if (!in_array(strtolower($ext), $media_types, true)) {
                 continue;

+ 3 - 3
system/src/Grav/Common/Processors/InitializeProcessor.php

@@ -105,12 +105,12 @@ class InitializeProcessor extends ProcessorBase
         // TODO: remove in 2.0.
         $this->container['accounts'];
 
-        // Initialize session.
-        $this->initializeSession($config);
-
         // Initialize URI (uses session, see issue #3269).
         $this->initializeUri($config);
 
+        // Initialize session.
+        $this->initializeSession($config);
+
         // Grav may return redirect response right away.
         $redirectCode = (int)$config->get('system.pages.redirect_trailing_slash', 1);
         if ($redirectCode) {

+ 20 - 3
system/src/Grav/Common/Processors/PagesProcessor.php

@@ -10,6 +10,8 @@
 namespace Grav\Common\Processors;
 
 use Grav\Common\Page\Interfaces\PageInterface;
+use Grav\Framework\RequestHandler\Exception\RequestException;
+use Grav\Plugin\Form\Forms;
 use RocketTheme\Toolbox\Event\Event;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
@@ -47,8 +49,17 @@ class PagesProcessor extends ProcessorBase
         $page = $this->container['page'];
 
         if (!$page->routable()) {
+            $exception = new RequestException($request, 'Page Not Found', 404);
+            $route = $this->container['route'];
             // If no page found, fire event
-            $event = new Event(['page' => $page]);
+            $event = new Event([
+                'page' => $page,
+                'code' => $exception->getCode(),
+                'message' => $exception->getMessage(),
+                'exception' => $exception,
+                'route' => $route,
+                'request' => $request
+            ]);
             $event->page = null;
             $event = $this->container->fireEvent('onPageNotFound', $event);
 
@@ -65,12 +76,18 @@ class PagesProcessor extends ProcessorBase
 
             $task = $this->container['task'];
             $action = $this->container['action'];
+
+            /** @var Forms $forms */
+            $forms = $this->container['forms'] ?? null;
+            $form = $forms ? $forms->getActiveForm() : null;
+
+            $options = ['page' => $page, 'form' => $form, 'request' => $request];
             if ($task) {
-                $event = new Event(['task' => $task, 'page' => $page]);
+                $event = new Event(['task' => $task] + $options);
                 $this->container->fireEvent('onPageTask', $event);
                 $this->container->fireEvent('onPageTask.' . $task, $event);
             } elseif ($action) {
-                $event = new Event(['action' => $action, 'page' => $page]);
+                $event = new Event(['action' => $action] + $options);
                 $this->container->fireEvent('onPageAction', $event);
                 $this->container->fireEvent('onPageAction.' . $action, $event);
             }

+ 3 - 1
system/src/Grav/Common/Scheduler/Job.php

@@ -390,7 +390,9 @@ class Job
         if (count($this->outputTo) > 0) {
             foreach ($this->outputTo as $file) {
                 $output_mode = $this->outputMode === 'append' ? FILE_APPEND | LOCK_EX : LOCK_EX;
-                file_put_contents($file, $this->output, $output_mode);
+                $timestamp = (new DateTime('now'))->format('c');
+                $output = $timestamp . "\n" . str_pad('', strlen($timestamp), '>') . "\n" . $this->output;
+                file_put_contents($file, $output, $output_mode);
             }
         }
 

+ 10 - 2
system/src/Grav/Common/Security.php

@@ -12,6 +12,7 @@ namespace Grav\Common;
 use enshrined\svgSanitize\Sanitizer;
 use Exception;
 use Grav\Common\Config\Config;
+use Grav\Common\Filesystem\Folder;
 use Grav\Common\Page\Pages;
 use function chr;
 use function count;
@@ -56,9 +57,16 @@ class Security
             $original_svg = file_get_contents($file);
             $clean_svg = $sanitizer->sanitize($original_svg);
 
-            // TODO: what to do with bad SVG files which return false?
-            if ($clean_svg !== false && $clean_svg !== $original_svg) {
+            // Quarantine bad SVG files and throw exception
+            if ($clean_svg !== false ) {
                 file_put_contents($file, $clean_svg);
+            } else {
+                $quarantine_file = basename($file);
+                $quarantine_dir = 'log://quarantine';
+                Folder::mkdir($quarantine_dir);
+                file_put_contents("$quarantine_dir/$quarantine_file", $original_svg);
+                unlink($file);
+                throw new Exception('SVG could not be sanitized, it has been moved to the logs/quarantine folder');
             }
         }
     }

+ 14 - 0
system/src/Grav/Common/Service/ConfigServiceProvider.php

@@ -17,6 +17,7 @@ use Grav\Common\Config\Config;
 use Grav\Common\Config\ConfigFileFinder;
 use Grav\Common\Config\Setup;
 use Grav\Common\Language\Language;
+use Grav\Framework\Mime\MimeTypes;
 use Pimple\Container;
 use Pimple\ServiceProviderInterface;
 use RocketTheme\Toolbox\File\YamlFile;
@@ -56,6 +57,19 @@ class ConfigServiceProvider implements ServiceProviderInterface
             return $config;
         };
 
+        $container['mime'] = function ($c) {
+            /** @var Config $config */
+            $config = $c['config'];
+            $mimes = $config->get('mime.types', []);
+            foreach ($config->get('media.types', []) as $ext => $media) {
+                if (!empty($media['mime'])) {
+                    $mimes[$ext] = array_unique(array_merge([$media['mime']], $mimes[$ext] ?? []));
+                }
+            }
+
+            return MimeTypes::createFromMimes($mimes);
+        };
+
         $container['languages'] = function ($c) {
             return static::languages($c);
         };

+ 11 - 2
system/src/Grav/Common/Service/TaskServiceProvider.php

@@ -12,6 +12,7 @@ namespace Grav\Common\Service;
 use Grav\Common\Grav;
 use Pimple\Container;
 use Pimple\ServiceProviderInterface;
+use Psr\Http\Message\ServerRequestInterface;
 
 /**
  * Class TaskServiceProvider
@@ -26,7 +27,11 @@ class TaskServiceProvider implements ServiceProviderInterface
     public function register(Container $container)
     {
         $container['task'] = function (Grav $c) {
-            $task = $_POST['task'] ?? $c['uri']->param('task');
+            /** @var ServerRequestInterface $request */
+            $request = $c['request'];
+            $body = $request->getParsedBody();
+
+            $task = $body['task'] ?? $c['uri']->param('task');
             if (null !== $task) {
                 $task = filter_var($task, FILTER_SANITIZE_STRING);
             }
@@ -35,7 +40,11 @@ class TaskServiceProvider implements ServiceProviderInterface
         };
 
         $container['action'] = function (Grav $c) {
-            $action = $_POST['action'] ?? $c['uri']->param('action');
+            /** @var ServerRequestInterface $request */
+            $request = $c['request'];
+            $body = $request->getParsedBody();
+
+            $action = $body['action'] ?? $c['uri']->param('action');
             if (null !== $action) {
                 $action = filter_var($action, FILTER_SANITIZE_STRING);
             }

+ 8 - 4
system/src/Grav/Common/Session.php

@@ -12,6 +12,7 @@ namespace Grav\Common;
 use Grav\Common\Form\FormFlash;
 use Grav\Events\SessionStartEvent;
 use Grav\Plugin\Form\Forms;
+use JsonException;
 use function is_string;
 
 /**
@@ -148,10 +149,11 @@ class Session extends \Grav\Framework\Session\Session
      * @param mixed $object
      * @param int $time
      * @return $this
+     * @throws JsonException
      */
     public function setFlashCookieObject($name, $object, $time = 60)
     {
-        setcookie($name, json_encode($object), time() + $time, '/');
+        setcookie($name, json_encode($object, JSON_THROW_ON_ERROR), $this->getCookieOptions($time));
 
         return $this;
     }
@@ -161,13 +163,15 @@ class Session extends \Grav\Framework\Session\Session
      *
      * @param string $name
      * @return mixed|null
+     * @throws JsonException
      */
     public function getFlashCookieObject($name)
     {
         if (isset($_COOKIE[$name])) {
-            $object = json_decode($_COOKIE[$name], false);
-            setcookie($name, '', time() - 3600, '/');
-            return $object;
+            $cookie = $_COOKIE[$name];
+            setcookie($name, '', $this->getCookieOptions(-42000));
+
+            return json_decode($cookie, false, 512, JSON_THROW_ON_ERROR);
         }
 
         return null;

+ 19 - 0
system/src/Grav/Common/Twig/Exception/TwigException.php

@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @package    Grav\Common\Twig\Exception
+ *
+ * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Twig\Exception;
+
+/**
+ * TwigException gets thrown when you use {% throw code message %} in twig.
+ *
+ * This allows Grav to catch 401, 403 and 404 exceptions and display proper error page.
+ */
+class TwigException extends \RuntimeException
+{
+}

+ 13 - 1
system/src/Grav/Common/Twig/Extension/GravExtension.php

@@ -155,6 +155,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
             new TwigFilter('bool', [$this, 'boolFilter']),
             new TwigFilter('float', [$this, 'floatFilter'], ['is_safe' => ['all']]),
             new TwigFilter('array', [$this, 'arrayFilter']),
+            new TwigFilter('yaml', [$this, 'yamlFilter']),
 
             // Object Types
             new TwigFilter('get_type', [$this, 'getTypeFunc']),
@@ -807,6 +808,17 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
         return (array)$input;
     }
 
+    /**
+     * @param array|object $value
+     * @param int|null $inline
+     * @param int|null $indent
+     * @return string
+     */
+    public function yamlFilter($value, $inline = null, $indent = null): string
+    {
+        return Yaml::dump($value, $inline, $indent);
+    }
+
     /**
      * @param Environment $twig
      * @return string
@@ -1499,7 +1511,7 @@ class GravExtension extends AbstractExtension implements GlobalsInterface
             }
 
             //Look for existing class
-            $svg = preg_replace_callback('/^<svg[^>]*(class=\")([^"]*)(\")[^>]*>/', function($matches) use ($classes, &$matched) {
+            $svg = preg_replace_callback('/^<svg[^>]*(class=\"([^"]*)\")[^>]*>/', function($matches) use ($classes, &$matched) {
                 if (isset($matches[2])) {
                     $new_classes = $matches[2] . $classes;
                     $matched = true;

+ 1 - 1
system/src/Grav/Common/Twig/Node/TwigNodeThrow.php

@@ -43,7 +43,7 @@ class TwigNodeThrow extends Node
         $compiler->addDebugInfo($this);
 
         $compiler
-            ->write('throw new \RuntimeException(')
+            ->write('throw new \Grav\Common\Twig\Exception\TwigException(')
             ->subcompile($this->getNode('message'))
             ->write(', ')
             ->write($this->getAttribute('code') ?: 500)

+ 7 - 8
system/src/Grav/Common/Twig/Node/TwigNodeTryCatch.php

@@ -49,16 +49,15 @@ class TwigNodeTryCatch extends Node
 
         $compiler
             ->indent()
-            ->subcompile($this->getNode('try'));
+            ->subcompile($this->getNode('try'))
+            ->outdent()
+            ->write('} catch (\Exception $e) {' . "\n")
+            ->indent()
+            ->write('if (isset($context[\'grav\'][\'debugger\'])) $context[\'grav\'][\'debugger\']->addException($e);' . "\n")
+            ->write('$context[\'e\'] = $e;' . "\n");
 
         if ($this->hasNode('catch')) {
-            $compiler
-                ->outdent()
-                ->write('} catch (\Exception $e) {' . "\n")
-                ->indent()
-                ->write('if (isset($context[\'grav\'][\'debugger\'])) $context[\'grav\'][\'debugger\']->addException($e);' . "\n")
-                ->write('$context[\'e\'] = $e;' . "\n")
-                ->subcompile($this->getNode('catch'));
+            $compiler->subcompile($this->getNode('catch'));
         }
 
         $compiler

+ 53 - 26
system/src/Grav/Common/Twig/Twig.php

@@ -16,6 +16,7 @@ use Grav\Common\Language\Language;
 use Grav\Common\Language\LanguageCodes;
 use Grav\Common\Page\Interfaces\PageInterface;
 use Grav\Common\Page\Pages;
+use Grav\Common\Twig\Exception\TwigException;
 use Grav\Common\Twig\Extension\FilesystemExtension;
 use Grav\Common\Twig\Extension\GravExtension;
 use Grav\Common\Utils;
@@ -26,6 +27,7 @@ use RuntimeException;
 use Twig\Cache\FilesystemCache;
 use Twig\Environment;
 use Twig\Error\LoaderError;
+use Twig\Error\RuntimeError;
 use Twig\Extension\CoreExtension;
 use Twig\Extension\DebugExtension;
 use Twig\Extension\StringLoaderExtension;
@@ -404,38 +406,63 @@ class Twig
      */
     public function processSite($format = null, array $vars = [])
     {
-        // set the page now its been processed
-        $this->grav->fireEvent('onTwigSiteVariables');
-        /** @var Pages $pages */
-        $pages = $this->grav['pages'];
-        /** @var PageInterface $page */
-        $page = $this->grav['page'];
-        $content = $page->content();
+        try {
+            $grav = $this->grav;
 
-        $twig_vars = $this->twig_vars;
+            // set the page now its been processed
+            $grav->fireEvent('onTwigSiteVariables');
 
-        $twig_vars['theme'] = $this->grav['config']->get('theme');
-        $twig_vars['pages'] = $pages->root();
-        $twig_vars['page'] = $page;
-        $twig_vars['header'] = $page->header();
-        $twig_vars['media'] = $page->media();
-        $twig_vars['content'] = $content;
-
-        // determine if params are set, if so disable twig cache
-        $params = $this->grav['uri']->params(null, true);
-        if (!empty($params)) {
-            $this->twig->setCache(false);
-        }
+            /** @var Pages $pages */
+            $pages = $grav['pages'];
+
+            /** @var PageInterface $page */
+            $page = $grav['page'];
+
+            $twig_vars = $this->twig_vars;
+            $twig_vars['theme'] = $grav['config']->get('theme');
+            $twig_vars['pages'] = $pages->root();
+            $twig_vars['page'] = $page;
+            $twig_vars['header'] = $page->header();
+            $twig_vars['media'] = $page->media();
+            $twig_vars['content'] = $page->content();
+
+            // determine if params are set, if so disable twig cache
+            $params = $grav['uri']->params(null, true);
+            if (!empty($params)) {
+                $this->twig->setCache(false);
+            }
 
-        // Get Twig template layout
-        $template = $this->getPageTwigTemplate($page, $format);
-        $page->templateFormat($format);
+            // Get Twig template layout
+            $template = $this->getPageTwigTemplate($page, $format);
+            $page->templateFormat($format);
 
-        try {
             $output = $this->twig->render($template, $vars + $twig_vars);
         } catch (LoaderError $e) {
-            $error_msg = $e->getMessage();
-            throw new RuntimeException($error_msg, 400, $e);
+            throw new RuntimeException($e->getMessage(), 400, $e);
+        } catch (RuntimeError $e) {
+            $prev = $e->getPrevious();
+            if ($prev instanceof TwigException) {
+                $code = $prev->getCode() ?: 500;
+                // Fire onPageNotFound event.
+                $event = new Event([
+                    'page' => $page,
+                    'code' => $code,
+                    'message' => $prev->getMessage(),
+                    'exception' => $prev,
+                    'route' => $grav['route'],
+                    'request' => $grav['request']
+                ]);
+                $event = $grav->fireEvent("onDisplayErrorPage.{$code}", $event);
+                $newPage = $event['page'];
+                if ($newPage && $newPage !== $page) {
+                    unset($grav['page']);
+                    $grav['page'] = $newPage;
+
+                    return $this->processSite($newPage->templateFormat(), $vars);
+                }
+            }
+
+            throw $e;
         }
 
         return $output;

+ 10 - 5
system/src/Grav/Common/Uri.php

@@ -675,10 +675,15 @@ class Uri
      */
     public static function ip()
     {
+        $ip = 'UNKNOWN';
+
         if (getenv('HTTP_CLIENT_IP')) {
             $ip = getenv('HTTP_CLIENT_IP');
+        } elseif (getenv('HTTP_CF_CONNECTING_IP')) {
+            $ip = getenv('HTTP_CF_CONNECTING_IP');
         } elseif (getenv('HTTP_X_FORWARDED_FOR') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) {
-            $ip = getenv('HTTP_X_FORWARDED_FOR');
+            $ips = array_map('trim', explode(',', getenv('HTTP_X_FORWARDED_FOR')));
+            $ip = array_shift($ips);
         } elseif (getenv('HTTP_X_FORWARDED') && Grav::instance()['config']->get('system.http_x_forwarded.ip')) {
             $ip = getenv('HTTP_X_FORWARDED');
         } elseif (getenv('HTTP_FORWARDED_FOR')) {
@@ -687,8 +692,6 @@ class Uri
             $ip = getenv('HTTP_FORWARDED');
         } elseif (getenv('REMOTE_ADDR')) {
             $ip = getenv('REMOTE_ADDR');
-        } else {
-            $ip = 'UNKNOWN';
         }
 
         return $ip;
@@ -1258,7 +1261,7 @@ class Uri
             $this->port = null;
         }
 
-        if ($this->hasStandardPort()) {
+        if ($this->port === 0 || $this->hasStandardPort()) {
             $this->port = null;
         }
 
@@ -1311,11 +1314,13 @@ class Uri
         if ($parts === false) {
             throw new RuntimeException('Malformed URL: ' . $url);
         }
+        $port = (int)($parts['port'] ?? 0);
+
         $this->scheme = $parts['scheme'] ?? null;
         $this->user = $parts['user'] ?? null;
         $this->password = $parts['pass'] ?? null;
         $this->host = $parts['host'] ?? null;
-        $this->port = isset($parts['port']) ? (int)$parts['port'] : null;
+        $this->port = $port ?: null;
         $this->path = $parts['path'] ?? '';
         $this->query = $parts['query'] ?? '';
         $this->fragment = $parts['fragment'] ?? null;

+ 41 - 5
system/src/Grav/Framework/Flex/FlexDirectory.php

@@ -47,7 +47,7 @@ use function is_callable;
  * @package Grav\Framework\Flex
  * @template T
  */
-class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
+class FlexDirectory implements FlexDirectoryInterface
 {
     use FlexAuthorizeTrait;
 
@@ -235,7 +235,17 @@ class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
 
         /** @var UniformResourceLocator $locator */
         $locator = $grav['locator'];
-        $filename = $locator->findResource($this->getDirectoryConfigUri($name), true);
+        $uri = $this->getDirectoryConfigUri($name);
+
+        // If configuration is found in main configuration, use it.
+        if (str_starts_with($uri, 'config://')) {
+            $path = str_replace('/', '.', substr($uri, 9, -5));
+
+            return (array)$grav['config']->get($path);
+        }
+
+        // Load the configuration file.
+        $filename = $locator->findResource($uri, true);
         if ($filename === false) {
             return [];
         }
@@ -821,20 +831,46 @@ class FlexDirectory implements FlexDirectoryInterface, FlexAuthorizeInterface
      * @param array $call
      * @return void
      */
-    protected function dynamicFlexField(array &$field, $property, array $call)
+    protected function dynamicFlexField(array &$field, $property, array $call): void
     {
         $params = (array)$call['params'];
         $object = $call['object'] ?? null;
         $method = array_shift($params);
+        $not = false;
+        if (str_starts_with($method, '!')) {
+            $method = substr($method, 1);
+            $not = true;
+        } elseif (str_starts_with($method, 'not ')) {
+            $method = substr($method, 4);
+            $not = true;
+        }
+        $method = trim($method);
 
         if ($object && method_exists($object, $method)) {
             $value = $object->{$method}(...$params);
             if (is_array($value) && isset($field[$property]) && is_array($field[$property])) {
-                $field[$property] = array_merge_recursive($field[$property], $value);
+                $value = $this->mergeArrays($field[$property], $value);
+            }
+            $field[$property] = $not ? !$value : $value;
+        }
+    }
+
+    /**
+     * @param array $array1
+     * @param array $array2
+     * @return array
+     */
+    protected function mergeArrays(array $array1, array $array2): array
+    {
+        foreach ($array2 as $key => $value) {
+            if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
+                $array1[$key] = $this->mergeArrays($array1[$key], $value);
             } else {
-                $field[$property] = $value;
+                $array1[$key] = $value;
             }
         }
+
+        return $array1;
     }
 
     /**

+ 7 - 3
system/src/Grav/Framework/Flex/FlexDirectoryForm.php

@@ -318,11 +318,11 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
     }
 
     /**
-     * @param string $field
-     * @param string $filename
+     * @param string|null $field
+     * @param string|null $filename
      * @return Route|null
      */
-    public function getFileDeleteAjaxRoute($field, $filename): ?Route
+    public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route
     {
         return null;
     }
@@ -453,7 +453,9 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
     protected function doSerialize(): array
     {
         return $this->doTraitSerialize() + [
+                'form' => $this->form,
                 'directory' => $this->directory,
+                'flexName' => $this->flexName
             ];
     }
 
@@ -465,7 +467,9 @@ class FlexDirectoryForm implements FlexDirectoryFormInterface, JsonSerializable
     {
         $this->doTraitUnserialize($data);
 
+        $this->form = $data['form'];
         $this->directory = $data['directory'];
+        $this->flexName = $data['flexName'];
     }
 
     /**

+ 28 - 7
system/src/Grav/Framework/Flex/FlexForm.php

@@ -103,7 +103,14 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
     {
         $this->name = $name;
         $this->setObject($object);
-        $this->setName($object->getFlexType(), $name);
+
+        if (isset($options['form']['name'])) {
+            // Use custom form name.
+            $this->flexName = $options['form']['name'];
+        } else {
+            // Use standard form name.
+            $this->setName($object->getFlexType(), $name);
+        }
         $this->setId($this->getName());
 
         $uniqueId = $options['unique_id'] ?? null;
@@ -371,22 +378,28 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
     {
         $object = $this->getObject();
         if (!method_exists($object, 'route')) {
-            return null;
+            /** @var Route $route */
+            $route = Grav::instance()['route'];
+
+            return $route->withExtension('json')->withGravParam('task', 'media.upload');
         }
 
         return $object->route('/edit.json/task:media.upload');
     }
 
     /**
-     * @param string $field
-     * @param string $filename
+     * @param string|null $field
+     * @param string|null $filename
      * @return Route|null
      */
-    public function getFileDeleteAjaxRoute($field, $filename): ?Route
+    public function getFileDeleteAjaxRoute($field = null, $filename = null): ?Route
     {
         $object = $this->getObject();
         if (!method_exists($object, 'route')) {
-            return null;
+            /** @var Route $route */
+            $route = Grav::instance()['route'];
+
+            return $route->withExtension('json')->withGravParam('task', 'media.delete');
         }
 
         return $object->route('/edit.json/task:media.delete');
@@ -536,7 +549,11 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
     protected function doSerialize(): array
     {
         return $this->doTraitSerialize() + [
+                'items' => $this->items,
+                'form' => $this->form,
                 'object' => $this->object,
+                'flexName' => $this->flexName,
+                'submitMethod' => $this->submitMethod,
             ];
     }
 
@@ -548,7 +565,11 @@ class FlexForm implements FlexObjectFormInterface, JsonSerializable
     {
         $this->doTraitUnserialize($data);
 
-        $this->object = $data['object'];
+        $this->items = $data['items'] ?? null;
+        $this->form = $data['form'] ?? null;
+        $this->object = $data['object'] ?? null;
+        $this->flexName = $data['flexName'] ?? null;
+        $this->submitMethod = $data['submitMethod'] ?? null;
     }
 
     /**

+ 43 - 6
system/src/Grav/Framework/Flex/FlexObject.php

@@ -44,6 +44,7 @@ use function is_array;
 use function is_object;
 use function is_scalar;
 use function is_string;
+use function json_encode;
 
 /**
  * Class FlexObject
@@ -70,6 +71,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
     /** @var array */
     private $_meta;
     /** @var array */
+    protected $_original;
+    /** @var array */
     protected $_changes;
     /** @var string */
     protected $storage_key;
@@ -369,7 +372,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
      */
     public function searchProperty(string $property, string $search, array $options = null): float
     {
-        $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
+        $options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options');
         $value = $this->getProperty($property);
 
         return $this->searchValue($property, $value, $search, $options);
@@ -383,7 +386,7 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
      */
     public function searchNestedProperty(string $property, string $search, array $options = null): float
     {
-        $options = $options ?? $this->getFlexDirectory()->getConfig('data.search.options', []);
+        $options = $options ?? (array)$this->getFlexDirectory()->getConfig('data.search.options');
         if ($property === 'key') {
             $value = $this->getKey();
         } else {
@@ -440,6 +443,16 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
         return 0;
     }
 
+    /**
+     * Get original data before update
+     *
+     * @return array
+     */
+    public function getOriginalData(): array
+    {
+        return $this->_original ?? [];
+    }
+
     /**
      * Get any changes based on data sent to update
      *
@@ -653,7 +666,8 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
             }
 
             // Store the changes
-            $this->_changes = Utils::arrayDiffMultidimensional($this->getElements(), $elements);
+            $this->_original = $this->getElements();
+            $this->_changes = Utils::arrayDiffMultidimensional($this->_original, $elements);
         }
 
         if ($files && method_exists($this, 'setUpdatedMedia')) {
@@ -691,6 +705,17 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
         return $this->create($key);
     }
 
+    /**
+     * @param UserInterface|null $user
+     */
+    public function check(UserInterface $user = null): void
+    {
+        // If user has been provided, check if the user has permissions to save this object.
+        if ($user && !$this->isAuthorized('save', null, $user)) {
+            throw new \RuntimeException('Forbidden', 403);
+        }
+    }
+
     /**
      * {@inheritdoc}
      * @see FlexObjectInterface::save()
@@ -809,11 +834,12 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
      */
     public function getForm(string $name = '', array $options = null)
     {
-        if (!isset($this->_forms[$name])) {
-            $this->_forms[$name] = $this->createFormObject($name, $options);
+        $hash = $name . '-' . md5(json_encode($options, JSON_THROW_ON_ERROR));
+        if (!isset($this->_forms[$hash])) {
+            $this->_forms[$hash] = $this->createFormObject($name, $options);
         }
 
-        return $this->_forms[$name];
+        return $this->_forms[$hash];
     }
 
     /**
@@ -1063,6 +1089,17 @@ class FlexObject implements FlexObjectInterface, FlexAuthorizeInterface
         return $action;
     }
 
+    /**
+     * Method to reset blueprints if the type changes.
+     *
+     * @return void
+     * @since 1.7.18
+     */
+    protected function resetBlueprints(): void
+    {
+        $this->_blueprint = [];
+    }
+
     // DEPRECATED METHODS
 
     /**

+ 1 - 1
system/src/Grav/Framework/Flex/Interfaces/FlexDirectoryInterface.php

@@ -17,7 +17,7 @@ use Grav\Framework\Cache\CacheInterface;
  * Interface FlexDirectoryInterface
  * @package Grav\Framework\Flex\Interfaces
  */
-interface FlexDirectoryInterface
+interface FlexDirectoryInterface extends FlexAuthorizeInterface
 {
     /**
      * @return bool

+ 2 - 2
system/src/Grav/Framework/Flex/Interfaces/FlexFormInterface.php

@@ -38,8 +38,8 @@ interface FlexFormInterface extends Serializable, FormInterface
     /**
      * Get route for deleting files by AJAX.
      *
-     * @param string $field     Field where the file is associated into.
-     * @param string $filename  Filename for the file.
+     * @param string|null $field     Field where the file is associated into.
+     * @param string|null $filename  Filename for the file.
      * @return Route|null       Returns Route object or null if file uploads are not enabled.
      */
     public function getFileDeleteAjaxRoute($field, $filename);

+ 9 - 3
system/src/Grav/Framework/Flex/Storage/FolderStorage.php

@@ -40,6 +40,8 @@ class FolderStorage extends AbstractFilesystemStorage
     protected $dataFolder;
     /** @var string Pattern to access an object. */
     protected $dataPattern = '{FOLDER}/{KEY}/{FILE}{EXT}';
+    /** @var string[] */
+    protected $variables = ['FOLDER' => '%1$s', 'KEY' => '%2$s', 'KEY:2' => '%3$s', 'FILE' => '%4$s', 'EXT' => '%5$s'];
     /** @var string Filename for the object. */
     protected $dataFile;
     /** @var string File extension for the object. */
@@ -380,6 +382,12 @@ class FolderStorage extends AbstractFilesystemStorage
             if (isset($data[0])) {
                 throw new RuntimeException('Broken object file');
             }
+
+            // Add key field to the object.
+            $keyField = $this->keyField;
+            if ($keyField !== 'storage_key' && !isset($data[$keyField])) {
+                $data[$keyField] = $key;
+            }
         } catch (RuntimeException $e) {
             $data = ['__ERROR' => $e->getMessage()];
         } finally {
@@ -692,9 +700,7 @@ class FolderStorage extends AbstractFilesystemStorage
         $this->keyLen = (int)($options['key_len'] ?? 32);
         $this->caseSensitive = (bool)($options['case_sensitive'] ?? true);
 
-        $variables = ['FOLDER' => '%1$s', 'KEY' => '%2$s', 'KEY:2' => '%3$s', 'FILE' => '%4$s', 'EXT' => '%5$s'];
-        $pattern = Utils::simpleTemplate($pattern, $variables);
-
+        $pattern = Utils::simpleTemplate($pattern, $this->variables);
         if (!$pattern) {
             throw new RuntimeException('Bad storage folder pattern');
         }

+ 1 - 1
system/src/Grav/Framework/Flex/Storage/SimpleStorage.php

@@ -455,7 +455,7 @@ class SimpleStorage extends AbstractFilesystemStorage
         $content = (array) $file->content();
         if ($this->prefix) {
             $data = new Data($content);
-            $content = $data->get($this->prefix);
+            $content = $data->get($this->prefix, []);
         }
 
         $file->free();

+ 1 - 1
system/src/Grav/Framework/Flex/Traits/FlexMediaTrait.php

@@ -120,7 +120,7 @@ trait FlexMediaTrait
         // Load settings for the field.
         $schema = $this->getBlueprint()->schema();
         $settings = $field && is_object($schema) ? (array)$schema->getProperty($field) : null;
-        if (!isset($settings) || !is_array($settings)) {
+        if (!is_array($settings)) {
             return null;
         }
 

+ 13 - 7
system/src/Grav/Framework/Form/FormFlash.php

@@ -120,7 +120,7 @@ class FormFlash implements FormFlashInterface
     protected function loadStoredForm(): ?array
     {
         $file = $this->getTmpIndex();
-        $exists = $file->exists();
+        $exists = $file && $file->exists();
 
         $data = null;
         if ($exists) {
@@ -246,8 +246,10 @@ class FormFlash implements FormFlashInterface
         if ($force || $this->data || $this->files) {
             // Only save if there is data or files to be saved.
             $file = $this->getTmpIndex();
-            $file->save($this->jsonSerialize());
-            $this->exists = true;
+            if ($file) {
+                $file->save($this->jsonSerialize());
+                $this->exists = true;
+            }
         } elseif ($this->exists) {
             // Delete empty form flash if it exists (it carries no information).
             return $this->delete();
@@ -476,12 +478,14 @@ class FormFlash implements FormFlashInterface
     }
 
     /**
-     * @return YamlFile
+     * @return ?YamlFile
      */
-    protected function getTmpIndex(): YamlFile
+    protected function getTmpIndex(): ?YamlFile
     {
+        $tmpDir = $this->getTmpDir();
+
         // Do not use CompiledYamlFile as the file can change multiple times per second.
-        return YamlFile::instance($this->getTmpDir() . '/index.yaml');
+        return $tmpDir ? YamlFile::instance($tmpDir . '/index.yaml') : null;
     }
 
     /**
@@ -503,7 +507,9 @@ class FormFlash implements FormFlashInterface
     {
         // Make sure that index file cache gets always cleared.
         $file = $this->getTmpIndex();
-        $file->free();
+        if ($file) {
+            $file->free();
+        }
 
         $tmpDir = $this->getTmpDir();
         if ($tmpDir && file_exists($tmpDir)) {

+ 107 - 0
system/src/Grav/Framework/Mime/MimeTypes.php

@@ -0,0 +1,107 @@
+<?php declare(strict_types=1);
+
+/**
+ * @package    Grav\Framework\Mime
+ *
+ * @copyright  Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Framework\Mime;
+
+use function in_array;
+
+/**
+ * Class to handle mime-types.
+ */
+class MimeTypes
+{
+    /** @var array */
+    protected $extensions;
+    /** @var array */
+    protected $mimes;
+
+    /**
+     * Create a new mime types instance with the given mappings.
+     *
+     * @param array $mimes An associative array containing ['ext' => ['mime/type', 'mime/type2']]
+     */
+    public static function createFromMimes(array $mimes): self
+    {
+        $extensions = [];
+        foreach ($mimes as $ext => $list) {
+            foreach ($list as $mime) {
+                $list = $extensions[$mime] ?? [];
+                if (!in_array($ext, $list, true)) {
+                    $list[] = $ext;
+                    $extensions[$mime] = $list;
+                }
+            }
+        }
+
+        return new static($extensions, $mimes);
+    }
+
+    /**
+     * @param string $extension
+     * @return string|null
+     */
+    public function getMimeType(string $extension): ?string
+    {
+        $extension = $this->cleanInput($extension);
+
+        return $this->mimes[$extension][0] ?? null;
+    }
+
+    /**
+     * @param string $mime
+     * @return string|null
+     */
+    public function getExtension(string $mime): ?string
+    {
+        $mime = $this->cleanInput($mime);
+
+        return $this->extensions[$mime][0] ?? null;
+    }
+
+    /**
+     * @param string $extension
+     * @return array
+     */
+    public function getMimeTypes(string $extension): array
+    {
+        $extension = $this->cleanInput($extension);
+
+        return $this->mimes[$extension] ?? [];
+    }
+
+    /**
+     * @param string $mime
+     * @return array
+     */
+    public function getExtensions(string $mime): array
+    {
+        $mime = $this->cleanInput($mime);
+
+        return $this->extensions[$mime] ?? [];
+    }
+
+    /**
+     * @param string $input
+     * @return string
+     */
+    protected function cleanInput(string $input): string
+    {
+        return strtolower(trim($input));
+    }
+
+    /**
+     * @param array $extensions
+     * @param array $mimes
+     */
+    protected function __construct(array $extensions, array $mimes)
+    {
+        $this->extensions = $extensions;
+        $this->mimes = $mimes;
+    }
+}

+ 0 - 1
system/src/Grav/Framework/Object/ObjectCollection.php

@@ -9,7 +9,6 @@
 
 namespace Grav\Framework\Object;
 
-use Doctrine\Common\Collections\Collection;
 use Doctrine\Common\Collections\Criteria;
 use Grav\Framework\Collection\ArrayCollection;
 use Grav\Framework\Object\Access\NestedPropertyCollectionTrait;

+ 33 - 0
system/src/Grav/Framework/Psr7/UploadedFile.php

@@ -23,6 +23,9 @@ class UploadedFile implements UploadedFileInterface
 {
     use UploadedFileDecoratorTrait;
 
+    /** @var array */
+    private $meta = [];
+
     /**
      * @param StreamInterface|string|resource $streamOrFile
      * @param int                             $size
@@ -34,4 +37,34 @@ class UploadedFile implements UploadedFileInterface
     {
         $this->uploadedFile = new \Nyholm\Psr7\UploadedFile($streamOrFile, $size, $errorStatus, $clientFilename, $clientMediaType);
     }
+
+    /**
+     * @param array $meta
+     * @return $this
+     */
+    public function setMeta(array $meta)
+    {
+        $this->meta = $meta;
+
+        return $this;
+    }
+
+    /**
+     * @param array $meta
+     * @return $this
+     */
+    public function addMeta(array $meta)
+    {
+        $this->meta = array_merge($this->meta, $meta);
+
+        return $this;
+    }
+
+    /**
+     * @return array
+     */
+    public function getMeta(): array
+    {
+        return $this->meta;
+    }
 }

+ 16 - 18
system/src/Grav/Framework/Session/Session.php

@@ -338,23 +338,12 @@ class Session implements SessionInterface
     {
         $name = $this->getName();
         if (null !== $name) {
-            $params = session_get_cookie_params();
-
-            $cookie_options = array (
-                'expires'  => time() - 42000,
-                'path'     => $params['path'],
-                'domain'   => $params['domain'],
-                'secure'   => $params['secure'],
-                'httponly' => $params['httponly'],
-                'samesite' => $params['samesite']
-            );
-
             $this->removeCookie();
 
             setcookie(
                 session_name(),
                 '',
-                $cookie_options
+                $this->getCookieOptions(-42000)
             );
         }
 
@@ -463,27 +452,36 @@ class Session implements SessionInterface
     }
 
     /**
-     * @return void
+     * Store something in cookie temporarily.
+     *
+     * @param int|null $lifetime
+     * @return array
      */
-    protected function setCookie(): void
+    public function getCookieOptions(int $lifetime = null): array
     {
         $params = session_get_cookie_params();
 
-        $cookie_options = array (
-            'expires'  => time() + $params['lifetime'],
+        return [
+            'expires'  => time() + ($lifetime ?? $params['lifetime']),
             'path'     => $params['path'],
             'domain'   => $params['domain'],
             'secure'   => $params['secure'],
             'httponly' => $params['httponly'],
             'samesite' => $params['samesite']
-        );
+        ];
+    }
 
+    /**
+     * @return void
+     */
+    protected function setCookie(): void
+    {
         $this->removeCookie();
 
         setcookie(
             session_name(),
             session_id(),
-            $cookie_options
+            $this->getCookieOptions()
         );
     }
 

+ 70 - 0
system/src/Phive/Twig/Extensions/Deferred/DeferredExtension.php

@@ -0,0 +1,70 @@
+<?php
+
+// Fix too many ob_get_clean() calls when exception is thrown inside the template.
+
+namespace Phive\Twig\Extensions\Deferred;
+
+class DeferredExtension extends \Twig_Extension
+{
+    /**
+     * @var array
+     */
+    private $blocks = array();
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getTokenParsers()
+    {
+        return array(new DeferredTokenParser());
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getNodeVisitors()
+    {
+        return array(new DeferredNodeVisitor());
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getName()
+    {
+        return 'deferred';
+    }
+
+    public function defer(\Twig_Template $template, $blockName)
+    {
+        ob_start();
+        $templateName = $template->getTemplateName();
+        $this->blocks[$templateName][] = [ob_get_level(), $blockName];
+    }
+
+    public function resolve(\Twig_Template $template, array $context, array $blocks)
+    {
+        $templateName = $template->getTemplateName();
+        if (empty($this->blocks[$templateName])) {
+            return;
+        }
+
+        while ($block = array_pop($this->blocks[$templateName])) {
+            [$level, $blockName] = $block;
+            if (ob_get_level() !== $level) {
+                continue;
+            }
+
+            $buffer = ob_get_clean();
+
+            $blocks[$blockName] = array($template, 'block_'.$blockName.'_deferred');
+            $template->displayBlock($blockName, $context, $blocks);
+
+            echo $buffer;
+        }
+
+        if ($parent = $template->getParent($context)) {
+            $this->resolve($parent, $context, $blocks);
+        }
+    }
+}