Răsfoiți Sursa

Merge pull request #6141 from mailcow/staging

2024-11
FreddleSpl0it 10 luni în urmă
părinte
comite
0a58aa293a
100 a modificat fișierele cu 4036 adăugiri și 2316 ștergeri
  1. 1 1
      .github/workflows/check_prs_if_on_staging.yml
  2. 1 1
      .github/workflows/update_postscreen_access_list.yml
  3. 1 0
      .gitignore
  4. 4 4
      data/Dockerfiles/dockerapi/main.py
  5. 142 3
      data/Dockerfiles/dockerapi/modules/DockerApi.py
  6. 16 3
      data/Dockerfiles/dovecot/docker-entrypoint.sh
  7. 4 4
      data/Dockerfiles/phpfpm/Dockerfile
  8. 11 2
      data/Dockerfiles/phpfpm/docker-entrypoint.sh
  9. 11 0
      data/Dockerfiles/postfix/docker-entrypoint.sh
  10. 2 2
      data/Dockerfiles/rspamd/Dockerfile
  11. 3 2
      data/Dockerfiles/sogo/Dockerfile
  12. 2 0
      data/Dockerfiles/sogo/docker-entrypoint.sh
  13. 2 0
      data/conf/postfix/main.cf
  14. 1 1
      data/conf/postfix/master.cf
  15. 80 21
      data/conf/postfix/postscreen_access.cidr
  16. 44 30
      data/conf/rspamd/local.d/mime_types.conf
  17. 1 0
      data/conf/rspamd/local.d/options.inc
  18. 31 31
      data/web/inc/functions.inc.php
  19. 247 49
      data/web/inc/functions.mailbox.inc.php
  20. 162 16
      data/web/inc/lib/composer.lock
  21. 15 2
      data/web/inc/lib/vendor/autoload.php
  22. 72 65
      data/web/inc/lib/vendor/composer/ClassLoader.php
  23. 12 5
      data/web/inc/lib/vendor/composer/InstalledVersions.php
  24. 2 0
      data/web/inc/lib/vendor/composer/autoload_classmap.php
  25. 6 0
      data/web/inc/lib/vendor/composer/autoload_files.php
  26. 1 0
      data/web/inc/lib/vendor/composer/autoload_psr4.php
  27. 10 17
      data/web/inc/lib/vendor/composer/autoload_real.php
  28. 13 0
      data/web/inc/lib/vendor/composer/autoload_static.php
  29. 168 16
      data/web/inc/lib/vendor/composer/installed.json
  30. 23 5
      data/web/inc/lib/vendor/composer/installed.php
  31. 2 2
      data/web/inc/lib/vendor/composer/platform_check.php
  32. 5 0
      data/web/inc/lib/vendor/symfony/deprecation-contracts/CHANGELOG.md
  33. 19 0
      data/web/inc/lib/vendor/symfony/deprecation-contracts/LICENSE
  34. 26 0
      data/web/inc/lib/vendor/symfony/deprecation-contracts/README.md
  35. 35 0
      data/web/inc/lib/vendor/symfony/deprecation-contracts/composer.json
  36. 27 0
      data/web/inc/lib/vendor/symfony/deprecation-contracts/function.php
  37. 19 0
      data/web/inc/lib/vendor/symfony/polyfill-php81/LICENSE
  38. 37 0
      data/web/inc/lib/vendor/symfony/polyfill-php81/Php81.php
  39. 18 0
      data/web/inc/lib/vendor/symfony/polyfill-php81/README.md
  40. 51 0
      data/web/inc/lib/vendor/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php
  41. 20 0
      data/web/inc/lib/vendor/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php
  42. 28 0
      data/web/inc/lib/vendor/symfony/polyfill-php81/bootstrap.php
  43. 33 0
      data/web/inc/lib/vendor/symfony/polyfill-php81/composer.json
  44. 0 18
      data/web/inc/lib/vendor/twig/twig/.editorconfig
  45. 0 4
      data/web/inc/lib/vendor/twig/twig/.gitattributes
  46. 0 149
      data/web/inc/lib/vendor/twig/twig/.github/workflows/ci.yml
  47. 0 64
      data/web/inc/lib/vendor/twig/twig/.github/workflows/documentation.yml
  48. 0 6
      data/web/inc/lib/vendor/twig/twig/.gitignore
  49. 0 20
      data/web/inc/lib/vendor/twig/twig/.php-cs-fixer.dist.php
  50. 176 1
      data/web/inc/lib/vendor/twig/twig/CHANGELOG
  51. 1 1
      data/web/inc/lib/vendor/twig/twig/LICENSE
  52. 1 1
      data/web/inc/lib/vendor/twig/twig/README.rst
  53. 12 9
      data/web/inc/lib/vendor/twig/twig/composer.json
  54. 136 0
      data/web/inc/lib/vendor/twig/twig/src/AbstractTwigCallable.php
  55. 20 0
      data/web/inc/lib/vendor/twig/twig/src/Attribute/FirstClassTwigCallableReady.php
  56. 20 0
      data/web/inc/lib/vendor/twig/twig/src/Attribute/YieldReady.php
  57. 79 0
      data/web/inc/lib/vendor/twig/twig/src/Cache/ChainCache.php
  58. 4 4
      data/web/inc/lib/vendor/twig/twig/src/Cache/FilesystemCache.php
  59. 25 0
      data/web/inc/lib/vendor/twig/twig/src/Cache/ReadOnlyFilesystemCache.php
  60. 56 13
      data/web/inc/lib/vendor/twig/twig/src/Compiler.php
  61. 86 37
      data/web/inc/lib/vendor/twig/twig/src/Environment.php
  62. 9 9
      data/web/inc/lib/vendor/twig/twig/src/Error/Error.php
  63. 2 2
      data/web/inc/lib/vendor/twig/twig/src/Error/SyntaxError.php
  64. 214 186
      data/web/inc/lib/vendor/twig/twig/src/ExpressionParser.php
  65. 1 1
      data/web/inc/lib/vendor/twig/twig/src/Extension/AbstractExtension.php
  66. 1153 971
      data/web/inc/lib/vendor/twig/twig/src/Extension/CoreExtension.php
  67. 29 31
      data/web/inc/lib/vendor/twig/twig/src/Extension/DebugExtension.php
  68. 84 300
      data/web/inc/lib/vendor/twig/twig/src/Extension/EscaperExtension.php
  69. 7 0
      data/web/inc/lib/vendor/twig/twig/src/Extension/ExtensionInterface.php
  70. 4 4
      data/web/inc/lib/vendor/twig/twig/src/Extension/GlobalsInterface.php
  71. 3 5
      data/web/inc/lib/vendor/twig/twig/src/Extension/OptimizerExtension.php
  72. 23 11
      data/web/inc/lib/vendor/twig/twig/src/Extension/SandboxExtension.php
  73. 4 4
      data/web/inc/lib/vendor/twig/twig/src/Extension/StagingExtension.php
  74. 18 20
      data/web/inc/lib/vendor/twig/twig/src/Extension/StringLoaderExtension.php
  75. 30 0
      data/web/inc/lib/vendor/twig/twig/src/Extension/YieldNotReadyExtension.php
  76. 63 38
      data/web/inc/lib/vendor/twig/twig/src/ExtensionSet.php
  77. 1 1
      data/web/inc/lib/vendor/twig/twig/src/FileExtensionEscapingStrategy.php
  78. 115 32
      data/web/inc/lib/vendor/twig/twig/src/Lexer.php
  79. 6 8
      data/web/inc/lib/vendor/twig/twig/src/Loader/ArrayLoader.php
  80. 28 15
      data/web/inc/lib/vendor/twig/twig/src/Loader/ChainLoader.php
  81. 10 10
      data/web/inc/lib/vendor/twig/twig/src/Loader/FilesystemLoader.php
  82. 2 2
      data/web/inc/lib/vendor/twig/twig/src/Markup.php
  83. 4 2
      data/web/inc/lib/vendor/twig/twig/src/Node/AutoEscapeNode.php
  84. 9 3
      data/web/inc/lib/vendor/twig/twig/src/Node/BlockNode.php
  85. 5 3
      data/web/inc/lib/vendor/twig/twig/src/Node/BlockReferenceNode.php
  86. 3 0
      data/web/inc/lib/vendor/twig/twig/src/Node/BodyNode.php
  87. 57 0
      data/web/inc/lib/vendor/twig/twig/src/Node/CaptureNode.php
  88. 3 1
      data/web/inc/lib/vendor/twig/twig/src/Node/CheckSecurityCallNode.php
  89. 14 17
      data/web/inc/lib/vendor/twig/twig/src/Node/CheckSecurityNode.php
  90. 3 1
      data/web/inc/lib/vendor/twig/twig/src/Node/CheckToStringNode.php
  91. 30 10
      data/web/inc/lib/vendor/twig/twig/src/Node/DeprecatedNode.php
  92. 4 2
      data/web/inc/lib/vendor/twig/twig/src/Node/DoNode.php
  93. 4 2
      data/web/inc/lib/vendor/twig/twig/src/Node/EmbedNode.php
  94. 4 0
      data/web/inc/lib/vendor/twig/twig/src/Node/Expression/AbstractExpression.php
  95. 58 8
      data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ArrayExpression.php
  96. 2 2
      data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ArrowFunctionExpression.php
  97. 3 3
      data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/EndsWithBinary.php
  98. 1 1
      data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/EqualBinary.php
  99. 1 1
      data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/GreaterBinary.php
  100. 1 1
      data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/GreaterEqualBinary.php

+ 1 - 1
.github/workflows/check_prs_if_on_staging.yml

@@ -10,7 +10,7 @@ jobs:
     if: github.event.pull_request.base.ref != 'staging' #check if the target branch is not staging
     steps:
       - name: Send message
-        uses: thollander/actions-comment-pull-request@v2.5.0
+        uses: thollander/actions-comment-pull-request@v3.0.1
         with:
           GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }}
           message: |

+ 1 - 1
.github/workflows/update_postscreen_access_list.yml

@@ -22,7 +22,7 @@ jobs:
           bash helper-scripts/update_postscreen_whitelist.sh
 
     - name: Create Pull Request
-      uses: peter-evans/create-pull-request@v6
+      uses: peter-evans/create-pull-request@v7
       with:
         token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
         commit-message: update postscreen_access.cidr

+ 1 - 0
.gitignore

@@ -45,6 +45,7 @@ data/conf/rspamd/override.d/*
 data/conf/sogo/custom-theme.js
 data/conf/sogo/plist_ldap
 data/conf/sogo/sieve.creds
+data/conf/sogo/cron.creds
 data/conf/sogo/sogo-full.svg
 data/gitea/
 data/gogs/

+ 4 - 4
data/Dockerfiles/dockerapi/main.py

@@ -90,7 +90,7 @@ async def get_container(container_id : str):
         if container._id == container_id:
           container_info = await container.show()
           return Response(content=json.dumps(container_info, indent=4), media_type="application/json")
-     
+
       res = {
         "type": "danger",
         "msg": "no container found"
@@ -130,7 +130,7 @@ async def get_containers():
 async def post_containers(container_id : str, post_action : str, request: Request):
   global dockerapi
 
-  try : 
+  try:
     request_json = await request.json()
   except Exception as err:
     request_json = {}
@@ -191,7 +191,7 @@ async def post_container_update_stats(container_id : str):
 
   stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats'))
   return Response(content=json.dumps(stats, indent=4), media_type="application/json")
-  
+
 
 # PubSub Handler
 async def handle_pubsub_messages(channel: aioredis.client.PubSub):
@@ -244,7 +244,7 @@ async def handle_pubsub_messages(channel: aioredis.client.PubSub):
               dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
           else:
             dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
-              
+
         await asyncio.sleep(0.0)
     except asyncio.TimeoutError:
       pass

+ 142 - 3
data/Dockerfiles/dockerapi/modules/DockerApi.py

@@ -159,7 +159,7 @@ class DockerApi:
             postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
             # todo: check each exit code
           res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'}
-          return Response(content=json.dumps(res, indent=4), media_type="application/json")        
+          return Response(content=json.dumps(res, indent=4), media_type="application/json")
   # api call: container_post - post_action: exec - cmd: mailq - task: list
   def container_post__exec__mailq__list(self, request_json, **kwargs):
     if 'container_id' in kwargs:
@@ -318,7 +318,7 @@ class DockerApi:
 
     if 'username' in request_json and 'script_name' in request_json:
       for container in self.sync_docker_client.containers.list(filters=filters):
-        cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"]  
+        cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"]
         sieve_return = container.exec_run(cmd)
         return self.exec_run_handler('utf8_text_only', sieve_return)
   # api call: container_post - post_action: exec - cmd: maildir - task: cleanup
@@ -342,6 +342,30 @@ class DockerApi:
           cmd = ["/bin/bash", "-c", cmd_vmail]
         maildir_cleanup = container.exec_run(cmd, user='vmail')
         return self.exec_run_handler('generic', maildir_cleanup)
+  # api call: container_post - post_action: exec - cmd: maildir - task: move
+  def container_post__exec__maildir__move(self, request_json, **kwargs):
+    if 'container_id' in kwargs:
+      filters = {"id": kwargs['container_id']}
+    elif 'container_name' in kwargs:
+      filters = {"name": kwargs['container_name']}
+
+    if 'old_maildir' in request_json and 'new_maildir' in request_json:
+      for container in self.sync_docker_client.containers.list(filters=filters):
+        vmail_name = request_json['old_maildir'].replace("'", "'\\''")
+        new_vmail_name = request_json['new_maildir'].replace("'", "'\\''")
+        cmd_vmail = f"if [[ -d '/var/vmail/{vmail_name}' ]]; then /bin/mv '/var/vmail/{vmail_name}' '/var/vmail/{new_vmail_name}'; fi"
+
+        index_name = request_json['old_maildir'].split("/")
+        new_index_name = request_json['new_maildir'].split("/")
+        if len(index_name) > 1 and len(new_index_name) > 1:
+          index_name = index_name[1].replace("'", "'\\''") + "@" + index_name[0].replace("'", "'\\''")
+          new_index_name = new_index_name[1].replace("'", "'\\''") + "@" + new_index_name[0].replace("'", "'\\''")
+          cmd_vmail_index = f"if [[ -d '/var/vmail_index/{index_name}' ]]; then /bin/mv '/var/vmail_index/{index_name}' '/var/vmail_index/{new_index_name}_index'; fi"
+          cmd = ["/bin/bash", "-c", cmd_vmail + " && " + cmd_vmail_index]
+        else:
+          cmd = ["/bin/bash", "-c", cmd_vmail]
+        maildir_move = container.exec_run(cmd, user='vmail')
+        return self.exec_run_handler('generic', maildir_move)
   # api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
   def container_post__exec__rspamd__worker_password(self, request_json, **kwargs):
     if 'container_id' in kwargs:
@@ -374,6 +398,121 @@ class DockerApi:
           self.logger.error('failed changing Rspamd password')
           res = { 'type': 'danger', 'msg': 'command did not complete' }
           return Response(content=json.dumps(res, indent=4), media_type="application/json")
+  # api call: container_post - post_action: exec - cmd: sogo - task: rename
+  def container_post__exec__sogo__rename_user(self, request_json, **kwargs):
+    if 'container_id' in kwargs:
+      filters = {"id": kwargs['container_id']}
+    elif 'container_name' in kwargs:
+      filters = {"name": kwargs['container_name']}
+
+    if 'old_username' in request_json and 'new_username' in request_json:
+      for container in self.sync_docker_client.containers.list(filters=filters):
+        old_username = request_json['old_username'].replace("'", "'\\''")
+        new_username = request_json['new_username'].replace("'", "'\\''")
+
+        sogo_return = container.exec_run(["/bin/bash", "-c", f"sogo-tool rename-user '{old_username}' '{new_username}'"], user='sogo')
+        return self.exec_run_handler('generic', sogo_return)
+  # api call: container_post - post_action: exec - cmd: doveadm - task: get_acl
+  def container_post__exec__doveadm__get_acl(self, request_json, **kwargs):
+    if 'container_id' in kwargs:
+      filters = {"id": kwargs['container_id']}
+    elif 'container_name' in kwargs:
+      filters = {"name": kwargs['container_name']}
+
+    for container in self.sync_docker_client.containers.list(filters=filters):
+      id = request_json['id'].replace("'", "'\\''")
+
+      shared_folders = container.exec_run(["/bin/bash", "-c", f"doveadm mailbox list -u '{id}'"])
+      shared_folders = shared_folders.output.decode('utf-8')
+      shared_folders = shared_folders.splitlines()
+
+      formatted_acls = []
+      mailbox_seen = []
+      for shared_folder in shared_folders:
+        if "Shared" not in shared_folder:
+          mailbox = shared_folder.replace("'", "'\\''")
+          if mailbox in mailbox_seen:
+            continue
+
+          acls = container.exec_run(["/bin/bash", "-c", f"doveadm acl get -u '{id}' '{mailbox}'"])
+          acls = acls.output.decode('utf-8').strip().splitlines()
+          if len(acls) >= 2:
+            for acl in acls[1:]:
+              user_id, rights = acl.split(maxsplit=1)
+              user_id = user_id.split('=')[1]
+              mailbox_seen.append(mailbox)
+              formatted_acls.append({ 'user': id, 'id': user_id, 'mailbox': mailbox, 'rights': rights.split() })
+        elif "Shared" in shared_folder and "/" in shared_folder:
+          shared_folder = shared_folder.split("/")
+          if len(shared_folder) < 3:
+            continue
+
+          user = shared_folder[1].replace("'", "'\\''")
+          mailbox = '/'.join(shared_folder[2:]).replace("'", "'\\''")
+          if mailbox in mailbox_seen:
+            continue
+
+          acls = container.exec_run(["/bin/bash", "-c", f"doveadm acl get -u '{user}' '{mailbox}'"])
+          acls = acls.output.decode('utf-8').strip().splitlines()
+          if len(acls) >= 2:
+            for acl in acls[1:]:
+              user_id, rights = acl.split(maxsplit=1)
+              user_id = user_id.split('=')[1].replace("'", "'\\''")
+              if user_id == id and mailbox not in mailbox_seen:
+                mailbox_seen.append(mailbox)
+                formatted_acls.append({ 'user': user, 'id': id, 'mailbox': mailbox, 'rights': rights.split() })
+
+      return Response(content=json.dumps(formatted_acls, indent=4), media_type="application/json")
+  # api call: container_post - post_action: exec - cmd: doveadm - task: delete_acl
+  def container_post__exec__doveadm__delete_acl(self, request_json, **kwargs):
+    if 'container_id' in kwargs:
+      filters = {"id": kwargs['container_id']}
+    elif 'container_name' in kwargs:
+      filters = {"name": kwargs['container_name']}
+
+    for container in self.sync_docker_client.containers.list(filters=filters):
+      user = request_json['user'].replace("'", "'\\''")
+      mailbox = request_json['mailbox'].replace("'", "'\\''")
+      id = request_json['id'].replace("'", "'\\''")
+
+      if user and mailbox and id:
+        acl_delete_return = container.exec_run(["/bin/bash", "-c", f"doveadm acl delete -u '{user}' '{mailbox}' 'user={id}'"])
+        return self.exec_run_handler('generic', acl_delete_return)
+  # api call: container_post - post_action: exec - cmd: doveadm - task: set_acl
+  def container_post__exec__doveadm__set_acl(self, request_json, **kwargs):
+    if 'container_id' in kwargs:
+      filters = {"id": kwargs['container_id']}
+    elif 'container_name' in kwargs:
+      filters = {"name": kwargs['container_name']}
+
+    for container in self.sync_docker_client.containers.list(filters=filters):
+      user = request_json['user'].replace("'", "'\\''")
+      mailbox = request_json['mailbox'].replace("'", "'\\''")
+      id = request_json['id'].replace("'", "'\\''")
+      rights = ""
+
+      available_rights = [
+        "admin",
+        "create",
+        "delete",
+        "expunge",
+        "insert",
+        "lookup",
+        "post",
+        "read",
+        "write",
+        "write-deleted",
+        "write-seen"
+      ]
+      for right in request_json['rights']:
+        right = right.replace("'", "'\\''").lower()
+        if right in available_rights:
+          rights += right + " "
+
+      if user and mailbox and id and rights:
+        acl_set_return = container.exec_run(["/bin/bash", "-c", f"doveadm acl set -u '{user}' '{mailbox}' 'user={id}' {rights}"])
+        return self.exec_run_handler('generic', acl_set_return)
+
 
   # Collect host stats
   async def get_host_stats(self, wait=5):
@@ -462,7 +601,7 @@ class DockerApi:
         except:
           pass
       return ''.join(total_data)
-      
+
     try :
       socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock
       if not cmd.endswith("\n"):

+ 16 - 3
data/Dockerfiles/dovecot/docker-entrypoint.sh

@@ -114,15 +114,15 @@ if [[ "${FLATCURVE_EXPERIMENTAL}" =~ ^([yY][eE][sS]|[yY]) ]]; then
 echo -e "\e[33mActivating Flatcurve as FTS Backend...\e[0m"
 echo -e "\e[33mDepending on your previous setup a full reindex might be needed... \e[0m"
 echo -e "\e[34mVisit https://docs.mailcow.email/manual-guides/Dovecot/u_e-dovecot-fts/#fts-related-dovecot-commands to learn how to reindex\e[0m"
-echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_flatcurve listescape replication' > /etc/dovecot/mail_plugins
+echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_flatcurve listescape replication lazy_expunge' > /etc/dovecot/mail_plugins
 echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify mail_log fts fts_flatcurve listescape replication' > /etc/dovecot/mail_plugins_imap
 echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl fts fts_flatcurve notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
 elif [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
-echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication' > /etc/dovecot/mail_plugins
+echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication lazy_expunge' > /etc/dovecot/mail_plugins
 echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify listescape replication mail_log' > /etc/dovecot/mail_plugins_imap
 echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
 else
-echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_solr listescape replication' > /etc/dovecot/mail_plugins
+echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_solr listescape replication lazy_expunge' > /etc/dovecot/mail_plugins
 echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify mail_log fts fts_solr listescape replication' > /etc/dovecot/mail_plugins_imap
 echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl fts fts_solr notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
 fi
@@ -371,6 +371,8 @@ EOF
 # Create random master Password for SOGo SSO
 RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
 echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
+# Creating additional creds file for SOGo notify crons (calendars, etc)
+echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
 cat <<EOF > /etc/dovecot/sogo-sso.conf
 # Autogenerated by mailcow
 passdb {
@@ -405,6 +407,17 @@ else
 	chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem
 fi
 
+# Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)
+if grep -qE 'ssl_min_protocol\s*=\s*(TLSv1|TLSv1\.1)\s*$' /etc/dovecot/dovecot.conf /etc/dovecot/extra.conf; then
+    sed -i '/\[openssl_init\]/a ssl_conf = ssl_configuration' /etc/ssl/openssl.cnf
+
+    echo "[ssl_configuration]" >> /etc/ssl/openssl.cnf
+    echo "system_default = tls_system_default" >> /etc/ssl/openssl.cnf
+    echo "[tls_system_default]" >> /etc/ssl/openssl.cnf
+    echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf
+    echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
+fi
+
 # Compile sieve scripts
 sievec /var/vmail/sieve/global_sieve_before.sieve
 sievec /var/vmail/sieve/global_sieve_after.sieve

+ 4 - 4
data/Dockerfiles/phpfpm/Dockerfile

@@ -1,17 +1,17 @@
-FROM php:8.2-fpm-alpine3.18
+FROM php:8.2-fpm-alpine3.20
 
 LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
 
 # renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
-ARG APCU_PECL_VERSION=5.1.23
+ARG APCU_PECL_VERSION=5.1.24
 # renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
 ARG IMAGICK_PECL_VERSION=3.7.0
 # renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
-ARG MAILPARSE_PECL_VERSION=3.1.6
+ARG MAILPARSE_PECL_VERSION=3.1.8
 # renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
 ARG MEMCACHED_PECL_VERSION=3.2.0
 # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
-ARG REDIS_PECL_VERSION=6.0.2
+ARG REDIS_PECL_VERSION=6.1.0
 # renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
 ARG COMPOSER_VERSION=2.6.6
 

+ 11 - 2
data/Dockerfiles/phpfpm/docker-entrypoint.sh

@@ -10,16 +10,25 @@ done
 
 # Do not attempt to write to slave
 if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
-  REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT}"
+  REDIS_HOST=$REDIS_SLAVEOF_IP
+  REDIS_PORT=$REDIS_SLAVEOF_PORT
 else
-  REDIS_CMDLINE="redis-cli -h redis -p 6379"
+  REDIS_HOST="redis"
+  REDIS_PORT="6379"
 fi
+REDIS_CMDLINE="redis-cli -h ${REDIS_HOST} -p ${REDIS_PORT}"
 
 until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
   echo "Waiting for Redis..."
   sleep 2
 done
 
+# Set redis session store
+echo -n '
+session.save_handler = redis
+session.save_path = "tcp://'${REDIS_HOST}':'${REDIS_PORT}'"
+' > /usr/local/etc/php/conf.d/session_store.ini
+
 # Check mysql_upgrade (master and slave)
 CONTAINER_ID=
 until [[ ! -z "${CONTAINER_ID}" ]] && [[ "${CONTAINER_ID}" =~ ^[[:alnum:]]*$ ]]; do

+ 11 - 0
data/Dockerfiles/postfix/docker-entrypoint.sh

@@ -12,4 +12,15 @@ if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
   cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
 fi
 
+# Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)
+if grep -qE '\!SSLv2|\!SSLv3|>=TLSv1(\.[0-1])?$' /opt/postfix/conf/main.cf /opt/postfix/conf/extra.cf; then
+    sed -i '/\[openssl_init\]/a ssl_conf = ssl_configuration' /etc/ssl/openssl.cnf
+
+    echo "[ssl_configuration]" >> /etc/ssl/openssl.cnf
+    echo "system_default = tls_system_default" >> /etc/ssl/openssl.cnf
+    echo "[tls_system_default]" >> /etc/ssl/openssl.cnf
+    echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf
+    echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
+fi  
+
 exec "$@"

+ 2 - 2
data/Dockerfiles/rspamd/Dockerfile

@@ -1,8 +1,8 @@
 FROM debian:bookworm-slim
-LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
+LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
 
 ARG DEBIAN_FRONTEND=noninteractive
-ARG RSPAMD_VER=rspamd_3.9.1-1~82f43560f
+ARG RSPAMD_VER=rspamd_3.10.2-1~b8a232043
 ARG CODENAME=bookworm
 ENV LC_ALL=C
 

+ 3 - 2
data/Dockerfiles/sogo/Dockerfile

@@ -33,13 +33,14 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
   && gosu nobody true \
   && mkdir /usr/share/doc/sogo \
   && touch /usr/share/doc/sogo/empty.sh \
-  && apt-key adv --keyserver keys.openpgp.org --recv-key 74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 \
+  && wget http://www.axis.cz/linux/debian/axis-archive-keyring.deb -O /tmp/axis-archive-keyring.deb \
+  && apt install -y /tmp/axis-archive-keyring.deb \
   && echo "deb [trusted=yes] ${SOGO_DEBIAN_REPOSITORY} ${DEBIAN_VERSION} sogo-v5" > /etc/apt/sources.list.d/sogo.list \
   && apt-get update && apt-get install -y --no-install-recommends \
     sogo \
     sogo-activesync \
   && apt-get autoclean \
-  && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/sogo.list \
+  && rm -rf /var/lib/apt/lists/* \
   && touch /etc/default/locale
 
 COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh

+ 2 - 0
data/Dockerfiles/sogo/docker-entrypoint.sh

@@ -10,6 +10,8 @@ if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
   cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
 fi
 
+echo "$TZ" > /etc/timezone
+
 # Run hooks
 for file in /hooks/*; do
   if [ -x "${file}" ]; then

+ 2 - 0
data/conf/postfix/main.cf

@@ -170,6 +170,8 @@ smtputf8_enable = no
 submission_smtpd_tls_mandatory_protocols = >=TLSv1.2
 smtps_smtpd_tls_mandatory_protocols = >=TLSv1.2
 parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks,qmqpd_authorized_clients
+# This Option is added to correctly set the X-Original-To Header when mails are send to lmtp (dovecot)
+lmtp_destination_recipient_limit=1
 
 # DO NOT EDIT ANYTHING BELOW #
 # Overrides #

+ 1 - 1
data/conf/postfix/master.cf

@@ -105,7 +105,7 @@ retry      unix  -       -       n       -       -       error
 discard    unix  -       -       n       -       -       discard
 local      unix  -       n       n       -       -       local
 virtual    unix  -       n       n       -       -       virtual
-lmtp       unix  -       -       n       -       -       lmtp
+lmtp       unix  -       -       n       -       -       lmtp flags=O
 anvil      unix  -       -       n       -       1       anvil
 scache     unix  -       -       n       -       1       scache
 maildrop   unix  -       n       n       -       -       pipe flags=DRhu

+ 80 - 21
data/conf/postfix/postscreen_access.cidr

@@ -1,6 +1,6 @@
-# Whitelist generated by Postwhite v3.4 on Thu Aug  1 00:16:45 UTC 2024
+# Whitelist generated by Postwhite v3.4 on Fri Nov  1 00:18:49 UTC 2024
 # https://github.com/stevejenkins/postwhite/
-# 1954 total rules
+# 2013 total rules
 2a00:1450:4000::/36	permit
 2a01:111:f400::/48	permit
 2a01:111:f403:8000::/50	permit
@@ -19,7 +19,8 @@
 8.20.114.31	permit
 8.25.194.0/23	permit
 8.25.196.0/23	permit
-10.162.0.0/16	permit
+8.39.54.0/23	permit
+8.40.222.0/23	permit
 12.130.86.238	permit
 13.110.208.0/21	permit
 13.110.209.0/24	permit
@@ -30,11 +31,10 @@
 15.200.21.50	permit
 15.200.44.248	permit
 15.200.201.185	permit
-17.41.0.0/16	permit
 17.57.155.0/24	permit
 17.57.156.0/24	permit
 17.58.0.0/16	permit
-17.142.0.0/15	permit
+17.143.234.140/30	permit
 18.156.89.250	permit
 18.157.243.190	permit
 18.194.95.56	permit
@@ -113,11 +113,15 @@
 40.92.0.0/16	permit
 40.107.0.0/16	permit
 40.112.65.63	permit
+40.233.64.216	permit
+40.233.83.78	permit
+40.233.88.28	permit
 43.228.184.0/22	permit
 44.206.138.57	permit
 44.217.45.156	permit
 44.236.56.93	permit
 44.238.220.251	permit
+45.14.148.0/22	permit
 46.19.170.16	permit
 46.226.48.0/21	permit
 46.228.36.37	permit
@@ -179,7 +183,9 @@
 50.18.126.162	permit
 50.31.32.0/19	permit
 50.31.36.205	permit
-50.56.130.220/30	permit
+50.56.130.220	permit
+50.56.130.221	permit
+50.56.130.222	permit
 52.1.14.157	permit
 52.5.230.59	permit
 52.27.5.72	permit
@@ -200,17 +206,18 @@
 52.96.91.34	permit
 52.96.111.82	permit
 52.96.172.98	permit
+52.96.214.50	permit
 52.96.222.194	permit
 52.96.222.226	permit
 52.96.223.2	permit
 52.96.228.130	permit
 52.96.229.242	permit
-52.100.0.0/14	permit
+52.100.0.0/15	permit
+52.102.0.0/16	permit
 52.103.0.0/17	permit
 52.119.213.144/28	permit
 52.185.106.240/28	permit
 52.200.59.0/24	permit
-52.205.61.79	permit
 52.207.191.216	permit
 52.222.62.51	permit
 52.222.73.83	permit
@@ -222,7 +229,6 @@
 52.236.28.240/28	permit
 54.90.148.255	permit
 54.165.19.38	permit
-54.172.97.247	permit
 54.174.52.0/24	permit
 54.174.57.0/24	permit
 54.174.59.0/24	permit
@@ -239,16 +245,12 @@
 54.244.54.130	permit
 54.244.242.0/24	permit
 54.255.61.23	permit
+57.103.64.0/18	permit
 62.13.128.0/24	permit
-62.13.128.196	permit
 62.13.129.128/25	permit
-62.13.136.0/22	permit
-62.13.140.0/22	permit
-62.13.144.0/22	permit
-62.13.148.0/23	permit
-62.13.150.0/23	permit
-62.13.152.0/23	permit
-62.13.159.196	permit
+62.13.136.0/21	permit
+62.13.144.0/21	permit
+62.13.152.0/21	permit
 62.17.146.128/26	permit
 62.179.121.0/24	permit
 62.201.172.0/27	permit
@@ -270,7 +272,6 @@
 64.127.115.252	permit
 64.132.88.0/23	permit
 64.132.92.0/24	permit
-64.147.123.128/27	permit
 64.207.219.7	permit
 64.207.219.8	permit
 64.207.219.9	permit
@@ -324,6 +325,7 @@
 65.110.161.77	permit
 65.123.29.213	permit
 65.123.29.220	permit
+65.154.166.0/24	permit
 65.212.180.36	permit
 66.102.0.0/20	permit
 66.119.150.192/26	permit
@@ -1283,6 +1285,9 @@
 117.120.16.0/21	permit
 119.42.242.52/31	permit
 119.42.242.156	permit
+121.244.91.48	permit
+121.244.91.52	permit
+122.15.156.182	permit
 123.126.78.64/29	permit
 124.108.96.24/31	permit
 124.108.96.28/31	permit
@@ -1311,7 +1316,9 @@
 129.41.77.70	permit
 129.41.169.249	permit
 129.80.5.164	permit
+129.80.64.36	permit
 129.80.67.121	permit
+129.80.145.156	permit
 129.145.74.12	permit
 129.146.88.28	permit
 129.146.147.105	permit
@@ -1322,6 +1329,9 @@
 129.153.168.146	permit
 129.153.190.200	permit
 129.153.194.228	permit
+129.154.255.129	permit
+129.158.56.255	permit
+129.159.22.159	permit
 129.159.87.137	permit
 129.213.195.191	permit
 130.61.9.72	permit
@@ -1338,7 +1348,19 @@
 134.170.141.64/26	permit
 134.170.143.0/24	permit
 134.170.174.0/24	permit
+135.84.80.0/24	permit
+135.84.81.0/24	permit
+135.84.82.0/24	permit
+135.84.83.0/24	permit
 135.84.216.0/22	permit
+136.143.160.0/24	permit
+136.143.161.0/24	permit
+136.143.162.0/24	permit
+136.143.178.49	permit
+136.143.182.0/23	permit
+136.143.184.0/24	permit
+136.143.188.0/24	permit
+136.143.190.0/23	permit
 136.147.128.0/20	permit
 136.147.135.0/24	permit
 136.147.176.0/20	permit
@@ -1353,7 +1375,9 @@
 139.138.46.219	permit
 139.138.57.55	permit
 139.138.58.119	permit
+139.167.79.86	permit
 139.180.17.0/24	permit
+140.238.148.191	permit
 141.148.159.229	permit
 141.193.32.0/23	permit
 141.193.184.32/27	permit
@@ -1362,6 +1386,7 @@
 141.193.185.32/27	permit
 141.193.185.64/26	permit
 141.193.185.128/25	permit
+143.47.120.152	permit
 143.55.224.0/21	permit
 143.55.232.0/22	permit
 143.55.236.0/22	permit
@@ -1375,7 +1400,10 @@
 144.178.38.0/24	permit
 145.253.228.160/29	permit
 145.253.239.128/29	permit
-146.20.14.104/30	permit
+146.20.14.104	permit
+146.20.14.105	permit
+146.20.14.106	permit
+146.20.14.107	permit
 146.20.112.0/26	permit
 146.20.113.0/24	permit
 146.20.191.0/24	permit
@@ -1394,10 +1422,14 @@
 149.72.248.236	permit
 149.97.173.180	permit
 150.230.98.160	permit
+151.145.38.14	permit
 152.67.105.195	permit
 152.69.200.236	permit
 152.70.155.126	permit
 155.248.208.51	permit
+155.248.220.138	permit
+155.248.234.149	permit
+155.248.237.141	permit
 157.55.0.192/26	permit
 157.55.1.128/26	permit
 157.55.2.0/25	permit
@@ -1452,7 +1484,9 @@
 163.114.132.120	permit
 163.114.134.16	permit
 163.114.135.16	permit
+164.152.23.32	permit
 164.177.132.168/30	permit
+165.173.128.0/24	permit
 166.78.68.0/22	permit
 166.78.68.221	permit
 166.78.69.169	permit
@@ -1476,13 +1510,21 @@
 167.220.67.232/29	permit
 168.138.5.36	permit
 168.138.73.51	permit
+168.138.77.31	permit
 168.245.0.0/17	permit
 168.245.12.252	permit
 168.245.46.9	permit
 168.245.127.231	permit
+169.148.129.0/24	permit
+169.148.131.0/24	permit
+169.148.142.10	permit
+169.148.144.0/25	permit
+169.148.144.10	permit
 170.10.68.0/22	permit
 170.10.128.0/24	permit
 170.10.129.0/24	permit
+170.10.132.56/29	permit
+170.10.132.64/29	permit
 170.10.133.0/24	permit
 172.217.0.0/19	permit
 172.217.32.0/20	permit
@@ -1491,6 +1533,7 @@
 172.217.192.0/19	permit
 172.253.56.0/21	permit
 172.253.112.0/20	permit
+173.0.84.0/29	permit
 173.0.84.224/27	permit
 173.0.94.244/30	permit
 173.194.0.0/16	permit
@@ -1509,7 +1552,6 @@
 174.36.114.148/30	permit
 174.36.114.152/29	permit
 174.37.67.28/30	permit
-174.129.203.189	permit
 175.41.215.51	permit
 176.32.105.0/24	permit
 176.32.127.0/24	permit
@@ -1582,6 +1624,8 @@
 188.172.128.0/20	permit
 192.0.64.0/18	permit
 192.18.139.154	permit
+192.18.145.36	permit
+192.18.152.58	permit
 192.30.252.0/22	permit
 192.161.144.0/20	permit
 192.162.87.0/24	permit
@@ -1634,13 +1678,22 @@
 199.16.156.0/22	permit
 199.33.145.1	permit
 199.33.145.32	permit
+199.34.22.36	permit
 199.59.148.0/22	permit
+199.67.80.2	permit
+199.67.80.20	permit
+199.67.82.2	permit
+199.67.82.20	permit
+199.67.84.0/24	permit
+199.67.86.0/24	permit
+199.67.88.0/24	permit
 199.101.161.130	permit
 199.101.162.0/25	permit
 199.122.120.0/21	permit
 199.122.123.0/24	permit
 199.127.232.0/22	permit
 199.255.192.0/22	permit
+202.12.124.128/27	permit
 202.129.242.0/23	permit
 202.165.102.47	permit
 202.177.148.100	permit
@@ -1691,7 +1744,11 @@
 204.92.114.187	permit
 204.92.114.203	permit
 204.92.114.204/31	permit
-204.220.160.0/20	permit
+204.141.32.0/23	permit
+204.141.42.0/23	permit
+204.220.160.0/21	permit
+204.220.168.0/21	permit
+204.220.176.0/20	permit
 204.232.168.0/24	permit
 205.139.110.0/24	permit
 205.201.128.0/20	permit
@@ -1942,6 +1999,8 @@
 2603:1030:20e:3::23c	permit
 2603:1030:b:3::152	permit
 2603:1030:c02:8::14	permit
+2607:13c0:0001:0000:0000:0000:0000:7000/116	permit
+2607:13c0:0002:0000:0000:0000:0000:1000/116	permit
 2607:f8b0:4000::/36	permit
 2620:109:c003:104::/64	permit
 2620:109:c003:104::215	permit

+ 44 - 30
data/conf/rspamd/local.d/mime_types.conf

@@ -1,27 +1,45 @@
+###############################################################################
+# This list is added/merged with defined defaults in LUA module:
+# https://github.com/rspamd/rspamd/blob/master/src/plugins/lua/mime_types.lua
+###############################################################################
+
 # Extensions that are treated as 'bad'
 # Number is score multiply factor
 bad_extensions = {
-  scr = 20,
-  lnk = 20,
-  exe = 20,
-  msi = 1,
-  msp = 1,
-  msu = 1,
-  jar = 2,
-  com = 20,
-  bat = 4,
-  cmd = 4,
-  ps1 = 4,
-  ace = 4,
-  arj = 4,
+  apk = 4,
+  appx = 4,
+  appxbundle = 4,
+  bat = 8,
   cab = 20,
+  cmd = 8,
+  com = 20,
+  diagcfg = 4,
+  diagpack = 4,
+  dmg = 8,
+  ex = 20,
+  ex_ = 20,
+  exe = 20,
+  img = 4,
+  jar = 8,
+  jnlp = 8,
+  js = 8,
+  jse = 8,
+  lnk = 20,
+  mjs = 8,
+  msi = 4,
+  msix = 4,
+  msixbundle = 4,
+  ps1 = 8,
+  scr = 20,
+  sct = 20,
+  vb = 20,
+  vbe = 20,
   vbs = 20,
-  hta = 4,
-  shs = 4,
-  wsc = 4,
-  wsf = 4,
-  iso = 8,
-  img = 8
+  vhd = 4,
+  py = 4,
+  reg = 8,
+  scf = 8,
+  vhdx = 4,
 };
 
 # Extensions that are particularly penalized for archives
@@ -30,18 +48,14 @@ bad_archive_extensions = {
   docx = 0.5,
   xlsx = 0.5,
   pdf = 1.0,
-  jar = 3,
-  js = 0.5,
-  vbs = 20,
-  exe = 20
+  jar = 12,
+  jnlp = 12,
+  bat = 12,
+  cmd = 12,
 };
 
 # Used to detect another archive in archive
 archive_extensions = {
-  zip = 1,
-  arj = 1,
-  rar = 1,
-  ace = 1,
-  7z = 1,
-  cab = 1
-};
+  tar = 1,
+  gz = 1,
+};

+ 1 - 0
data/conf/rspamd/local.d/options.inc

@@ -2,6 +2,7 @@ dns {
   enable_dnssec = true;
 }
 map_watch_interval = 30s;
+task_timeout = 30s;
 disable_monitoring = true;
 # In case a task times out (like DNS lookup), soft reject the message
 # instead of silently accepting the message without further processing.

+ 31 - 31
data/web/inc/functions.inc.php

@@ -939,10 +939,10 @@ function check_login($user, $pass, $app_passwd_data = false) {
     $stmt->execute(array(':user' => $user));
     $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
   }
-  foreach ($rows as $row) { 
+  foreach ($rows as $row) {
     // verify password
     if (verify_hash($row['password'], $pass) !== false) {
-      if (!array_key_exists("app_passwd_id", $row)){ 
+      if (!array_key_exists("app_passwd_id", $row)){
         // password is not a app password
         // check for tfa authenticators
         $authenticators = get_tfa($user);
@@ -953,11 +953,6 @@ function check_login($user, $pass, $app_passwd_data = false) {
           $_SESSION['pending_mailcow_cc_role'] = "user";
           $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
           unset($_SESSION['ldelay']);
-          $_SESSION['return'][] =  array(
-            'type' => 'success',
-            'log' => array(__FUNCTION__, $user, '*'),
-            'msg' => array('logged_in_as', $user)
-          );
           return "pending";
         } else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
           // no authenticators found, login successfull
@@ -966,6 +961,11 @@ function check_login($user, $pass, $app_passwd_data = false) {
           $stmt->execute(array(':user' => $user));
 
           unset($_SESSION['ldelay']);
+          $_SESSION['return'][] =  array(
+            'type' => 'success',
+            'log' => array(__FUNCTION__, $user, '*'),
+            'msg' => array('logged_in_as', $user)
+          );
           return "user";
         }
       } elseif ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) {
@@ -1028,7 +1028,7 @@ function update_sogo_static_view($mailbox = null) {
     // Check if the mailbox exists
     $stmt = $pdo->prepare("SELECT username FROM mailbox WHERE username = :mailbox AND active = '1'");
     $stmt->execute(array(':mailbox' => $mailbox));
-    $row = $stmt->fetch(PDO::FETCH_ASSOC);  
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
     if ($row){
       $mailbox_exists = true;
     }
@@ -1056,7 +1056,7 @@ function update_sogo_static_view($mailbox = null) {
               LEFT OUTER JOIN grouped_sender_acl_external external_acl ON external_acl.username = mailbox.username
             WHERE
               mailbox.active = '1'";
-  
+
   if ($mailbox_exists) {
     $query .= " AND mailbox.username = :mailbox";
     $stmt = $pdo->prepare($query);
@@ -1065,9 +1065,9 @@ function update_sogo_static_view($mailbox = null) {
     $query .= " GROUP BY mailbox.username";
     $stmt = $pdo->query($query);
   }
-  
+
   $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
-  
+
   flush_memcached();
 }
 function edit_user_account($_data) {
@@ -1100,7 +1100,7 @@ function edit_user_account($_data) {
           AND `username` = :user");
     $stmt->execute(array(':user' => $username));
     $row = $stmt->fetch(PDO::FETCH_ASSOC);
-  
+
     if (!verify_hash($row['password'], $password_old)) {
       $_SESSION['return'][] =  array(
         'type' => 'danger',
@@ -1109,7 +1109,7 @@ function edit_user_account($_data) {
       );
       return false;
     }
-  
+
     $password_new = $_data['user_new_pass'];
     $password_new2  = $_data['user_new_pass2'];
     if (password_check($password_new, $password_new2) !== true) {
@@ -1124,7 +1124,7 @@ function edit_user_account($_data) {
       ':password_hashed' => $password_hashed,
       ':username' => $username
     ));
-  
+
     update_sogo_static_view();
   }
   // edit password recovery email
@@ -1374,7 +1374,7 @@ function set_tfa($_data) {
             $_data['registration']->certificate,
             0
         ));
-    
+
         $_SESSION['return'][] =  array(
             'type' => 'success',
             'log' => array(__FUNCTION__, $_data_log),
@@ -1544,7 +1544,7 @@ function unset_tfa_key($_data) {
 
   try {
     if (!is_numeric($id)) $access_denied = true;
-    
+
     // set access_denied error
     if ($access_denied){
       $_SESSION['return'][] = array(
@@ -1553,7 +1553,7 @@ function unset_tfa_key($_data) {
         'msg' => 'access_denied'
       );
       return false;
-    } 
+    }
 
     // check if it's last key
     $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
@@ -1602,7 +1602,7 @@ function get_tfa($username = null, $id = null) {
         WHERE `username` = :username AND `active` = '1'");
     $stmt->execute(array(':username' => $username));
     $results = $stmt->fetchAll(PDO::FETCH_ASSOC);
- 
+
     // no tfa methods found
     if (count($results) == 0) {
         $data['name'] = 'none';
@@ -1810,8 +1810,8 @@ function verify_tfa_login($username, $_data) {
                   'msg' => array('webauthn_authenticator_failed')
               );
               return false;
-            } 
-            
+            }
+
             if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) {
                 $_SESSION['return'][] =  array(
                     'type' => 'danger',
@@ -2173,7 +2173,7 @@ function cors($action, $data = null) {
           'msg' => 'access_denied'
         );
         return false;
-      }    
+      }
 
       $allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array($_SERVER['SERVER_NAME']);
       $allowed_origins = !is_array($allowed_origins) ? array_filter(array_map('trim', explode("\n", $allowed_origins))) : $allowed_origins;
@@ -2206,7 +2206,7 @@ function cors($action, $data = null) {
         $redis->hMSet('CORS_SETTINGS', array(
           'allowed_origins' => implode(', ', $allowed_origins),
           'allowed_methods' => implode(', ', $allowed_methods)
-        ));   
+        ));
       } catch (RedisException $e) {
         $_SESSION['return'][] = array(
           'type' => 'danger',
@@ -2258,10 +2258,10 @@ function cors($action, $data = null) {
       header('Access-Control-Allow-Headers: Accept, Content-Type, X-Api-Key, Origin');
 
       // Access-Control settings requested, this is just a preflight request
-      if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS' && 
+      if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS' &&
         isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) &&
         isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
-  
+
         $allowed_methods = explode(', ', $cors_settings["allowed_methods"]);
         if (in_array($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'], $allowed_methods, true))
           // method allowed send 200 OK
@@ -2315,7 +2315,7 @@ function reset_password($action, $data = null) {
     break;
     case 'issue':
       $username = $data;
-      
+
       // perform cleanup
       $stmt = $pdo->prepare("DELETE FROM `reset_password` WHERE created < DATE_SUB(NOW(), INTERVAL :lifetime MINUTE);");
       $stmt->execute(array(':lifetime' => $PW_RESET_TOKEN_LIFETIME));
@@ -2397,8 +2397,8 @@ function reset_password($action, $data = null) {
       $request_date = new DateTime();
       $locale_date = locale_get_default();
       $date_formatter = new IntlDateFormatter(
-        $locale_date, 
-        IntlDateFormatter::FULL, 
+        $locale_date,
+        IntlDateFormatter::FULL,
         IntlDateFormatter::FULL
       );
       $formatted_request_date = $date_formatter->format($request_date);
@@ -2514,7 +2514,7 @@ function reset_password($action, $data = null) {
       $stmt->execute(array(
         ':username' => $username
       ));
-   
+
       $_SESSION['return'][] = array(
         'type' => 'success',
         'log' => array(__FUNCTION__, $action, $_data_log),
@@ -2557,7 +2557,7 @@ function reset_password($action, $data = null) {
       $text = $data['text'];
       $html = $data['html'];
       $subject = $data['subject'];
-    
+
       if (!filter_var($from, FILTER_VALIDATE_EMAIL)) {
         $_SESSION['return'][] =  array(
           'type' => 'danger',
@@ -2590,7 +2590,7 @@ function reset_password($action, $data = null) {
         );
         return false;
       }
-    
+
       ini_set('max_execution_time', 0);
       ini_set('max_input_time', 0);
       $mail = new PHPMailer;
@@ -2622,7 +2622,7 @@ function reset_password($action, $data = null) {
         return false;
       }
       $mail->ClearAllRecipients();
-    
+
       return true;
     break;
   }

+ 247 - 49
data/web/inc/functions.mailbox.inc.php

@@ -1233,7 +1233,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             ':active' => $active
           ));
 
-          
+
           if (isset($_data['acl'])) {
             $_data['acl'] = (array)$_data['acl'];
             $_data['spam_alias'] = (in_array('spam_alias', $_data['acl'])) ? 1 : 0;
@@ -1265,14 +1265,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             $_data['quarantine_attachments'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_attachments']);
             $_data['quarantine_notification'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_notification']);
             $_data['quarantine_category'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_category']);
-            $_data['app_passwds'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_app_passwds']);     
-            $_data['pw_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_pw_reset']);     
+            $_data['app_passwds'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_app_passwds']);
+            $_data['pw_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_pw_reset']);
           }
 
           try {
-            $stmt = $pdo->prepare("INSERT INTO `user_acl` 
+            $stmt = $pdo->prepare("INSERT INTO `user_acl`
               (`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`,
-                `pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`, `pw_reset`) 
+                `pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`, `pw_reset`)
               VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset,
                 :pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds, :pw_reset) ");
             $stmt->execute(array(
@@ -1467,7 +1467,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             return false;
           }
-          
+
           // check attributes
           $attr = array();
           $attr['tags']                       = (isset($_data['tags'])) ? $_data['tags'] : array();
@@ -1557,7 +1557,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
             $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
             $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
-          }   
+          }
           else {
             $attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
             $attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
@@ -2109,7 +2109,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 );
                 return false;
               }
-  
+
               // check if param is whitelisted
               if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){
                 // bad option
@@ -2802,11 +2802,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             // check name
             if ($is_now["template"] == "Default" && $is_now["template"] != $_data["template"]){
               // keep template name of Default template
-              $_data["template"]                   = $is_now["template"]; 
+              $_data["template"]                   = $is_now["template"];
             }
             else {
-              $_data["template"]                   = (isset($_data["template"])) ? $_data["template"] : $is_now["template"]; 
-            }   
+              $_data["template"]                   = (isset($_data["template"])) ? $_data["template"] : $is_now["template"];
+            }
             // check attributes
             $attr = array();
             $attr['tags']                       = (isset($_data['tags'])) ? $_data['tags'] : array();
@@ -2833,10 +2833,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               ":id" => $id ,
               ":template" => $_data["template"] ,
               ":attributes" => json_encode($attr)
-            )); 
+            ));
           }
 
-  
+
           $_SESSION['return'][] = array(
             'type' => 'success',
             'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -3192,7 +3192,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 ':tag_name' => $tag,
               ));
             }
-            
+
             $_SESSION['return'][] = array(
               'type' => 'success',
               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -3203,6 +3203,197 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           }
           return true;
         break;
+        case 'mailbox_rename':
+          $domain = $_data['domain'];
+          $old_local_part = $_data['old_local_part'];
+          $old_username = $old_local_part . "@" . $domain;
+          $new_local_part = $_data['new_local_part'];
+          $new_username = $new_local_part . "@" . $domain;
+          $create_alias = intval($_data['create_alias']);
+
+          if (!filter_var($old_username, FILTER_VALIDATE_EMAIL)) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => array('username_invalid', $old_username)
+            );
+            return false;
+          }
+          if (!filter_var($new_username, FILTER_VALIDATE_EMAIL)) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => array('username_invalid', $new_username)
+            );
+            return false;
+          }
+
+          $is_now = mailbox('get', 'mailbox_details', $old_username);
+          if (empty($is_now)) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => 'access_denied'
+            );
+            return false;
+          }
+
+          if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $is_now['domain'])) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => 'access_denied'
+            );
+            return false;
+          }
+
+          // get imap acls
+          try {
+            $exec_fields = array(
+              'cmd' => 'doveadm',
+              'task' => 'get_acl',
+              'id' => $old_username
+            );
+            $imap_acls = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true);
+            // delete imap acls
+            foreach ($imap_acls as $imap_acl) {
+              $exec_fields = array(
+                'cmd' => 'doveadm',
+                'task' => 'delete_acl',
+                'user' => $imap_acl['user'],
+                'mailbox' => $imap_acl['mailbox'],
+                'id' => $imap_acl['id']
+              );
+              docker('post', 'dovecot-mailcow', 'exec', $exec_fields);
+            }
+          } catch (Exception $e) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => $e->getMessage()
+            );
+            return false;
+          }
+
+          // rename username in sql
+          try {
+            $pdo->beginTransaction();
+            $pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
+
+            // Update username in mailbox table
+            $pdo->prepare('UPDATE mailbox SET username = :new_username, local_part = :new_local_part WHERE username = :old_username')
+              ->execute([
+                ':new_username' => $new_username,
+                ':new_local_part' => $new_local_part,
+                ':old_username' => $old_username
+              ]);
+
+            $pdo->prepare("UPDATE alias SET address = :new_username, goto = :new_username2 WHERE address = :old_username")
+            ->execute([
+              ':new_username' => $new_username,
+              ':new_username2' => $new_username,
+              ':old_username' => $old_username
+            ]);
+
+            // Update the username in all related tables
+            $tables = [
+              'tags_mailbox' => ['username'],
+              'sieve_filters' => ['username'],
+              'app_passwd' => ['mailbox'],
+              'user_acl' => ['username'],
+              'da_acl' => ['username'],
+              'quota2' => ['username'],
+              'quota2replica' => ['username'],
+              'pushover' => ['username'],
+              'alias' => ['goto'],
+              "imapsync" => ['user2'],
+              'bcc_maps' => ['local_dest', 'bcc_dest'],
+              'recipient_maps' => ['old_dest', 'new_dest'],
+              'sender_acl' => ['logged_in_as', 'send_as']
+            ];
+            foreach ($tables as $table => $columns) {
+              foreach ($columns as $column) {
+                $stmt = $pdo->prepare("UPDATE $table SET $column = :new_username WHERE $column = :old_username")
+                  ->execute([
+                    ':new_username' => $new_username,
+                    ':old_username' => $old_username
+                  ]);
+              }
+            }
+
+            // Update c_uid, c_name and mail in _sogo_static_view table
+            $pdo->prepare("UPDATE _sogo_static_view SET c_uid = :new_username, c_name = :new_username2, mail = :new_username3 WHERE c_uid = :old_username")
+              ->execute([
+                ':new_username' => $new_username,
+                ':new_username2' => $new_username,
+                ':new_username3' => $new_username,
+                ':old_username' => $old_username
+              ]);
+
+            // Re-enable foreign key checks
+            $pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
+            $pdo->commit();
+          } catch (PDOException $e) {
+            // Rollback the transaction if something goes wrong
+            $pdo->rollBack();
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => $e->getMessage()
+            );
+            return false;
+          }
+
+          // move maildir
+          $exec_fields = array(
+            'cmd' => 'maildir',
+            'task' => 'move',
+            'old_maildir' => $domain . '/' . $old_local_part,
+            'new_maildir' => $domain . '/' . $new_local_part
+          );
+          docker('post', 'dovecot-mailcow', 'exec', $exec_fields);
+
+          // rename username in sogo
+          $exec_fields = array(
+            'cmd' => 'sogo',
+            'task' => 'rename_user',
+            'old_username' => $old_username,
+            'new_username' => $new_username
+          );
+          docker('post', 'sogo-mailcow', 'exec', $exec_fields);
+
+          // set imap acls
+          foreach ($imap_acls as $imap_acl) {
+            $user_id = ($imap_acl['id'] == $old_username) ? $new_username : $imap_acl['id'];
+            $user = ($imap_acl['user'] == $old_username) ? $new_username : $imap_acl['user'];
+            $exec_fields = array(
+              'cmd' => 'doveadm',
+              'task' => 'set_acl',
+              'user' => $user,
+              'mailbox' => $imap_acl['mailbox'],
+              'id' => $user_id,
+              'rights' => $imap_acl['rights']
+            );
+            docker('post', 'dovecot-mailcow', 'exec', $exec_fields);
+          }
+
+          // create alias
+          if ($create_alias == 1) {
+            mailbox("add", "alias", array(
+              "address" => $old_username,
+              "goto" => $new_username,
+              "active" => 1,
+              "sogo_visible" => 1,
+              "private_comment" => sprintf($lang['success']['mailbox_renamed'], $old_username, $new_username)
+            ));
+          }
+
+          $_SESSION['return'][] = array(
+            'type' => 'success',
+            'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+            'msg' => array('mailbox_renamed', $old_username, $new_username)
+          );
+        break;
         case 'mailbox_templates':
           if ($_SESSION['mailcow_cc_role'] != "admin") {
             $_SESSION['return'][] = array(
@@ -3235,11 +3426,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             // check name
             if ($is_now["template"] == "Default" && $is_now["template"] != $_data["template"]){
               // keep template name of Default template
-              $_data["template"]                   = $is_now["template"]; 
+              $_data["template"]                   = $is_now["template"];
             }
             else {
-              $_data["template"]                   = (isset($_data["template"])) ? $_data["template"] : $is_now["template"]; 
-            }   
+              $_data["template"]                   = (isset($_data["template"])) ? $_data["template"] : $is_now["template"];
+            }
             // check attributes
             $attr = array();
             $attr["quota"]                       = isset($_data['quota']) ? intval($_data['quota']) * 1048576 : 0;
@@ -3259,11 +3450,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
               $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
               $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
-            }          
-            else { 
+            }
+            else {
               foreach ($is_now as $key => $value){
                 $attr[$key] = $is_now[$key];
-              }    
+              }
             }
             if (isset($_data['acl'])) {
               $_data['acl'] = (array)$_data['acl'];
@@ -3282,10 +3473,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               $attr['acl_quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0;
               $attr['acl_app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0;
               $attr['acl_pw_reset'] = (in_array('pw_reset', $_data['acl'])) ? 1 : 0;
-            } else {    
+            } else {
               foreach ($is_now as $key => $value){
                 $attr[$key] = $is_now[$key];
-              }        
+              }
             }
 
 
@@ -3297,7 +3488,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               ":id" => $id ,
               ":template" => $_data["template"] ,
               ":attributes" => json_encode($attr)
-            )); 
+            ));
           }
 
 
@@ -3326,7 +3517,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
-            $is_now = mailbox('get', 'mailbox_details', $mailbox);            
+            $is_now = mailbox('get', 'mailbox_details', $mailbox);
             if(!empty($is_now)){
               if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $is_now['domain'])) {
                 $_SESSION['return'][] = array(
@@ -3353,15 +3544,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             $stmt->execute(array(
               ":username" => $mailbox,
               ":custom_attributes" => json_encode($attributes)
-            ));             
-            
+            ));
+
             $_SESSION['return'][] = array(
               'type' => 'success',
               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
               'msg' => array('mailbox_modified', $mailbox)
             );
           }
-          
+
           return true;
         break;
         case 'resource':
@@ -3443,7 +3634,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
           }
         break;
-        case 'domain_wide_footer':  
+        case 'domain_wide_footer':
           if (!is_array($_data['domains'])) {
             $domains = array();
             $domains[] = $_data['domains'];
@@ -3696,7 +3887,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
 
             // prepend domain to array
             $params = array();
-            foreach ($tags as $key => $val){ 
+            foreach ($tags as $key => $val){
               array_push($params, '%'.$_data.'%');
               array_push($params, '%'.$val.'%');
             }
@@ -3705,7 +3896,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
 
             $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
             while($row = array_shift($rows)) {
-              if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], explode('@', $row['username'])[1])) 
+              if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], explode('@', $row['username'])[1]))
                 $mailboxes[] = $row['username'];
             }
           }
@@ -4260,7 +4451,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             while($row = array_shift($rows)) {
               if ($_SESSION['mailcow_cc_role'] == "admin")
                 $domains[] = $row['domain'];
-              elseif (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['domain'])) 
+              elseif (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['domain']))
                 $domains[] = $row['domain'];
             }
           } else {
@@ -4420,19 +4611,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           }
           $_data = (isset($_data)) ? intval($_data) : null;
 
-          if (isset($_data)){          
-            $stmt = $pdo->prepare("SELECT * FROM `templates` 
+          if (isset($_data)){
+            $stmt = $pdo->prepare("SELECT * FROM `templates`
               WHERE `id` = :id AND type = :type");
             $stmt->execute(array(
               ":id" => $_data,
               ":type" => "domain"
             ));
             $row = $stmt->fetch(PDO::FETCH_ASSOC);
-  
+
             if (empty($row)){
               return false;
             }
-  
+
             $row["attributes"] = json_decode($row["attributes"], true);
             return $row;
           }
@@ -4440,11 +4631,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             $stmt = $pdo->prepare("SELECT * FROM `templates` WHERE `type` =  'domain'");
             $stmt->execute();
             $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-  
+
             if (empty($rows)){
               return false;
             }
-  
+
             foreach($rows as $key => $row){
               $rows[$key]["attributes"] = json_decode($row["attributes"], true);
             }
@@ -4546,6 +4737,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             }
             else if ($SaslLogs['service'] == 'pop3') {
               $last_pop3_login = strtotime($SaslLogs['datetime']);
+            }
+			else if ($SaslLogs['service'] == 'SSO') {
+              $last_sso_login = strtotime($SaslLogs['datetime']);
             }
           }
           if (!isset($last_imap_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
@@ -4556,10 +4750,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           }
           if (!isset($last_pop3_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
             $last_pop3_login = 0;
+          }
+		  if (!isset($last_sso_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
+            $last_sso_login = 0;
           }
           $mailboxdata['last_imap_login'] = $last_imap_login;
           $mailboxdata['last_smtp_login'] = $last_smtp_login;
           $mailboxdata['last_pop3_login'] = $last_pop3_login;
+          $mailboxdata['last_sso_login'] = $last_sso_login;
 
           if (!isset($_extra) || $_extra != 'reduced') {
             $rl = ratelimit('get', 'mailbox', $_data);
@@ -4610,19 +4808,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           }
           $_data = (isset($_data)) ? intval($_data) : null;
 
-          if (isset($_data)){          
-            $stmt = $pdo->prepare("SELECT * FROM `templates` 
+          if (isset($_data)){
+            $stmt = $pdo->prepare("SELECT * FROM `templates`
               WHERE `id` = :id AND type = :type");
             $stmt->execute(array(
               ":id" => $_data,
               ":type" => "mailbox"
             ));
             $row = $stmt->fetch(PDO::FETCH_ASSOC);
-  
+
             if (empty($row)){
               return false;
             }
-  
+
             $row["attributes"] = json_decode($row["attributes"], true);
             return $row;
           }
@@ -5064,7 +5262,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             $ids = $_data['ids'];
           }
 
-          
+
           foreach ($ids as $id) {
             // delete template
             $stmt = $pdo->prepare("DELETE FROM `templates`
@@ -5377,7 +5575,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
-            
+
             update_sogo_static_view($username);
             $_SESSION['return'][] = array(
               'type' => 'success',
@@ -5404,7 +5602,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             $ids = $_data['ids'];
           }
 
-          
+
           foreach ($ids as $id) {
             // delete template
             $stmt = $pdo->prepare("DELETE FROM `templates`
@@ -5413,7 +5611,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               ":id" => $id,
               ":type" => "mailbox",
               ":template" => "Default"
-            )); 
+            ));
           }
 
           $_SESSION['return'][] = array(
@@ -5487,7 +5685,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
           }
         break;
-        case 'tags_domain':    
+        case 'tags_domain':
           if (!is_array($_data['domain'])) {
             $domains = array();
             $domains[] = $_data['domain'];
@@ -5500,7 +5698,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
 
 
           $wasModified = false;
-          foreach ($domains as $domain) {            
+          foreach ($domains as $domain) {
             if (!is_valid_domain_name($domain)) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
@@ -5517,7 +5715,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               return false;
             }
-            
+
             foreach($tags as $tag){
               // delete tag
               $wasModified = true;
@@ -5572,7 +5770,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             // delete tags
             foreach($tags as $tag){
               $wasModified = true;
-              
+
               $stmt = $pdo->prepare("DELETE FROM `tags_mailbox` WHERE `username` = :username AND `tag_name` = :tag_name");
               $stmt->execute(array(
                 ':username' => $username,

+ 162 - 16
data/web/inc/lib/composer.lock

@@ -1039,6 +1039,73 @@
             },
             "time": "2017-04-19T22:01:50+00:00"
         },
+        {
+            "name": "symfony/deprecation-contracts",
+            "version": "v3.5.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/deprecation-contracts.git",
+                "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
+                "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "3.5-dev"
+                },
+                "thanks": {
+                    "name": "symfony/contracts",
+                    "url": "https://github.com/symfony/contracts"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "function.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "A generic function and convention to trigger deprecation notices",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-04-18T09:32:20+00:00"
+        },
         {
             "name": "symfony/polyfill-ctype",
             "version": "v1.24.0",
@@ -1287,6 +1354,82 @@
             ],
             "time": "2021-09-13T13:58:33+00:00"
         },
+        {
+            "name": "symfony/polyfill-php81",
+            "version": "v1.31.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php81.git",
+                "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
+                "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2"
+            },
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php81\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2024-09-09T11:45:10+00:00"
+        },
         {
             "name": "symfony/translation",
             "version": "v6.0.5",
@@ -1604,34 +1747,37 @@
         },
         {
             "name": "twig/twig",
-            "version": "v3.4.3",
+            "version": "v3.14.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/twigphp/Twig.git",
-                "reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58"
+                "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/twigphp/Twig/zipball/c38fd6b0b7f370c198db91ffd02e23b517426b58",
-                "reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58",
+                "url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
+                "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
                 "shasum": ""
             },
             "require": {
-                "php": ">=7.2.5",
+                "php": ">=8.0.2",
+                "symfony/deprecation-contracts": "^2.5|^3",
                 "symfony/polyfill-ctype": "^1.8",
-                "symfony/polyfill-mbstring": "^1.3"
+                "symfony/polyfill-mbstring": "^1.3",
+                "symfony/polyfill-php81": "^1.29"
             },
             "require-dev": {
-                "psr/container": "^1.0",
-                "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0"
+                "psr/container": "^1.0|^2.0",
+                "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.4-dev"
-                }
-            },
             "autoload": {
+                "files": [
+                    "src/Resources/core.php",
+                    "src/Resources/debug.php",
+                    "src/Resources/escaper.php",
+                    "src/Resources/string_loader.php"
+                ],
                 "psr-4": {
                     "Twig\\": "src/"
                 }
@@ -1664,7 +1810,7 @@
             ],
             "support": {
                 "issues": "https://github.com/twigphp/Twig/issues",
-                "source": "https://github.com/twigphp/Twig/tree/v3.4.3"
+                "source": "https://github.com/twigphp/Twig/tree/v3.14.0"
             },
             "funding": [
                 {
@@ -1676,7 +1822,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-09-28T08:42:51+00:00"
+            "time": "2024-09-09T17:55:12+00:00"
         },
         {
             "name": "yubico/u2flib-server",
@@ -1728,5 +1874,5 @@
     "prefer-lowest": false,
     "platform": [],
     "platform-dev": [],
-    "plugin-api-version": "2.3.0"
+    "plugin-api-version": "2.6.0"
 }

+ 15 - 2
data/web/inc/lib/vendor/autoload.php

@@ -3,8 +3,21 @@
 // autoload.php @generated by Composer
 
 if (PHP_VERSION_ID < 50600) {
-    echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
-    exit(1);
+    if (!headers_sent()) {
+        header('HTTP/1.1 500 Internal Server Error');
+    }
+    $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+    if (!ini_get('display_errors')) {
+        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+            fwrite(STDERR, $err);
+        } elseif (!headers_sent()) {
+            echo $err;
+        }
+    }
+    trigger_error(
+        $err,
+        E_USER_ERROR
+    );
 }
 
 require_once __DIR__ . '/composer/autoload_real.php';

+ 72 - 65
data/web/inc/lib/vendor/composer/ClassLoader.php

@@ -42,35 +42,37 @@ namespace Composer\Autoload;
  */
 class ClassLoader
 {
-    /** @var ?string */
+    /** @var \Closure(string):void */
+    private static $includeFile;
+
+    /** @var string|null */
     private $vendorDir;
 
     // PSR-4
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, int>>
+     * @var array<string, array<string, int>>
      */
     private $prefixLengthsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, array<int, string>>
+     * @var array<string, list<string>>
      */
     private $prefixDirsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr4 = array();
 
     // PSR-0
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, string[]>>
+     * List of PSR-0 prefixes
+     *
+     * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
+     *
+     * @var array<string, array<string, list<string>>>
      */
     private $prefixesPsr0 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr0 = array();
 
@@ -78,8 +80,7 @@ class ClassLoader
     private $useIncludePath = false;
 
     /**
-     * @var string[]
-     * @psalm-var array<string, string>
+     * @var array<string, string>
      */
     private $classMap = array();
 
@@ -87,29 +88,29 @@ class ClassLoader
     private $classMapAuthoritative = false;
 
     /**
-     * @var bool[]
-     * @psalm-var array<string, bool>
+     * @var array<string, bool>
      */
     private $missingClasses = array();
 
-    /** @var ?string */
+    /** @var string|null */
     private $apcuPrefix;
 
     /**
-     * @var self[]
+     * @var array<string, self>
      */
     private static $registeredLoaders = array();
 
     /**
-     * @param ?string $vendorDir
+     * @param string|null $vendorDir
      */
     public function __construct($vendorDir = null)
     {
         $this->vendorDir = $vendorDir;
+        self::initializeIncludeClosure();
     }
 
     /**
-     * @return string[]
+     * @return array<string, list<string>>
      */
     public function getPrefixes()
     {
@@ -121,8 +122,7 @@ class ClassLoader
     }
 
     /**
-     * @return array[]
-     * @psalm-return array<string, array<int, string>>
+     * @return array<string, list<string>>
      */
     public function getPrefixesPsr4()
     {
@@ -130,8 +130,7 @@ class ClassLoader
     }
 
     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirs()
     {
@@ -139,8 +138,7 @@ class ClassLoader
     }
 
     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirsPsr4()
     {
@@ -148,8 +146,7 @@ class ClassLoader
     }
 
     /**
-     * @return string[] Array of classname => path
-     * @psalm-return array<string, string>
+     * @return array<string, string> Array of classname => path
      */
     public function getClassMap()
     {
@@ -157,8 +154,7 @@ class ClassLoader
     }
 
     /**
-     * @param string[] $classMap Class to filename map
-     * @psalm-param array<string, string> $classMap
+     * @param array<string, string> $classMap Class to filename map
      *
      * @return void
      */
@@ -175,24 +171,25 @@ class ClassLoader
      * Registers a set of PSR-0 directories for a given prefix, either
      * appending or prepending to the ones previously set for this prefix.
      *
-     * @param string          $prefix  The prefix
-     * @param string[]|string $paths   The PSR-0 root directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix
+     * @param list<string>|string $paths   The PSR-0 root directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @return void
      */
     public function add($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             if ($prepend) {
                 $this->fallbackDirsPsr0 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr0
                 );
             } else {
                 $this->fallbackDirsPsr0 = array_merge(
                     $this->fallbackDirsPsr0,
-                    (array) $paths
+                    $paths
                 );
             }
 
@@ -201,19 +198,19 @@ class ClassLoader
 
         $first = $prefix[0];
         if (!isset($this->prefixesPsr0[$first][$prefix])) {
-            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+            $this->prefixesPsr0[$first][$prefix] = $paths;
 
             return;
         }
         if ($prepend) {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixesPsr0[$first][$prefix]
             );
         } else {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
                 $this->prefixesPsr0[$first][$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -222,9 +219,9 @@ class ClassLoader
      * Registers a set of PSR-4 directories for a given namespace, either
      * appending or prepending to the ones previously set for this namespace.
      *
-     * @param string          $prefix  The prefix/namespace, with trailing '\\'
-     * @param string[]|string $paths   The PSR-4 base directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix/namespace, with trailing '\\'
+     * @param list<string>|string $paths   The PSR-4 base directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @throws \InvalidArgumentException
      *
@@ -232,17 +229,18 @@ class ClassLoader
      */
     public function addPsr4($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             // Register directories for the root namespace.
             if ($prepend) {
                 $this->fallbackDirsPsr4 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr4
                 );
             } else {
                 $this->fallbackDirsPsr4 = array_merge(
                     $this->fallbackDirsPsr4,
-                    (array) $paths
+                    $paths
                 );
             }
         } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@@ -252,18 +250,18 @@ class ClassLoader
                 throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
             }
             $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
-            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+            $this->prefixDirsPsr4[$prefix] = $paths;
         } elseif ($prepend) {
             // Prepend directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixDirsPsr4[$prefix]
             );
         } else {
             // Append directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
                 $this->prefixDirsPsr4[$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -272,8 +270,8 @@ class ClassLoader
      * Registers a set of PSR-0 directories for a given prefix,
      * replacing any others previously set for this prefix.
      *
-     * @param string          $prefix The prefix
-     * @param string[]|string $paths  The PSR-0 base directories
+     * @param string              $prefix The prefix
+     * @param list<string>|string $paths  The PSR-0 base directories
      *
      * @return void
      */
@@ -290,8 +288,8 @@ class ClassLoader
      * Registers a set of PSR-4 directories for a given namespace,
      * replacing any others previously set for this namespace.
      *
-     * @param string          $prefix The prefix/namespace, with trailing '\\'
-     * @param string[]|string $paths  The PSR-4 base directories
+     * @param string              $prefix The prefix/namespace, with trailing '\\'
+     * @param list<string>|string $paths  The PSR-4 base directories
      *
      * @throws \InvalidArgumentException
      *
@@ -425,7 +423,8 @@ class ClassLoader
     public function loadClass($class)
     {
         if ($file = $this->findFile($class)) {
-            includeFile($file);
+            $includeFile = self::$includeFile;
+            $includeFile($file);
 
             return true;
         }
@@ -476,9 +475,9 @@ class ClassLoader
     }
 
     /**
-     * Returns the currently registered loaders indexed by their corresponding vendor directories.
+     * Returns the currently registered loaders keyed by their corresponding vendor directories.
      *
-     * @return self[]
+     * @return array<string, self>
      */
     public static function getRegisteredLoaders()
     {
@@ -555,18 +554,26 @@ class ClassLoader
 
         return false;
     }
-}
 
-/**
- * Scope isolated include.
- *
- * Prevents access to $this/self from included files.
- *
- * @param  string $file
- * @return void
- * @private
- */
-function includeFile($file)
-{
-    include $file;
+    /**
+     * @return void
+     */
+    private static function initializeIncludeClosure()
+    {
+        if (self::$includeFile !== null) {
+            return;
+        }
+
+        /**
+         * Scope isolated include.
+         *
+         * Prevents access to $this/self from included files.
+         *
+         * @param  string $file
+         * @return void
+         */
+        self::$includeFile = \Closure::bind(static function($file) {
+            include $file;
+        }, null, null);
+    }
 }

+ 12 - 5
data/web/inc/lib/vendor/composer/InstalledVersions.php

@@ -98,7 +98,7 @@ class InstalledVersions
     {
         foreach (self::getInstalled() as $installed) {
             if (isset($installed['versions'][$packageName])) {
-                return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
+                return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
             }
         }
 
@@ -119,7 +119,7 @@ class InstalledVersions
      */
     public static function satisfies(VersionParser $parser, $packageName, $constraint)
     {
-        $constraint = $parser->parseConstraints($constraint);
+        $constraint = $parser->parseConstraints((string) $constraint);
         $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
 
         return $provided->matches($constraint);
@@ -328,7 +328,9 @@ class InstalledVersions
                 if (isset(self::$installedByVendor[$vendorDir])) {
                     $installed[] = self::$installedByVendor[$vendorDir];
                 } elseif (is_file($vendorDir.'/composer/installed.php')) {
-                    $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
+                    /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                    $required = require $vendorDir.'/composer/installed.php';
+                    $installed[] = self::$installedByVendor[$vendorDir] = $required;
                     if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
                         self::$installed = $installed[count($installed) - 1];
                     }
@@ -340,12 +342,17 @@ class InstalledVersions
             // only require the installed.php file if this file is loaded from its dumped location,
             // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
             if (substr(__DIR__, -8, 1) !== 'C') {
-                self::$installed = require __DIR__ . '/installed.php';
+                /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                $required = require __DIR__ . '/installed.php';
+                self::$installed = $required;
             } else {
                 self::$installed = array();
             }
         }
-        $installed[] = self::$installed;
+
+        if (self::$installed !== array()) {
+            $installed[] = self::$installed;
+        }
 
         return $installed;
     }

+ 2 - 0
data/web/inc/lib/vendor/composer/autoload_classmap.php

@@ -7,7 +7,9 @@ $baseDir = dirname($vendorDir);
 
 return array(
     'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
+    'CURLStringFile' => $vendorDir . '/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php',
     'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
+    'ReturnTypeWillChange' => $vendorDir . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php',
     'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
     'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
     'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',

+ 6 - 0
data/web/inc/lib/vendor/composer/autoload_files.php

@@ -10,8 +10,14 @@ return array(
     'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
     'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
     '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
+    '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
     '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
+    '23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php',
     'fe62ba7e10580d903cc46d808b5961a4' => $vendorDir . '/tightenco/collect/src/Collect/Support/helpers.php',
     'caf31cc6ec7cf2241cb6f12c226c3846' => $vendorDir . '/tightenco/collect/src/Collect/Support/alias.php',
     '04c6c5c2f7095ccf6c481d3e53e1776f' => $vendorDir . '/mustangostang/spyc/Spyc.php',
+    '89efb1254ef2d1c5d80096acd12c4098' => $vendorDir . '/twig/twig/src/Resources/core.php',
+    'ffecb95d45175fd40f75be8a23b34f90' => $vendorDir . '/twig/twig/src/Resources/debug.php',
+    'c7baa00073ee9c61edf148c51917cfb4' => $vendorDir . '/twig/twig/src/Resources/escaper.php',
+    'f844ccf1d25df8663951193c3fc307c8' => $vendorDir . '/twig/twig/src/Resources/string_loader.php',
 );

+ 1 - 0
data/web/inc/lib/vendor/composer/autoload_psr4.php

@@ -8,6 +8,7 @@ $baseDir = dirname($vendorDir);
 return array(
     'Twig\\' => array($vendorDir . '/twig/twig/src'),
     'Tightenco\\Collect\\' => array($vendorDir . '/tightenco/collect/src/Collect'),
+    'Symfony\\Polyfill\\Php81\\' => array($vendorDir . '/symfony/polyfill-php81'),
     'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
     'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
     'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),

+ 10 - 17
data/web/inc/lib/vendor/composer/autoload_real.php

@@ -33,25 +33,18 @@ class ComposerAutoloaderInit873464e4bd965a3168f133248b1b218b
 
         $loader->register(true);
 
-        $includeFiles = \Composer\Autoload\ComposerStaticInit873464e4bd965a3168f133248b1b218b::$files;
-        foreach ($includeFiles as $fileIdentifier => $file) {
-            composerRequire873464e4bd965a3168f133248b1b218b($fileIdentifier, $file);
+        $filesToLoad = \Composer\Autoload\ComposerStaticInit873464e4bd965a3168f133248b1b218b::$files;
+        $requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
+            if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+                $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+
+                require $file;
+            }
+        }, null, null);
+        foreach ($filesToLoad as $fileIdentifier => $file) {
+            $requireFile($fileIdentifier, $file);
         }
 
         return $loader;
     }
 }
-
-/**
- * @param string $fileIdentifier
- * @param string $file
- * @return void
- */
-function composerRequire873464e4bd965a3168f133248b1b218b($fileIdentifier, $file)
-{
-    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
-        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
-
-        require $file;
-    }
-}

+ 13 - 0
data/web/inc/lib/vendor/composer/autoload_static.php

@@ -11,10 +11,16 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
         'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
         'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php',
         '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
+        '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
         '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
+        '23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php',
         'fe62ba7e10580d903cc46d808b5961a4' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/helpers.php',
         'caf31cc6ec7cf2241cb6f12c226c3846' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/alias.php',
         '04c6c5c2f7095ccf6c481d3e53e1776f' => __DIR__ . '/..' . '/mustangostang/spyc/Spyc.php',
+        '89efb1254ef2d1c5d80096acd12c4098' => __DIR__ . '/..' . '/twig/twig/src/Resources/core.php',
+        'ffecb95d45175fd40f75be8a23b34f90' => __DIR__ . '/..' . '/twig/twig/src/Resources/debug.php',
+        'c7baa00073ee9c61edf148c51917cfb4' => __DIR__ . '/..' . '/twig/twig/src/Resources/escaper.php',
+        'f844ccf1d25df8663951193c3fc307c8' => __DIR__ . '/..' . '/twig/twig/src/Resources/string_loader.php',
     );
 
     public static $prefixLengthsPsr4 = array (
@@ -25,6 +31,7 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
         ),
         'S' => 
         array (
+            'Symfony\\Polyfill\\Php81\\' => 23,
             'Symfony\\Polyfill\\Php80\\' => 23,
             'Symfony\\Polyfill\\Mbstring\\' => 26,
             'Symfony\\Polyfill\\Ctype\\' => 23,
@@ -80,6 +87,10 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
         array (
             0 => __DIR__ . '/..' . '/tightenco/collect/src/Collect',
         ),
+        'Symfony\\Polyfill\\Php81\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/symfony/polyfill-php81',
+        ),
         'Symfony\\Polyfill\\Php80\\' => 
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
@@ -170,7 +181,9 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
 
     public static $classMap = array (
         'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
+        'CURLStringFile' => __DIR__ . '/..' . '/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php',
         'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+        'ReturnTypeWillChange' => __DIR__ . '/..' . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php',
         'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
         'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
         'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',

+ 168 - 16
data/web/inc/lib/vendor/composer/installed.json

@@ -1068,6 +1068,76 @@
             ],
             "install-path": "../soundasleep/html2text"
         },
+        {
+            "name": "symfony/deprecation-contracts",
+            "version": "v3.5.0",
+            "version_normalized": "3.5.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/deprecation-contracts.git",
+                "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
+                "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.1"
+            },
+            "time": "2024-04-18T09:32:20+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "3.5-dev"
+                },
+                "thanks": {
+                    "name": "symfony/contracts",
+                    "url": "https://github.com/symfony/contracts"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "function.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "A generic function and convention to trigger deprecation notices",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../symfony/deprecation-contracts"
+        },
         {
             "name": "symfony/polyfill-ctype",
             "version": "v1.24.0",
@@ -1325,6 +1395,85 @@
             ],
             "install-path": "../symfony/polyfill-php80"
         },
+        {
+            "name": "symfony/polyfill-php81",
+            "version": "v1.31.0",
+            "version_normalized": "1.31.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php81.git",
+                "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
+                "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2"
+            },
+            "time": "2024-09-09T11:45:10+00:00",
+            "type": "library",
+            "extra": {
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php81\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../symfony/polyfill-php81"
+        },
         {
             "name": "symfony/translation",
             "version": "v6.0.5",
@@ -1654,37 +1803,40 @@
         },
         {
             "name": "twig/twig",
-            "version": "v3.4.3",
-            "version_normalized": "3.4.3.0",
+            "version": "v3.14.0",
+            "version_normalized": "3.14.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/twigphp/Twig.git",
-                "reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58"
+                "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/twigphp/Twig/zipball/c38fd6b0b7f370c198db91ffd02e23b517426b58",
-                "reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58",
+                "url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
+                "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
                 "shasum": ""
             },
             "require": {
-                "php": ">=7.2.5",
+                "php": ">=8.0.2",
+                "symfony/deprecation-contracts": "^2.5|^3",
                 "symfony/polyfill-ctype": "^1.8",
-                "symfony/polyfill-mbstring": "^1.3"
+                "symfony/polyfill-mbstring": "^1.3",
+                "symfony/polyfill-php81": "^1.29"
             },
             "require-dev": {
-                "psr/container": "^1.0",
-                "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0"
+                "psr/container": "^1.0|^2.0",
+                "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
             },
-            "time": "2022-09-28T08:42:51+00:00",
+            "time": "2024-09-09T17:55:12+00:00",
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.4-dev"
-                }
-            },
             "installation-source": "dist",
             "autoload": {
+                "files": [
+                    "src/Resources/core.php",
+                    "src/Resources/debug.php",
+                    "src/Resources/escaper.php",
+                    "src/Resources/string_loader.php"
+                ],
                 "psr-4": {
                     "Twig\\": "src/"
                 }
@@ -1717,7 +1869,7 @@
             ],
             "support": {
                 "issues": "https://github.com/twigphp/Twig/issues",
-                "source": "https://github.com/twigphp/Twig/tree/v3.4.3"
+                "source": "https://github.com/twigphp/Twig/tree/v3.14.0"
             },
             "funding": [
                 {

+ 23 - 5
data/web/inc/lib/vendor/composer/installed.php

@@ -3,7 +3,7 @@
         'name' => '__root__',
         'pretty_version' => 'dev-master',
         'version' => 'dev-master',
-        'reference' => '8e0b1d8aee4af02311692cb031695cc2ac3850fd',
+        'reference' => '220fdbb168792c07493db330d898b345cc902055',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -13,7 +13,7 @@
         '__root__' => array(
             'pretty_version' => 'dev-master',
             'version' => 'dev-master',
-            'reference' => '8e0b1d8aee4af02311692cb031695cc2ac3850fd',
+            'reference' => '220fdbb168792c07493db330d898b345cc902055',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
@@ -175,6 +175,15 @@
             'aliases' => array(),
             'dev_requirement' => false,
         ),
+        'symfony/deprecation-contracts' => array(
+            'pretty_version' => 'v3.5.0',
+            'version' => '3.5.0.0',
+            'reference' => '0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
         'symfony/polyfill-ctype' => array(
             'pretty_version' => 'v1.24.0',
             'version' => '1.24.0.0',
@@ -202,6 +211,15 @@
             'aliases' => array(),
             'dev_requirement' => false,
         ),
+        'symfony/polyfill-php81' => array(
+            'pretty_version' => 'v1.31.0',
+            'version' => '1.31.0.0',
+            'reference' => '4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../symfony/polyfill-php81',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
         'symfony/translation' => array(
             'pretty_version' => 'v6.0.5',
             'version' => '6.0.5.0',
@@ -245,9 +263,9 @@
             'dev_requirement' => false,
         ),
         'twig/twig' => array(
-            'pretty_version' => 'v3.4.3',
-            'version' => '3.4.3.0',
-            'reference' => 'c38fd6b0b7f370c198db91ffd02e23b517426b58',
+            'pretty_version' => 'v3.14.0',
+            'version' => '3.14.0.0',
+            'reference' => '126b2c97818dbff0cdf3fbfc881aedb3d40aae72',
             'type' => 'library',
             'install_path' => __DIR__ . '/../twig/twig',
             'aliases' => array(),

+ 2 - 2
data/web/inc/lib/vendor/composer/platform_check.php

@@ -4,8 +4,8 @@
 
 $issues = array();
 
-if (!(PHP_VERSION_ID >= 80002)) {
-    $issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.2". You are running ' . PHP_VERSION . '.';
+if (!(PHP_VERSION_ID >= 80100)) {
+    $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
 }
 
 if ($issues) {

+ 5 - 0
data/web/inc/lib/vendor/symfony/deprecation-contracts/CHANGELOG.md

@@ -0,0 +1,5 @@
+CHANGELOG
+=========
+
+The changelog is maintained for all Symfony contracts at the following URL:
+https://github.com/symfony/contracts/blob/main/CHANGELOG.md

+ 19 - 0
data/web/inc/lib/vendor/symfony/deprecation-contracts/LICENSE

@@ -0,0 +1,19 @@
+Copyright (c) 2020-present Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 26 - 0
data/web/inc/lib/vendor/symfony/deprecation-contracts/README.md

@@ -0,0 +1,26 @@
+Symfony Deprecation Contracts
+=============================
+
+A generic function and convention to trigger deprecation notices.
+
+This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices.
+
+By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component,
+the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments.
+
+The function requires at least 3 arguments:
+ - the name of the Composer package that is triggering the deprecation
+ - the version of the package that introduced the deprecation
+ - the message of the deprecation
+ - more arguments can be provided: they will be inserted in the message using `printf()` formatting
+
+Example:
+```php
+trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin');
+```
+
+This will generate the following message:
+`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.`
+
+While not recommended, the deprecation notices can be completely ignored by declaring an empty
+`function trigger_deprecation() {}` in your application.

+ 35 - 0
data/web/inc/lib/vendor/symfony/deprecation-contracts/composer.json

@@ -0,0 +1,35 @@
+{
+    "name": "symfony/deprecation-contracts",
+    "type": "library",
+    "description": "A generic function and convention to trigger deprecation notices",
+    "homepage": "https://symfony.com",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Nicolas Grekas",
+            "email": "p@tchwork.com"
+        },
+        {
+            "name": "Symfony Community",
+            "homepage": "https://symfony.com/contributors"
+        }
+    ],
+    "require": {
+        "php": ">=8.1"
+    },
+    "autoload": {
+        "files": [
+            "function.php"
+        ]
+    },
+    "minimum-stability": "dev",
+    "extra": {
+        "branch-alias": {
+            "dev-main": "3.5-dev"
+        },
+        "thanks": {
+            "name": "symfony/contracts",
+            "url": "https://github.com/symfony/contracts"
+        }
+    }
+}

+ 27 - 0
data/web/inc/lib/vendor/symfony/deprecation-contracts/function.php

@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+if (!function_exists('trigger_deprecation')) {
+    /**
+     * Triggers a silenced deprecation notice.
+     *
+     * @param string $package The name of the Composer package that is triggering the deprecation
+     * @param string $version The version of the package that introduced the deprecation
+     * @param string $message The message of the deprecation
+     * @param mixed  ...$args Values to insert in the message using printf() formatting
+     *
+     * @author Nicolas Grekas <p@tchwork.com>
+     */
+    function trigger_deprecation(string $package, string $version, string $message, mixed ...$args): void
+    {
+        @trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED);
+    }
+}

+ 19 - 0
data/web/inc/lib/vendor/symfony/polyfill-php81/LICENSE

@@ -0,0 +1,19 @@
+Copyright (c) 2021-present Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 37 - 0
data/web/inc/lib/vendor/symfony/polyfill-php81/Php81.php

@@ -0,0 +1,37 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Polyfill\Php81;
+
+/**
+ * @author Nicolas Grekas <p@tchwork.com>
+ *
+ * @internal
+ */
+final class Php81
+{
+    public static function array_is_list(array $array): bool
+    {
+        if ([] === $array || $array === array_values($array)) {
+            return true;
+        }
+
+        $nextKey = -1;
+
+        foreach ($array as $k => $v) {
+            if ($k !== ++$nextKey) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}

+ 18 - 0
data/web/inc/lib/vendor/symfony/polyfill-php81/README.md

@@ -0,0 +1,18 @@
+Symfony Polyfill / Php81
+========================
+
+This component provides features added to PHP 8.1 core:
+
+- [`array_is_list`](https://php.net/array_is_list)
+- [`enum_exists`](https://php.net/enum-exists)
+- [`MYSQLI_REFRESH_REPLICA`](https://php.net/mysqli.constants#constantmysqli-refresh-replica) constant
+- [`ReturnTypeWillChange`](https://wiki.php.net/rfc/internal_method_return_types)
+- [`CURLStringFile`](https://php.net/CURLStringFile) (but only if PHP >= 7.4 is used)
+
+More information can be found in the
+[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
+
+License
+=======
+
+This library is released under the [MIT license](LICENSE).

+ 51 - 0
data/web/inc/lib/vendor/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php

@@ -0,0 +1,51 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+if (\PHP_VERSION_ID >= 70400 && extension_loaded('curl')) {
+    /**
+     * @property string $data
+     */
+    class CURLStringFile extends CURLFile
+    {
+        private $data;
+
+        public function __construct(string $data, string $postname, string $mime = 'application/octet-stream')
+        {
+            $this->data = $data;
+            parent::__construct('data://application/octet-stream;base64,'.base64_encode($data), $mime, $postname);
+        }
+
+        public function __set(string $name, $value): void
+        {
+            if ('data' !== $name) {
+                $this->$name = $value;
+
+                return;
+            }
+
+            if (is_object($value) ? !method_exists($value, '__toString') : !is_scalar($value)) {
+                throw new TypeError('Cannot assign '.gettype($value).' to property CURLStringFile::$data of type string');
+            }
+
+            $this->name = 'data://application/octet-stream;base64,'.base64_encode($value);
+        }
+
+        public function __isset(string $name): bool
+        {
+            return isset($this->$name);
+        }
+
+        public function &__get(string $name)
+        {
+            return $this->$name;
+        }
+    }
+}

+ 20 - 0
data/web/inc/lib/vendor/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php

@@ -0,0 +1,20 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+if (\PHP_VERSION_ID < 80100) {
+    #[Attribute(Attribute::TARGET_METHOD)]
+    final class ReturnTypeWillChange
+    {
+        public function __construct()
+        {
+        }
+    }
+}

+ 28 - 0
data/web/inc/lib/vendor/symfony/polyfill-php81/bootstrap.php

@@ -0,0 +1,28 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Polyfill\Php81 as p;
+
+if (\PHP_VERSION_ID >= 80100) {
+    return;
+}
+
+if (defined('MYSQLI_REFRESH_SLAVE') && !defined('MYSQLI_REFRESH_REPLICA')) {
+    define('MYSQLI_REFRESH_REPLICA', 64);
+}
+
+if (!function_exists('array_is_list')) {
+    function array_is_list(array $array): bool { return p\Php81::array_is_list($array); }
+}
+
+if (!function_exists('enum_exists')) {
+    function enum_exists(string $enum, bool $autoload = true): bool { return $autoload && class_exists($enum) && false; }
+}

+ 33 - 0
data/web/inc/lib/vendor/symfony/polyfill-php81/composer.json

@@ -0,0 +1,33 @@
+{
+    "name": "symfony/polyfill-php81",
+    "type": "library",
+    "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
+    "keywords": ["polyfill", "shim", "compatibility", "portable"],
+    "homepage": "https://symfony.com",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Nicolas Grekas",
+            "email": "p@tchwork.com"
+        },
+        {
+            "name": "Symfony Community",
+            "homepage": "https://symfony.com/contributors"
+        }
+    ],
+    "require": {
+        "php": ">=7.2"
+    },
+    "autoload": {
+        "psr-4": { "Symfony\\Polyfill\\Php81\\": "" },
+        "files": [ "bootstrap.php" ],
+        "classmap": [ "Resources/stubs" ]
+    },
+    "minimum-stability": "dev",
+    "extra": {
+        "thanks": {
+            "name": "symfony/polyfill",
+            "url": "https://github.com/symfony/polyfill"
+        }
+    }
+}

+ 0 - 18
data/web/inc/lib/vendor/twig/twig/.editorconfig

@@ -1,18 +0,0 @@
-; top-most EditorConfig file
-root = true
-
-; Unix-style newlines
-[*]
-end_of_line = LF
-
-[*.php]
-indent_style = space
-indent_size = 4
-
-[*.test]
-indent_style = space
-indent_size = 4
-
-[*.rst]
-indent_style = space
-indent_size = 4

+ 0 - 4
data/web/inc/lib/vendor/twig/twig/.gitattributes

@@ -1,4 +0,0 @@
-/doc/ export-ignore
-/extra/ export-ignore
-/tests/ export-ignore
-/phpunit.xml.dist export-ignore

+ 0 - 149
data/web/inc/lib/vendor/twig/twig/.github/workflows/ci.yml

@@ -1,149 +0,0 @@
-name: "CI"
-
-on:
-    pull_request:
-    push:
-        branches:
-            - '3.x'
-
-env:
-    SYMFONY_PHPUNIT_DISABLE_RESULT_CACHE: 1
-
-permissions:
-  contents: read
-
-jobs:
-    tests:
-        name: "PHP ${{ matrix.php-version }}"
-
-        runs-on: 'ubuntu-latest'
-
-        continue-on-error: ${{ matrix.experimental }}
-
-        strategy:
-            matrix:
-                php-version:
-                    - '7.2.5'
-                    - '7.3'
-                    - '7.4'
-                    - '8.0'
-                    - '8.1'
-                experimental: [false]
-
-        steps:
-            - name: "Checkout code"
-              uses: actions/checkout@v4
-
-            - name: "Install PHP with extensions"
-              uses: shivammathur/setup-php@v2
-              with:
-                  coverage: "none"
-                  php-version: ${{ matrix.php-version }}
-                  ini-values: memory_limit=-1
-
-            - name: "Add PHPUnit matcher"
-              run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
-
-            - run: composer install
-
-            - name: "Install PHPUnit"
-              run: vendor/bin/simple-phpunit install
-
-            - name: "PHPUnit version"
-              run: vendor/bin/simple-phpunit --version
-
-            - name: "Run tests"
-              run: vendor/bin/simple-phpunit
-
-    extension-tests:
-        needs:
-            - 'tests'
-
-        name: "${{ matrix.extension }} with PHP ${{ matrix.php-version }}"
-
-        runs-on: 'ubuntu-latest'
-
-        continue-on-error: true
-
-        strategy:
-            matrix:
-                php-version:
-                    - '7.2.5'
-                    - '7.3'
-                    - '7.4'
-                    - '8.0'
-                    - '8.1'
-                extension:
-                    - 'extra/cache-extra'
-                    - 'extra/cssinliner-extra'
-                    - 'extra/html-extra'
-                    - 'extra/inky-extra'
-                    - 'extra/intl-extra'
-                    - 'extra/markdown-extra'
-                    - 'extra/string-extra'
-                    - 'extra/twig-extra-bundle'
-                experimental: [false]
-
-        steps:
-            - name: "Checkout code"
-              uses: actions/checkout@v4
-
-            - name: "Install PHP with extensions"
-              uses: shivammathur/setup-php@v2
-              with:
-                  coverage: "none"
-                  php-version: ${{ matrix.php-version }}
-                  ini-values: memory_limit=-1
-
-            - name: "Add PHPUnit matcher"
-              run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
-
-            - run: composer install
-
-            - name: "Install PHPUnit"
-              run: vendor/bin/simple-phpunit install
-
-            - name: "PHPUnit version"
-              run: vendor/bin/simple-phpunit --version
-
-            - name: "Composer install"
-              working-directory: ${{ matrix.extension}}
-              run: composer install
-
-            - name: "Run tests"
-              working-directory: ${{ matrix.extension}}
-              run: ../../vendor/bin/simple-phpunit
-
-#
-#    Drupal does not support Twig 3 now!
-#
-#    integration-tests:
-#        needs:
-#            - 'tests'
-#
-#        name: "Integration tests with PHP ${{ matrix.php-version }}"
-#
-#        runs-on: 'ubuntu-20.04'
-#
-#        continue-on-error: true
-#
-#        strategy:
-#            matrix:
-#                php-version:
-#                    - '7.3'
-#
-#        steps:
-#            - name: "Checkout code"
-#              uses: actions/checkout@v2
-#
-#            - name: "Install PHP with extensions"
-#              uses: shivammathur/setup-php@2
-#              with:
-#                  coverage: "none"
-#                  extensions: "gd, pdo_sqlite"
-#                  php-version: ${{ matrix.php-version }}
-#                  ini-values: memory_limit=-1
-#                  tools: composer:v2
-#
-#            - run: bash ./tests/drupal_test.sh
-#              shell: "bash"

+ 0 - 64
data/web/inc/lib/vendor/twig/twig/.github/workflows/documentation.yml

@@ -1,64 +0,0 @@
-name: "Documentation"
-
-on:
-    pull_request:
-    push:
-        branches:
-            - '2.x'
-            - '3.x'
-
-permissions:
-  contents: read
-
-jobs:
-    build:
-        name: "Build"
-
-        runs-on: ubuntu-latest
-
-        steps:
-            -   name: "Checkout code"
-                uses: actions/checkout@v4
-
-            -   name: "Set-up PHP"
-                uses: shivammathur/setup-php@v2
-                with:
-                    php-version: 8.1
-                    coverage: none
-                    tools: "composer:v2"
-
-            -   name: Get composer cache directory
-                id: composercache
-                working-directory: doc/_build
-                run: echo "::set-output name=dir::$(composer config cache-files-dir)"
-
-            -   name: Cache dependencies
-                uses: actions/cache@v3
-                with:
-                    path: ${{ steps.composercache.outputs.dir }}
-                    key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
-                    restore-keys: ${{ runner.os }}-composer-
-
-            -   name: "Install dependencies"
-                working-directory: doc/_build
-                run: composer install --prefer-dist --no-progress
-
-            -   name: "Build the docs"
-                working-directory: doc/_build
-                run: php build.php --disable-cache
-
-    doctor-rst:
-        name: "DOCtor-RST"
-
-        runs-on: ubuntu-latest
-
-        steps:
-            - name: "Checkout code"
-              uses: actions/checkout@v4
-
-            - name: "Run DOCtor-RST"
-              uses: docker://oskarstark/doctor-rst
-              with:
-                  args: --short
-              env:
-                  DOCS_DIR: 'doc/'

+ 0 - 6
data/web/inc/lib/vendor/twig/twig/.gitignore

@@ -1,6 +0,0 @@
-/doc/_build/vendor
-/doc/_build/output
-/composer.lock
-/phpunit.xml
-/vendor
-.phpunit.result.cache

+ 0 - 20
data/web/inc/lib/vendor/twig/twig/.php-cs-fixer.dist.php

@@ -1,20 +0,0 @@
-<?php
-
-return (new PhpCsFixer\Config())
-    ->setRules([
-        '@Symfony' => true,
-        '@Symfony:risky' => true,
-        '@PHPUnit75Migration:risky' => true,
-        'php_unit_dedicate_assert' => ['target' => '5.6'],
-        'array_syntax' => ['syntax' => 'short'],
-        'php_unit_fqcn_annotation' => true,
-        'no_unreachable_default_argument_value' => false,
-        'braces' => ['allow_single_line_closure' => true],
-        'heredoc_to_nowdoc' => false,
-        'ordered_imports' => true,
-        'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'],
-        'native_function_invocation' => ['include' => ['@compiler_optimized'], 'scope' => 'all'],
-    ])
-    ->setRiskyAllowed(true)
-    ->setFinder((new PhpCsFixer\Finder())->in(__DIR__))
-;

+ 176 - 1
data/web/inc/lib/vendor/twig/twig/CHANGELOG

@@ -1,3 +1,178 @@
+# 3.14.0 (2024-09-09)
+
+ * Fix a security issue when an included sandboxed template has been loaded before without the sandbox context
+ * Add the possibility to reset globals via `Environment::resetGlobals()`
+ * Deprecate `Environment::mergeGlobals()`
+
+# 3.13.0 (2024-09-07)
+
+ * Add the `types` tag (experimental)
+ * Deprecate the `Twig\Test\NodeTestCase::getTests()` data provider, override `provideTests()` instead.
+ * Mark `Twig\Test\NodeTestCase::getEnvironment()` as final, override `createEnvironment()` instead.
+ * Deprecate `Twig\Test\NodeTestCase::getVariableGetter()`, call `createVariableGetter()` instead.
+ * Deprecate `Twig\Test\NodeTestCase::getAttributeGetter()`, call `createAttributeGetter()` instead.
+ * Deprecate not overriding `Twig\Test\IntegrationTestCase::getFixturesDirectory()`, this method will be abstract in 4.0
+ * Marked `Twig\Test\IntegrationTestCase::getTests()` and `getLegacyTests()` as final
+
+# 3.12.0 (2024-08-29)
+
+ * Deprecate the fact that the `extends` and `use` tags are always allowed in a sandboxed template.
+   This behavior will change in 4.0 where these tags will need to be explicitly allowed like any other tag.
+ * Deprecate the "tag" constructor argument of the "Twig\Node\Node" class as the tag is now automatically set by the Parser when needed
+ * Fix precedence of two-word tests when the first word is a valid test
+ * Deprecate the `spaceless` filter
+ * Deprecate some internal methods from `Parser`: `getBlockStack()`, `hasBlock()`, `getBlock()`, `hasMacro()`, `hasTraits()`, `getParent()`
+ * Deprecate passing `null` to `Twig\Parser::setParent()`
+ * Update `Node::__toString()` to include the node tag if set
+ * Add support for integers in methods of `Twig\Node\Node` that take a Node name
+ * Deprecate not passing a `BodyNode` instance as the body of a `ModuleNode` or `MacroNode` constructor
+ * Deprecate returning "null" from "TokenParserInterface::parse()".
+ * Deprecate `OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES`
+ * Fix performance regression when `use_yield` is `false` (which is the default)
+ * Improve compatibility when `use_yield` is `false` (as extensions still using `echo` will work as is)
+ * Accept colons (`:`) in addition to equals (`=`) to separate argument names and values in named arguments
+ * Add the `html_cva` function (in the HTML extra package)
+ * Add support for named arguments to the `block` and `attribute` functions
+ * Throw a SyntaxError exception at compile time when a Twig callable has not the minimum number of required arguments
+ * Add a `CallableArgumentsExtractor` class
+ * Deprecate passing a name to `FunctionExpression`, `FilterExpression`, and `TestExpression`;
+   pass a `TwigFunction`, `TwigFilter`, or `TestFilter` instead
+ * Deprecate all Twig callable attributes on `FunctionExpression`, `FilterExpression`, and `TestExpression`
+ * Deprecate the `filter` node of `FilterExpression`
+ * Add the notion of Twig callables (functions, filters, and tests)
+ * Bump minimum PHP version to 8.0
+ * Fix integration tests when a test has more than one data/expect section and deprecations
+ * Add the `enum_cases` function
+
+# 3.11.0 (2024-08-08)
+
+ * Deprecate `OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER`
+ * Add `Twig\Cache\ChainCache` and `Twig\Cache\ReadOnlyFilesystemCache`
+ * Add the possibility to deprecate attributes and nodes on `Node`
+ * Add the possibility to add a package and a version to the `deprecated` tag
+ * Add the possibility to add a package for filter/function/test deprecations
+ * Mark `ConstantExpression` as being `@final`
+ * Add the `find` filter
+ * Fix optimizer mode validation in `OptimizerNodeVisitor`
+ * Add the possibility to yield from a generator in `PrintNode`
+ * Add the `shuffle` filter
+ * Add the `singular` and `plural` filters in `StringExtension`
+ * Deprecate the second argument of `Twig\Node\Expression\CallExpression::compileArguments()`
+ * Deprecate `Twig\ExpressionParser\parseHashExpression()` in favor of
+   `Twig\ExpressionParser::parseMappingExpression()`
+ * Deprecate `Twig\ExpressionParser\parseArrayExpression()` in favor of
+   `Twig\ExpressionParser::parseSequenceExpression()`
+ * Add `sequence` and `mapping` tests
+ * Deprecate `Twig\Node\Expression\NameExpression::isSimple()` and
+    `Twig\Node\Expression\NameExpression::isSpecial()`
+
+# 3.10.3 (2024-05-16)
+
+ * Fix missing ; in generated code
+
+# 3.10.2 (2024-05-14)
+
+ * Fix support for the deprecated escaper signature
+
+# 3.10.1 (2024-05-12)
+
+ * Fix BC break on escaper extension
+ * Fix constant return type
+
+# 3.10.0 (2024-05-11)
+
+ * Make `CoreExtension::formatDate`, `CoreExtension::convertDate`, and
+   `CoreExtension::formatNumber` part of the public API
+ * Add `needs_charset` option for filters and functions
+ * Extract the escaping logic from the `EscaperExtension` class to a new
+   `EscaperRuntime` class.
+
+   The following methods from ``Twig\\Extension\\EscaperExtension`` are
+   deprecated: ``setEscaper()``, ``getEscapers()``, ``setSafeClasses``,
+   ``addSafeClasses()``. Use the same methods on the
+   ``Twig\\Runtime\\EscaperRuntime`` class instead.
+  * Fix capturing output from extensions that still use echo
+  * Fix a PHP warning in the Lexer on malformed templates
+  * Fix blocks not available under some circumstances
+  * Synchronize source context in templates when setting a Node on a Node
+
+# 3.9.3 (2024-04-18)
+
+ * Add missing `twig_escape_filter_is_safe` deprecated function
+ * Fix yield usage with CaptureNode
+ * Add missing unwrap call when using a TemplateWrapper instance internally
+ * Ensure Lexer is initialized early on
+
+# 3.9.2 (2024-04-17)
+
+ * Fix usage of display_end hook
+
+# 3.9.1 (2024-04-17)
+
+ * Fix missing `$blocks` variable in `CaptureNode`
+
+# 3.9.0 (2024-04-16)
+
+ * Add support for PHP 8.4
+ * Deprecate AbstractNodeVisitor
+ * Deprecate passing Template to Environment::resolveTemplate(), Environment::load(), and Template::loadTemplate()
+ * Add a new "yield" mode for output generation;
+   Node implementations that use "echo" or "print" should use "yield" instead;
+   all Node implementations should be flagged with `#[YieldReady]` once they've been made ready for "yield";
+   the "use_yield" Environment option can be turned on when all nodes have been made `#[YieldReady]`;
+   "yield" will be the only strategy supported in the next major version
+ * Add return type for Symfony 7 compatibility
+ * Fix premature loop exit in Security Policy lookup of allowed methods/properties
+ * Deprecate all internal extension functions in favor of methods on the extension classes
+ * Mark all extension functions as @internal
+ * Add SourcePolicyInterface to selectively enable the Sandbox based on a template's Source
+ * Throw a proper Twig exception when using cycle on an empty array
+
+# 3.8.0 (2023-11-21)
+
+ * Catch errors thrown during template rendering
+ * Fix IntlExtension::formatDateTime use of date formatter prototype
+ * Fix premature loop exit in Security Policy lookup of allowed methods/properties
+ * Remove NumberFormatter::TYPE_CURRENCY (deprecated in PHP 8.3)
+ * Restore return type annotations
+ * Allow Symfony 7 packages to be installed
+ * Deprecate `twig_test_iterable` function. Use the native `is_iterable` instead.
+
+# 3.7.1 (2023-08-28)
+
+ * Fix some phpdocs
+
+# 3.7.0 (2023-07-26)
+
+ * Add support for the ...spread operator on arrays and hashes
+
+# 3.6.1 (2023-06-08)
+
+ * Suppress some native return type deprecation messages
+
+# 3.6.0 (2023-05-03)
+
+ * Allow psr/container 2.0
+ * Add the new PHP 8.0 IntlDateFormatter::RELATIVE_* constants for date formatting
+ * Make the Lexer initialize itself lazily
+
+# 3.5.1 (2023-02-08)
+
+ * Arrow functions passed to the "reduce" filter now accept the current key as a third argument
+ * Restores the leniency of the matches twig comparison
+ * Fix error messages in sandboxed mode for "has some" and "has every"
+
+# 3.5.0 (2022-12-27)
+
+ * Make Twig\ExpressionParser non-internal
+ * Add "has some" and "has every" operators
+ * Add Compile::reset()
+ * Throw a better runtime error when the "matches" regexp is not valid
+ * Add "twig *_names" intl functions
+ * Fix optimizing closures callbacks
+ * Add a better exception when getting an undefined constant via `constant`
+ * Fix `if` nodes when outside of a block and with an empty body
+
 # 3.4.3 (2022-09-28)
 
  * Fix a security issue on filesystem loader (possibility to load a template outside a configured directory)
@@ -141,7 +316,7 @@
  * removed Parser::isReservedMacroName()
  * removed SanboxedPrintNode
  * removed Node::setTemplateName()
- * made classes maked as "@final" final
+ * made classes marked as "@final" final
  * removed InitRuntimeInterface, ExistsLoaderInterface, and SourceContextLoaderInterface
  * removed the "spaceless" tag
  * removed Twig\Environment::getBaseTemplateClass() and Twig\Environment::setBaseTemplateClass()

+ 1 - 1
data/web/inc/lib/vendor/twig/twig/LICENSE

@@ -1,4 +1,4 @@
-Copyright (c) 2009-2022 by the Twig Team.
+Copyright (c) 2009-present by the Twig Team.
 
 All rights reserved.
 

+ 1 - 1
data/web/inc/lib/vendor/twig/twig/README.rst

@@ -11,7 +11,7 @@ Sponsors
 
 .. raw:: html
 
-    <a href="https://blackfire.io/docs/introduction?utm_source=twig&utm_medium=github_readme&utm_campaign=logo">
+    <a href="https://docs.blackfire.io/introduction?utm_source=twig&utm_medium=github_readme&utm_campaign=logo">
         <img src="https://static.blackfire.io/assets/intemporals/logo/png/blackfire-io_secondary_horizontal_transparent.png?1" width="255px" alt="Blackfire.io">
     </a>
 

+ 12 - 9
data/web/inc/lib/vendor/twig/twig/composer.json

@@ -24,15 +24,23 @@
         }
     ],
     "require": {
-        "php": ">=7.2.5",
+        "php": ">=8.0.2",
+        "symfony/deprecation-contracts": "^2.5|^3",
         "symfony/polyfill-mbstring": "^1.3",
-        "symfony/polyfill-ctype": "^1.8"
+        "symfony/polyfill-ctype": "^1.8",
+        "symfony/polyfill-php81": "^1.29"
     },
     "require-dev": {
-        "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0",
-        "psr/container": "^1.0"
+        "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0",
+        "psr/container": "^1.0|^2.0"
     },
     "autoload": {
+        "files": [
+            "src/Resources/core.php",
+            "src/Resources/debug.php",
+            "src/Resources/escaper.php",
+            "src/Resources/string_loader.php"
+        ],
         "psr-4" : {
             "Twig\\" : "src/"
         }
@@ -41,10 +49,5 @@
         "psr-4" : {
             "Twig\\Tests\\" : "tests/"
         }
-    },
-    "extra": {
-        "branch-alias": {
-            "dev-master": "3.4-dev"
-        }
     }
 }

+ 136 - 0
data/web/inc/lib/vendor/twig/twig/src/AbstractTwigCallable.php

@@ -0,0 +1,136 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Twig;
+
+/**
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+abstract class AbstractTwigCallable implements TwigCallableInterface
+{
+    protected $options;
+
+    private $name;
+    private $dynamicName;
+    private $callable;
+    private $arguments;
+
+    public function __construct(string $name, $callable = null, array $options = [])
+    {
+        $this->name = $this->dynamicName = $name;
+        $this->callable = $callable;
+        $this->arguments = [];
+        $this->options = array_merge([
+            'needs_environment' => false,
+            'needs_context' => false,
+            'needs_charset' => false,
+            'is_variadic' => false,
+            'deprecated' => false,
+            'deprecating_package' => '',
+            'alternative' => null,
+        ], $options);
+    }
+
+    public function __toString(): string
+    {
+        return \sprintf('%s(%s)', static::class, $this->name);
+    }
+
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    public function getDynamicName(): string
+    {
+        return $this->dynamicName;
+    }
+
+    public function getCallable()
+    {
+        return $this->callable;
+    }
+
+    public function getNodeClass(): string
+    {
+        return $this->options['node_class'];
+    }
+
+    public function needsCharset(): bool
+    {
+        return $this->options['needs_charset'];
+    }
+
+    public function needsEnvironment(): bool
+    {
+        return $this->options['needs_environment'];
+    }
+
+    public function needsContext(): bool
+    {
+        return $this->options['needs_context'];
+    }
+
+    public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self
+    {
+        $new = clone $this;
+        $new->name = $name;
+        $new->dynamicName = $dynamicName;
+        $new->arguments = $arguments;
+
+        return $new;
+    }
+
+    /**
+     * @deprecated since Twig 3.12, use withDynamicArguments() instead
+     */
+    public function setArguments(array $arguments): void
+    {
+        trigger_deprecation('twig/twig', '3.12', 'The "%s::setArguments()" method is deprecated, use "%s::withDynamicArguments()" instead.', static::class, static::class);
+
+        $this->arguments = $arguments;
+    }
+
+    public function getArguments(): array
+    {
+        return $this->arguments;
+    }
+
+    public function isVariadic(): bool
+    {
+        return $this->options['is_variadic'];
+    }
+
+    public function isDeprecated(): bool
+    {
+        return (bool) $this->options['deprecated'];
+    }
+
+    public function getDeprecatingPackage(): string
+    {
+        return $this->options['deprecating_package'];
+    }
+
+    public function getDeprecatedVersion(): string
+    {
+        return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated'];
+    }
+
+    public function getAlternative(): ?string
+    {
+        return $this->options['alternative'];
+    }
+
+    public function getMinimalNumberOfRequiredArguments(): int
+    {
+        return ($this->options['needs_charset'] ? 1 : 0) + ($this->options['needs_environment'] ? 1 : 0) + ($this->options['needs_context'] ? 1 : 0) + \count($this->arguments);
+    }
+}

+ 20 - 0
data/web/inc/lib/vendor/twig/twig/src/Attribute/FirstClassTwigCallableReady.php

@@ -0,0 +1,20 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Twig\Attribute;
+
+/**
+ * Marks nodes that are ready to accept a TwigCallable instead of its name.
+ */
+#[\Attribute(\Attribute::TARGET_METHOD)]
+final class FirstClassTwigCallableReady
+{
+}

+ 20 - 0
data/web/inc/lib/vendor/twig/twig/src/Attribute/YieldReady.php

@@ -0,0 +1,20 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Twig\Attribute;
+
+/**
+ * Marks nodes that are ready for using "yield" instead of "echo" or "print()" for rendering.
+ */
+#[\Attribute(\Attribute::TARGET_CLASS)]
+final class YieldReady
+{
+}

+ 79 - 0
data/web/inc/lib/vendor/twig/twig/src/Cache/ChainCache.php

@@ -0,0 +1,79 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Twig\Cache;
+
+/**
+ * Chains several caches together.
+ *
+ * Cached items are fetched from the first cache having them in its data store.
+ * They are saved and deleted in all adapters at once.
+ *
+ * @author Quentin Devos <quentin@devos.pm>
+ */
+final class ChainCache implements CacheInterface
+{
+    /**
+     * @param iterable<CacheInterface> $caches The ordered list of caches used to store and fetch cached items
+     */
+    public function __construct(
+        private iterable $caches,
+    ) {
+    }
+
+    public function generateKey(string $name, string $className): string
+    {
+        return $className.'#'.$name;
+    }
+
+    public function write(string $key, string $content): void
+    {
+        $splitKey = $this->splitKey($key);
+
+        foreach ($this->caches as $cache) {
+            $cache->write($cache->generateKey(...$splitKey), $content);
+        }
+    }
+
+    public function load(string $key): void
+    {
+        [$name, $className] = $this->splitKey($key);
+
+        foreach ($this->caches as $cache) {
+            $cache->load($cache->generateKey($name, $className));
+
+            if (class_exists($className, false)) {
+                break;
+            }
+        }
+    }
+
+    public function getTimestamp(string $key): int
+    {
+        $splitKey = $this->splitKey($key);
+
+        foreach ($this->caches as $cache) {
+            if (0 < $timestamp = $cache->getTimestamp($cache->generateKey(...$splitKey))) {
+                return $timestamp;
+            }
+        }
+
+        return 0;
+    }
+
+    /**
+     * @return string[]
+     */
+    private function splitKey(string $key): array
+    {
+        return array_reverse(explode('#', $key, 2));
+    }
+}

+ 4 - 4
data/web/inc/lib/vendor/twig/twig/src/Cache/FilesystemCache.php

@@ -50,11 +50,11 @@ class FilesystemCache implements CacheInterface
             if (false === @mkdir($dir, 0777, true)) {
                 clearstatcache(true, $dir);
                 if (!is_dir($dir)) {
-                    throw new \RuntimeException(sprintf('Unable to create the cache directory (%s).', $dir));
+                    throw new \RuntimeException(\sprintf('Unable to create the cache directory (%s).', $dir));
                 }
             }
         } elseif (!is_writable($dir)) {
-            throw new \RuntimeException(sprintf('Unable to write in the cache directory (%s).', $dir));
+            throw new \RuntimeException(\sprintf('Unable to write in the cache directory (%s).', $dir));
         }
 
         $tmpFile = tempnam($dir, basename($key));
@@ -63,7 +63,7 @@ class FilesystemCache implements CacheInterface
 
             if (self::FORCE_BYTECODE_INVALIDATION == ($this->options & self::FORCE_BYTECODE_INVALIDATION)) {
                 // Compile cached file into bytecode cache
-                if (\function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
+                if (\function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
                     @opcache_invalidate($key, true);
                 } elseif (\function_exists('apc_compile_file')) {
                     apc_compile_file($key);
@@ -73,7 +73,7 @@ class FilesystemCache implements CacheInterface
             return;
         }
 
-        throw new \RuntimeException(sprintf('Failed to write cache file "%s".', $key));
+        throw new \RuntimeException(\sprintf('Failed to write cache file "%s".', $key));
     }
 
     public function getTimestamp(string $key): int

+ 25 - 0
data/web/inc/lib/vendor/twig/twig/src/Cache/ReadOnlyFilesystemCache.php

@@ -0,0 +1,25 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Twig\Cache;
+
+/**
+ * Implements a cache on the filesystem that can only be read, not written to.
+ *
+ * @author Quentin Devos <quentin@devos.pm>
+ */
+class ReadOnlyFilesystemCache extends FilesystemCache
+{
+    public function write(string $key, string $content): void
+    {
+        // Do nothing with the content, it's a read-only filesystem.
+    }
+}

+ 56 - 13
data/web/inc/lib/vendor/twig/twig/src/Compiler.php

@@ -22,15 +22,16 @@ class Compiler
     private $lastLine;
     private $source;
     private $indentation;
-    private $env;
     private $debugInfo = [];
     private $sourceOffset;
     private $sourceLine;
     private $varNameSalt = 0;
+    private $didUseEcho = false;
+    private $didUseEchoStack = [];
 
-    public function __construct(Environment $env)
-    {
-        $this->env = $env;
+    public function __construct(
+        private Environment $env,
+    ) {
     }
 
     public function getEnvironment(): Environment
@@ -46,7 +47,7 @@ class Compiler
     /**
      * @return $this
      */
-    public function compile(Node $node, int $indentation = 0)
+    public function reset(int $indentation = 0)
     {
         $this->lastLine = null;
         $this->source = '';
@@ -57,23 +58,54 @@ class Compiler
         $this->indentation = $indentation;
         $this->varNameSalt = 0;
 
-        $node->compile($this);
-
         return $this;
     }
 
+    /**
+     * @return $this
+     */
+    public function compile(Node $node, int $indentation = 0)
+    {
+        $this->reset($indentation);
+        $this->didUseEchoStack[] = $this->didUseEcho;
+
+        try {
+            $this->didUseEcho = false;
+            $node->compile($this);
+
+            if ($this->didUseEcho) {
+                trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[YieldReady].', $this->didUseEcho, \get_class($node));
+            }
+
+            return $this;
+        } finally {
+            $this->didUseEcho = array_pop($this->didUseEchoStack);
+        }
+    }
+
     /**
      * @return $this
      */
     public function subcompile(Node $node, bool $raw = true)
     {
-        if (false === $raw) {
+        if (!$raw) {
             $this->source .= str_repeat(' ', $this->indentation * 4);
         }
 
-        $node->compile($this);
+        $this->didUseEchoStack[] = $this->didUseEcho;
 
-        return $this;
+        try {
+            $this->didUseEcho = false;
+            $node->compile($this);
+
+            if ($this->didUseEcho) {
+                trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[YieldReady].', $this->didUseEcho, \get_class($node));
+            }
+
+            return $this;
+        } finally {
+            $this->didUseEcho = array_pop($this->didUseEchoStack);
+        }
     }
 
     /**
@@ -83,6 +115,7 @@ class Compiler
      */
     public function raw(string $string)
     {
+        $this->checkForEcho($string);
         $this->source .= $string;
 
         return $this;
@@ -96,6 +129,7 @@ class Compiler
     public function write(...$strings)
     {
         foreach ($strings as $string) {
+            $this->checkForEcho($string);
             $this->source .= str_repeat(' ', $this->indentation * 4).$string;
         }
 
@@ -109,7 +143,7 @@ class Compiler
      */
     public function string(string $value)
     {
-        $this->source .= sprintf('"%s"', addcslashes($value, "\0\t\"\$\\"));
+        $this->source .= \sprintf('"%s"', addcslashes($value, "\0\t\"\$\\"));
 
         return $this;
     }
@@ -161,7 +195,7 @@ class Compiler
     public function addDebugInfo(Node $node)
     {
         if ($node->getTemplateLine() != $this->lastLine) {
-            $this->write(sprintf("// line %d\n", $node->getTemplateLine()));
+            $this->write(\sprintf("// line %d\n", $node->getTemplateLine()));
 
             $this->sourceLine += substr_count($this->source, "\n", $this->sourceOffset);
             $this->sourceOffset = \strlen($this->source);
@@ -209,6 +243,15 @@ class Compiler
 
     public function getVarName(): string
     {
-        return sprintf('__internal_compile_%d', $this->varNameSalt++);
+        return \sprintf('__internal_compile_%d', $this->varNameSalt++);
+    }
+
+    private function checkForEcho(string $string): void
+    {
+        if ($this->didUseEcho) {
+            return;
+        }
+
+        $this->didUseEcho = preg_match('/^\s*+(echo|print)\b/', $string, $m) ? $m[1] : false;
     }
 }

+ 86 - 37
data/web/inc/lib/vendor/twig/twig/src/Environment.php

@@ -22,12 +22,17 @@ use Twig\Extension\CoreExtension;
 use Twig\Extension\EscaperExtension;
 use Twig\Extension\ExtensionInterface;
 use Twig\Extension\OptimizerExtension;
+use Twig\Extension\YieldNotReadyExtension;
 use Twig\Loader\ArrayLoader;
 use Twig\Loader\ChainLoader;
 use Twig\Loader\LoaderInterface;
+use Twig\Node\Expression\Binary\AbstractBinary;
+use Twig\Node\Expression\Unary\AbstractUnary;
 use Twig\Node\ModuleNode;
 use Twig\Node\Node;
 use Twig\NodeVisitor\NodeVisitorInterface;
+use Twig\Runtime\EscaperRuntime;
+use Twig\RuntimeLoader\FactoryRuntimeLoader;
 use Twig\RuntimeLoader\RuntimeLoaderInterface;
 use Twig\TokenParser\TokenParserInterface;
 
@@ -38,11 +43,11 @@ use Twig\TokenParser\TokenParserInterface;
  */
 class Environment
 {
-    public const VERSION = '3.4.3';
-    public const VERSION_ID = 30403;
+    public const VERSION = '3.14.0';
+    public const VERSION_ID = 31400;
     public const MAJOR_VERSION = 3;
-    public const MINOR_VERSION = 4;
-    public const RELEASE_VERSION = 3;
+    public const MINOR_VERSION = 14;
+    public const RELEASE_VERSION = 0;
     public const EXTRA_VERSION = '';
 
     private $charset;
@@ -53,16 +58,19 @@ class Environment
     private $lexer;
     private $parser;
     private $compiler;
+    /** @var array<string, mixed> */
     private $globals = [];
     private $resolvedGlobals;
     private $loadedTemplates;
     private $strictVariables;
-    private $templateClassPrefix = '__TwigTemplate_';
     private $originalCache;
     private $extensionSet;
     private $runtimeLoaders = [];
     private $runtimes = [];
     private $optionsHash;
+    /** @var bool */
+    private $useYield;
+    private $defaultRuntimeLoader;
 
     /**
      * Constructor.
@@ -94,8 +102,12 @@ class Environment
      *  * optimizations: A flag that indicates which optimizations to apply
      *                   (default to -1 which means that all optimizations are enabled;
      *                   set it to 0 to disable).
+     *
+     *  * use_yield: true: forces templates to exclusively use "yield" instead of "echo" (all extensions must be yield ready)
+     *               false (default): allows templates to use a mix of "yield" and "echo" calls to allow for a progressive migration
+     *               Switch to "true" when possible as this will be the only supported mode in Twig 4.0
      */
-    public function __construct(LoaderInterface $loader, $options = [])
+    public function __construct(LoaderInterface $loader, array $options = [])
     {
         $this->setLoader($loader);
 
@@ -107,20 +119,38 @@ class Environment
             'cache' => false,
             'auto_reload' => null,
             'optimizations' => -1,
+            'use_yield' => false,
         ], $options);
 
+        $this->useYield = (bool) $options['use_yield'];
         $this->debug = (bool) $options['debug'];
         $this->setCharset($options['charset'] ?? 'UTF-8');
         $this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload'];
         $this->strictVariables = (bool) $options['strict_variables'];
         $this->setCache($options['cache']);
         $this->extensionSet = new ExtensionSet();
+        $this->defaultRuntimeLoader = new FactoryRuntimeLoader([
+            EscaperRuntime::class => function () { return new EscaperRuntime($this->charset); },
+        ]);
 
         $this->addExtension(new CoreExtension());
-        $this->addExtension(new EscaperExtension($options['autoescape']));
+        $escaperExt = new EscaperExtension($options['autoescape']);
+        $escaperExt->setEnvironment($this, false);
+        $this->addExtension($escaperExt);
+        if (\PHP_VERSION_ID >= 80000) {
+            $this->addExtension(new YieldNotReadyExtension($this->useYield));
+        }
         $this->addExtension(new OptimizerExtension($options['optimizations']));
     }
 
+    /**
+     * @internal
+     */
+    public function useYield(): bool
+    {
+        return $this->useYield;
+    }
+
     /**
      * Enables debugging mode.
      */
@@ -246,7 +276,6 @@ class Environment
      *
      *  * The cache key for the given template;
      *  * The currently enabled extensions;
-     *  * Whether the Twig C extension is available or not;
      *  * PHP version;
      *  * Twig version;
      *  * Options with what environment was created.
@@ -256,11 +285,11 @@ class Environment
      *
      * @internal
      */
-    public function getTemplateClass(string $name, int $index = null): string
+    public function getTemplateClass(string $name, ?int $index = null): string
     {
         $key = $this->getLoader()->getCacheKey($name).$this->optionsHash;
 
-        return $this->templateClassPrefix.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index);
+        return '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index);
     }
 
     /**
@@ -305,6 +334,11 @@ class Environment
         if ($name instanceof TemplateWrapper) {
             return $name;
         }
+        if ($name instanceof Template) {
+            trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', self::class, __METHOD__);
+
+            return $name;
+        }
 
         return new TemplateWrapper($this, $this->loadTemplate($this->getTemplateClass($name), $name));
     }
@@ -315,8 +349,8 @@ class Environment
      * This method is for internal use only and should never be called
      * directly.
      *
-     * @param string $name  The template name
-     * @param int    $index The index if it is an embedded template
+     * @param string   $name  The template name
+     * @param int|null $index The index if it is an embedded template
      *
      * @throws LoaderError  When the template cannot be found
      * @throws RuntimeError When a previously generated cache is corrupted
@@ -324,7 +358,7 @@ class Environment
      *
      * @internal
      */
-    public function loadTemplate(string $cls, string $name, int $index = null): Template
+    public function loadTemplate(string $cls, string $name, ?int $index = null): Template
     {
         $mainCls = $cls;
         if (null !== $index) {
@@ -342,7 +376,6 @@ class Environment
                 $this->cache->load($key);
             }
 
-            $source = null;
             if (!class_exists($cls, false)) {
                 $source = $this->getLoader()->getSourceContext($name);
                 $content = $this->compileSource($source);
@@ -359,7 +392,7 @@ class Environment
                 }
 
                 if (!class_exists($cls, false)) {
-                    throw new RuntimeError(sprintf('Failed to load Twig template "%s", index "%s": cache might be corrupted.', $name, $index), -1, $source);
+                    throw new RuntimeError(\sprintf('Failed to load Twig template "%s", index "%s": cache might be corrupted.', $name, $index), -1, $source);
                 }
             }
         }
@@ -374,19 +407,19 @@ class Environment
      *
      * This method should not be used as a generic way to load templates.
      *
-     * @param string $template The template source
-     * @param string $name     An optional name of the template to be used in error messages
+     * @param string      $template The template source
+     * @param string|null $name     An optional name of the template to be used in error messages
      *
      * @throws LoaderError When the template cannot be found
      * @throws SyntaxError When an error occurred during compilation
      */
-    public function createTemplate(string $template, string $name = null): TemplateWrapper
+    public function createTemplate(string $template, ?string $name = null): TemplateWrapper
     {
         $hash = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $template, false);
         if (null !== $name) {
-            $name = sprintf('%s (string template %s)', $name, $hash);
+            $name = \sprintf('%s (string template %s)', $name, $hash);
         } else {
-            $name = sprintf('__string_template__%s', $hash);
+            $name = \sprintf('__string_template__%s', $hash);
         }
 
         $loader = new ChainLoader([
@@ -419,10 +452,10 @@ class Environment
     /**
      * Tries to load a template consecutively from an array.
      *
-     * Similar to load() but it also accepts instances of \Twig\Template and
-     * \Twig\TemplateWrapper, and an array of templates where each is tried to be loaded.
+     * Similar to load() but it also accepts instances of \Twig\TemplateWrapper
+     * and an array of templates where each is tried to be loaded.
      *
-     * @param string|TemplateWrapper|array $names A template or an array of templates to try consecutively
+     * @param string|TemplateWrapper|array<string|TemplateWrapper> $names A template or an array of templates to try consecutively
      *
      * @throws LoaderError When none of the templates can be found
      * @throws SyntaxError When an error occurred during compilation
@@ -436,7 +469,9 @@ class Environment
         $count = \count($names);
         foreach ($names as $name) {
             if ($name instanceof Template) {
-                return $name;
+                trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', Template::class, __METHOD__);
+
+                return new TemplateWrapper($this, $name);
             }
             if ($name instanceof TemplateWrapper) {
                 return $name;
@@ -449,7 +484,7 @@ class Environment
             return $this->load($name);
         }
 
-        throw new LoaderError(sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names)));
+        throw new LoaderError(\sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names)));
     }
 
     public function setLexer(Lexer $lexer)
@@ -518,7 +553,7 @@ class Environment
             $e->setSourceContext($source);
             throw $e;
         } catch (\Exception $e) {
-            throw new SyntaxError(sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e);
+            throw new SyntaxError(\sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e);
         }
     }
 
@@ -534,7 +569,7 @@ class Environment
 
     public function setCharset(string $charset)
     {
-        if ('UTF8' === $charset = null === $charset ? null : strtoupper($charset)) {
+        if ('UTF8' === $charset = strtoupper($charset ?: '')) {
             // iconv on Windows requires "UTF-8" instead of "UTF8"
             $charset = 'UTF-8';
         }
@@ -592,7 +627,11 @@ class Environment
             }
         }
 
-        throw new RuntimeError(sprintf('Unable to load the "%s" runtime.', $class));
+        if (null !== $runtime = $this->defaultRuntimeLoader->load($class)) {
+            return $this->runtimes[$class] = $runtime;
+        }
+
+        throw new RuntimeError(\sprintf('Unable to load the "%s" runtime.', $class));
     }
 
     public function addExtension(ExtensionInterface $extension)
@@ -763,7 +802,7 @@ class Environment
     public function addGlobal(string $name, $value)
     {
         if ($this->extensionSet->isInitialized() && !\array_key_exists($name, $this->getGlobals())) {
-            throw new \LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
+            throw new \LogicException(\sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
         }
 
         if (null !== $this->resolvedGlobals) {
@@ -775,6 +814,8 @@ class Environment
 
     /**
      * @internal
+     *
+     * @return array<string, mixed>
      */
     public function getGlobals(): array
     {
@@ -789,21 +830,26 @@ class Environment
         return array_merge($this->extensionSet->getGlobals(), $this->globals);
     }
 
+    public function resetGlobals(): void
+    {
+        $this->resolvedGlobals = null;
+        $this->extensionSet->resetGlobals();
+    }
+
+    /**
+     * @deprecated since Twig 3.14
+     */
     public function mergeGlobals(array $context): array
     {
-        // we don't use array_merge as the context being generally
-        // bigger than globals, this code is faster.
-        foreach ($this->getGlobals() as $key => $value) {
-            if (!\array_key_exists($key, $context)) {
-                $context[$key] = $value;
-            }
-        }
+        trigger_deprecation('twig/twig', '3.14', 'The "%s" method is deprecated.', __METHOD__);
 
-        return $context;
+        return $context + $this->getGlobals();
     }
 
     /**
      * @internal
+     *
+     * @return array<string, array{precedence: int, class: class-string<AbstractUnary>}>
      */
     public function getUnaryOperators(): array
     {
@@ -812,6 +858,8 @@ class Environment
 
     /**
      * @internal
+     *
+     * @return array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: ExpressionParser::OPERATOR_*}>
      */
     public function getBinaryOperators(): array
     {
@@ -827,6 +875,7 @@ class Environment
             self::VERSION,
             (int) $this->debug,
             (int) $this->strictVariables,
+            $this->useYield ? '1' : '0',
         ]);
     }
 }

+ 9 - 9
data/web/inc/lib/vendor/twig/twig/src/Error/Error.php

@@ -53,7 +53,7 @@ class Error extends \Exception
      * @param int         $lineno  The template line where the error occurred
      * @param Source|null $source  The source context where the error occurred
      */
-    public function __construct(string $message, int $lineno = -1, Source $source = null, \Exception $previous = null)
+    public function __construct(string $message, int $lineno = -1, ?Source $source = null, ?\Throwable $previous = null)
     {
         parent::__construct('', 0, $previous);
 
@@ -93,7 +93,7 @@ class Error extends \Exception
         return $this->name ? new Source($this->sourceCode, $this->name, $this->sourcePath) : null;
     }
 
-    public function setSourceContext(Source $source = null): void
+    public function setSourceContext(?Source $source = null): void
     {
         if (null === $source) {
             $this->sourceCode = $this->name = $this->sourcePath = null;
@@ -130,28 +130,28 @@ class Error extends \Exception
         }
 
         $dot = false;
-        if ('.' === substr($this->message, -1)) {
+        if (str_ends_with($this->message, '.')) {
             $this->message = substr($this->message, 0, -1);
             $dot = true;
         }
 
         $questionMark = false;
-        if ('?' === substr($this->message, -1)) {
+        if (str_ends_with($this->message, '?')) {
             $this->message = substr($this->message, 0, -1);
             $questionMark = true;
         }
 
         if ($this->name) {
-            if (\is_string($this->name) || (\is_object($this->name) && method_exists($this->name, '__toString'))) {
-                $name = sprintf('"%s"', $this->name);
+            if (\is_string($this->name) || $this->name instanceof \Stringable) {
+                $name = \sprintf('"%s"', $this->name);
             } else {
                 $name = json_encode($this->name);
             }
-            $this->message .= sprintf(' in %s', $name);
+            $this->message .= \sprintf(' in %s', $name);
         }
 
         if ($this->lineno && $this->lineno >= 0) {
-            $this->message .= sprintf(' at line %d', $this->lineno);
+            $this->message .= \sprintf(' at line %d', $this->lineno);
         }
 
         if ($dot) {
@@ -172,7 +172,7 @@ class Error extends \Exception
         foreach ($backtrace as $trace) {
             if (isset($trace['object']) && $trace['object'] instanceof Template) {
                 $currentClass = \get_class($trace['object']);
-                $isEmbedContainer = null === $templateClass ? false : 0 === strpos($templateClass, $currentClass);
+                $isEmbedContainer = null === $templateClass ? false : str_starts_with($templateClass, $currentClass);
                 if (null === $this->name || ($this->name == $trace['object']->getTemplateName() && !$isEmbedContainer)) {
                     $template = $trace['object'];
                     $templateClass = \get_class($trace['object']);

+ 2 - 2
data/web/inc/lib/vendor/twig/twig/src/Error/SyntaxError.php

@@ -30,7 +30,7 @@ class SyntaxError extends Error
         $alternatives = [];
         foreach ($items as $item) {
             $lev = levenshtein($name, $item);
-            if ($lev <= \strlen($name) / 3 || false !== strpos($item, $name)) {
+            if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) {
                 $alternatives[$item] = $lev;
             }
         }
@@ -41,6 +41,6 @@ class SyntaxError extends Error
 
         asort($alternatives);
 
-        $this->appendMessage(sprintf(' Did you mean "%s"?', implode('", "', array_keys($alternatives))));
+        $this->appendMessage(\sprintf(' Did you mean "%s"?', implode('", "', array_keys($alternatives))));
     }
 }

+ 214 - 186
data/web/inc/lib/vendor/twig/twig/src/ExpressionParser.php

@@ -12,20 +12,21 @@
 
 namespace Twig;
 
+use Twig\Attribute\FirstClassTwigCallableReady;
 use Twig\Error\SyntaxError;
 use Twig\Node\Expression\AbstractExpression;
 use Twig\Node\Expression\ArrayExpression;
 use Twig\Node\Expression\ArrowFunctionExpression;
 use Twig\Node\Expression\AssignNameExpression;
+use Twig\Node\Expression\Binary\AbstractBinary;
 use Twig\Node\Expression\Binary\ConcatBinary;
-use Twig\Node\Expression\BlockReferenceExpression;
 use Twig\Node\Expression\ConditionalExpression;
 use Twig\Node\Expression\ConstantExpression;
 use Twig\Node\Expression\GetAttrExpression;
 use Twig\Node\Expression\MethodCallExpression;
 use Twig\Node\Expression\NameExpression;
-use Twig\Node\Expression\ParentExpression;
 use Twig\Node\Expression\TestExpression;
+use Twig\Node\Expression\Unary\AbstractUnary;
 use Twig\Node\Expression\Unary\NegUnary;
 use Twig\Node\Expression\Unary\NotUnary;
 use Twig\Node\Expression\Unary\PosUnary;
@@ -40,23 +41,22 @@ use Twig\Node\Node;
  * @see https://en.wikipedia.org/wiki/Operator-precedence_parser
  *
  * @author Fabien Potencier <fabien@symfony.com>
- *
- * @internal
  */
 class ExpressionParser
 {
     public const OPERATOR_LEFT = 1;
     public const OPERATOR_RIGHT = 2;
 
-    private $parser;
-    private $env;
+    /** @var array<string, array{precedence: int, class: class-string<AbstractUnary>}> */
     private $unaryOperators;
+    /** @var array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: self::OPERATOR_*}> */
     private $binaryOperators;
+    private $readyNodes = [];
 
-    public function __construct(Parser $parser, Environment $env)
-    {
-        $this->parser = $parser;
-        $this->env = $env;
+    public function __construct(
+        private Parser $parser,
+        private Environment $env,
+    ) {
         $this->unaryOperators = $env->getUnaryOperators();
         $this->binaryOperators = $env->getBinaryOperators();
     }
@@ -80,7 +80,7 @@ class ExpressionParser
             } elseif (isset($op['callable'])) {
                 $expr = $op['callable']($this->parser, $expr);
             } else {
-                $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']);
+                $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence'], true);
                 $class = $op['class'];
                 $expr = new $class($expr, $expr1, $token->getLine());
             }
@@ -103,52 +103,52 @@ class ExpressionParser
         $stream = $this->parser->getStream();
 
         // short array syntax (one argument, no parentheses)?
-        if ($stream->look(1)->test(/* Token::ARROW_TYPE */ 12)) {
+        if ($stream->look(1)->test(Token::ARROW_TYPE)) {
             $line = $stream->getCurrent()->getLine();
-            $token = $stream->expect(/* Token::NAME_TYPE */ 5);
+            $token = $stream->expect(Token::NAME_TYPE);
             $names = [new AssignNameExpression($token->getValue(), $token->getLine())];
-            $stream->expect(/* Token::ARROW_TYPE */ 12);
+            $stream->expect(Token::ARROW_TYPE);
 
             return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
         }
 
         // first, determine if we are parsing an arrow function by finding => (long form)
         $i = 0;
-        if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
+        if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, '(')) {
             return null;
         }
         ++$i;
         while (true) {
             // variable name
             ++$i;
-            if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
+            if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ',')) {
                 break;
             }
             ++$i;
         }
-        if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) {
+        if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ')')) {
             return null;
         }
         ++$i;
-        if (!$stream->look($i)->test(/* Token::ARROW_TYPE */ 12)) {
+        if (!$stream->look($i)->test(Token::ARROW_TYPE)) {
             return null;
         }
 
         // yes, let's parse it properly
-        $token = $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '(');
+        $token = $stream->expect(Token::PUNCTUATION_TYPE, '(');
         $line = $token->getLine();
 
         $names = [];
         while (true) {
-            $token = $stream->expect(/* Token::NAME_TYPE */ 5);
+            $token = $stream->expect(Token::NAME_TYPE);
             $names[] = new AssignNameExpression($token->getValue(), $token->getLine());
 
-            if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
+            if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
                 break;
             }
         }
-        $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ')');
-        $stream->expect(/* Token::ARROW_TYPE */ 12);
+        $stream->expect(Token::PUNCTUATION_TYPE, ')');
+        $stream->expect(Token::ARROW_TYPE);
 
         return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
     }
@@ -164,10 +164,10 @@ class ExpressionParser
             $class = $operator['class'];
 
             return $this->parsePostfixExpression(new $class($expr, $token->getLine()));
-        } elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
+        } elseif ($token->test(Token::PUNCTUATION_TYPE, '(')) {
             $this->parser->getStream()->next();
             $expr = $this->parseExpression();
-            $this->parser->getStream()->expect(/* Token::PUNCTUATION_TYPE */ 9, ')', 'An opened parenthesis is not properly closed');
+            $this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed');
 
             return $this->parsePostfixExpression($expr);
         }
@@ -177,15 +177,18 @@ class ExpressionParser
 
     private function parseConditionalExpression($expr): AbstractExpression
     {
-        while ($this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, '?')) {
-            if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
+        while ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, '?')) {
+            if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) {
                 $expr2 = $this->parseExpression();
-                if ($this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
+                if ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) {
+                    // Ternary operator (expr ? expr2 : expr3)
                     $expr3 = $this->parseExpression();
                 } else {
+                    // Ternary without else (expr ? expr2)
                     $expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine());
                 }
             } else {
+                // Ternary without then (expr ?: expr3)
                 $expr2 = $expr;
                 $expr3 = $this->parseExpression();
             }
@@ -198,19 +201,19 @@ class ExpressionParser
 
     private function isUnary(Token $token): bool
     {
-        return $token->test(/* Token::OPERATOR_TYPE */ 8) && isset($this->unaryOperators[$token->getValue()]);
+        return $token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]);
     }
 
     private function isBinary(Token $token): bool
     {
-        return $token->test(/* Token::OPERATOR_TYPE */ 8) && isset($this->binaryOperators[$token->getValue()]);
+        return $token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]);
     }
 
     public function parsePrimaryExpression()
     {
         $token = $this->parser->getCurrentToken();
         switch ($token->getType()) {
-            case /* Token::NAME_TYPE */ 5:
+            case Token::NAME_TYPE:
                 $this->parser->getStream()->next();
                 switch ($token->getValue()) {
                     case 'true':
@@ -239,17 +242,17 @@ class ExpressionParser
                 }
                 break;
 
-            case /* Token::NUMBER_TYPE */ 6:
+            case Token::NUMBER_TYPE:
                 $this->parser->getStream()->next();
                 $node = new ConstantExpression($token->getValue(), $token->getLine());
                 break;
 
-            case /* Token::STRING_TYPE */ 7:
-            case /* Token::INTERPOLATION_START_TYPE */ 10:
+            case Token::STRING_TYPE:
+            case Token::INTERPOLATION_START_TYPE:
                 $node = $this->parseStringExpression();
                 break;
 
-            case /* Token::OPERATOR_TYPE */ 8:
+            case Token::OPERATOR_TYPE:
                 if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) {
                     // in this context, string operators are variable names
                     $this->parser->getStream()->next();
@@ -260,7 +263,7 @@ class ExpressionParser
                 if (isset($this->unaryOperators[$token->getValue()])) {
                     $class = $this->unaryOperators[$token->getValue()]['class'];
                     if (!\in_array($class, [NegUnary::class, PosUnary::class])) {
-                        throw new SyntaxError(sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
+                        throw new SyntaxError(\sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
                     }
 
                     $this->parser->getStream()->next();
@@ -272,14 +275,14 @@ class ExpressionParser
 
                 // no break
             default:
-                if ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '[')) {
-                    $node = $this->parseArrayExpression();
-                } elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '{')) {
-                    $node = $this->parseHashExpression();
-                } elseif ($token->test(/* Token::OPERATOR_TYPE */ 8, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) {
-                    throw new SyntaxError(sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
+                if ($token->test(Token::PUNCTUATION_TYPE, '[')) {
+                    $node = $this->parseSequenceExpression();
+                } elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) {
+                    $node = $this->parseMappingExpression();
+                } elseif ($token->test(Token::OPERATOR_TYPE, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) {
+                    throw new SyntaxError(\sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
                 } else {
-                    throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
+                    throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
                 }
         }
 
@@ -294,12 +297,12 @@ class ExpressionParser
         // a string cannot be followed by another string in a single expression
         $nextCanBeString = true;
         while (true) {
-            if ($nextCanBeString && $token = $stream->nextIf(/* Token::STRING_TYPE */ 7)) {
+            if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) {
                 $nodes[] = new ConstantExpression($token->getValue(), $token->getLine());
                 $nextCanBeString = false;
-            } elseif ($stream->nextIf(/* Token::INTERPOLATION_START_TYPE */ 10)) {
+            } elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) {
                 $nodes[] = $this->parseExpression();
-                $stream->expect(/* Token::INTERPOLATION_END_TYPE */ 11);
+                $stream->expect(Token::INTERPOLATION_END_TYPE);
                 $nextCanBeString = true;
             } else {
                 break;
@@ -314,56 +317,91 @@ class ExpressionParser
         return $expr;
     }
 
+    /**
+     * @deprecated since 3.11, use parseSequenceExpression() instead
+     */
     public function parseArrayExpression()
+    {
+        trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseSequenceExpression()" instead.', __METHOD__);
+
+        return $this->parseSequenceExpression();
+    }
+
+    public function parseSequenceExpression()
     {
         $stream = $this->parser->getStream();
-        $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '[', 'An array element was expected');
+        $stream->expect(Token::PUNCTUATION_TYPE, '[', 'A sequence element was expected');
 
         $node = new ArrayExpression([], $stream->getCurrent()->getLine());
         $first = true;
-        while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
+        while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) {
             if (!$first) {
-                $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'An array element must be followed by a comma');
+                $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma');
 
                 // trailing ,?
-                if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
+                if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
                     break;
                 }
             }
             $first = false;
 
-            $node->addElement($this->parseExpression());
+            if ($stream->test(Token::SPREAD_TYPE)) {
+                $stream->next();
+                $expr = $this->parseExpression();
+                $expr->setAttribute('spread', true);
+                $node->addElement($expr);
+            } else {
+                $node->addElement($this->parseExpression());
+            }
         }
-        $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']', 'An opened array is not properly closed');
+        $stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed');
 
         return $node;
     }
 
+    /**
+     * @deprecated since 3.11, use parseMappingExpression() instead
+     */
     public function parseHashExpression()
+    {
+        trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseMappingExpression()" instead.', __METHOD__);
+
+        return $this->parseMappingExpression();
+    }
+
+    public function parseMappingExpression()
     {
         $stream = $this->parser->getStream();
-        $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '{', 'A hash element was expected');
+        $stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected');
 
         $node = new ArrayExpression([], $stream->getCurrent()->getLine());
         $first = true;
-        while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) {
+        while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) {
             if (!$first) {
-                $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'A hash value must be followed by a comma');
+                $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma');
 
                 // trailing ,?
-                if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) {
+                if ($stream->test(Token::PUNCTUATION_TYPE, '}')) {
                     break;
                 }
             }
             $first = false;
 
-            // a hash key can be:
+            if ($stream->test(Token::SPREAD_TYPE)) {
+                $stream->next();
+                $value = $this->parseExpression();
+                $value->setAttribute('spread', true);
+                $node->addElement($value);
+                continue;
+            }
+
+            // a mapping key can be:
             //
             //  * a number -- 12
             //  * a string -- 'a'
             //  * a name, which is equivalent to a string -- a
             //  * an expression, which must be enclosed in parentheses -- (1 + 2)
-            if ($token = $stream->nextIf(/* Token::NAME_TYPE */ 5)) {
+            if ($token = $stream->nextIf(Token::NAME_TYPE)) {
                 $key = new ConstantExpression($token->getValue(), $token->getLine());
 
                 // {a} is a shortcut for {a:a}
@@ -372,22 +410,22 @@ class ExpressionParser
                     $node->addElement($value, $key);
                     continue;
                 }
-            } elseif (($token = $stream->nextIf(/* Token::STRING_TYPE */ 7)) || $token = $stream->nextIf(/* Token::NUMBER_TYPE */ 6)) {
+            } elseif (($token = $stream->nextIf(Token::STRING_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) {
                 $key = new ConstantExpression($token->getValue(), $token->getLine());
-            } elseif ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
+            } elseif ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
                 $key = $this->parseExpression();
             } else {
                 $current = $stream->getCurrent();
 
-                throw new SyntaxError(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $stream->getSourceContext());
+                throw new SyntaxError(\sprintf('A mapping key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $stream->getSourceContext());
             }
 
-            $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ':', 'A hash key must be followed by a colon (:)');
+            $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)');
             $value = $this->parseExpression();
 
             $node->addElement($value, $key);
         }
-        $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '}', 'An opened hash is not properly closed');
+        $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed');
 
         return $node;
     }
@@ -396,7 +434,7 @@ class ExpressionParser
     {
         while (true) {
             $token = $this->parser->getCurrentToken();
-            if (/* Token::PUNCTUATION_TYPE */ 9 == $token->getType()) {
+            if (Token::PUNCTUATION_TYPE == $token->getType()) {
                 if ('.' == $token->getValue() || '[' == $token->getValue()) {
                     $node = $this->parseSubscriptExpression($node);
                 } elseif ('|' == $token->getValue()) {
@@ -414,50 +452,37 @@ class ExpressionParser
 
     public function getFunctionNode($name, $line)
     {
-        switch ($name) {
-            case 'parent':
-                $this->parseArguments();
-                if (!\count($this->parser->getBlockStack())) {
-                    throw new SyntaxError('Calling "parent" outside a block is forbidden.', $line, $this->parser->getStream()->getSourceContext());
-                }
+        if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
+            $arguments = new ArrayExpression([], $line);
+            foreach ($this->parseArguments() as $n) {
+                $arguments->addElement($n);
+            }
 
-                if (!$this->parser->getParent() && !$this->parser->hasTraits()) {
-                    throw new SyntaxError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', $line, $this->parser->getStream()->getSourceContext());
-                }
+            $node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line);
+            $node->setAttribute('safe', true);
 
-                return new ParentExpression($this->parser->peekBlockStack(), $line);
-            case 'block':
-                $args = $this->parseArguments();
-                if (\count($args) < 1) {
-                    throw new SyntaxError('The "block" function takes one argument (the block name).', $line, $this->parser->getStream()->getSourceContext());
-                }
+            return $node;
+        }
 
-                return new BlockReferenceExpression($args->getNode(0), \count($args) > 1 ? $args->getNode(1) : null, $line);
-            case 'attribute':
-                $args = $this->parseArguments();
-                if (\count($args) < 2) {
-                    throw new SyntaxError('The "attribute" function takes at least two arguments (the variable and the attributes).', $line, $this->parser->getStream()->getSourceContext());
-                }
+        $args = $this->parseArguments(true);
+        $function = $this->getFunction($name, $line);
 
-                return new GetAttrExpression($args->getNode(0), $args->getNode(1), \count($args) > 2 ? $args->getNode(2) : null, Template::ANY_CALL, $line);
-            default:
-                if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
-                    $arguments = new ArrayExpression([], $line);
-                    foreach ($this->parseArguments() as $n) {
-                        $arguments->addElement($n);
-                    }
-
-                    $node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line);
-                    $node->setAttribute('safe', true);
+        if ($function->getParserCallable()) {
+            $fakeNode = new Node(lineno: $line);
+            $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext());
 
-                    return $node;
-                }
+            return ($function->getParserCallable())($this->parser, $fakeNode, $args, $line);
+        }
 
-                $args = $this->parseArguments(true);
-                $class = $this->getFunctionNodeClass($name, $line);
+        if (!isset($this->readyNodes[$class = $function->getNodeClass()])) {
+            $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
+        }
 
-                return new $class($name, $args, $line);
+        if (!$ready = $this->readyNodes[$class]) {
+            trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class);
         }
+
+        return new $class($ready ? $function : $function->getName(), $args, $line);
     }
 
     public function parseSubscriptExpression($node)
@@ -470,29 +495,25 @@ class ExpressionParser
         if ('.' == $token->getValue()) {
             $token = $stream->next();
             if (
-                /* Token::NAME_TYPE */ 5 == $token->getType()
+                Token::NAME_TYPE == $token->getType()
                 ||
-                /* Token::NUMBER_TYPE */ 6 == $token->getType()
+                Token::NUMBER_TYPE == $token->getType()
                 ||
-                (/* Token::OPERATOR_TYPE */ 8 == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue()))
+                (Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue()))
             ) {
                 $arg = new ConstantExpression($token->getValue(), $lineno);
 
-                if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
+                if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
                     $type = Template::METHOD_CALL;
                     foreach ($this->parseArguments() as $n) {
                         $arguments->addElement($n);
                     }
                 }
             } else {
-                throw new SyntaxError(sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext());
+                throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext());
             }
 
             if ($node instanceof NameExpression && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) {
-                if (!$arg instanceof ConstantExpression) {
-                    throw new SyntaxError(sprintf('Dynamic macro names are not supported (called on "%s").', $node->getAttribute('name')), $token->getLine(), $stream->getSourceContext());
-                }
-
                 $name = $arg->getAttribute('value');
 
                 $node = new MethodCallExpression($node, 'macro_'.$name, $arguments, $lineno);
@@ -505,34 +526,34 @@ class ExpressionParser
 
             // slice?
             $slice = false;
-            if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
+            if ($stream->test(Token::PUNCTUATION_TYPE, ':')) {
                 $slice = true;
                 $arg = new ConstantExpression(0, $token->getLine());
             } else {
                 $arg = $this->parseExpression();
             }
 
-            if ($stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
+            if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) {
                 $slice = true;
             }
 
             if ($slice) {
-                if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
+                if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
                     $length = new ConstantExpression(null, $token->getLine());
                 } else {
                     $length = $this->parseExpression();
                 }
 
-                $class = $this->getFilterNodeClass('slice', $token->getLine());
+                $filter = $this->getFilter('slice', $token->getLine());
                 $arguments = new Node([$arg, $length]);
-                $filter = new $class($node, new ConstantExpression('slice', $token->getLine()), $arguments, $token->getLine());
+                $filter = new ($filter->getNodeClass())($node, $filter, $arguments, $token->getLine());
 
-                $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']');
+                $stream->expect(Token::PUNCTUATION_TYPE, ']');
 
                 return $filter;
             }
 
-            $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']');
+            $stream->expect(Token::PUNCTUATION_TYPE, ']');
         }
 
         return new GetAttrExpression($node, $arg, $arguments, $type, $lineno);
@@ -545,23 +566,35 @@ class ExpressionParser
         return $this->parseFilterExpressionRaw($node);
     }
 
-    public function parseFilterExpressionRaw($node, $tag = null)
+    public function parseFilterExpressionRaw($node)
     {
+        if (\func_num_args() > 1) {
+            trigger_deprecation('twig/twig', '3.12', 'Passing a second argument to "%s()" is deprecated.', __METHOD__);
+        }
+
         while (true) {
-            $token = $this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5);
+            $token = $this->parser->getStream()->expect(Token::NAME_TYPE);
 
-            $name = new ConstantExpression($token->getValue(), $token->getLine());
-            if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
+            if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) {
                 $arguments = new Node();
             } else {
                 $arguments = $this->parseArguments(true, false, true);
             }
 
-            $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine());
+            $filter = $this->getFilter($token->getValue(), $token->getLine());
 
-            $node = new $class($node, $name, $arguments, $token->getLine(), $tag);
+            $ready = true;
+            if (!isset($this->readyNodes[$class = $filter->getNodeClass()])) {
+                $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
+            }
 
-            if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '|')) {
+            if (!$ready = $this->readyNodes[$class]) {
+                trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class);
+            }
+
+            $node = new $class($node, $ready ? $filter : new ConstantExpression($filter->getName(), $token->getLine()), $arguments, $token->getLine());
+
+            if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '|')) {
                 break;
             }
 
@@ -575,7 +608,7 @@ class ExpressionParser
      * Parses arguments.
      *
      * @param bool $namedArguments Whether to allow named arguments or not
-     * @param bool $definition     Whether we are parsing arguments for a function definition
+     * @param bool $definition     Whether we are parsing arguments for a function (or macro) definition
      *
      * @return Node
      *
@@ -586,28 +619,28 @@ class ExpressionParser
         $args = [];
         $stream = $this->parser->getStream();
 
-        $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '(', 'A list of arguments must begin with an opening parenthesis');
-        while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) {
+        $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
+        while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) {
             if (!empty($args)) {
-                $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'Arguments must be separated by a comma');
+                $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
 
                 // if the comma above was a trailing comma, early exit the argument parse loop
-                if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) {
+                if ($stream->test(Token::PUNCTUATION_TYPE, ')')) {
                     break;
                 }
             }
 
             if ($definition) {
-                $token = $stream->expect(/* Token::NAME_TYPE */ 5, null, 'An argument must be a name');
+                $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name');
                 $value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine());
             } else {
                 $value = $this->parseExpression(0, $allowArrow);
             }
 
             $name = null;
-            if ($namedArguments && $token = $stream->nextIf(/* Token::OPERATOR_TYPE */ 8, '=')) {
+            if ($namedArguments && (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':')))) {
                 if (!$value instanceof NameExpression) {
-                    throw new SyntaxError(sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext());
+                    throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext());
                 }
                 $name = $value->getAttribute('name');
 
@@ -615,7 +648,7 @@ class ExpressionParser
                     $value = $this->parsePrimaryExpression();
 
                     if (!$this->checkConstantExpression($value)) {
-                        throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, or an array).', $token->getLine(), $stream->getSourceContext());
+                        throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext());
                     }
                 } else {
                     $value = $this->parseExpression(0, $allowArrow);
@@ -626,6 +659,7 @@ class ExpressionParser
                 if (null === $name) {
                     $name = $value->getAttribute('name');
                     $value = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine());
+                    $value->setAttribute('is_implicit', true);
                 }
                 $args[$name] = $value;
             } else {
@@ -636,7 +670,7 @@ class ExpressionParser
                 }
             }
         }
-        $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ')', 'A list of arguments must be closed by a parenthesis');
+        $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
 
         return new Node($args);
     }
@@ -647,19 +681,19 @@ class ExpressionParser
         $targets = [];
         while (true) {
             $token = $this->parser->getCurrentToken();
-            if ($stream->test(/* Token::OPERATOR_TYPE */ 8) && preg_match(Lexer::REGEX_NAME, $token->getValue())) {
+            if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) {
                 // in this context, string operators are variable names
                 $this->parser->getStream()->next();
             } else {
-                $stream->expect(/* Token::NAME_TYPE */ 5, null, 'Only variables can be assigned to');
+                $stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to');
             }
             $value = $token->getValue();
             if (\in_array(strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), ['true', 'false', 'none', 'null'])) {
-                throw new SyntaxError(sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext());
+                throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext());
             }
             $targets[] = new AssignNameExpression($value, $token->getLine());
 
-            if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
+            if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
                 break;
             }
         }
@@ -672,7 +706,7 @@ class ExpressionParser
         $targets = [];
         while (true) {
             $targets[] = $this->parseExpression();
-            if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
+            if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) {
                 break;
             }
         }
@@ -688,121 +722,115 @@ class ExpressionParser
     private function parseTestExpression(Node $node): TestExpression
     {
         $stream = $this->parser->getStream();
-        list($name, $test) = $this->getTest($node->getTemplateLine());
+        $test = $this->getTest($node->getTemplateLine());
 
-        $class = $this->getTestNodeClass($test);
         $arguments = null;
-        if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
+        if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
             $arguments = $this->parseArguments(true);
         } elseif ($test->hasOneMandatoryArgument()) {
             $arguments = new Node([0 => $this->parsePrimaryExpression()]);
         }
 
-        if ('defined' === $name && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) {
+        if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) {
             $node = new MethodCallExpression($alias['node'], $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine());
             $node->setAttribute('safe', true);
         }
 
-        return new $class($node, $name, $arguments, $this->parser->getCurrentToken()->getLine());
+        $ready = $test instanceof TwigTest;
+        if (!isset($this->readyNodes[$class = $test->getNodeClass()])) {
+            $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
+        }
+
+        if (!$ready = $this->readyNodes[$class]) {
+            trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigTest" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class);
+        }
+
+        return new $class($node, $ready ? $test : $test->getName(), $arguments, $this->parser->getCurrentToken()->getLine());
     }
 
-    private function getTest(int $line): array
+    private function getTest(int $line): TwigTest
     {
         $stream = $this->parser->getStream();
-        $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue();
+        $name = $stream->expect(Token::NAME_TYPE)->getValue();
 
-        if ($test = $this->env->getTest($name)) {
-            return [$name, $test];
-        }
-
-        if ($stream->test(/* Token::NAME_TYPE */ 5)) {
+        if ($stream->test(Token::NAME_TYPE)) {
             // try 2-words tests
             $name = $name.' '.$this->parser->getCurrentToken()->getValue();
 
             if ($test = $this->env->getTest($name)) {
                 $stream->next();
-
-                return [$name, $test];
             }
+        } else {
+            $test = $this->env->getTest($name);
         }
 
-        $e = new SyntaxError(sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext());
-        $e->addSuggestions($name, array_keys($this->env->getTests()));
+        if (!$test) {
+            $e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext());
+            $e->addSuggestions($name, array_keys($this->env->getTests()));
 
-        throw $e;
-    }
+            throw $e;
+        }
 
-    private function getTestNodeClass(TwigTest $test): string
-    {
         if ($test->isDeprecated()) {
             $stream = $this->parser->getStream();
-            $message = sprintf('Twig Test "%s" is deprecated', $test->getName());
+            $message = \sprintf('Twig Test "%s" is deprecated', $test->getName());
 
-            if ($test->getDeprecatedVersion()) {
-                $message .= sprintf(' since version %s', $test->getDeprecatedVersion());
-            }
             if ($test->getAlternative()) {
-                $message .= sprintf('. Use "%s" instead', $test->getAlternative());
+                $message .= \sprintf('. Use "%s" instead', $test->getAlternative());
             }
             $src = $stream->getSourceContext();
-            $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine());
+            $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine());
 
-            @trigger_error($message, \E_USER_DEPRECATED);
+            trigger_deprecation($test->getDeprecatingPackage(), $test->getDeprecatedVersion(), $message);
         }
 
-        return $test->getNodeClass();
+        return $test;
     }
 
-    private function getFunctionNodeClass(string $name, int $line): string
+    private function getFunction(string $name, int $line): TwigFunction
     {
         if (!$function = $this->env->getFunction($name)) {
-            $e = new SyntaxError(sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext());
+            $e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext());
             $e->addSuggestions($name, array_keys($this->env->getFunctions()));
 
             throw $e;
         }
 
         if ($function->isDeprecated()) {
-            $message = sprintf('Twig Function "%s" is deprecated', $function->getName());
-            if ($function->getDeprecatedVersion()) {
-                $message .= sprintf(' since version %s', $function->getDeprecatedVersion());
-            }
+            $message = \sprintf('Twig Function "%s" is deprecated', $function->getName());
             if ($function->getAlternative()) {
-                $message .= sprintf('. Use "%s" instead', $function->getAlternative());
+                $message .= \sprintf('. Use "%s" instead', $function->getAlternative());
             }
             $src = $this->parser->getStream()->getSourceContext();
-            $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
+            $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
 
-            @trigger_error($message, \E_USER_DEPRECATED);
+            trigger_deprecation($function->getDeprecatingPackage(), $function->getDeprecatedVersion(), $message);
         }
 
-        return $function->getNodeClass();
+        return $function;
     }
 
-    private function getFilterNodeClass(string $name, int $line): string
+    private function getFilter(string $name, int $line): TwigFilter
     {
         if (!$filter = $this->env->getFilter($name)) {
-            $e = new SyntaxError(sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext());
+            $e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext());
             $e->addSuggestions($name, array_keys($this->env->getFilters()));
 
             throw $e;
         }
 
         if ($filter->isDeprecated()) {
-            $message = sprintf('Twig Filter "%s" is deprecated', $filter->getName());
-            if ($filter->getDeprecatedVersion()) {
-                $message .= sprintf(' since version %s', $filter->getDeprecatedVersion());
-            }
+            $message = \sprintf('Twig Filter "%s" is deprecated', $filter->getName());
             if ($filter->getAlternative()) {
-                $message .= sprintf('. Use "%s" instead', $filter->getAlternative());
+                $message .= \sprintf('. Use "%s" instead', $filter->getAlternative());
             }
             $src = $this->parser->getStream()->getSourceContext();
-            $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
+            $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
 
-            @trigger_error($message, \E_USER_DEPRECATED);
+            trigger_deprecation($filter->getDeprecatingPackage(), $filter->getDeprecatedVersion(), $message);
         }
 
-        return $filter->getNodeClass();
+        return $filter;
     }
 
     // checks that the node only contains "constant" elements

+ 1 - 1
data/web/inc/lib/vendor/twig/twig/src/Extension/AbstractExtension.php

@@ -40,6 +40,6 @@ abstract class AbstractExtension implements ExtensionInterface
 
     public function getOperators()
     {
-        return [];
+        return [[], []];
     }
 }

Fișier diff suprimat deoarece este prea mare
+ 1153 - 971
data/web/inc/lib/vendor/twig/twig/src/Extension/CoreExtension.php


+ 29 - 31
data/web/inc/lib/vendor/twig/twig/src/Extension/DebugExtension.php

@@ -9,7 +9,11 @@
  * file that was distributed with this source code.
  */
 
-namespace Twig\Extension {
+namespace Twig\Extension;
+
+use Twig\Environment;
+use Twig\Template;
+use Twig\TemplateWrapper;
 use Twig\TwigFunction;
 
 final class DebugExtension extends AbstractExtension
@@ -18,47 +22,41 @@ final class DebugExtension extends AbstractExtension
     {
         // dump is safe if var_dump is overridden by xdebug
         $isDumpOutputHtmlSafe = \extension_loaded('xdebug')
-            // false means that it was not set (and the default is on) or it explicitly enabled
-            && (false === ini_get('xdebug.overload_var_dump') || ini_get('xdebug.overload_var_dump'))
-            // false means that it was not set (and the default is on) or it explicitly enabled
-            // xdebug.overload_var_dump produces HTML only when html_errors is also enabled
-            && (false === ini_get('html_errors') || ini_get('html_errors'))
+            // Xdebug overloads var_dump in develop mode when html_errors is enabled
+            && str_contains(\ini_get('xdebug.mode'), 'develop')
+            && (false === \ini_get('html_errors') || \ini_get('html_errors'))
             || 'cli' === \PHP_SAPI
         ;
 
         return [
-            new TwigFunction('dump', 'twig_var_dump', ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]),
+            new TwigFunction('dump', [self::class, 'dump'], ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]),
         ];
     }
-}
-}
 
-namespace {
-use Twig\Environment;
-use Twig\Template;
-use Twig\TemplateWrapper;
-
-function twig_var_dump(Environment $env, $context, ...$vars)
-{
-    if (!$env->isDebug()) {
-        return;
-    }
+    /**
+     * @internal
+     */
+    public static function dump(Environment $env, $context, ...$vars)
+    {
+        if (!$env->isDebug()) {
+            return;
+        }
 
-    ob_start();
+        ob_start();
 
-    if (!$vars) {
-        $vars = [];
-        foreach ($context as $key => $value) {
-            if (!$value instanceof Template && !$value instanceof TemplateWrapper) {
-                $vars[$key] = $value;
+        if (!$vars) {
+            $vars = [];
+            foreach ($context as $key => $value) {
+                if (!$value instanceof Template && !$value instanceof TemplateWrapper) {
+                    $vars[$key] = $value;
+                }
             }
+
+            var_dump($vars);
+        } else {
+            var_dump(...$vars);
         }
 
-        var_dump($vars);
-    } else {
-        var_dump(...$vars);
+        return ob_get_clean();
     }
-
-    return ob_get_clean();
-}
 }

+ 84 - 300
data/web/inc/lib/vendor/twig/twig/src/Extension/EscaperExtension.php

@@ -9,22 +9,24 @@
  * file that was distributed with this source code.
  */
 
-namespace Twig\Extension {
+namespace Twig\Extension;
+
+use Twig\Environment;
 use Twig\FileExtensionEscapingStrategy;
+use Twig\Node\Expression\ConstantExpression;
+use Twig\Node\Expression\Filter\RawFilter;
+use Twig\Node\Node;
 use Twig\NodeVisitor\EscaperNodeVisitor;
+use Twig\Runtime\EscaperRuntime;
 use Twig\TokenParser\AutoEscapeTokenParser;
 use Twig\TwigFilter;
 
 final class EscaperExtension extends AbstractExtension
 {
-    private $defaultStrategy;
+    private $environment;
     private $escapers = [];
-
-    /** @internal */
-    public $safeClasses = [];
-
-    /** @internal */
-    public $safeLookup = [];
+    private $escaper;
+    private $defaultStrategy;
 
     /**
      * @param string|false|callable $defaultStrategy An escaping strategy
@@ -49,19 +51,43 @@ final class EscaperExtension extends AbstractExtension
     public function getFilters(): array
     {
         return [
-            new TwigFilter('escape', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
-            new TwigFilter('e', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
-            new TwigFilter('raw', 'twig_raw_filter', ['is_safe' => ['all']]),
+            new TwigFilter('escape', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]),
+            new TwigFilter('e', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]),
+            new TwigFilter('raw', null, ['is_safe' => ['all'], 'node_class' => RawFilter::class]),
         ];
     }
 
+    /**
+     * @deprecated since Twig 3.10
+     */
+    public function setEnvironment(Environment $environment): void
+    {
+        $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true;
+        if ($triggerDeprecation) {
+            trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__);
+        }
+
+        $this->environment = $environment;
+        $this->escaper = $environment->getRuntime(EscaperRuntime::class);
+    }
+
+    /**
+     * @deprecated since Twig 3.10
+     */
+    public function setEscaperRuntime(EscaperRuntime $escaper)
+    {
+        trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__);
+
+        $this->escaper = $escaper;
+    }
+
     /**
      * Sets the default strategy to use when not defined by the user.
      *
      * The strategy can be a valid PHP callback that takes the template
      * name as an argument and returns the strategy to use.
      *
-     * @param string|false|callable $defaultStrategy An escaping strategy
+     * @param string|false|callable(string $templateName): string $defaultStrategy An escaping strategy
      */
     public function setDefaultStrategy($defaultStrategy): void
     {
@@ -93,324 +119,82 @@ final class EscaperExtension extends AbstractExtension
     /**
      * Defines a new escaper to be used via the escape filter.
      *
-     * @param string   $strategy The strategy name that should be used as a strategy in the escape call
-     * @param callable $callable A valid PHP callable
+     * @param string                                        $strategy The strategy name that should be used as a strategy in the escape call
+     * @param callable(Environment, string, string): string $callable A valid PHP callable
+     *
+     * @deprecated since Twig 3.10
      */
     public function setEscaper($strategy, callable $callable)
     {
+        trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setEscaper()" method instead (be warned that Environment is not passed anymore to the callable).', __METHOD__);
+
+        if (!isset($this->environment)) {
+            throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
+        }
+
         $this->escapers[$strategy] = $callable;
+        $callable = function ($string, $charset) use ($callable) {
+            return $callable($this->environment, $string, $charset);
+        };
+
+        $this->escaper->setEscaper($strategy, $callable);
     }
 
     /**
      * Gets all defined escapers.
      *
-     * @return callable[] An array of escapers
+     * @return array<string, callable(Environment, string, string): string> An array of escapers
+     *
+     * @deprecated since Twig 3.10
      */
     public function getEscapers()
     {
+        trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::getEscaper()" method instead.', __METHOD__);
+
         return $this->escapers;
     }
 
+    /**
+     * @deprecated since Twig 3.10
+     */
     public function setSafeClasses(array $safeClasses = [])
     {
-        $this->safeClasses = [];
-        $this->safeLookup = [];
-        foreach ($safeClasses as $class => $strategies) {
-            $this->addSafeClass($class, $strategies);
-        }
-    }
+        trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setSafeClasses()" method instead.', __METHOD__);
 
-    public function addSafeClass(string $class, array $strategies)
-    {
-        $class = ltrim($class, '\\');
-        if (!isset($this->safeClasses[$class])) {
-            $this->safeClasses[$class] = [];
+        if (!isset($this->escaper)) {
+            throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
         }
-        $this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies);
 
-        foreach ($strategies as $strategy) {
-            $this->safeLookup[$strategy][$class] = true;
-        }
+        $this->escaper->setSafeClasses($safeClasses);
     }
-}
-}
 
-namespace {
-use Twig\Environment;
-use Twig\Error\RuntimeError;
-use Twig\Extension\EscaperExtension;
-use Twig\Markup;
-use Twig\Node\Expression\ConstantExpression;
-use Twig\Node\Node;
-
-/**
- * Marks a variable as being safe.
- *
- * @param string $string A PHP variable
- */
-function twig_raw_filter($string)
-{
-    return $string;
-}
-
-/**
- * Escapes a string.
- *
- * @param mixed  $string     The value to be escaped
- * @param string $strategy   The escaping strategy
- * @param string $charset    The charset
- * @param bool   $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
- *
- * @return string
- */
-function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
-{
-    if ($autoescape && $string instanceof Markup) {
-        return $string;
-    }
-
-    if (!\is_string($string)) {
-        if (\is_object($string) && method_exists($string, '__toString')) {
-            if ($autoescape) {
-                $c = \get_class($string);
-                $ext = $env->getExtension(EscaperExtension::class);
-                if (!isset($ext->safeClasses[$c])) {
-                    $ext->safeClasses[$c] = [];
-                    foreach (class_parents($string) + class_implements($string) as $class) {
-                        if (isset($ext->safeClasses[$class])) {
-                            $ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class]));
-                            foreach ($ext->safeClasses[$class] as $s) {
-                                $ext->safeLookup[$s][$c] = true;
-                            }
-                        }
-                    }
-                }
-                if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) {
-                    return (string) $string;
-                }
-            }
+    /**
+     * @deprecated since Twig 3.10
+     */
+    public function addSafeClass(string $class, array $strategies)
+    {
+        trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::addSafeClass()" method instead.', __METHOD__);
 
-            $string = (string) $string;
-        } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) {
-            return $string;
+        if (!isset($this->escaper)) {
+            throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
         }
-    }
 
-    if ('' === $string) {
-        return '';
+        $this->escaper->addSafeClass($class, $strategies);
     }
 
-    if (null === $charset) {
-        $charset = $env->getCharset();
-    }
-
-    switch ($strategy) {
-        case 'html':
-            // see https://www.php.net/htmlspecialchars
-
-            // Using a static variable to avoid initializing the array
-            // each time the function is called. Moving the declaration on the
-            // top of the function slow downs other escaping strategies.
-            static $htmlspecialcharsCharsets = [
-                'ISO-8859-1' => true, 'ISO8859-1' => true,
-                'ISO-8859-15' => true, 'ISO8859-15' => true,
-                'utf-8' => true, 'UTF-8' => true,
-                'CP866' => true, 'IBM866' => true, '866' => true,
-                'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true,
-                '1251' => true,
-                'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true,
-                'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true,
-                'BIG5' => true, '950' => true,
-                'GB2312' => true, '936' => true,
-                'BIG5-HKSCS' => true,
-                'SHIFT_JIS' => true, 'SJIS' => true, '932' => true,
-                'EUC-JP' => true, 'EUCJP' => true,
-                'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true,
-            ];
-
-            if (isset($htmlspecialcharsCharsets[$charset])) {
-                return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset);
-            }
-
-            if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
-                // cache the lowercase variant for future iterations
-                $htmlspecialcharsCharsets[$charset] = true;
-
-                return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset);
-            }
-
-            $string = twig_convert_encoding($string, 'UTF-8', $charset);
-            $string = htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8');
-
-            return iconv('UTF-8', $charset, $string);
-
-        case 'js':
-            // escape all non-alphanumeric characters
-            // into their \x or \uHHHH representations
-            if ('UTF-8' !== $charset) {
-                $string = twig_convert_encoding($string, 'UTF-8', $charset);
-            }
-
-            if (!preg_match('//u', $string)) {
-                throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
-            }
-
-            $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) {
-                $char = $matches[0];
-
-                /*
-                 * A few characters have short escape sequences in JSON and JavaScript.
-                 * Escape sequences supported only by JavaScript, not JSON, are omitted.
-                 * \" is also supported but omitted, because the resulting string is not HTML safe.
-                 */
-                static $shortMap = [
-                    '\\' => '\\\\',
-                    '/' => '\\/',
-                    "\x08" => '\b',
-                    "\x0C" => '\f',
-                    "\x0A" => '\n',
-                    "\x0D" => '\r',
-                    "\x09" => '\t',
-                ];
-
-                if (isset($shortMap[$char])) {
-                    return $shortMap[$char];
-                }
-
-                $codepoint = mb_ord($char, 'UTF-8');
-                if (0x10000 > $codepoint) {
-                    return sprintf('\u%04X', $codepoint);
-                }
-
-                // Split characters outside the BMP into surrogate pairs
-                // https://tools.ietf.org/html/rfc2781.html#section-2.1
-                $u = $codepoint - 0x10000;
-                $high = 0xD800 | ($u >> 10);
-                $low = 0xDC00 | ($u & 0x3FF);
-
-                return sprintf('\u%04X\u%04X', $high, $low);
-            }, $string);
-
-            if ('UTF-8' !== $charset) {
-                $string = iconv('UTF-8', $charset, $string);
-            }
-
-            return $string;
-
-        case 'css':
-            if ('UTF-8' !== $charset) {
-                $string = twig_convert_encoding($string, 'UTF-8', $charset);
-            }
-
-            if (!preg_match('//u', $string)) {
-                throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
-            }
-
-            $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) {
-                $char = $matches[0];
-
-                return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8'));
-            }, $string);
-
-            if ('UTF-8' !== $charset) {
-                $string = iconv('UTF-8', $charset, $string);
-            }
-
-            return $string;
-
-        case 'html_attr':
-            if ('UTF-8' !== $charset) {
-                $string = twig_convert_encoding($string, 'UTF-8', $charset);
-            }
-
-            if (!preg_match('//u', $string)) {
-                throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
-            }
-
-            $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) {
-                /**
-                 * This function is adapted from code coming from Zend Framework.
-                 *
-                 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com)
-                 * @license   https://framework.zend.com/license/new-bsd New BSD License
-                 */
-                $chr = $matches[0];
-                $ord = \ord($chr);
-
-                /*
-                 * The following replaces characters undefined in HTML with the
-                 * hex entity for the Unicode replacement character.
-                 */
-                if (($ord <= 0x1f && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7f && $ord <= 0x9f)) {
-                    return '&#xFFFD;';
-                }
-
-                /*
-                 * Check if the current character to escape has a name entity we should
-                 * replace it with while grabbing the hex value of the character.
-                 */
-                if (1 === \strlen($chr)) {
-                    /*
-                     * While HTML supports far more named entities, the lowest common denominator
-                     * has become HTML5's XML Serialisation which is restricted to the those named
-                     * entities that XML supports. Using HTML entities would result in this error:
-                     *     XML Parsing Error: undefined entity
-                     */
-                    static $entityMap = [
-                        34 => '&quot;', /* quotation mark */
-                        38 => '&amp;',  /* ampersand */
-                        60 => '&lt;',   /* less-than sign */
-                        62 => '&gt;',   /* greater-than sign */
-                    ];
-
-                    if (isset($entityMap[$ord])) {
-                        return $entityMap[$ord];
-                    }
-
-                    return sprintf('&#x%02X;', $ord);
-                }
-
-                /*
-                 * Per OWASP recommendations, we'll use hex entities for any other
-                 * characters where a named entity does not exist.
-                 */
-                return sprintf('&#x%04X;', mb_ord($chr, 'UTF-8'));
-            }, $string);
-
-            if ('UTF-8' !== $charset) {
-                $string = iconv('UTF-8', $charset, $string);
-            }
-
-            return $string;
-
-        case 'url':
-            return rawurlencode($string);
-
-        default:
-            $escapers = $env->getExtension(EscaperExtension::class)->getEscapers();
-            if (array_key_exists($strategy, $escapers)) {
-                return $escapers[$strategy]($env, $string, $charset);
+    /**
+     * @internal
+     */
+    public static function escapeFilterIsSafe(Node $filterArgs)
+    {
+        foreach ($filterArgs as $arg) {
+            if ($arg instanceof ConstantExpression) {
+                return [$arg->getAttribute('value')];
             }
 
-            $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers)));
-
-            throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies));
-    }
-}
-
-/**
- * @internal
- */
-function twig_escape_filter_is_safe(Node $filterArgs)
-{
-    foreach ($filterArgs as $arg) {
-        if ($arg instanceof ConstantExpression) {
-            return [$arg->getAttribute('value')];
+            return [];
         }
 
-        return [];
+        return ['html'];
     }
-
-    return ['html'];
-}
 }

+ 7 - 0
data/web/inc/lib/vendor/twig/twig/src/Extension/ExtensionInterface.php

@@ -11,6 +11,8 @@
 
 namespace Twig\Extension;
 
+use Twig\ExpressionParser;
+use Twig\Node\Expression\AbstractExpression;
 use Twig\NodeVisitor\NodeVisitorInterface;
 use Twig\TokenParser\TokenParserInterface;
 use Twig\TwigFilter;
@@ -63,6 +65,11 @@ interface ExtensionInterface
      * Returns a list of operators to add to the existing list.
      *
      * @return array<array> First array of unary operators, second array of binary operators
+     *
+     * @psalm-return array{
+     *     array<string, array{precedence: int, class: class-string<AbstractExpression>}>,
+     *     array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}>
+     * }
      */
     public function getOperators();
 }

+ 4 - 4
data/web/inc/lib/vendor/twig/twig/src/Extension/GlobalsInterface.php

@@ -12,14 +12,14 @@
 namespace Twig\Extension;
 
 /**
- * Enables usage of the deprecated Twig\Extension\AbstractExtension::getGlobals() method.
- *
- * Explicitly implement this interface if you really need to implement the
- * deprecated getGlobals() method in your extensions.
+ * Allows Twig extensions to add globals to the context.
  *
  * @author Fabien Potencier <fabien@symfony.com>
  */
 interface GlobalsInterface
 {
+    /**
+     * @return array<string, mixed>
+     */
     public function getGlobals(): array;
 }

+ 3 - 5
data/web/inc/lib/vendor/twig/twig/src/Extension/OptimizerExtension.php

@@ -15,11 +15,9 @@ use Twig\NodeVisitor\OptimizerNodeVisitor;
 
 final class OptimizerExtension extends AbstractExtension
 {
-    private $optimizers;
-
-    public function __construct(int $optimizers = -1)
-    {
-        $this->optimizers = $optimizers;
+    public function __construct(
+        private int $optimizers = -1,
+    ) {
     }
 
     public function getNodeVisitors(): array

+ 23 - 11
data/web/inc/lib/vendor/twig/twig/src/Extension/SandboxExtension.php

@@ -15,6 +15,7 @@ use Twig\NodeVisitor\SandboxNodeVisitor;
 use Twig\Sandbox\SecurityNotAllowedMethodError;
 use Twig\Sandbox\SecurityNotAllowedPropertyError;
 use Twig\Sandbox\SecurityPolicyInterface;
+use Twig\Sandbox\SourcePolicyInterface;
 use Twig\Source;
 use Twig\TokenParser\SandboxTokenParser;
 
@@ -23,11 +24,13 @@ final class SandboxExtension extends AbstractExtension
     private $sandboxedGlobally;
     private $sandboxed;
     private $policy;
+    private $sourcePolicy;
 
-    public function __construct(SecurityPolicyInterface $policy, $sandboxed = false)
+    public function __construct(SecurityPolicyInterface $policy, $sandboxed = false, ?SourcePolicyInterface $sourcePolicy = null)
     {
         $this->policy = $policy;
         $this->sandboxedGlobally = $sandboxed;
+        $this->sourcePolicy = $sourcePolicy;
     }
 
     public function getTokenParsers(): array
@@ -50,9 +53,9 @@ final class SandboxExtension extends AbstractExtension
         $this->sandboxed = false;
     }
 
-    public function isSandboxed(): bool
+    public function isSandboxed(?Source $source = null): bool
     {
-        return $this->sandboxedGlobally || $this->sandboxed;
+        return $this->sandboxedGlobally || $this->sandboxed || $this->isSourceSandboxed($source);
     }
 
     public function isSandboxedGlobally(): bool
@@ -60,6 +63,15 @@ final class SandboxExtension extends AbstractExtension
         return $this->sandboxedGlobally;
     }
 
+    private function isSourceSandboxed(?Source $source): bool
+    {
+        if (null === $source || null === $this->sourcePolicy) {
+            return false;
+        }
+
+        return $this->sourcePolicy->enableSandbox($source);
+    }
+
     public function setSecurityPolicy(SecurityPolicyInterface $policy)
     {
         $this->policy = $policy;
@@ -70,16 +82,16 @@ final class SandboxExtension extends AbstractExtension
         return $this->policy;
     }
 
-    public function checkSecurity($tags, $filters, $functions): void
+    public function checkSecurity($tags, $filters, $functions, ?Source $source = null): void
     {
-        if ($this->isSandboxed()) {
+        if ($this->isSandboxed($source)) {
             $this->policy->checkSecurity($tags, $filters, $functions);
         }
     }
 
-    public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $source = null): void
+    public function checkMethodAllowed($obj, $method, int $lineno = -1, ?Source $source = null): void
     {
-        if ($this->isSandboxed()) {
+        if ($this->isSandboxed($source)) {
             try {
                 $this->policy->checkMethodAllowed($obj, $method);
             } catch (SecurityNotAllowedMethodError $e) {
@@ -91,9 +103,9 @@ final class SandboxExtension extends AbstractExtension
         }
     }
 
-    public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $source = null): void
+    public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source $source = null): void
     {
-        if ($this->isSandboxed()) {
+        if ($this->isSandboxed($source)) {
             try {
                 $this->policy->checkPropertyAllowed($obj, $property);
             } catch (SecurityNotAllowedPropertyError $e) {
@@ -105,9 +117,9 @@ final class SandboxExtension extends AbstractExtension
         }
     }
 
-    public function ensureToStringAllowed($obj, int $lineno = -1, Source $source = null)
+    public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null)
     {
-        if ($this->isSandboxed() && \is_object($obj) && method_exists($obj, '__toString')) {
+        if ($this->isSandboxed($source) && $obj instanceof \Stringable) {
             try {
                 $this->policy->checkMethodAllowed($obj, '__toString');
             } catch (SecurityNotAllowedMethodError $e) {

+ 4 - 4
data/web/inc/lib/vendor/twig/twig/src/Extension/StagingExtension.php

@@ -35,7 +35,7 @@ final class StagingExtension extends AbstractExtension
     public function addFunction(TwigFunction $function): void
     {
         if (isset($this->functions[$function->getName()])) {
-            throw new \LogicException(sprintf('Function "%s" is already registered.', $function->getName()));
+            throw new \LogicException(\sprintf('Function "%s" is already registered.', $function->getName()));
         }
 
         $this->functions[$function->getName()] = $function;
@@ -49,7 +49,7 @@ final class StagingExtension extends AbstractExtension
     public function addFilter(TwigFilter $filter): void
     {
         if (isset($this->filters[$filter->getName()])) {
-            throw new \LogicException(sprintf('Filter "%s" is already registered.', $filter->getName()));
+            throw new \LogicException(\sprintf('Filter "%s" is already registered.', $filter->getName()));
         }
 
         $this->filters[$filter->getName()] = $filter;
@@ -73,7 +73,7 @@ final class StagingExtension extends AbstractExtension
     public function addTokenParser(TokenParserInterface $parser): void
     {
         if (isset($this->tokenParsers[$parser->getTag()])) {
-            throw new \LogicException(sprintf('Tag "%s" is already registered.', $parser->getTag()));
+            throw new \LogicException(\sprintf('Tag "%s" is already registered.', $parser->getTag()));
         }
 
         $this->tokenParsers[$parser->getTag()] = $parser;
@@ -87,7 +87,7 @@ final class StagingExtension extends AbstractExtension
     public function addTest(TwigTest $test): void
     {
         if (isset($this->tests[$test->getName()])) {
-            throw new \LogicException(sprintf('Test "%s" is already registered.', $test->getName()));
+            throw new \LogicException(\sprintf('Test "%s" is already registered.', $test->getName()));
         }
 
         $this->tests[$test->getName()] = $test;

+ 18 - 20
data/web/inc/lib/vendor/twig/twig/src/Extension/StringLoaderExtension.php

@@ -9,7 +9,10 @@
  * file that was distributed with this source code.
  */
 
-namespace Twig\Extension {
+namespace Twig\Extension;
+
+use Twig\Environment;
+use Twig\TemplateWrapper;
 use Twig\TwigFunction;
 
 final class StringLoaderExtension extends AbstractExtension
@@ -17,26 +20,21 @@ final class StringLoaderExtension extends AbstractExtension
     public function getFunctions(): array
     {
         return [
-            new TwigFunction('template_from_string', 'twig_template_from_string', ['needs_environment' => true]),
+            new TwigFunction('template_from_string', [self::class, 'templateFromString'], ['needs_environment' => true]),
         ];
     }
-}
-}
-
-namespace {
-use Twig\Environment;
-use Twig\TemplateWrapper;
 
-/**
- * Loads a template from a string.
- *
- *     {{ include(template_from_string("Hello {{ name }}")) }}
- *
- * @param string $template A template as a string or object implementing __toString()
- * @param string $name     An optional name of the template to be used in error messages
- */
-function twig_template_from_string(Environment $env, $template, string $name = null): TemplateWrapper
-{
-    return $env->createTemplate((string) $template, $name);
-}
+    /**
+     * Loads a template from a string.
+     *
+     *     {{ include(template_from_string("Hello {{ name }}")) }}
+     *
+     * @param string|null $name An optional name of the template to be used in error messages
+     *
+     * @internal
+     */
+    public static function templateFromString(Environment $env, string|\Stringable $template, ?string $name = null): TemplateWrapper
+    {
+        return $env->createTemplate((string) $template, $name);
+    }
 }

+ 30 - 0
data/web/inc/lib/vendor/twig/twig/src/Extension/YieldNotReadyExtension.php

@@ -0,0 +1,30 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Twig\Extension;
+
+use Twig\NodeVisitor\YieldNotReadyNodeVisitor;
+
+/**
+ * @internal to be removed in Twig 4
+ */
+final class YieldNotReadyExtension extends AbstractExtension
+{
+    public function __construct(
+        private bool $useYield,
+    ) {
+    }
+
+    public function getNodeVisitors(): array
+    {
+        return [new YieldNotReadyNodeVisitor($this->useYield)];
+    }
+}

+ 63 - 38
data/web/inc/lib/vendor/twig/twig/src/ExtensionSet.php

@@ -15,6 +15,9 @@ use Twig\Error\RuntimeError;
 use Twig\Extension\ExtensionInterface;
 use Twig\Extension\GlobalsInterface;
 use Twig\Extension\StagingExtension;
+use Twig\Node\Expression\AbstractExpression;
+use Twig\Node\Expression\Binary\AbstractBinary;
+use Twig\Node\Expression\Unary\AbstractUnary;
 use Twig\NodeVisitor\NodeVisitorInterface;
 use Twig\TokenParser\TokenParserInterface;
 
@@ -31,11 +34,23 @@ final class ExtensionSet
     private $staging;
     private $parsers;
     private $visitors;
+    /** @var array<string, TwigFilter> */
     private $filters;
+    /** @var array<string, TwigFilter> */
+    private $dynamicFilters;
+    /** @var array<string, TwigTest> */
     private $tests;
+    /** @var array<string, TwigTest> */
+    private $dynamicTests;
+    /** @var array<string, TwigFunction> */
     private $functions;
+    /** @var array<string, TwigFunction> */
+    private $dynamicFunctions;
+    /** @var array<string, array{precedence: int, class: class-string<AbstractExpression>}> */
     private $unaryOperators;
+    /** @var array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}> */
     private $binaryOperators;
+    /** @var array<string, mixed> */
     private $globals;
     private $functionCallbacks = [];
     private $filterCallbacks = [];
@@ -62,7 +77,7 @@ final class ExtensionSet
         $class = ltrim($class, '\\');
 
         if (!isset($this->extensions[$class])) {
-            throw new RuntimeError(sprintf('The "%s" extension is not enabled.', $class));
+            throw new RuntimeError(\sprintf('The "%s" extension is not enabled.', $class));
         }
 
         return $this->extensions[$class];
@@ -117,11 +132,11 @@ final class ExtensionSet
         $class = \get_class($extension);
 
         if ($this->initialized) {
-            throw new \LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class));
+            throw new \LogicException(\sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class));
         }
 
         if (isset($this->extensions[$class])) {
-            throw new \LogicException(sprintf('Unable to register extension "%s" as it is already registered.', $class));
+            throw new \LogicException(\sprintf('Unable to register extension "%s" as it is already registered.', $class));
         }
 
         $this->extensions[$class] = $extension;
@@ -130,7 +145,7 @@ final class ExtensionSet
     public function addFunction(TwigFunction $function): void
     {
         if ($this->initialized) {
-            throw new \LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $function->getName()));
+            throw new \LogicException(\sprintf('Unable to add function "%s" as extensions have already been initialized.', $function->getName()));
         }
 
         $this->staging->addFunction($function);
@@ -158,14 +173,11 @@ final class ExtensionSet
             return $this->functions[$name];
         }
 
-        foreach ($this->functions as $pattern => $function) {
-            $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
-
-            if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) {
+        foreach ($this->dynamicFunctions as $pattern => $function) {
+            if (preg_match($pattern, $name, $matches)) {
                 array_shift($matches);
-                $function->setArguments($matches);
 
-                return $function;
+                return $function->withDynamicArguments($name, $function->getName(), $matches);
             }
         }
 
@@ -186,7 +198,7 @@ final class ExtensionSet
     public function addFilter(TwigFilter $filter): void
     {
         if ($this->initialized) {
-            throw new \LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $filter->getName()));
+            throw new \LogicException(\sprintf('Unable to add filter "%s" as extensions have already been initialized.', $filter->getName()));
         }
 
         $this->staging->addFilter($filter);
@@ -214,14 +226,11 @@ final class ExtensionSet
             return $this->filters[$name];
         }
 
-        foreach ($this->filters as $pattern => $filter) {
-            $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
-
-            if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) {
+        foreach ($this->dynamicFilters as $pattern => $filter) {
+            if (preg_match($pattern, $name, $matches)) {
                 array_shift($matches);
-                $filter->setArguments($matches);
 
-                return $filter;
+                return $filter->withDynamicArguments($name, $filter->getName(), $matches);
             }
         }
 
@@ -305,6 +314,9 @@ final class ExtensionSet
         $this->parserCallbacks[] = $callable;
     }
 
+    /**
+     * @return array<string, mixed>
+     */
     public function getGlobals(): array
     {
         if (null !== $this->globals) {
@@ -317,12 +329,7 @@ final class ExtensionSet
                 continue;
             }
 
-            $extGlobals = $extension->getGlobals();
-            if (!\is_array($extGlobals)) {
-                throw new \UnexpectedValueException(sprintf('"%s::getGlobals()" must return an array of globals.', \get_class($extension)));
-            }
-
-            $globals = array_merge($globals, $extGlobals);
+            $globals = array_merge($globals, $extension->getGlobals());
         }
 
         if ($this->initialized) {
@@ -332,10 +339,15 @@ final class ExtensionSet
         return $globals;
     }
 
+    public function resetGlobals(): void
+    {
+        $this->globals = null;
+    }
+
     public function addTest(TwigTest $test): void
     {
         if ($this->initialized) {
-            throw new \LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $test->getName()));
+            throw new \LogicException(\sprintf('Unable to add test "%s" as extensions have already been initialized.', $test->getName()));
         }
 
         $this->staging->addTest($test);
@@ -363,22 +375,20 @@ final class ExtensionSet
             return $this->tests[$name];
         }
 
-        foreach ($this->tests as $pattern => $test) {
-            $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count);
-
-            if ($count) {
-                if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
-                    array_shift($matches);
-                    $test->setArguments($matches);
+        foreach ($this->dynamicTests as $pattern => $test) {
+            if (preg_match($pattern, $name, $matches)) {
+                array_shift($matches);
 
-                    return $test;
-                }
+                return $test->withDynamicArguments($name, $test->getName(), $matches);
             }
         }
 
         return null;
     }
 
+    /**
+     * @return array<string, array{precedence: int, class: class-string<AbstractExpression>}>
+     */
     public function getUnaryOperators(): array
     {
         if (!$this->initialized) {
@@ -388,6 +398,9 @@ final class ExtensionSet
         return $this->unaryOperators;
     }
 
+    /**
+     * @return array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}>
+     */
     public function getBinaryOperators(): array
     {
         if (!$this->initialized) {
@@ -403,6 +416,9 @@ final class ExtensionSet
         $this->filters = [];
         $this->functions = [];
         $this->tests = [];
+        $this->dynamicFilters = [];
+        $this->dynamicFunctions = [];
+        $this->dynamicTests = [];
         $this->visitors = [];
         $this->unaryOperators = [];
         $this->binaryOperators = [];
@@ -419,17 +435,26 @@ final class ExtensionSet
     {
         // filters
         foreach ($extension->getFilters() as $filter) {
-            $this->filters[$filter->getName()] = $filter;
+            $this->filters[$name = $filter->getName()] = $filter;
+            if (str_contains($name, '*')) {
+                $this->dynamicFilters['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $filter;
+            }
         }
 
         // functions
         foreach ($extension->getFunctions() as $function) {
-            $this->functions[$function->getName()] = $function;
+            $this->functions[$name = $function->getName()] = $function;
+            if (str_contains($name, '*')) {
+                $this->dynamicFunctions['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $function;
+            }
         }
 
         // tests
         foreach ($extension->getTests() as $test) {
-            $this->tests[$test->getName()] = $test;
+            $this->tests[$name = $test->getName()] = $test;
+            if (str_contains($name, '*')) {
+                $this->dynamicTests['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $test;
+            }
         }
 
         // token parsers
@@ -449,11 +474,11 @@ final class ExtensionSet
         // operators
         if ($operators = $extension->getOperators()) {
             if (!\is_array($operators)) {
-                throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), \is_object($operators) ? \get_class($operators) : \gettype($operators).(\is_resource($operators) ? '' : '#'.$operators)));
+                throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), \is_object($operators) ? \get_class($operators) : \gettype($operators).(\is_resource($operators) ? '' : '#'.$operators)));
             }
 
             if (2 !== \count($operators)) {
-                throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators)));
+                throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators)));
             }
 
             $this->unaryOperators = array_merge($this->unaryOperators, $operators[0]);

+ 1 - 1
data/web/inc/lib/vendor/twig/twig/src/FileExtensionEscapingStrategy.php

@@ -37,7 +37,7 @@ class FileExtensionEscapingStrategy
             return 'html'; // return html for directories
         }
 
-        if ('.twig' === substr($name, -5)) {
+        if (str_ends_with($name, '.twig')) {
             $name = substr($name, 0, -5);
         }
 

+ 115 - 32
data/web/inc/lib/vendor/twig/twig/src/Lexer.php

@@ -19,6 +19,8 @@ use Twig\Error\SyntaxError;
  */
 class Lexer
 {
+    private $isInitialized = false;
+
     private $tokens;
     private $code;
     private $cursor;
@@ -48,6 +50,14 @@ class Lexer
     public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As';
     public const PUNCTUATION = '()[]{}?:.,|';
 
+    private const SPECIAL_CHARS = [
+        'f' => "\f",
+        'n' => "\n",
+        'r' => "\r",
+        't' => "\t",
+        'v' => "\v",
+    ];
+
     public function __construct(Environment $env, array $options = [])
     {
         $this->env = $env;
@@ -61,6 +71,13 @@ class Lexer
             'whitespace_line_chars' => ' \t\0\x0B',
             'interpolation' => ['#{', '}'],
         ], $options);
+    }
+
+    private function initialize()
+    {
+        if ($this->isInitialized) {
+            return;
+        }
 
         // when PHP 7.3 is the min version, we will be able to remove the '#' part in preg_quote as it's part of the default
         $this->regexes = [
@@ -149,10 +166,14 @@ class Lexer
             'interpolation_start' => '{'.preg_quote($this->options['interpolation'][0], '#').'\s*}A',
             'interpolation_end' => '{\s*'.preg_quote($this->options['interpolation'][1], '#').'}A',
         ];
+
+        $this->isInitialized = true;
     }
 
     public function tokenize(Source $source): TokenStream
     {
+        $this->initialize();
+
         $this->source = $source;
         $this->code = str_replace(["\r\n", "\r"], "\n", $source->getCode());
         $this->cursor = 0;
@@ -194,11 +215,11 @@ class Lexer
             }
         }
 
-        $this->pushToken(/* Token::EOF_TYPE */ -1);
+        $this->pushToken(Token::EOF_TYPE);
 
         if (!empty($this->brackets)) {
-            list($expect, $lineno) = array_pop($this->brackets);
-            throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
+            [$expect, $lineno] = array_pop($this->brackets);
+            throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
         }
 
         return new TokenStream($this->tokens, $this->source);
@@ -208,7 +229,7 @@ class Lexer
     {
         // if no matches are left we return the rest of the template as simple text token
         if ($this->position == \count($this->positions[0]) - 1) {
-            $this->pushToken(/* Token::TEXT_TYPE */ 0, substr($this->code, $this->cursor));
+            $this->pushToken(Token::TEXT_TYPE, substr($this->code, $this->cursor));
             $this->cursor = $this->end;
 
             return;
@@ -237,7 +258,7 @@ class Lexer
                 $text = rtrim($text, " \t\0\x0B");
             }
         }
-        $this->pushToken(/* Token::TEXT_TYPE */ 0, $text);
+        $this->pushToken(Token::TEXT_TYPE, $text);
         $this->moveCursor($textContent.$position[0]);
 
         switch ($this->positions[1][$this->position][0]) {
@@ -255,14 +276,14 @@ class Lexer
                     $this->moveCursor($match[0]);
                     $this->lineno = (int) $match[1];
                 } else {
-                    $this->pushToken(/* Token::BLOCK_START_TYPE */ 1);
+                    $this->pushToken(Token::BLOCK_START_TYPE);
                     $this->pushState(self::STATE_BLOCK);
                     $this->currentVarBlockLine = $this->lineno;
                 }
                 break;
 
             case $this->options['tag_variable'][0]:
-                $this->pushToken(/* Token::VAR_START_TYPE */ 2);
+                $this->pushToken(Token::VAR_START_TYPE);
                 $this->pushState(self::STATE_VAR);
                 $this->currentVarBlockLine = $this->lineno;
                 break;
@@ -272,7 +293,7 @@ class Lexer
     private function lexBlock(): void
     {
         if (empty($this->brackets) && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) {
-            $this->pushToken(/* Token::BLOCK_END_TYPE */ 3);
+            $this->pushToken(Token::BLOCK_END_TYPE);
             $this->moveCursor($match[0]);
             $this->popState();
         } else {
@@ -283,7 +304,7 @@ class Lexer
     private function lexVar(): void
     {
         if (empty($this->brackets) && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) {
-            $this->pushToken(/* Token::VAR_END_TYPE */ 4);
+            $this->pushToken(Token::VAR_END_TYPE);
             $this->moveCursor($match[0]);
             $this->popState();
         } else {
@@ -298,23 +319,28 @@ class Lexer
             $this->moveCursor($match[0]);
 
             if ($this->cursor >= $this->end) {
-                throw new SyntaxError(sprintf('Unclosed "%s".', self::STATE_BLOCK === $this->state ? 'block' : 'variable'), $this->currentVarBlockLine, $this->source);
+                throw new SyntaxError(\sprintf('Unclosed "%s".', self::STATE_BLOCK === $this->state ? 'block' : 'variable'), $this->currentVarBlockLine, $this->source);
             }
         }
 
+        // spread operator
+        if ('.' === $this->code[$this->cursor] && ($this->cursor + 2 < $this->end) && '.' === $this->code[$this->cursor + 1] && '.' === $this->code[$this->cursor + 2]) {
+            $this->pushToken(Token::SPREAD_TYPE, '...');
+            $this->moveCursor('...');
+        }
         // arrow function
-        if ('=' === $this->code[$this->cursor] && '>' === $this->code[$this->cursor + 1]) {
+        elseif ('=' === $this->code[$this->cursor] && ($this->cursor + 1 < $this->end) && '>' === $this->code[$this->cursor + 1]) {
             $this->pushToken(Token::ARROW_TYPE, '=>');
             $this->moveCursor('=>');
         }
         // operators
         elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) {
-            $this->pushToken(/* Token::OPERATOR_TYPE */ 8, preg_replace('/\s+/', ' ', $match[0]));
+            $this->pushToken(Token::OPERATOR_TYPE, preg_replace('/\s+/', ' ', $match[0]));
             $this->moveCursor($match[0]);
         }
         // names
         elseif (preg_match(self::REGEX_NAME, $this->code, $match, 0, $this->cursor)) {
-            $this->pushToken(/* Token::NAME_TYPE */ 5, $match[0]);
+            $this->pushToken(Token::NAME_TYPE, $match[0]);
             $this->moveCursor($match[0]);
         }
         // numbers
@@ -323,33 +349,33 @@ class Lexer
             if (ctype_digit($match[0]) && $number <= \PHP_INT_MAX) {
                 $number = (int) $match[0]; // integers lower than the maximum
             }
-            $this->pushToken(/* Token::NUMBER_TYPE */ 6, $number);
+            $this->pushToken(Token::NUMBER_TYPE, $number);
             $this->moveCursor($match[0]);
         }
         // punctuation
-        elseif (false !== strpos(self::PUNCTUATION, $this->code[$this->cursor])) {
+        elseif (str_contains(self::PUNCTUATION, $this->code[$this->cursor])) {
             // opening bracket
-            if (false !== strpos('([{', $this->code[$this->cursor])) {
+            if (str_contains('([{', $this->code[$this->cursor])) {
                 $this->brackets[] = [$this->code[$this->cursor], $this->lineno];
             }
             // closing bracket
-            elseif (false !== strpos(')]}', $this->code[$this->cursor])) {
+            elseif (str_contains(')]}', $this->code[$this->cursor])) {
                 if (empty($this->brackets)) {
-                    throw new SyntaxError(sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
+                    throw new SyntaxError(\sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
                 }
 
-                list($expect, $lineno) = array_pop($this->brackets);
+                [$expect, $lineno] = array_pop($this->brackets);
                 if ($this->code[$this->cursor] != strtr($expect, '([{', ')]}')) {
-                    throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
+                    throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
                 }
             }
 
-            $this->pushToken(/* Token::PUNCTUATION_TYPE */ 9, $this->code[$this->cursor]);
+            $this->pushToken(Token::PUNCTUATION_TYPE, $this->code[$this->cursor]);
             ++$this->cursor;
         }
         // strings
         elseif (preg_match(self::REGEX_STRING, $this->code, $match, 0, $this->cursor)) {
-            $this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes(substr($match[0], 1, -1)));
+            $this->pushToken(Token::STRING_TYPE, $this->stripcslashes(substr($match[0], 1, -1), substr($match[0], 0, 1)));
             $this->moveCursor($match[0]);
         }
         // opening double quoted string
@@ -360,8 +386,65 @@ class Lexer
         }
         // unlexable
         else {
-            throw new SyntaxError(sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
+            throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
+        }
+    }
+
+    private function stripcslashes(string $str, string $quoteType): string
+    {
+        $result = '';
+        $length = \strlen($str);
+
+        $i = 0;
+        while ($i < $length) {
+            if (false === $pos = strpos($str, '\\', $i)) {
+                $result .= substr($str, $i);
+                break;
+            }
+
+            $result .= substr($str, $i, $pos - $i);
+            $i = $pos + 1;
+
+            if ($i >= $length) {
+                $result .= '\\';
+                break;
+            }
+
+            $nextChar = $str[$i];
+
+            if (isset(self::SPECIAL_CHARS[$nextChar])) {
+                $result .= self::SPECIAL_CHARS[$nextChar];
+            } elseif ('\\' === $nextChar) {
+                $result .= $nextChar;
+            } elseif ("'" === $nextChar || '"' === $nextChar) {
+                if ($nextChar !== $quoteType) {
+                    trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1);
+                }
+                $result .= $nextChar;
+            } elseif ('#' === $nextChar && $i + 1 < $length && '{' === $str[$i + 1]) {
+                $result .= '#{';
+                ++$i;
+            } elseif ('x' === $nextChar && $i + 1 < $length && ctype_xdigit($str[$i + 1])) {
+                $hex = $str[++$i];
+                if ($i + 1 < $length && ctype_xdigit($str[$i + 1])) {
+                    $hex .= $str[++$i];
+                }
+                $result .= \chr(hexdec($hex));
+            } elseif (ctype_digit($nextChar) && $nextChar < '8') {
+                $octal = $nextChar;
+                while ($i + 1 < $length && ctype_digit($str[$i + 1]) && $str[$i + 1] < '8' && \strlen($octal) < 3) {
+                    $octal .= $str[++$i];
+                }
+                $result .= \chr(octdec($octal));
+            } else {
+                trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1);
+                $result .= $nextChar;
+            }
+
+            ++$i;
         }
+
+        return $result;
     }
 
     private function lexRawData(): void
@@ -385,7 +468,7 @@ class Lexer
             }
         }
 
-        $this->pushToken(/* Token::TEXT_TYPE */ 0, $text);
+        $this->pushToken(Token::TEXT_TYPE, $text);
     }
 
     private function lexComment(): void
@@ -401,23 +484,23 @@ class Lexer
     {
         if (preg_match($this->regexes['interpolation_start'], $this->code, $match, 0, $this->cursor)) {
             $this->brackets[] = [$this->options['interpolation'][0], $this->lineno];
-            $this->pushToken(/* Token::INTERPOLATION_START_TYPE */ 10);
+            $this->pushToken(Token::INTERPOLATION_START_TYPE);
             $this->moveCursor($match[0]);
             $this->pushState(self::STATE_INTERPOLATION);
-        } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && \strlen($match[0]) > 0) {
-            $this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes($match[0]));
+        } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && '' !== $match[0]) {
+            $this->pushToken(Token::STRING_TYPE, $this->stripcslashes($match[0], '"'));
             $this->moveCursor($match[0]);
         } elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) {
-            list($expect, $lineno) = array_pop($this->brackets);
+            [$expect, $lineno] = array_pop($this->brackets);
             if ('"' != $this->code[$this->cursor]) {
-                throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
+                throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
             }
 
             $this->popState();
             ++$this->cursor;
         } else {
             // unlexable
-            throw new SyntaxError(sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
+            throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
         }
     }
 
@@ -426,7 +509,7 @@ class Lexer
         $bracket = end($this->brackets);
         if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, 0, $this->cursor)) {
             array_pop($this->brackets);
-            $this->pushToken(/* Token::INTERPOLATION_END_TYPE */ 11);
+            $this->pushToken(Token::INTERPOLATION_END_TYPE);
             $this->moveCursor($match[0]);
             $this->popState();
         } else {
@@ -437,7 +520,7 @@ class Lexer
     private function pushToken($type, $value = ''): void
     {
         // do not push empty text tokens
-        if (/* Token::TEXT_TYPE */ 0 === $type && '' === $value) {
+        if (Token::TEXT_TYPE === $type && '' === $value) {
             return;
         }
 

+ 6 - 8
data/web/inc/lib/vendor/twig/twig/src/Loader/ArrayLoader.php

@@ -28,14 +28,12 @@ use Twig\Source;
  */
 final class ArrayLoader implements LoaderInterface
 {
-    private $templates = [];
-
     /**
      * @param array $templates An array of templates (keys are the names, and values are the source code)
      */
-    public function __construct(array $templates = [])
-    {
-        $this->templates = $templates;
+    public function __construct(
+        private array $templates = [],
+    ) {
     }
 
     public function setTemplate(string $name, string $template): void
@@ -46,7 +44,7 @@ final class ArrayLoader implements LoaderInterface
     public function getSourceContext(string $name): Source
     {
         if (!isset($this->templates[$name])) {
-            throw new LoaderError(sprintf('Template "%s" is not defined.', $name));
+            throw new LoaderError(\sprintf('Template "%s" is not defined.', $name));
         }
 
         return new Source($this->templates[$name], $name);
@@ -60,7 +58,7 @@ final class ArrayLoader implements LoaderInterface
     public function getCacheKey(string $name): string
     {
         if (!isset($this->templates[$name])) {
-            throw new LoaderError(sprintf('Template "%s" is not defined.', $name));
+            throw new LoaderError(\sprintf('Template "%s" is not defined.', $name));
         }
 
         return $name.':'.$this->templates[$name];
@@ -69,7 +67,7 @@ final class ArrayLoader implements LoaderInterface
     public function isFresh(string $name, int $time): bool
     {
         if (!isset($this->templates[$name])) {
-            throw new LoaderError(sprintf('Template "%s" is not defined.', $name));
+            throw new LoaderError(\sprintf('Template "%s" is not defined.', $name));
         }
 
         return true;

+ 28 - 15
data/web/inc/lib/vendor/twig/twig/src/Loader/ChainLoader.php

@@ -21,22 +21,28 @@ use Twig\Source;
  */
 final class ChainLoader implements LoaderInterface
 {
+    /**
+     * @var array<string, bool>
+     */
     private $hasSourceCache = [];
-    private $loaders = [];
 
     /**
-     * @param LoaderInterface[] $loaders
+     * @param iterable<LoaderInterface> $loaders
      */
-    public function __construct(array $loaders = [])
-    {
-        foreach ($loaders as $loader) {
-            $this->addLoader($loader);
-        }
+    public function __construct(
+        private iterable $loaders = [],
+    ) {
     }
 
     public function addLoader(LoaderInterface $loader): void
     {
-        $this->loaders[] = $loader;
+        $current = $this->loaders;
+
+        $this->loaders = (static function () use ($current, $loader): \Generator {
+            yield from $current;
+            yield $loader;
+        })();
+
         $this->hasSourceCache = [];
     }
 
@@ -45,13 +51,18 @@ final class ChainLoader implements LoaderInterface
      */
     public function getLoaders(): array
     {
+        if (!\is_array($this->loaders)) {
+            $this->loaders = iterator_to_array($this->loaders, false);
+        }
+
         return $this->loaders;
     }
 
     public function getSourceContext(string $name): Source
     {
         $exceptions = [];
-        foreach ($this->loaders as $loader) {
+
+        foreach ($this->getLoaders() as $loader) {
             if (!$loader->exists($name)) {
                 continue;
             }
@@ -63,7 +74,7 @@ final class ChainLoader implements LoaderInterface
             }
         }
 
-        throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
+        throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
     }
 
     public function exists(string $name): bool
@@ -72,7 +83,7 @@ final class ChainLoader implements LoaderInterface
             return $this->hasSourceCache[$name];
         }
 
-        foreach ($this->loaders as $loader) {
+        foreach ($this->getLoaders() as $loader) {
             if ($loader->exists($name)) {
                 return $this->hasSourceCache[$name] = true;
             }
@@ -84,7 +95,8 @@ final class ChainLoader implements LoaderInterface
     public function getCacheKey(string $name): string
     {
         $exceptions = [];
-        foreach ($this->loaders as $loader) {
+
+        foreach ($this->getLoaders() as $loader) {
             if (!$loader->exists($name)) {
                 continue;
             }
@@ -96,13 +108,14 @@ final class ChainLoader implements LoaderInterface
             }
         }
 
-        throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
+        throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
     }
 
     public function isFresh(string $name, int $time): bool
     {
         $exceptions = [];
-        foreach ($this->loaders as $loader) {
+
+        foreach ($this->getLoaders() as $loader) {
             if (!$loader->exists($name)) {
                 continue;
             }
@@ -114,6 +127,6 @@ final class ChainLoader implements LoaderInterface
             }
         }
 
-        throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
+        throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : ''));
     }
 }

+ 10 - 10
data/web/inc/lib/vendor/twig/twig/src/Loader/FilesystemLoader.php

@@ -34,9 +34,9 @@ class FilesystemLoader implements LoaderInterface
      * @param string|array $paths    A path or an array of paths where to look for templates
      * @param string|null  $rootPath The root path common to all relative paths (null for getcwd())
      */
-    public function __construct($paths = [], string $rootPath = null)
+    public function __construct($paths = [], ?string $rootPath = null)
     {
-        $this->rootPath = (null === $rootPath ? getcwd() : $rootPath).\DIRECTORY_SEPARATOR;
+        $this->rootPath = ($rootPath ?? getcwd()).\DIRECTORY_SEPARATOR;
         if (null !== $rootPath && false !== ($realPath = realpath($rootPath))) {
             $this->rootPath = $realPath.\DIRECTORY_SEPARATOR;
         }
@@ -89,7 +89,7 @@ class FilesystemLoader implements LoaderInterface
 
         $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path;
         if (!is_dir($checkPath)) {
-            throw new LoaderError(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
+            throw new LoaderError(\sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
         }
 
         $this->paths[$namespace][] = rtrim($path, '/\\');
@@ -105,7 +105,7 @@ class FilesystemLoader implements LoaderInterface
 
         $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path;
         if (!is_dir($checkPath)) {
-            throw new LoaderError(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
+            throw new LoaderError(\sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath));
         }
 
         $path = rtrim($path, '/\\');
@@ -183,7 +183,7 @@ class FilesystemLoader implements LoaderInterface
         }
 
         try {
-            list($namespace, $shortname) = $this->parseName($name);
+            [$namespace, $shortname] = $this->parseName($name);
 
             $this->validateName($shortname);
         } catch (LoaderError $e) {
@@ -195,7 +195,7 @@ class FilesystemLoader implements LoaderInterface
         }
 
         if (!isset($this->paths[$namespace])) {
-            $this->errorCache[$name] = sprintf('There are no registered paths for namespace "%s".', $namespace);
+            $this->errorCache[$name] = \sprintf('There are no registered paths for namespace "%s".', $namespace);
 
             if (!$throw) {
                 return null;
@@ -218,7 +218,7 @@ class FilesystemLoader implements LoaderInterface
             }
         }
 
-        $this->errorCache[$name] = sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace]));
+        $this->errorCache[$name] = \sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace]));
 
         if (!$throw) {
             return null;
@@ -236,7 +236,7 @@ class FilesystemLoader implements LoaderInterface
     {
         if (isset($name[0]) && '@' == $name[0]) {
             if (false === $pos = strpos($name, '/')) {
-                throw new LoaderError(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
+                throw new LoaderError(\sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name));
             }
 
             $namespace = substr($name, 1, $pos - 1);
@@ -250,7 +250,7 @@ class FilesystemLoader implements LoaderInterface
 
     private function validateName(string $name): void
     {
-        if (false !== strpos($name, "\0")) {
+        if (str_contains($name, "\0")) {
             throw new LoaderError('A template name cannot contain NUL bytes.');
         }
 
@@ -265,7 +265,7 @@ class FilesystemLoader implements LoaderInterface
             }
 
             if ($level < 0) {
-                throw new LoaderError(sprintf('Looks like you try to load a template outside configured directories (%s).', $name));
+                throw new LoaderError(\sprintf('Looks like you try to load a template outside configured directories (%s).', $name));
             }
         }
     }

+ 2 - 2
data/web/inc/lib/vendor/twig/twig/src/Markup.php

@@ -16,10 +16,10 @@ namespace Twig;
  *
  * @author Fabien Potencier <fabien@symfony.com>
  */
-class Markup implements \Countable, \JsonSerializable
+class Markup implements \Countable, \JsonSerializable, \Stringable
 {
     private $content;
-    private $charset;
+    private ?string $charset;
 
     public function __construct($content, $charset)
     {

+ 4 - 2
data/web/inc/lib/vendor/twig/twig/src/Node/AutoEscapeNode.php

@@ -11,6 +11,7 @@
 
 namespace Twig\Node;
 
+use Twig\Attribute\YieldReady;
 use Twig\Compiler;
 
 /**
@@ -24,11 +25,12 @@ use Twig\Compiler;
  *
  * @author Fabien Potencier <fabien@symfony.com>
  */
+#[YieldReady]
 class AutoEscapeNode extends Node
 {
-    public function __construct($value, Node $body, int $lineno, string $tag = 'autoescape')
+    public function __construct($value, Node $body, int $lineno)
     {
-        parent::__construct(['body' => $body], ['value' => $value], $lineno, $tag);
+        parent::__construct(['body' => $body], ['value' => $value], $lineno);
     }
 
     public function compile(Compiler $compiler): void

+ 9 - 3
data/web/inc/lib/vendor/twig/twig/src/Node/BlockNode.php

@@ -12,6 +12,7 @@
 
 namespace Twig\Node;
 
+use Twig\Attribute\YieldReady;
 use Twig\Compiler;
 
 /**
@@ -19,24 +20,29 @@ use Twig\Compiler;
  *
  * @author Fabien Potencier <fabien@symfony.com>
  */
+#[YieldReady]
 class BlockNode extends Node
 {
-    public function __construct(string $name, Node $body, int $lineno, string $tag = null)
+    public function __construct(string $name, Node $body, int $lineno)
     {
-        parent::__construct(['body' => $body], ['name' => $name], $lineno, $tag);
+        parent::__construct(['body' => $body], ['name' => $name], $lineno);
     }
 
     public function compile(Compiler $compiler): void
     {
         $compiler
             ->addDebugInfo($this)
-            ->write(sprintf("public function block_%s(\$context, array \$blocks = [])\n", $this->getAttribute('name')), "{\n")
+            ->write("/**\n")
+            ->write(" * @return iterable<null|scalar|\Stringable>\n")
+            ->write(" */\n")
+            ->write(\sprintf("public function block_%s(array \$context, array \$blocks = []): iterable\n", $this->getAttribute('name')), "{\n")
             ->indent()
             ->write("\$macros = \$this->macros;\n")
         ;
 
         $compiler
             ->subcompile($this->getNode('body'))
+            ->write("yield from [];\n")
             ->outdent()
             ->write("}\n\n")
         ;

+ 5 - 3
data/web/inc/lib/vendor/twig/twig/src/Node/BlockReferenceNode.php

@@ -12,6 +12,7 @@
 
 namespace Twig\Node;
 
+use Twig\Attribute\YieldReady;
 use Twig\Compiler;
 
 /**
@@ -19,18 +20,19 @@ use Twig\Compiler;
  *
  * @author Fabien Potencier <fabien@symfony.com>
  */
+#[YieldReady]
 class BlockReferenceNode extends Node implements NodeOutputInterface
 {
-    public function __construct(string $name, int $lineno, string $tag = null)
+    public function __construct(string $name, int $lineno)
     {
-        parent::__construct([], ['name' => $name], $lineno, $tag);
+        parent::__construct([], ['name' => $name], $lineno);
     }
 
     public function compile(Compiler $compiler): void
     {
         $compiler
             ->addDebugInfo($this)
-            ->write(sprintf("\$this->displayBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name')))
+            ->write(\sprintf("yield from \$this->unwrap()->yieldBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name')))
         ;
     }
 }

+ 3 - 0
data/web/inc/lib/vendor/twig/twig/src/Node/BodyNode.php

@@ -11,11 +11,14 @@
 
 namespace Twig\Node;
 
+use Twig\Attribute\YieldReady;
+
 /**
  * Represents a body node.
  *
  * @author Fabien Potencier <fabien@symfony.com>
  */
+#[YieldReady]
 class BodyNode extends Node
 {
 }

+ 57 - 0
data/web/inc/lib/vendor/twig/twig/src/Node/CaptureNode.php

@@ -0,0 +1,57 @@
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Twig\Node;
+
+use Twig\Attribute\YieldReady;
+use Twig\Compiler;
+
+/**
+ * Represents a node for which we need to capture the output.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+#[YieldReady]
+class CaptureNode extends Node
+{
+    public function __construct(Node $body, int $lineno)
+    {
+        parent::__construct(['body' => $body], ['raw' => false], $lineno);
+    }
+
+    public function compile(Compiler $compiler): void
+    {
+        $useYield = $compiler->getEnvironment()->useYield();
+
+        if (!$this->getAttribute('raw')) {
+            $compiler->raw("('' === \$tmp = ");
+        }
+        $compiler
+            ->raw($useYield ? "implode('', iterator_to_array(" : '\\Twig\\Extension\\CoreExtension::captureOutput(')
+            ->raw("(function () use (&\$context, \$macros, \$blocks) {\n")
+            ->indent()
+            ->subcompile($this->getNode('body'))
+            ->write("yield from [];\n")
+            ->outdent()
+            ->write('})()')
+        ;
+        if ($useYield) {
+            $compiler->raw(', false))');
+        } else {
+            $compiler->raw(')');
+        }
+        if (!$this->getAttribute('raw')) {
+            $compiler->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset());");
+        } else {
+            $compiler->raw(';');
+        }
+    }
+}

+ 3 - 1
data/web/inc/lib/vendor/twig/twig/src/Node/CheckSecurityCallNode.php

@@ -11,17 +11,19 @@
 
 namespace Twig\Node;
 
+use Twig\Attribute\YieldReady;
 use Twig\Compiler;
 
 /**
  * @author Fabien Potencier <fabien@symfony.com>
  */
+#[YieldReady]
 class CheckSecurityCallNode extends Node
 {
     public function compile(Compiler $compiler)
     {
         $compiler
-            ->write("\$this->sandbox = \$this->env->getExtension('\Twig\Extension\SandboxExtension');\n")
+            ->write("\$this->sandbox = \$this->extensions[SandboxExtension::class];\n")
             ->write("\$this->checkSecurity();\n")
         ;
     }

+ 14 - 17
data/web/inc/lib/vendor/twig/twig/src/Node/CheckSecurityNode.php

@@ -11,17 +11,24 @@
 
 namespace Twig\Node;
 
+use Twig\Attribute\YieldReady;
 use Twig\Compiler;
 
 /**
  * @author Fabien Potencier <fabien@symfony.com>
  */
+#[YieldReady]
 class CheckSecurityNode extends Node
 {
     private $usedFilters;
     private $usedTags;
     private $usedFunctions;
 
+    /**
+     * @param array<string, int> $usedFilters
+     * @param array<string, int> $usedTags
+     * @param array<string, int> $usedFunctions
+     */
     public function __construct(array $usedFilters, array $usedTags, array $usedFunctions)
     {
         $this->usedFilters = $usedFilters;
@@ -33,32 +40,22 @@ class CheckSecurityNode extends Node
 
     public function compile(Compiler $compiler): void
     {
-        $tags = $filters = $functions = [];
-        foreach (['tags', 'filters', 'functions'] as $type) {
-            foreach ($this->{'used'.ucfirst($type)} as $name => $node) {
-                if ($node instanceof Node) {
-                    ${$type}[$name] = $node->getTemplateLine();
-                } else {
-                    ${$type}[$node] = null;
-                }
-            }
-        }
-
         $compiler
             ->write("\n")
             ->write("public function checkSecurity()\n")
             ->write("{\n")
             ->indent()
-            ->write('static $tags = ')->repr(array_filter($tags))->raw(";\n")
-            ->write('static $filters = ')->repr(array_filter($filters))->raw(";\n")
-            ->write('static $functions = ')->repr(array_filter($functions))->raw(";\n\n")
+            ->write('static $tags = ')->repr(array_filter($this->usedTags))->raw(";\n")
+            ->write('static $filters = ')->repr(array_filter($this->usedFilters))->raw(";\n")
+            ->write('static $functions = ')->repr(array_filter($this->usedFunctions))->raw(";\n\n")
             ->write("try {\n")
             ->indent()
             ->write("\$this->sandbox->checkSecurity(\n")
             ->indent()
-            ->write(!$tags ? "[],\n" : "['".implode("', '", array_keys($tags))."'],\n")
-            ->write(!$filters ? "[],\n" : "['".implode("', '", array_keys($filters))."'],\n")
-            ->write(!$functions ? "[]\n" : "['".implode("', '", array_keys($functions))."']\n")
+            ->write(!$this->usedTags ? "[],\n" : "['".implode("', '", array_keys($this->usedTags))."'],\n")
+            ->write(!$this->usedFilters ? "[],\n" : "['".implode("', '", array_keys($this->usedFilters))."'],\n")
+            ->write(!$this->usedFunctions ? "[],\n" : "['".implode("', '", array_keys($this->usedFunctions))."'],\n")
+            ->write("\$this->source\n")
             ->outdent()
             ->write(");\n")
             ->outdent()

+ 3 - 1
data/web/inc/lib/vendor/twig/twig/src/Node/CheckToStringNode.php

@@ -11,6 +11,7 @@
 
 namespace Twig\Node;
 
+use Twig\Attribute\YieldReady;
 use Twig\Compiler;
 use Twig\Node\Expression\AbstractExpression;
 
@@ -24,11 +25,12 @@ use Twig\Node\Expression\AbstractExpression;
  *
  * @author Fabien Potencier <fabien@symfony.com>
  */
+#[YieldReady]
 class CheckToStringNode extends AbstractExpression
 {
     public function __construct(AbstractExpression $expr)
     {
-        parent::__construct(['expr' => $expr], [], $expr->getTemplateLine(), $expr->getNodeTag());
+        parent::__construct(['expr' => $expr], [], $expr->getTemplateLine());
     }
 
     public function compile(Compiler $compiler): void

+ 30 - 10
data/web/inc/lib/vendor/twig/twig/src/Node/DeprecatedNode.php

@@ -11,6 +11,7 @@
 
 namespace Twig\Node;
 
+use Twig\Attribute\YieldReady;
 use Twig\Compiler;
 use Twig\Node\Expression\AbstractExpression;
 use Twig\Node\Expression\ConstantExpression;
@@ -20,11 +21,12 @@ use Twig\Node\Expression\ConstantExpression;
  *
  * @author Yonel Ceruto <yonelceruto@gmail.com>
  */
+#[YieldReady]
 class DeprecatedNode extends Node
 {
-    public function __construct(AbstractExpression $expr, int $lineno, string $tag = null)
+    public function __construct(AbstractExpression $expr, int $lineno)
     {
-        parent::__construct(['expr' => $expr], [], $lineno, $tag);
+        parent::__construct(['expr' => $expr], [], $lineno);
     }
 
     public function compile(Compiler $compiler): void
@@ -33,21 +35,39 @@ class DeprecatedNode extends Node
 
         $expr = $this->getNode('expr');
 
-        if ($expr instanceof ConstantExpression) {
-            $compiler->write('@trigger_error(')
-                ->subcompile($expr);
-        } else {
+        if (!$expr instanceof ConstantExpression) {
             $varName = $compiler->getVarName();
-            $compiler->write(sprintf('$%s = ', $varName))
+            $compiler
+                ->write(\sprintf('$%s = ', $varName))
                 ->subcompile($expr)
                 ->raw(";\n")
-                ->write(sprintf('@trigger_error($%s', $varName));
+            ;
+        }
+
+        $compiler->write('trigger_deprecation(');
+        if ($this->hasNode('package')) {
+            $compiler->subcompile($this->getNode('package'));
+        } else {
+            $compiler->raw("''");
+        }
+        $compiler->raw(', ');
+        if ($this->hasNode('version')) {
+            $compiler->subcompile($this->getNode('version'));
+        } else {
+            $compiler->raw("''");
+        }
+        $compiler->raw(', ');
+
+        if ($expr instanceof ConstantExpression) {
+            $compiler->subcompile($expr);
+        } else {
+            $compiler->write(\sprintf('$%s', $varName));
         }
 
         $compiler
             ->raw('.')
-            ->string(sprintf(' ("%s" at line %d).', $this->getTemplateName(), $this->getTemplateLine()))
-            ->raw(", E_USER_DEPRECATED);\n")
+            ->string(\sprintf(' in "%s" at line %d.', $this->getTemplateName(), $this->getTemplateLine()))
+            ->raw(");\n")
         ;
     }
 }

+ 4 - 2
data/web/inc/lib/vendor/twig/twig/src/Node/DoNode.php

@@ -11,6 +11,7 @@
 
 namespace Twig\Node;
 
+use Twig\Attribute\YieldReady;
 use Twig\Compiler;
 use Twig\Node\Expression\AbstractExpression;
 
@@ -19,11 +20,12 @@ use Twig\Node\Expression\AbstractExpression;
  *
  * @author Fabien Potencier <fabien@symfony.com>
  */
+#[YieldReady]
 class DoNode extends Node
 {
-    public function __construct(AbstractExpression $expr, int $lineno, string $tag = null)
+    public function __construct(AbstractExpression $expr, int $lineno)
     {
-        parent::__construct(['expr' => $expr], [], $lineno, $tag);
+        parent::__construct(['expr' => $expr], [], $lineno);
     }
 
     public function compile(Compiler $compiler): void

+ 4 - 2
data/web/inc/lib/vendor/twig/twig/src/Node/EmbedNode.php

@@ -11,6 +11,7 @@
 
 namespace Twig\Node;
 
+use Twig\Attribute\YieldReady;
 use Twig\Compiler;
 use Twig\Node\Expression\AbstractExpression;
 use Twig\Node\Expression\ConstantExpression;
@@ -20,12 +21,13 @@ use Twig\Node\Expression\ConstantExpression;
  *
  * @author Fabien Potencier <fabien@symfony.com>
  */
+#[YieldReady]
 class EmbedNode extends IncludeNode
 {
     // we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module)
-    public function __construct(string $name, int $index, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, string $tag = null)
+    public function __construct(string $name, int $index, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno)
     {
-        parent::__construct(new ConstantExpression('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno, $tag);
+        parent::__construct(new ConstantExpression('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno);
 
         $this->setAttribute('name', $name);
         $this->setAttribute('index', $index);

+ 4 - 0
data/web/inc/lib/vendor/twig/twig/src/Node/Expression/AbstractExpression.php

@@ -21,4 +21,8 @@ use Twig\Node\Node;
  */
 abstract class AbstractExpression extends Node
 {
+    public function isGenerator(): bool
+    {
+        return $this->hasAttribute('is_generator') && $this->getAttribute('is_generator');
+    }
 }

+ 58 - 8
data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ArrayExpression.php

@@ -55,7 +55,7 @@ class ArrayExpression extends AbstractExpression
         return false;
     }
 
-    public function addElement(AbstractExpression $value, AbstractExpression $key = null): void
+    public function addElement(AbstractExpression $value, ?AbstractExpression $key = null): void
     {
         if (null === $key) {
             $key = new ConstantExpression(++$this->index, $value->getTemplateLine());
@@ -66,20 +66,70 @@ class ArrayExpression extends AbstractExpression
 
     public function compile(Compiler $compiler): void
     {
+        $keyValuePairs = $this->getKeyValuePairs();
+        $needsArrayMergeSpread = \PHP_VERSION_ID < 80100 && $this->hasSpreadItem($keyValuePairs);
+
+        if ($needsArrayMergeSpread) {
+            $compiler->raw('CoreExtension::merge(');
+        }
         $compiler->raw('[');
         $first = true;
-        foreach ($this->getKeyValuePairs() as $pair) {
+        $reopenAfterMergeSpread = false;
+        $nextIndex = 0;
+        foreach ($keyValuePairs as $pair) {
+            if ($reopenAfterMergeSpread) {
+                $compiler->raw(', [');
+                $reopenAfterMergeSpread = false;
+            }
+
+            if ($needsArrayMergeSpread && $pair['value']->hasAttribute('spread')) {
+                $compiler->raw('], ')->subcompile($pair['value']);
+                $first = true;
+                $reopenAfterMergeSpread = true;
+                continue;
+            }
             if (!$first) {
                 $compiler->raw(', ');
             }
             $first = false;
 
-            $compiler
-                ->subcompile($pair['key'])
-                ->raw(' => ')
-                ->subcompile($pair['value'])
-            ;
+            if ($pair['value']->hasAttribute('spread') && !$needsArrayMergeSpread) {
+                $compiler->raw('...')->subcompile($pair['value']);
+                ++$nextIndex;
+            } else {
+                $key = $pair['key'] instanceof ConstantExpression ? $pair['key']->getAttribute('value') : null;
+
+                if ($nextIndex !== $key) {
+                    if (\is_int($key)) {
+                        $nextIndex = $key + 1;
+                    }
+                    $compiler
+                        ->subcompile($pair['key'])
+                        ->raw(' => ')
+                    ;
+                } else {
+                    ++$nextIndex;
+                }
+
+                $compiler->subcompile($pair['value']);
+            }
+        }
+        if (!$reopenAfterMergeSpread) {
+            $compiler->raw(']');
         }
-        $compiler->raw(']');
+        if ($needsArrayMergeSpread) {
+            $compiler->raw(')');
+        }
+    }
+
+    private function hasSpreadItem(array $pairs): bool
+    {
+        foreach ($pairs as $pair) {
+            if ($pair['value']->hasAttribute('spread')) {
+                return true;
+            }
+        }
+
+        return false;
     }
 }

+ 2 - 2
data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ArrowFunctionExpression.php

@@ -21,9 +21,9 @@ use Twig\Node\Node;
  */
 class ArrowFunctionExpression extends AbstractExpression
 {
-    public function __construct(AbstractExpression $expr, Node $names, $lineno, $tag = null)
+    public function __construct(AbstractExpression $expr, Node $names, $lineno)
     {
-        parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno, $tag);
+        parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno);
     }
 
     public function compile(Compiler $compiler): void

+ 3 - 3
data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/EndsWithBinary.php

@@ -20,11 +20,11 @@ class EndsWithBinary extends AbstractBinary
         $left = $compiler->getVarName();
         $right = $compiler->getVarName();
         $compiler
-            ->raw(sprintf('(is_string($%s = ', $left))
+            ->raw(\sprintf('(is_string($%s = ', $left))
             ->subcompile($this->getNode('left'))
-            ->raw(sprintf(') && is_string($%s = ', $right))
+            ->raw(\sprintf(') && is_string($%s = ', $right))
             ->subcompile($this->getNode('right'))
-            ->raw(sprintf(') && (\'\' === $%2$s || $%2$s === substr($%1$s, -strlen($%2$s))))', $left, $right))
+            ->raw(\sprintf(') && str_ends_with($%1$s, $%2$s))', $left, $right))
         ;
     }
 

+ 1 - 1
data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/EqualBinary.php

@@ -24,7 +24,7 @@ class EqualBinary extends AbstractBinary
         }
 
         $compiler
-            ->raw('(0 === twig_compare(')
+            ->raw('(0 === CoreExtension::compare(')
             ->subcompile($this->getNode('left'))
             ->raw(', ')
             ->subcompile($this->getNode('right'))

+ 1 - 1
data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/GreaterBinary.php

@@ -24,7 +24,7 @@ class GreaterBinary extends AbstractBinary
         }
 
         $compiler
-            ->raw('(1 === twig_compare(')
+            ->raw('(1 === CoreExtension::compare(')
             ->subcompile($this->getNode('left'))
             ->raw(', ')
             ->subcompile($this->getNode('right'))

+ 1 - 1
data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/GreaterEqualBinary.php

@@ -24,7 +24,7 @@ class GreaterEqualBinary extends AbstractBinary
         }
 
         $compiler
-            ->raw('(0 <= twig_compare(')
+            ->raw('(0 <= CoreExtension::compare(')
             ->subcompile($this->getNode('left'))
             ->raw(', ')
             ->subcompile($this->getNode('right'))

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff