1
0
mirror of https://github.com/mailcow/mailcow-dockerized.git synced 2024-12-23 02:04:46 +02:00

Merge remote-tracking branch 'origin/staging' into nightly

This commit is contained in:
FreddleSpl0it 2024-11-12 15:10:03 +01:00
commit 9542698e95
No known key found for this signature in database
GPG Key ID: 00E14E7634F4BEC5
205 changed files with 7559 additions and 3663 deletions

View File

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

1
.gitignore vendored
View File

@ -46,6 +46,7 @@ data/conf/sogo/custom-theme.js
data/conf/sogo/plist_ldap data/conf/sogo/plist_ldap
data/conf/sogo/plist_ldap.sh data/conf/sogo/plist_ldap.sh
data/conf/sogo/sieve.creds data/conf/sogo/sieve.creds
data/conf/sogo/cron.creds
data/conf/sogo/sogo-full.svg data/conf/sogo/sogo-full.svg
data/gitea/ data/gitea/
data/gogs/ data/gogs/

View File

@ -130,7 +130,7 @@ async def get_containers():
async def post_containers(container_id : str, post_action : str, request: Request): async def post_containers(container_id : str, post_action : str, request: Request):
global dockerapi global dockerapi
try : try:
request_json = await request.json() request_json = await request.json()
except Exception as err: except Exception as err:
request_json = {} request_json = {}

View File

@ -342,6 +342,30 @@ class DockerApi:
cmd = ["/bin/bash", "-c", cmd_vmail] cmd = ["/bin/bash", "-c", cmd_vmail]
maildir_cleanup = container.exec_run(cmd, user='vmail') maildir_cleanup = container.exec_run(cmd, user='vmail')
return self.exec_run_handler('generic', maildir_cleanup) 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 # api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
def container_post__exec__rspamd__worker_password(self, request_json, **kwargs): def container_post__exec__rspamd__worker_password(self, request_json, **kwargs):
if 'container_id' in kwargs: if 'container_id' in kwargs:
@ -374,6 +398,121 @@ class DockerApi:
self.logger.error('failed changing Rspamd password') self.logger.error('failed changing Rspamd password')
res = { 'type': 'danger', 'msg': 'command did not complete' } res = { 'type': 'danger', 'msg': 'command did not complete' }
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: 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 # Collect host stats
async def get_host_stats(self, wait=5): async def get_host_stats(self, wait=5):

View File

@ -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[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[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 -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 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 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 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 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 echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
else 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 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 echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl fts fts_solr notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
fi fi
@ -254,6 +254,8 @@ EOF
# Create random master Password for SOGo SSO # Create random master Password for SOGo SSO
RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1) 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 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
if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then
# Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated # Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated
@ -281,6 +283,17 @@ else
chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem
fi 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 # Compile sieve scripts
sievec /var/vmail/sieve/global_sieve_before.sieve sievec /var/vmail/sieve/global_sieve_before.sieve
sievec /var/vmail/sieve/global_sieve_after.sieve sievec /var/vmail/sieve/global_sieve_after.sieve

View File

@ -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>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$ # 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>.*)$ # renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
ARG IMAGICK_PECL_VERSION=3.7.0 ARG IMAGICK_PECL_VERSION=3.7.0
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$ # 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>.*)$ # renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MEMCACHED_PECL_VERSION=3.2.0 ARG MEMCACHED_PECL_VERSION=3.2.0
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$ # 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>.*)$ # renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
ARG COMPOSER_VERSION=2.6.6 ARG COMPOSER_VERSION=2.6.6

View File

@ -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 cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
fi 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 "$@" exec "$@"

View File

@ -1,8 +1,8 @@
FROM debian:bookworm-slim 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 DEBIAN_FRONTEND=noninteractive
ARG RSPAMD_VER=rspamd_3.9.1-1~82f43560f ARG RSPAMD_VER=rspamd_3.10.2-1~b8a232043
ARG CODENAME=bookworm ARG CODENAME=bookworm
ENV LC_ALL=C ENV LC_ALL=C

View File

@ -33,13 +33,14 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
&& gosu nobody true \ && gosu nobody true \
&& mkdir /usr/share/doc/sogo \ && mkdir /usr/share/doc/sogo \
&& touch /usr/share/doc/sogo/empty.sh \ && 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 \ && 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 \ && apt-get update && apt-get install -y --no-install-recommends \
sogo \ sogo \
sogo-activesync \ sogo-activesync \
&& apt-get autoclean \ && 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 && touch /etc/default/locale
COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh

View File

@ -170,6 +170,8 @@ smtputf8_enable = no
submission_smtpd_tls_mandatory_protocols = >=TLSv1.2 submission_smtpd_tls_mandatory_protocols = >=TLSv1.2
smtps_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 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 # # DO NOT EDIT ANYTHING BELOW #
# Overrides # # Overrides #

View File

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

View File

@ -1,6 +1,6 @@
# Whitelist generated by Postwhite v3.4 on Sun Sep 1 00:19:07 UTC 2024 # Whitelist generated by Postwhite v3.4 on Fri Nov 1 00:18:49 UTC 2024
# https://github.com/stevejenkins/postwhite/ # https://github.com/stevejenkins/postwhite/
# 1994 total rules # 2013 total rules
2a00:1450:4000::/36 permit 2a00:1450:4000::/36 permit
2a01:111:f400::/48 permit 2a01:111:f400::/48 permit
2a01:111:f403:8000::/50 permit 2a01:111:f403:8000::/50 permit
@ -21,7 +21,6 @@
8.25.196.0/23 permit 8.25.196.0/23 permit
8.39.54.0/23 permit 8.39.54.0/23 permit
8.40.222.0/23 permit 8.40.222.0/23 permit
10.162.0.0/16 permit
12.130.86.238 permit 12.130.86.238 permit
13.110.208.0/21 permit 13.110.208.0/21 permit
13.110.209.0/24 permit 13.110.209.0/24 permit
@ -32,11 +31,10 @@
15.200.21.50 permit 15.200.21.50 permit
15.200.44.248 permit 15.200.44.248 permit
15.200.201.185 permit 15.200.201.185 permit
17.41.0.0/16 permit
17.57.155.0/24 permit 17.57.155.0/24 permit
17.57.156.0/24 permit 17.57.156.0/24 permit
17.58.0.0/16 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.156.89.250 permit
18.157.243.190 permit 18.157.243.190 permit
18.194.95.56 permit 18.194.95.56 permit
@ -115,11 +113,15 @@
40.92.0.0/16 permit 40.92.0.0/16 permit
40.107.0.0/16 permit 40.107.0.0/16 permit
40.112.65.63 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 43.228.184.0/22 permit
44.206.138.57 permit 44.206.138.57 permit
44.217.45.156 permit 44.217.45.156 permit
44.236.56.93 permit 44.236.56.93 permit
44.238.220.251 permit 44.238.220.251 permit
45.14.148.0/22 permit
46.19.170.16 permit 46.19.170.16 permit
46.226.48.0/21 permit 46.226.48.0/21 permit
46.228.36.37 permit 46.228.36.37 permit
@ -181,7 +183,9 @@
50.18.126.162 permit 50.18.126.162 permit
50.31.32.0/19 permit 50.31.32.0/19 permit
50.31.36.205 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.1.14.157 permit
52.5.230.59 permit 52.5.230.59 permit
52.27.5.72 permit 52.27.5.72 permit
@ -208,12 +212,12 @@
52.96.223.2 permit 52.96.223.2 permit
52.96.228.130 permit 52.96.228.130 permit
52.96.229.242 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.103.0.0/17 permit
52.119.213.144/28 permit 52.119.213.144/28 permit
52.185.106.240/28 permit 52.185.106.240/28 permit
52.200.59.0/24 permit 52.200.59.0/24 permit
52.205.61.79 permit
52.207.191.216 permit 52.207.191.216 permit
52.222.62.51 permit 52.222.62.51 permit
52.222.73.83 permit 52.222.73.83 permit
@ -225,7 +229,6 @@
52.236.28.240/28 permit 52.236.28.240/28 permit
54.90.148.255 permit 54.90.148.255 permit
54.165.19.38 permit 54.165.19.38 permit
54.172.97.247 permit
54.174.52.0/24 permit 54.174.52.0/24 permit
54.174.57.0/24 permit 54.174.57.0/24 permit
54.174.59.0/24 permit 54.174.59.0/24 permit
@ -242,16 +245,12 @@
54.244.54.130 permit 54.244.54.130 permit
54.244.242.0/24 permit 54.244.242.0/24 permit
54.255.61.23 permit 54.255.61.23 permit
57.103.64.0/18 permit
62.13.128.0/24 permit 62.13.128.0/24 permit
62.13.128.196 permit
62.13.129.128/25 permit 62.13.129.128/25 permit
62.13.136.0/22 permit 62.13.136.0/21 permit
62.13.140.0/22 permit 62.13.144.0/21 permit
62.13.144.0/22 permit 62.13.152.0/21 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.17.146.128/26 permit 62.17.146.128/26 permit
62.179.121.0/24 permit 62.179.121.0/24 permit
62.201.172.0/27 permit 62.201.172.0/27 permit
@ -273,7 +272,6 @@
64.127.115.252 permit 64.127.115.252 permit
64.132.88.0/23 permit 64.132.88.0/23 permit
64.132.92.0/24 permit 64.132.92.0/24 permit
64.147.123.128/27 permit
64.207.219.7 permit 64.207.219.7 permit
64.207.219.8 permit 64.207.219.8 permit
64.207.219.9 permit 64.207.219.9 permit
@ -1318,7 +1316,9 @@
129.41.77.70 permit 129.41.77.70 permit
129.41.169.249 permit 129.41.169.249 permit
129.80.5.164 permit 129.80.5.164 permit
129.80.64.36 permit
129.80.67.121 permit 129.80.67.121 permit
129.80.145.156 permit
129.145.74.12 permit 129.145.74.12 permit
129.146.88.28 permit 129.146.88.28 permit
129.146.147.105 permit 129.146.147.105 permit
@ -1329,6 +1329,9 @@
129.153.168.146 permit 129.153.168.146 permit
129.153.190.200 permit 129.153.190.200 permit
129.153.194.228 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.159.87.137 permit
129.213.195.191 permit 129.213.195.191 permit
130.61.9.72 permit 130.61.9.72 permit
@ -1352,6 +1355,7 @@
135.84.216.0/22 permit 135.84.216.0/22 permit
136.143.160.0/24 permit 136.143.160.0/24 permit
136.143.161.0/24 permit 136.143.161.0/24 permit
136.143.162.0/24 permit
136.143.178.49 permit 136.143.178.49 permit
136.143.182.0/23 permit 136.143.182.0/23 permit
136.143.184.0/24 permit 136.143.184.0/24 permit
@ -1373,6 +1377,7 @@
139.138.58.119 permit 139.138.58.119 permit
139.167.79.86 permit 139.167.79.86 permit
139.180.17.0/24 permit 139.180.17.0/24 permit
140.238.148.191 permit
141.148.159.229 permit 141.148.159.229 permit
141.193.32.0/23 permit 141.193.32.0/23 permit
141.193.184.32/27 permit 141.193.184.32/27 permit
@ -1381,6 +1386,7 @@
141.193.185.32/27 permit 141.193.185.32/27 permit
141.193.185.64/26 permit 141.193.185.64/26 permit
141.193.185.128/25 permit 141.193.185.128/25 permit
143.47.120.152 permit
143.55.224.0/21 permit 143.55.224.0/21 permit
143.55.232.0/22 permit 143.55.232.0/22 permit
143.55.236.0/22 permit 143.55.236.0/22 permit
@ -1394,7 +1400,10 @@
144.178.38.0/24 permit 144.178.38.0/24 permit
145.253.228.160/29 permit 145.253.228.160/29 permit
145.253.239.128/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.112.0/26 permit
146.20.113.0/24 permit 146.20.113.0/24 permit
146.20.191.0/24 permit 146.20.191.0/24 permit
@ -1413,10 +1422,14 @@
149.72.248.236 permit 149.72.248.236 permit
149.97.173.180 permit 149.97.173.180 permit
150.230.98.160 permit 150.230.98.160 permit
151.145.38.14 permit
152.67.105.195 permit 152.67.105.195 permit
152.69.200.236 permit 152.69.200.236 permit
152.70.155.126 permit 152.70.155.126 permit
155.248.208.51 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.0.192/26 permit
157.55.1.128/26 permit 157.55.1.128/26 permit
157.55.2.0/25 permit 157.55.2.0/25 permit
@ -1497,6 +1510,7 @@
167.220.67.232/29 permit 167.220.67.232/29 permit
168.138.5.36 permit 168.138.5.36 permit
168.138.73.51 permit 168.138.73.51 permit
168.138.77.31 permit
168.245.0.0/17 permit 168.245.0.0/17 permit
168.245.12.252 permit 168.245.12.252 permit
168.245.46.9 permit 168.245.46.9 permit
@ -1519,6 +1533,7 @@
172.217.192.0/19 permit 172.217.192.0/19 permit
172.253.56.0/21 permit 172.253.56.0/21 permit
172.253.112.0/20 permit 172.253.112.0/20 permit
173.0.84.0/29 permit
173.0.84.224/27 permit 173.0.84.224/27 permit
173.0.94.244/30 permit 173.0.94.244/30 permit
173.194.0.0/16 permit 173.194.0.0/16 permit
@ -1537,7 +1552,6 @@
174.36.114.148/30 permit 174.36.114.148/30 permit
174.36.114.152/29 permit 174.36.114.152/29 permit
174.37.67.28/30 permit 174.37.67.28/30 permit
174.129.203.189 permit
175.41.215.51 permit 175.41.215.51 permit
176.32.105.0/24 permit 176.32.105.0/24 permit
176.32.127.0/24 permit 176.32.127.0/24 permit
@ -1610,6 +1624,8 @@
188.172.128.0/20 permit 188.172.128.0/20 permit
192.0.64.0/18 permit 192.0.64.0/18 permit
192.18.139.154 permit 192.18.139.154 permit
192.18.145.36 permit
192.18.152.58 permit
192.30.252.0/22 permit 192.30.252.0/22 permit
192.161.144.0/20 permit 192.161.144.0/20 permit
192.162.87.0/24 permit 192.162.87.0/24 permit
@ -1677,6 +1693,7 @@
199.122.123.0/24 permit 199.122.123.0/24 permit
199.127.232.0/22 permit 199.127.232.0/22 permit
199.255.192.0/22 permit 199.255.192.0/22 permit
202.12.124.128/27 permit
202.129.242.0/23 permit 202.129.242.0/23 permit
202.165.102.47 permit 202.165.102.47 permit
202.177.148.100 permit 202.177.148.100 permit
@ -1729,7 +1746,9 @@
204.92.114.204/31 permit 204.92.114.204/31 permit
204.141.32.0/23 permit 204.141.32.0/23 permit
204.141.42.0/23 permit 204.141.42.0/23 permit
204.220.160.0/20 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 204.232.168.0/24 permit
205.139.110.0/24 permit 205.139.110.0/24 permit
205.201.128.0/20 permit 205.201.128.0/20 permit

View File

@ -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' # Extensions that are treated as 'bad'
# Number is score multiply factor # Number is score multiply factor
bad_extensions = { bad_extensions = {
scr = 20, apk = 4,
lnk = 20, appx = 4,
exe = 20, appxbundle = 4,
msi = 1, bat = 8,
msp = 1,
msu = 1,
jar = 2,
com = 20,
bat = 4,
cmd = 4,
ps1 = 4,
ace = 4,
arj = 4,
cab = 20, 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, vbs = 20,
hta = 4, vhd = 4,
shs = 4, py = 4,
wsc = 4, reg = 8,
wsf = 4, scf = 8,
iso = 8, vhdx = 4,
img = 8
}; };
# Extensions that are particularly penalized for archives # Extensions that are particularly penalized for archives
@ -30,18 +48,14 @@ bad_archive_extensions = {
docx = 0.5, docx = 0.5,
xlsx = 0.5, xlsx = 0.5,
pdf = 1.0, pdf = 1.0,
jar = 3, jar = 12,
js = 0.5, jnlp = 12,
vbs = 20, bat = 12,
exe = 20 cmd = 12,
}; };
# Used to detect another archive in archive # Used to detect another archive in archive
archive_extensions = { archive_extensions = {
zip = 1, tar = 1,
arj = 1, gz = 1,
rar = 1,
ace = 1,
7z = 1,
cab = 1
}; };

View File

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

View File

@ -3285,6 +3285,197 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
return true; return true;
break; 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_from_template': case 'mailbox_from_template':
$stmt = $pdo->prepare("SELECT * FROM `templates` $stmt = $pdo->prepare("SELECT * FROM `templates`
WHERE `template` = :template AND type = 'mailbox'"); WHERE `template` = :template AND type = 'mailbox'");

View File

@ -1896,16 +1896,16 @@
}, },
{ {
"name": "symfony/deprecation-contracts", "name": "symfony/deprecation-contracts",
"version": "v3.4.0", "version": "v3.5.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git", "url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1914,7 +1914,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "3.4-dev" "dev-main": "3.5-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/contracts", "name": "symfony/contracts",
@ -1943,7 +1943,7 @@
"description": "A generic function and convention to trigger deprecation notices", "description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
}, },
"funding": [ "funding": [
{ {
@ -1959,7 +1959,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2023-05-23T14:45:45+00:00" "time": "2024-04-18T09:32:20+00:00"
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
@ -2203,6 +2203,82 @@
], ],
"time": "2024-01-29T20:11:03+00:00" "time": "2024-01-29T20:11:03+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", "name": "symfony/translation",
"version": "v6.4.3", "version": "v6.4.3",
@ -2517,34 +2593,37 @@
}, },
{ {
"name": "twig/twig", "name": "twig/twig",
"version": "v3.4.3", "version": "v3.14.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/twigphp/Twig.git", "url": "https://github.com/twigphp/Twig.git",
"reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58" "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/c38fd6b0b7f370c198db91ffd02e23b517426b58", "url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
"reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58", "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=7.2.5", "php": ">=8.0.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "^1.8", "symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3" "symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-php81": "^1.29"
}, },
"require-dev": { "require-dev": {
"psr/container": "^1.0", "psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
}, },
"type": "library", "type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.4-dev"
}
},
"autoload": { "autoload": {
"files": [
"src/Resources/core.php",
"src/Resources/debug.php",
"src/Resources/escaper.php",
"src/Resources/string_loader.php"
],
"psr-4": { "psr-4": {
"Twig\\": "src/" "Twig\\": "src/"
} }
@ -2577,7 +2656,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/twigphp/Twig/issues", "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": [ "funding": [
{ {
@ -2589,7 +2668,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-09-28T08:42:51+00:00" "time": "2024-09-09T17:55:12+00:00"
} }
], ],
"packages-dev": [], "packages-dev": [],

View File

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

View File

@ -14,8 +14,14 @@ return array(
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php', 'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php', '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php',
'fe62ba7e10580d903cc46d808b5961a4' => $vendorDir . '/tightenco/collect/src/Collect/Support/helpers.php', 'fe62ba7e10580d903cc46d808b5961a4' => $vendorDir . '/tightenco/collect/src/Collect/Support/helpers.php',
'caf31cc6ec7cf2241cb6f12c226c3846' => $vendorDir . '/tightenco/collect/src/Collect/Support/alias.php', 'caf31cc6ec7cf2241cb6f12c226c3846' => $vendorDir . '/tightenco/collect/src/Collect/Support/alias.php',
'04c6c5c2f7095ccf6c481d3e53e1776f' => $vendorDir . '/mustangostang/spyc/Spyc.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',
); );

View File

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

View File

@ -15,10 +15,16 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php', 'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php',
'667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php', '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php',
'fe62ba7e10580d903cc46d808b5961a4' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/helpers.php', 'fe62ba7e10580d903cc46d808b5961a4' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/helpers.php',
'caf31cc6ec7cf2241cb6f12c226c3846' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/alias.php', 'caf31cc6ec7cf2241cb6f12c226c3846' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/alias.php',
'04c6c5c2f7095ccf6c481d3e53e1776f' => __DIR__ . '/..' . '/mustangostang/spyc/Spyc.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 ( public static $prefixLengthsPsr4 = array (
@ -29,6 +35,7 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
), ),
'S' => 'S' =>
array ( array (
'Symfony\\Polyfill\\Php81\\' => 23,
'Symfony\\Polyfill\\Php80\\' => 23, 'Symfony\\Polyfill\\Php80\\' => 23,
'Symfony\\Polyfill\\Mbstring\\' => 26, 'Symfony\\Polyfill\\Mbstring\\' => 26,
'Symfony\\Polyfill\\Ctype\\' => 23, 'Symfony\\Polyfill\\Ctype\\' => 23,
@ -100,6 +107,10 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
array ( array (
0 => __DIR__ . '/..' . '/tightenco/collect/src/Collect', 0 => __DIR__ . '/..' . '/tightenco/collect/src/Collect',
), ),
'Symfony\\Polyfill\\Php81\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php81',
),
'Symfony\\Polyfill\\Php80\\' => 'Symfony\\Polyfill\\Php80\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80', 0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
@ -231,8 +242,9 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
public static $classMap = array ( public static $classMap = array (
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', 'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'CURLStringFile' => __DIR__ . '/..' . '/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', 'ReturnTypeWillChange' => __DIR__ . '/..' . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php',
'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', 'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', 'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', 'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',

View File

@ -1961,27 +1961,27 @@
}, },
{ {
"name": "symfony/deprecation-contracts", "name": "symfony/deprecation-contracts",
"version": "v3.4.0", "version": "v3.5.0",
"version_normalized": "3.4.0.0", "version_normalized": "3.5.0.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git", "url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=8.1" "php": ">=8.1"
}, },
"time": "2023-05-23T14:45:45+00:00", "time": "2024-04-18T09:32:20+00:00",
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "3.4-dev" "dev-main": "3.5-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/contracts", "name": "symfony/contracts",
@ -2011,7 +2011,7 @@
"description": "A generic function and convention to trigger deprecation notices", "description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
}, },
"funding": [ "funding": [
{ {
@ -2280,6 +2280,85 @@
], ],
"install-path": "../symfony/polyfill-php80" "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", "name": "symfony/translation",
"version": "v6.4.3", "version": "v6.4.3",
@ -2606,37 +2685,40 @@
}, },
{ {
"name": "twig/twig", "name": "twig/twig",
"version": "v3.4.3", "version": "v3.14.0",
"version_normalized": "3.4.3.0", "version_normalized": "3.14.0.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/twigphp/Twig.git", "url": "https://github.com/twigphp/Twig.git",
"reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58" "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/c38fd6b0b7f370c198db91ffd02e23b517426b58", "url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
"reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58", "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=7.2.5", "php": ">=8.0.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "^1.8", "symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3" "symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-php81": "^1.29"
}, },
"require-dev": { "require-dev": {
"psr/container": "^1.0", "psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.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", "type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.4-dev"
}
},
"installation-source": "dist", "installation-source": "dist",
"autoload": { "autoload": {
"files": [
"src/Resources/core.php",
"src/Resources/debug.php",
"src/Resources/escaper.php",
"src/Resources/string_loader.php"
],
"psr-4": { "psr-4": {
"Twig\\": "src/" "Twig\\": "src/"
} }
@ -2669,7 +2751,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/twigphp/Twig/issues", "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": [ "funding": [
{ {

View File

@ -3,7 +3,7 @@
'name' => '__root__', 'name' => '__root__',
'pretty_version' => 'dev-master', 'pretty_version' => 'dev-master',
'version' => 'dev-master', 'version' => 'dev-master',
'reference' => '40146839efb3754b2db4045f0111178ffd1883c5', 'reference' => '220fdbb168792c07493db330d898b345cc902055',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@ -13,7 +13,7 @@
'__root__' => array( '__root__' => array(
'pretty_version' => 'dev-master', 'pretty_version' => 'dev-master',
'version' => 'dev-master', 'version' => 'dev-master',
'reference' => '40146839efb3754b2db4045f0111178ffd1883c5', 'reference' => '220fdbb168792c07493db330d898b345cc902055',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@ -308,9 +308,9 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'symfony/deprecation-contracts' => array( 'symfony/deprecation-contracts' => array(
'pretty_version' => 'v3.4.0', 'pretty_version' => 'v3.5.0',
'version' => '3.4.0.0', 'version' => '3.5.0.0',
'reference' => '7c3aff79d10325257a001fcf92d991f24fc967cf', 'reference' => '0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/deprecation-contracts', 'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
'aliases' => array(), 'aliases' => array(),
@ -343,6 +343,15 @@
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, '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( 'symfony/translation' => array(
'pretty_version' => 'v6.4.3', 'pretty_version' => 'v6.4.3',
'version' => '6.4.3.0', 'version' => '6.4.3.0',
@ -386,9 +395,9 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'twig/twig' => array( 'twig/twig' => array(
'pretty_version' => 'v3.4.3', 'pretty_version' => 'v3.14.0',
'version' => '3.4.3.0', 'version' => '3.14.0.0',
'reference' => 'c38fd6b0b7f370c198db91ffd02e23b517426b58', 'reference' => '126b2c97818dbff0cdf3fbfc881aedb3d40aae72',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../twig/twig', 'install_path' => __DIR__ . '/../twig/twig',
'aliases' => array(), 'aliases' => array(),

View File

@ -25,7 +25,7 @@
"minimum-stability": "dev", "minimum-stability": "dev",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "3.4-dev" "dev-main": "3.5-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/contracts", "name": "symfony/contracts",

View File

@ -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.

View File

@ -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;
}
}

View File

@ -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).

View File

@ -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;
}
}
}

View File

@ -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()
{
}
}
}

View File

@ -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; }
}

View File

@ -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"
}
}
}

View File

@ -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

View File

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

View File

@ -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"

View File

@ -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/'

View File

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

View File

@ -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__))
;

View File

@ -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) # 3.4.3 (2022-09-28)
* Fix a security issue on filesystem loader (possibility to load a template outside a configured directory) * Fix a security issue on filesystem loader (possibility to load a template outside a configured directory)
@ -141,7 +316,7 @@
* removed Parser::isReservedMacroName() * removed Parser::isReservedMacroName()
* removed SanboxedPrintNode * removed SanboxedPrintNode
* removed Node::setTemplateName() * removed Node::setTemplateName()
* made classes maked as "@final" final * made classes marked as "@final" final
* removed InitRuntimeInterface, ExistsLoaderInterface, and SourceContextLoaderInterface * removed InitRuntimeInterface, ExistsLoaderInterface, and SourceContextLoaderInterface
* removed the "spaceless" tag * removed the "spaceless" tag
* removed Twig\Environment::getBaseTemplateClass() and Twig\Environment::setBaseTemplateClass() * removed Twig\Environment::getBaseTemplateClass() and Twig\Environment::setBaseTemplateClass()

View File

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

View File

@ -11,7 +11,7 @@ Sponsors
.. raw:: html .. 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"> <img src="https://static.blackfire.io/assets/intemporals/logo/png/blackfire-io_secondary_horizontal_transparent.png?1" width="255px" alt="Blackfire.io">
</a> </a>

View File

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

View File

@ -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);
}
}

View File

@ -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
{
}

View File

@ -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
{
}

View File

@ -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));
}
}

View File

@ -50,11 +50,11 @@ class FilesystemCache implements CacheInterface
if (false === @mkdir($dir, 0777, true)) { if (false === @mkdir($dir, 0777, true)) {
clearstatcache(true, $dir); clearstatcache(true, $dir);
if (!is_dir($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)) { } 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)); $tmpFile = tempnam($dir, basename($key));
@ -63,7 +63,7 @@ class FilesystemCache implements CacheInterface
if (self::FORCE_BYTECODE_INVALIDATION == ($this->options & self::FORCE_BYTECODE_INVALIDATION)) { if (self::FORCE_BYTECODE_INVALIDATION == ($this->options & self::FORCE_BYTECODE_INVALIDATION)) {
// Compile cached file into bytecode cache // 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); @opcache_invalidate($key, true);
} elseif (\function_exists('apc_compile_file')) { } elseif (\function_exists('apc_compile_file')) {
apc_compile_file($key); apc_compile_file($key);
@ -73,7 +73,7 @@ class FilesystemCache implements CacheInterface
return; 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 public function getTimestamp(string $key): int

View File

@ -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.
}
}

View File

@ -22,15 +22,16 @@ class Compiler
private $lastLine; private $lastLine;
private $source; private $source;
private $indentation; private $indentation;
private $env;
private $debugInfo = []; private $debugInfo = [];
private $sourceOffset; private $sourceOffset;
private $sourceLine; private $sourceLine;
private $varNameSalt = 0; private $varNameSalt = 0;
private $didUseEcho = false;
private $didUseEchoStack = [];
public function __construct(Environment $env) public function __construct(
{ private Environment $env,
$this->env = $env; ) {
} }
public function getEnvironment(): Environment public function getEnvironment(): Environment
@ -46,7 +47,7 @@ class Compiler
/** /**
* @return $this * @return $this
*/ */
public function compile(Node $node, int $indentation = 0) public function reset(int $indentation = 0)
{ {
$this->lastLine = null; $this->lastLine = null;
$this->source = ''; $this->source = '';
@ -57,9 +58,29 @@ class Compiler
$this->indentation = $indentation; $this->indentation = $indentation;
$this->varNameSalt = 0; $this->varNameSalt = 0;
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); $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; return $this;
} finally {
$this->didUseEcho = array_pop($this->didUseEchoStack);
}
} }
/** /**
@ -67,13 +88,24 @@ class Compiler
*/ */
public function subcompile(Node $node, bool $raw = true) public function subcompile(Node $node, bool $raw = true)
{ {
if (false === $raw) { if (!$raw) {
$this->source .= str_repeat(' ', $this->indentation * 4); $this->source .= str_repeat(' ', $this->indentation * 4);
} }
$this->didUseEchoStack[] = $this->didUseEcho;
try {
$this->didUseEcho = false;
$node->compile($this); $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; return $this;
} finally {
$this->didUseEcho = array_pop($this->didUseEchoStack);
}
} }
/** /**
@ -83,6 +115,7 @@ class Compiler
*/ */
public function raw(string $string) public function raw(string $string)
{ {
$this->checkForEcho($string);
$this->source .= $string; $this->source .= $string;
return $this; return $this;
@ -96,6 +129,7 @@ class Compiler
public function write(...$strings) public function write(...$strings)
{ {
foreach ($strings as $string) { foreach ($strings as $string) {
$this->checkForEcho($string);
$this->source .= str_repeat(' ', $this->indentation * 4).$string; $this->source .= str_repeat(' ', $this->indentation * 4).$string;
} }
@ -109,7 +143,7 @@ class Compiler
*/ */
public function string(string $value) public function string(string $value)
{ {
$this->source .= sprintf('"%s"', addcslashes($value, "\0\t\"\$\\")); $this->source .= \sprintf('"%s"', addcslashes($value, "\0\t\"\$\\"));
return $this; return $this;
} }
@ -161,7 +195,7 @@ class Compiler
public function addDebugInfo(Node $node) public function addDebugInfo(Node $node)
{ {
if ($node->getTemplateLine() != $this->lastLine) { 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->sourceLine += substr_count($this->source, "\n", $this->sourceOffset);
$this->sourceOffset = \strlen($this->source); $this->sourceOffset = \strlen($this->source);
@ -209,6 +243,15 @@ class Compiler
public function getVarName(): string 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;
} }
} }

View File

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

View File

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

View File

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

View File

@ -12,20 +12,21 @@
namespace Twig; namespace Twig;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Error\SyntaxError; use Twig\Error\SyntaxError;
use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ArrayExpression;
use Twig\Node\Expression\ArrowFunctionExpression; use Twig\Node\Expression\ArrowFunctionExpression;
use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\AssignNameExpression;
use Twig\Node\Expression\Binary\AbstractBinary;
use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\Binary\ConcatBinary;
use Twig\Node\Expression\BlockReferenceExpression;
use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConditionalExpression;
use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\MethodCallExpression; use Twig\Node\Expression\MethodCallExpression;
use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\ParentExpression;
use Twig\Node\Expression\TestExpression; use Twig\Node\Expression\TestExpression;
use Twig\Node\Expression\Unary\AbstractUnary;
use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\NegUnary;
use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\NotUnary;
use Twig\Node\Expression\Unary\PosUnary; use Twig\Node\Expression\Unary\PosUnary;
@ -40,23 +41,22 @@ use Twig\Node\Node;
* @see https://en.wikipedia.org/wiki/Operator-precedence_parser * @see https://en.wikipedia.org/wiki/Operator-precedence_parser
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/ */
class ExpressionParser class ExpressionParser
{ {
public const OPERATOR_LEFT = 1; public const OPERATOR_LEFT = 1;
public const OPERATOR_RIGHT = 2; public const OPERATOR_RIGHT = 2;
private $parser; /** @var array<string, array{precedence: int, class: class-string<AbstractUnary>}> */
private $env;
private $unaryOperators; private $unaryOperators;
/** @var array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: self::OPERATOR_*}> */
private $binaryOperators; private $binaryOperators;
private $readyNodes = [];
public function __construct(Parser $parser, Environment $env) public function __construct(
{ private Parser $parser,
$this->parser = $parser; private Environment $env,
$this->env = $env; ) {
$this->unaryOperators = $env->getUnaryOperators(); $this->unaryOperators = $env->getUnaryOperators();
$this->binaryOperators = $env->getBinaryOperators(); $this->binaryOperators = $env->getBinaryOperators();
} }
@ -80,7 +80,7 @@ class ExpressionParser
} elseif (isset($op['callable'])) { } elseif (isset($op['callable'])) {
$expr = $op['callable']($this->parser, $expr); $expr = $op['callable']($this->parser, $expr);
} else { } 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']; $class = $op['class'];
$expr = new $class($expr, $expr1, $token->getLine()); $expr = new $class($expr, $expr1, $token->getLine());
} }
@ -103,52 +103,52 @@ class ExpressionParser
$stream = $this->parser->getStream(); $stream = $this->parser->getStream();
// short array syntax (one argument, no parentheses)? // 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(); $line = $stream->getCurrent()->getLine();
$token = $stream->expect(/* Token::NAME_TYPE */ 5); $token = $stream->expect(Token::NAME_TYPE);
$names = [new AssignNameExpression($token->getValue(), $token->getLine())]; $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); return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
} }
// first, determine if we are parsing an arrow function by finding => (long form) // first, determine if we are parsing an arrow function by finding => (long form)
$i = 0; $i = 0;
if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, '(')) {
return null; return null;
} }
++$i; ++$i;
while (true) { while (true) {
// variable name // variable name
++$i; ++$i;
if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, ',')) { if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ',')) {
break; break;
} }
++$i; ++$i;
} }
if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) { if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ')')) {
return null; return null;
} }
++$i; ++$i;
if (!$stream->look($i)->test(/* Token::ARROW_TYPE */ 12)) { if (!$stream->look($i)->test(Token::ARROW_TYPE)) {
return null; return null;
} }
// yes, let's parse it properly // yes, let's parse it properly
$token = $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '('); $token = $stream->expect(Token::PUNCTUATION_TYPE, '(');
$line = $token->getLine(); $line = $token->getLine();
$names = []; $names = [];
while (true) { while (true) {
$token = $stream->expect(/* Token::NAME_TYPE */ 5); $token = $stream->expect(Token::NAME_TYPE);
$names[] = new AssignNameExpression($token->getValue(), $token->getLine()); $names[] = new AssignNameExpression($token->getValue(), $token->getLine());
if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break; break;
} }
} }
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ')'); $stream->expect(Token::PUNCTUATION_TYPE, ')');
$stream->expect(/* Token::ARROW_TYPE */ 12); $stream->expect(Token::ARROW_TYPE);
return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line); return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
} }
@ -164,10 +164,10 @@ class ExpressionParser
$class = $operator['class']; $class = $operator['class'];
return $this->parsePostfixExpression(new $class($expr, $token->getLine())); 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(); $this->parser->getStream()->next();
$expr = $this->parseExpression(); $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); return $this->parsePostfixExpression($expr);
} }
@ -177,15 +177,18 @@ class ExpressionParser
private function parseConditionalExpression($expr): AbstractExpression private function parseConditionalExpression($expr): AbstractExpression
{ {
while ($this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, '?')) { while ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, '?')) {
if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) { if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) {
$expr2 = $this->parseExpression(); $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(); $expr3 = $this->parseExpression();
} else { } else {
// Ternary without else (expr ? expr2)
$expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine()); $expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine());
} }
} else { } else {
// Ternary without then (expr ?: expr3)
$expr2 = $expr; $expr2 = $expr;
$expr3 = $this->parseExpression(); $expr3 = $this->parseExpression();
} }
@ -198,19 +201,19 @@ class ExpressionParser
private function isUnary(Token $token): bool 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 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() public function parsePrimaryExpression()
{ {
$token = $this->parser->getCurrentToken(); $token = $this->parser->getCurrentToken();
switch ($token->getType()) { switch ($token->getType()) {
case /* Token::NAME_TYPE */ 5: case Token::NAME_TYPE:
$this->parser->getStream()->next(); $this->parser->getStream()->next();
switch ($token->getValue()) { switch ($token->getValue()) {
case 'true': case 'true':
@ -239,17 +242,17 @@ class ExpressionParser
} }
break; break;
case /* Token::NUMBER_TYPE */ 6: case Token::NUMBER_TYPE:
$this->parser->getStream()->next(); $this->parser->getStream()->next();
$node = new ConstantExpression($token->getValue(), $token->getLine()); $node = new ConstantExpression($token->getValue(), $token->getLine());
break; break;
case /* Token::STRING_TYPE */ 7: case Token::STRING_TYPE:
case /* Token::INTERPOLATION_START_TYPE */ 10: case Token::INTERPOLATION_START_TYPE:
$node = $this->parseStringExpression(); $node = $this->parseStringExpression();
break; break;
case /* Token::OPERATOR_TYPE */ 8: case Token::OPERATOR_TYPE:
if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) { if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) {
// in this context, string operators are variable names // in this context, string operators are variable names
$this->parser->getStream()->next(); $this->parser->getStream()->next();
@ -260,7 +263,7 @@ class ExpressionParser
if (isset($this->unaryOperators[$token->getValue()])) { if (isset($this->unaryOperators[$token->getValue()])) {
$class = $this->unaryOperators[$token->getValue()]['class']; $class = $this->unaryOperators[$token->getValue()]['class'];
if (!\in_array($class, [NegUnary::class, PosUnary::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(); $this->parser->getStream()->next();
@ -272,14 +275,14 @@ class ExpressionParser
// no break // no break
default: default:
if ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '[')) { if ($token->test(Token::PUNCTUATION_TYPE, '[')) {
$node = $this->parseArrayExpression(); $node = $this->parseSequenceExpression();
} elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '{')) { } elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) {
$node = $this->parseHashExpression(); $node = $this->parseMappingExpression();
} elseif ($token->test(/* Token::OPERATOR_TYPE */ 8, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { } 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()); 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 { } 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 // a string cannot be followed by another string in a single expression
$nextCanBeString = true; $nextCanBeString = true;
while (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()); $nodes[] = new ConstantExpression($token->getValue(), $token->getLine());
$nextCanBeString = false; $nextCanBeString = false;
} elseif ($stream->nextIf(/* Token::INTERPOLATION_START_TYPE */ 10)) { } elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) {
$nodes[] = $this->parseExpression(); $nodes[] = $this->parseExpression();
$stream->expect(/* Token::INTERPOLATION_END_TYPE */ 11); $stream->expect(Token::INTERPOLATION_END_TYPE);
$nextCanBeString = true; $nextCanBeString = true;
} else { } else {
break; break;
@ -314,56 +317,91 @@ class ExpressionParser
return $expr; return $expr;
} }
/**
* @deprecated since 3.11, use parseSequenceExpression() instead
*/
public function parseArrayExpression() 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 = $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()); $node = new ArrayExpression([], $stream->getCurrent()->getLine());
$first = true; $first = true;
while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) { while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) {
if (!$first) { 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 ,? // trailing ,?
if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) { if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
break; break;
} }
} }
$first = false; $first = false;
if ($stream->test(Token::SPREAD_TYPE)) {
$stream->next();
$expr = $this->parseExpression();
$expr->setAttribute('spread', true);
$node->addElement($expr);
} else {
$node->addElement($this->parseExpression()); $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; return $node;
} }
/**
* @deprecated since 3.11, use parseMappingExpression() instead
*/
public function parseHashExpression() 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 = $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()); $node = new ArrayExpression([], $stream->getCurrent()->getLine());
$first = true; $first = true;
while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) { while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) {
if (!$first) { 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 ,? // trailing ,?
if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) { if ($stream->test(Token::PUNCTUATION_TYPE, '}')) {
break; break;
} }
} }
$first = false; $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 number -- 12
// * a string -- 'a' // * a string -- 'a'
// * a name, which is equivalent to a string -- a // * a name, which is equivalent to a string -- a
// * an expression, which must be enclosed in parentheses -- (1 + 2) // * 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()); $key = new ConstantExpression($token->getValue(), $token->getLine());
// {a} is a shortcut for {a:a} // {a} is a shortcut for {a:a}
@ -372,22 +410,22 @@ class ExpressionParser
$node->addElement($value, $key); $node->addElement($value, $key);
continue; 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()); $key = new ConstantExpression($token->getValue(), $token->getLine());
} elseif ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { } elseif ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
$key = $this->parseExpression(); $key = $this->parseExpression();
} else { } else {
$current = $stream->getCurrent(); $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(); $value = $this->parseExpression();
$node->addElement($value, $key); $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; return $node;
} }
@ -396,7 +434,7 @@ class ExpressionParser
{ {
while (true) { while (true) {
$token = $this->parser->getCurrentToken(); $token = $this->parser->getCurrentToken();
if (/* Token::PUNCTUATION_TYPE */ 9 == $token->getType()) { if (Token::PUNCTUATION_TYPE == $token->getType()) {
if ('.' == $token->getValue() || '[' == $token->getValue()) { if ('.' == $token->getValue() || '[' == $token->getValue()) {
$node = $this->parseSubscriptExpression($node); $node = $this->parseSubscriptExpression($node);
} elseif ('|' == $token->getValue()) { } elseif ('|' == $token->getValue()) {
@ -414,33 +452,6 @@ class ExpressionParser
public function getFunctionNode($name, $line) 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 (!$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());
}
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 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());
}
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)) { if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
$arguments = new ArrayExpression([], $line); $arguments = new ArrayExpression([], $line);
foreach ($this->parseArguments() as $n) { foreach ($this->parseArguments() as $n) {
@ -454,10 +465,24 @@ class ExpressionParser
} }
$args = $this->parseArguments(true); $args = $this->parseArguments(true);
$class = $this->getFunctionNodeClass($name, $line); $function = $this->getFunction($name, $line);
return new $class($name, $args, $line); if ($function->getParserCallable()) {
$fakeNode = new Node(lineno: $line);
$fakeNode->setSourceContext($this->parser->getStream()->getSourceContext());
return ($function->getParserCallable())($this->parser, $fakeNode, $args, $line);
} }
if (!isset($this->readyNodes[$class = $function->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 "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) public function parseSubscriptExpression($node)
@ -470,29 +495,25 @@ class ExpressionParser
if ('.' == $token->getValue()) { if ('.' == $token->getValue()) {
$token = $stream->next(); $token = $stream->next();
if ( 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); $arg = new ConstantExpression($token->getValue(), $lineno);
if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
$type = Template::METHOD_CALL; $type = Template::METHOD_CALL;
foreach ($this->parseArguments() as $n) { foreach ($this->parseArguments() as $n) {
$arguments->addElement($n); $arguments->addElement($n);
} }
} }
} else { } 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 ($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'); $name = $arg->getAttribute('value');
$node = new MethodCallExpression($node, 'macro_'.$name, $arguments, $lineno); $node = new MethodCallExpression($node, 'macro_'.$name, $arguments, $lineno);
@ -505,34 +526,34 @@ class ExpressionParser
// slice? // slice?
$slice = false; $slice = false;
if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ':')) { if ($stream->test(Token::PUNCTUATION_TYPE, ':')) {
$slice = true; $slice = true;
$arg = new ConstantExpression(0, $token->getLine()); $arg = new ConstantExpression(0, $token->getLine());
} else { } else {
$arg = $this->parseExpression(); $arg = $this->parseExpression();
} }
if ($stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) { if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) {
$slice = true; $slice = true;
} }
if ($slice) { if ($slice) {
if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) { if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
$length = new ConstantExpression(null, $token->getLine()); $length = new ConstantExpression(null, $token->getLine());
} else { } else {
$length = $this->parseExpression(); $length = $this->parseExpression();
} }
$class = $this->getFilterNodeClass('slice', $token->getLine()); $filter = $this->getFilter('slice', $token->getLine());
$arguments = new Node([$arg, $length]); $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; return $filter;
} }
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']'); $stream->expect(Token::PUNCTUATION_TYPE, ']');
} }
return new GetAttrExpression($node, $arg, $arguments, $type, $lineno); return new GetAttrExpression($node, $arg, $arguments, $type, $lineno);
@ -545,23 +566,35 @@ class ExpressionParser
return $this->parseFilterExpressionRaw($node); return $this->parseFilterExpressionRaw($node);
} }
public function parseFilterExpressionRaw($node, $tag = null) public function parseFilterExpressionRaw($node)
{ {
while (true) { if (\func_num_args() > 1) {
$token = $this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5); trigger_deprecation('twig/twig', '3.12', 'Passing a second argument to "%s()" is deprecated.', __METHOD__);
}
$name = new ConstantExpression($token->getValue(), $token->getLine()); while (true) {
if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { $token = $this->parser->getStream()->expect(Token::NAME_TYPE);
if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) {
$arguments = new Node(); $arguments = new Node();
} else { } else {
$arguments = $this->parseArguments(true, false, true); $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; break;
} }
@ -575,7 +608,7 @@ class ExpressionParser
* Parses arguments. * Parses arguments.
* *
* @param bool $namedArguments Whether to allow named arguments or not * @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 * @return Node
* *
@ -586,28 +619,28 @@ class ExpressionParser
$args = []; $args = [];
$stream = $this->parser->getStream(); $stream = $this->parser->getStream();
$stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '(', 'A list of arguments must begin with an opening parenthesis'); $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) { while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) {
if (!empty($args)) { 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 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; break;
} }
} }
if ($definition) { 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()); $value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine());
} else { } else {
$value = $this->parseExpression(0, $allowArrow); $value = $this->parseExpression(0, $allowArrow);
} }
$name = null; $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) { 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'); $name = $value->getAttribute('name');
@ -615,7 +648,7 @@ class ExpressionParser
$value = $this->parsePrimaryExpression(); $value = $this->parsePrimaryExpression();
if (!$this->checkConstantExpression($value)) { 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 { } else {
$value = $this->parseExpression(0, $allowArrow); $value = $this->parseExpression(0, $allowArrow);
@ -626,6 +659,7 @@ class ExpressionParser
if (null === $name) { if (null === $name) {
$name = $value->getAttribute('name'); $name = $value->getAttribute('name');
$value = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine()); $value = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine());
$value->setAttribute('is_implicit', true);
} }
$args[$name] = $value; $args[$name] = $value;
} else { } 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); return new Node($args);
} }
@ -647,19 +681,19 @@ class ExpressionParser
$targets = []; $targets = [];
while (true) { while (true) {
$token = $this->parser->getCurrentToken(); $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 // in this context, string operators are variable names
$this->parser->getStream()->next(); $this->parser->getStream()->next();
} else { } 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(); $value = $token->getValue();
if (\in_array(strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), ['true', 'false', 'none', 'null'])) { 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()); $targets[] = new AssignNameExpression($value, $token->getLine());
if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break; break;
} }
} }
@ -672,7 +706,7 @@ class ExpressionParser
$targets = []; $targets = [];
while (true) { while (true) {
$targets[] = $this->parseExpression(); $targets[] = $this->parseExpression();
if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) {
break; break;
} }
} }
@ -688,121 +722,115 @@ class ExpressionParser
private function parseTestExpression(Node $node): TestExpression private function parseTestExpression(Node $node): TestExpression
{ {
$stream = $this->parser->getStream(); $stream = $this->parser->getStream();
list($name, $test) = $this->getTest($node->getTemplateLine()); $test = $this->getTest($node->getTemplateLine());
$class = $this->getTestNodeClass($test);
$arguments = null; $arguments = null;
if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
$arguments = $this->parseArguments(true); $arguments = $this->parseArguments(true);
} elseif ($test->hasOneMandatoryArgument()) { } elseif ($test->hasOneMandatoryArgument()) {
$arguments = new Node([0 => $this->parsePrimaryExpression()]); $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 = new MethodCallExpression($alias['node'], $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine());
$node->setAttribute('safe', true); $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);
} }
private function getTest(int $line): array 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): TwigTest
{ {
$stream = $this->parser->getStream(); $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)) { if ($stream->test(Token::NAME_TYPE)) {
return [$name, $test];
}
if ($stream->test(/* Token::NAME_TYPE */ 5)) {
// try 2-words tests // try 2-words tests
$name = $name.' '.$this->parser->getCurrentToken()->getValue(); $name = $name.' '.$this->parser->getCurrentToken()->getValue();
if ($test = $this->env->getTest($name)) { if ($test = $this->env->getTest($name)) {
$stream->next(); $stream->next();
return [$name, $test];
} }
} else {
$test = $this->env->getTest($name);
} }
$e = new SyntaxError(sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext()); if (!$test) {
$e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext());
$e->addSuggestions($name, array_keys($this->env->getTests())); $e->addSuggestions($name, array_keys($this->env->getTests()));
throw $e; throw $e;
} }
private function getTestNodeClass(TwigTest $test): string
{
if ($test->isDeprecated()) { if ($test->isDeprecated()) {
$stream = $this->parser->getStream(); $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()) { if ($test->getAlternative()) {
$message .= sprintf('. Use "%s" instead', $test->getAlternative()); $message .= \sprintf('. Use "%s" instead', $test->getAlternative());
} }
$src = $stream->getSourceContext(); $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)) { 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())); $e->addSuggestions($name, array_keys($this->env->getFunctions()));
throw $e; throw $e;
} }
if ($function->isDeprecated()) { if ($function->isDeprecated()) {
$message = sprintf('Twig Function "%s" is deprecated', $function->getName()); $message = \sprintf('Twig Function "%s" is deprecated', $function->getName());
if ($function->getDeprecatedVersion()) {
$message .= sprintf(' since version %s', $function->getDeprecatedVersion());
}
if ($function->getAlternative()) { if ($function->getAlternative()) {
$message .= sprintf('. Use "%s" instead', $function->getAlternative()); $message .= \sprintf('. Use "%s" instead', $function->getAlternative());
} }
$src = $this->parser->getStream()->getSourceContext(); $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)) { 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())); $e->addSuggestions($name, array_keys($this->env->getFilters()));
throw $e; throw $e;
} }
if ($filter->isDeprecated()) { if ($filter->isDeprecated()) {
$message = sprintf('Twig Filter "%s" is deprecated', $filter->getName()); $message = \sprintf('Twig Filter "%s" is deprecated', $filter->getName());
if ($filter->getDeprecatedVersion()) {
$message .= sprintf(' since version %s', $filter->getDeprecatedVersion());
}
if ($filter->getAlternative()) { if ($filter->getAlternative()) {
$message .= sprintf('. Use "%s" instead', $filter->getAlternative()); $message .= \sprintf('. Use "%s" instead', $filter->getAlternative());
} }
$src = $this->parser->getStream()->getSourceContext(); $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 // checks that the node only contains "constant" elements

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,11 @@
* file that was distributed with this source code. * 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; use Twig\TwigFunction;
final class DebugExtension extends AbstractExtension final class DebugExtension extends AbstractExtension
@ -18,28 +22,22 @@ final class DebugExtension extends AbstractExtension
{ {
// dump is safe if var_dump is overridden by xdebug // dump is safe if var_dump is overridden by xdebug
$isDumpOutputHtmlSafe = \extension_loaded('xdebug') $isDumpOutputHtmlSafe = \extension_loaded('xdebug')
// false means that it was not set (and the default is on) or it explicitly enabled // Xdebug overloads var_dump in develop mode when html_errors is enabled
&& (false === ini_get('xdebug.overload_var_dump') || ini_get('xdebug.overload_var_dump')) && str_contains(\ini_get('xdebug.mode'), 'develop')
// false means that it was not set (and the default is on) or it explicitly enabled && (false === \ini_get('html_errors') || \ini_get('html_errors'))
// xdebug.overload_var_dump produces HTML only when html_errors is also enabled
&& (false === ini_get('html_errors') || ini_get('html_errors'))
|| 'cli' === \PHP_SAPI || 'cli' === \PHP_SAPI
; ;
return [ 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; * @internal
use Twig\Template; */
use Twig\TemplateWrapper; public static function dump(Environment $env, $context, ...$vars)
{
function twig_var_dump(Environment $env, $context, ...$vars)
{
if (!$env->isDebug()) { if (!$env->isDebug()) {
return; return;
} }
@ -60,5 +58,5 @@ function twig_var_dump(Environment $env, $context, ...$vars)
} }
return ob_get_clean(); return ob_get_clean();
} }
} }

View File

@ -9,22 +9,24 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
namespace Twig\Extension { namespace Twig\Extension;
use Twig\Environment;
use Twig\FileExtensionEscapingStrategy; use Twig\FileExtensionEscapingStrategy;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\Filter\RawFilter;
use Twig\Node\Node;
use Twig\NodeVisitor\EscaperNodeVisitor; use Twig\NodeVisitor\EscaperNodeVisitor;
use Twig\Runtime\EscaperRuntime;
use Twig\TokenParser\AutoEscapeTokenParser; use Twig\TokenParser\AutoEscapeTokenParser;
use Twig\TwigFilter; use Twig\TwigFilter;
final class EscaperExtension extends AbstractExtension final class EscaperExtension extends AbstractExtension
{ {
private $defaultStrategy; private $environment;
private $escapers = []; private $escapers = [];
private $escaper;
/** @internal */ private $defaultStrategy;
public $safeClasses = [];
/** @internal */
public $safeLookup = [];
/** /**
* @param string|false|callable $defaultStrategy An escaping strategy * @param string|false|callable $defaultStrategy An escaping strategy
@ -49,19 +51,43 @@ final class EscaperExtension extends AbstractExtension
public function getFilters(): array public function getFilters(): array
{ {
return [ return [
new TwigFilter('escape', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']), new TwigFilter('escape', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]),
new TwigFilter('e', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']), new TwigFilter('e', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]),
new TwigFilter('raw', 'twig_raw_filter', ['is_safe' => ['all']]), 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. * Sets the default strategy to use when not defined by the user.
* *
* The strategy can be a valid PHP callback that takes the template * The strategy can be a valid PHP callback that takes the template
* name as an argument and returns the strategy to use. * 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 public function setDefaultStrategy($defaultStrategy): void
{ {
@ -94,315 +120,73 @@ final class EscaperExtension extends AbstractExtension
* Defines a new escaper to be used via the escape filter. * 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 string $strategy The strategy name that should be used as a strategy in the escape call
* @param callable $callable A valid PHP callable * @param callable(Environment, string, string): string $callable A valid PHP callable
*
* @deprecated since Twig 3.10
*/ */
public function setEscaper($strategy, callable $callable) 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; $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. * 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() 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; return $this->escapers;
} }
/**
* @deprecated since Twig 3.10
*/
public function setSafeClasses(array $safeClasses = []) public function setSafeClasses(array $safeClasses = [])
{ {
$this->safeClasses = []; trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setSafeClasses()" method instead.', __METHOD__);
$this->safeLookup = [];
foreach ($safeClasses as $class => $strategies) { if (!isset($this->escaper)) {
$this->addSafeClass($class, $strategies); throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
}
} }
$this->escaper->setSafeClasses($safeClasses);
}
/**
* @deprecated since Twig 3.10
*/
public function addSafeClass(string $class, array $strategies) public function addSafeClass(string $class, array $strategies)
{ {
$class = ltrim($class, '\\'); trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::addSafeClass()" method instead.', __METHOD__);
if (!isset($this->safeClasses[$class])) {
$this->safeClasses[$class] = [];
}
$this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies);
foreach ($strategies as $strategy) { if (!isset($this->escaper)) {
$this->safeLookup[$strategy][$class] = true; throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__));
}
}
}
}
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)) { $this->escaper->addSafeClass($class, $strategies);
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;
}
} }
$string = (string) $string;
} elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) {
return $string;
}
}
if ('' === $string) {
return '';
}
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);
}
$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 * @internal
*/ */
function twig_escape_filter_is_safe(Node $filterArgs) public static function escapeFilterIsSafe(Node $filterArgs)
{ {
foreach ($filterArgs as $arg) { foreach ($filterArgs as $arg) {
if ($arg instanceof ConstantExpression) { if ($arg instanceof ConstantExpression) {
return [$arg->getAttribute('value')]; return [$arg->getAttribute('value')];
@ -412,5 +196,5 @@ function twig_escape_filter_is_safe(Node $filterArgs)
} }
return ['html']; return ['html'];
} }
} }

View File

@ -11,6 +11,8 @@
namespace Twig\Extension; namespace Twig\Extension;
use Twig\ExpressionParser;
use Twig\Node\Expression\AbstractExpression;
use Twig\NodeVisitor\NodeVisitorInterface; use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface; use Twig\TokenParser\TokenParserInterface;
use Twig\TwigFilter; use Twig\TwigFilter;
@ -63,6 +65,11 @@ interface ExtensionInterface
* Returns a list of operators to add to the existing list. * Returns a list of operators to add to the existing list.
* *
* @return array<array> First array of unary operators, second array of binary operators * @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(); public function getOperators();
} }

View File

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

View File

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

View File

@ -15,6 +15,7 @@ use Twig\NodeVisitor\SandboxNodeVisitor;
use Twig\Sandbox\SecurityNotAllowedMethodError; use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityNotAllowedPropertyError; use Twig\Sandbox\SecurityNotAllowedPropertyError;
use Twig\Sandbox\SecurityPolicyInterface; use Twig\Sandbox\SecurityPolicyInterface;
use Twig\Sandbox\SourcePolicyInterface;
use Twig\Source; use Twig\Source;
use Twig\TokenParser\SandboxTokenParser; use Twig\TokenParser\SandboxTokenParser;
@ -23,11 +24,13 @@ final class SandboxExtension extends AbstractExtension
private $sandboxedGlobally; private $sandboxedGlobally;
private $sandboxed; private $sandboxed;
private $policy; 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->policy = $policy;
$this->sandboxedGlobally = $sandboxed; $this->sandboxedGlobally = $sandboxed;
$this->sourcePolicy = $sourcePolicy;
} }
public function getTokenParsers(): array public function getTokenParsers(): array
@ -50,9 +53,9 @@ final class SandboxExtension extends AbstractExtension
$this->sandboxed = false; $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 public function isSandboxedGlobally(): bool
@ -60,6 +63,15 @@ final class SandboxExtension extends AbstractExtension
return $this->sandboxedGlobally; 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) public function setSecurityPolicy(SecurityPolicyInterface $policy)
{ {
$this->policy = $policy; $this->policy = $policy;
@ -70,16 +82,16 @@ final class SandboxExtension extends AbstractExtension
return $this->policy; 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); $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 { try {
$this->policy->checkMethodAllowed($obj, $method); $this->policy->checkMethodAllowed($obj, $method);
} catch (SecurityNotAllowedMethodError $e) { } 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 { try {
$this->policy->checkPropertyAllowed($obj, $property); $this->policy->checkPropertyAllowed($obj, $property);
} catch (SecurityNotAllowedPropertyError $e) { } 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 { try {
$this->policy->checkMethodAllowed($obj, '__toString'); $this->policy->checkMethodAllowed($obj, '__toString');
} catch (SecurityNotAllowedMethodError $e) { } catch (SecurityNotAllowedMethodError $e) {

View File

@ -35,7 +35,7 @@ final class StagingExtension extends AbstractExtension
public function addFunction(TwigFunction $function): void public function addFunction(TwigFunction $function): void
{ {
if (isset($this->functions[$function->getName()])) { 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; $this->functions[$function->getName()] = $function;
@ -49,7 +49,7 @@ final class StagingExtension extends AbstractExtension
public function addFilter(TwigFilter $filter): void public function addFilter(TwigFilter $filter): void
{ {
if (isset($this->filters[$filter->getName()])) { 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; $this->filters[$filter->getName()] = $filter;
@ -73,7 +73,7 @@ final class StagingExtension extends AbstractExtension
public function addTokenParser(TokenParserInterface $parser): void public function addTokenParser(TokenParserInterface $parser): void
{ {
if (isset($this->tokenParsers[$parser->getTag()])) { 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; $this->tokenParsers[$parser->getTag()] = $parser;
@ -87,7 +87,7 @@ final class StagingExtension extends AbstractExtension
public function addTest(TwigTest $test): void public function addTest(TwigTest $test): void
{ {
if (isset($this->tests[$test->getName()])) { 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; $this->tests[$test->getName()] = $test;

View File

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

View File

@ -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)];
}
}

View File

@ -15,6 +15,9 @@ use Twig\Error\RuntimeError;
use Twig\Extension\ExtensionInterface; use Twig\Extension\ExtensionInterface;
use Twig\Extension\GlobalsInterface; use Twig\Extension\GlobalsInterface;
use Twig\Extension\StagingExtension; 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\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\TokenParserInterface; use Twig\TokenParser\TokenParserInterface;
@ -31,11 +34,23 @@ final class ExtensionSet
private $staging; private $staging;
private $parsers; private $parsers;
private $visitors; private $visitors;
/** @var array<string, TwigFilter> */
private $filters; private $filters;
/** @var array<string, TwigFilter> */
private $dynamicFilters;
/** @var array<string, TwigTest> */
private $tests; private $tests;
/** @var array<string, TwigTest> */
private $dynamicTests;
/** @var array<string, TwigFunction> */
private $functions; private $functions;
/** @var array<string, TwigFunction> */
private $dynamicFunctions;
/** @var array<string, array{precedence: int, class: class-string<AbstractExpression>}> */
private $unaryOperators; private $unaryOperators;
/** @var array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}> */
private $binaryOperators; private $binaryOperators;
/** @var array<string, mixed> */
private $globals; private $globals;
private $functionCallbacks = []; private $functionCallbacks = [];
private $filterCallbacks = []; private $filterCallbacks = [];
@ -62,7 +77,7 @@ final class ExtensionSet
$class = ltrim($class, '\\'); $class = ltrim($class, '\\');
if (!isset($this->extensions[$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]; return $this->extensions[$class];
@ -117,11 +132,11 @@ final class ExtensionSet
$class = \get_class($extension); $class = \get_class($extension);
if ($this->initialized) { 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])) { 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; $this->extensions[$class] = $extension;
@ -130,7 +145,7 @@ final class ExtensionSet
public function addFunction(TwigFunction $function): void public function addFunction(TwigFunction $function): void
{ {
if ($this->initialized) { 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); $this->staging->addFunction($function);
@ -158,14 +173,11 @@ final class ExtensionSet
return $this->functions[$name]; return $this->functions[$name];
} }
foreach ($this->functions as $pattern => $function) { foreach ($this->dynamicFunctions as $pattern => $function) {
$pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); if (preg_match($pattern, $name, $matches)) {
if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) {
array_shift($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 public function addFilter(TwigFilter $filter): void
{ {
if ($this->initialized) { 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); $this->staging->addFilter($filter);
@ -214,14 +226,11 @@ final class ExtensionSet
return $this->filters[$name]; return $this->filters[$name];
} }
foreach ($this->filters as $pattern => $filter) { foreach ($this->dynamicFilters as $pattern => $filter) {
$pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); if (preg_match($pattern, $name, $matches)) {
if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) {
array_shift($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; $this->parserCallbacks[] = $callable;
} }
/**
* @return array<string, mixed>
*/
public function getGlobals(): array public function getGlobals(): array
{ {
if (null !== $this->globals) { if (null !== $this->globals) {
@ -317,12 +329,7 @@ final class ExtensionSet
continue; continue;
} }
$extGlobals = $extension->getGlobals(); $globals = array_merge($globals, $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);
} }
if ($this->initialized) { if ($this->initialized) {
@ -332,10 +339,15 @@ final class ExtensionSet
return $globals; return $globals;
} }
public function resetGlobals(): void
{
$this->globals = null;
}
public function addTest(TwigTest $test): void public function addTest(TwigTest $test): void
{ {
if ($this->initialized) { 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); $this->staging->addTest($test);
@ -363,22 +375,20 @@ final class ExtensionSet
return $this->tests[$name]; return $this->tests[$name];
} }
foreach ($this->tests as $pattern => $test) { foreach ($this->dynamicTests as $pattern => $test) {
$pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); if (preg_match($pattern, $name, $matches)) {
if ($count) {
if (preg_match('#^'.$pattern.'$#', $name, $matches)) {
array_shift($matches); array_shift($matches);
$test->setArguments($matches);
return $test; return $test->withDynamicArguments($name, $test->getName(), $matches);
}
} }
} }
return null; return null;
} }
/**
* @return array<string, array{precedence: int, class: class-string<AbstractExpression>}>
*/
public function getUnaryOperators(): array public function getUnaryOperators(): array
{ {
if (!$this->initialized) { if (!$this->initialized) {
@ -388,6 +398,9 @@ final class ExtensionSet
return $this->unaryOperators; return $this->unaryOperators;
} }
/**
* @return array<string, array{precedence: int, class?: class-string<AbstractExpression>, associativity: ExpressionParser::OPERATOR_*}>
*/
public function getBinaryOperators(): array public function getBinaryOperators(): array
{ {
if (!$this->initialized) { if (!$this->initialized) {
@ -403,6 +416,9 @@ final class ExtensionSet
$this->filters = []; $this->filters = [];
$this->functions = []; $this->functions = [];
$this->tests = []; $this->tests = [];
$this->dynamicFilters = [];
$this->dynamicFunctions = [];
$this->dynamicTests = [];
$this->visitors = []; $this->visitors = [];
$this->unaryOperators = []; $this->unaryOperators = [];
$this->binaryOperators = []; $this->binaryOperators = [];
@ -419,17 +435,26 @@ final class ExtensionSet
{ {
// filters // filters
foreach ($extension->getFilters() as $filter) { 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 // functions
foreach ($extension->getFunctions() as $function) { 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 // tests
foreach ($extension->getTests() as $test) { 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 // token parsers
@ -449,11 +474,11 @@ final class ExtensionSet
// operators // operators
if ($operators = $extension->getOperators()) { if ($operators = $extension->getOperators()) {
if (!\is_array($operators)) { 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)) { 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]); $this->unaryOperators = array_merge($this->unaryOperators, $operators[0]);

View File

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

View File

@ -19,6 +19,8 @@ use Twig\Error\SyntaxError;
*/ */
class Lexer class Lexer
{ {
private $isInitialized = false;
private $tokens; private $tokens;
private $code; private $code;
private $cursor; private $cursor;
@ -48,6 +50,14 @@ class Lexer
public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As'; public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As';
public const PUNCTUATION = '()[]{}?:.,|'; public const PUNCTUATION = '()[]{}?:.,|';
private const SPECIAL_CHARS = [
'f' => "\f",
'n' => "\n",
'r' => "\r",
't' => "\t",
'v' => "\v",
];
public function __construct(Environment $env, array $options = []) public function __construct(Environment $env, array $options = [])
{ {
$this->env = $env; $this->env = $env;
@ -61,6 +71,13 @@ class Lexer
'whitespace_line_chars' => ' \t\0\x0B', 'whitespace_line_chars' => ' \t\0\x0B',
'interpolation' => ['#{', '}'], 'interpolation' => ['#{', '}'],
], $options); ], $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 // 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 = [ $this->regexes = [
@ -149,10 +166,14 @@ class Lexer
'interpolation_start' => '{'.preg_quote($this->options['interpolation'][0], '#').'\s*}A', 'interpolation_start' => '{'.preg_quote($this->options['interpolation'][0], '#').'\s*}A',
'interpolation_end' => '{\s*'.preg_quote($this->options['interpolation'][1], '#').'}A', 'interpolation_end' => '{\s*'.preg_quote($this->options['interpolation'][1], '#').'}A',
]; ];
$this->isInitialized = true;
} }
public function tokenize(Source $source): TokenStream public function tokenize(Source $source): TokenStream
{ {
$this->initialize();
$this->source = $source; $this->source = $source;
$this->code = str_replace(["\r\n", "\r"], "\n", $source->getCode()); $this->code = str_replace(["\r\n", "\r"], "\n", $source->getCode());
$this->cursor = 0; $this->cursor = 0;
@ -194,11 +215,11 @@ class Lexer
} }
} }
$this->pushToken(/* Token::EOF_TYPE */ -1); $this->pushToken(Token::EOF_TYPE);
if (!empty($this->brackets)) { if (!empty($this->brackets)) {
list($expect, $lineno) = array_pop($this->brackets); [$expect, $lineno] = array_pop($this->brackets);
throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source); throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
} }
return new TokenStream($this->tokens, $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 no matches are left we return the rest of the template as simple text token
if ($this->position == \count($this->positions[0]) - 1) { 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; $this->cursor = $this->end;
return; return;
@ -237,7 +258,7 @@ class Lexer
$text = rtrim($text, " \t\0\x0B"); $text = rtrim($text, " \t\0\x0B");
} }
} }
$this->pushToken(/* Token::TEXT_TYPE */ 0, $text); $this->pushToken(Token::TEXT_TYPE, $text);
$this->moveCursor($textContent.$position[0]); $this->moveCursor($textContent.$position[0]);
switch ($this->positions[1][$this->position][0]) { switch ($this->positions[1][$this->position][0]) {
@ -255,14 +276,14 @@ class Lexer
$this->moveCursor($match[0]); $this->moveCursor($match[0]);
$this->lineno = (int) $match[1]; $this->lineno = (int) $match[1];
} else { } else {
$this->pushToken(/* Token::BLOCK_START_TYPE */ 1); $this->pushToken(Token::BLOCK_START_TYPE);
$this->pushState(self::STATE_BLOCK); $this->pushState(self::STATE_BLOCK);
$this->currentVarBlockLine = $this->lineno; $this->currentVarBlockLine = $this->lineno;
} }
break; break;
case $this->options['tag_variable'][0]: 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->pushState(self::STATE_VAR);
$this->currentVarBlockLine = $this->lineno; $this->currentVarBlockLine = $this->lineno;
break; break;
@ -272,7 +293,7 @@ class Lexer
private function lexBlock(): void private function lexBlock(): void
{ {
if (empty($this->brackets) && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) { 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->moveCursor($match[0]);
$this->popState(); $this->popState();
} else { } else {
@ -283,7 +304,7 @@ class Lexer
private function lexVar(): void private function lexVar(): void
{ {
if (empty($this->brackets) && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) { 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->moveCursor($match[0]);
$this->popState(); $this->popState();
} else { } else {
@ -298,23 +319,28 @@ class Lexer
$this->moveCursor($match[0]); $this->moveCursor($match[0]);
if ($this->cursor >= $this->end) { 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 // 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->pushToken(Token::ARROW_TYPE, '=>');
$this->moveCursor('=>'); $this->moveCursor('=>');
} }
// operators // operators
elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) { 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]); $this->moveCursor($match[0]);
} }
// names // names
elseif (preg_match(self::REGEX_NAME, $this->code, $match, 0, $this->cursor)) { 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]); $this->moveCursor($match[0]);
} }
// numbers // numbers
@ -323,33 +349,33 @@ class Lexer
if (ctype_digit($match[0]) && $number <= \PHP_INT_MAX) { if (ctype_digit($match[0]) && $number <= \PHP_INT_MAX) {
$number = (int) $match[0]; // integers lower than the maximum $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]); $this->moveCursor($match[0]);
} }
// punctuation // punctuation
elseif (false !== strpos(self::PUNCTUATION, $this->code[$this->cursor])) { elseif (str_contains(self::PUNCTUATION, $this->code[$this->cursor])) {
// opening bracket // opening bracket
if (false !== strpos('([{', $this->code[$this->cursor])) { if (str_contains('([{', $this->code[$this->cursor])) {
$this->brackets[] = [$this->code[$this->cursor], $this->lineno]; $this->brackets[] = [$this->code[$this->cursor], $this->lineno];
} }
// closing bracket // closing bracket
elseif (false !== strpos(')]}', $this->code[$this->cursor])) { elseif (str_contains(')]}', $this->code[$this->cursor])) {
if (empty($this->brackets)) { 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, '([{', ')]}')) { 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; ++$this->cursor;
} }
// strings // strings
elseif (preg_match(self::REGEX_STRING, $this->code, $match, 0, $this->cursor)) { 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]); $this->moveCursor($match[0]);
} }
// opening double quoted string // opening double quoted string
@ -360,10 +386,67 @@ class Lexer
} }
// unlexable // unlexable
else { 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 private function lexRawData(): void
{ {
if (!preg_match($this->regexes['lex_raw_data'], $this->code, $match, \PREG_OFFSET_CAPTURE, $this->cursor)) { if (!preg_match($this->regexes['lex_raw_data'], $this->code, $match, \PREG_OFFSET_CAPTURE, $this->cursor)) {
@ -385,7 +468,7 @@ class Lexer
} }
} }
$this->pushToken(/* Token::TEXT_TYPE */ 0, $text); $this->pushToken(Token::TEXT_TYPE, $text);
} }
private function lexComment(): void private function lexComment(): void
@ -401,23 +484,23 @@ class Lexer
{ {
if (preg_match($this->regexes['interpolation_start'], $this->code, $match, 0, $this->cursor)) { if (preg_match($this->regexes['interpolation_start'], $this->code, $match, 0, $this->cursor)) {
$this->brackets[] = [$this->options['interpolation'][0], $this->lineno]; $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->moveCursor($match[0]);
$this->pushState(self::STATE_INTERPOLATION); $this->pushState(self::STATE_INTERPOLATION);
} elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && \strlen($match[0]) > 0) { } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && '' !== $match[0]) {
$this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes($match[0])); $this->pushToken(Token::STRING_TYPE, $this->stripcslashes($match[0], '"'));
$this->moveCursor($match[0]); $this->moveCursor($match[0]);
} elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) { } 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]) { 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->popState();
++$this->cursor; ++$this->cursor;
} else { } else {
// unlexable // 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); $bracket = end($this->brackets);
if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, 0, $this->cursor)) { if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, 0, $this->cursor)) {
array_pop($this->brackets); array_pop($this->brackets);
$this->pushToken(/* Token::INTERPOLATION_END_TYPE */ 11); $this->pushToken(Token::INTERPOLATION_END_TYPE);
$this->moveCursor($match[0]); $this->moveCursor($match[0]);
$this->popState(); $this->popState();
} else { } else {
@ -437,7 +520,7 @@ class Lexer
private function pushToken($type, $value = ''): void private function pushToken($type, $value = ''): void
{ {
// do not push empty text tokens // do not push empty text tokens
if (/* Token::TEXT_TYPE */ 0 === $type && '' === $value) { if (Token::TEXT_TYPE === $type && '' === $value) {
return; return;
} }

View File

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

View File

@ -21,22 +21,28 @@ use Twig\Source;
*/ */
final class ChainLoader implements LoaderInterface final class ChainLoader implements LoaderInterface
{ {
/**
* @var array<string, bool>
*/
private $hasSourceCache = []; private $hasSourceCache = [];
private $loaders = [];
/** /**
* @param LoaderInterface[] $loaders * @param iterable<LoaderInterface> $loaders
*/ */
public function __construct(array $loaders = []) public function __construct(
{ private iterable $loaders = [],
foreach ($loaders as $loader) { ) {
$this->addLoader($loader);
}
} }
public function addLoader(LoaderInterface $loader): void 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 = []; $this->hasSourceCache = [];
} }
@ -45,13 +51,18 @@ final class ChainLoader implements LoaderInterface
*/ */
public function getLoaders(): array public function getLoaders(): array
{ {
if (!\is_array($this->loaders)) {
$this->loaders = iterator_to_array($this->loaders, false);
}
return $this->loaders; return $this->loaders;
} }
public function getSourceContext(string $name): Source public function getSourceContext(string $name): Source
{ {
$exceptions = []; $exceptions = [];
foreach ($this->loaders as $loader) {
foreach ($this->getLoaders() as $loader) {
if (!$loader->exists($name)) { if (!$loader->exists($name)) {
continue; 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 public function exists(string $name): bool
@ -72,7 +83,7 @@ final class ChainLoader implements LoaderInterface
return $this->hasSourceCache[$name]; return $this->hasSourceCache[$name];
} }
foreach ($this->loaders as $loader) { foreach ($this->getLoaders() as $loader) {
if ($loader->exists($name)) { if ($loader->exists($name)) {
return $this->hasSourceCache[$name] = true; return $this->hasSourceCache[$name] = true;
} }
@ -84,7 +95,8 @@ final class ChainLoader implements LoaderInterface
public function getCacheKey(string $name): string public function getCacheKey(string $name): string
{ {
$exceptions = []; $exceptions = [];
foreach ($this->loaders as $loader) {
foreach ($this->getLoaders() as $loader) {
if (!$loader->exists($name)) { if (!$loader->exists($name)) {
continue; 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 public function isFresh(string $name, int $time): bool
{ {
$exceptions = []; $exceptions = [];
foreach ($this->loaders as $loader) {
foreach ($this->getLoaders() as $loader) {
if (!$loader->exists($name)) { if (!$loader->exists($name)) {
continue; 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).')' : ''));
} }
} }

View File

@ -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|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()) * @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))) { if (null !== $rootPath && false !== ($realPath = realpath($rootPath))) {
$this->rootPath = $realPath.\DIRECTORY_SEPARATOR; $this->rootPath = $realPath.\DIRECTORY_SEPARATOR;
} }
@ -89,7 +89,7 @@ class FilesystemLoader implements LoaderInterface
$checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path; $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path;
if (!is_dir($checkPath)) { 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, '/\\'); $this->paths[$namespace][] = rtrim($path, '/\\');
@ -105,7 +105,7 @@ class FilesystemLoader implements LoaderInterface
$checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path; $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path;
if (!is_dir($checkPath)) { 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, '/\\'); $path = rtrim($path, '/\\');
@ -183,7 +183,7 @@ class FilesystemLoader implements LoaderInterface
} }
try { try {
list($namespace, $shortname) = $this->parseName($name); [$namespace, $shortname] = $this->parseName($name);
$this->validateName($shortname); $this->validateName($shortname);
} catch (LoaderError $e) { } catch (LoaderError $e) {
@ -195,7 +195,7 @@ class FilesystemLoader implements LoaderInterface
} }
if (!isset($this->paths[$namespace])) { 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) { if (!$throw) {
return null; 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) { if (!$throw) {
return null; return null;
@ -236,7 +236,7 @@ class FilesystemLoader implements LoaderInterface
{ {
if (isset($name[0]) && '@' == $name[0]) { if (isset($name[0]) && '@' == $name[0]) {
if (false === $pos = strpos($name, '/')) { 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); $namespace = substr($name, 1, $pos - 1);
@ -250,7 +250,7 @@ class FilesystemLoader implements LoaderInterface
private function validateName(string $name): void 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.'); throw new LoaderError('A template name cannot contain NUL bytes.');
} }
@ -265,7 +265,7 @@ class FilesystemLoader implements LoaderInterface
} }
if ($level < 0) { 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));
} }
} }
} }

View File

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

View File

@ -11,6 +11,7 @@
namespace Twig\Node; namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler; use Twig\Compiler;
/** /**
@ -24,11 +25,12 @@ use Twig\Compiler;
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
#[YieldReady]
class AutoEscapeNode extends Node 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 public function compile(Compiler $compiler): void

View File

@ -12,6 +12,7 @@
namespace Twig\Node; namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler; use Twig\Compiler;
/** /**
@ -19,24 +20,29 @@ use Twig\Compiler;
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
#[YieldReady]
class BlockNode extends Node 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 public function compile(Compiler $compiler): void
{ {
$compiler $compiler
->addDebugInfo($this) ->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() ->indent()
->write("\$macros = \$this->macros;\n") ->write("\$macros = \$this->macros;\n")
; ;
$compiler $compiler
->subcompile($this->getNode('body')) ->subcompile($this->getNode('body'))
->write("yield from [];\n")
->outdent() ->outdent()
->write("}\n\n") ->write("}\n\n")
; ;

View File

@ -12,6 +12,7 @@
namespace Twig\Node; namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler; use Twig\Compiler;
/** /**
@ -19,18 +20,19 @@ use Twig\Compiler;
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
#[YieldReady]
class BlockReferenceNode extends Node implements NodeOutputInterface 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 public function compile(Compiler $compiler): void
{ {
$compiler $compiler
->addDebugInfo($this) ->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')))
; ;
} }
} }

View File

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

View File

@ -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(';');
}
}
}

View File

@ -11,17 +11,19 @@
namespace Twig\Node; namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler; use Twig\Compiler;
/** /**
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
#[YieldReady]
class CheckSecurityCallNode extends Node class CheckSecurityCallNode extends Node
{ {
public function compile(Compiler $compiler) public function compile(Compiler $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") ->write("\$this->checkSecurity();\n")
; ;
} }

View File

@ -11,17 +11,24 @@
namespace Twig\Node; namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler; use Twig\Compiler;
/** /**
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
#[YieldReady]
class CheckSecurityNode extends Node class CheckSecurityNode extends Node
{ {
private $usedFilters; private $usedFilters;
private $usedTags; private $usedTags;
private $usedFunctions; 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) public function __construct(array $usedFilters, array $usedTags, array $usedFunctions)
{ {
$this->usedFilters = $usedFilters; $this->usedFilters = $usedFilters;
@ -33,32 +40,22 @@ class CheckSecurityNode extends Node
public function compile(Compiler $compiler): void 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 $compiler
->write("\n") ->write("\n")
->write("public function checkSecurity()\n") ->write("public function checkSecurity()\n")
->write("{\n") ->write("{\n")
->indent() ->indent()
->write('static $tags = ')->repr(array_filter($tags))->raw(";\n") ->write('static $tags = ')->repr(array_filter($this->usedTags))->raw(";\n")
->write('static $filters = ')->repr(array_filter($filters))->raw(";\n") ->write('static $filters = ')->repr(array_filter($this->usedFilters))->raw(";\n")
->write('static $functions = ')->repr(array_filter($functions))->raw(";\n\n") ->write('static $functions = ')->repr(array_filter($this->usedFunctions))->raw(";\n\n")
->write("try {\n") ->write("try {\n")
->indent() ->indent()
->write("\$this->sandbox->checkSecurity(\n") ->write("\$this->sandbox->checkSecurity(\n")
->indent() ->indent()
->write(!$tags ? "[],\n" : "['".implode("', '", array_keys($tags))."'],\n") ->write(!$this->usedTags ? "[],\n" : "['".implode("', '", array_keys($this->usedTags))."'],\n")
->write(!$filters ? "[],\n" : "['".implode("', '", array_keys($filters))."'],\n") ->write(!$this->usedFilters ? "[],\n" : "['".implode("', '", array_keys($this->usedFilters))."'],\n")
->write(!$functions ? "[]\n" : "['".implode("', '", array_keys($functions))."']\n") ->write(!$this->usedFunctions ? "[],\n" : "['".implode("', '", array_keys($this->usedFunctions))."'],\n")
->write("\$this->source\n")
->outdent() ->outdent()
->write(");\n") ->write(");\n")
->outdent() ->outdent()

View File

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

View File

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

View File

@ -11,6 +11,7 @@
namespace Twig\Node; namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler; use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\AbstractExpression;
@ -19,11 +20,12 @@ use Twig\Node\Expression\AbstractExpression;
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
#[YieldReady]
class DoNode extends Node 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 public function compile(Compiler $compiler): void

View File

@ -11,6 +11,7 @@
namespace Twig\Node; namespace Twig\Node;
use Twig\Attribute\YieldReady;
use Twig\Compiler; use Twig\Compiler;
use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\ConstantExpression;
@ -20,12 +21,13 @@ use Twig\Node\Expression\ConstantExpression;
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
#[YieldReady]
class EmbedNode extends IncludeNode 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) // 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('name', $name);
$this->setAttribute('index', $index); $this->setAttribute('index', $index);

View File

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

View File

@ -55,7 +55,7 @@ class ArrayExpression extends AbstractExpression
return false; return false;
} }
public function addElement(AbstractExpression $value, AbstractExpression $key = null): void public function addElement(AbstractExpression $value, ?AbstractExpression $key = null): void
{ {
if (null === $key) { if (null === $key) {
$key = new ConstantExpression(++$this->index, $value->getTemplateLine()); $key = new ConstantExpression(++$this->index, $value->getTemplateLine());
@ -66,20 +66,70 @@ class ArrayExpression extends AbstractExpression
public function compile(Compiler $compiler): void 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('['); $compiler->raw('[');
$first = true; $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) { if (!$first) {
$compiler->raw(', '); $compiler->raw(', ');
} }
$first = false; $first = false;
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 $compiler
->subcompile($pair['key']) ->subcompile($pair['key'])
->raw(' => ') ->raw(' => ')
->subcompile($pair['value'])
; ;
} 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;
}
} }

View File

@ -21,9 +21,9 @@ use Twig\Node\Node;
*/ */
class ArrowFunctionExpression extends AbstractExpression 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 public function compile(Compiler $compiler): void

View File

@ -20,11 +20,11 @@ class EndsWithBinary extends AbstractBinary
$left = $compiler->getVarName(); $left = $compiler->getVarName();
$right = $compiler->getVarName(); $right = $compiler->getVarName();
$compiler $compiler
->raw(sprintf('(is_string($%s = ', $left)) ->raw(\sprintf('(is_string($%s = ', $left))
->subcompile($this->getNode('left')) ->subcompile($this->getNode('left'))
->raw(sprintf(') && is_string($%s = ', $right)) ->raw(\sprintf(') && is_string($%s = ', $right))
->subcompile($this->getNode('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))
; ;
} }

View File

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

View File

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

View File

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

View File

@ -0,0 +1,33 @@
<?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\Expression\Binary;
use Twig\Compiler;
class HasEveryBinary extends AbstractBinary
{
public function compile(Compiler $compiler): void
{
$compiler
->raw('CoreExtension::arrayEvery($this->env, ')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))
->raw(')')
;
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('');
}
}

View File

@ -0,0 +1,33 @@
<?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\Expression\Binary;
use Twig\Compiler;
class HasSomeBinary extends AbstractBinary
{
public function compile(Compiler $compiler): void
{
$compiler
->raw('CoreExtension::arraySome($this->env, ')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))
->raw(')')
;
}
public function operator(Compiler $compiler): Compiler
{
return $compiler->raw('');
}
}

View File

@ -18,7 +18,7 @@ class InBinary extends AbstractBinary
public function compile(Compiler $compiler): void public function compile(Compiler $compiler): void
{ {
$compiler $compiler
->raw('twig_in_filter(') ->raw('CoreExtension::inFilter(')
->subcompile($this->getNode('left')) ->subcompile($this->getNode('left'))
->raw(', ') ->raw(', ')
->subcompile($this->getNode('right')) ->subcompile($this->getNode('right'))

View File

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

View File

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

View File

@ -18,7 +18,7 @@ class MatchesBinary extends AbstractBinary
public function compile(Compiler $compiler): void public function compile(Compiler $compiler): void
{ {
$compiler $compiler
->raw('preg_match(') ->raw('CoreExtension::matches(')
->subcompile($this->getNode('right')) ->subcompile($this->getNode('right'))
->raw(', ') ->raw(', ')
->subcompile($this->getNode('left')) ->subcompile($this->getNode('left'))

View File

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

View File

@ -18,7 +18,7 @@ class NotInBinary extends AbstractBinary
public function compile(Compiler $compiler): void public function compile(Compiler $compiler): void
{ {
$compiler $compiler
->raw('!twig_in_filter(') ->raw('!CoreExtension::inFilter(')
->subcompile($this->getNode('left')) ->subcompile($this->getNode('left'))
->raw(', ') ->raw(', ')
->subcompile($this->getNode('right')) ->subcompile($this->getNode('right'))

View File

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

View File

@ -22,14 +22,14 @@ use Twig\Node\Node;
*/ */
class BlockReferenceExpression extends AbstractExpression class BlockReferenceExpression extends AbstractExpression
{ {
public function __construct(Node $name, ?Node $template, int $lineno, string $tag = null) public function __construct(Node $name, ?Node $template, int $lineno)
{ {
$nodes = ['name' => $name]; $nodes = ['name' => $name];
if (null !== $template) { if (null !== $template) {
$nodes['template'] = $template; $nodes['template'] = $template;
} }
parent::__construct($nodes, ['is_defined_test' => false, 'output' => false], $lineno, $tag); parent::__construct($nodes, ['is_defined_test' => false, 'output' => false], $lineno);
} }
public function compile(Compiler $compiler): void public function compile(Compiler $compiler): void
@ -40,8 +40,9 @@ class BlockReferenceExpression extends AbstractExpression
if ($this->getAttribute('output')) { if ($this->getAttribute('output')) {
$compiler->addDebugInfo($this); $compiler->addDebugInfo($this);
$compiler->write('yield from ');
$this $this
->compileTemplateCall($compiler, 'displayBlock') ->compileTemplateCall($compiler, 'yieldBlock')
->raw(";\n"); ->raw(";\n");
} else { } else {
$this->compileTemplateCall($compiler, 'renderBlock'); $this->compileTemplateCall($compiler, 'renderBlock');
@ -65,7 +66,7 @@ class BlockReferenceExpression extends AbstractExpression
; ;
} }
$compiler->raw(sprintf('->%s', $method)); $compiler->raw(\sprintf('->unwrap()->%s', $method));
return $this->compileBlockArguments($compiler); return $this->compileBlockArguments($compiler);
} }

View File

@ -15,40 +15,49 @@ use Twig\Compiler;
use Twig\Error\SyntaxError; use Twig\Error\SyntaxError;
use Twig\Extension\ExtensionInterface; use Twig\Extension\ExtensionInterface;
use Twig\Node\Node; use Twig\Node\Node;
use Twig\TwigCallableInterface;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Twig\TwigTest;
use Twig\Util\CallableArgumentsExtractor;
use Twig\Util\ReflectionCallable;
abstract class CallExpression extends AbstractExpression abstract class CallExpression extends AbstractExpression
{ {
private $reflector; private $reflector = null;
protected function compileCallable(Compiler $compiler) protected function compileCallable(Compiler $compiler)
{ {
$callable = $this->getAttribute('callable'); $twigCallable = $this->getTwigCallable();
$callable = $twigCallable->getCallable();
if (\is_string($callable) && false === strpos($callable, '::')) { if (\is_string($callable) && !str_contains($callable, '::')) {
$compiler->raw($callable); $compiler->raw($callable);
} else { } else {
[$r, $callable] = $this->reflectCallable($callable); $rc = $this->reflectCallable($twigCallable);
$r = $rc->getReflector();
$callable = $rc->getCallable();
if (\is_string($callable)) { if (\is_string($callable)) {
$compiler->raw($callable); $compiler->raw($callable);
} elseif (\is_array($callable) && \is_string($callable[0])) { } elseif (\is_array($callable) && \is_string($callable[0])) {
if (!$r instanceof \ReflectionMethod || $r->isStatic()) { if (!$r instanceof \ReflectionMethod || $r->isStatic()) {
$compiler->raw(sprintf('%s::%s', $callable[0], $callable[1])); $compiler->raw(\sprintf('%s::%s', $callable[0], $callable[1]));
} else { } else {
$compiler->raw(sprintf('$this->env->getRuntime(\'%s\')->%s', $callable[0], $callable[1])); $compiler->raw(\sprintf('$this->env->getRuntime(\'%s\')->%s', $callable[0], $callable[1]));
} }
} elseif (\is_array($callable) && $callable[0] instanceof ExtensionInterface) { } elseif (\is_array($callable) && $callable[0] instanceof ExtensionInterface) {
$class = \get_class($callable[0]); $class = \get_class($callable[0]);
if (!$compiler->getEnvironment()->hasExtension($class)) { if (!$compiler->getEnvironment()->hasExtension($class)) {
// Compile a non-optimized call to trigger a \Twig\Error\RuntimeError, which cannot be a compile-time error // Compile a non-optimized call to trigger a \Twig\Error\RuntimeError, which cannot be a compile-time error
$compiler->raw(sprintf('$this->env->getExtension(\'%s\')', $class)); $compiler->raw(\sprintf('$this->env->getExtension(\'%s\')', $class));
} else { } else {
$compiler->raw(sprintf('$this->extensions[\'%s\']', ltrim($class, '\\'))); $compiler->raw(\sprintf('$this->extensions[\'%s\']', ltrim($class, '\\')));
} }
$compiler->raw(sprintf('->%s', $callable[1])); $compiler->raw(\sprintf('->%s', $callable[1]));
} else { } else {
$compiler->raw(sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $this->getAttribute('name'))); $compiler->raw(\sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $twigCallable->getDynamicName()));
} }
} }
@ -57,16 +66,30 @@ abstract class CallExpression extends AbstractExpression
protected function compileArguments(Compiler $compiler, $isArray = false): void protected function compileArguments(Compiler $compiler, $isArray = false): void
{ {
if (\func_num_args() >= 2) {
trigger_deprecation('twig/twig', '3.11', 'Passing a second argument to "%s()" is deprecated.', __METHOD__);
}
$compiler->raw($isArray ? '[' : '('); $compiler->raw($isArray ? '[' : '(');
$first = true; $first = true;
if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) { $twigCallable = $this->getAttribute('twig_callable');
if ($twigCallable->needsCharset()) {
$compiler->raw('$this->env->getCharset()');
$first = false;
}
if ($twigCallable->needsEnvironment()) {
if (!$first) {
$compiler->raw(', ');
}
$compiler->raw('$this->env'); $compiler->raw('$this->env');
$first = false; $first = false;
} }
if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) { if ($twigCallable->needsContext()) {
if (!$first) { if (!$first) {
$compiler->raw(', '); $compiler->raw(', ');
} }
@ -74,15 +97,13 @@ abstract class CallExpression extends AbstractExpression
$first = false; $first = false;
} }
if ($this->hasAttribute('arguments')) { foreach ($twigCallable->getArguments() as $argument) {
foreach ($this->getAttribute('arguments') as $argument) {
if (!$first) { if (!$first) {
$compiler->raw(', '); $compiler->raw(', ');
} }
$compiler->string($argument); $compiler->string($argument);
$first = false; $first = false;
} }
}
if ($this->hasNode('node')) { if ($this->hasNode('node')) {
if (!$first) { if (!$first) {
@ -93,8 +114,7 @@ abstract class CallExpression extends AbstractExpression
} }
if ($this->hasNode('arguments')) { if ($this->hasNode('arguments')) {
$callable = $this->getAttribute('callable'); $arguments = (new CallableArgumentsExtractor($this, $this->getTwigCallable()))->extractArguments($this->getNode('arguments'));
$arguments = $this->getArguments($callable, $this->getNode('arguments'));
foreach ($arguments as $node) { foreach ($arguments as $node) {
if (!$first) { if (!$first) {
$compiler->raw(', '); $compiler->raw(', ');
@ -107,8 +127,13 @@ abstract class CallExpression extends AbstractExpression
$compiler->raw($isArray ? ']' : ')'); $compiler->raw($isArray ? ']' : ')');
} }
/**
* @deprecated since 3.12, use Twig\Util\CallableArgumentsExtractor::getArguments() instead
*/
protected function getArguments($callable, $arguments) protected function getArguments($callable, $arguments)
{ {
trigger_deprecation('twig/twig', '3.12', 'The "%s()" method is deprecated, use Twig\Util\CallableArgumentsExtractor::getArguments() instead.', __METHOD__);
$callType = $this->getAttribute('type'); $callType = $this->getAttribute('type');
$callName = $this->getAttribute('name'); $callName = $this->getAttribute('name');
@ -119,28 +144,28 @@ abstract class CallExpression extends AbstractExpression
$named = true; $named = true;
$name = $this->normalizeName($name); $name = $this->normalizeName($name);
} elseif ($named) { } elseif ($named) {
throw new SyntaxError(sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
} }
$parameters[$name] = $node; $parameters[$name] = $node;
} }
$isVariadic = $this->hasAttribute('is_variadic') && $this->getAttribute('is_variadic'); $isVariadic = $this->getAttribute('twig_callable')->isVariadic();
if (!$named && !$isVariadic) { if (!$named && !$isVariadic) {
return $parameters; return $parameters;
} }
if (!$callable) { if (!$callable) {
if ($named) { if ($named) {
$message = sprintf('Named arguments are not supported for %s "%s".', $callType, $callName); $message = \sprintf('Named arguments are not supported for %s "%s".', $callType, $callName);
} else { } else {
$message = sprintf('Arbitrary positional arguments are not supported for %s "%s".', $callType, $callName); $message = \sprintf('Arbitrary positional arguments are not supported for %s "%s".', $callType, $callName);
} }
throw new \LogicException($message); throw new \LogicException($message);
} }
list($callableParameters, $isPhpVariadic) = $this->getCallableParameters($callable, $isVariadic); [$callableParameters, $isPhpVariadic] = $this->getCallableParameters($callable, $isVariadic);
$arguments = []; $arguments = [];
$names = []; $names = [];
$missingArguments = []; $missingArguments = [];
@ -160,11 +185,11 @@ abstract class CallExpression extends AbstractExpression
if (\array_key_exists($name, $parameters)) { if (\array_key_exists($name, $parameters)) {
if (\array_key_exists($pos, $parameters)) { if (\array_key_exists($pos, $parameters)) {
throw new SyntaxError(sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
} }
if (\count($missingArguments)) { if (\count($missingArguments)) {
throw new SyntaxError(sprintf( throw new SyntaxError(\sprintf(
'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".', 'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".',
$name, $callType, $callName, implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) $name, $callType, $callName, implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments)
), $this->getTemplateLine(), $this->getSourceContext()); ), $this->getTemplateLine(), $this->getSourceContext());
@ -189,7 +214,7 @@ abstract class CallExpression extends AbstractExpression
$missingArguments[] = $name; $missingArguments[] = $name;
} }
} else { } else {
throw new SyntaxError(sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext());
} }
} }
@ -220,7 +245,7 @@ abstract class CallExpression extends AbstractExpression
} }
throw new SyntaxError( throw new SyntaxError(
sprintf( \sprintf(
'Unknown argument%s "%s" for %s "%s(%s)".', 'Unknown argument%s "%s" for %s "%s(%s)".',
\count($parameters) > 1 ? 's' : '', implode('", "', array_keys($parameters)), $callType, $callName, implode(', ', $names) \count($parameters) > 1 ? 's' : '', implode('", "', array_keys($parameters)), $callType, $callName, implode(', ', $names)
), ),
@ -232,88 +257,106 @@ abstract class CallExpression extends AbstractExpression
return $arguments; return $arguments;
} }
/**
* @deprecated since 3.12
*/
protected function normalizeName(string $name): string protected function normalizeName(string $name): string
{ {
trigger_deprecation('twig/twig', '3.12', 'The "%s()" method is deprecated.', __METHOD__);
return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)); return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name));
} }
// To be removed in 4.0
private function getCallableParameters($callable, bool $isVariadic): array private function getCallableParameters($callable, bool $isVariadic): array
{ {
[$r, , $callableName] = $this->reflectCallable($callable); $twigCallable = $this->getAttribute('twig_callable');
$rc = $this->reflectCallable($twigCallable);
$r = $rc->getReflector();
$callableName = $rc->getName();
$parameters = $r->getParameters(); $parameters = $r->getParameters();
if ($this->hasNode('node')) { if ($this->hasNode('node')) {
array_shift($parameters); array_shift($parameters);
} }
if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) { if ($twigCallable->needsCharset()) {
array_shift($parameters); array_shift($parameters);
} }
if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) { if ($twigCallable->needsEnvironment()) {
array_shift($parameters); array_shift($parameters);
} }
if ($this->hasAttribute('arguments') && null !== $this->getAttribute('arguments')) { if ($twigCallable->needsContext()) {
foreach ($this->getAttribute('arguments') as $argument) {
array_shift($parameters); array_shift($parameters);
} }
foreach ($twigCallable->getArguments() as $argument) {
array_shift($parameters);
} }
$isPhpVariadic = false; $isPhpVariadic = false;
if ($isVariadic) { if ($isVariadic) {
$argument = end($parameters); $argument = end($parameters);
$isArray = $argument && $argument->hasType() && 'array' === $argument->getType()->getName(); $isArray = $argument && $argument->hasType() && $argument->getType() instanceof \ReflectionNamedType && 'array' === $argument->getType()->getName();
if ($isArray && $argument->isDefaultValueAvailable() && [] === $argument->getDefaultValue()) { if ($isArray && $argument->isDefaultValueAvailable() && [] === $argument->getDefaultValue()) {
array_pop($parameters); array_pop($parameters);
} elseif ($argument && $argument->isVariadic()) { } elseif ($argument && $argument->isVariadic()) {
array_pop($parameters); array_pop($parameters);
$isPhpVariadic = true; $isPhpVariadic = true;
} else { } else {
throw new \LogicException(sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->getAttribute('type'), $this->getAttribute('name'))); throw new \LogicException(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->getAttribute('type'), $twigCallable->getName()));
} }
} }
return [$parameters, $isPhpVariadic]; return [$parameters, $isPhpVariadic];
} }
private function reflectCallable($callable) private function reflectCallable(TwigCallableInterface $callable): ReflectionCallable
{ {
if (null !== $this->reflector) { if (!$this->reflector) {
$this->reflector = new ReflectionCallable($callable);
}
return $this->reflector; return $this->reflector;
} }
if (\is_string($callable) && false !== $pos = strpos($callable, '::')) { /**
$callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)]; * Overrides the Twig callable based on attributes (as potentially, attributes changed between the creation and the compilation of the node).
} *
* To be removed in 4.0 and replace by $this->getAttribute('twig_callable').
*/
private function getTwigCallable(): TwigCallableInterface
{
$current = $this->getAttribute('twig_callable');
if (\is_array($callable) && method_exists($callable[0], $callable[1])) { $this->setAttribute('twig_callable', match ($this->getAttribute('type')) {
$r = new \ReflectionMethod($callable[0], $callable[1]); 'test' => (new TwigTest(
$this->getAttribute('name'),
$this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(),
[
'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(),
],
))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ?: $current->getArguments()),
'function' => (new TwigFunction(
$this->hasAttribute('name') ? $this->getAttribute('name') : $current->getName(),
$this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(),
[
'needs_environment' => $this->hasAttribute('needs_environment') ? $this->getAttribute('needs_environment') : $current->needsEnvironment(),
'needs_context' => $this->hasAttribute('needs_context') ? $this->getAttribute('needs_context') : $current->needsContext(),
'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(),
'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(),
],
))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ?: $current->getArguments()),
'filter' => (new TwigFilter(
$this->getAttribute('name'),
$this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(),
[
'needs_environment' => $this->hasAttribute('needs_environment') ? $this->getAttribute('needs_environment') : $current->needsEnvironment(),
'needs_context' => $this->hasAttribute('needs_context') ? $this->getAttribute('needs_context') : $current->needsContext(),
'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(),
'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(),
],
))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ?: $current->getArguments()),
});
return $this->reflector = [$r, $callable, $r->class.'::'.$r->name]; return $this->getAttribute('twig_callable');
}
$checkVisibility = $callable instanceof \Closure;
try {
$closure = \Closure::fromCallable($callable);
} catch (\TypeError $e) {
throw new \LogicException(sprintf('Callback for %s "%s" is not callable in the current scope.', $this->getAttribute('type'), $this->getAttribute('name')), 0, $e);
}
$r = new \ReflectionFunction($closure);
if (false !== strpos($r->name, '{closure}')) {
return $this->reflector = [$r, $callable, 'Closure'];
}
if ($object = $r->getClosureThis()) {
$callable = [$object, $r->name];
$callableName = (\function_exists('get_debug_type') ? get_debug_type($object) : \get_class($object)).'::'.$r->name;
} elseif ($class = $r->getClosureScopeClass()) {
$callableName = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name;
} else {
$callable = $callableName = $r->name;
}
if ($checkVisibility && \is_array($callable) && method_exists(...$callable) && !(new \ReflectionMethod(...$callable))->isPublic()) {
$callable = $r->getClosure();
}
return $this->reflector = [$r, $callable, $callableName];
} }
} }

View File

@ -23,6 +23,15 @@ class ConditionalExpression extends AbstractExpression
public function compile(Compiler $compiler): void public function compile(Compiler $compiler): void
{ {
// Ternary with no then uses Elvis operator
if ($this->getNode('expr1') === $this->getNode('expr2')) {
$compiler
->raw('((')
->subcompile($this->getNode('expr1'))
->raw(') ?: (')
->subcompile($this->getNode('expr3'))
->raw('))');
} else {
$compiler $compiler
->raw('((') ->raw('((')
->subcompile($this->getNode('expr1')) ->subcompile($this->getNode('expr1'))
@ -30,7 +39,7 @@ class ConditionalExpression extends AbstractExpression
->subcompile($this->getNode('expr2')) ->subcompile($this->getNode('expr2'))
->raw(') : (') ->raw(') : (')
->subcompile($this->getNode('expr3')) ->subcompile($this->getNode('expr3'))
->raw('))') ->raw('))');
; }
} }
} }

View File

@ -14,6 +14,9 @@ namespace Twig\Node\Expression;
use Twig\Compiler; use Twig\Compiler;
/**
* @final
*/
class ConstantExpression extends AbstractExpression class ConstantExpression extends AbstractExpression
{ {
public function __construct($value, int $lineno) public function __construct($value, int $lineno)

Some files were not shown because too many files have changed in this diff Show More