From 10dfd0a4433df4ad519115e8dc380b58543b383b Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Wed, 21 Aug 2024 10:10:34 +0200 Subject: [PATCH 01/37] [Web][DockerApi] Add the ability to rename the local part of a mailbox --- .../dockerapi/modules/DockerApi.py | 44 +++- data/web/inc/functions.inc.php | 62 ++--- data/web/inc/functions.mailbox.inc.php | 238 ++++++++++++++---- data/web/inc/triggers.inc.php | 29 ++- data/web/js/site/edit.js | 5 + data/web/js/site/mailbox.js | 6 +- data/web/json_api.php | 5 +- data/web/lang/lang.de-de.json | 7 +- data/web/lang/lang.en-gb.json | 5 + data/web/templates/edit/mailbox.twig | 58 ++++- docker-compose.yml | 4 +- 11 files changed, 356 insertions(+), 107 deletions(-) diff --git a/data/Dockerfiles/dockerapi/modules/DockerApi.py b/data/Dockerfiles/dockerapi/modules/DockerApi.py index 560199091..9fdd039e4 100644 --- a/data/Dockerfiles/dockerapi/modules/DockerApi.py +++ b/data/Dockerfiles/dockerapi/modules/DockerApi.py @@ -159,7 +159,7 @@ class DockerApi: postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix') # todo: check each exit code res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'} - return Response(content=json.dumps(res, indent=4), media_type="application/json") + return Response(content=json.dumps(res, indent=4), media_type="application/json") # api call: container_post - post_action: exec - cmd: mailq - task: list def container_post__exec__mailq__list(self, request_json, **kwargs): if 'container_id' in kwargs: @@ -318,7 +318,7 @@ class DockerApi: if 'username' in request_json and 'script_name' in request_json: for container in self.sync_docker_client.containers.list(filters=filters): - cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"] + cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"] sieve_return = container.exec_run(cmd) return self.exec_run_handler('utf8_text_only', sieve_return) # api call: container_post - post_action: exec - cmd: maildir - task: cleanup @@ -342,6 +342,30 @@ class DockerApi: cmd = ["/bin/bash", "-c", cmd_vmail] maildir_cleanup = container.exec_run(cmd, user='vmail') return self.exec_run_handler('generic', maildir_cleanup) + # api call: container_post - post_action: exec - cmd: maildir - task: move + def container_post__exec__maildir__move(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + if 'old_maildir' in request_json and 'new_maildir' in request_json: + for container in self.sync_docker_client.containers.list(filters=filters): + vmail_name = request_json['old_maildir'].replace("'", "'\\''") + new_vmail_name = request_json['new_maildir'].replace("'", "'\\''") + cmd_vmail = "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 = "if [[ -d '/var/vmail_index/" + index_name + "' ]]; then /bin/mv '/var/vmail_index/" + index_name + "' '/var/vmail_index/" + new_index_name + "_index'; fi" + cmd = ["/bin/bash", "-c", cmd_vmail + " && " + cmd_vmail_index] + else: + cmd = ["/bin/bash", "-c", cmd_vmail] + maildir_move = container.exec_run(cmd, user='vmail') + return self.exec_run_handler('generic', maildir_move) # api call: container_post - post_action: exec - cmd: rspamd - task: worker_password def container_post__exec__rspamd__worker_password(self, request_json, **kwargs): if 'container_id' in kwargs: @@ -374,6 +398,20 @@ class DockerApi: self.logger.error('failed changing Rspamd password') res = { 'type': 'danger', 'msg': 'command did not complete' } return Response(content=json.dumps(res, indent=4), media_type="application/json") + # api call: container_post - post_action: exec - cmd: sogo - task: rename + def container_post__exec__sogo__rename_user(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + if 'old_username' in request_json and 'new_username' in request_json: + for container in self.sync_docker_client.containers.list(filters=filters): + old_username = request_json['old_username'].replace("'", "'\\''") + new_username = request_json['new_username'].replace("'", "'\\''") + + sogo_return = container.exec_run(['sogo-tool', 'rename-user', old_username, new_username], user='sogo') + return self.exec_run_handler('generic', sogo_return) # Collect host stats async def get_host_stats(self, wait=5): @@ -462,7 +500,7 @@ class DockerApi: except: pass return ''.join(total_data) - + try : socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock if not cmd.endswith("\n"): diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 25d08b9fe..fd6c7fc2f 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -939,10 +939,10 @@ function check_login($user, $pass, $app_passwd_data = false) { $stmt->execute(array(':user' => $user)); $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC)); } - foreach ($rows as $row) { + foreach ($rows as $row) { // verify password if (verify_hash($row['password'], $pass) !== false) { - if (!array_key_exists("app_passwd_id", $row)){ + if (!array_key_exists("app_passwd_id", $row)){ // password is not a app password // check for tfa authenticators $authenticators = get_tfa($user); @@ -953,11 +953,6 @@ function check_login($user, $pass, $app_passwd_data = false) { $_SESSION['pending_mailcow_cc_role'] = "user"; $_SESSION['pending_tfa_methods'] = $authenticators['additional']; unset($_SESSION['ldelay']); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $user, '*'), - 'msg' => array('logged_in_as', $user) - ); return "pending"; } else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) { // no authenticators found, login successfull @@ -966,6 +961,11 @@ function check_login($user, $pass, $app_passwd_data = false) { $stmt->execute(array(':user' => $user)); unset($_SESSION['ldelay']); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $user, '*'), + 'msg' => array('logged_in_as', $user) + ); return "user"; } } elseif ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) { @@ -1028,7 +1028,7 @@ function update_sogo_static_view($mailbox = null) { // Check if the mailbox exists $stmt = $pdo->prepare("SELECT username FROM mailbox WHERE username = :mailbox AND active = '1'"); $stmt->execute(array(':mailbox' => $mailbox)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); + $row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row){ $mailbox_exists = true; } @@ -1056,7 +1056,7 @@ function update_sogo_static_view($mailbox = null) { LEFT OUTER JOIN grouped_sender_acl_external external_acl ON external_acl.username = mailbox.username WHERE mailbox.active = '1'"; - + if ($mailbox_exists) { $query .= " AND mailbox.username = :mailbox"; $stmt = $pdo->prepare($query); @@ -1065,9 +1065,9 @@ function update_sogo_static_view($mailbox = null) { $query .= " GROUP BY mailbox.username"; $stmt = $pdo->query($query); } - + $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');"); - + flush_memcached(); } function edit_user_account($_data) { @@ -1100,7 +1100,7 @@ function edit_user_account($_data) { AND `username` = :user"); $stmt->execute(array(':user' => $username)); $row = $stmt->fetch(PDO::FETCH_ASSOC); - + if (!verify_hash($row['password'], $password_old)) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -1109,7 +1109,7 @@ function edit_user_account($_data) { ); return false; } - + $password_new = $_data['user_new_pass']; $password_new2 = $_data['user_new_pass2']; if (password_check($password_new, $password_new2) !== true) { @@ -1124,7 +1124,7 @@ function edit_user_account($_data) { ':password_hashed' => $password_hashed, ':username' => $username )); - + update_sogo_static_view(); } // edit password recovery email @@ -1374,7 +1374,7 @@ function set_tfa($_data) { $_data['registration']->certificate, 0 )); - + $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_data_log), @@ -1544,7 +1544,7 @@ function unset_tfa_key($_data) { try { if (!is_numeric($id)) $access_denied = true; - + // set access_denied error if ($access_denied){ $_SESSION['return'][] = array( @@ -1553,7 +1553,7 @@ function unset_tfa_key($_data) { 'msg' => 'access_denied' ); return false; - } + } // check if it's last key $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa` @@ -1602,7 +1602,7 @@ function get_tfa($username = null, $id = null) { WHERE `username` = :username AND `active` = '1'"); $stmt->execute(array(':username' => $username)); $results = $stmt->fetchAll(PDO::FETCH_ASSOC); - + // no tfa methods found if (count($results) == 0) { $data['name'] = 'none'; @@ -1810,8 +1810,8 @@ function verify_tfa_login($username, $_data) { 'msg' => array('webauthn_authenticator_failed') ); return false; - } - + } + if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -2173,7 +2173,7 @@ function cors($action, $data = null) { 'msg' => 'access_denied' ); return false; - } + } $allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array($_SERVER['SERVER_NAME']); $allowed_origins = !is_array($allowed_origins) ? array_filter(array_map('trim', explode("\n", $allowed_origins))) : $allowed_origins; @@ -2206,7 +2206,7 @@ function cors($action, $data = null) { $redis->hMSet('CORS_SETTINGS', array( 'allowed_origins' => implode(', ', $allowed_origins), 'allowed_methods' => implode(', ', $allowed_methods) - )); + )); } catch (RedisException $e) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -2258,10 +2258,10 @@ function cors($action, $data = null) { header('Access-Control-Allow-Headers: Accept, Content-Type, X-Api-Key, Origin'); // Access-Control settings requested, this is just a preflight request - if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS' && + if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS' && isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) && isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) { - + $allowed_methods = explode(', ', $cors_settings["allowed_methods"]); if (in_array($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'], $allowed_methods, true)) // method allowed send 200 OK @@ -2315,7 +2315,7 @@ function reset_password($action, $data = null) { break; case 'issue': $username = $data; - + // perform cleanup $stmt = $pdo->prepare("DELETE FROM `reset_password` WHERE created < DATE_SUB(NOW(), INTERVAL :lifetime MINUTE);"); $stmt->execute(array(':lifetime' => $PW_RESET_TOKEN_LIFETIME)); @@ -2397,8 +2397,8 @@ function reset_password($action, $data = null) { $request_date = new DateTime(); $locale_date = locale_get_default(); $date_formatter = new IntlDateFormatter( - $locale_date, - IntlDateFormatter::FULL, + $locale_date, + IntlDateFormatter::FULL, IntlDateFormatter::FULL ); $formatted_request_date = $date_formatter->format($request_date); @@ -2514,7 +2514,7 @@ function reset_password($action, $data = null) { $stmt->execute(array( ':username' => $username )); - + $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $action, $_data_log), @@ -2557,7 +2557,7 @@ function reset_password($action, $data = null) { $text = $data['text']; $html = $data['html']; $subject = $data['subject']; - + if (!filter_var($from, FILTER_VALIDATE_EMAIL)) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -2590,7 +2590,7 @@ function reset_password($action, $data = null) { ); return false; } - + ini_set('max_execution_time', 0); ini_set('max_input_time', 0); $mail = new PHPMailer; @@ -2622,7 +2622,7 @@ function reset_password($action, $data = null) { return false; } $mail->ClearAllRecipients(); - + return true; break; } diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index c927ce494..35e21f1c8 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -1233,7 +1233,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':active' => $active )); - + if (isset($_data['acl'])) { $_data['acl'] = (array)$_data['acl']; $_data['spam_alias'] = (in_array('spam_alias', $_data['acl'])) ? 1 : 0; @@ -1265,14 +1265,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $_data['quarantine_attachments'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_attachments']); $_data['quarantine_notification'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_notification']); $_data['quarantine_category'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_category']); - $_data['app_passwds'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_app_passwds']); - $_data['pw_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_pw_reset']); + $_data['app_passwds'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_app_passwds']); + $_data['pw_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_pw_reset']); } try { - $stmt = $pdo->prepare("INSERT INTO `user_acl` + $stmt = $pdo->prepare("INSERT INTO `user_acl` (`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`, - `pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`, `pw_reset`) + `pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`, `pw_reset`) VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset, :pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds, :pw_reset) "); $stmt->execute(array( @@ -1467,7 +1467,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); return false; } - + // check attributes $attr = array(); $attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : array(); @@ -1557,7 +1557,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0; $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; - } + } else { $attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']); $attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']); @@ -2109,7 +2109,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); return false; } - + // check if param is whitelisted if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){ // bad option @@ -2802,11 +2802,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // check name if ($is_now["template"] == "Default" && $is_now["template"] != $_data["template"]){ // keep template name of Default template - $_data["template"] = $is_now["template"]; + $_data["template"] = $is_now["template"]; } else { - $_data["template"] = (isset($_data["template"])) ? $_data["template"] : $is_now["template"]; - } + $_data["template"] = (isset($_data["template"])) ? $_data["template"] : $is_now["template"]; + } // check attributes $attr = array(); $attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : array(); @@ -2833,10 +2833,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ":id" => $id , ":template" => $_data["template"] , ":attributes" => json_encode($attr) - )); + )); } - + $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), @@ -3192,7 +3192,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':tag_name' => $tag, )); } - + $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), @@ -3203,6 +3203,146 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } return true; break; + case 'mailbox_rename': + $domain = $_data['domain']; + $old_local_part = $_data['old_local_part']; + $old_username = $old_local_part . "@" . $domain; + $new_local_part = $_data['new_local_part']; + $new_username = $new_local_part . "@" . $domain; + $create_alias = intval($_data['create_alias']); + + if (!filter_var($old_username, FILTER_VALIDATE_EMAIL)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('username_invalid', $old_username) + ); + return false; + } + if (!filter_var($new_username, FILTER_VALIDATE_EMAIL)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('username_invalid', $new_username) + ); + return false; + } + + $is_now = mailbox('get', 'mailbox_details', $old_username); + if (empty($is_now)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'access_denied' + ); + return false; + } + + if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $is_now['domain'])) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'access_denied' + ); + return false; + } + + // rename username in sql + try { + $pdo->beginTransaction(); + $pdo->exec('SET FOREIGN_KEY_CHECKS = 0'); + + $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 + ]); + + // 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' + ]; + + foreach ($tables as $table => $column) { + $pdo->prepare("UPDATE $table SET $column = :new_username WHERE $column = :old_username") + ->execute([ + ':new_username' => $new_username, + ':old_username' => $old_username + ]); + } + + $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 + ]); + + $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 + ]); + + + // 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() + ); + } + + // 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); + + // create alias + if ($create_alias == 1) { + mailbox("add", "alias", array( + "address" => $old_username, + "goto" => $new_username, + "active" => 1, + "sogo_visible" => 1, + "private_comment" => sprintf($lang['success']['mailbox_renamed'], $old_username, $new_username) + )); + } + + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('mailbox_renamed', $old_username, $new_username) + ); + break; case 'mailbox_templates': if ($_SESSION['mailcow_cc_role'] != "admin") { $_SESSION['return'][] = array( @@ -3235,11 +3375,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // check name if ($is_now["template"] == "Default" && $is_now["template"] != $_data["template"]){ // keep template name of Default template - $_data["template"] = $is_now["template"]; + $_data["template"] = $is_now["template"]; } else { - $_data["template"] = (isset($_data["template"])) ? $_data["template"] : $is_now["template"]; - } + $_data["template"] = (isset($_data["template"])) ? $_data["template"] : $is_now["template"]; + } // check attributes $attr = array(); $attr["quota"] = isset($_data['quota']) ? intval($_data['quota']) * 1048576 : 0; @@ -3259,11 +3399,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0; $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; - } - else { + } + else { foreach ($is_now as $key => $value){ $attr[$key] = $is_now[$key]; - } + } } if (isset($_data['acl'])) { $_data['acl'] = (array)$_data['acl']; @@ -3282,10 +3422,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $attr['acl_quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0; $attr['acl_app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0; $attr['acl_pw_reset'] = (in_array('pw_reset', $_data['acl'])) ? 1 : 0; - } else { + } else { foreach ($is_now as $key => $value){ $attr[$key] = $is_now[$key]; - } + } } @@ -3297,7 +3437,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ":id" => $id , ":template" => $_data["template"] , ":attributes" => json_encode($attr) - )); + )); } @@ -3326,7 +3466,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); continue; } - $is_now = mailbox('get', 'mailbox_details', $mailbox); + $is_now = mailbox('get', 'mailbox_details', $mailbox); if(!empty($is_now)){ if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $is_now['domain'])) { $_SESSION['return'][] = array( @@ -3353,15 +3493,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $stmt->execute(array( ":username" => $mailbox, ":custom_attributes" => json_encode($attributes) - )); - + )); + $_SESSION['return'][] = array( 'type' => 'success', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => array('mailbox_modified', $mailbox) ); } - + return true; break; case 'resource': @@ -3443,7 +3583,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); } break; - case 'domain_wide_footer': + case 'domain_wide_footer': if (!is_array($_data['domains'])) { $domains = array(); $domains[] = $_data['domains']; @@ -3696,7 +3836,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // prepend domain to array $params = array(); - foreach ($tags as $key => $val){ + foreach ($tags as $key => $val){ array_push($params, '%'.$_data.'%'); array_push($params, '%'.$val.'%'); } @@ -3705,7 +3845,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); while($row = array_shift($rows)) { - if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], explode('@', $row['username'])[1])) + if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], explode('@', $row['username'])[1])) $mailboxes[] = $row['username']; } } @@ -4260,7 +4400,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { while($row = array_shift($rows)) { if ($_SESSION['mailcow_cc_role'] == "admin") $domains[] = $row['domain']; - elseif (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['domain'])) + elseif (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['domain'])) $domains[] = $row['domain']; } } else { @@ -4420,19 +4560,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } $_data = (isset($_data)) ? intval($_data) : null; - if (isset($_data)){ - $stmt = $pdo->prepare("SELECT * FROM `templates` + if (isset($_data)){ + $stmt = $pdo->prepare("SELECT * FROM `templates` WHERE `id` = :id AND type = :type"); $stmt->execute(array( ":id" => $_data, ":type" => "domain" )); $row = $stmt->fetch(PDO::FETCH_ASSOC); - + if (empty($row)){ return false; } - + $row["attributes"] = json_decode($row["attributes"], true); return $row; } @@ -4440,11 +4580,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $stmt = $pdo->prepare("SELECT * FROM `templates` WHERE `type` = 'domain'"); $stmt->execute(); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - + if (empty($rows)){ return false; } - + foreach($rows as $key => $row){ $rows[$key]["attributes"] = json_decode($row["attributes"], true); } @@ -4610,19 +4750,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } $_data = (isset($_data)) ? intval($_data) : null; - if (isset($_data)){ - $stmt = $pdo->prepare("SELECT * FROM `templates` + if (isset($_data)){ + $stmt = $pdo->prepare("SELECT * FROM `templates` WHERE `id` = :id AND type = :type"); $stmt->execute(array( ":id" => $_data, ":type" => "mailbox" )); $row = $stmt->fetch(PDO::FETCH_ASSOC); - + if (empty($row)){ return false; } - + $row["attributes"] = json_decode($row["attributes"], true); return $row; } @@ -5064,7 +5204,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $ids = $_data['ids']; } - + foreach ($ids as $id) { // delete template $stmt = $pdo->prepare("DELETE FROM `templates` @@ -5377,7 +5517,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); continue; } - + update_sogo_static_view($username); $_SESSION['return'][] = array( 'type' => 'success', @@ -5404,7 +5544,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $ids = $_data['ids']; } - + foreach ($ids as $id) { // delete template $stmt = $pdo->prepare("DELETE FROM `templates` @@ -5413,7 +5553,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ":id" => $id, ":type" => "mailbox", ":template" => "Default" - )); + )); } $_SESSION['return'][] = array( @@ -5487,7 +5627,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); } break; - case 'tags_domain': + case 'tags_domain': if (!is_array($_data['domain'])) { $domains = array(); $domains[] = $_data['domain']; @@ -5500,7 +5640,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $wasModified = false; - foreach ($domains as $domain) { + foreach ($domains as $domain) { if (!is_valid_domain_name($domain)) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -5517,7 +5657,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); return false; } - + foreach($tags as $tag){ // delete tag $wasModified = true; @@ -5572,7 +5712,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // delete tags foreach($tags as $tag){ $wasModified = true; - + $stmt = $pdo->prepare("DELETE FROM `tags_mailbox` WHERE `username` = :username AND `tag_name` = :tag_name"); $stmt->execute(array( ':username' => $username, diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index 5c625e414..34e47a544 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -18,7 +18,7 @@ if (isset($_POST["pw_reset_request"]) && !empty($_POST['username'])) { if (isset($_POST["pw_reset"])) { $username = reset_password("check", $_POST['token']); $reset_result = reset_password("reset", array( - 'new_password' => $_POST['new_password'], + 'new_password' => $_POST['new_password'], 'new_password2' => $_POST['new_password2'], 'token' => $_POST['token'], 'username' => $username, @@ -52,7 +52,7 @@ if (isset($_POST["verify_tfa_login"])) { unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_tfa_methods']); - + header("Location: /user"); } } else { @@ -89,27 +89,30 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { if ($as == "admin") { $_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_role'] = "admin"; - header("Location: /admin"); + header("Location: /debug"); + die(); } elseif ($as == "domainadmin") { $_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_role'] = "domainadmin"; header("Location: /mailbox"); + die(); } elseif ($as == "user") { $_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_role'] = "user"; - $http_parameters = explode('&', $_SESSION['index_query_string']); - unset($_SESSION['index_query_string']); - if (in_array('mobileconfig', $http_parameters)) { - if (in_array('only_email', $http_parameters)) { - header("Location: /mobileconfig.php?only_email"); - die(); - } - header("Location: /mobileconfig.php"); - die(); - } + $http_parameters = explode('&', $_SESSION['index_query_string']); + unset($_SESSION['index_query_string']); + if (in_array('mobileconfig', $http_parameters)) { + if (in_array('only_email', $http_parameters)) { + header("Location: /mobileconfig.php?only_email"); + die(); + } + header("Location: /mobileconfig.php"); + die(); + } header("Location: /user"); + die(); } elseif ($as != "pending") { unset($_SESSION['pending_mailcow_cc_username']); diff --git a/data/web/js/site/edit.js b/data/web/js/site/edit.js index d68954989..f9fe707c6 100644 --- a/data/web/js/site/edit.js +++ b/data/web/js/site/edit.js @@ -58,6 +58,11 @@ $(document).ready(function() { $('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val()); }); + $("#show_mailbox_rename_form").click(function() { + $("#rename_warning").hide(); + $("#rename_form").removeClass("d-none"); + }); + // load tags if ($('#tags').length){ var tagsEl = $('#tags').parent().find('.tag-values')[0]; diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js index 51dbcf435..2c9fba8f5 100644 --- a/data/web/js/site/mailbox.js +++ b/data/web/js/site/mailbox.js @@ -2354,7 +2354,7 @@ jQuery(function($){ else $(tab).find(".table_collapse_option").hide(); } - + function filterByDomain(json, column, table){ var tableId = $(table.table().container()).attr('id'); // Create the `select` element @@ -2377,12 +2377,12 @@ jQuery(function($){ } }); }); - + // get unique domain list domains = domains.filter(function(value, index, array) { return array.indexOf(value) === index; }); - + // add domains to select domains.forEach(function(domain) { select.append($('')); diff --git a/data/web/json_api.php b/data/web/json_api.php index e14dd9962..66054e6d3 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -509,7 +509,7 @@ if (isset($_GET['query'])) { print(json_encode($getArgs)); $_SESSION['challenge'] = $WebAuthn->getChallenge(); return; - break; + break; case "fail2ban": if (!isset($_SESSION['mailcow_cc_role'])){ switch ($object) { @@ -2020,6 +2020,9 @@ if (isset($_GET['query'])) { case "rl-mbox": process_edit_return(ratelimit('edit', 'mailbox', array_merge(array('object' => $items), $attr))); break; + case "rename-mbox": + process_edit_return(mailbox('edit', 'mailbox_rename', array_merge(array('mailbox' => $items), $attr))); + break; case "user-acl": process_edit_return(acl('edit', 'user', array_merge(array('username' => $items), $attr))); break; diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 189774eec..79a6dc383 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -641,6 +641,10 @@ "mailbox": "Mailbox bearbeiten", "mailbox_quota_def": "Standard-Quota einer Mailbox", "mailbox_relayhost_info": "Wird auf eine Mailbox und direkte Alias-Adressen angewendet. Überschreibt die Einstellung einer Domain.", + "mailbox_rename": "Mailbox umbennnen", + "mailbox_rename_agree": "Ich habe ein Backup erstellt.", + "mailbox_rename_warning": "WICHTIG! Vor dem Umbenennen der Mailbox ein Backup erstellen.", + "mailbox_rename_alias": "Alias automatisch erstellen", "max_aliases": "Max. Aliasse", "max_mailboxes": "Max. Mailboxanzahl", "max_quota": "Max. Größe per Mailbox (MiB)", @@ -764,7 +768,7 @@ "login": "Anmelden", "mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.", "new_password": "Neues Passwort", - "new_password_confirm": "Neues Passwort bestätigen", + "new_password_confirm": "Neues Passwort bestätigen", "other_logins": "Key Login", "password": "Passwort", "reset_password": "Passwort zurücksetzen", @@ -1084,6 +1088,7 @@ "mailbox_added": "Mailbox %s wurde angelegt", "mailbox_modified": "Änderungen an Mailbox %s wurden gespeichert", "mailbox_removed": "Mailbox %s wurde entfernt", + "mailbox_renamed": "Mailbox wurde von %s in %s umbenannt", "nginx_reloaded": "Nginx wurde neu geladen", "object_modified": "Änderungen an Objekt %s wurden gespeichert", "password_policy_saved": "Passwortrichtlinie wurde erfolgreich gespeichert", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index 600441809..7833d7e43 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -641,6 +641,10 @@ "mailbox": "Edit mailbox", "mailbox_quota_def": "Default mailbox quota", "mailbox_relayhost_info": "Applied to the mailbox and direct aliases only, does override a domain relayhost.", + "mailbox_rename": "Rename mailbox", + "mailbox_rename_agree": "I have created a backup.", + "mailbox_rename_warning": "IMPORTANT! Create a backup before renaming the mailbox.", + "mailbox_rename_alias": "Create alias automatically", "max_aliases": "Max. aliases", "max_mailboxes": "Max. possible mailboxes", "max_quota": "Max. quota per mailbox (MiB)", @@ -1091,6 +1095,7 @@ "mailbox_added": "Mailbox %s has been added", "mailbox_modified": "Changes to mailbox %s have been saved", "mailbox_removed": "Mailbox %s has been removed", + "mailbox_renamed": "Mailbox was renamed from %s to %s", "nginx_reloaded": "Nginx was reloaded", "object_modified": "Changes to object %s have been saved", "password_policy_saved": "Password policy was saved successfully", diff --git a/data/web/templates/edit/mailbox.twig b/data/web/templates/edit/mailbox.twig index 8de0095f2..b21ff2eec 100644 --- a/data/web/templates/edit/mailbox.twig +++ b/data/web/templates/edit/mailbox.twig @@ -9,6 +9,7 @@ +
@@ -287,10 +288,10 @@
-
+
@@ -465,10 +466,10 @@ {% include 'mailbox/rl-frame.twig' %} - +
-
+

{{ lang.edit.mbox_rl_info }}

@@ -477,6 +478,55 @@
+
+
+
+ +
+
+
+
+

{{ lang.edit.mailbox_rename_warning }}

+
+
+
+ +
+
+
+
+
+ + + +
+
+
+ + @{{ result.domain }} +
+
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
{% else %} diff --git a/docker-compose.yml b/docker-compose.yml index cf0a028ff..86a033236 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -534,7 +534,7 @@ services: - watchdog dockerapi-mailcow: - image: mailcow/dockerapi:2.08 + image: mailcow/dockerapi:2.09 security_opt: - label=disable restart: always @@ -552,7 +552,7 @@ services: aliases: - dockerapi - + ##### Will be removed soon ##### solr-mailcow: image: mailcow/solr:1.8.3 From be5a181be5b4717c080faf05e47965e8273ea9ea Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Thu, 22 Aug 2024 10:10:05 +0200 Subject: [PATCH 02/37] [Web][DockerApi] migrate imap acl on mbox rename --- data/Dockerfiles/dockerapi/main.py | 8 +- .../dockerapi/modules/DockerApi.py | 81 ++++++++++++++++++- data/web/inc/functions.mailbox.inc.php | 32 ++++++++ 3 files changed, 115 insertions(+), 6 deletions(-) diff --git a/data/Dockerfiles/dockerapi/main.py b/data/Dockerfiles/dockerapi/main.py index fca61bb02..6f7a6042c 100644 --- a/data/Dockerfiles/dockerapi/main.py +++ b/data/Dockerfiles/dockerapi/main.py @@ -90,7 +90,7 @@ async def get_container(container_id : str): if container._id == container_id: container_info = await container.show() return Response(content=json.dumps(container_info, indent=4), media_type="application/json") - + res = { "type": "danger", "msg": "no container found" @@ -130,7 +130,7 @@ async def get_containers(): async def post_containers(container_id : str, post_action : str, request: Request): global dockerapi - try : + try: request_json = await request.json() except Exception as err: request_json = {} @@ -191,7 +191,7 @@ async def post_container_update_stats(container_id : str): stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats')) return Response(content=json.dumps(stats, indent=4), media_type="application/json") - + # PubSub Handler async def handle_pubsub_messages(channel: aioredis.client.PubSub): @@ -244,7 +244,7 @@ async def handle_pubsub_messages(channel: aioredis.client.PubSub): dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json)) else: dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json)) - + await asyncio.sleep(0.0) except asyncio.TimeoutError: pass diff --git a/data/Dockerfiles/dockerapi/modules/DockerApi.py b/data/Dockerfiles/dockerapi/modules/DockerApi.py index 9fdd039e4..da0cc34ba 100644 --- a/data/Dockerfiles/dockerapi/modules/DockerApi.py +++ b/data/Dockerfiles/dockerapi/modules/DockerApi.py @@ -353,14 +353,14 @@ class DockerApi: 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 = "if [[ -d '/var/vmail/" + vmail_name + "' ]]; then /bin/mv '/var/vmail/" + vmail_name + "' '/var/vmail/" + new_vmail_name + "'; fi" + 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 = "if [[ -d '/var/vmail_index/" + index_name + "' ]]; then /bin/mv '/var/vmail_index/" + index_name + "' '/var/vmail_index/" + new_index_name + "_index'; fi" + 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] @@ -412,6 +412,83 @@ class DockerApi: sogo_return = container.exec_run(['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: dovecot - task: get_acl + def container_post__exec__dovecot__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): + vmail_name = request_json['maildir'].replace("'", "'\\''") + shared_folders = container.exec_run(["/bin/bash", "-c", f"find /var/vmail/{vmail_name}/Maildir/Shared -mindepth 2 -type d | sed 's:^/var/vmail/{vmail_name}/Maildir/Shared/::' | tr '/' '\\\\'"]) + shared_folders = shared_folders.output.decode('utf-8') + formatted_acls = [] + if shared_folders: + shared_folders = shared_folders.splitlines() + for shared_folder in shared_folders: + if "\\." not in shared_folder: + continue + shared_folder = shared_folder.split("\\.") + acls = container.exec_run(["/bin/bash", "-c", f"doveadm acl get -u {shared_folder[0]} {shared_folder[1]}"]) + acls = acls.output.decode('utf-8').strip().splitlines() + if len(acls) >= 2: + for acl in acls[1:]: + id, rights = acls[1].split(maxsplit=1) + id = id.replace("user=", "") + formatted_acls.append({ 'user': shared_folder[0], 'id': id, 'mailbox': shared_folder[1], 'rights': rights.split() }) + + return Response(content=json.dumps(formatted_acls, indent=4), media_type="application/json") + # api call: container_post - post_action: exec - cmd: dovecot - task: delete_acl + def container_post__exec__dovecot__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: dovecot - task: set_acl + def container_post__exec__dovecot__set_acl(self, request_json, **kwargs): + if 'container_id' in kwargs: + filters = {"id": kwargs['container_id']} + elif 'container_name' in kwargs: + filters = {"name": kwargs['container_name']} + + for container in self.sync_docker_client.containers.list(filters=filters): + user = request_json['user'].replace("'", "'\\''") + mailbox = request_json['mailbox'].replace("'", "'\\''") + id = request_json['id'].replace("'", "'\\''") + rights = "" + + available_rights = [ + "admin", + "create", + "delete", + "expunge", + "insert", + "lookup", + "post", + "read", + "write", + "write-deleted", + "write-seen" + ] + for right in request_json['rights']: + right = right.replace("'", "'\\''").lower() + if right in available_rights: + rights += right + " " + + if user and mailbox and id and rights: + acl_set_return = container.exec_run(["/bin/bash", "-c", f'doveadm acl set -u {user} {mailbox} "user={id}" {rights}']) + return self.exec_run_handler('generic', acl_set_return) + # Collect host stats async def get_host_stats(self, wait=5): diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 35e21f1c8..762f233eb 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3247,6 +3247,25 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { return false; } + // get imap acls + $exec_fields = array( + 'cmd' => 'dovecot', + 'task' => 'get_acl', + 'maildir' => $domain . '/' . $old_local_part, + ); + $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' => 'dovecot', + 'task' => 'delete_acl', + 'user' => $imap_acl['user'], + 'mailbox' => $imap_acl['mailbox'], + 'id' => $imap_acl['id'] + ); + docker('post', 'dovecot-mailcow', 'exec', $exec_fields); + } + // rename username in sql try { $pdo->beginTransaction(); @@ -3326,6 +3345,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); docker('post', 'sogo-mailcow', 'exec', $exec_fields); + // set imap acls + foreach ($imap_acls as $imap_acl) { + $exec_fields = array( + 'cmd' => 'dovecot', + 'task' => 'set_acl', + 'user' => $imap_acl['user'], + 'mailbox' => $imap_acl['mailbox'], + 'id' => $new_username, + 'rights' => $imap_acl['rights'] + ); + docker('post', 'dovecot-mailcow', 'exec', $exec_fields); + } + // create alias if ($create_alias == 1) { mailbox("add", "alias", array( From 8e7b27aae469d5dbe757779cd11df6eb11430b8c Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Fri, 23 Aug 2024 09:30:23 +0200 Subject: [PATCH 03/37] [DockerApi] rework doveadm__get_acl function --- .../dockerapi/modules/DockerApi.py | 52 +++++++++++-------- data/web/inc/functions.mailbox.inc.php | 8 +-- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/data/Dockerfiles/dockerapi/modules/DockerApi.py b/data/Dockerfiles/dockerapi/modules/DockerApi.py index da0cc34ba..909ac2872 100644 --- a/data/Dockerfiles/dockerapi/modules/DockerApi.py +++ b/data/Dockerfiles/dockerapi/modules/DockerApi.py @@ -412,35 +412,45 @@ class DockerApi: sogo_return = container.exec_run(['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: dovecot - task: get_acl - def container_post__exec__dovecot__get_acl(self, request_json, **kwargs): + # 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): - vmail_name = request_json['maildir'].replace("'", "'\\''") - shared_folders = container.exec_run(["/bin/bash", "-c", f"find /var/vmail/{vmail_name}/Maildir/Shared -mindepth 2 -type d | sed 's:^/var/vmail/{vmail_name}/Maildir/Shared/::' | tr '/' '\\\\'"]) + 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 = [] - if shared_folders: - shared_folders = shared_folders.splitlines() - for shared_folder in shared_folders: - if "\\." not in shared_folder: - continue - shared_folder = shared_folder.split("\\.") - acls = container.exec_run(["/bin/bash", "-c", f"doveadm acl get -u {shared_folder[0]} {shared_folder[1]}"]) - acls = acls.output.decode('utf-8').strip().splitlines() - if len(acls) >= 2: - for acl in acls[1:]: - id, rights = acls[1].split(maxsplit=1) - id = id.replace("user=", "") - formatted_acls.append({ 'user': shared_folder[0], 'id': id, 'mailbox': shared_folder[1], 'rights': rights.split() }) + mailbox_seen = [] + for shared_folder in shared_folders: + if "Shared" not in shared_folder and "/" not in shared_folder: + continue + shared_folder = shared_folder.split("/") + if len(shared_folder) < 3: + continue + + user = shared_folder[1] + mailbox = '/'.join(shared_folder[2:]) + 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:]: + _, rights = acls[1].split(maxsplit=1) + 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: dovecot - task: delete_acl - def container_post__exec__dovecot__delete_acl(self, request_json, **kwargs): + # 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: @@ -454,8 +464,8 @@ class DockerApi: 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: dovecot - task: set_acl - def container_post__exec__dovecot__set_acl(self, request_json, **kwargs): + # 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: diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 762f233eb..baf635dda 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3249,15 +3249,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // get imap acls $exec_fields = array( - 'cmd' => 'dovecot', + 'cmd' => 'doveadm', 'task' => 'get_acl', - 'maildir' => $domain . '/' . $old_local_part, + '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' => 'dovecot', + 'cmd' => 'doveadm', 'task' => 'delete_acl', 'user' => $imap_acl['user'], 'mailbox' => $imap_acl['mailbox'], @@ -3348,7 +3348,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // set imap acls foreach ($imap_acls as $imap_acl) { $exec_fields = array( - 'cmd' => 'dovecot', + 'cmd' => 'doveadm', 'task' => 'set_acl', 'user' => $imap_acl['user'], 'mailbox' => $imap_acl['mailbox'], From 822d9a7de6e72efbfecc882245d47629ba26cc9e Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Tue, 27 Aug 2024 10:07:07 +0200 Subject: [PATCH 04/37] [Web] rename goto in alias table --- data/web/inc/functions.mailbox.inc.php | 22 ++++++++++++---------- data/web/lang/lang.de-de.json | 1 + data/web/lang/lang.en-gb.json | 1 + data/web/templates/edit/mailbox.twig | 9 ++++++--- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index baf635dda..6c24c13f5 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3271,6 +3271,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $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, @@ -3278,6 +3279,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':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', @@ -3287,9 +3295,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'da_acl' => 'username', 'quota2' => 'username', 'quota2replica' => 'username', - 'pushover' => 'username' + 'pushover' => 'username', + 'alias' => 'goto' ]; - foreach ($tables as $table => $column) { $pdo->prepare("UPDATE $table SET $column = :new_username WHERE $column = :old_username") ->execute([ @@ -3298,6 +3306,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ]); } + // 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, @@ -3306,14 +3315,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':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 - ]); - - // Re-enable foreign key checks $pdo->exec('SET FOREIGN_KEY_CHECKS = 1'); $pdo->commit(); @@ -3325,6 +3326,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'msg' => $e->getMessage() ); + return false; } // move maildir diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 79a6dc383..d22187c6a 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -645,6 +645,7 @@ "mailbox_rename_agree": "Ich habe ein Backup erstellt.", "mailbox_rename_warning": "WICHTIG! Vor dem Umbenennen der Mailbox ein Backup erstellen.", "mailbox_rename_alias": "Alias automatisch erstellen", + "mailbox_rename_title": "Neuer Lokaler Mailbox Name", "max_aliases": "Max. Aliasse", "max_mailboxes": "Max. Mailboxanzahl", "max_quota": "Max. Größe per Mailbox (MiB)", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index 7833d7e43..6e898099b 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -645,6 +645,7 @@ "mailbox_rename_agree": "I have created a backup.", "mailbox_rename_warning": "IMPORTANT! Create a backup before renaming the mailbox.", "mailbox_rename_alias": "Create alias automatically", + "mailbox_rename_title": "New local mailbox name", "max_aliases": "Max. aliases", "max_mailboxes": "Max. possible mailboxes", "max_quota": "Max. quota per mailbox (MiB)", diff --git a/data/web/templates/edit/mailbox.twig b/data/web/templates/edit/mailbox.twig index b21ff2eec..04be194c1 100644 --- a/data/web/templates/edit/mailbox.twig +++ b/data/web/templates/edit/mailbox.twig @@ -504,7 +504,10 @@
-
+
+ {{ lang.edit.mailbox_rename_title }} +
+
@{{ result.domain }} @@ -512,12 +515,12 @@
-
+
-
+
From d21c1bfa72b761507ff3ba47ff3454315356aa50 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Wed, 28 Aug 2024 10:48:44 +0200 Subject: [PATCH 05/37] [Web] add error handling for get_acl call --- data/web/inc/functions.mailbox.inc.php | 35 ++++++++++++++++---------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 6c24c13f5..9a1ae5777 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3248,22 +3248,31 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } // get imap acls - $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) { + try { $exec_fields = array( 'cmd' => 'doveadm', - 'task' => 'delete_acl', - 'user' => $imap_acl['user'], - 'mailbox' => $imap_acl['mailbox'], - 'id' => $imap_acl['id'] + 'task' => 'get_acl', + 'id' => $old_username ); - docker('post', 'dovecot-mailcow', 'exec', $exec_fields); + $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 From 4f9e37c0c3f8543406d83b452e91b22298cb0550 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Wed, 28 Aug 2024 11:16:29 +0200 Subject: [PATCH 06/37] [Web] rename user in bcc_maps, recipient_maps and imapsync table --- data/web/inc/functions.mailbox.inc.php | 35 +++++++++++++++----------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 9a1ae5777..9c01e78e1 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3297,22 +3297,27 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // 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' + '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'] ]; - foreach ($tables as $table => $column) { - $pdo->prepare("UPDATE $table SET $column = :new_username WHERE $column = :old_username") - ->execute([ - ':new_username' => $new_username, - ':old_username' => $old_username - ]); + 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 From f2e35dff68c1763c94a1b05eb389ae3b0ec4406b Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Thu, 5 Sep 2024 12:40:30 +0200 Subject: [PATCH 07/37] [Web] rename user in sender_acl table --- data/web/inc/functions.mailbox.inc.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 9c01e78e1..276e5629f 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3308,7 +3308,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'alias' => ['goto'], "imapsync" => ['user2'], 'bcc_maps' => ['local_dest', 'bcc_dest'], - 'recipient_maps' => ['old_dest', 'new_dest'] + 'recipient_maps' => ['old_dest', 'new_dest'], + 'sender_acl' => ['logged_in_as', 'send_as'] ]; foreach ($tables as $table => $columns) { foreach ($columns as $column) { From 22f7f61ac95c1d094efe0064ae692820339609a1 Mon Sep 17 00:00:00 2001 From: airon-assustadus <122687837+airon-assustadus@users.noreply.github.com> Date: Thu, 5 Sep 2024 10:09:49 -0300 Subject: [PATCH 08/37] feat/brazilian-translations (#6048) # What - Adding some brazilian translations that were missing Co-authored-by: Airon Teixeira --- data/web/lang/lang.pt-br.json | 114 +++++++++++++++++++++++++++------- 1 file changed, 92 insertions(+), 22 deletions(-) diff --git a/data/web/lang/lang.pt-br.json b/data/web/lang/lang.pt-br.json index 45dbf5147..0364bd191 100644 --- a/data/web/lang/lang.pt-br.json +++ b/data/web/lang/lang.pt-br.json @@ -14,6 +14,7 @@ "prohibited": "Proibido pela ACL", "protocol_access": "Alterar o acesso ao protocolo", "pushover": "Pushover", + "pw_reset": "Permite resetar a senha do usuário do Mailcow", "quarantine": "Ações de quarentena", "quarantine_attachments": "Anexos de quarentena", "quarantine_category": "Alterar categoria de notificação de quarentena", @@ -28,8 +29,7 @@ "spam_score": "Pontuação de spam", "syncjobs": "Trabalhos de sincronização", "tls_policy": "Política de TLS", - "unlimited_quota": "Cota ilimitada para mailboxes", - "pw_reset": "Permite redefinir a senha do usuário" + "unlimited_quota": "Cota ilimitada para mailboxes" }, "add": { "activate_filter_warn": "Todos os outros filtros serão desativados quando a opção ativa estiver marcada.", @@ -143,6 +143,7 @@ "api_read_only": "Acesso somente para leitura", "api_read_write": "Acesso de leitura e gravação", "api_skip_ip_check": "Ignorar verificação de IP para API", + "app_hide": "Esconder para login", "app_links": "Links de aplicativos", "app_name": "Nome do aplicativo", "apps_name": "Nome “mailcow Apps”", @@ -155,6 +156,7 @@ "logo_dark_label": "Invertido para o modo escuro", "configuration": "Configuração", "convert_html_to_text": "Converter HTML em texto sem formatação", + "copy_to_clipboard": "Text copied to clipboard!", "cors_settings": "Configurações do CORS", "credentials_transport_warning": "Aviso: Adicionar uma nova entrada no mapa de transporte atualizará as credenciais de todas as entradas com uma coluna correspondente do próximo salto.", "customer_id": "ID do cliente", @@ -188,6 +190,8 @@ "f2b_blacklist": "Redes/hosts na lista negra", "f2b_filter": "Filtros Regex", "f2b_list_info": "Um host ou rede na lista negra sempre superará uma entidade na lista branca. As atualizações da lista levarão alguns segundos para serem aplicadas.", + "f2b_manage_external": "Gerenciar Fail2Ban externamente", + "f2b_manage_external_info": "O Fail2ban ainda manterá a lista de banimentos, mas não definirá ativamente regras para bloquear o tráfego. Use a lista de banimento gerada abaixo para bloquear externamente o tráfego.", "f2b_max_attempts": "Máximo de tentativas", "f2b_max_ban_time": "Tempo (s) máximo (s) de banimento", "f2b_netban_ipv4": "Tamanho da sub-rede IPv4 a ser proibida (8-32)", @@ -196,6 +200,7 @@ "f2b_regex_info": "Registros considerados: SoGo, Postfix, Dovecot, PHP-FPM.", "f2b_retry_window": "Repita a (s) janela (s) para o máximo de tentativas", "f2b_whitelist": "Redes/hosts incluídos na lista branca", + "filter": "Filtro", "filter_table": "Tabela de filtros", "forwarding_hosts": "Anfitriões de encaminhamento", "forwarding_hosts_add_hint": "Você pode especificar endereços IPv4/IPv6, redes em notação CIDR, nomes de host (que serão resolvidos para endereços IP) ou nomes de domínio (que serão resolvidos para endereços IP consultando registros SPF ou, na ausência deles, registros MX).", @@ -208,6 +213,38 @@ "help_text": "Substituir texto de ajuda abaixo da máscara de login (HTML permitido)", "host": "Anfitrião", "html": "HTML", + "iam": "Provedor de Identificação", + "iam_attribute_field": "Campo de Atributo", + "iam_authorize_url": "Endpoint de Autorização", + "iam_auth_flow": "Fluxo de Autenticação", + "iam_auth_flow_info": "Além do fluxo de código de autorização (Standard Flow no Keycloak), que é usado para login no Single-Sign On, o mailcow também suporta o fluxo de autenticação com credenciais diretas. O fluxo Mailpassword tenta validar as credenciais do usuário usando a API REST Admin do Keycloak. O mailcow recupera a senha com o hash do atributo mailcow_password, que é mapeado no Keycloak.", + "iam_basedn": "DN Base", + "iam_client_id": "ID do Cliente", + "iam_client_secret": "Secret do Cliente", + "iam_client_scopes": "Escopos do Cliente", + "iam_description": "Configure um provedor externo para autenticação
As caixas de emails dos usuários serão automaticamente criadas no primeiro login, garanta que um atributo de mapeamento foi definido.", + "iam_extra_permission": "Para que as seguintes configurações funcionem, o cliente do mailcow no Keycloak precisa de um Service Account e de permissão para view-users", + "iam_host": "Servidor", + "iam_import_users": "Importar usuários", + "iam_mapping": "Atributo de mapeamento", + "iam_bindpass": "Senha de monitoramento", + "iam_periodic_full_sync": "Sincronização completa periódica", + "iam_port": "Porta", + "iam_realm": "Realm", + "iam_redirect_url": "Url de redirecionamento", + "iam_rest_flow": "Fluxo de Mailpassword", + "iam_server_url": "URL do Servidor", + "iam_sso": "SSO", + "iam_sync_interval": "Sincronizar / Intervalo de importação (min)", + "iam_test_connection": "Testar conexão", + "iam_token_url": "Endpoint do Token", + "iam_userinfo_url": "Endpoint de informações do Usuário", + "iam_username_field": "Campo de nome do usuário (username)", + "iam_binddn": "DN de monitoramento", + "iam_use_ssl": "Usar SSL", + "iam_use_tls": "Usar TLS", + "iam_version": "Versão", + "ignore_ssl_error": "Ignora erros de SSL", "import": "Importar", "import_private_key": "Importar chave privada", "in_use_by": "Em uso por", @@ -254,6 +291,11 @@ "password_policy_numbers": "Deve conter pelo menos um número", "password_policy_special_chars": "Deve conter caracteres especiais", "password_repeat": "Senha de confirmação (repetição)", + "password_reset_info": "Se nenhum email de recuperação for definido, esta função não poderá ser utilizada.", + "password_reset_settings": "Configurações de recuperação de senha", + "password_reset_tmpl_html": "Template HTML", + "password_reset_tmpl_text": "Template de Texto", + "password_settings": "Configurações de senha", "priority": "Prioridade", "private_key": "Chave privada", "quarantine": "Quarentena", @@ -294,6 +336,8 @@ "remove_row": "Remover linha", "reset_default": "Redefinir para o padrão", "reset_limit": "Remover o hash", + "reset_password_vars": "{{link}} Link de recuperação de senha gerado
{{username}} nome da caixa de email do usuário que requisitou a recuperação de senha
{{username2}} Nome da caixa de email recuperada
{{date}} Data que a requisição de recuperação de senha foi feita
{{token_lifetime}} O tempo de vida em minutos do token
{{hostname}} O servidor do mailcow", + "restore_template": "Deixe limpo para o template padrão de recuperação.", "routing": "Roteamento", "rsetting_add_rule": "Adicionar regra", "rsetting_content": "Conteúdo da regra", @@ -323,6 +367,7 @@ "subject": "Assunto", "success": "Sucesso", "sys_mails": "E-mails do sistema", + "task": "Tarefa", "text": "Texto", "time": "Hora", "title": "Título", @@ -347,12 +392,10 @@ "unchanged_if_empty": "Se inalterado, deixe em branco", "upload": "Carregar", "username": "Nome de usuário", + "user_link": "Link do User", "validate_license_now": "Valide o GUID em relação ao servidor de licenças", "verify": "Verificar", - "yes": "✓", - "copy_to_clipboard": "Texto copiado para a área de transferência!", - "f2b_manage_external": "Gerenciar Fail2Ban externamente", - "f2b_manage_external_info": "O Fail2ban ainda manterá a lista de banimentos, mas não definirá ativamente regras para bloquear o tráfego. Use a lista de banimento gerada abaixo para bloquear externamente o tráfego." + "yes": "✓" }, "danger": { "access_denied": "Acesso negado ou dados de formulário inválidos", @@ -364,6 +407,7 @@ "aliases_in_use": "O máximo de aliases deve ser maior ou igual a %d", "app_name_empty": "O nome do aplicativo não pode estar vazio", "app_passwd_id_invalid": "ID de senha do aplicativo %s inválida", + "authsource_in_use": "O provedor de identificação não pode ser alterado ou removido por estar sendo utilizado por um ou mais usuários.", "bcc_empty": "O destino do BCC não pode estar vazio", "bcc_exists": "Existe um mapa BCC %s para o tipo %s", "bcc_must_be_email": "O destino %s do BCC não é um endereço de e-mail válido", @@ -394,8 +438,11 @@ "goto_empty": "Um endereço de alias deve conter pelo menos um endereço de destino válido", "goto_invalid": "O endereço Goto %s é inválido", "ham_learn_error": "Erro de aprendizado do Ham: %s", + "iam_test_connection": "Falha na conexão", "imagick_exception": "Erro: exceção Imagick ao ler a imagem", + "img_dimensions_exceeded": "A imagem excede o tamanho máximo", "img_invalid": "Não é possível validar o arquivo de imagem", + "img_size_exceeded": "A imagem excede o tamanho máximo", "img_tmp_missing": "Não é possível validar o arquivo de imagem: Arquivo temporário não encontrado", "invalid_bcc_map_type": "Tipo de mapa BCC inválido", "invalid_destination": "O formato de destino “%s” é inválido", @@ -406,6 +453,7 @@ "invalid_nexthop_authenticated": "O próximo salto existe com credenciais diferentes. Primeiro, atualize as credenciais existentes para o próximo salto.", "invalid_recipient_map_new": "Novo destinatário inválido especificado: %s", "invalid_recipient_map_old": "Destinatário original inválido especificado: %s", + "invalid_reset_token": "Token de recuperação inválido", "ip_list_empty": "A lista de IPs permitidos não pode estar vazia", "is_alias": "%s já é conhecido como endereço de alias", "is_alias_or_mailbox": "%s já é conhecido como alias, mailbox ou alias de endereço expandido a partir de um domínio de alias.", @@ -435,6 +483,8 @@ "password_complexity": "A senha não atende à política", "password_empty": "A senha não deve estar vazia", "password_mismatch": "A senha de confirmação não corresponde", + "password_reset_invalid_user": "Caixa de email não encontrada ou email de recuperação não definido", + "password_reset_na": "A recuperação de senha está indisponível no momento. Por favor, contacte o administrador.", "policy_list_from_exists": "Existe um registro com nome próprio", "policy_list_from_invalid": "O registro tem formato inválido", "private_key_error": "Erro de chave privada: %s", @@ -443,10 +493,13 @@ "pushover_token": "O token Pushover tem um formato errado", "quota_not_0_not_numeric": "A cota deve ser numérica e >= 0", "recipient_map_entry_exists": "Existe uma entrada de mapa de destinatários “%s”", + "recovery_email_failed": "Não foi possível enviar um email de recuperação. Por favor, contacte seu administrador.", "redis_error": "Erro do Redis: %s", "relayhost_invalid": "A entrada de mapa %s é inválida", "release_send_failed": "A mensagem não pôde ser liberada: %s", + "required_data_missing": "O campo obrigatório %s está faltando", "reset_f2b_regex": "O filtro Regex não pôde ser redefinido a tempo. Tente novamente ou aguarde mais alguns segundos e recarregue o site.", + "reset_token_limit_exceeded": "O limite de token de recuperação foi excedido. Por favor tente novamente mais tarde.", "resource_invalid": "O nome do recurso %s é inválido", "rl_timeframe": "O prazo do limite de taxa está incorreto", "rspamd_ui_pw_length": "A senha do Rspamd UI deve ter pelo menos 6 caracteres", @@ -469,6 +522,7 @@ "tls_policy_map_dest_invalid": "O destino da política é inválido", "tls_policy_map_entry_exists": "Existe uma entrada “%s” no mapa de políticas TLS", "tls_policy_map_parameter_invalid": "O parâmetro de política é inválido", + "to_invalid": "Destinatário não pode ser vazio", "totp_verification_failed": "Falha na verificação do TOTP", "transport_dest_exists": "O destino de transporte “%s” existe", "webauthn_verification_failed": "Falha na verificação do WebAuthn: %s", @@ -481,9 +535,7 @@ "username_invalid": "O nome de usuário %s não pode ser usado", "validity_missing": "Por favor, atribua um período de validade", "value_missing": "Forneça todos os valores", - "yotp_verification_failed": "Falha na verificação do Yubico OTP: %s", - "img_dimensions_exceeded": "A imagem excede o tamanho máximo", - "img_size_exceeded": "A imagem excede o tamanho máximo" + "yotp_verification_failed": "Falha na verificação do Yubico OTP: %s" }, "datatables": { "collapse_all": "Recolher tudo", @@ -582,6 +634,7 @@ "client_secret": "Segredo do cliente", "comment_info": "Um comentário privado não é visível para o usuário, enquanto um comentário público é mostrado como dica de ferramenta ao passar o mouse sobre ele na visão geral do usuário", "created_on": "Criado em", + "custom_attributes": "Atributos personalizados", "delete1": "Excluir da fonte quando concluído", "delete2": "Excluir mensagens no destino que não estão na origem", "delete2duplicates": "Excluir duplicatas no destino", @@ -602,6 +655,7 @@ "custom": "{= foo =} - Se o mailbox tiver o atributo personalizado \"foo\" com valor \"bar\", retornará \"bar\"" }, "domain_footer_plain": "Rodapé simples", + "domain_footer_skip_replies": "Ignore o rodapé nos e-mails de resposta", "domain_quota": "Cota de domínio", "domains": "Domínios", "dont_check_sender_acl": "Desativar a verificação de remetente para o domínio %s (+ domínios de alias)", @@ -637,6 +691,7 @@ "none_inherit": "Nenhum/Herdar", "nexthop": "Próximo salto", "password": "Senha", + "password_recovery_email": "Email de recuperação de senha", "password_repeat": "Senha de confirmação (repetição)", "previous": "Página anterior", "private_comment": "Comentário privado", @@ -673,8 +728,8 @@ "sieve_desc": "Breve descrição", "sieve_type": "Tipo de filtro", "skipcrossduplicates": "Ignore mensagens duplicadas entre pastas (primeiro a chegar, primeiro a ser servido)", - "sogo_access": "Conceder acesso de login direto ao SoGo", - "sogo_access_info": "O login único de dentro da interface do usuário de e-mail continua funcionando. Essa configuração não afeta o acesso a todos os outros serviços nem exclui ou altera o perfil SoGo existente de um usuário.", + "sogo_access": "Encaminhamento direto para o SOGoo", + "sogo_access_info": "Depois de fazer login, o usuário é automaticamente redirecionado para o SOGo.", "sogo_visible": "O alias é visível no SoGo", "sogo_visible_info": "Essa opção afeta somente objetos, que podem ser exibidos no SoGo (endereços de alias compartilhados ou não compartilhados apontando para pelo menos uma mailbox local). Se estiver oculto, um alias não aparecerá como remetente selecionável no SoGo.", "spam_alias": "Crie ou altere endereços de alias com limite de tempo", @@ -690,9 +745,7 @@ "title": "Editar objeto", "unchanged_if_empty": "Se inalterado, deixe em branco", "username": "Nome de usuário", - "validate_save": "Valide e salve", - "custom_attributes": "Atributos personalizados", - "domain_footer_skip_replies": "Ignore o rodapé nos e-mails de resposta" + "validate_save": "Valide e salve" }, "fido2": { "confirm": "Confirme", @@ -742,12 +795,19 @@ "session_expires": "Sua sessão expirará em cerca de 15 segundos" }, "login": { + "back_to_mailcow": "Voltar para o mailcow", "delayed": "O login foi atrasado em %s segundos.", "fido2_webauthn": "Login do FIDO2/WebAuthn", + "forgot_password": "> Esqueceu a senha?", + "invalid_pass_reset_token": "O token de recuperação de senha está inválido ou expirado.
Por favor solicite um novo link de recuperação de senha.", "login": "Login", "mobileconfig_info": "Faça login como usuário da mailbox para baixar o perfil de conexão Apple solicitado.", + "new_password": "Nova senha", + "new_password_confirm": "Confirmar nova senha", "other_logins": "Login com chave", "password": "Senha", + "reset_password": "Recuperar a senha", + "request_reset_password": "Solicitar troca de senha", "username": "Nome de usuário" }, "mailbox": { @@ -824,6 +884,7 @@ "goto_ham": "Aprenda como presunto", "goto_spam": "Aprenda como spam", "hourly": "A cada hora", + "iam": "Provedor de Identificação", "in_use": "Em uso (%)", "inactive": "Inativo", "insert_preset": "Inserir exemplo de predefinição “%s”", @@ -1053,11 +1114,13 @@ "domain_removed": "O domínio %s foi removido", "dovecot_restart_success": "O Dovecot foi reiniciado com sucesso", "eas_reset": "Os dispositivos ActiveSync para o usuário %s foram redefinidos", + "f2b_banlist_refreshed": "O Banlist ID foi atualizado com sucesso.", "f2b_modified": "As alterações nos parâmetros do Fail2ban foram salvas", "forwarding_host_added": "O host de encaminhamento %s foi adicionado", "forwarding_host_removed": "O host de encaminhamento %s foi removido", "global_filter_written": "O filtro foi gravado com sucesso no arquivo", "hash_deleted": "Hash excluído", + "iam_test_connection": "Conexão realizada com sucesso", "ip_check_opt_in_modified": "A verificação de IP foi salva com sucesso", "item_deleted": "Item %s excluído com sucesso", "item_released": "Item %s lançado", @@ -1072,11 +1135,13 @@ "nginx_reloaded": "O Nginx foi recarregado", "object_modified": "As alterações no objeto %s foram salvas", "password_policy_saved": "A política de senha foi salva com sucesso", + "password_changed_success": "Alteração de senha realizada com sucesso", "pushover_settings_edited": "Configurações de Pushover definidas com sucesso. Verifique as credenciais.", "qlearn_spam": "A ID da mensagem %s foi detectada como spam e excluída", "queue_command_success": "Comando de fila concluído com sucesso", "recipient_map_entry_deleted": "A ID do mapa do destinatário %s foi excluída", "recipient_map_entry_saved": "A entrada “%s” do mapa do destinatário foi salva", + "recovery_email_sent": "Email de recuperação enviado para %s", "relayhost_added": "A entrada de mapa %s foi adicionada", "relayhost_removed": "A entrada de mapa %s foi removida", "reset_main_logo": "Redefinir para o logotipo padrão", @@ -1099,10 +1164,10 @@ "verified_fido2_login": "Login FIDO2 verificado", "verified_totp_login": "Login TOTP verificado", "verified_webauthn_login": "Login verificado do WebAuthn", - "verified_yotp_login": "Login OTP verificado do Yubico", - "f2b_banlist_refreshed": "O Banlist ID foi atualizado com sucesso." + "verified_yotp_login": "Login OTP verificado do Yubico" }, "tfa": { + "authenticators": "Autenticadores", "api_register": "%s usa a API Yubico Cloud. Obtenha uma chave de API para sua chave aqui", "confirm": "Confirme", "confirm_totp_token": "Confirme suas alterações inserindo o token gerado", @@ -1127,8 +1192,7 @@ "webauthn": "Autenticação WebAuthn", "waiting_usb_auth": "Aguardando o dispositivo USB...

Toque no botão no seu dispositivo USB agora.", "waiting_usb_register": "Aguardando o dispositivo USB...

Digite sua senha acima e confirme seu registro tocando no botão no seu dispositivo USB.", - "yubi_otp": "Autenticação Yubico OTP", - "authenticators": "Autenticadores" + "yubi_otp": "Autenticação Yubico OTP" }, "user": { "action": "Ação", @@ -1153,6 +1217,8 @@ "apple_connection_profile_complete": "Esse perfil de conexão inclui parâmetros IMAP e SMTP, bem como caminhos CalDAV (calendários) e CardDAV (contatos) para um dispositivo Apple.", "apple_connection_profile_mailonly": "Esse perfil de conexão inclui parâmetros de configuração IMAP e SMTP para um dispositivo Apple.", "apple_connection_profile_with_app_password": "Uma nova senha de aplicativo é gerada e adicionada ao perfil para que nenhuma senha precise ser inserida ao configurar seu dispositivo. Não compartilhe o arquivo, pois ele concede acesso total à sua mailbox.", + "attribute": "Atributo", + "authentication": "Autenticação", "change_password": "Alterar senha", "change_password_hint_app_passwords": "Sua conta tem %d senhas de aplicativos que não serão alteradas. Para gerenciá-las, acesse a guia Senhas do aplicativo.", "clear_recent_successful_connections": "Conexões bem-sucedidas e claras", @@ -1206,10 +1272,13 @@ "no_last_login": "Nenhuma informação de login da última interface", "no_record": "Sem registro", "open_logs": "Registros abertos", - "open_webmail_sso": "Faça login no webmail", + "open_webmail_sso": "Webmail", + "overview": "Overview", "password": "Senha", "password_now": "Senha atual (confirme as alterações)", "password_repeat": "Senha (repetição)", + "password_reset_info": "Se nenhum email de recuperação for definido, esta função não poderá ser utilizada.", + "protocols": "Protocolos", "pushover_evaluate_x_prio": "Escale e-mails de alta prioridade [X-Priority: 1]", "pushover_info": "As configurações de notificação push serão aplicadas a todos os e-mails limpos (sem spam) entregues a %s, incluindo aliases (compartilhados, não compartilhados, marcados).", "pushover_only_x_prio": "Considere somente e-mails de alta prioridade [X-Priority: 1]", @@ -1220,6 +1289,7 @@ "pushover_sound": "Som", "pushover_vars": "Quando nenhum filtro de remetente for definido, todos os e-mails serão considerados.
Os filtros Regex, bem como as verificações exatas do remetente, podem ser definidos individualmente e serão considerados sequencialmente. Eles não dependem um do outro.
Variáveis utilizáveis para texto e título (observe as políticas de proteção de dados)", "pushover_verify": "Verifique as credenciais", + "pw_recovery_email": "Email de recuperação de senha", "q_add_header": "Pasta de lixo eletrônico", "q_all": "Todas as categorias", "q_reject": "Rejeitado", @@ -1276,6 +1346,7 @@ "tag_in_subfolder": "Na subpasta", "tag_in_subject": "No assunto", "text": "Texto", + "tfa_info": "Duplo fator de autenticação ajuda a proteger sua conta. Se você habilitar, você pode precisar de senhas de aplicativos para logar nos aplicativos ou serviços que não suportam duplo fator de autenticação (Ex. Clientes de email).", "title": "Título", "tls_enforce_in": "Imponha a entrada de TLS", "tls_enforce_out": "Imponha a saída TLS", @@ -1283,6 +1354,7 @@ "tls_policy_warning": "Aviso: Se você decidir impor a transferência de e-mail criptografada, poderá perder e-mails.
As mensagens que não satisfizerem a política serão devolvidas com uma falha grave pelo sistema de correio.
Essa opção se aplica ao seu endereço de e-mail principal (nome de login), a todos os endereços derivados de domínios de alias, bem como aos endereços de alias com apenas essa única mailbox como destino.", "user_settings": "Configurações do usuário", "username": "Nome de usuário", + "value": "Valor", "verify": "Verificar", "waiting": "Esperando", "week": "semana", @@ -1290,9 +1362,7 @@ "weeks": "semanas", "with_app_password": "com senha do aplicativo", "year": "ano", - "years": "anos", - "attribute": "Atributo", - "value": "Valor" + "years": "anos" }, "warning": { "cannot_delete_self": "Não é possível excluir o usuário conectado", From fe3d08515ead7b53732966a1434231768f977a91 Mon Sep 17 00:00:00 2001 From: milkmaker Date: Fri, 6 Sep 2024 07:13:59 +0200 Subject: [PATCH 09/37] [Web] Language file updated by 'Cleanup translation files' addon (#6064) --- data/web/lang/lang.pt-br.json | 45 ----------------------------------- 1 file changed, 45 deletions(-) diff --git a/data/web/lang/lang.pt-br.json b/data/web/lang/lang.pt-br.json index 0364bd191..2fbd43535 100644 --- a/data/web/lang/lang.pt-br.json +++ b/data/web/lang/lang.pt-br.json @@ -143,7 +143,6 @@ "api_read_only": "Acesso somente para leitura", "api_read_write": "Acesso de leitura e gravação", "api_skip_ip_check": "Ignorar verificação de IP para API", - "app_hide": "Esconder para login", "app_links": "Links de aplicativos", "app_name": "Nome do aplicativo", "apps_name": "Nome “mailcow Apps”", @@ -200,7 +199,6 @@ "f2b_regex_info": "Registros considerados: SoGo, Postfix, Dovecot, PHP-FPM.", "f2b_retry_window": "Repita a (s) janela (s) para o máximo de tentativas", "f2b_whitelist": "Redes/hosts incluídos na lista branca", - "filter": "Filtro", "filter_table": "Tabela de filtros", "forwarding_hosts": "Anfitriões de encaminhamento", "forwarding_hosts_add_hint": "Você pode especificar endereços IPv4/IPv6, redes em notação CIDR, nomes de host (que serão resolvidos para endereços IP) ou nomes de domínio (que serão resolvidos para endereços IP consultando registros SPF ou, na ausência deles, registros MX).", @@ -213,38 +211,6 @@ "help_text": "Substituir texto de ajuda abaixo da máscara de login (HTML permitido)", "host": "Anfitrião", "html": "HTML", - "iam": "Provedor de Identificação", - "iam_attribute_field": "Campo de Atributo", - "iam_authorize_url": "Endpoint de Autorização", - "iam_auth_flow": "Fluxo de Autenticação", - "iam_auth_flow_info": "Além do fluxo de código de autorização (Standard Flow no Keycloak), que é usado para login no Single-Sign On, o mailcow também suporta o fluxo de autenticação com credenciais diretas. O fluxo Mailpassword tenta validar as credenciais do usuário usando a API REST Admin do Keycloak. O mailcow recupera a senha com o hash do atributo mailcow_password, que é mapeado no Keycloak.", - "iam_basedn": "DN Base", - "iam_client_id": "ID do Cliente", - "iam_client_secret": "Secret do Cliente", - "iam_client_scopes": "Escopos do Cliente", - "iam_description": "Configure um provedor externo para autenticação
As caixas de emails dos usuários serão automaticamente criadas no primeiro login, garanta que um atributo de mapeamento foi definido.", - "iam_extra_permission": "Para que as seguintes configurações funcionem, o cliente do mailcow no Keycloak precisa de um Service Account e de permissão para view-users", - "iam_host": "Servidor", - "iam_import_users": "Importar usuários", - "iam_mapping": "Atributo de mapeamento", - "iam_bindpass": "Senha de monitoramento", - "iam_periodic_full_sync": "Sincronização completa periódica", - "iam_port": "Porta", - "iam_realm": "Realm", - "iam_redirect_url": "Url de redirecionamento", - "iam_rest_flow": "Fluxo de Mailpassword", - "iam_server_url": "URL do Servidor", - "iam_sso": "SSO", - "iam_sync_interval": "Sincronizar / Intervalo de importação (min)", - "iam_test_connection": "Testar conexão", - "iam_token_url": "Endpoint do Token", - "iam_userinfo_url": "Endpoint de informações do Usuário", - "iam_username_field": "Campo de nome do usuário (username)", - "iam_binddn": "DN de monitoramento", - "iam_use_ssl": "Usar SSL", - "iam_use_tls": "Usar TLS", - "iam_version": "Versão", - "ignore_ssl_error": "Ignora erros de SSL", "import": "Importar", "import_private_key": "Importar chave privada", "in_use_by": "Em uso por", @@ -367,7 +333,6 @@ "subject": "Assunto", "success": "Sucesso", "sys_mails": "E-mails do sistema", - "task": "Tarefa", "text": "Texto", "time": "Hora", "title": "Título", @@ -392,7 +357,6 @@ "unchanged_if_empty": "Se inalterado, deixe em branco", "upload": "Carregar", "username": "Nome de usuário", - "user_link": "Link do User", "validate_license_now": "Valide o GUID em relação ao servidor de licenças", "verify": "Verificar", "yes": "✓" @@ -407,7 +371,6 @@ "aliases_in_use": "O máximo de aliases deve ser maior ou igual a %d", "app_name_empty": "O nome do aplicativo não pode estar vazio", "app_passwd_id_invalid": "ID de senha do aplicativo %s inválida", - "authsource_in_use": "O provedor de identificação não pode ser alterado ou removido por estar sendo utilizado por um ou mais usuários.", "bcc_empty": "O destino do BCC não pode estar vazio", "bcc_exists": "Existe um mapa BCC %s para o tipo %s", "bcc_must_be_email": "O destino %s do BCC não é um endereço de e-mail válido", @@ -438,7 +401,6 @@ "goto_empty": "Um endereço de alias deve conter pelo menos um endereço de destino válido", "goto_invalid": "O endereço Goto %s é inválido", "ham_learn_error": "Erro de aprendizado do Ham: %s", - "iam_test_connection": "Falha na conexão", "imagick_exception": "Erro: exceção Imagick ao ler a imagem", "img_dimensions_exceeded": "A imagem excede o tamanho máximo", "img_invalid": "Não é possível validar o arquivo de imagem", @@ -497,7 +459,6 @@ "redis_error": "Erro do Redis: %s", "relayhost_invalid": "A entrada de mapa %s é inválida", "release_send_failed": "A mensagem não pôde ser liberada: %s", - "required_data_missing": "O campo obrigatório %s está faltando", "reset_f2b_regex": "O filtro Regex não pôde ser redefinido a tempo. Tente novamente ou aguarde mais alguns segundos e recarregue o site.", "reset_token_limit_exceeded": "O limite de token de recuperação foi excedido. Por favor tente novamente mais tarde.", "resource_invalid": "O nome do recurso %s é inválido", @@ -884,7 +845,6 @@ "goto_ham": "Aprenda como presunto", "goto_spam": "Aprenda como spam", "hourly": "A cada hora", - "iam": "Provedor de Identificação", "in_use": "Em uso (%)", "inactive": "Inativo", "insert_preset": "Inserir exemplo de predefinição “%s”", @@ -1120,7 +1080,6 @@ "forwarding_host_removed": "O host de encaminhamento %s foi removido", "global_filter_written": "O filtro foi gravado com sucesso no arquivo", "hash_deleted": "Hash excluído", - "iam_test_connection": "Conexão realizada com sucesso", "ip_check_opt_in_modified": "A verificação de IP foi salva com sucesso", "item_deleted": "Item %s excluído com sucesso", "item_released": "Item %s lançado", @@ -1218,7 +1177,6 @@ "apple_connection_profile_mailonly": "Esse perfil de conexão inclui parâmetros de configuração IMAP e SMTP para um dispositivo Apple.", "apple_connection_profile_with_app_password": "Uma nova senha de aplicativo é gerada e adicionada ao perfil para que nenhuma senha precise ser inserida ao configurar seu dispositivo. Não compartilhe o arquivo, pois ele concede acesso total à sua mailbox.", "attribute": "Atributo", - "authentication": "Autenticação", "change_password": "Alterar senha", "change_password_hint_app_passwords": "Sua conta tem %d senhas de aplicativos que não serão alteradas. Para gerenciá-las, acesse a guia Senhas do aplicativo.", "clear_recent_successful_connections": "Conexões bem-sucedidas e claras", @@ -1273,12 +1231,10 @@ "no_record": "Sem registro", "open_logs": "Registros abertos", "open_webmail_sso": "Webmail", - "overview": "Overview", "password": "Senha", "password_now": "Senha atual (confirme as alterações)", "password_repeat": "Senha (repetição)", "password_reset_info": "Se nenhum email de recuperação for definido, esta função não poderá ser utilizada.", - "protocols": "Protocolos", "pushover_evaluate_x_prio": "Escale e-mails de alta prioridade [X-Priority: 1]", "pushover_info": "As configurações de notificação push serão aplicadas a todos os e-mails limpos (sem spam) entregues a %s, incluindo aliases (compartilhados, não compartilhados, marcados).", "pushover_only_x_prio": "Considere somente e-mails de alta prioridade [X-Priority: 1]", @@ -1346,7 +1302,6 @@ "tag_in_subfolder": "Na subpasta", "tag_in_subject": "No assunto", "text": "Texto", - "tfa_info": "Duplo fator de autenticação ajuda a proteger sua conta. Se você habilitar, você pode precisar de senhas de aplicativos para logar nos aplicativos ou serviços que não suportam duplo fator de autenticação (Ex. Clientes de email).", "title": "Título", "tls_enforce_in": "Imponha a entrada de TLS", "tls_enforce_out": "Imponha a saída TLS", From 220fdbb168792c07493db330d898b345cc902055 Mon Sep 17 00:00:00 2001 From: Hassan A Hashim Date: Fri, 6 Sep 2024 08:14:34 +0300 Subject: [PATCH 10/37] Add missing `Russian` translation (#6065) --- data/web/lang/lang.ru-ru.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/data/web/lang/lang.ru-ru.json b/data/web/lang/lang.ru-ru.json index 4a1092f27..792d32661 100644 --- a/data/web/lang/lang.ru-ru.json +++ b/data/web/lang/lang.ru-ru.json @@ -476,6 +476,10 @@ "debug": { "chart_this_server": "Диаграмма (текущий сервер)", "containers_info": "Статус контейнеров Docker", + "container_running": "Работающий", + "container_disabled": "Контейнер остановлен или отключен", + "container_stopped": "Остановлен", + "current_time": "Системное время", "disk_usage": "Использование дискового пространства", "docs": "Проиндексировано объектов", "external_logs": "Внешние журналы", @@ -496,6 +500,7 @@ "started_on": "Запущен в", "static_logs": "Статические журналы", "success": "Успех", + "no_update_available": "Система обновлена до последней версии", "system_containers": "Система и контейнеры", "uptime": "Время работы", "username": "Имя пользователя" @@ -674,6 +679,7 @@ "apps": "Приложения", "debug": "Состояние сервера", "email": "E-Mail", + "mailcow_system": "Система", "mailcow_config": "Конфигурация", "quarantine": "Карантин", "restart_netfilter": "Перезапустить netfilter", From 1528e8766a0af49e3eb378b479d03c41ff8ae75f Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Fri, 6 Sep 2024 15:59:52 +0200 Subject: [PATCH 11/37] [DockerApi] correctly escape user input --- data/Dockerfiles/dockerapi/modules/DockerApi.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/data/Dockerfiles/dockerapi/modules/DockerApi.py b/data/Dockerfiles/dockerapi/modules/DockerApi.py index 909ac2872..64bcc4d95 100644 --- a/data/Dockerfiles/dockerapi/modules/DockerApi.py +++ b/data/Dockerfiles/dockerapi/modules/DockerApi.py @@ -410,7 +410,7 @@ class DockerApi: old_username = request_json['old_username'].replace("'", "'\\''") new_username = request_json['new_username'].replace("'", "'\\''") - sogo_return = container.exec_run(['sogo-tool', 'rename-user', old_username, new_username], user='sogo') + 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): @@ -422,7 +422,7 @@ class DockerApi: 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 = 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() @@ -435,12 +435,12 @@ class DockerApi: if len(shared_folder) < 3: continue - user = shared_folder[1] - mailbox = '/'.join(shared_folder[2:]) + 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 = 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:]: @@ -462,7 +462,7 @@ class DockerApi: 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}"']) + 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): @@ -496,7 +496,7 @@ class DockerApi: 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}']) + 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) From fda95301ba62fa1b354d177aa0e88526d9e6fd9e Mon Sep 17 00:00:00 2001 From: Niklas Meyer Date: Tue, 15 Oct 2024 10:32:08 +0200 Subject: [PATCH 12/37] fix: added tls1.0/1.1 patch for openssl when using older tls versions in override (#6105) --- data/Dockerfiles/dovecot/docker-entrypoint.sh | 11 +++++++++++ data/Dockerfiles/postfix/docker-entrypoint.sh | 11 +++++++++++ docker-compose.yml | 4 ++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 2f0bfadf3..3cfdd77a6 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -405,6 +405,17 @@ else chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem fi +# Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20) +if grep -qE 'ssl_min_protocol\s*=\s*(TLSv1|TLSv1\.1)\s*$' /etc/dovecot/dovecot.conf /etc/dovecot/extra.conf; then + sed -i '/\[openssl_init\]/a ssl_conf = ssl_configuration' /etc/ssl/openssl.cnf + + echo "[ssl_configuration]" >> /etc/ssl/openssl.cnf + echo "system_default = tls_system_default" >> /etc/ssl/openssl.cnf + echo "[tls_system_default]" >> /etc/ssl/openssl.cnf + echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf + echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf +fi + # Compile sieve scripts sievec /var/vmail/sieve/global_sieve_before.sieve sievec /var/vmail/sieve/global_sieve_after.sieve diff --git a/data/Dockerfiles/postfix/docker-entrypoint.sh b/data/Dockerfiles/postfix/docker-entrypoint.sh index c97b12844..7b6c5d4aa 100755 --- a/data/Dockerfiles/postfix/docker-entrypoint.sh +++ b/data/Dockerfiles/postfix/docker-entrypoint.sh @@ -12,4 +12,15 @@ if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf fi +# Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20) +if grep -qE '\!SSLv2|\!SSLv3|>=TLSv1(\.[0-1])?$' /opt/postfix/conf/main.cf /opt/postfix/conf/extra.cf; then + sed -i '/\[openssl_init\]/a ssl_conf = ssl_configuration' /etc/ssl/openssl.cnf + + echo "[ssl_configuration]" >> /etc/ssl/openssl.cnf + echo "system_default = tls_system_default" >> /etc/ssl/openssl.cnf + echo "[tls_system_default]" >> /etc/ssl/openssl.cnf + echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf + echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf +fi + exec "$@" diff --git a/docker-compose.yml b/docker-compose.yml index df5f296c5..f0bee182b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -224,7 +224,7 @@ services: - sogo dovecot-mailcow: - image: mailcow/dovecot:2.1 + image: mailcow/dovecot:2.2 depends_on: - mysql-mailcow - netfilter-mailcow @@ -308,7 +308,7 @@ services: - dovecot postfix-mailcow: - image: mailcow/postfix:1.76 + image: mailcow/postfix:1.77 depends_on: mysql-mailcow: condition: service_started From 54a0d53deb16fa3089d8717fd9e6c5b833a8d82e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:34:19 +0200 Subject: [PATCH 13/37] chore(deps): update thollander/actions-comment-pull-request action to v3 (#6102) Signed-off-by: milkmaker Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/check_prs_if_on_staging.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_prs_if_on_staging.yml b/.github/workflows/check_prs_if_on_staging.yml index 3e779a448..6ba3b89c0 100644 --- a/.github/workflows/check_prs_if_on_staging.yml +++ b/.github/workflows/check_prs_if_on_staging.yml @@ -10,7 +10,7 @@ jobs: if: github.event.pull_request.base.ref != 'staging' #check if the target branch is not staging steps: - name: Send message - uses: thollander/actions-comment-pull-request@v2.5.0 + uses: thollander/actions-comment-pull-request@v3.0.0 with: GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }} message: | From 1538fda71c7b615cffb4382b695fbbb45de21708 Mon Sep 17 00:00:00 2001 From: milkmaker Date: Tue, 15 Oct 2024 10:34:39 +0200 Subject: [PATCH 14/37] update postscreen_access.cidr (#6093) --- data/conf/postfix/postscreen_access.cidr | 30 ++++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/data/conf/postfix/postscreen_access.cidr b/data/conf/postfix/postscreen_access.cidr index f77d4e9ce..b9a185aa5 100644 --- a/data/conf/postfix/postscreen_access.cidr +++ b/data/conf/postfix/postscreen_access.cidr @@ -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 Tue Oct 1 00:18:56 UTC 2024 # https://github.com/stevejenkins/postwhite/ -# 1994 total rules +# 1970 total rules 2a00:1450:4000::/36 permit 2a01:111:f400::/48 permit 2a01:111:f403:8000::/50 permit @@ -115,6 +115,9 @@ 40.92.0.0/16 permit 40.107.0.0/16 permit 40.112.65.63 permit +40.233.64.216 permit +40.233.83.78 permit +40.233.88.28 permit 43.228.184.0/22 permit 44.206.138.57 permit 44.217.45.156 permit @@ -208,12 +211,12 @@ 52.96.223.2 permit 52.96.228.130 permit 52.96.229.242 permit -52.100.0.0/14 permit +52.100.0.0/15 permit +52.102.0.0/16 permit 52.103.0.0/17 permit 52.119.213.144/28 permit 52.185.106.240/28 permit 52.200.59.0/24 permit -52.205.61.79 permit 52.207.191.216 permit 52.222.62.51 permit 52.222.73.83 permit @@ -225,7 +228,6 @@ 52.236.28.240/28 permit 54.90.148.255 permit 54.165.19.38 permit -54.172.97.247 permit 54.174.52.0/24 permit 54.174.57.0/24 permit 54.174.59.0/24 permit @@ -273,7 +275,6 @@ 64.127.115.252 permit 64.132.88.0/23 permit 64.132.92.0/24 permit -64.147.123.128/27 permit 64.207.219.7 permit 64.207.219.8 permit 64.207.219.9 permit @@ -1373,6 +1374,7 @@ 139.138.58.119 permit 139.167.79.86 permit 139.180.17.0/24 permit +140.238.148.191 permit 141.148.159.229 permit 141.193.32.0/23 permit 141.193.184.32/27 permit @@ -1413,10 +1415,14 @@ 149.72.248.236 permit 149.97.173.180 permit 150.230.98.160 permit +151.145.38.14 permit 152.67.105.195 permit 152.69.200.236 permit 152.70.155.126 permit 155.248.208.51 permit +155.248.220.138 permit +155.248.234.149 permit +155.248.237.141 permit 157.55.0.192/26 permit 157.55.1.128/26 permit 157.55.2.0/25 permit @@ -1497,6 +1503,7 @@ 167.220.67.232/29 permit 168.138.5.36 permit 168.138.73.51 permit +168.138.77.31 permit 168.245.0.0/17 permit 168.245.12.252 permit 168.245.46.9 permit @@ -1519,6 +1526,7 @@ 172.217.192.0/19 permit 172.253.56.0/21 permit 172.253.112.0/20 permit +173.0.84.0/29 permit 173.0.84.224/27 permit 173.0.94.244/30 permit 173.194.0.0/16 permit @@ -1537,7 +1545,6 @@ 174.36.114.148/30 permit 174.36.114.152/29 permit 174.37.67.28/30 permit -174.129.203.189 permit 175.41.215.51 permit 176.32.105.0/24 permit 176.32.127.0/24 permit @@ -1610,6 +1617,8 @@ 188.172.128.0/20 permit 192.0.64.0/18 permit 192.18.139.154 permit +192.18.145.36 permit +192.18.152.58 permit 192.30.252.0/22 permit 192.161.144.0/20 permit 192.162.87.0/24 permit @@ -1677,6 +1686,7 @@ 199.122.123.0/24 permit 199.127.232.0/22 permit 199.255.192.0/22 permit +202.12.124.128/27 permit 202.129.242.0/23 permit 202.165.102.47 permit 202.177.148.100 permit @@ -1727,9 +1737,9 @@ 204.92.114.187 permit 204.92.114.203 permit 204.92.114.204/31 permit -204.141.32.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 205.139.110.0/24 permit 205.201.128.0/20 permit From 932cf453de345f6546832e958e8e3f3779212cdc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:34:57 +0200 Subject: [PATCH 15/37] chore(deps): update dependency nextcloud/server to v28.0.11 (#6101) Signed-off-by: milkmaker Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- helper-scripts/nextcloud.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helper-scripts/nextcloud.sh b/helper-scripts/nextcloud.sh index 19b4c28c1..2df3ca161 100755 --- a/helper-scripts/nextcloud.sh +++ b/helper-scripts/nextcloud.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # renovate: datasource=github-releases depName=nextcloud/server versioning=semver extractVersion=^v(?.*)$ -NEXTCLOUD_VERSION=28.0.6 +NEXTCLOUD_VERSION=28.0.11 display_warning() { local message=("$@") From 9a58e5e35a8e8484a7c3d68c1e0dc11fe2fcba98 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:45:32 +0200 Subject: [PATCH 16/37] chore(deps): update dependency phpredis/phpredis to v6.1.0 (#6098) Signed-off-by: milkmaker --- data/Dockerfiles/phpfpm/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile index 0ac722b22..e00609509 100644 --- a/data/Dockerfiles/phpfpm/Dockerfile +++ b/data/Dockerfiles/phpfpm/Dockerfile @@ -11,7 +11,7 @@ ARG MAILPARSE_PECL_VERSION=3.1.6 # renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?.*)$ ARG MEMCACHED_PECL_VERSION=3.2.0 # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?.*)$ -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=(?.*)$ ARG COMPOSER_VERSION=2.6.6 From 4c9690e87cd350640d423d821122328e78ae9fb1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 11:09:23 +0200 Subject: [PATCH 17/37] chore(deps): update dependency php/pecl-mail-mailparse to v3.1.8 (#6096) Signed-off-by: milkmaker Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- data/Dockerfiles/phpfpm/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile index e00609509..1ec4e0c22 100644 --- a/data/Dockerfiles/phpfpm/Dockerfile +++ b/data/Dockerfiles/phpfpm/Dockerfile @@ -7,7 +7,7 @@ ARG APCU_PECL_VERSION=5.1.23 # renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?.*)$ ARG IMAGICK_PECL_VERSION=3.7.0 # renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?.*)$ -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(?.*)$ ARG MEMCACHED_PECL_VERSION=3.2.0 # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?.*)$ From 382056ec181565470f8d2791d5af4059d1aa41d0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 11:24:26 +0200 Subject: [PATCH 18/37] chore(deps): update dependency krakjoe/apcu to v5.1.24 (#6087) Signed-off-by: milkmaker Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- data/Dockerfiles/phpfpm/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile index 1ec4e0c22..b4704cfb0 100644 --- a/data/Dockerfiles/phpfpm/Dockerfile +++ b/data/Dockerfiles/phpfpm/Dockerfile @@ -3,7 +3,7 @@ FROM php:8.2-fpm-alpine3.18 LABEL maintainer = "The Infrastructure Company GmbH " # renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?.*)$ -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=(?.*)$ ARG IMAGICK_PECL_VERSION=3.7.0 # renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?.*)$ From 982e823c71ab64efe5b82de5842406f995af35a4 Mon Sep 17 00:00:00 2001 From: Niklas Meyer Date: Tue, 15 Oct 2024 16:13:51 +0200 Subject: [PATCH 19/37] sogo: upgrade to 5.11.1 (#6109) --- data/Dockerfiles/sogo/Dockerfile | 5 +++-- docker-compose.yml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index 7b8b1c717..78da39bec 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -33,13 +33,14 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \ && gosu nobody true \ && mkdir /usr/share/doc/sogo \ && touch /usr/share/doc/sogo/empty.sh \ - && apt-key adv --keyserver keys.openpgp.org --recv-key 74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 \ + && wget http://www.axis.cz/linux/debian/axis-archive-keyring.deb -O /tmp/axis-archive-keyring.deb \ + && apt install -y /tmp/axis-archive-keyring.deb \ && echo "deb [trusted=yes] ${SOGO_DEBIAN_REPOSITORY} ${DEBIAN_VERSION} sogo-v5" > /etc/apt/sources.list.d/sogo.list \ && apt-get update && apt-get install -y --no-install-recommends \ sogo \ sogo-activesync \ && apt-get autoclean \ - && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/sogo.list \ + && rm -rf /var/lib/apt/lists/* \ && touch /etc/default/locale COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh diff --git a/docker-compose.yml b/docker-compose.yml index f0bee182b..52bce5ed1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -177,7 +177,7 @@ services: - phpfpm sogo-mailcow: - image: mailcow/sogo:1.126 + image: mailcow/sogo:1.127 environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} From c53bf85480697c24912214bf74a1718a73052a06 Mon Sep 17 00:00:00 2001 From: Niklas Meyer Date: Wed, 16 Oct 2024 10:35:39 +0200 Subject: [PATCH 20/37] postfix: add X-Original-To header per default (#6110) --- data/conf/postfix/main.cf | 2 ++ data/conf/postfix/master.cf | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/data/conf/postfix/main.cf b/data/conf/postfix/main.cf index 6a87f2ecb..6721204cb 100644 --- a/data/conf/postfix/main.cf +++ b/data/conf/postfix/main.cf @@ -170,6 +170,8 @@ smtputf8_enable = no submission_smtpd_tls_mandatory_protocols = >=TLSv1.2 smtps_smtpd_tls_mandatory_protocols = >=TLSv1.2 parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks,qmqpd_authorized_clients +# This Option is added to correctly set the X-Original-To Header when mails are send to lmtp (dovecot) +lmtp_destination_recipient_limit=1 # DO NOT EDIT ANYTHING BELOW # # Overrides # diff --git a/data/conf/postfix/master.cf b/data/conf/postfix/master.cf index df91a3900..d5114df28 100644 --- a/data/conf/postfix/master.cf +++ b/data/conf/postfix/master.cf @@ -105,7 +105,7 @@ retry unix - - n - - error discard unix - - n - - discard local unix - n n - - local virtual unix - n n - - virtual -lmtp unix - - n - - lmtp +lmtp unix - - n - - lmtp flags=O anvil unix - - n - 1 anvil scache unix - - n - 1 scache maildrop unix - n n - - pipe flags=DRhu From 8dcaffe925f095630ab194fbd5efa8b71d3731c1 Mon Sep 17 00:00:00 2001 From: Niklas Meyer Date: Wed, 16 Oct 2024 10:35:54 +0200 Subject: [PATCH 21/37] php: upgrade to alpine 3.20 (base os) (#6106) --- data/Dockerfiles/phpfpm/Dockerfile | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile index b4704cfb0..e9ebe071c 100644 --- a/data/Dockerfiles/phpfpm/Dockerfile +++ b/data/Dockerfiles/phpfpm/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.2-fpm-alpine3.18 +FROM php:8.2-fpm-alpine3.20 LABEL maintainer = "The Infrastructure Company GmbH " diff --git a/docker-compose.yml b/docker-compose.yml index 52bce5ed1..01f8ee90b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -112,7 +112,7 @@ services: - rspamd php-fpm-mailcow: - image: mailcow/phpfpm:1.90 + image: mailcow/phpfpm:1.91 command: "php-fpm -d date.timezone=${TZ} -d expose_php=0" depends_on: - redis-mailcow From 5a0f20b9eaad8a04aa8e8db9e959dae5c4d663b5 Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 16 Oct 2024 15:29:16 +0200 Subject: [PATCH 22/37] Update dependency twig/twig to v3.14.0 (#6071) --- data/web/inc/lib/composer.lock | 178 +- data/web/inc/lib/vendor/autoload.php | 17 +- .../inc/lib/vendor/composer/ClassLoader.php | 137 +- .../lib/vendor/composer/InstalledVersions.php | 17 +- .../lib/vendor/composer/autoload_classmap.php | 2 + .../lib/vendor/composer/autoload_files.php | 6 + .../inc/lib/vendor/composer/autoload_psr4.php | 1 + .../inc/lib/vendor/composer/autoload_real.php | 27 +- .../lib/vendor/composer/autoload_static.php | 13 + .../inc/lib/vendor/composer/installed.json | 184 +- .../web/inc/lib/vendor/composer/installed.php | 28 +- .../lib/vendor/composer/platform_check.php | 4 +- .../deprecation-contracts/CHANGELOG.md | 5 + .../symfony/deprecation-contracts/LICENSE | 19 + .../symfony/deprecation-contracts/README.md | 26 + .../deprecation-contracts/composer.json | 35 + .../deprecation-contracts/function.php | 27 + .../lib/vendor/symfony/polyfill-php81/LICENSE | 19 + .../vendor/symfony/polyfill-php81/Php81.php | 37 + .../vendor/symfony/polyfill-php81/README.md | 18 + .../Resources/stubs/CURLStringFile.php | 51 + .../Resources/stubs/ReturnTypeWillChange.php | 20 + .../symfony/polyfill-php81/bootstrap.php | 28 + .../symfony/polyfill-php81/composer.json | 33 + .../inc/lib/vendor/twig/twig/.editorconfig | 18 - .../inc/lib/vendor/twig/twig/.gitattributes | 4 - .../vendor/twig/twig/.github/workflows/ci.yml | 149 - .../twig/.github/workflows/documentation.yml | 64 - data/web/inc/lib/vendor/twig/twig/.gitignore | 6 - .../vendor/twig/twig/.php-cs-fixer.dist.php | 20 - data/web/inc/lib/vendor/twig/twig/CHANGELOG | 177 +- data/web/inc/lib/vendor/twig/twig/LICENSE | 2 +- data/web/inc/lib/vendor/twig/twig/README.rst | 2 +- .../inc/lib/vendor/twig/twig/composer.json | 21 +- .../twig/twig/src/AbstractTwigCallable.php | 136 + .../Attribute/FirstClassTwigCallableReady.php | 20 + .../twig/twig/src/Attribute/YieldReady.php | 20 + .../vendor/twig/twig/src/Cache/ChainCache.php | 79 + .../twig/twig/src/Cache/FilesystemCache.php | 8 +- .../src/Cache/ReadOnlyFilesystemCache.php | 25 + .../inc/lib/vendor/twig/twig/src/Compiler.php | 69 +- .../lib/vendor/twig/twig/src/Environment.php | 123 +- .../lib/vendor/twig/twig/src/Error/Error.php | 18 +- .../twig/twig/src/Error/SyntaxError.php | 4 +- .../vendor/twig/twig/src/ExpressionParser.php | 418 +-- .../twig/src/Extension/AbstractExtension.php | 2 +- .../twig/twig/src/Extension/CoreExtension.php | 2841 +++++++++-------- .../twig/src/Extension/DebugExtension.php | 66 +- .../twig/src/Extension/EscaperExtension.php | 394 +-- .../twig/src/Extension/ExtensionInterface.php | 7 + .../twig/src/Extension/GlobalsInterface.php | 8 +- .../twig/src/Extension/OptimizerExtension.php | 8 +- .../twig/src/Extension/SandboxExtension.php | 34 +- .../twig/src/Extension/StagingExtension.php | 8 +- .../src/Extension/StringLoaderExtension.php | 38 +- .../src/Extension/YieldNotReadyExtension.php | 30 + .../lib/vendor/twig/twig/src/ExtensionSet.php | 101 +- .../src/FileExtensionEscapingStrategy.php | 2 +- .../inc/lib/vendor/twig/twig/src/Lexer.php | 147 +- .../twig/twig/src/Loader/ArrayLoader.php | 14 +- .../twig/twig/src/Loader/ChainLoader.php | 43 +- .../twig/twig/src/Loader/FilesystemLoader.php | 20 +- .../inc/lib/vendor/twig/twig/src/Markup.php | 4 +- .../twig/twig/src/Node/AutoEscapeNode.php | 6 +- .../vendor/twig/twig/src/Node/BlockNode.php | 12 +- .../twig/twig/src/Node/BlockReferenceNode.php | 8 +- .../vendor/twig/twig/src/Node/BodyNode.php | 3 + .../vendor/twig/twig/src/Node/CaptureNode.php | 57 + .../twig/src/Node/CheckSecurityCallNode.php | 4 +- .../twig/twig/src/Node/CheckSecurityNode.php | 31 +- .../twig/twig/src/Node/CheckToStringNode.php | 4 +- .../twig/twig/src/Node/DeprecatedNode.php | 40 +- .../lib/vendor/twig/twig/src/Node/DoNode.php | 6 +- .../vendor/twig/twig/src/Node/EmbedNode.php | 6 +- .../Node/Expression/AbstractExpression.php | 4 + .../src/Node/Expression/ArrayExpression.php | 66 +- .../Expression/ArrowFunctionExpression.php | 4 +- .../Node/Expression/Binary/EndsWithBinary.php | 6 +- .../Node/Expression/Binary/EqualBinary.php | 2 +- .../Node/Expression/Binary/GreaterBinary.php | 2 +- .../Expression/Binary/GreaterEqualBinary.php | 2 +- .../Node/Expression/Binary/HasEveryBinary.php | 33 + .../Node/Expression/Binary/HasSomeBinary.php | 33 + .../src/Node/Expression/Binary/InBinary.php | 2 +- .../src/Node/Expression/Binary/LessBinary.php | 2 +- .../Expression/Binary/LessEqualBinary.php | 2 +- .../Node/Expression/Binary/MatchesBinary.php | 2 +- .../Node/Expression/Binary/NotEqualBinary.php | 2 +- .../Node/Expression/Binary/NotInBinary.php | 2 +- .../Expression/Binary/StartsWithBinary.php | 6 +- .../Expression/BlockReferenceExpression.php | 9 +- .../src/Node/Expression/CallExpression.php | 193 +- .../Node/Expression/ConditionalExpression.php | 27 +- .../Node/Expression/ConstantExpression.php | 3 + .../Node/Expression/Filter/DefaultFilter.php | 23 +- .../src/Node/Expression/Filter/RawFilter.php | 36 + .../src/Node/Expression/FilterExpression.php | 55 +- .../Node/Expression/FunctionExpression.php | 53 +- .../FunctionNode/EnumCasesFunction.php | 41 + .../src/Node/Expression/GetAttrExpression.php | 2 +- .../twig/src/Node/Expression/InlinePrint.php | 3 +- .../Node/Expression/MethodCallExpression.php | 6 +- .../src/Node/Expression/NameExpression.php | 14 +- .../Expression/NullCoalesceExpression.php | 5 +- .../src/Node/Expression/ParentExpression.php | 6 +- .../src/Node/Expression/Test/ConstantTest.php | 6 +- .../src/Node/Expression/Test/DefinedTest.php | 9 +- .../Node/Expression/Test/DivisiblebyTest.php | 2 +- .../src/Node/Expression/Test/SameasTest.php | 2 +- .../src/Node/Expression/TestExpression.php | 40 +- .../vendor/twig/twig/src/Node/FlushNode.php | 17 +- .../vendor/twig/twig/src/Node/ForLoopNode.php | 8 +- .../lib/vendor/twig/twig/src/Node/ForNode.php | 19 +- .../lib/vendor/twig/twig/src/Node/IfNode.php | 13 +- .../vendor/twig/twig/src/Node/ImportNode.php | 16 +- .../vendor/twig/twig/src/Node/IncludeNode.php | 22 +- .../vendor/twig/twig/src/Node/MacroNode.php | 45 +- .../vendor/twig/twig/src/Node/ModuleNode.php | 101 +- .../twig/twig/src/Node/NameDeprecation.php | 46 + .../lib/vendor/twig/twig/src/Node/Node.php | 146 +- .../vendor/twig/twig/src/Node/PrintNode.php | 13 +- .../vendor/twig/twig/src/Node/SandboxNode.php | 6 +- .../lib/vendor/twig/twig/src/Node/SetNode.php | 51 +- .../vendor/twig/twig/src/Node/TextNode.php | 7 +- .../vendor/twig/twig/src/Node/TypesNode.php | 28 + .../vendor/twig/twig/src/Node/WithNode.php | 20 +- .../src/NodeVisitor/AbstractNodeVisitor.php | 4 +- .../src/NodeVisitor/EscaperNodeVisitor.php | 35 +- .../MacroAutoImportNodeVisitor.php | 10 +- .../src/NodeVisitor/OptimizerNodeVisitor.php | 70 +- .../NodeVisitor/SafeAnalysisNodeVisitor.php | 28 +- .../src/NodeVisitor/SandboxNodeVisitor.php | 15 +- .../NodeVisitor/YieldNotReadyNodeVisitor.php | 59 + .../inc/lib/vendor/twig/twig/src/Parser.php | 77 +- .../twig/src/Profiler/Dumper/BaseDumper.php | 2 +- .../src/Profiler/Dumper/BlackfireDumper.php | 6 +- .../twig/src/Profiler/Dumper/HtmlDumper.php | 6 +- .../twig/src/Profiler/Dumper/TextDumper.php | 6 +- .../src/Profiler/Node/EnterProfileNode.php | 6 +- .../src/Profiler/Node/LeaveProfileNode.php | 4 +- .../NodeVisitor/ProfilerNodeVisitor.php | 9 +- .../vendor/twig/twig/src/Profiler/Profile.php | 17 +- .../vendor/twig/twig/src/Resources/core.php | 541 ++++ .../vendor/twig/twig/src/Resources/debug.php | 25 + .../twig/twig/src/Resources/escaper.php | 51 + .../twig/twig/src/Resources/string_loader.php | 26 + .../twig/twig/src/Runtime/EscaperRuntime.php | 327 ++ .../RuntimeLoader/ContainerRuntimeLoader.php | 8 +- .../RuntimeLoader/FactoryRuntimeLoader.php | 8 +- .../twig/twig/src/Sandbox/SecurityPolicy.php | 26 +- .../src/Sandbox/SourcePolicyInterface.php | 24 + .../inc/lib/vendor/twig/twig/src/Source.php | 14 +- .../inc/lib/vendor/twig/twig/src/Template.php | 313 +- .../vendor/twig/twig/src/TemplateWrapper.php | 39 +- .../twig/src/Test/IntegrationTestCase.php | 59 +- .../twig/twig/src/Test/NodeTestCase.php | 74 +- .../inc/lib/vendor/twig/twig/src/Token.php | 32 +- .../twig/src/TokenParser/ApplyTokenParser.php | 8 +- .../src/TokenParser/AutoEscapeTokenParser.php | 8 +- .../twig/src/TokenParser/BlockTokenParser.php | 15 +- .../src/TokenParser/DeprecatedTokenParser.php | 29 +- .../twig/src/TokenParser/DoTokenParser.php | 4 +- .../twig/src/TokenParser/EmbedTokenParser.php | 18 +- .../src/TokenParser/ExtendsTokenParser.php | 7 +- .../twig/src/TokenParser/FlushTokenParser.php | 4 +- .../twig/src/TokenParser/ForTokenParser.php | 16 +- .../twig/src/TokenParser/FromTokenParser.php | 16 +- .../twig/src/TokenParser/IfTokenParser.php | 12 +- .../src/TokenParser/ImportTokenParser.php | 8 +- .../src/TokenParser/IncludeTokenParser.php | 14 +- .../twig/src/TokenParser/MacroTokenParser.php | 14 +- .../src/TokenParser/SandboxTokenParser.php | 6 +- .../twig/src/TokenParser/SetTokenParser.php | 10 +- .../twig/src/TokenParser/TypesTokenParser.php | 86 + .../twig/src/TokenParser/UseTokenParser.php | 14 +- .../twig/src/TokenParser/WithTokenParser.php | 10 +- .../lib/vendor/twig/twig/src/TokenStream.php | 23 +- .../twig/twig/src/TwigCallableInterface.php | 53 + .../lib/vendor/twig/twig/src/TwigFilter.php | 78 +- .../lib/vendor/twig/twig/src/TwigFunction.php | 79 +- .../inc/lib/vendor/twig/twig/src/TwigTest.php | 69 +- .../src/Util/CallableArgumentsExtractor.php | 204 ++ .../twig/src/Util/DeprecationCollector.php | 10 +- .../twig/twig/src/Util/ReflectionCallable.php | 92 + 184 files changed, 6895 insertions(+), 3455 deletions(-) create mode 100644 data/web/inc/lib/vendor/symfony/deprecation-contracts/CHANGELOG.md create mode 100644 data/web/inc/lib/vendor/symfony/deprecation-contracts/LICENSE create mode 100644 data/web/inc/lib/vendor/symfony/deprecation-contracts/README.md create mode 100644 data/web/inc/lib/vendor/symfony/deprecation-contracts/composer.json create mode 100644 data/web/inc/lib/vendor/symfony/deprecation-contracts/function.php create mode 100644 data/web/inc/lib/vendor/symfony/polyfill-php81/LICENSE create mode 100644 data/web/inc/lib/vendor/symfony/polyfill-php81/Php81.php create mode 100644 data/web/inc/lib/vendor/symfony/polyfill-php81/README.md create mode 100644 data/web/inc/lib/vendor/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php create mode 100644 data/web/inc/lib/vendor/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php create mode 100644 data/web/inc/lib/vendor/symfony/polyfill-php81/bootstrap.php create mode 100644 data/web/inc/lib/vendor/symfony/polyfill-php81/composer.json delete mode 100644 data/web/inc/lib/vendor/twig/twig/.editorconfig delete mode 100644 data/web/inc/lib/vendor/twig/twig/.gitattributes delete mode 100644 data/web/inc/lib/vendor/twig/twig/.github/workflows/ci.yml delete mode 100644 data/web/inc/lib/vendor/twig/twig/.github/workflows/documentation.yml delete mode 100644 data/web/inc/lib/vendor/twig/twig/.gitignore delete mode 100644 data/web/inc/lib/vendor/twig/twig/.php-cs-fixer.dist.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/AbstractTwigCallable.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Attribute/FirstClassTwigCallableReady.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Attribute/YieldReady.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Cache/ChainCache.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Cache/ReadOnlyFilesystemCache.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Extension/YieldNotReadyExtension.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Node/CaptureNode.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/HasEveryBinary.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/HasSomeBinary.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Filter/RawFilter.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Node/NameDeprecation.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Node/TypesNode.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Resources/core.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Resources/debug.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Resources/escaper.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Resources/string_loader.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Runtime/EscaperRuntime.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Sandbox/SourcePolicyInterface.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/TokenParser/TypesTokenParser.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/TwigCallableInterface.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Util/CallableArgumentsExtractor.php create mode 100644 data/web/inc/lib/vendor/twig/twig/src/Util/ReflectionCallable.php diff --git a/data/web/inc/lib/composer.lock b/data/web/inc/lib/composer.lock index 758535375..422d353ab 100644 --- a/data/web/inc/lib/composer.lock +++ b/data/web/inc/lib/composer.lock @@ -1039,6 +1039,73 @@ }, "time": "2017-04-19T22:01:50+00:00" }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.24.0", @@ -1287,6 +1354,82 @@ ], "time": "2021-09-13T13:58:33+00:00" }, + { + "name": "symfony/polyfill-php81", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, { "name": "symfony/translation", "version": "v6.0.5", @@ -1604,34 +1747,37 @@ }, { "name": "twig/twig", - "version": "v3.4.3", + "version": "v3.14.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58" + "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/c38fd6b0b7f370c198db91ffd02e23b517426b58", - "reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72", + "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.0.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-mbstring": "^1.3" + "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-php81": "^1.29" }, "require-dev": { - "psr/container": "^1.0", - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4-dev" - } - }, "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], "psr-4": { "Twig\\": "src/" } @@ -1664,7 +1810,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.4.3" + "source": "https://github.com/twigphp/Twig/tree/v3.14.0" }, "funding": [ { @@ -1676,7 +1822,7 @@ "type": "tidelift" } ], - "time": "2022-09-28T08:42:51+00:00" + "time": "2024-09-09T17:55:12+00:00" }, { "name": "yubico/u2flib-server", @@ -1728,5 +1874,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/data/web/inc/lib/vendor/autoload.php b/data/web/inc/lib/vendor/autoload.php index c21364e81..a0fb8f4ac 100644 --- a/data/web/inc/lib/vendor/autoload.php +++ b/data/web/inc/lib/vendor/autoload.php @@ -3,8 +3,21 @@ // autoload.php @generated by Composer if (PHP_VERSION_ID < 50600) { - echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; - exit(1); + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, $err); + } elseif (!headers_sent()) { + echo $err; + } + } + trigger_error( + $err, + E_USER_ERROR + ); } require_once __DIR__ . '/composer/autoload_real.php'; diff --git a/data/web/inc/lib/vendor/composer/ClassLoader.php b/data/web/inc/lib/vendor/composer/ClassLoader.php index afef3fa2a..7824d8f7e 100644 --- a/data/web/inc/lib/vendor/composer/ClassLoader.php +++ b/data/web/inc/lib/vendor/composer/ClassLoader.php @@ -42,35 +42,37 @@ namespace Composer\Autoload; */ class ClassLoader { - /** @var ?string */ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ private $vendorDir; // PSR-4 /** - * @var array[] - * @psalm-var array> + * @var array> */ private $prefixLengthsPsr4 = array(); /** - * @var array[] - * @psalm-var array> + * @var array> */ private $prefixDirsPsr4 = array(); /** - * @var array[] - * @psalm-var array + * @var list */ private $fallbackDirsPsr4 = array(); // PSR-0 /** - * @var array[] - * @psalm-var array> + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array>> */ private $prefixesPsr0 = array(); /** - * @var array[] - * @psalm-var array + * @var list */ private $fallbackDirsPsr0 = array(); @@ -78,8 +80,7 @@ class ClassLoader private $useIncludePath = false; /** - * @var string[] - * @psalm-var array + * @var array */ private $classMap = array(); @@ -87,29 +88,29 @@ class ClassLoader private $classMapAuthoritative = false; /** - * @var bool[] - * @psalm-var array + * @var array */ private $missingClasses = array(); - /** @var ?string */ + /** @var string|null */ private $apcuPrefix; /** - * @var self[] + * @var array */ private static $registeredLoaders = array(); /** - * @param ?string $vendorDir + * @param string|null $vendorDir */ public function __construct($vendorDir = null) { $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); } /** - * @return string[] + * @return array> */ public function getPrefixes() { @@ -121,8 +122,7 @@ class ClassLoader } /** - * @return array[] - * @psalm-return array> + * @return array> */ public function getPrefixesPsr4() { @@ -130,8 +130,7 @@ class ClassLoader } /** - * @return array[] - * @psalm-return array + * @return list */ public function getFallbackDirs() { @@ -139,8 +138,7 @@ class ClassLoader } /** - * @return array[] - * @psalm-return array + * @return list */ public function getFallbackDirsPsr4() { @@ -148,8 +146,7 @@ class ClassLoader } /** - * @return string[] Array of classname => path - * @psalm-return array + * @return array Array of classname => path */ public function getClassMap() { @@ -157,8 +154,7 @@ class ClassLoader } /** - * @param string[] $classMap Class to filename map - * @psalm-param array $classMap + * @param array $classMap Class to filename map * * @return void */ @@ -175,24 +171,25 @@ class ClassLoader * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * - * @param string $prefix The prefix - * @param string[]|string $paths The PSR-0 root directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories * * @return void */ public function add($prefix, $paths, $prepend = false) { + $paths = (array) $paths; if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( - (array) $paths, + $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, - (array) $paths + $paths ); } @@ -201,19 +198,19 @@ class ClassLoader $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { - $this->prefixesPsr0[$first][$prefix] = (array) $paths; + $this->prefixesPsr0[$first][$prefix] = $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( - (array) $paths, + $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], - (array) $paths + $paths ); } } @@ -222,9 +219,9 @@ class ClassLoader * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param string[]|string $paths The PSR-4 base directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException * @@ -232,17 +229,18 @@ class ClassLoader */ public function addPsr4($prefix, $paths, $prepend = false) { + $paths = (array) $paths; if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( - (array) $paths, + $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, - (array) $paths + $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { @@ -252,18 +250,18 @@ class ClassLoader throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; - $this->prefixDirsPsr4[$prefix] = (array) $paths; + $this->prefixDirsPsr4[$prefix] = $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( - (array) $paths, + $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], - (array) $paths + $paths ); } } @@ -272,8 +270,8 @@ class ClassLoader * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * - * @param string $prefix The prefix - * @param string[]|string $paths The PSR-0 base directories + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 base directories * * @return void */ @@ -290,8 +288,8 @@ class ClassLoader * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param string[]|string $paths The PSR-4 base directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException * @@ -425,7 +423,8 @@ class ClassLoader public function loadClass($class) { if ($file = $this->findFile($class)) { - includeFile($file); + $includeFile = self::$includeFile; + $includeFile($file); return true; } @@ -476,9 +475,9 @@ class ClassLoader } /** - * Returns the currently registered loaders indexed by their corresponding vendor directories. + * Returns the currently registered loaders keyed by their corresponding vendor directories. * - * @return self[] + * @return array */ public static function getRegisteredLoaders() { @@ -555,18 +554,26 @@ class ClassLoader return false; } -} -/** - * Scope isolated include. - * - * Prevents access to $this/self from included files. - * - * @param string $file - * @return void - * @private - */ -function includeFile($file) -{ - include $file; + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } } diff --git a/data/web/inc/lib/vendor/composer/InstalledVersions.php b/data/web/inc/lib/vendor/composer/InstalledVersions.php index c6b54af7b..51e734a77 100644 --- a/data/web/inc/lib/vendor/composer/InstalledVersions.php +++ b/data/web/inc/lib/vendor/composer/InstalledVersions.php @@ -98,7 +98,7 @@ class InstalledVersions { foreach (self::getInstalled() as $installed) { if (isset($installed['versions'][$packageName])) { - return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; } } @@ -119,7 +119,7 @@ class InstalledVersions */ public static function satisfies(VersionParser $parser, $packageName, $constraint) { - $constraint = $parser->parseConstraints($constraint); + $constraint = $parser->parseConstraints((string) $constraint); $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); return $provided->matches($constraint); @@ -328,7 +328,9 @@ class InstalledVersions if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { - $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + $installed[] = self::$installedByVendor[$vendorDir] = $required; if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { self::$installed = $installed[count($installed) - 1]; } @@ -340,12 +342,17 @@ class InstalledVersions // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { - self::$installed = require __DIR__ . '/installed.php'; + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; } else { self::$installed = array(); } } - $installed[] = self::$installed; + + if (self::$installed !== array()) { + $installed[] = self::$installed; + } return $installed; } diff --git a/data/web/inc/lib/vendor/composer/autoload_classmap.php b/data/web/inc/lib/vendor/composer/autoload_classmap.php index f3327f110..83b86c267 100644 --- a/data/web/inc/lib/vendor/composer/autoload_classmap.php +++ b/data/web/inc/lib/vendor/composer/autoload_classmap.php @@ -7,7 +7,9 @@ $baseDir = dirname($vendorDir); return array( 'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', + 'CURLStringFile' => $vendorDir . '/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php', 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', + 'ReturnTypeWillChange' => $vendorDir . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php', 'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', 'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', 'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', diff --git a/data/web/inc/lib/vendor/composer/autoload_files.php b/data/web/inc/lib/vendor/composer/autoload_files.php index 502893f7d..526f6b6f4 100644 --- a/data/web/inc/lib/vendor/composer/autoload_files.php +++ b/data/web/inc/lib/vendor/composer/autoload_files.php @@ -10,8 +10,14 @@ return array( 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php', 'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php', '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php', + '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php', '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', + '23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php', 'fe62ba7e10580d903cc46d808b5961a4' => $vendorDir . '/tightenco/collect/src/Collect/Support/helpers.php', 'caf31cc6ec7cf2241cb6f12c226c3846' => $vendorDir . '/tightenco/collect/src/Collect/Support/alias.php', '04c6c5c2f7095ccf6c481d3e53e1776f' => $vendorDir . '/mustangostang/spyc/Spyc.php', + '89efb1254ef2d1c5d80096acd12c4098' => $vendorDir . '/twig/twig/src/Resources/core.php', + 'ffecb95d45175fd40f75be8a23b34f90' => $vendorDir . '/twig/twig/src/Resources/debug.php', + 'c7baa00073ee9c61edf148c51917cfb4' => $vendorDir . '/twig/twig/src/Resources/escaper.php', + 'f844ccf1d25df8663951193c3fc307c8' => $vendorDir . '/twig/twig/src/Resources/string_loader.php', ); diff --git a/data/web/inc/lib/vendor/composer/autoload_psr4.php b/data/web/inc/lib/vendor/composer/autoload_psr4.php index 83d1f5655..0a55fb6cd 100644 --- a/data/web/inc/lib/vendor/composer/autoload_psr4.php +++ b/data/web/inc/lib/vendor/composer/autoload_psr4.php @@ -8,6 +8,7 @@ $baseDir = dirname($vendorDir); return array( 'Twig\\' => array($vendorDir . '/twig/twig/src'), 'Tightenco\\Collect\\' => array($vendorDir . '/tightenco/collect/src/Collect'), + 'Symfony\\Polyfill\\Php81\\' => array($vendorDir . '/symfony/polyfill-php81'), 'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'), 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), 'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'), diff --git a/data/web/inc/lib/vendor/composer/autoload_real.php b/data/web/inc/lib/vendor/composer/autoload_real.php index 6891b0c9e..5e2082534 100644 --- a/data/web/inc/lib/vendor/composer/autoload_real.php +++ b/data/web/inc/lib/vendor/composer/autoload_real.php @@ -33,25 +33,18 @@ class ComposerAutoloaderInit873464e4bd965a3168f133248b1b218b $loader->register(true); - $includeFiles = \Composer\Autoload\ComposerStaticInit873464e4bd965a3168f133248b1b218b::$files; - foreach ($includeFiles as $fileIdentifier => $file) { - composerRequire873464e4bd965a3168f133248b1b218b($fileIdentifier, $file); + $filesToLoad = \Composer\Autoload\ComposerStaticInit873464e4bd965a3168f133248b1b218b::$files; + $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + + require $file; + } + }, null, null); + foreach ($filesToLoad as $fileIdentifier => $file) { + $requireFile($fileIdentifier, $file); } return $loader; } } - -/** - * @param string $fileIdentifier - * @param string $file - * @return void - */ -function composerRequire873464e4bd965a3168f133248b1b218b($fileIdentifier, $file) -{ - if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { - $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; - - require $file; - } -} diff --git a/data/web/inc/lib/vendor/composer/autoload_static.php b/data/web/inc/lib/vendor/composer/autoload_static.php index 94606e831..6b2f04a5f 100644 --- a/data/web/inc/lib/vendor/composer/autoload_static.php +++ b/data/web/inc/lib/vendor/composer/autoload_static.php @@ -11,10 +11,16 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', 'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php', '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php', + '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', + '23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php', 'fe62ba7e10580d903cc46d808b5961a4' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/helpers.php', 'caf31cc6ec7cf2241cb6f12c226c3846' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/alias.php', '04c6c5c2f7095ccf6c481d3e53e1776f' => __DIR__ . '/..' . '/mustangostang/spyc/Spyc.php', + '89efb1254ef2d1c5d80096acd12c4098' => __DIR__ . '/..' . '/twig/twig/src/Resources/core.php', + 'ffecb95d45175fd40f75be8a23b34f90' => __DIR__ . '/..' . '/twig/twig/src/Resources/debug.php', + 'c7baa00073ee9c61edf148c51917cfb4' => __DIR__ . '/..' . '/twig/twig/src/Resources/escaper.php', + 'f844ccf1d25df8663951193c3fc307c8' => __DIR__ . '/..' . '/twig/twig/src/Resources/string_loader.php', ); public static $prefixLengthsPsr4 = array ( @@ -25,6 +31,7 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b ), 'S' => array ( + 'Symfony\\Polyfill\\Php81\\' => 23, 'Symfony\\Polyfill\\Php80\\' => 23, 'Symfony\\Polyfill\\Mbstring\\' => 26, 'Symfony\\Polyfill\\Ctype\\' => 23, @@ -80,6 +87,10 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b array ( 0 => __DIR__ . '/..' . '/tightenco/collect/src/Collect', ), + 'Symfony\\Polyfill\\Php81\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-php81', + ), 'Symfony\\Polyfill\\Php80\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-php80', @@ -170,7 +181,9 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b public static $classMap = array ( 'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', + 'CURLStringFile' => __DIR__ . '/..' . '/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'ReturnTypeWillChange' => __DIR__ . '/..' . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php', 'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', 'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', 'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', diff --git a/data/web/inc/lib/vendor/composer/installed.json b/data/web/inc/lib/vendor/composer/installed.json index e21edcc68..dfb2fb0cc 100644 --- a/data/web/inc/lib/vendor/composer/installed.json +++ b/data/web/inc/lib/vendor/composer/installed.json @@ -1068,6 +1068,76 @@ ], "install-path": "../soundasleep/html2text" }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.0", + "version_normalized": "3.5.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "time": "2024-04-18T09:32:20+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/deprecation-contracts" + }, { "name": "symfony/polyfill-ctype", "version": "v1.24.0", @@ -1325,6 +1395,85 @@ ], "install-path": "../symfony/polyfill-php80" }, + { + "name": "symfony/polyfill-php81", + "version": "v1.31.0", + "version_normalized": "1.31.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "time": "2024-09-09T11:45:10+00:00", + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-php81" + }, { "name": "symfony/translation", "version": "v6.0.5", @@ -1654,37 +1803,40 @@ }, { "name": "twig/twig", - "version": "v3.4.3", - "version_normalized": "3.4.3.0", + "version": "v3.14.0", + "version_normalized": "3.14.0.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58" + "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/c38fd6b0b7f370c198db91ffd02e23b517426b58", - "reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72", + "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.0.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-mbstring": "^1.3" + "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-php81": "^1.29" }, "require-dev": { - "psr/container": "^1.0", - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, - "time": "2022-09-28T08:42:51+00:00", + "time": "2024-09-09T17:55:12+00:00", "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4-dev" - } - }, "installation-source": "dist", "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], "psr-4": { "Twig\\": "src/" } @@ -1717,7 +1869,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.4.3" + "source": "https://github.com/twigphp/Twig/tree/v3.14.0" }, "funding": [ { diff --git a/data/web/inc/lib/vendor/composer/installed.php b/data/web/inc/lib/vendor/composer/installed.php index c45f177e2..5d78fc9f3 100644 --- a/data/web/inc/lib/vendor/composer/installed.php +++ b/data/web/inc/lib/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => '__root__', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '8e0b1d8aee4af02311692cb031695cc2ac3850fd', + 'reference' => '220fdbb168792c07493db330d898b345cc902055', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -13,7 +13,7 @@ '__root__' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '8e0b1d8aee4af02311692cb031695cc2ac3850fd', + 'reference' => '220fdbb168792c07493db330d898b345cc902055', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -175,6 +175,15 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'symfony/deprecation-contracts' => array( + 'pretty_version' => 'v3.5.0', + 'version' => '3.5.0.0', + 'reference' => '0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/deprecation-contracts', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'symfony/polyfill-ctype' => array( 'pretty_version' => 'v1.24.0', 'version' => '1.24.0.0', @@ -202,6 +211,15 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'symfony/polyfill-php81' => array( + 'pretty_version' => 'v1.31.0', + 'version' => '1.31.0.0', + 'reference' => '4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-php81', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'symfony/translation' => array( 'pretty_version' => 'v6.0.5', 'version' => '6.0.5.0', @@ -245,9 +263,9 @@ 'dev_requirement' => false, ), 'twig/twig' => array( - 'pretty_version' => 'v3.4.3', - 'version' => '3.4.3.0', - 'reference' => 'c38fd6b0b7f370c198db91ffd02e23b517426b58', + 'pretty_version' => 'v3.14.0', + 'version' => '3.14.0.0', + 'reference' => '126b2c97818dbff0cdf3fbfc881aedb3d40aae72', 'type' => 'library', 'install_path' => __DIR__ . '/../twig/twig', 'aliases' => array(), diff --git a/data/web/inc/lib/vendor/composer/platform_check.php b/data/web/inc/lib/vendor/composer/platform_check.php index b168ddd5d..4c3a5d68f 100644 --- a/data/web/inc/lib/vendor/composer/platform_check.php +++ b/data/web/inc/lib/vendor/composer/platform_check.php @@ -4,8 +4,8 @@ $issues = array(); -if (!(PHP_VERSION_ID >= 80002)) { - $issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.2". You are running ' . PHP_VERSION . '.'; +if (!(PHP_VERSION_ID >= 80100)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.'; } if ($issues) { diff --git a/data/web/inc/lib/vendor/symfony/deprecation-contracts/CHANGELOG.md b/data/web/inc/lib/vendor/symfony/deprecation-contracts/CHANGELOG.md new file mode 100644 index 000000000..7932e2613 --- /dev/null +++ b/data/web/inc/lib/vendor/symfony/deprecation-contracts/CHANGELOG.md @@ -0,0 +1,5 @@ +CHANGELOG +========= + +The changelog is maintained for all Symfony contracts at the following URL: +https://github.com/symfony/contracts/blob/main/CHANGELOG.md diff --git a/data/web/inc/lib/vendor/symfony/deprecation-contracts/LICENSE b/data/web/inc/lib/vendor/symfony/deprecation-contracts/LICENSE new file mode 100644 index 000000000..0ed3a2465 --- /dev/null +++ b/data/web/inc/lib/vendor/symfony/deprecation-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/data/web/inc/lib/vendor/symfony/deprecation-contracts/README.md b/data/web/inc/lib/vendor/symfony/deprecation-contracts/README.md new file mode 100644 index 000000000..9814864c0 --- /dev/null +++ b/data/web/inc/lib/vendor/symfony/deprecation-contracts/README.md @@ -0,0 +1,26 @@ +Symfony Deprecation Contracts +============================= + +A generic function and convention to trigger deprecation notices. + +This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices. + +By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component, +the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments. + +The function requires at least 3 arguments: + - the name of the Composer package that is triggering the deprecation + - the version of the package that introduced the deprecation + - the message of the deprecation + - more arguments can be provided: they will be inserted in the message using `printf()` formatting + +Example: +```php +trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin'); +``` + +This will generate the following message: +`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.` + +While not recommended, the deprecation notices can be completely ignored by declaring an empty +`function trigger_deprecation() {}` in your application. diff --git a/data/web/inc/lib/vendor/symfony/deprecation-contracts/composer.json b/data/web/inc/lib/vendor/symfony/deprecation-contracts/composer.json new file mode 100644 index 000000000..ceb6c0796 --- /dev/null +++ b/data/web/inc/lib/vendor/symfony/deprecation-contracts/composer.json @@ -0,0 +1,35 @@ +{ + "name": "symfony/deprecation-contracts", + "type": "library", + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.1" + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + } +} diff --git a/data/web/inc/lib/vendor/symfony/deprecation-contracts/function.php b/data/web/inc/lib/vendor/symfony/deprecation-contracts/function.php new file mode 100644 index 000000000..2d56512ba --- /dev/null +++ b/data/web/inc/lib/vendor/symfony/deprecation-contracts/function.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (!function_exists('trigger_deprecation')) { + /** + * Triggers a silenced deprecation notice. + * + * @param string $package The name of the Composer package that is triggering the deprecation + * @param string $version The version of the package that introduced the deprecation + * @param string $message The message of the deprecation + * @param mixed ...$args Values to insert in the message using printf() formatting + * + * @author Nicolas Grekas + */ + function trigger_deprecation(string $package, string $version, string $message, mixed ...$args): void + { + @trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED); + } +} diff --git a/data/web/inc/lib/vendor/symfony/polyfill-php81/LICENSE b/data/web/inc/lib/vendor/symfony/polyfill-php81/LICENSE new file mode 100644 index 000000000..99c6bdf35 --- /dev/null +++ b/data/web/inc/lib/vendor/symfony/polyfill-php81/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2021-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/data/web/inc/lib/vendor/symfony/polyfill-php81/Php81.php b/data/web/inc/lib/vendor/symfony/polyfill-php81/Php81.php new file mode 100644 index 000000000..f0507b765 --- /dev/null +++ b/data/web/inc/lib/vendor/symfony/polyfill-php81/Php81.php @@ -0,0 +1,37 @@ + + * + * 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 + * + * @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; + } +} diff --git a/data/web/inc/lib/vendor/symfony/polyfill-php81/README.md b/data/web/inc/lib/vendor/symfony/polyfill-php81/README.md new file mode 100644 index 000000000..c07ef7820 --- /dev/null +++ b/data/web/inc/lib/vendor/symfony/polyfill-php81/README.md @@ -0,0 +1,18 @@ +Symfony Polyfill / Php81 +======================== + +This component provides features added to PHP 8.1 core: + +- [`array_is_list`](https://php.net/array_is_list) +- [`enum_exists`](https://php.net/enum-exists) +- [`MYSQLI_REFRESH_REPLICA`](https://php.net/mysqli.constants#constantmysqli-refresh-replica) constant +- [`ReturnTypeWillChange`](https://wiki.php.net/rfc/internal_method_return_types) +- [`CURLStringFile`](https://php.net/CURLStringFile) (but only if PHP >= 7.4 is used) + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/data/web/inc/lib/vendor/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php b/data/web/inc/lib/vendor/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php new file mode 100644 index 000000000..5ff93fcaf --- /dev/null +++ b/data/web/inc/lib/vendor/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php @@ -0,0 +1,51 @@ + + * + * 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; + } + } +} diff --git a/data/web/inc/lib/vendor/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php b/data/web/inc/lib/vendor/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php new file mode 100644 index 000000000..cb7720a8d --- /dev/null +++ b/data/web/inc/lib/vendor/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php @@ -0,0 +1,20 @@ + + * + * 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() + { + } + } +} diff --git a/data/web/inc/lib/vendor/symfony/polyfill-php81/bootstrap.php b/data/web/inc/lib/vendor/symfony/polyfill-php81/bootstrap.php new file mode 100644 index 000000000..9f872e02f --- /dev/null +++ b/data/web/inc/lib/vendor/symfony/polyfill-php81/bootstrap.php @@ -0,0 +1,28 @@ + + * + * 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; } +} diff --git a/data/web/inc/lib/vendor/symfony/polyfill-php81/composer.json b/data/web/inc/lib/vendor/symfony/polyfill-php81/composer.json new file mode 100644 index 000000000..28b6408ea --- /dev/null +++ b/data/web/inc/lib/vendor/symfony/polyfill-php81/composer.json @@ -0,0 +1,33 @@ +{ + "name": "symfony/polyfill-php81", + "type": "library", + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "keywords": ["polyfill", "shim", "compatibility", "portable"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Php81\\": "" }, + "files": [ "bootstrap.php" ], + "classmap": [ "Resources/stubs" ] + }, + "minimum-stability": "dev", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/data/web/inc/lib/vendor/twig/twig/.editorconfig b/data/web/inc/lib/vendor/twig/twig/.editorconfig deleted file mode 100644 index 270f1d1b7..000000000 --- a/data/web/inc/lib/vendor/twig/twig/.editorconfig +++ /dev/null @@ -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 diff --git a/data/web/inc/lib/vendor/twig/twig/.gitattributes b/data/web/inc/lib/vendor/twig/twig/.gitattributes deleted file mode 100644 index 06bc36713..000000000 --- a/data/web/inc/lib/vendor/twig/twig/.gitattributes +++ /dev/null @@ -1,4 +0,0 @@ -/doc/ export-ignore -/extra/ export-ignore -/tests/ export-ignore -/phpunit.xml.dist export-ignore diff --git a/data/web/inc/lib/vendor/twig/twig/.github/workflows/ci.yml b/data/web/inc/lib/vendor/twig/twig/.github/workflows/ci.yml deleted file mode 100644 index 65c2c4077..000000000 --- a/data/web/inc/lib/vendor/twig/twig/.github/workflows/ci.yml +++ /dev/null @@ -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" diff --git a/data/web/inc/lib/vendor/twig/twig/.github/workflows/documentation.yml b/data/web/inc/lib/vendor/twig/twig/.github/workflows/documentation.yml deleted file mode 100644 index 5caa8a33f..000000000 --- a/data/web/inc/lib/vendor/twig/twig/.github/workflows/documentation.yml +++ /dev/null @@ -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/' diff --git a/data/web/inc/lib/vendor/twig/twig/.gitignore b/data/web/inc/lib/vendor/twig/twig/.gitignore deleted file mode 100644 index b197246ba..000000000 --- a/data/web/inc/lib/vendor/twig/twig/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -/doc/_build/vendor -/doc/_build/output -/composer.lock -/phpunit.xml -/vendor -.phpunit.result.cache diff --git a/data/web/inc/lib/vendor/twig/twig/.php-cs-fixer.dist.php b/data/web/inc/lib/vendor/twig/twig/.php-cs-fixer.dist.php deleted file mode 100644 index b07ac7fca..000000000 --- a/data/web/inc/lib/vendor/twig/twig/.php-cs-fixer.dist.php +++ /dev/null @@ -1,20 +0,0 @@ -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__)) -; diff --git a/data/web/inc/lib/vendor/twig/twig/CHANGELOG b/data/web/inc/lib/vendor/twig/twig/CHANGELOG index 379387644..44c79b133 100644 --- a/data/web/inc/lib/vendor/twig/twig/CHANGELOG +++ b/data/web/inc/lib/vendor/twig/twig/CHANGELOG @@ -1,3 +1,178 @@ +# 3.14.0 (2024-09-09) + + * Fix a security issue when an included sandboxed template has been loaded before without the sandbox context + * Add the possibility to reset globals via `Environment::resetGlobals()` + * Deprecate `Environment::mergeGlobals()` + +# 3.13.0 (2024-09-07) + + * Add the `types` tag (experimental) + * Deprecate the `Twig\Test\NodeTestCase::getTests()` data provider, override `provideTests()` instead. + * Mark `Twig\Test\NodeTestCase::getEnvironment()` as final, override `createEnvironment()` instead. + * Deprecate `Twig\Test\NodeTestCase::getVariableGetter()`, call `createVariableGetter()` instead. + * Deprecate `Twig\Test\NodeTestCase::getAttributeGetter()`, call `createAttributeGetter()` instead. + * Deprecate not overriding `Twig\Test\IntegrationTestCase::getFixturesDirectory()`, this method will be abstract in 4.0 + * Marked `Twig\Test\IntegrationTestCase::getTests()` and `getLegacyTests()` as final + +# 3.12.0 (2024-08-29) + + * Deprecate the fact that the `extends` and `use` tags are always allowed in a sandboxed template. + This behavior will change in 4.0 where these tags will need to be explicitly allowed like any other tag. + * Deprecate the "tag" constructor argument of the "Twig\Node\Node" class as the tag is now automatically set by the Parser when needed + * Fix precedence of two-word tests when the first word is a valid test + * Deprecate the `spaceless` filter + * Deprecate some internal methods from `Parser`: `getBlockStack()`, `hasBlock()`, `getBlock()`, `hasMacro()`, `hasTraits()`, `getParent()` + * Deprecate passing `null` to `Twig\Parser::setParent()` + * Update `Node::__toString()` to include the node tag if set + * Add support for integers in methods of `Twig\Node\Node` that take a Node name + * Deprecate not passing a `BodyNode` instance as the body of a `ModuleNode` or `MacroNode` constructor + * Deprecate returning "null" from "TokenParserInterface::parse()". + * Deprecate `OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES` + * Fix performance regression when `use_yield` is `false` (which is the default) + * Improve compatibility when `use_yield` is `false` (as extensions still using `echo` will work as is) + * Accept colons (`:`) in addition to equals (`=`) to separate argument names and values in named arguments + * Add the `html_cva` function (in the HTML extra package) + * Add support for named arguments to the `block` and `attribute` functions + * Throw a SyntaxError exception at compile time when a Twig callable has not the minimum number of required arguments + * Add a `CallableArgumentsExtractor` class + * Deprecate passing a name to `FunctionExpression`, `FilterExpression`, and `TestExpression`; + pass a `TwigFunction`, `TwigFilter`, or `TestFilter` instead + * Deprecate all Twig callable attributes on `FunctionExpression`, `FilterExpression`, and `TestExpression` + * Deprecate the `filter` node of `FilterExpression` + * Add the notion of Twig callables (functions, filters, and tests) + * Bump minimum PHP version to 8.0 + * Fix integration tests when a test has more than one data/expect section and deprecations + * Add the `enum_cases` function + +# 3.11.0 (2024-08-08) + + * Deprecate `OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER` + * Add `Twig\Cache\ChainCache` and `Twig\Cache\ReadOnlyFilesystemCache` + * Add the possibility to deprecate attributes and nodes on `Node` + * Add the possibility to add a package and a version to the `deprecated` tag + * Add the possibility to add a package for filter/function/test deprecations + * Mark `ConstantExpression` as being `@final` + * Add the `find` filter + * Fix optimizer mode validation in `OptimizerNodeVisitor` + * Add the possibility to yield from a generator in `PrintNode` + * Add the `shuffle` filter + * Add the `singular` and `plural` filters in `StringExtension` + * Deprecate the second argument of `Twig\Node\Expression\CallExpression::compileArguments()` + * Deprecate `Twig\ExpressionParser\parseHashExpression()` in favor of + `Twig\ExpressionParser::parseMappingExpression()` + * Deprecate `Twig\ExpressionParser\parseArrayExpression()` in favor of + `Twig\ExpressionParser::parseSequenceExpression()` + * Add `sequence` and `mapping` tests + * Deprecate `Twig\Node\Expression\NameExpression::isSimple()` and + `Twig\Node\Expression\NameExpression::isSpecial()` + +# 3.10.3 (2024-05-16) + + * Fix missing ; in generated code + +# 3.10.2 (2024-05-14) + + * Fix support for the deprecated escaper signature + +# 3.10.1 (2024-05-12) + + * Fix BC break on escaper extension + * Fix constant return type + +# 3.10.0 (2024-05-11) + + * Make `CoreExtension::formatDate`, `CoreExtension::convertDate`, and + `CoreExtension::formatNumber` part of the public API + * Add `needs_charset` option for filters and functions + * Extract the escaping logic from the `EscaperExtension` class to a new + `EscaperRuntime` class. + + The following methods from ``Twig\\Extension\\EscaperExtension`` are + deprecated: ``setEscaper()``, ``getEscapers()``, ``setSafeClasses``, + ``addSafeClasses()``. Use the same methods on the + ``Twig\\Runtime\\EscaperRuntime`` class instead. + * Fix capturing output from extensions that still use echo + * Fix a PHP warning in the Lexer on malformed templates + * Fix blocks not available under some circumstances + * Synchronize source context in templates when setting a Node on a Node + +# 3.9.3 (2024-04-18) + + * Add missing `twig_escape_filter_is_safe` deprecated function + * Fix yield usage with CaptureNode + * Add missing unwrap call when using a TemplateWrapper instance internally + * Ensure Lexer is initialized early on + +# 3.9.2 (2024-04-17) + + * Fix usage of display_end hook + +# 3.9.1 (2024-04-17) + + * Fix missing `$blocks` variable in `CaptureNode` + +# 3.9.0 (2024-04-16) + + * Add support for PHP 8.4 + * Deprecate AbstractNodeVisitor + * Deprecate passing Template to Environment::resolveTemplate(), Environment::load(), and Template::loadTemplate() + * Add a new "yield" mode for output generation; + Node implementations that use "echo" or "print" should use "yield" instead; + all Node implementations should be flagged with `#[YieldReady]` once they've been made ready for "yield"; + the "use_yield" Environment option can be turned on when all nodes have been made `#[YieldReady]`; + "yield" will be the only strategy supported in the next major version + * Add return type for Symfony 7 compatibility + * Fix premature loop exit in Security Policy lookup of allowed methods/properties + * Deprecate all internal extension functions in favor of methods on the extension classes + * Mark all extension functions as @internal + * Add SourcePolicyInterface to selectively enable the Sandbox based on a template's Source + * Throw a proper Twig exception when using cycle on an empty array + +# 3.8.0 (2023-11-21) + + * Catch errors thrown during template rendering + * Fix IntlExtension::formatDateTime use of date formatter prototype + * Fix premature loop exit in Security Policy lookup of allowed methods/properties + * Remove NumberFormatter::TYPE_CURRENCY (deprecated in PHP 8.3) + * Restore return type annotations + * Allow Symfony 7 packages to be installed + * Deprecate `twig_test_iterable` function. Use the native `is_iterable` instead. + +# 3.7.1 (2023-08-28) + + * Fix some phpdocs + +# 3.7.0 (2023-07-26) + + * Add support for the ...spread operator on arrays and hashes + +# 3.6.1 (2023-06-08) + + * Suppress some native return type deprecation messages + +# 3.6.0 (2023-05-03) + + * Allow psr/container 2.0 + * Add the new PHP 8.0 IntlDateFormatter::RELATIVE_* constants for date formatting + * Make the Lexer initialize itself lazily + +# 3.5.1 (2023-02-08) + + * Arrow functions passed to the "reduce" filter now accept the current key as a third argument + * Restores the leniency of the matches twig comparison + * Fix error messages in sandboxed mode for "has some" and "has every" + +# 3.5.0 (2022-12-27) + + * Make Twig\ExpressionParser non-internal + * Add "has some" and "has every" operators + * Add Compile::reset() + * Throw a better runtime error when the "matches" regexp is not valid + * Add "twig *_names" intl functions + * Fix optimizing closures callbacks + * Add a better exception when getting an undefined constant via `constant` + * Fix `if` nodes when outside of a block and with an empty body + # 3.4.3 (2022-09-28) * Fix a security issue on filesystem loader (possibility to load a template outside a configured directory) @@ -141,7 +316,7 @@ * removed Parser::isReservedMacroName() * removed SanboxedPrintNode * removed Node::setTemplateName() - * made classes maked as "@final" final + * made classes marked as "@final" final * removed InitRuntimeInterface, ExistsLoaderInterface, and SourceContextLoaderInterface * removed the "spaceless" tag * removed Twig\Environment::getBaseTemplateClass() and Twig\Environment::setBaseTemplateClass() diff --git a/data/web/inc/lib/vendor/twig/twig/LICENSE b/data/web/inc/lib/vendor/twig/twig/LICENSE index 8711927f6..fd8234e51 100644 --- a/data/web/inc/lib/vendor/twig/twig/LICENSE +++ b/data/web/inc/lib/vendor/twig/twig/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2009-2022 by the Twig Team. +Copyright (c) 2009-present by the Twig Team. All rights reserved. diff --git a/data/web/inc/lib/vendor/twig/twig/README.rst b/data/web/inc/lib/vendor/twig/twig/README.rst index fbe7e9a9f..7bf8c673e 100644 --- a/data/web/inc/lib/vendor/twig/twig/README.rst +++ b/data/web/inc/lib/vendor/twig/twig/README.rst @@ -11,7 +11,7 @@ Sponsors .. raw:: html - + Blackfire.io diff --git a/data/web/inc/lib/vendor/twig/twig/composer.json b/data/web/inc/lib/vendor/twig/twig/composer.json index 33e46405c..e0c3e6c6c 100644 --- a/data/web/inc/lib/vendor/twig/twig/composer.json +++ b/data/web/inc/lib/vendor/twig/twig/composer.json @@ -24,15 +24,23 @@ } ], "require": { - "php": ">=7.2.5", + "php": ">=8.0.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-ctype": "^1.8" + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-php81": "^1.29" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0", - "psr/container": "^1.0" + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0", + "psr/container": "^1.0|^2.0" }, "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], "psr-4" : { "Twig\\" : "src/" } @@ -41,10 +49,5 @@ "psr-4" : { "Twig\\Tests\\" : "tests/" } - }, - "extra": { - "branch-alias": { - "dev-master": "3.4-dev" - } } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/AbstractTwigCallable.php b/data/web/inc/lib/vendor/twig/twig/src/AbstractTwigCallable.php new file mode 100644 index 000000000..f67184300 --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/AbstractTwigCallable.php @@ -0,0 +1,136 @@ + + */ +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); + } +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/Attribute/FirstClassTwigCallableReady.php b/data/web/inc/lib/vendor/twig/twig/src/Attribute/FirstClassTwigCallableReady.php new file mode 100644 index 000000000..ffd8cffc8 --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Attribute/FirstClassTwigCallableReady.php @@ -0,0 +1,20 @@ + + */ +final class ChainCache implements CacheInterface +{ + /** + * @param iterable $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)); + } +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/Cache/FilesystemCache.php b/data/web/inc/lib/vendor/twig/twig/src/Cache/FilesystemCache.php index e075563ae..2e79fac05 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Cache/FilesystemCache.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Cache/FilesystemCache.php @@ -50,11 +50,11 @@ class FilesystemCache implements CacheInterface if (false === @mkdir($dir, 0777, true)) { clearstatcache(true, $dir); if (!is_dir($dir)) { - throw new \RuntimeException(sprintf('Unable to create the cache directory (%s).', $dir)); + throw new \RuntimeException(\sprintf('Unable to create the cache directory (%s).', $dir)); } } } elseif (!is_writable($dir)) { - throw new \RuntimeException(sprintf('Unable to write in the cache directory (%s).', $dir)); + throw new \RuntimeException(\sprintf('Unable to write in the cache directory (%s).', $dir)); } $tmpFile = tempnam($dir, basename($key)); @@ -63,7 +63,7 @@ class FilesystemCache implements CacheInterface if (self::FORCE_BYTECODE_INVALIDATION == ($this->options & self::FORCE_BYTECODE_INVALIDATION)) { // Compile cached file into bytecode cache - if (\function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) { + if (\function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) { @opcache_invalidate($key, true); } elseif (\function_exists('apc_compile_file')) { apc_compile_file($key); @@ -73,7 +73,7 @@ class FilesystemCache implements CacheInterface return; } - throw new \RuntimeException(sprintf('Failed to write cache file "%s".', $key)); + throw new \RuntimeException(\sprintf('Failed to write cache file "%s".', $key)); } public function getTimestamp(string $key): int diff --git a/data/web/inc/lib/vendor/twig/twig/src/Cache/ReadOnlyFilesystemCache.php b/data/web/inc/lib/vendor/twig/twig/src/Cache/ReadOnlyFilesystemCache.php new file mode 100644 index 000000000..3ba6514c9 --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Cache/ReadOnlyFilesystemCache.php @@ -0,0 +1,25 @@ + + */ +class ReadOnlyFilesystemCache extends FilesystemCache +{ + public function write(string $key, string $content): void + { + // Do nothing with the content, it's a read-only filesystem. + } +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/Compiler.php b/data/web/inc/lib/vendor/twig/twig/src/Compiler.php index 95e1f183b..1a43aa7f6 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Compiler.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Compiler.php @@ -22,15 +22,16 @@ class Compiler private $lastLine; private $source; private $indentation; - private $env; private $debugInfo = []; private $sourceOffset; private $sourceLine; private $varNameSalt = 0; + private $didUseEcho = false; + private $didUseEchoStack = []; - public function __construct(Environment $env) - { - $this->env = $env; + public function __construct( + private Environment $env, + ) { } public function getEnvironment(): Environment @@ -46,7 +47,7 @@ class Compiler /** * @return $this */ - public function compile(Node $node, int $indentation = 0) + public function reset(int $indentation = 0) { $this->lastLine = null; $this->source = ''; @@ -57,23 +58,54 @@ class Compiler $this->indentation = $indentation; $this->varNameSalt = 0; - $node->compile($this); - return $this; } + /** + * @return $this + */ + public function compile(Node $node, int $indentation = 0) + { + $this->reset($indentation); + $this->didUseEchoStack[] = $this->didUseEcho; + + try { + $this->didUseEcho = false; + $node->compile($this); + + if ($this->didUseEcho) { + trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[YieldReady].', $this->didUseEcho, \get_class($node)); + } + + return $this; + } finally { + $this->didUseEcho = array_pop($this->didUseEchoStack); + } + } + /** * @return $this */ public function subcompile(Node $node, bool $raw = true) { - if (false === $raw) { + if (!$raw) { $this->source .= str_repeat(' ', $this->indentation * 4); } - $node->compile($this); + $this->didUseEchoStack[] = $this->didUseEcho; - return $this; + try { + $this->didUseEcho = false; + $node->compile($this); + + if ($this->didUseEcho) { + trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[YieldReady].', $this->didUseEcho, \get_class($node)); + } + + return $this; + } finally { + $this->didUseEcho = array_pop($this->didUseEchoStack); + } } /** @@ -83,6 +115,7 @@ class Compiler */ public function raw(string $string) { + $this->checkForEcho($string); $this->source .= $string; return $this; @@ -96,6 +129,7 @@ class Compiler public function write(...$strings) { foreach ($strings as $string) { + $this->checkForEcho($string); $this->source .= str_repeat(' ', $this->indentation * 4).$string; } @@ -109,7 +143,7 @@ class Compiler */ public function string(string $value) { - $this->source .= sprintf('"%s"', addcslashes($value, "\0\t\"\$\\")); + $this->source .= \sprintf('"%s"', addcslashes($value, "\0\t\"\$\\")); return $this; } @@ -161,7 +195,7 @@ class Compiler public function addDebugInfo(Node $node) { if ($node->getTemplateLine() != $this->lastLine) { - $this->write(sprintf("// line %d\n", $node->getTemplateLine())); + $this->write(\sprintf("// line %d\n", $node->getTemplateLine())); $this->sourceLine += substr_count($this->source, "\n", $this->sourceOffset); $this->sourceOffset = \strlen($this->source); @@ -209,6 +243,15 @@ class Compiler public function getVarName(): string { - return sprintf('__internal_compile_%d', $this->varNameSalt++); + return \sprintf('__internal_compile_%d', $this->varNameSalt++); + } + + private function checkForEcho(string $string): void + { + if ($this->didUseEcho) { + return; + } + + $this->didUseEcho = preg_match('/^\s*+(echo|print)\b/', $string, $m) ? $m[1] : false; } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Environment.php b/data/web/inc/lib/vendor/twig/twig/src/Environment.php index 85aaab916..24e55e979 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Environment.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Environment.php @@ -22,12 +22,17 @@ use Twig\Extension\CoreExtension; use Twig\Extension\EscaperExtension; use Twig\Extension\ExtensionInterface; use Twig\Extension\OptimizerExtension; +use Twig\Extension\YieldNotReadyExtension; use Twig\Loader\ArrayLoader; use Twig\Loader\ChainLoader; use Twig\Loader\LoaderInterface; +use Twig\Node\Expression\Binary\AbstractBinary; +use Twig\Node\Expression\Unary\AbstractUnary; use Twig\Node\ModuleNode; use Twig\Node\Node; use Twig\NodeVisitor\NodeVisitorInterface; +use Twig\Runtime\EscaperRuntime; +use Twig\RuntimeLoader\FactoryRuntimeLoader; use Twig\RuntimeLoader\RuntimeLoaderInterface; use Twig\TokenParser\TokenParserInterface; @@ -38,11 +43,11 @@ use Twig\TokenParser\TokenParserInterface; */ class Environment { - public const VERSION = '3.4.3'; - public const VERSION_ID = 30403; + public const VERSION = '3.14.0'; + public const VERSION_ID = 31400; public const MAJOR_VERSION = 3; - public const MINOR_VERSION = 4; - public const RELEASE_VERSION = 3; + public const MINOR_VERSION = 14; + public const RELEASE_VERSION = 0; public const EXTRA_VERSION = ''; private $charset; @@ -53,16 +58,19 @@ class Environment private $lexer; private $parser; private $compiler; + /** @var array */ private $globals = []; private $resolvedGlobals; private $loadedTemplates; private $strictVariables; - private $templateClassPrefix = '__TwigTemplate_'; private $originalCache; private $extensionSet; private $runtimeLoaders = []; private $runtimes = []; private $optionsHash; + /** @var bool */ + private $useYield; + private $defaultRuntimeLoader; /** * Constructor. @@ -94,8 +102,12 @@ class Environment * * optimizations: A flag that indicates which optimizations to apply * (default to -1 which means that all optimizations are enabled; * set it to 0 to disable). + * + * * use_yield: true: forces templates to exclusively use "yield" instead of "echo" (all extensions must be yield ready) + * false (default): allows templates to use a mix of "yield" and "echo" calls to allow for a progressive migration + * Switch to "true" when possible as this will be the only supported mode in Twig 4.0 */ - public function __construct(LoaderInterface $loader, $options = []) + public function __construct(LoaderInterface $loader, array $options = []) { $this->setLoader($loader); @@ -107,20 +119,38 @@ class Environment 'cache' => false, 'auto_reload' => null, 'optimizations' => -1, + 'use_yield' => false, ], $options); + $this->useYield = (bool) $options['use_yield']; $this->debug = (bool) $options['debug']; $this->setCharset($options['charset'] ?? 'UTF-8'); $this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload']; $this->strictVariables = (bool) $options['strict_variables']; $this->setCache($options['cache']); $this->extensionSet = new ExtensionSet(); + $this->defaultRuntimeLoader = new FactoryRuntimeLoader([ + EscaperRuntime::class => function () { return new EscaperRuntime($this->charset); }, + ]); $this->addExtension(new CoreExtension()); - $this->addExtension(new EscaperExtension($options['autoescape'])); + $escaperExt = new EscaperExtension($options['autoescape']); + $escaperExt->setEnvironment($this, false); + $this->addExtension($escaperExt); + if (\PHP_VERSION_ID >= 80000) { + $this->addExtension(new YieldNotReadyExtension($this->useYield)); + } $this->addExtension(new OptimizerExtension($options['optimizations'])); } + /** + * @internal + */ + public function useYield(): bool + { + return $this->useYield; + } + /** * Enables debugging mode. */ @@ -246,7 +276,6 @@ class Environment * * * The cache key for the given template; * * The currently enabled extensions; - * * Whether the Twig C extension is available or not; * * PHP version; * * Twig version; * * Options with what environment was created. @@ -256,11 +285,11 @@ class Environment * * @internal */ - public function getTemplateClass(string $name, int $index = null): string + public function getTemplateClass(string $name, ?int $index = null): string { $key = $this->getLoader()->getCacheKey($name).$this->optionsHash; - return $this->templateClassPrefix.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index); + return '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index); } /** @@ -305,6 +334,11 @@ class Environment if ($name instanceof TemplateWrapper) { return $name; } + if ($name instanceof Template) { + trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', self::class, __METHOD__); + + return $name; + } return new TemplateWrapper($this, $this->loadTemplate($this->getTemplateClass($name), $name)); } @@ -315,8 +349,8 @@ class Environment * This method is for internal use only and should never be called * directly. * - * @param string $name The template name - * @param int $index The index if it is an embedded template + * @param string $name The template name + * @param int|null $index The index if it is an embedded template * * @throws LoaderError When the template cannot be found * @throws RuntimeError When a previously generated cache is corrupted @@ -324,7 +358,7 @@ class Environment * * @internal */ - public function loadTemplate(string $cls, string $name, int $index = null): Template + public function loadTemplate(string $cls, string $name, ?int $index = null): Template { $mainCls = $cls; if (null !== $index) { @@ -342,7 +376,6 @@ class Environment $this->cache->load($key); } - $source = null; if (!class_exists($cls, false)) { $source = $this->getLoader()->getSourceContext($name); $content = $this->compileSource($source); @@ -359,7 +392,7 @@ class Environment } if (!class_exists($cls, false)) { - throw new RuntimeError(sprintf('Failed to load Twig template "%s", index "%s": cache might be corrupted.', $name, $index), -1, $source); + throw new RuntimeError(\sprintf('Failed to load Twig template "%s", index "%s": cache might be corrupted.', $name, $index), -1, $source); } } } @@ -374,19 +407,19 @@ class Environment * * This method should not be used as a generic way to load templates. * - * @param string $template The template source - * @param string $name An optional name of the template to be used in error messages + * @param string $template The template source + * @param string|null $name An optional name of the template to be used in error messages * * @throws LoaderError When the template cannot be found * @throws SyntaxError When an error occurred during compilation */ - public function createTemplate(string $template, string $name = null): TemplateWrapper + public function createTemplate(string $template, ?string $name = null): TemplateWrapper { $hash = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $template, false); if (null !== $name) { - $name = sprintf('%s (string template %s)', $name, $hash); + $name = \sprintf('%s (string template %s)', $name, $hash); } else { - $name = sprintf('__string_template__%s', $hash); + $name = \sprintf('__string_template__%s', $hash); } $loader = new ChainLoader([ @@ -419,10 +452,10 @@ class Environment /** * Tries to load a template consecutively from an array. * - * Similar to load() but it also accepts instances of \Twig\Template and - * \Twig\TemplateWrapper, and an array of templates where each is tried to be loaded. + * Similar to load() but it also accepts instances of \Twig\TemplateWrapper + * and an array of templates where each is tried to be loaded. * - * @param string|TemplateWrapper|array $names A template or an array of templates to try consecutively + * @param string|TemplateWrapper|array $names A template or an array of templates to try consecutively * * @throws LoaderError When none of the templates can be found * @throws SyntaxError When an error occurred during compilation @@ -436,7 +469,9 @@ class Environment $count = \count($names); foreach ($names as $name) { if ($name instanceof Template) { - return $name; + trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', Template::class, __METHOD__); + + return new TemplateWrapper($this, $name); } if ($name instanceof TemplateWrapper) { return $name; @@ -449,7 +484,7 @@ class Environment return $this->load($name); } - throw new LoaderError(sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names))); + throw new LoaderError(\sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names))); } public function setLexer(Lexer $lexer) @@ -518,7 +553,7 @@ class Environment $e->setSourceContext($source); throw $e; } catch (\Exception $e) { - throw new SyntaxError(sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e); + throw new SyntaxError(\sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e); } } @@ -534,7 +569,7 @@ class Environment public function setCharset(string $charset) { - if ('UTF8' === $charset = null === $charset ? null : strtoupper($charset)) { + if ('UTF8' === $charset = strtoupper($charset ?: '')) { // iconv on Windows requires "UTF-8" instead of "UTF8" $charset = 'UTF-8'; } @@ -592,7 +627,11 @@ class Environment } } - throw new RuntimeError(sprintf('Unable to load the "%s" runtime.', $class)); + if (null !== $runtime = $this->defaultRuntimeLoader->load($class)) { + return $this->runtimes[$class] = $runtime; + } + + throw new RuntimeError(\sprintf('Unable to load the "%s" runtime.', $class)); } public function addExtension(ExtensionInterface $extension) @@ -763,7 +802,7 @@ class Environment public function addGlobal(string $name, $value) { if ($this->extensionSet->isInitialized() && !\array_key_exists($name, $this->getGlobals())) { - throw new \LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name)); + throw new \LogicException(\sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name)); } if (null !== $this->resolvedGlobals) { @@ -775,6 +814,8 @@ class Environment /** * @internal + * + * @return array */ public function getGlobals(): array { @@ -789,21 +830,26 @@ class Environment return array_merge($this->extensionSet->getGlobals(), $this->globals); } + public function resetGlobals(): void + { + $this->resolvedGlobals = null; + $this->extensionSet->resetGlobals(); + } + + /** + * @deprecated since Twig 3.14 + */ public function mergeGlobals(array $context): array { - // we don't use array_merge as the context being generally - // bigger than globals, this code is faster. - foreach ($this->getGlobals() as $key => $value) { - if (!\array_key_exists($key, $context)) { - $context[$key] = $value; - } - } + trigger_deprecation('twig/twig', '3.14', 'The "%s" method is deprecated.', __METHOD__); - return $context; + return $context + $this->getGlobals(); } /** * @internal + * + * @return array}> */ public function getUnaryOperators(): array { @@ -812,6 +858,8 @@ class Environment /** * @internal + * + * @return array, associativity: ExpressionParser::OPERATOR_*}> */ public function getBinaryOperators(): array { @@ -827,6 +875,7 @@ class Environment self::VERSION, (int) $this->debug, (int) $this->strictVariables, + $this->useYield ? '1' : '0', ]); } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Error/Error.php b/data/web/inc/lib/vendor/twig/twig/src/Error/Error.php index a68be65f2..61c309fa1 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Error/Error.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Error/Error.php @@ -53,7 +53,7 @@ class Error extends \Exception * @param int $lineno The template line where the error occurred * @param Source|null $source The source context where the error occurred */ - public function __construct(string $message, int $lineno = -1, Source $source = null, \Exception $previous = null) + public function __construct(string $message, int $lineno = -1, ?Source $source = null, ?\Throwable $previous = null) { parent::__construct('', 0, $previous); @@ -93,7 +93,7 @@ class Error extends \Exception return $this->name ? new Source($this->sourceCode, $this->name, $this->sourcePath) : null; } - public function setSourceContext(Source $source = null): void + public function setSourceContext(?Source $source = null): void { if (null === $source) { $this->sourceCode = $this->name = $this->sourcePath = null; @@ -130,28 +130,28 @@ class Error extends \Exception } $dot = false; - if ('.' === substr($this->message, -1)) { + if (str_ends_with($this->message, '.')) { $this->message = substr($this->message, 0, -1); $dot = true; } $questionMark = false; - if ('?' === substr($this->message, -1)) { + if (str_ends_with($this->message, '?')) { $this->message = substr($this->message, 0, -1); $questionMark = true; } if ($this->name) { - if (\is_string($this->name) || (\is_object($this->name) && method_exists($this->name, '__toString'))) { - $name = sprintf('"%s"', $this->name); + if (\is_string($this->name) || $this->name instanceof \Stringable) { + $name = \sprintf('"%s"', $this->name); } else { $name = json_encode($this->name); } - $this->message .= sprintf(' in %s', $name); + $this->message .= \sprintf(' in %s', $name); } if ($this->lineno && $this->lineno >= 0) { - $this->message .= sprintf(' at line %d', $this->lineno); + $this->message .= \sprintf(' at line %d', $this->lineno); } if ($dot) { @@ -172,7 +172,7 @@ class Error extends \Exception foreach ($backtrace as $trace) { if (isset($trace['object']) && $trace['object'] instanceof Template) { $currentClass = \get_class($trace['object']); - $isEmbedContainer = null === $templateClass ? false : 0 === strpos($templateClass, $currentClass); + $isEmbedContainer = null === $templateClass ? false : str_starts_with($templateClass, $currentClass); if (null === $this->name || ($this->name == $trace['object']->getTemplateName() && !$isEmbedContainer)) { $template = $trace['object']; $templateClass = \get_class($trace['object']); diff --git a/data/web/inc/lib/vendor/twig/twig/src/Error/SyntaxError.php b/data/web/inc/lib/vendor/twig/twig/src/Error/SyntaxError.php index 726b3309e..841b653f5 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Error/SyntaxError.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Error/SyntaxError.php @@ -30,7 +30,7 @@ class SyntaxError extends Error $alternatives = []; foreach ($items as $item) { $lev = levenshtein($name, $item); - if ($lev <= \strlen($name) / 3 || false !== strpos($item, $name)) { + if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) { $alternatives[$item] = $lev; } } @@ -41,6 +41,6 @@ class SyntaxError extends Error asort($alternatives); - $this->appendMessage(sprintf(' Did you mean "%s"?', implode('", "', array_keys($alternatives)))); + $this->appendMessage(\sprintf(' Did you mean "%s"?', implode('", "', array_keys($alternatives)))); } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/ExpressionParser.php b/data/web/inc/lib/vendor/twig/twig/src/ExpressionParser.php index 70b6eb05c..dc4a6015d 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/ExpressionParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/ExpressionParser.php @@ -12,20 +12,21 @@ namespace Twig; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Error\SyntaxError; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ArrowFunctionExpression; use Twig\Node\Expression\AssignNameExpression; +use Twig\Node\Expression\Binary\AbstractBinary; use Twig\Node\Expression\Binary\ConcatBinary; -use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\MethodCallExpression; use Twig\Node\Expression\NameExpression; -use Twig\Node\Expression\ParentExpression; use Twig\Node\Expression\TestExpression; +use Twig\Node\Expression\Unary\AbstractUnary; use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; @@ -40,23 +41,22 @@ use Twig\Node\Node; * @see https://en.wikipedia.org/wiki/Operator-precedence_parser * * @author Fabien Potencier - * - * @internal */ class ExpressionParser { public const OPERATOR_LEFT = 1; public const OPERATOR_RIGHT = 2; - private $parser; - private $env; + /** @var array}> */ private $unaryOperators; + /** @var array, associativity: self::OPERATOR_*}> */ private $binaryOperators; + private $readyNodes = []; - public function __construct(Parser $parser, Environment $env) - { - $this->parser = $parser; - $this->env = $env; + public function __construct( + private Parser $parser, + private Environment $env, + ) { $this->unaryOperators = $env->getUnaryOperators(); $this->binaryOperators = $env->getBinaryOperators(); } @@ -80,7 +80,7 @@ class ExpressionParser } elseif (isset($op['callable'])) { $expr = $op['callable']($this->parser, $expr); } else { - $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']); + $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence'], true); $class = $op['class']; $expr = new $class($expr, $expr1, $token->getLine()); } @@ -103,52 +103,52 @@ class ExpressionParser $stream = $this->parser->getStream(); // short array syntax (one argument, no parentheses)? - if ($stream->look(1)->test(/* Token::ARROW_TYPE */ 12)) { + if ($stream->look(1)->test(Token::ARROW_TYPE)) { $line = $stream->getCurrent()->getLine(); - $token = $stream->expect(/* Token::NAME_TYPE */ 5); + $token = $stream->expect(Token::NAME_TYPE); $names = [new AssignNameExpression($token->getValue(), $token->getLine())]; - $stream->expect(/* Token::ARROW_TYPE */ 12); + $stream->expect(Token::ARROW_TYPE); return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line); } // first, determine if we are parsing an arrow function by finding => (long form) $i = 0; - if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { + if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, '(')) { return null; } ++$i; while (true) { // variable name ++$i; - if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ',')) { break; } ++$i; } - if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) { + if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ')')) { return null; } ++$i; - if (!$stream->look($i)->test(/* Token::ARROW_TYPE */ 12)) { + if (!$stream->look($i)->test(Token::ARROW_TYPE)) { return null; } // yes, let's parse it properly - $token = $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '('); + $token = $stream->expect(Token::PUNCTUATION_TYPE, '('); $line = $token->getLine(); $names = []; while (true) { - $token = $stream->expect(/* Token::NAME_TYPE */ 5); + $token = $stream->expect(Token::NAME_TYPE); $names[] = new AssignNameExpression($token->getValue(), $token->getLine()); - if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; } } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ')'); - $stream->expect(/* Token::ARROW_TYPE */ 12); + $stream->expect(Token::PUNCTUATION_TYPE, ')'); + $stream->expect(Token::ARROW_TYPE); return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line); } @@ -164,10 +164,10 @@ class ExpressionParser $class = $operator['class']; return $this->parsePostfixExpression(new $class($expr, $token->getLine())); - } elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { + } elseif ($token->test(Token::PUNCTUATION_TYPE, '(')) { $this->parser->getStream()->next(); $expr = $this->parseExpression(); - $this->parser->getStream()->expect(/* Token::PUNCTUATION_TYPE */ 9, ')', 'An opened parenthesis is not properly closed'); + $this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); return $this->parsePostfixExpression($expr); } @@ -177,15 +177,18 @@ class ExpressionParser private function parseConditionalExpression($expr): AbstractExpression { - while ($this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, '?')) { - if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) { + while ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, '?')) { + if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) { $expr2 = $this->parseExpression(); - if ($this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) { + if ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) { + // Ternary operator (expr ? expr2 : expr3) $expr3 = $this->parseExpression(); } else { + // Ternary without else (expr ? expr2) $expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine()); } } else { + // Ternary without then (expr ?: expr3) $expr2 = $expr; $expr3 = $this->parseExpression(); } @@ -198,19 +201,19 @@ class ExpressionParser private function isUnary(Token $token): bool { - return $token->test(/* Token::OPERATOR_TYPE */ 8) && isset($this->unaryOperators[$token->getValue()]); + return $token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]); } private function isBinary(Token $token): bool { - return $token->test(/* Token::OPERATOR_TYPE */ 8) && isset($this->binaryOperators[$token->getValue()]); + return $token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]); } public function parsePrimaryExpression() { $token = $this->parser->getCurrentToken(); switch ($token->getType()) { - case /* Token::NAME_TYPE */ 5: + case Token::NAME_TYPE: $this->parser->getStream()->next(); switch ($token->getValue()) { case 'true': @@ -239,17 +242,17 @@ class ExpressionParser } break; - case /* Token::NUMBER_TYPE */ 6: + case Token::NUMBER_TYPE: $this->parser->getStream()->next(); $node = new ConstantExpression($token->getValue(), $token->getLine()); break; - case /* Token::STRING_TYPE */ 7: - case /* Token::INTERPOLATION_START_TYPE */ 10: + case Token::STRING_TYPE: + case Token::INTERPOLATION_START_TYPE: $node = $this->parseStringExpression(); break; - case /* Token::OPERATOR_TYPE */ 8: + case Token::OPERATOR_TYPE: if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) { // in this context, string operators are variable names $this->parser->getStream()->next(); @@ -260,7 +263,7 @@ class ExpressionParser if (isset($this->unaryOperators[$token->getValue()])) { $class = $this->unaryOperators[$token->getValue()]['class']; if (!\in_array($class, [NegUnary::class, PosUnary::class])) { - throw new SyntaxError(sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); + throw new SyntaxError(\sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } $this->parser->getStream()->next(); @@ -272,14 +275,14 @@ class ExpressionParser // no break default: - if ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '[')) { - $node = $this->parseArrayExpression(); - } elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '{')) { - $node = $this->parseHashExpression(); - } elseif ($token->test(/* Token::OPERATOR_TYPE */ 8, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { - throw new SyntaxError(sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); + if ($token->test(Token::PUNCTUATION_TYPE, '[')) { + $node = $this->parseSequenceExpression(); + } elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) { + $node = $this->parseMappingExpression(); + } elseif ($token->test(Token::OPERATOR_TYPE, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { + throw new SyntaxError(\sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } else { - throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); + throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } } @@ -294,12 +297,12 @@ class ExpressionParser // a string cannot be followed by another string in a single expression $nextCanBeString = true; while (true) { - if ($nextCanBeString && $token = $stream->nextIf(/* Token::STRING_TYPE */ 7)) { + if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) { $nodes[] = new ConstantExpression($token->getValue(), $token->getLine()); $nextCanBeString = false; - } elseif ($stream->nextIf(/* Token::INTERPOLATION_START_TYPE */ 10)) { + } elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) { $nodes[] = $this->parseExpression(); - $stream->expect(/* Token::INTERPOLATION_END_TYPE */ 11); + $stream->expect(Token::INTERPOLATION_END_TYPE); $nextCanBeString = true; } else { break; @@ -314,56 +317,91 @@ class ExpressionParser return $expr; } + /** + * @deprecated since 3.11, use parseSequenceExpression() instead + */ public function parseArrayExpression() + { + trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseSequenceExpression()" instead.', __METHOD__); + + return $this->parseSequenceExpression(); + } + + public function parseSequenceExpression() { $stream = $this->parser->getStream(); - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '[', 'An array element was expected'); + $stream->expect(Token::PUNCTUATION_TYPE, '[', 'A sequence element was expected'); $node = new ArrayExpression([], $stream->getCurrent()->getLine()); $first = true; - while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) { + while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) { if (!$first) { - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'An array element must be followed by a comma'); + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma'); // trailing ,? - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) { + if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { break; } } $first = false; - $node->addElement($this->parseExpression()); + if ($stream->test(Token::SPREAD_TYPE)) { + $stream->next(); + $expr = $this->parseExpression(); + $expr->setAttribute('spread', true); + $node->addElement($expr); + } else { + $node->addElement($this->parseExpression()); + } } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']', 'An opened array is not properly closed'); + $stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed'); return $node; } + /** + * @deprecated since 3.11, use parseMappingExpression() instead + */ public function parseHashExpression() + { + trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseMappingExpression()" instead.', __METHOD__); + + return $this->parseMappingExpression(); + } + + public function parseMappingExpression() { $stream = $this->parser->getStream(); - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '{', 'A hash element was expected'); + $stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected'); $node = new ArrayExpression([], $stream->getCurrent()->getLine()); $first = true; - while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) { + while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) { if (!$first) { - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'A hash value must be followed by a comma'); + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma'); // trailing ,? - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) { + if ($stream->test(Token::PUNCTUATION_TYPE, '}')) { break; } } $first = false; - // a hash key can be: + if ($stream->test(Token::SPREAD_TYPE)) { + $stream->next(); + $value = $this->parseExpression(); + $value->setAttribute('spread', true); + $node->addElement($value); + continue; + } + + // a mapping key can be: // // * a number -- 12 // * a string -- 'a' // * a name, which is equivalent to a string -- a // * an expression, which must be enclosed in parentheses -- (1 + 2) - if ($token = $stream->nextIf(/* Token::NAME_TYPE */ 5)) { + if ($token = $stream->nextIf(Token::NAME_TYPE)) { $key = new ConstantExpression($token->getValue(), $token->getLine()); // {a} is a shortcut for {a:a} @@ -372,22 +410,22 @@ class ExpressionParser $node->addElement($value, $key); continue; } - } elseif (($token = $stream->nextIf(/* Token::STRING_TYPE */ 7)) || $token = $stream->nextIf(/* Token::NUMBER_TYPE */ 6)) { + } elseif (($token = $stream->nextIf(Token::STRING_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) { $key = new ConstantExpression($token->getValue(), $token->getLine()); - } elseif ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { + } elseif ($stream->test(Token::PUNCTUATION_TYPE, '(')) { $key = $this->parseExpression(); } else { $current = $stream->getCurrent(); - throw new SyntaxError(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('A mapping key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $stream->getSourceContext()); } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ':', 'A hash key must be followed by a colon (:)'); + $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)'); $value = $this->parseExpression(); $node->addElement($value, $key); } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '}', 'An opened hash is not properly closed'); + $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed'); return $node; } @@ -396,7 +434,7 @@ class ExpressionParser { while (true) { $token = $this->parser->getCurrentToken(); - if (/* Token::PUNCTUATION_TYPE */ 9 == $token->getType()) { + if (Token::PUNCTUATION_TYPE == $token->getType()) { if ('.' == $token->getValue() || '[' == $token->getValue()) { $node = $this->parseSubscriptExpression($node); } elseif ('|' == $token->getValue()) { @@ -414,50 +452,37 @@ class ExpressionParser public function getFunctionNode($name, $line) { - switch ($name) { - case 'parent': - $this->parseArguments(); - if (!\count($this->parser->getBlockStack())) { - throw new SyntaxError('Calling "parent" outside a block is forbidden.', $line, $this->parser->getStream()->getSourceContext()); - } + if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { + $arguments = new ArrayExpression([], $line); + foreach ($this->parseArguments() as $n) { + $arguments->addElement($n); + } - if (!$this->parser->getParent() && !$this->parser->hasTraits()) { - throw new SyntaxError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', $line, $this->parser->getStream()->getSourceContext()); - } + $node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line); + $node->setAttribute('safe', true); - return new ParentExpression($this->parser->peekBlockStack(), $line); - case 'block': - $args = $this->parseArguments(); - if (\count($args) < 1) { - throw new SyntaxError('The "block" function takes one argument (the block name).', $line, $this->parser->getStream()->getSourceContext()); - } - - return 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)) { - $arguments = new ArrayExpression([], $line); - foreach ($this->parseArguments() as $n) { - $arguments->addElement($n); - } - - $node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line); - $node->setAttribute('safe', true); - - return $node; - } - - $args = $this->parseArguments(true); - $class = $this->getFunctionNodeClass($name, $line); - - return new $class($name, $args, $line); + return $node; } + + $args = $this->parseArguments(true); + $function = $this->getFunction($name, $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) @@ -470,29 +495,25 @@ class ExpressionParser if ('.' == $token->getValue()) { $token = $stream->next(); if ( - /* Token::NAME_TYPE */ 5 == $token->getType() + Token::NAME_TYPE == $token->getType() || - /* Token::NUMBER_TYPE */ 6 == $token->getType() + Token::NUMBER_TYPE == $token->getType() || - (/* Token::OPERATOR_TYPE */ 8 == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue())) + (Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue())) ) { $arg = new ConstantExpression($token->getValue(), $lineno); - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { + if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { $type = Template::METHOD_CALL; foreach ($this->parseArguments() as $n) { $arguments->addElement($n); } } } else { - throw new SyntaxError(sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext()); + throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext()); } if ($node instanceof NameExpression && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) { - if (!$arg instanceof ConstantExpression) { - throw new SyntaxError(sprintf('Dynamic macro names are not supported (called on "%s").', $node->getAttribute('name')), $token->getLine(), $stream->getSourceContext()); - } - $name = $arg->getAttribute('value'); $node = new MethodCallExpression($node, 'macro_'.$name, $arguments, $lineno); @@ -505,34 +526,34 @@ class ExpressionParser // slice? $slice = false; - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ':')) { + if ($stream->test(Token::PUNCTUATION_TYPE, ':')) { $slice = true; $arg = new ConstantExpression(0, $token->getLine()); } else { $arg = $this->parseExpression(); } - if ($stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) { + if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) { $slice = true; } if ($slice) { - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) { + if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { $length = new ConstantExpression(null, $token->getLine()); } else { $length = $this->parseExpression(); } - $class = $this->getFilterNodeClass('slice', $token->getLine()); + $filter = $this->getFilter('slice', $token->getLine()); $arguments = new Node([$arg, $length]); - $filter = new $class($node, new ConstantExpression('slice', $token->getLine()), $arguments, $token->getLine()); + $filter = new ($filter->getNodeClass())($node, $filter, $arguments, $token->getLine()); - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']'); + $stream->expect(Token::PUNCTUATION_TYPE, ']'); return $filter; } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']'); + $stream->expect(Token::PUNCTUATION_TYPE, ']'); } return new GetAttrExpression($node, $arg, $arguments, $type, $lineno); @@ -545,23 +566,35 @@ class ExpressionParser return $this->parseFilterExpressionRaw($node); } - public function parseFilterExpressionRaw($node, $tag = null) + public function parseFilterExpressionRaw($node) { - while (true) { - $token = $this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5); + if (\func_num_args() > 1) { + trigger_deprecation('twig/twig', '3.12', 'Passing a second argument to "%s()" is deprecated.', __METHOD__); + } - $name = new ConstantExpression($token->getValue(), $token->getLine()); - if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { + while (true) { + $token = $this->parser->getStream()->expect(Token::NAME_TYPE); + + if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) { $arguments = new Node(); } else { $arguments = $this->parseArguments(true, false, true); } - $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine()); + $filter = $this->getFilter($token->getValue(), $token->getLine()); - $node = new $class($node, $name, $arguments, $token->getLine(), $tag); + $ready = true; + if (!isset($this->readyNodes[$class = $filter->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); + } - if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '|')) { + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); + } + + $node = new $class($node, $ready ? $filter : new ConstantExpression($filter->getName(), $token->getLine()), $arguments, $token->getLine()); + + if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '|')) { break; } @@ -575,7 +608,7 @@ class ExpressionParser * Parses arguments. * * @param bool $namedArguments Whether to allow named arguments or not - * @param bool $definition Whether we are parsing arguments for a function definition + * @param bool $definition Whether we are parsing arguments for a function (or macro) definition * * @return Node * @@ -586,28 +619,28 @@ class ExpressionParser $args = []; $stream = $this->parser->getStream(); - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '(', 'A list of arguments must begin with an opening parenthesis'); - while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) { + $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { if (!empty($args)) { - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'Arguments must be separated by a comma'); + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); // if the comma above was a trailing comma, early exit the argument parse loop - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) { + if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { break; } } if ($definition) { - $token = $stream->expect(/* Token::NAME_TYPE */ 5, null, 'An argument must be a name'); + $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name'); $value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine()); } else { $value = $this->parseExpression(0, $allowArrow); } $name = null; - if ($namedArguments && $token = $stream->nextIf(/* Token::OPERATOR_TYPE */ 8, '=')) { + if ($namedArguments && (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':')))) { if (!$value instanceof NameExpression) { - throw new SyntaxError(sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); } $name = $value->getAttribute('name'); @@ -615,7 +648,7 @@ class ExpressionParser $value = $this->parsePrimaryExpression(); if (!$this->checkConstantExpression($value)) { - throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, or an array).', $token->getLine(), $stream->getSourceContext()); + throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext()); } } else { $value = $this->parseExpression(0, $allowArrow); @@ -626,6 +659,7 @@ class ExpressionParser if (null === $name) { $name = $value->getAttribute('name'); $value = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine()); + $value->setAttribute('is_implicit', true); } $args[$name] = $value; } else { @@ -636,7 +670,7 @@ class ExpressionParser } } } - $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ')', 'A list of arguments must be closed by a parenthesis'); + $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); return new Node($args); } @@ -647,19 +681,19 @@ class ExpressionParser $targets = []; while (true) { $token = $this->parser->getCurrentToken(); - if ($stream->test(/* Token::OPERATOR_TYPE */ 8) && preg_match(Lexer::REGEX_NAME, $token->getValue())) { + if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) { // in this context, string operators are variable names $this->parser->getStream()->next(); } else { - $stream->expect(/* Token::NAME_TYPE */ 5, null, 'Only variables can be assigned to'); + $stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to'); } $value = $token->getValue(); if (\in_array(strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), ['true', 'false', 'none', 'null'])) { - throw new SyntaxError(sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext()); } $targets[] = new AssignNameExpression($value, $token->getLine()); - if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; } } @@ -672,7 +706,7 @@ class ExpressionParser $targets = []; while (true) { $targets[] = $this->parseExpression(); - if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; } } @@ -688,121 +722,115 @@ class ExpressionParser private function parseTestExpression(Node $node): TestExpression { $stream = $this->parser->getStream(); - list($name, $test) = $this->getTest($node->getTemplateLine()); + $test = $this->getTest($node->getTemplateLine()); - $class = $this->getTestNodeClass($test); $arguments = null; - if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) { + if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { $arguments = $this->parseArguments(true); } elseif ($test->hasOneMandatoryArgument()) { $arguments = new Node([0 => $this->parsePrimaryExpression()]); } - if ('defined' === $name && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) { + if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) { $node = new MethodCallExpression($alias['node'], $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine()); $node->setAttribute('safe', true); } - return new $class($node, $name, $arguments, $this->parser->getCurrentToken()->getLine()); - } - - private function getTest(int $line): array - { - $stream = $this->parser->getStream(); - $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); - - if ($test = $this->env->getTest($name)) { - return [$name, $test]; + $ready = $test instanceof TwigTest; + if (!isset($this->readyNodes[$class = $test->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); } - if ($stream->test(/* Token::NAME_TYPE */ 5)) { + 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(); + $name = $stream->expect(Token::NAME_TYPE)->getValue(); + + if ($stream->test(Token::NAME_TYPE)) { // try 2-words tests $name = $name.' '.$this->parser->getCurrentToken()->getValue(); if ($test = $this->env->getTest($name)) { $stream->next(); - - return [$name, $test]; } + } else { + $test = $this->env->getTest($name); } - $e = new SyntaxError(sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext()); - $e->addSuggestions($name, array_keys($this->env->getTests())); + if (!$test) { + $e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext()); + $e->addSuggestions($name, array_keys($this->env->getTests())); - throw $e; - } + throw $e; + } - private function getTestNodeClass(TwigTest $test): string - { if ($test->isDeprecated()) { $stream = $this->parser->getStream(); - $message = sprintf('Twig Test "%s" is deprecated', $test->getName()); + $message = \sprintf('Twig Test "%s" is deprecated', $test->getName()); - if ($test->getDeprecatedVersion()) { - $message .= sprintf(' since version %s', $test->getDeprecatedVersion()); - } if ($test->getAlternative()) { - $message .= sprintf('. Use "%s" instead', $test->getAlternative()); + $message .= \sprintf('. Use "%s" instead', $test->getAlternative()); } $src = $stream->getSourceContext(); - $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine()); + $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine()); - @trigger_error($message, \E_USER_DEPRECATED); + trigger_deprecation($test->getDeprecatingPackage(), $test->getDeprecatedVersion(), $message); } - return $test->getNodeClass(); + return $test; } - private function getFunctionNodeClass(string $name, int $line): string + private function getFunction(string $name, int $line): TwigFunction { if (!$function = $this->env->getFunction($name)) { - $e = new SyntaxError(sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext()); + $e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext()); $e->addSuggestions($name, array_keys($this->env->getFunctions())); throw $e; } if ($function->isDeprecated()) { - $message = sprintf('Twig Function "%s" is deprecated', $function->getName()); - if ($function->getDeprecatedVersion()) { - $message .= sprintf(' since version %s', $function->getDeprecatedVersion()); - } + $message = \sprintf('Twig Function "%s" is deprecated', $function->getName()); if ($function->getAlternative()) { - $message .= sprintf('. Use "%s" instead', $function->getAlternative()); + $message .= \sprintf('. Use "%s" instead', $function->getAlternative()); } $src = $this->parser->getStream()->getSourceContext(); - $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line); + $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line); - @trigger_error($message, \E_USER_DEPRECATED); + trigger_deprecation($function->getDeprecatingPackage(), $function->getDeprecatedVersion(), $message); } - return $function->getNodeClass(); + return $function; } - private function getFilterNodeClass(string $name, int $line): string + private function getFilter(string $name, int $line): TwigFilter { if (!$filter = $this->env->getFilter($name)) { - $e = new SyntaxError(sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext()); + $e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext()); $e->addSuggestions($name, array_keys($this->env->getFilters())); throw $e; } if ($filter->isDeprecated()) { - $message = sprintf('Twig Filter "%s" is deprecated', $filter->getName()); - if ($filter->getDeprecatedVersion()) { - $message .= sprintf(' since version %s', $filter->getDeprecatedVersion()); - } + $message = \sprintf('Twig Filter "%s" is deprecated', $filter->getName()); if ($filter->getAlternative()) { - $message .= sprintf('. Use "%s" instead', $filter->getAlternative()); + $message .= \sprintf('. Use "%s" instead', $filter->getAlternative()); } $src = $this->parser->getStream()->getSourceContext(); - $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line); + $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line); - @trigger_error($message, \E_USER_DEPRECATED); + trigger_deprecation($filter->getDeprecatingPackage(), $filter->getDeprecatedVersion(), $message); } - return $filter->getNodeClass(); + return $filter; } // checks that the node only contains "constant" elements diff --git a/data/web/inc/lib/vendor/twig/twig/src/Extension/AbstractExtension.php b/data/web/inc/lib/vendor/twig/twig/src/Extension/AbstractExtension.php index 422925f31..a1b083b68 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Extension/AbstractExtension.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Extension/AbstractExtension.php @@ -40,6 +40,6 @@ abstract class AbstractExtension implements ExtensionInterface public function getOperators() { - return []; + return [[], []]; } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Extension/CoreExtension.php b/data/web/inc/lib/vendor/twig/twig/src/Extension/CoreExtension.php index b77985859..3ed27a35c 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Extension/CoreExtension.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Extension/CoreExtension.php @@ -9,8 +9,15 @@ * file that was distributed with this source code. */ -namespace Twig\Extension { +namespace Twig\Extension; + +use Twig\Environment; +use Twig\Error\LoaderError; +use Twig\Error\RuntimeError; +use Twig\Error\SyntaxError; use Twig\ExpressionParser; +use Twig\Markup; +use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\Binary\AddBinary; use Twig\Node\Expression\Binary\AndBinary; use Twig\Node\Expression\Binary\BitwiseAndBinary; @@ -23,6 +30,8 @@ use Twig\Node\Expression\Binary\EqualBinary; use Twig\Node\Expression\Binary\FloorDivBinary; use Twig\Node\Expression\Binary\GreaterBinary; use Twig\Node\Expression\Binary\GreaterEqualBinary; +use Twig\Node\Expression\Binary\HasEveryBinary; +use Twig\Node\Expression\Binary\HasSomeBinary; use Twig\Node\Expression\Binary\InBinary; use Twig\Node\Expression\Binary\LessBinary; use Twig\Node\Expression\Binary\LessEqualBinary; @@ -37,8 +46,12 @@ use Twig\Node\Expression\Binary\RangeBinary; use Twig\Node\Expression\Binary\SpaceshipBinary; use Twig\Node\Expression\Binary\StartsWithBinary; use Twig\Node\Expression\Binary\SubBinary; +use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\Filter\DefaultFilter; +use Twig\Node\Expression\FunctionNode\EnumCasesFunction; +use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\NullCoalesceExpression; +use Twig\Node\Expression\ParentExpression; use Twig\Node\Expression\Test\ConstantTest; use Twig\Node\Expression\Test\DefinedTest; use Twig\Node\Expression\Test\DivisiblebyTest; @@ -49,7 +62,12 @@ use Twig\Node\Expression\Test\SameasTest; use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; +use Twig\Node\Node; use Twig\NodeVisitor\MacroAutoImportNodeVisitor; +use Twig\Parser; +use Twig\Source; +use Twig\Template; +use Twig\TemplateWrapper; use Twig\TokenParser\ApplyTokenParser; use Twig\TokenParser\BlockTokenParser; use Twig\TokenParser\DeprecatedTokenParser; @@ -64,11 +82,13 @@ use Twig\TokenParser\ImportTokenParser; use Twig\TokenParser\IncludeTokenParser; use Twig\TokenParser\MacroTokenParser; use Twig\TokenParser\SetTokenParser; +use Twig\TokenParser\TypesTokenParser; use Twig\TokenParser\UseTokenParser; use Twig\TokenParser\WithTokenParser; use Twig\TwigFilter; use Twig\TwigFunction; use Twig\TwigTest; +use Twig\Util\CallableArgumentsExtractor; final class CoreExtension extends AbstractExtension { @@ -79,8 +99,8 @@ final class CoreExtension extends AbstractExtension /** * Sets the default format to be used by the date filter. * - * @param string $format The default date format string - * @param string $dateIntervalFormat The default date interval format string + * @param string|null $format The default date format string + * @param string|null $dateIntervalFormat The default date interval format string */ public function setDateFormat($format = null, $dateIntervalFormat = null) { @@ -163,6 +183,7 @@ final class CoreExtension extends AbstractExtension new ImportTokenParser(), new FromTokenParser(), new SetTokenParser(), + new TypesTokenParser(), new FlushTokenParser(), new DoTokenParser(), new EmbedTokenParser(), @@ -175,65 +196,71 @@ final class CoreExtension extends AbstractExtension { return [ // formatting filters - new TwigFilter('date', 'twig_date_format_filter', ['needs_environment' => true]), - new TwigFilter('date_modify', 'twig_date_modify_filter', ['needs_environment' => true]), - new TwigFilter('format', 'twig_sprintf'), - new TwigFilter('replace', 'twig_replace_filter'), - new TwigFilter('number_format', 'twig_number_format_filter', ['needs_environment' => true]), + new TwigFilter('date', [$this, 'formatDate']), + new TwigFilter('date_modify', [$this, 'modifyDate']), + new TwigFilter('format', [self::class, 'sprintf']), + new TwigFilter('replace', [self::class, 'replace']), + new TwigFilter('number_format', [$this, 'formatNumber']), new TwigFilter('abs', 'abs'), - new TwigFilter('round', 'twig_round'), + new TwigFilter('round', [self::class, 'round']), // encoding - new TwigFilter('url_encode', 'twig_urlencode_filter'), + new TwigFilter('url_encode', [self::class, 'urlencode']), new TwigFilter('json_encode', 'json_encode'), - new TwigFilter('convert_encoding', 'twig_convert_encoding'), + new TwigFilter('convert_encoding', [self::class, 'convertEncoding']), // string filters - new TwigFilter('title', 'twig_title_string_filter', ['needs_environment' => true]), - new TwigFilter('capitalize', 'twig_capitalize_string_filter', ['needs_environment' => true]), - new TwigFilter('upper', 'twig_upper_filter', ['needs_environment' => true]), - new TwigFilter('lower', 'twig_lower_filter', ['needs_environment' => true]), - new TwigFilter('striptags', 'twig_striptags'), - new TwigFilter('trim', 'twig_trim_filter'), - new TwigFilter('nl2br', 'twig_nl2br', ['pre_escape' => 'html', 'is_safe' => ['html']]), - new TwigFilter('spaceless', 'twig_spaceless', ['is_safe' => ['html']]), + new TwigFilter('title', [self::class, 'titleCase'], ['needs_charset' => true]), + new TwigFilter('capitalize', [self::class, 'capitalize'], ['needs_charset' => true]), + new TwigFilter('upper', [self::class, 'upper'], ['needs_charset' => true]), + new TwigFilter('lower', [self::class, 'lower'], ['needs_charset' => true]), + new TwigFilter('striptags', [self::class, 'striptags']), + new TwigFilter('trim', [self::class, 'trim']), + new TwigFilter('nl2br', [self::class, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]), + new TwigFilter('spaceless', [self::class, 'spaceless'], ['is_safe' => ['html'], 'deprecated' => '3.12', 'deprecating_package' => 'twig/twig']), // array helpers - new TwigFilter('join', 'twig_join_filter'), - new TwigFilter('split', 'twig_split_filter', ['needs_environment' => true]), - new TwigFilter('sort', 'twig_sort_filter', ['needs_environment' => true]), - new TwigFilter('merge', 'twig_array_merge'), - new TwigFilter('batch', 'twig_array_batch'), - new TwigFilter('column', 'twig_array_column'), - new TwigFilter('filter', 'twig_array_filter', ['needs_environment' => true]), - new TwigFilter('map', 'twig_array_map', ['needs_environment' => true]), - new TwigFilter('reduce', 'twig_array_reduce', ['needs_environment' => true]), + new TwigFilter('join', [self::class, 'join']), + new TwigFilter('split', [self::class, 'split'], ['needs_charset' => true]), + new TwigFilter('sort', [self::class, 'sort'], ['needs_environment' => true]), + new TwigFilter('merge', [self::class, 'merge']), + new TwigFilter('batch', [self::class, 'batch']), + new TwigFilter('column', [self::class, 'column']), + new TwigFilter('filter', [self::class, 'filter'], ['needs_environment' => true]), + new TwigFilter('map', [self::class, 'map'], ['needs_environment' => true]), + new TwigFilter('reduce', [self::class, 'reduce'], ['needs_environment' => true]), + new TwigFilter('find', [self::class, 'find'], ['needs_environment' => true]), // string/array filters - new TwigFilter('reverse', 'twig_reverse_filter', ['needs_environment' => true]), - new TwigFilter('length', 'twig_length_filter', ['needs_environment' => true]), - new TwigFilter('slice', 'twig_slice', ['needs_environment' => true]), - new TwigFilter('first', 'twig_first', ['needs_environment' => true]), - new TwigFilter('last', 'twig_last', ['needs_environment' => true]), + new TwigFilter('reverse', [self::class, 'reverse'], ['needs_charset' => true]), + new TwigFilter('shuffle', [self::class, 'shuffle'], ['needs_charset' => true]), + new TwigFilter('length', [self::class, 'length'], ['needs_charset' => true]), + new TwigFilter('slice', [self::class, 'slice'], ['needs_charset' => true]), + new TwigFilter('first', [self::class, 'first'], ['needs_charset' => true]), + new TwigFilter('last', [self::class, 'last'], ['needs_charset' => true]), // iteration and runtime - new TwigFilter('default', '_twig_default_filter', ['node_class' => DefaultFilter::class]), - new TwigFilter('keys', 'twig_get_array_keys_filter'), + new TwigFilter('default', [self::class, 'default'], ['node_class' => DefaultFilter::class]), + new TwigFilter('keys', [self::class, 'keys']), ]; } public function getFunctions(): array { return [ + new TwigFunction('parent', null, ['parser_callable' => [self::class, 'parseParentFunction']]), + new TwigFunction('block', null, ['parser_callable' => [self::class, 'parseBlockFunction']]), + new TwigFunction('attribute', null, ['parser_callable' => [self::class, 'parseAttributeFunction']]), new TwigFunction('max', 'max'), new TwigFunction('min', 'min'), new TwigFunction('range', 'range'), - new TwigFunction('constant', 'twig_constant'), - new TwigFunction('cycle', 'twig_cycle'), - new TwigFunction('random', 'twig_random', ['needs_environment' => true]), - new TwigFunction('date', 'twig_date_converter', ['needs_environment' => true]), - new TwigFunction('include', 'twig_include', ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), - new TwigFunction('source', 'twig_source', ['needs_environment' => true, 'is_safe' => ['all']]), + new TwigFunction('constant', [self::class, 'constant']), + new TwigFunction('cycle', [self::class, 'cycle']), + new TwigFunction('random', [self::class, 'random'], ['needs_charset' => true]), + new TwigFunction('date', [$this, 'convertDate']), + new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), + new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]), + new TwigFunction('enum_cases', [self::class, 'enumCases'], ['node_class' => EnumCasesFunction::class]), ]; } @@ -248,8 +275,10 @@ final class CoreExtension extends AbstractExtension new TwigTest('null', null, ['node_class' => NullTest::class]), new TwigTest('divisible by', null, ['node_class' => DivisiblebyTest::class, 'one_mandatory_argument' => true]), new TwigTest('constant', null, ['node_class' => ConstantTest::class]), - new TwigTest('empty', 'twig_test_empty'), - new TwigTest('iterable', 'twig_test_iterable'), + new TwigTest('empty', [self::class, 'testEmpty']), + new TwigTest('iterable', 'is_iterable'), + new TwigTest('sequence', [self::class, 'testSequence']), + new TwigTest('mapping', [self::class, 'testMapping']), ]; } @@ -284,6 +313,8 @@ final class CoreExtension extends AbstractExtension 'matches' => ['precedence' => 20, 'class' => MatchesBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'starts with' => ['precedence' => 20, 'class' => StartsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'ends with' => ['precedence' => 20, 'class' => EndsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], + 'has some' => ['precedence' => 20, 'class' => HasSomeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], + 'has every' => ['precedence' => 20, 'class' => HasEveryBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '..' => ['precedence' => 25, 'class' => RangeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '+' => ['precedence' => 30, 'class' => AddBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '-' => ['precedence' => 30, 'class' => SubBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], @@ -299,193 +330,226 @@ final class CoreExtension extends AbstractExtension ], ]; } -} -} -namespace { - use Twig\Environment; - use Twig\Error\LoaderError; - use Twig\Error\RuntimeError; - use Twig\Extension\CoreExtension; - use Twig\Extension\SandboxExtension; - use Twig\Markup; - use Twig\Source; - use Twig\Template; - use Twig\TemplateWrapper; + /** + * Cycles over a sequence. + * + * @param array|\ArrayAccess $values A non-empty sequence of values + * @param positive-int $position The position of the value to return in the cycle + * + * @return mixed The value at the given position in the sequence, wrapping around as needed + * + * @internal + */ + public static function cycle($values, $position): mixed + { + if (!\is_array($values)) { + if (!$values instanceof \ArrayAccess) { + throw new RuntimeError('The "cycle" function expects an array or "ArrayAccess" as first argument.'); + } -/** - * Cycles over a value. - * - * @param \ArrayAccess|array $values - * @param int $position The cycle position - * - * @return string The next value in the cycle - */ -function twig_cycle($values, $position) -{ - if (!\is_array($values) && !$values instanceof \ArrayAccess) { - return $values; + if (!is_countable($values)) { + // To be uncommented in 4.0 + // throw new RuntimeError('The "cycle" function expects a countable sequence as first argument.'); + + trigger_deprecation('twig/twig', '3.12', 'Passing a non-countable sequence of values to "%s()" is deprecated.', __METHOD__); + + return $values; + } + + $values = self::toArray($values, false); + } + + if (!$count = \count($values)) { + throw new RuntimeError('The "cycle" function does not work on empty sequences.'); + } + + return $values[$position % $count]; } - return $values[$position % \count($values)]; -} + /** + * Returns a random value depending on the supplied parameter type: + * - a random item from a \Traversable or array + * - a random character from a string + * - a random integer between 0 and the integer parameter. + * + * @param \Traversable|array|int|float|string $values The values to pick a random item from + * @param int|null $max Maximum value used when $values is an int + * + * @return mixed A random value from the given sequence + * + * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is) + * + * @internal + */ + public static function random(string $charset, $values = null, $max = null) + { + if (null === $values) { + return null === $max ? mt_rand() : mt_rand(0, (int) $max); + } -/** - * Returns a random value depending on the supplied parameter type: - * - a random item from a \Traversable or array - * - a random character from a string - * - a random integer between 0 and the integer parameter. - * - * @param \Traversable|array|int|float|string $values The values to pick a random item from - * @param int|null $max Maximum value used when $values is an int - * - * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is) - * - * @return mixed A random value from the given sequence - */ -function twig_random(Environment $env, $values = null, $max = null) -{ - if (null === $values) { - return null === $max ? mt_rand() : mt_rand(0, (int) $max); - } - - if (\is_int($values) || \is_float($values)) { - if (null === $max) { - if ($values < 0) { - $max = 0; - $min = $values; + if (\is_int($values) || \is_float($values)) { + if (null === $max) { + if ($values < 0) { + $max = 0; + $min = $values; + } else { + $max = $values; + $min = 0; + } } else { - $max = $values; - $min = 0; + $min = $values; } + + return mt_rand((int) $min, (int) $max); + } + + if (\is_string($values)) { + if ('' === $values) { + return ''; + } + + if ('UTF-8' !== $charset) { + $values = self::convertEncoding($values, 'UTF-8', $charset); + } + + // unicode version of str_split() + // split at all positions, but not after the start and not before the end + $values = preg_split('/(? $value) { + $values[$i] = self::convertEncoding($value, $charset, 'UTF-8'); + } + } + } + + if (!is_iterable($values)) { + return $values; + } + + $values = self::toArray($values); + + if (0 === \count($values)) { + throw new RuntimeError('The random function cannot pick from an empty sequence/mapping.'); + } + + return $values[array_rand($values, 1)]; + } + + /** + * Formats a date. + * + * {{ post.published_at|date("m/d/Y") }} + * + * @param \DateTimeInterface|\DateInterval|string $date A date + * @param string|null $format The target format, null to use the default + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + */ + public function formatDate($date, $format = null, $timezone = null): string + { + if (null === $format) { + $formats = $this->getDateFormat(); + $format = $date instanceof \DateInterval ? $formats[1] : $formats[0]; + } + + if ($date instanceof \DateInterval) { + return $date->format($format); + } + + return $this->convertDate($date, $timezone)->format($format); + } + + /** + * Returns a new date object modified. + * + * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} + * + * @param \DateTimeInterface|string $date A date + * @param string $modifier A modifier string + * + * @return \DateTime|\DateTimeImmutable + * + * @internal + */ + public function modifyDate($date, $modifier) + { + return $this->convertDate($date, false)->modify($modifier); + } + + /** + * Returns a formatted string. + * + * @param string|null $format + * @param ...$values + * + * @internal + */ + public static function sprintf($format, ...$values): string + { + return \sprintf($format ?? '', ...$values); + } + + /** + * @internal + */ + public static function dateConverter(Environment $env, $date, $format = null, $timezone = null): string + { + return $env->getExtension(self::class)->formatDate($date, $format, $timezone); + } + + /** + * Converts an input to a \DateTime instance. + * + * {% if date(user.created_at) < date('+2days') %} + * {# do something #} + * {% endif %} + * + * @param \DateTimeInterface|string|null $date A date or null to use the current time + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * + * @return \DateTime|\DateTimeImmutable + */ + public function convertDate($date = null, $timezone = null) + { + // determine the timezone + if (false !== $timezone) { + if (null === $timezone) { + $timezone = $this->getTimezone(); + } elseif (!$timezone instanceof \DateTimeZone) { + $timezone = new \DateTimeZone($timezone); + } + } + + // immutable dates + if ($date instanceof \DateTimeImmutable) { + return false !== $timezone ? $date->setTimezone($timezone) : $date; + } + + if ($date instanceof \DateTime) { + $date = clone $date; + if (false !== $timezone) { + $date->setTimezone($timezone); + } + + return $date; + } + + if (null === $date || 'now' === $date) { + if (null === $date) { + $date = 'now'; + } + + return new \DateTime($date, false !== $timezone ? $timezone : $this->getTimezone()); + } + + $asString = (string) $date; + if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { + $date = new \DateTime('@'.$date); } else { - $min = $values; - $max = $max; + $date = new \DateTime($date, $this->getTimezone()); } - return mt_rand((int) $min, (int) $max); - } - - if (\is_string($values)) { - if ('' === $values) { - return ''; - } - - $charset = $env->getCharset(); - - if ('UTF-8' !== $charset) { - $values = twig_convert_encoding($values, 'UTF-8', $charset); - } - - // unicode version of str_split() - // split at all positions, but not after the start and not before the end - $values = preg_split('/(? $value) { - $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8'); - } - } - } - - if (!twig_test_iterable($values)) { - return $values; - } - - $values = twig_to_array($values); - - if (0 === \count($values)) { - throw new RuntimeError('The random function cannot pick from an empty array.'); - } - - return $values[array_rand($values, 1)]; -} - -/** - * Converts a date to the given format. - * - * {{ post.published_at|date("m/d/Y") }} - * - * @param \DateTimeInterface|\DateInterval|string $date A date - * @param string|null $format The target format, null to use the default - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged - * - * @return string The formatted date - */ -function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null) -{ - if (null === $format) { - $formats = $env->getExtension(CoreExtension::class)->getDateFormat(); - $format = $date instanceof \DateInterval ? $formats[1] : $formats[0]; - } - - if ($date instanceof \DateInterval) { - return $date->format($format); - } - - return twig_date_converter($env, $date, $timezone)->format($format); -} - -/** - * Returns a new date object modified. - * - * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} - * - * @param \DateTimeInterface|string $date A date - * @param string $modifier A modifier string - * - * @return \DateTimeInterface - */ -function twig_date_modify_filter(Environment $env, $date, $modifier) -{ - $date = twig_date_converter($env, $date, false); - - return $date->modify($modifier); -} - -/** - * Returns a formatted string. - * - * @param string|null $format - * @param ...$values - * - * @return string - */ -function twig_sprintf($format, ...$values) -{ - return sprintf($format ?? '', ...$values); -} - -/** - * Converts an input to a \DateTime instance. - * - * {% if date(user.created_at) < date('+2days') %} - * {# do something #} - * {% endif %} - * - * @param \DateTimeInterface|string|null $date A date or null to use the current time - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged - * - * @return \DateTimeInterface - */ -function twig_date_converter(Environment $env, $date = null, $timezone = null) -{ - // determine the timezone - if (false !== $timezone) { - if (null === $timezone) { - $timezone = $env->getExtension(CoreExtension::class)->getTimezone(); - } elseif (!$timezone instanceof \DateTimeZone) { - $timezone = new \DateTimeZone($timezone); - } - } - - // immutable dates - if ($date instanceof \DateTimeImmutable) { - return false !== $timezone ? $date->setTimezone($timezone) : $date; - } - - if ($date instanceof \DateTimeInterface) { - $date = clone $date; if (false !== $timezone) { $date->setTimezone($timezone); } @@ -493,968 +557,1086 @@ function twig_date_converter(Environment $env, $date = null, $timezone = null) return $date; } - if (null === $date || 'now' === $date) { - if (null === $date) { - $date = 'now'; + /** + * Replaces strings within a string. + * + * @param string|null $str String to replace in + * @param array|\Traversable $from Replace values + * + * @internal + */ + public static function replace($str, $from): string + { + if (!is_iterable($from)) { + throw new RuntimeError(\sprintf('The "replace" filter expects a sequence/mapping or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); } - return new \DateTime($date, false !== $timezone ? $timezone : $env->getExtension(CoreExtension::class)->getTimezone()); + return strtr($str ?? '', self::toArray($from)); } - $asString = (string) $date; - if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { - $date = new \DateTime('@'.$date); - } else { - $date = new \DateTime($date, $env->getExtension(CoreExtension::class)->getTimezone()); - } + /** + * Rounds a number. + * + * @param int|float|string|null $value The value to round + * @param int|float $precision The rounding precision + * @param string $method The method to use for rounding + * + * @return int|float The rounded number + * + * @internal + */ + public static function round($value, $precision = 0, $method = 'common') + { + $value = (float) $value; - if (false !== $timezone) { - $date->setTimezone($timezone); - } - - return $date; -} - -/** - * Replaces strings within a string. - * - * @param string|null $str String to replace in - * @param array|\Traversable $from Replace values - * - * @return string - */ -function twig_replace_filter($str, $from) -{ - if (!twig_test_iterable($from)) { - throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from))); - } - - return strtr($str ?? '', twig_to_array($from)); -} - -/** - * Rounds a number. - * - * @param int|float|string|null $value The value to round - * @param int|float $precision The rounding precision - * @param string $method The method to use for rounding - * - * @return int|float The rounded number - */ -function twig_round($value, $precision = 0, $method = 'common') -{ - $value = (float) $value; - - if ('common' === $method) { - return round($value, $precision); - } - - if ('ceil' !== $method && 'floor' !== $method) { - throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.'); - } - - return $method($value * 10 ** $precision) / 10 ** $precision; -} - -/** - * Number format filter. - * - * All of the formatting options can be left null, in that case the defaults will - * be used. Supplying any of the parameters will override the defaults set in the - * environment object. - * - * @param mixed $number A float/int/string of the number to format - * @param int $decimal the number of decimal points to display - * @param string $decimalPoint the character(s) to use for the decimal point - * @param string $thousandSep the character(s) to use for the thousands separator - * - * @return string The formatted number - */ -function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) -{ - $defaults = $env->getExtension(CoreExtension::class)->getNumberFormat(); - if (null === $decimal) { - $decimal = $defaults[0]; - } - - if (null === $decimalPoint) { - $decimalPoint = $defaults[1]; - } - - if (null === $thousandSep) { - $thousandSep = $defaults[2]; - } - - return number_format((float) $number, $decimal, $decimalPoint, $thousandSep); -} - -/** - * URL encodes (RFC 3986) a string as a path segment or an array as a query string. - * - * @param string|array|null $url A URL or an array of query parameters - * - * @return string The URL encoded value - */ -function twig_urlencode_filter($url) -{ - if (\is_array($url)) { - return http_build_query($url, '', '&', \PHP_QUERY_RFC3986); - } - - return rawurlencode($url ?? ''); -} - -/** - * Merges an array with another one. - * - * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} - * - * {% set items = items|merge({ 'peugeot': 'car' }) %} - * - * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car' } #} - * - * @param array|\Traversable $arr1 An array - * @param array|\Traversable $arr2 An array - * - * @return array The merged array - */ -function twig_array_merge($arr1, $arr2) -{ - if (!twig_test_iterable($arr1)) { - throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($arr1))); - } - - if (!twig_test_iterable($arr2)) { - throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as second argument.', \gettype($arr2))); - } - - return array_merge(twig_to_array($arr1), twig_to_array($arr2)); -} - -/** - * Slices a variable. - * - * @param mixed $item A variable - * @param int $start Start of the slice - * @param int $length Size of the slice - * @param bool $preserveKeys Whether to preserve key or not (when the input is an array) - * - * @return mixed The sliced variable - */ -function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) -{ - if ($item instanceof \Traversable) { - while ($item instanceof \IteratorAggregate) { - $item = $item->getIterator(); + if ('common' === $method) { + return round($value, $precision); } - if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) { - try { - return iterator_to_array(new \LimitIterator($item, $start, null === $length ? -1 : $length), $preserveKeys); - } catch (\OutOfBoundsException $e) { - return []; + if ('ceil' !== $method && 'floor' !== $method) { + throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.'); + } + + return $method($value * 10 ** $precision) / 10 ** $precision; + } + + /** + * Formats a number. + * + * All of the formatting options can be left null, in that case the defaults will + * be used. Supplying any of the parameters will override the defaults set in the + * environment object. + * + * @param mixed $number A float/int/string of the number to format + * @param int|null $decimal the number of decimal points to display + * @param string|null $decimalPoint the character(s) to use for the decimal point + * @param string|null $thousandSep the character(s) to use for the thousands separator + */ + public function formatNumber($number, $decimal = null, $decimalPoint = null, $thousandSep = null): string + { + $defaults = $this->getNumberFormat(); + if (null === $decimal) { + $decimal = $defaults[0]; + } + + if (null === $decimalPoint) { + $decimalPoint = $defaults[1]; + } + + if (null === $thousandSep) { + $thousandSep = $defaults[2]; + } + + return number_format((float) $number, $decimal, $decimalPoint, $thousandSep); + } + + /** + * URL encodes (RFC 3986) a string as a path segment or an array as a query string. + * + * @param string|array|null $url A URL or an array of query parameters + * + * @internal + */ + public static function urlencode($url): string + { + if (\is_array($url)) { + return http_build_query($url, '', '&', \PHP_QUERY_RFC3986); + } + + return rawurlencode($url ?? ''); + } + + /** + * Merges any number of arrays or Traversable objects. + * + * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} + * + * {% set items = items|merge({ 'peugeot': 'car' }, { 'banana': 'fruit' }) %} + * + * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'banana': 'fruit' } #} + * + * @param array|\Traversable ...$arrays Any number of arrays or Traversable objects to merge + * + * @internal + */ + public static function merge(...$arrays): array + { + $result = []; + + foreach ($arrays as $argNumber => $array) { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The merge filter only works with sequences/mappings or "Traversable", got "%s" for argument %d.', \gettype($array), $argNumber + 1)); } + + $result = array_merge($result, self::toArray($array)); } - $item = iterator_to_array($item, $preserveKeys); + return $result; } - if (\is_array($item)) { - return \array_slice($item, $start, $length, $preserveKeys); - } + /** + * Slices a variable. + * + * @param mixed $item A variable + * @param int $start Start of the slice + * @param int $length Size of the slice + * @param bool $preserveKeys Whether to preserve key or not (when the input is an array) + * + * @return mixed The sliced variable + * + * @internal + */ + public static function slice(string $charset, $item, $start, $length = null, $preserveKeys = false) + { + if ($item instanceof \Traversable) { + while ($item instanceof \IteratorAggregate) { + $item = $item->getIterator(); + } - return (string) mb_substr((string) $item, $start, $length, $env->getCharset()); -} + if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) { + try { + return iterator_to_array(new \LimitIterator($item, $start, $length ?? -1), $preserveKeys); + } catch (\OutOfBoundsException $e) { + return []; + } + } -/** - * Returns the first element of the item. - * - * @param mixed $item A variable - * - * @return mixed The first element of the item - */ -function twig_first(Environment $env, $item) -{ - $elements = twig_slice($env, $item, 0, 1, false); - - return \is_string($elements) ? $elements : current($elements); -} - -/** - * Returns the last element of the item. - * - * @param mixed $item A variable - * - * @return mixed The last element of the item - */ -function twig_last(Environment $env, $item) -{ - $elements = twig_slice($env, $item, -1, 1, false); - - return \is_string($elements) ? $elements : current($elements); -} - -/** - * Joins the values to a string. - * - * The separators between elements are empty strings per default, you can define them with the optional parameters. - * - * {{ [1, 2, 3]|join(', ', ' and ') }} - * {# returns 1, 2 and 3 #} - * - * {{ [1, 2, 3]|join('|') }} - * {# returns 1|2|3 #} - * - * {{ [1, 2, 3]|join }} - * {# returns 123 #} - * - * @param array $value An array - * @param string $glue The separator - * @param string|null $and The separator for the last pair - * - * @return string The concatenated string - */ -function twig_join_filter($value, $glue = '', $and = null) -{ - if (!twig_test_iterable($value)) { - $value = (array) $value; - } - - $value = twig_to_array($value, false); - - if (0 === \count($value)) { - return ''; - } - - if (null === $and || $and === $glue) { - return implode($glue, $value); - } - - if (1 === \count($value)) { - return $value[0]; - } - - return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1]; -} - -/** - * Splits the string into an array. - * - * {{ "one,two,three"|split(',') }} - * {# returns [one, two, three] #} - * - * {{ "one,two,three,four,five"|split(',', 3) }} - * {# returns [one, two, "three,four,five"] #} - * - * {{ "123"|split('') }} - * {# returns [1, 2, 3] #} - * - * {{ "aabbcc"|split('', 2) }} - * {# returns [aa, bb, cc] #} - * - * @param string|null $value A string - * @param string $delimiter The delimiter - * @param int $limit The limit - * - * @return array The split string as an array - */ -function twig_split_filter(Environment $env, $value, $delimiter, $limit = null) -{ - $value = $value ?? ''; - - if (\strlen($delimiter) > 0) { - return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); - } - - if ($limit <= 1) { - return preg_split('/(?getCharset()); - if ($length < $limit) { - return [$value]; - } - - $r = []; - for ($i = 0; $i < $length; $i += $limit) { - $r[] = mb_substr($value, $i, $limit, $env->getCharset()); - } - - return $r; -} - -// The '_default' filter is used internally to avoid using the ternary operator -// which costs a lot for big contexts (before PHP 5.4). So, on average, -// a function call is cheaper. -/** - * @internal - */ -function _twig_default_filter($value, $default = '') -{ - if (twig_test_empty($value)) { - return $default; - } - - return $value; -} - -/** - * Returns the keys for the given array. - * - * It is useful when you want to iterate over the keys of an array: - * - * {% for key in array|keys %} - * {# ... #} - * {% endfor %} - * - * @param array $array An array - * - * @return array The keys - */ -function twig_get_array_keys_filter($array) -{ - if ($array instanceof \Traversable) { - while ($array instanceof \IteratorAggregate) { - $array = $array->getIterator(); + $item = iterator_to_array($item, $preserveKeys); } - $keys = []; - if ($array instanceof \Iterator) { - $array->rewind(); - while ($array->valid()) { - $keys[] = $array->key(); - $array->next(); + if (\is_array($item)) { + return \array_slice($item, $start, $length, $preserveKeys); + } + + return mb_substr((string) $item, $start, $length, $charset); + } + + /** + * Returns the first element of the item. + * + * @param mixed $item A variable + * + * @return mixed The first element of the item + * + * @internal + */ + public static function first(string $charset, $item) + { + $elements = self::slice($charset, $item, 0, 1, false); + + return \is_string($elements) ? $elements : current($elements); + } + + /** + * Returns the last element of the item. + * + * @param mixed $item A variable + * + * @return mixed The last element of the item + * + * @internal + */ + public static function last(string $charset, $item) + { + $elements = self::slice($charset, $item, -1, 1, false); + + return \is_string($elements) ? $elements : current($elements); + } + + /** + * Joins the values to a string. + * + * The separators between elements are empty strings per default, you can define them with the optional parameters. + * + * {{ [1, 2, 3]|join(', ', ' and ') }} + * {# returns 1, 2 and 3 #} + * + * {{ [1, 2, 3]|join('|') }} + * {# returns 1|2|3 #} + * + * {{ [1, 2, 3]|join }} + * {# returns 123 #} + * + * @param array $value An array + * @param string $glue The separator + * @param string|null $and The separator for the last pair + * + * @internal + */ + public static function join($value, $glue = '', $and = null): string + { + if (!is_iterable($value)) { + $value = (array) $value; + } + + $value = self::toArray($value, false); + + if (0 === \count($value)) { + return ''; + } + + if (null === $and || $and === $glue) { + return implode($glue, $value); + } + + if (1 === \count($value)) { + return $value[0]; + } + + return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1]; + } + + /** + * Splits the string into an array. + * + * {{ "one,two,three"|split(',') }} + * {# returns [one, two, three] #} + * + * {{ "one,two,three,four,five"|split(',', 3) }} + * {# returns [one, two, "three,four,five"] #} + * + * {{ "123"|split('') }} + * {# returns [1, 2, 3] #} + * + * {{ "aabbcc"|split('', 2) }} + * {# returns [aa, bb, cc] #} + * + * @param string|null $value A string + * @param string $delimiter The delimiter + * @param int|null $limit The limit + * + * @internal + */ + public static function split(string $charset, $value, $delimiter, $limit = null): array + { + $value = $value ?? ''; + + if ('' !== $delimiter) { + return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); + } + + if ($limit <= 1) { + return preg_split('/(?getIterator(); + } + + $keys = []; + if ($array instanceof \Iterator) { + $array->rewind(); + while ($array->valid()) { + $keys[] = $array->key(); + $array->next(); + } + + return $keys; + } + + foreach ($array as $key => $item) { + $keys[] = $key; } return $keys; } - foreach ($array as $key => $item) { - $keys[] = $key; + if (!\is_array($array)) { + return []; } - return $keys; + return array_keys($array); } - if (!\is_array($array)) { - return []; - } - - return array_keys($array); -} - -/** - * Reverses a variable. - * - * @param array|\Traversable|string|null $item An array, a \Traversable instance, or a string - * @param bool $preserveKeys Whether to preserve key or not - * - * @return mixed The reversed input - */ -function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) -{ - if ($item instanceof \Traversable) { - return array_reverse(iterator_to_array($item), $preserveKeys); - } - - if (\is_array($item)) { - return array_reverse($item, $preserveKeys); - } - - $string = (string) $item; - - $charset = $env->getCharset(); - - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, 'UTF-8', $charset); - } - - preg_match_all('/./us', $string, $matches); - - $string = implode('', array_reverse($matches[0])); - - if ('UTF-8' !== $charset) { - $string = twig_convert_encoding($string, $charset, 'UTF-8'); - } - - return $string; -} - -/** - * Sorts an array. - * - * @param array|\Traversable $array - * - * @return array - */ -function twig_sort_filter(Environment $env, $array, $arrow = null) -{ - if ($array instanceof \Traversable) { - $array = iterator_to_array($array); - } elseif (!\is_array($array)) { - throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array))); - } - - if (null !== $arrow) { - twig_check_arrow_in_sandbox($env, $arrow, 'sort', 'filter'); - - uasort($array, $arrow); - } else { - asort($array); - } - - return $array; -} - -/** - * @internal - */ -function twig_in_filter($value, $compare) -{ - if ($value instanceof Markup) { - $value = (string) $value; - } - if ($compare instanceof Markup) { - $compare = (string) $compare; - } - - if (\is_string($compare)) { - if (\is_string($value) || \is_int($value) || \is_float($value)) { - return '' === $value || false !== strpos($compare, (string) $value); + /** + * Reverses a variable. + * + * @param array|\Traversable|string|null $item An array, a \Traversable instance, or a string + * @param bool $preserveKeys Whether to preserve key or not + * + * @return mixed The reversed input + * + * @internal + */ + public static function reverse(string $charset, $item, $preserveKeys = false) + { + if ($item instanceof \Traversable) { + return array_reverse(iterator_to_array($item), $preserveKeys); } - return false; + if (\is_array($item)) { + return array_reverse($item, $preserveKeys); + } + + $string = (string) $item; + + if ('UTF-8' !== $charset) { + $string = self::convertEncoding($string, 'UTF-8', $charset); + } + + preg_match_all('/./us', $string, $matches); + + $string = implode('', array_reverse($matches[0])); + + if ('UTF-8' !== $charset) { + $string = self::convertEncoding($string, $charset, 'UTF-8'); + } + + return $string; } - if (!is_iterable($compare)) { - return false; + /** + * Shuffles an array, a \Traversable instance, or a string. + * The function does not preserve keys. + * + * @param array|\Traversable|string|null $item + * + * @return mixed + * + * @internal + */ + public static function shuffle(string $charset, $item) + { + if (\is_string($item)) { + if ('UTF-8' !== $charset) { + $item = self::convertEncoding($item, 'UTF-8', $charset); + } + + $item = preg_split('/(? string + if (\is_int($a) && \is_string($b)) { + $bTrim = trim($b, " \t\n\r\v\f"); + if (!is_numeric($bTrim)) { + return (string) $a <=> $b; + } + if ((int) $bTrim == $bTrim) { + return $a <=> (int) $bTrim; + } else { + return (float) $a <=> (float) $bTrim; + } + } + if (\is_string($a) && \is_int($b)) { + $aTrim = trim($a, " \t\n\r\v\f"); + if (!is_numeric($aTrim)) { + return $a <=> (string) $b; + } + if ((int) $aTrim == $aTrim) { + return (int) $aTrim <=> $b; + } else { + return (float) $aTrim <=> (float) $b; + } + } + + // float <=> string + if (\is_float($a) && \is_string($b)) { + if (is_nan($a)) { + return 1; + } + $bTrim = trim($b, " \t\n\r\v\f"); + if (!is_numeric($bTrim)) { + return (string) $a <=> $b; + } + + return $a <=> (float) $bTrim; + } + if (\is_string($a) && \is_float($b)) { + if (is_nan($b)) { + return 1; + } + $aTrim = trim($a, " \t\n\r\v\f"); + if (!is_numeric($aTrim)) { + return $a <=> (string) $b; + } + + return (float) $aTrim <=> $b; + } + + // fallback to <=> + return $a <=> $b; + } + + /** + * @throws RuntimeError When an invalid pattern is used + * + * @internal + */ + public static function matches(string $regexp, ?string $str): int + { + set_error_handler(function ($t, $m) use ($regexp) { + throw new RuntimeError(\sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); + }); + try { + return preg_match($regexp, $str ?? ''); + } finally { + restore_error_handler(); } } - return false; -} + /** + * Returns a trimmed string. + * + * @param string|null $string + * @param string|null $characterMask + * @param string $side + * + * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both') + * + * @internal + */ + public static function trim($string, $characterMask = null, $side = 'both'): string + { + if (null === $characterMask) { + $characterMask = " \t\n\r\0\x0B"; + } -/** - * Compares two values using a more strict version of the PHP non-strict comparison operator. - * - * @see https://wiki.php.net/rfc/string_to_number_comparison - * @see https://wiki.php.net/rfc/trailing_whitespace_numerics - * - * @internal - */ -function twig_compare($a, $b) -{ - // int <=> string - if (\is_int($a) && \is_string($b)) { - $bTrim = trim($b, " \t\n\r\v\f"); - if (!is_numeric($bTrim)) { - return (string) $a <=> $b; - } - if ((int) $bTrim == $bTrim) { - return $a <=> (int) $bTrim; - } else { - return (float) $a <=> (float) $bTrim; - } - } - if (\is_string($a) && \is_int($b)) { - $aTrim = trim($a, " \t\n\r\v\f"); - if (!is_numeric($aTrim)) { - return $a <=> (string) $b; - } - if ((int) $aTrim == $aTrim) { - return (int) $aTrim <=> $b; - } else { - return (float) $aTrim <=> (float) $b; + switch ($side) { + case 'both': + return trim($string ?? '', $characterMask); + case 'left': + return ltrim($string ?? '', $characterMask); + case 'right': + return rtrim($string ?? '', $characterMask); + default: + throw new RuntimeError('Trimming side must be "left", "right" or "both".'); } } - // float <=> string - if (\is_float($a) && \is_string($b)) { - if (is_nan($a)) { - return 1; - } - $bTrim = trim($b, " \t\n\r\v\f"); - if (!is_numeric($bTrim)) { - return (string) $a <=> $b; + /** + * Inserts HTML line breaks before all newlines in a string. + * + * @param string|null $string + * + * @internal + */ + public static function nl2br($string): string + { + return nl2br($string ?? ''); + } + + /** + * Removes whitespaces between HTML tags. + * + * @param string|null $content + * + * @internal + */ + public static function spaceless($content): string + { + return trim(preg_replace('/>\s+<', $content ?? '')); + } + + /** + * @param string|null $string + * @param string $to + * @param string $from + * + * @internal + */ + public static function convertEncoding($string, $to, $from): string + { + if (!\function_exists('iconv')) { + throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); } - return $a <=> (float) $bTrim; + return iconv($from, $to, $string ?? ''); } - if (\is_string($a) && \is_float($b)) { - if (is_nan($b)) { - return 1; - } - $aTrim = trim($a, " \t\n\r\v\f"); - if (!is_numeric($aTrim)) { - return $a <=> (string) $b; + + /** + * Returns the length of a variable. + * + * @param mixed $thing A variable + * + * @internal + */ + public static function length(string $charset, $thing): int + { + if (null === $thing) { + return 0; } - return (float) $aTrim <=> $b; + if (\is_scalar($thing)) { + return mb_strlen($thing, $charset); + } + + if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) { + return \count($thing); + } + + if ($thing instanceof \Traversable) { + return iterator_count($thing); + } + + if ($thing instanceof \Stringable) { + return mb_strlen((string) $thing, $charset); + } + + return 1; } - // fallback to <=> - return $a <=> $b; -} - -/** - * Returns a trimmed string. - * - * @param string|null $string - * @param string|null $characterMask - * @param string $side - * - * @return string - * - * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both') - */ -function twig_trim_filter($string, $characterMask = null, $side = 'both') -{ - if (null === $characterMask) { - $characterMask = " \t\n\r\0\x0B"; + /** + * Converts a string to uppercase. + * + * @param string|null $string A string + * + * @internal + */ + public static function upper(string $charset, $string): string + { + return mb_strtoupper($string ?? '', $charset); } - switch ($side) { - case 'both': - return trim($string ?? '', $characterMask); - case 'left': - return ltrim($string ?? '', $characterMask); - case 'right': - return rtrim($string ?? '', $characterMask); - default: - throw new RuntimeError('Trimming side must be "left", "right" or "both".'); - } -} - -/** - * Inserts HTML line breaks before all newlines in a string. - * - * @param string|null $string - * - * @return string - */ -function twig_nl2br($string) -{ - return nl2br($string ?? ''); -} - -/** - * Removes whitespaces between HTML tags. - * - * @param string|null $string - * - * @return string - */ -function twig_spaceless($content) -{ - return trim(preg_replace('/>\s+<', $content ?? '')); -} - -/** - * @param string|null $string - * @param string $to - * @param string $from - * - * @return string - */ -function twig_convert_encoding($string, $to, $from) -{ - if (!\function_exists('iconv')) { - throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); + /** + * Converts a string to lowercase. + * + * @param string|null $string A string + * + * @internal + */ + public static function lower(string $charset, $string): string + { + return mb_strtolower($string ?? '', $charset); } - return iconv($from, $to, $string ?? ''); -} - -/** - * Returns the length of a variable. - * - * @param mixed $thing A variable - * - * @return int The length of the value - */ -function twig_length_filter(Environment $env, $thing) -{ - if (null === $thing) { - return 0; + /** + * Strips HTML and PHP tags from a string. + * + * @param string|null $string + * @param string[]|string|null $allowable_tags + * + * @internal + */ + public static function striptags($string, $allowable_tags = null): string + { + return strip_tags($string ?? '', $allowable_tags); } - if (is_scalar($thing)) { - return mb_strlen($thing, $env->getCharset()); - } - - if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) { - return \count($thing); - } - - if ($thing instanceof \Traversable) { - return iterator_count($thing); - } - - if (method_exists($thing, '__toString') && !$thing instanceof \Countable) { - return mb_strlen((string) $thing, $env->getCharset()); - } - - return 1; -} - -/** - * Converts a string to uppercase. - * - * @param string|null $string A string - * - * @return string The uppercased string - */ -function twig_upper_filter(Environment $env, $string) -{ - return mb_strtoupper($string ?? '', $env->getCharset()); -} - -/** - * Converts a string to lowercase. - * - * @param string|null $string A string - * - * @return string The lowercased string - */ -function twig_lower_filter(Environment $env, $string) -{ - return mb_strtolower($string ?? '', $env->getCharset()); -} - -/** - * Strips HTML and PHP tags from a string. - * - * @param string|null $string - * @param string[]|string|null $string - * - * @return string - */ -function twig_striptags($string, $allowable_tags = null) -{ - return strip_tags($string ?? '', $allowable_tags); -} - -/** - * Returns a titlecased string. - * - * @param string|null $string A string - * - * @return string The titlecased string - */ -function twig_title_string_filter(Environment $env, $string) -{ - if (null !== $charset = $env->getCharset()) { + /** + * Returns a titlecased string. + * + * @param string|null $string A string + * + * @internal + */ + public static function titleCase(string $charset, $string): string + { return mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset); } - return ucwords(strtolower($string ?? '')); -} + /** + * Returns a capitalized string. + * + * @param string|null $string A string + * + * @internal + */ + public static function capitalize(string $charset, $string): string + { + return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset); + } -/** - * Returns a capitalized string. - * - * @param string|null $string A string - * - * @return string The capitalized string - */ -function twig_capitalize_string_filter(Environment $env, $string) -{ - $charset = $env->getCharset(); + /** + * @internal + */ + public static function callMacro(Template $template, string $method, array $args, int $lineno, array $context, Source $source) + { + if (!method_exists($template, $method)) { + $parent = $template; + while ($parent = $parent->getParent($context)) { + if (method_exists($parent, $method)) { + return $parent->$method(...$args); + } + } - return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset); -} + throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source); + } -/** - * @internal - */ -function twig_call_macro(Template $template, string $method, array $args, int $lineno, array $context, Source $source) -{ - if (!method_exists($template, $method)) { - $parent = $template; - while ($parent = $parent->getParent($context)) { - if (method_exists($parent, $method)) { - return $parent->$method(...$args); + return $template->$method(...$args); + } + + /** + * @template TSequence + * + * @param TSequence $seq + * + * @return ($seq is iterable ? TSequence : array{}) + * + * @internal + */ + public static function ensureTraversable($seq) + { + if (is_iterable($seq)) { + return $seq; + } + + return []; + } + + /** + * @internal + */ + public static function toArray($seq, $preserveKeys = true) + { + if ($seq instanceof \Traversable) { + return iterator_to_array($seq, $preserveKeys); + } + + if (!\is_array($seq)) { + return $seq; + } + + return $preserveKeys ? $seq : array_values($seq); + } + + /** + * Checks if a variable is empty. + * + * {# evaluates to true if the foo variable is null, false, or the empty string #} + * {% if foo is empty %} + * {# ... #} + * {% endif %} + * + * @param mixed $value A variable + * + * @internal + */ + public static function testEmpty($value): bool + { + if ($value instanceof \Countable) { + return 0 === \count($value); + } + + if ($value instanceof \Traversable) { + return !iterator_count($value); + } + + if ($value instanceof \Stringable) { + return '' === (string) $value; + } + + return '' === $value || false === $value || null === $value || [] === $value; + } + + /** + * Checks if a variable is a sequence. + * + * {# evaluates to true if the foo variable is a sequence #} + * {% if foo is sequence %} + * {# ... #} + * {% endif %} + * + * @param mixed $value + * + * @internal + */ + public static function testSequence($value): bool + { + if ($value instanceof \ArrayObject) { + $value = $value->getArrayCopy(); + } + + if ($value instanceof \Traversable) { + $value = iterator_to_array($value); + } + + return \is_array($value) && array_is_list($value); + } + + /** + * Checks if a variable is a mapping. + * + * {# evaluates to true if the foo variable is a mapping #} + * {% if foo is mapping %} + * {# ... #} + * {% endif %} + * + * @param mixed $value + * + * @internal + */ + public static function testMapping($value): bool + { + if ($value instanceof \ArrayObject) { + $value = $value->getArrayCopy(); + } + + if ($value instanceof \Traversable) { + $value = iterator_to_array($value); + } + + return (\is_array($value) && !array_is_list($value)) || \is_object($value); + } + + /** + * Renders a template. + * + * @param array $context + * @param string|array|TemplateWrapper $template The template to render or an array of templates to try consecutively + * @param array $variables The variables to pass to the template + * @param bool $withContext + * @param bool $ignoreMissing Whether to ignore missing templates or not + * @param bool $sandboxed Whether to sandbox the template or not + * + * @internal + */ + public static function include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false): string + { + $alreadySandboxed = false; + $sandbox = null; + if ($withContext) { + $variables = array_merge($context, $variables); + } + + if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) { + $sandbox = $env->getExtension(SandboxExtension::class); + if (!$alreadySandboxed = $sandbox->isSandboxed()) { + $sandbox->enableSandbox(); } } - throw new RuntimeError(sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source); - } - - return $template->$method(...$args); -} - -/** - * @internal - */ -function twig_ensure_traversable($seq) -{ - if ($seq instanceof \Traversable || \is_array($seq)) { - return $seq; - } - - return []; -} - -/** - * @internal - */ -function twig_to_array($seq, $preserveKeys = true) -{ - if ($seq instanceof \Traversable) { - return iterator_to_array($seq, $preserveKeys); - } - - if (!\is_array($seq)) { - return $seq; - } - - return $preserveKeys ? $seq : array_values($seq); -} - -/** - * Checks if a variable is empty. - * - * {# evaluates to true if the foo variable is null, false, or the empty string #} - * {% if foo is empty %} - * {# ... #} - * {% endif %} - * - * @param mixed $value A variable - * - * @return bool true if the value is empty, false otherwise - */ -function twig_test_empty($value) -{ - if ($value instanceof \Countable) { - return 0 === \count($value); - } - - if ($value instanceof \Traversable) { - return !iterator_count($value); - } - - if (\is_object($value) && method_exists($value, '__toString')) { - return '' === (string) $value; - } - - return '' === $value || false === $value || null === $value || [] === $value; -} - -/** - * Checks if a variable is traversable. - * - * {# evaluates to true if the foo variable is an array or a traversable object #} - * {% if foo is iterable %} - * {# ... #} - * {% endif %} - * - * @param mixed $value A variable - * - * @return bool true if the value is traversable - */ -function twig_test_iterable($value) -{ - return $value instanceof \Traversable || \is_array($value); -} - -/** - * Renders a template. - * - * @param array $context - * @param string|array $template The template to render or an array of templates to try consecutively - * @param array $variables The variables to pass to the template - * @param bool $withContext - * @param bool $ignoreMissing Whether to ignore missing templates or not - * @param bool $sandboxed Whether to sandbox the template or not - * - * @return string The rendered template - */ -function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) -{ - $alreadySandboxed = false; - $sandbox = null; - if ($withContext) { - $variables = array_merge($context, $variables); - } - - if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) { - $sandbox = $env->getExtension(SandboxExtension::class); - if (!$alreadySandboxed = $sandbox->isSandboxed()) { - $sandbox->enableSandbox(); - } - - foreach ((\is_array($template) ? $template : [$template]) as $name) { - // if a Template instance is passed, it might have been instantiated outside of a sandbox, check security - if ($name instanceof TemplateWrapper || $name instanceof Template) { - $name->unwrap()->checkSecurity(); - } - } - } - - try { - $loaded = null; try { - $loaded = $env->resolveTemplate($template); + $loaded = null; + try { + $loaded = $env->resolveTemplate($template); + } catch (LoaderError $e) { + if (!$ignoreMissing) { + throw $e; + } + + return ''; + } + + if ($isSandboxed) { + $loaded->unwrap()->checkSecurity(); + } + + return $loaded->render($variables); + } finally { + if ($isSandboxed && !$alreadySandboxed) { + $sandbox->disableSandbox(); + } + } + } + + /** + * Returns a template content without rendering it. + * + * @param string $name The template name + * @param bool $ignoreMissing Whether to ignore missing templates or not + * + * @internal + */ + public static function source(Environment $env, $name, $ignoreMissing = false): string + { + $loader = $env->getLoader(); + try { + return $loader->getSourceContext($name)->getCode(); } catch (LoaderError $e) { if (!$ignoreMissing) { throw $e; } - } - return $loaded ? $loaded->render($variables) : ''; - } finally { - if ($isSandboxed && !$alreadySandboxed) { - $sandbox->disableSandbox(); - } - } -} - -/** - * Returns a template content without rendering it. - * - * @param string $name The template name - * @param bool $ignoreMissing Whether to ignore missing templates or not - * - * @return string The template source - */ -function twig_source(Environment $env, $name, $ignoreMissing = false) -{ - $loader = $env->getLoader(); - try { - return $loader->getSourceContext($name)->getCode(); - } catch (LoaderError $e) { - if (!$ignoreMissing) { - throw $e; - } - } -} - -/** - * Provides the ability to get constants from instances as well as class/global constants. - * - * @param string $constant The name of the constant - * @param object|null $object The object to get the constant from - * - * @return string - */ -function twig_constant($constant, $object = null) -{ - if (null !== $object) { - if ('class' === $constant) { - return \get_class($object); - } - - $constant = \get_class($object).'::'.$constant; - } - - return \constant($constant); -} - -/** - * Checks if a constant exists. - * - * @param string $constant The name of the constant - * @param object|null $object The object to get the constant from - * - * @return bool - */ -function twig_constant_is_defined($constant, $object = null) -{ - if (null !== $object) { - if ('class' === $constant) { - return true; - } - - $constant = \get_class($object).'::'.$constant; - } - - return \defined($constant); -} - -/** - * Batches item. - * - * @param array $items An array of items - * @param int $size The size of the batch - * @param mixed $fill A value used to fill missing items - * - * @return array - */ -function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) -{ - if (!twig_test_iterable($items)) { - throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); - } - - $size = ceil($size); - - $result = array_chunk(twig_to_array($items, $preserveKeys), $size, $preserveKeys); - - if (null !== $fill && $result) { - $last = \count($result) - 1; - if ($fillCount = $size - \count($result[$last])) { - for ($i = 0; $i < $fillCount; ++$i) { - $result[$last][] = $fill; - } + return ''; } } - return $result; -} + /** + * Returns the list of cases of the enum. + * + * @template T of \UnitEnum + * + * @param class-string $enum + * + * @return list + * + * @internal + */ + public static function enumCases(string $enum): array + { + if (!enum_exists($enum)) { + throw new RuntimeError(\sprintf('Enum "%s" does not exist.', $enum)); + } -/** - * Returns the attribute value for a given array/object. - * - * @param mixed $object The object or array from where to get the item - * @param mixed $item The item to get from the array or object - * @param array $arguments An array of arguments to pass if the item is an object method - * @param string $type The type of attribute (@see \Twig\Template constants) - * @param bool $isDefinedTest Whether this is only a defined check - * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not - * @param int $lineno The template line where the attribute was called - * - * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true - * - * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false - * - * @internal - */ -function twig_get_attribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) -{ - // array - if (/* Template::METHOD_CALL */ 'method' !== $type) { - $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; + return $enum::cases(); + } - if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) - || ($object instanceof ArrayAccess && isset($object[$arrayItem])) - ) { - if ($isDefinedTest) { - return true; + /** + * Provides the ability to get constants from instances as well as class/global constants. + * + * @param string $constant The name of the constant + * @param object|null $object The object to get the constant from + * @param bool $checkDefined Whether to check if the constant is defined or not + * + * @return mixed Class constants can return many types like scalars, arrays, and + * objects depending on the PHP version (\BackedEnum, \UnitEnum, etc.) + * When $checkDefined is true, returns true when the constant is defined, false otherwise + * + * @internal + */ + public static function constant($constant, $object = null, bool $checkDefined = false) + { + if (null !== $object) { + if ('class' === $constant) { + return $checkDefined ? true : \get_class($object); } - return $object[$arrayItem]; + $constant = \get_class($object).'::'.$constant; } - if (/* Template::ARRAY_CALL */ 'array' === $type || !\is_object($object)) { + if (!\defined($constant)) { + if ($checkDefined) { + return false; + } + + if ('::class' === strtolower(substr($constant, -7))) { + throw new RuntimeError(\sprintf('You cannot use the Twig function "constant()" to access "%s". You could provide an object and call constant("class", $object) or use the class name directly as a string.', $constant)); + } + + throw new RuntimeError(\sprintf('Constant "%s" is undefined.', $constant)); + } + + return $checkDefined ? true : \constant($constant); + } + + /** + * Batches item. + * + * @param array $items An array of items + * @param int $size The size of the batch + * @param mixed $fill A value used to fill missing items + * + * @internal + */ + public static function batch($items, $size, $fill = null, $preserveKeys = true): array + { + if (!is_iterable($items)) { + throw new RuntimeError(\sprintf('The "batch" filter expects a sequence/mapping or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); + } + + $size = (int) ceil($size); + + $result = array_chunk(self::toArray($items, $preserveKeys), $size, $preserveKeys); + + if (null !== $fill && $result) { + $last = \count($result) - 1; + if ($fillCount = $size - \count($result[$last])) { + for ($i = 0; $i < $fillCount; ++$i) { + $result[$last][] = $fill; + } + } + } + + return $result; + } + + /** + * Returns the attribute value for a given array/object. + * + * @param mixed $object The object or array from where to get the item + * @param mixed $item The item to get from the array or object + * @param array $arguments An array of arguments to pass if the item is an object method + * @param string $type The type of attribute (@see \Twig\Template constants) + * @param bool $isDefinedTest Whether this is only a defined check + * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not + * @param int $lineno The template line where the attribute was called + * + * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true + * + * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false + * + * @internal + */ + public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) + { + // array + if (Template::METHOD_CALL !== $type) { + $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; + + if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) + || ($object instanceof \ArrayAccess && isset($object[$arrayItem])) + ) { + if ($isDefinedTest) { + return true; + } + + return $object[$arrayItem]; + } + + if (Template::ARRAY_CALL === $type || !\is_object($object)) { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + + if ($object instanceof \ArrayAccess) { + $message = \sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object)); + } elseif (\is_object($object)) { + $message = \sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); + } elseif (\is_array($object)) { + if (empty($object)) { + $message = \sprintf('Key "%s" does not exist as the sequence/mapping is empty.', $arrayItem); + } else { + $message = \sprintf('Key "%s" for sequence/mapping with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); + } + } elseif (Template::ARRAY_CALL === $type) { + if (null === $object) { + $message = \sprintf('Impossible to access a key ("%s") on a null variable.', $item); + } else { + $message = \sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + } + } elseif (null === $object) { + $message = \sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); + } else { + $message = \sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + } + + throw new RuntimeError($message, $lineno, $source); + } + } + + if (!\is_object($object)) { if ($isDefinedTest) { return false; } @@ -1463,233 +1645,326 @@ function twig_get_attribute(Environment $env, Source $source, $object, $item, ar return; } - if ($object instanceof ArrayAccess) { - $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object)); - } elseif (\is_object($object)) { - $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); + if (null === $object) { + $message = \sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); } elseif (\is_array($object)) { - if (empty($object)) { - $message = sprintf('Key "%s" does not exist as the array is empty.', $arrayItem); - } else { - $message = sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); - } - } elseif (/* Template::ARRAY_CALL */ 'array' === $type) { - if (null === $object) { - $message = sprintf('Impossible to access a key ("%s") on a null variable.', $item); - } else { - $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); - } - } elseif (null === $object) { - $message = sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); + $message = \sprintf('Impossible to invoke a method ("%s") on a sequence/mapping.', $item); } else { - $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + $message = \sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); } throw new RuntimeError($message, $lineno, $source); } - } - if (!\is_object($object)) { - if ($isDefinedTest) { - return false; + if ($object instanceof Template) { + throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source); } - if ($ignoreStrictCheck || !$env->isStrictVariables()) { - return; - } + // object property + if (Template::METHOD_CALL !== $type) { + if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { + if ($isDefinedTest) { + return true; + } - if (null === $object) { - $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); - } elseif (\is_array($object)) { - $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item); - } else { - $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); - } + if ($sandboxed) { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); + } - throw new RuntimeError($message, $lineno, $source); - } - - if ($object instanceof Template) { - throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source); - } - - // object property - if (/* Template::METHOD_CALL */ 'method' !== $type) { - if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { - if ($isDefinedTest) { - return true; + return $object->$item; } - - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); - } - - return $object->$item; } - } - static $cache = []; + static $cache = []; - $class = \get_class($object); + $class = \get_class($object); - // object method - // precedence: getXxx() > isXxx() > hasXxx() - if (!isset($cache[$class])) { - $methods = get_class_methods($object); - sort($methods); - $lcMethods = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, $methods); - $classCache = []; - foreach ($methods as $i => $method) { - $classCache[$method] = $method; - $classCache[$lcName = $lcMethods[$i]] = $method; + // object method + // precedence: getXxx() > isXxx() > hasXxx() + if (!isset($cache[$class])) { + $methods = get_class_methods($object); + sort($methods); + $lcMethods = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, $methods); + $classCache = []; + foreach ($methods as $i => $method) { + $classCache[$method] = $method; + $classCache[$lcName = $lcMethods[$i]] = $method; - if ('g' === $lcName[0] && 0 === strpos($lcName, 'get')) { - $name = substr($method, 3); - $lcName = substr($lcName, 3); - } elseif ('i' === $lcName[0] && 0 === strpos($lcName, 'is')) { - $name = substr($method, 2); - $lcName = substr($lcName, 2); - } elseif ('h' === $lcName[0] && 0 === strpos($lcName, 'has')) { - $name = substr($method, 3); - $lcName = substr($lcName, 3); - if (\in_array('is'.$lcName, $lcMethods)) { + if ('g' === $lcName[0] && str_starts_with($lcName, 'get')) { + $name = substr($method, 3); + $lcName = substr($lcName, 3); + } elseif ('i' === $lcName[0] && str_starts_with($lcName, 'is')) { + $name = substr($method, 2); + $lcName = substr($lcName, 2); + } elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) { + $name = substr($method, 3); + $lcName = substr($lcName, 3); + if (\in_array('is'.$lcName, $lcMethods)) { + continue; + } + } else { continue; } - } else { - continue; - } - // skip get() and is() methods (in which case, $name is empty) - if ($name) { - if (!isset($classCache[$name])) { - $classCache[$name] = $method; - } + // skip get() and is() methods (in which case, $name is empty) + if ($name) { + if (!isset($classCache[$name])) { + $classCache[$name] = $method; + } - if (!isset($classCache[$lcName])) { - $classCache[$lcName] = $method; + if (!isset($classCache[$lcName])) { + $classCache[$lcName] = $method; + } } } + $cache[$class] = $classCache; + } + + $call = false; + if (isset($cache[$class][$item])) { + $method = $cache[$class][$item]; + } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) { + $method = $cache[$class][$lcItem]; + } elseif (isset($cache[$class]['__call'])) { + $method = $item; + $call = true; + } else { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + + throw new RuntimeError(\sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); } - $cache[$class] = $classCache; - } - $call = false; - if (isset($cache[$class][$item])) { - $method = $cache[$class][$item]; - } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) { - $method = $cache[$class][$lcItem]; - } elseif (isset($cache[$class]['__call'])) { - $method = $item; - $call = true; - } else { if ($isDefinedTest) { - return false; + return true; } - if ($ignoreStrictCheck || !$env->isStrictVariables()) { - return; + if ($sandboxed) { + $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); } - throw new RuntimeError(sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); + // Some objects throw exceptions when they have __call, and the method we try + // to call is not supported. If ignoreStrictCheck is true, we should return null. + try { + $ret = $object->$method(...$arguments); + } catch (\BadMethodCallException $e) { + if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) { + return; + } + throw $e; + } + + return $ret; } - if ($isDefinedTest) { + /** + * Returns the values from a single column in the input array. + * + *
+     *  {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
+     *
+     *  {% set fruits = items|column('fruit') %}
+     *
+     *  {# fruits now contains ['apple', 'orange'] #}
+     * 
+ * + * @param array|\Traversable $array An array + * @param int|string $name The column name + * @param int|string|null $index The column to use as the index/keys for the returned array + * + * @return array The array of values + * + * @internal + */ + public static function column($array, $name, $index = null): array + { + if ($array instanceof \Traversable) { + $array = iterator_to_array($array); + } elseif (!\is_array($array)) { + throw new RuntimeError(\sprintf('The column filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', \gettype($array))); + } + + return array_column($array, $name, $index); + } + + /** + * @internal + */ + public static function filter(Environment $env, $array, $arrow) + { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "filter" filter expects a sequence/mapping or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); + } + + self::checkArrowInSandbox($env, $arrow, 'filter', 'filter'); + + if (\is_array($array)) { + return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); + } + + // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator + return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow); + } + + /** + * @internal + */ + public static function find(Environment $env, $array, $arrow) + { + self::checkArrowInSandbox($env, $arrow, 'find', 'filter'); + + foreach ($array as $k => $v) { + if ($arrow($v, $k)) { + return $v; + } + } + + return null; + } + + /** + * @internal + */ + public static function map(Environment $env, $array, $arrow) + { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "map" filter expects a sequence/mapping or "Traversable", got "%s".', get_debug_type($array))); + } + + self::checkArrowInSandbox($env, $arrow, 'map', 'filter'); + + $r = []; + foreach ($array as $k => $v) { + $r[$k] = $arrow($v, $k); + } + + return $r; + } + + /** + * @internal + */ + public static function reduce(Environment $env, $array, $arrow, $initial = null) + { + self::checkArrowInSandbox($env, $arrow, 'reduce', 'filter'); + + if (!\is_array($array) && !$array instanceof \Traversable) { + throw new RuntimeError(\sprintf('The "reduce" filter only works with sequences/mappings or "Traversable", got "%s" as first argument.', \gettype($array))); + } + + $accumulator = $initial; + foreach ($array as $key => $value) { + $accumulator = $arrow($accumulator, $value, $key); + } + + return $accumulator; + } + + /** + * @internal + */ + public static function arraySome(Environment $env, $array, $arrow) + { + self::checkArrowInSandbox($env, $arrow, 'has some', 'operator'); + + foreach ($array as $k => $v) { + if ($arrow($v, $k)) { + return true; + } + } + + return false; + } + + /** + * @internal + */ + public static function arrayEvery(Environment $env, $array, $arrow) + { + self::checkArrowInSandbox($env, $arrow, 'has every', 'operator'); + + foreach ($array as $k => $v) { + if (!$arrow($v, $k)) { + return false; + } + } + return true; } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); - } - - // Some objects throw exceptions when they have __call, and the method we try - // to call is not supported. If ignoreStrictCheck is true, we should return null. - try { - $ret = $object->$method(...$arguments); - } catch (\BadMethodCallException $e) { - if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) { - return; + /** + * @internal + */ + public static function checkArrowInSandbox(Environment $env, $arrow, $thing, $type) + { + if (!$arrow instanceof \Closure && $env->hasExtension(SandboxExtension::class) && $env->getExtension(SandboxExtension::class)->isSandboxed()) { + throw new RuntimeError(\sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type)); } - throw $e; } - return $ret; -} + /** + * @internal to be removed in Twig 4 + */ + public static function captureOutput(iterable $body): string + { + $level = ob_get_level(); + ob_start(); -/** - * Returns the values from a single column in the input array. - * - *
- *  {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
- *
- *  {% set fruits = items|column('fruit') %}
- *
- *  {# fruits now contains ['apple', 'orange'] #}
- * 
- * - * @param array|Traversable $array An array - * @param mixed $name The column name - * @param mixed $index The column to use as the index/keys for the returned array - * - * @return array The array of values - */ -function twig_array_column($array, $name, $index = null): array -{ - if ($array instanceof Traversable) { - $array = iterator_to_array($array); - } elseif (!\is_array($array)) { - throw new RuntimeError(sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); - } + try { + foreach ($body as $data) { + echo $data; + } + } catch (\Throwable $e) { + while (ob_get_level() > $level) { + ob_end_clean(); + } - return array_column($array, $name, $index); -} - -function twig_array_filter(Environment $env, $array, $arrow) -{ - if (!twig_test_iterable($array)) { - throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array))); - } - - twig_check_arrow_in_sandbox($env, $arrow, 'filter', 'filter'); - - if (\is_array($array)) { - return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); - } - - // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator - return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow); -} - -function twig_array_map(Environment $env, $array, $arrow) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'map', 'filter'); - - $r = []; - foreach ($array as $k => $v) { - $r[$k] = $arrow($v, $k); - } - - return $r; -} - -function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) -{ - twig_check_arrow_in_sandbox($env, $arrow, 'reduce', 'filter'); - - if (!\is_array($array)) { - if (!$array instanceof \Traversable) { - throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array))); + throw $e; } - $array = iterator_to_array($array); + return ob_get_clean(); } - return array_reduce($array, $arrow, $initial); -} + /** + * @internal + */ + public static function parseParentFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + { + if (!$blockName = $parser->peekBlockStack()) { + throw new SyntaxError('Calling the "parent" function outside of a block is forbidden.', $line, $parser->getStream()->getSourceContext()); + } -function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type) -{ - if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) { - throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type)); + if (!$parser->hasInheritance()) { + throw new SyntaxError('Calling the "parent" function on a template that does not call "extends" or "use" is forbidden.', $line, $parser->getStream()->getSourceContext()); + } + + return new ParentExpression($blockName, $line); + } + + /** + * @internal + */ + public static function parseBlockFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + { + $fakeFunction = new TwigFunction('block', fn ($name, $template = null) => null); + $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args); + + return new BlockReferenceExpression($args[0], $args[1] ?? null, $line); + } + + /** + * @internal + */ + public static function parseAttributeFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + { + $fakeFunction = new TwigFunction('attribute', fn ($variable, $attribute, $arguments = null) => null); + $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args); + + return new GetAttrExpression($args[0], $args[1], $args[2] ?? null, Template::ANY_CALL, $line); } } -} diff --git a/data/web/inc/lib/vendor/twig/twig/src/Extension/DebugExtension.php b/data/web/inc/lib/vendor/twig/twig/src/Extension/DebugExtension.php index bfb23d7bd..dac21c317 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Extension/DebugExtension.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Extension/DebugExtension.php @@ -9,7 +9,11 @@ * file that was distributed with this source code. */ -namespace Twig\Extension { +namespace Twig\Extension; + +use Twig\Environment; +use Twig\Template; +use Twig\TemplateWrapper; use Twig\TwigFunction; final class DebugExtension extends AbstractExtension @@ -18,47 +22,41 @@ final class DebugExtension extends AbstractExtension { // dump is safe if var_dump is overridden by xdebug $isDumpOutputHtmlSafe = \extension_loaded('xdebug') - // false means that it was not set (and the default is on) or it explicitly enabled - && (false === ini_get('xdebug.overload_var_dump') || ini_get('xdebug.overload_var_dump')) - // false means that it was not set (and the default is on) or it explicitly enabled - // xdebug.overload_var_dump produces HTML only when html_errors is also enabled - && (false === ini_get('html_errors') || ini_get('html_errors')) + // Xdebug overloads var_dump in develop mode when html_errors is enabled + && str_contains(\ini_get('xdebug.mode'), 'develop') + && (false === \ini_get('html_errors') || \ini_get('html_errors')) || 'cli' === \PHP_SAPI ; return [ - new TwigFunction('dump', 'twig_var_dump', ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]), + new TwigFunction('dump', [self::class, 'dump'], ['is_safe' => $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]), ]; } -} -} -namespace { -use Twig\Environment; -use Twig\Template; -use Twig\TemplateWrapper; - -function twig_var_dump(Environment $env, $context, ...$vars) -{ - if (!$env->isDebug()) { - return; - } - - ob_start(); - - if (!$vars) { - $vars = []; - foreach ($context as $key => $value) { - if (!$value instanceof Template && !$value instanceof TemplateWrapper) { - $vars[$key] = $value; - } + /** + * @internal + */ + public static function dump(Environment $env, $context, ...$vars) + { + if (!$env->isDebug()) { + return; } - var_dump($vars); - } else { - var_dump(...$vars); - } + ob_start(); - return ob_get_clean(); -} + if (!$vars) { + $vars = []; + foreach ($context as $key => $value) { + if (!$value instanceof Template && !$value instanceof TemplateWrapper) { + $vars[$key] = $value; + } + } + + var_dump($vars); + } else { + var_dump(...$vars); + } + + return ob_get_clean(); + } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Extension/EscaperExtension.php b/data/web/inc/lib/vendor/twig/twig/src/Extension/EscaperExtension.php index 9d2251dc6..52531c436 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Extension/EscaperExtension.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Extension/EscaperExtension.php @@ -9,22 +9,24 @@ * file that was distributed with this source code. */ -namespace Twig\Extension { +namespace Twig\Extension; + +use Twig\Environment; use Twig\FileExtensionEscapingStrategy; +use Twig\Node\Expression\ConstantExpression; +use Twig\Node\Expression\Filter\RawFilter; +use Twig\Node\Node; use Twig\NodeVisitor\EscaperNodeVisitor; +use Twig\Runtime\EscaperRuntime; use Twig\TokenParser\AutoEscapeTokenParser; use Twig\TwigFilter; final class EscaperExtension extends AbstractExtension { - private $defaultStrategy; + private $environment; private $escapers = []; - - /** @internal */ - public $safeClasses = []; - - /** @internal */ - public $safeLookup = []; + private $escaper; + private $defaultStrategy; /** * @param string|false|callable $defaultStrategy An escaping strategy @@ -49,19 +51,43 @@ final class EscaperExtension extends AbstractExtension public function getFilters(): array { return [ - new TwigFilter('escape', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']), - new TwigFilter('e', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']), - new TwigFilter('raw', 'twig_raw_filter', ['is_safe' => ['all']]), + new TwigFilter('escape', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), + new TwigFilter('e', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), + new TwigFilter('raw', null, ['is_safe' => ['all'], 'node_class' => RawFilter::class]), ]; } + /** + * @deprecated since Twig 3.10 + */ + public function setEnvironment(Environment $environment): void + { + $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true; + if ($triggerDeprecation) { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__); + } + + $this->environment = $environment; + $this->escaper = $environment->getRuntime(EscaperRuntime::class); + } + + /** + * @deprecated since Twig 3.10 + */ + public function setEscaperRuntime(EscaperRuntime $escaper) + { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__); + + $this->escaper = $escaper; + } + /** * Sets the default strategy to use when not defined by the user. * * The strategy can be a valid PHP callback that takes the template * name as an argument and returns the strategy to use. * - * @param string|false|callable $defaultStrategy An escaping strategy + * @param string|false|callable(string $templateName): string $defaultStrategy An escaping strategy */ public function setDefaultStrategy($defaultStrategy): void { @@ -93,324 +119,82 @@ final class EscaperExtension extends AbstractExtension /** * Defines a new escaper to be used via the escape filter. * - * @param string $strategy The strategy name that should be used as a strategy in the escape call - * @param callable $callable A valid PHP callable + * @param string $strategy The strategy name that should be used as a strategy in the escape call + * @param callable(Environment, string, string): string $callable A valid PHP callable + * + * @deprecated since Twig 3.10 */ public function setEscaper($strategy, callable $callable) { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setEscaper()" method instead (be warned that Environment is not passed anymore to the callable).', __METHOD__); + + if (!isset($this->environment)) { + throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); + } + $this->escapers[$strategy] = $callable; + $callable = function ($string, $charset) use ($callable) { + return $callable($this->environment, $string, $charset); + }; + + $this->escaper->setEscaper($strategy, $callable); } /** * Gets all defined escapers. * - * @return callable[] An array of escapers + * @return array An array of escapers + * + * @deprecated since Twig 3.10 */ public function getEscapers() { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::getEscaper()" method instead.', __METHOD__); + return $this->escapers; } + /** + * @deprecated since Twig 3.10 + */ public function setSafeClasses(array $safeClasses = []) { - $this->safeClasses = []; - $this->safeLookup = []; - foreach ($safeClasses as $class => $strategies) { - $this->addSafeClass($class, $strategies); + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setSafeClasses()" method instead.', __METHOD__); + + if (!isset($this->escaper)) { + 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) { - $class = ltrim($class, '\\'); - if (!isset($this->safeClasses[$class])) { - $this->safeClasses[$class] = []; - } - $this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies); + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::addSafeClass()" method instead.', __METHOD__); - foreach ($strategies as $strategy) { - $this->safeLookup[$strategy][$class] = true; - } - } -} -} - -namespace { -use Twig\Environment; -use Twig\Error\RuntimeError; -use Twig\Extension\EscaperExtension; -use Twig\Markup; -use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Node; - -/** - * Marks a variable as being safe. - * - * @param string $string A PHP variable - */ -function twig_raw_filter($string) -{ - return $string; -} - -/** - * Escapes a string. - * - * @param mixed $string The value to be escaped - * @param string $strategy The escaping strategy - * @param string $charset The charset - * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) - * - * @return string - */ -function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) -{ - if ($autoescape && $string instanceof Markup) { - return $string; - } - - if (!\is_string($string)) { - if (\is_object($string) && method_exists($string, '__toString')) { - if ($autoescape) { - $c = \get_class($string); - $ext = $env->getExtension(EscaperExtension::class); - if (!isset($ext->safeClasses[$c])) { - $ext->safeClasses[$c] = []; - foreach (class_parents($string) + class_implements($string) as $class) { - if (isset($ext->safeClasses[$class])) { - $ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class])); - foreach ($ext->safeClasses[$class] as $s) { - $ext->safeLookup[$s][$c] = true; - } - } - } - } - if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) { - return (string) $string; - } - } - - $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 '�'; - } - - /* - * 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 => '"', /* quotation mark */ - 38 => '&', /* ampersand */ - 60 => '<', /* less-than sign */ - 62 => '>', /* 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 - */ -function twig_escape_filter_is_safe(Node $filterArgs) -{ - foreach ($filterArgs as $arg) { - if ($arg instanceof ConstantExpression) { - return [$arg->getAttribute('value')]; + if (!isset($this->escaper)) { + throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', __METHOD__)); } - return []; + $this->escaper->addSafeClass($class, $strategies); } - return ['html']; -} + /** + * @internal + */ + public static function escapeFilterIsSafe(Node $filterArgs) + { + foreach ($filterArgs as $arg) { + if ($arg instanceof ConstantExpression) { + return [$arg->getAttribute('value')]; + } + + return []; + } + + return ['html']; + } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Extension/ExtensionInterface.php b/data/web/inc/lib/vendor/twig/twig/src/Extension/ExtensionInterface.php index 75fa237e1..10a42b6b1 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Extension/ExtensionInterface.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Extension/ExtensionInterface.php @@ -11,6 +11,8 @@ namespace Twig\Extension; +use Twig\ExpressionParser; +use Twig\Node\Expression\AbstractExpression; use Twig\NodeVisitor\NodeVisitorInterface; use Twig\TokenParser\TokenParserInterface; use Twig\TwigFilter; @@ -63,6 +65,11 @@ interface ExtensionInterface * Returns a list of operators to add to the existing list. * * @return array First array of unary operators, second array of binary operators + * + * @psalm-return array{ + * array}>, + * array, associativity: ExpressionParser::OPERATOR_*}> + * } */ public function getOperators(); } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Extension/GlobalsInterface.php b/data/web/inc/lib/vendor/twig/twig/src/Extension/GlobalsInterface.php index ec0c68292..d52cd107e 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Extension/GlobalsInterface.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Extension/GlobalsInterface.php @@ -12,14 +12,14 @@ namespace Twig\Extension; /** - * Enables usage of the deprecated Twig\Extension\AbstractExtension::getGlobals() method. - * - * Explicitly implement this interface if you really need to implement the - * deprecated getGlobals() method in your extensions. + * Allows Twig extensions to add globals to the context. * * @author Fabien Potencier */ interface GlobalsInterface { + /** + * @return array + */ public function getGlobals(): array; } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Extension/OptimizerExtension.php b/data/web/inc/lib/vendor/twig/twig/src/Extension/OptimizerExtension.php index 965bfdb04..d3fe46a67 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Extension/OptimizerExtension.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Extension/OptimizerExtension.php @@ -15,11 +15,9 @@ use Twig\NodeVisitor\OptimizerNodeVisitor; final class OptimizerExtension extends AbstractExtension { - private $optimizers; - - public function __construct(int $optimizers = -1) - { - $this->optimizers = $optimizers; + public function __construct( + private int $optimizers = -1, + ) { } public function getNodeVisitors(): array diff --git a/data/web/inc/lib/vendor/twig/twig/src/Extension/SandboxExtension.php b/data/web/inc/lib/vendor/twig/twig/src/Extension/SandboxExtension.php index c861159b6..4e96760f7 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Extension/SandboxExtension.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Extension/SandboxExtension.php @@ -15,6 +15,7 @@ use Twig\NodeVisitor\SandboxNodeVisitor; use Twig\Sandbox\SecurityNotAllowedMethodError; use Twig\Sandbox\SecurityNotAllowedPropertyError; use Twig\Sandbox\SecurityPolicyInterface; +use Twig\Sandbox\SourcePolicyInterface; use Twig\Source; use Twig\TokenParser\SandboxTokenParser; @@ -23,11 +24,13 @@ final class SandboxExtension extends AbstractExtension private $sandboxedGlobally; private $sandboxed; private $policy; + private $sourcePolicy; - public function __construct(SecurityPolicyInterface $policy, $sandboxed = false) + public function __construct(SecurityPolicyInterface $policy, $sandboxed = false, ?SourcePolicyInterface $sourcePolicy = null) { $this->policy = $policy; $this->sandboxedGlobally = $sandboxed; + $this->sourcePolicy = $sourcePolicy; } public function getTokenParsers(): array @@ -50,9 +53,9 @@ final class SandboxExtension extends AbstractExtension $this->sandboxed = false; } - public function isSandboxed(): bool + public function isSandboxed(?Source $source = null): bool { - return $this->sandboxedGlobally || $this->sandboxed; + return $this->sandboxedGlobally || $this->sandboxed || $this->isSourceSandboxed($source); } public function isSandboxedGlobally(): bool @@ -60,6 +63,15 @@ final class SandboxExtension extends AbstractExtension return $this->sandboxedGlobally; } + private function isSourceSandboxed(?Source $source): bool + { + if (null === $source || null === $this->sourcePolicy) { + return false; + } + + return $this->sourcePolicy->enableSandbox($source); + } + public function setSecurityPolicy(SecurityPolicyInterface $policy) { $this->policy = $policy; @@ -70,16 +82,16 @@ final class SandboxExtension extends AbstractExtension return $this->policy; } - public function checkSecurity($tags, $filters, $functions): void + public function checkSecurity($tags, $filters, $functions, ?Source $source = null): void { - if ($this->isSandboxed()) { + if ($this->isSandboxed($source)) { $this->policy->checkSecurity($tags, $filters, $functions); } } - public function checkMethodAllowed($obj, $method, int $lineno = -1, Source $source = null): void + public function checkMethodAllowed($obj, $method, int $lineno = -1, ?Source $source = null): void { - if ($this->isSandboxed()) { + if ($this->isSandboxed($source)) { try { $this->policy->checkMethodAllowed($obj, $method); } catch (SecurityNotAllowedMethodError $e) { @@ -91,9 +103,9 @@ final class SandboxExtension extends AbstractExtension } } - public function checkPropertyAllowed($obj, $property, int $lineno = -1, Source $source = null): void + public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source $source = null): void { - if ($this->isSandboxed()) { + if ($this->isSandboxed($source)) { try { $this->policy->checkPropertyAllowed($obj, $property); } catch (SecurityNotAllowedPropertyError $e) { @@ -105,9 +117,9 @@ final class SandboxExtension extends AbstractExtension } } - public function ensureToStringAllowed($obj, int $lineno = -1, Source $source = null) + public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null) { - if ($this->isSandboxed() && \is_object($obj) && method_exists($obj, '__toString')) { + if ($this->isSandboxed($source) && $obj instanceof \Stringable) { try { $this->policy->checkMethodAllowed($obj, '__toString'); } catch (SecurityNotAllowedMethodError $e) { diff --git a/data/web/inc/lib/vendor/twig/twig/src/Extension/StagingExtension.php b/data/web/inc/lib/vendor/twig/twig/src/Extension/StagingExtension.php index 0ea47f90c..59db2ca7d 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Extension/StagingExtension.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Extension/StagingExtension.php @@ -35,7 +35,7 @@ final class StagingExtension extends AbstractExtension public function addFunction(TwigFunction $function): void { if (isset($this->functions[$function->getName()])) { - throw new \LogicException(sprintf('Function "%s" is already registered.', $function->getName())); + throw new \LogicException(\sprintf('Function "%s" is already registered.', $function->getName())); } $this->functions[$function->getName()] = $function; @@ -49,7 +49,7 @@ final class StagingExtension extends AbstractExtension public function addFilter(TwigFilter $filter): void { if (isset($this->filters[$filter->getName()])) { - throw new \LogicException(sprintf('Filter "%s" is already registered.', $filter->getName())); + throw new \LogicException(\sprintf('Filter "%s" is already registered.', $filter->getName())); } $this->filters[$filter->getName()] = $filter; @@ -73,7 +73,7 @@ final class StagingExtension extends AbstractExtension public function addTokenParser(TokenParserInterface $parser): void { if (isset($this->tokenParsers[$parser->getTag()])) { - throw new \LogicException(sprintf('Tag "%s" is already registered.', $parser->getTag())); + throw new \LogicException(\sprintf('Tag "%s" is already registered.', $parser->getTag())); } $this->tokenParsers[$parser->getTag()] = $parser; @@ -87,7 +87,7 @@ final class StagingExtension extends AbstractExtension public function addTest(TwigTest $test): void { if (isset($this->tests[$test->getName()])) { - throw new \LogicException(sprintf('Test "%s" is already registered.', $test->getName())); + throw new \LogicException(\sprintf('Test "%s" is already registered.', $test->getName())); } $this->tests[$test->getName()] = $test; diff --git a/data/web/inc/lib/vendor/twig/twig/src/Extension/StringLoaderExtension.php b/data/web/inc/lib/vendor/twig/twig/src/Extension/StringLoaderExtension.php index 7b4514710..698d181f1 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Extension/StringLoaderExtension.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Extension/StringLoaderExtension.php @@ -9,7 +9,10 @@ * file that was distributed with this source code. */ -namespace Twig\Extension { +namespace Twig\Extension; + +use Twig\Environment; +use Twig\TemplateWrapper; use Twig\TwigFunction; final class StringLoaderExtension extends AbstractExtension @@ -17,26 +20,21 @@ final class StringLoaderExtension extends AbstractExtension public function getFunctions(): array { return [ - new TwigFunction('template_from_string', 'twig_template_from_string', ['needs_environment' => true]), + new TwigFunction('template_from_string', [self::class, 'templateFromString'], ['needs_environment' => true]), ]; } -} -} -namespace { -use Twig\Environment; -use Twig\TemplateWrapper; - -/** - * Loads a template from a string. - * - * {{ include(template_from_string("Hello {{ name }}")) }} - * - * @param string $template A template as a string or object implementing __toString() - * @param string $name An optional name of the template to be used in error messages - */ -function twig_template_from_string(Environment $env, $template, string $name = null): TemplateWrapper -{ - return $env->createTemplate((string) $template, $name); -} + /** + * Loads a template from a string. + * + * {{ include(template_from_string("Hello {{ name }}")) }} + * + * @param string|null $name An optional name of the template to be used in error messages + * + * @internal + */ + public static function templateFromString(Environment $env, string|\Stringable $template, ?string $name = null): TemplateWrapper + { + return $env->createTemplate((string) $template, $name); + } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Extension/YieldNotReadyExtension.php b/data/web/inc/lib/vendor/twig/twig/src/Extension/YieldNotReadyExtension.php new file mode 100644 index 000000000..49dfb8085 --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Extension/YieldNotReadyExtension.php @@ -0,0 +1,30 @@ +useYield)]; + } +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/ExtensionSet.php b/data/web/inc/lib/vendor/twig/twig/src/ExtensionSet.php index 36e5bbc59..28d57a41c 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/ExtensionSet.php +++ b/data/web/inc/lib/vendor/twig/twig/src/ExtensionSet.php @@ -15,6 +15,9 @@ use Twig\Error\RuntimeError; use Twig\Extension\ExtensionInterface; use Twig\Extension\GlobalsInterface; use Twig\Extension\StagingExtension; +use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Expression\Binary\AbstractBinary; +use Twig\Node\Expression\Unary\AbstractUnary; use Twig\NodeVisitor\NodeVisitorInterface; use Twig\TokenParser\TokenParserInterface; @@ -31,11 +34,23 @@ final class ExtensionSet private $staging; private $parsers; private $visitors; + /** @var array */ private $filters; + /** @var array */ + private $dynamicFilters; + /** @var array */ private $tests; + /** @var array */ + private $dynamicTests; + /** @var array */ private $functions; + /** @var array */ + private $dynamicFunctions; + /** @var array}> */ private $unaryOperators; + /** @var array, associativity: ExpressionParser::OPERATOR_*}> */ private $binaryOperators; + /** @var array */ private $globals; private $functionCallbacks = []; private $filterCallbacks = []; @@ -62,7 +77,7 @@ final class ExtensionSet $class = ltrim($class, '\\'); if (!isset($this->extensions[$class])) { - throw new RuntimeError(sprintf('The "%s" extension is not enabled.', $class)); + throw new RuntimeError(\sprintf('The "%s" extension is not enabled.', $class)); } return $this->extensions[$class]; @@ -117,11 +132,11 @@ final class ExtensionSet $class = \get_class($extension); if ($this->initialized) { - throw new \LogicException(sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class)); + throw new \LogicException(\sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class)); } if (isset($this->extensions[$class])) { - throw new \LogicException(sprintf('Unable to register extension "%s" as it is already registered.', $class)); + throw new \LogicException(\sprintf('Unable to register extension "%s" as it is already registered.', $class)); } $this->extensions[$class] = $extension; @@ -130,7 +145,7 @@ final class ExtensionSet public function addFunction(TwigFunction $function): void { if ($this->initialized) { - throw new \LogicException(sprintf('Unable to add function "%s" as extensions have already been initialized.', $function->getName())); + throw new \LogicException(\sprintf('Unable to add function "%s" as extensions have already been initialized.', $function->getName())); } $this->staging->addFunction($function); @@ -158,14 +173,11 @@ final class ExtensionSet return $this->functions[$name]; } - foreach ($this->functions as $pattern => $function) { - $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); - - if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) { + foreach ($this->dynamicFunctions as $pattern => $function) { + if (preg_match($pattern, $name, $matches)) { array_shift($matches); - $function->setArguments($matches); - return $function; + return $function->withDynamicArguments($name, $function->getName(), $matches); } } @@ -186,7 +198,7 @@ final class ExtensionSet public function addFilter(TwigFilter $filter): void { if ($this->initialized) { - throw new \LogicException(sprintf('Unable to add filter "%s" as extensions have already been initialized.', $filter->getName())); + throw new \LogicException(\sprintf('Unable to add filter "%s" as extensions have already been initialized.', $filter->getName())); } $this->staging->addFilter($filter); @@ -214,14 +226,11 @@ final class ExtensionSet return $this->filters[$name]; } - foreach ($this->filters as $pattern => $filter) { - $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); - - if ($count && preg_match('#^'.$pattern.'$#', $name, $matches)) { + foreach ($this->dynamicFilters as $pattern => $filter) { + if (preg_match($pattern, $name, $matches)) { array_shift($matches); - $filter->setArguments($matches); - return $filter; + return $filter->withDynamicArguments($name, $filter->getName(), $matches); } } @@ -305,6 +314,9 @@ final class ExtensionSet $this->parserCallbacks[] = $callable; } + /** + * @return array + */ public function getGlobals(): array { if (null !== $this->globals) { @@ -317,12 +329,7 @@ final class ExtensionSet continue; } - $extGlobals = $extension->getGlobals(); - if (!\is_array($extGlobals)) { - throw new \UnexpectedValueException(sprintf('"%s::getGlobals()" must return an array of globals.', \get_class($extension))); - } - - $globals = array_merge($globals, $extGlobals); + $globals = array_merge($globals, $extension->getGlobals()); } if ($this->initialized) { @@ -332,10 +339,15 @@ final class ExtensionSet return $globals; } + public function resetGlobals(): void + { + $this->globals = null; + } + public function addTest(TwigTest $test): void { if ($this->initialized) { - throw new \LogicException(sprintf('Unable to add test "%s" as extensions have already been initialized.', $test->getName())); + throw new \LogicException(\sprintf('Unable to add test "%s" as extensions have already been initialized.', $test->getName())); } $this->staging->addTest($test); @@ -363,22 +375,20 @@ final class ExtensionSet return $this->tests[$name]; } - foreach ($this->tests as $pattern => $test) { - $pattern = str_replace('\\*', '(.*?)', preg_quote($pattern, '#'), $count); + foreach ($this->dynamicTests as $pattern => $test) { + if (preg_match($pattern, $name, $matches)) { + array_shift($matches); - if ($count) { - if (preg_match('#^'.$pattern.'$#', $name, $matches)) { - array_shift($matches); - $test->setArguments($matches); - - return $test; - } + return $test->withDynamicArguments($name, $test->getName(), $matches); } } return null; } + /** + * @return array}> + */ public function getUnaryOperators(): array { if (!$this->initialized) { @@ -388,6 +398,9 @@ final class ExtensionSet return $this->unaryOperators; } + /** + * @return array, associativity: ExpressionParser::OPERATOR_*}> + */ public function getBinaryOperators(): array { if (!$this->initialized) { @@ -403,6 +416,9 @@ final class ExtensionSet $this->filters = []; $this->functions = []; $this->tests = []; + $this->dynamicFilters = []; + $this->dynamicFunctions = []; + $this->dynamicTests = []; $this->visitors = []; $this->unaryOperators = []; $this->binaryOperators = []; @@ -419,17 +435,26 @@ final class ExtensionSet { // filters foreach ($extension->getFilters() as $filter) { - $this->filters[$filter->getName()] = $filter; + $this->filters[$name = $filter->getName()] = $filter; + if (str_contains($name, '*')) { + $this->dynamicFilters['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $filter; + } } // functions foreach ($extension->getFunctions() as $function) { - $this->functions[$function->getName()] = $function; + $this->functions[$name = $function->getName()] = $function; + if (str_contains($name, '*')) { + $this->dynamicFunctions['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $function; + } } // tests foreach ($extension->getTests() as $test) { - $this->tests[$test->getName()] = $test; + $this->tests[$name = $test->getName()] = $test; + if (str_contains($name, '*')) { + $this->dynamicTests['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $test; + } } // token parsers @@ -449,11 +474,11 @@ final class ExtensionSet // operators if ($operators = $extension->getOperators()) { if (!\is_array($operators)) { - throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), \is_object($operators) ? \get_class($operators) : \gettype($operators).(\is_resource($operators) ? '' : '#'.$operators))); + throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), \is_object($operators) ? \get_class($operators) : \gettype($operators).(\is_resource($operators) ? '' : '#'.$operators))); } if (2 !== \count($operators)) { - throw new \InvalidArgumentException(sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); + throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); } $this->unaryOperators = array_merge($this->unaryOperators, $operators[0]); diff --git a/data/web/inc/lib/vendor/twig/twig/src/FileExtensionEscapingStrategy.php b/data/web/inc/lib/vendor/twig/twig/src/FileExtensionEscapingStrategy.php index 65198bbb6..812071bf9 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/FileExtensionEscapingStrategy.php +++ b/data/web/inc/lib/vendor/twig/twig/src/FileExtensionEscapingStrategy.php @@ -37,7 +37,7 @@ class FileExtensionEscapingStrategy return 'html'; // return html for directories } - if ('.twig' === substr($name, -5)) { + if (str_ends_with($name, '.twig')) { $name = substr($name, 0, -5); } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Lexer.php b/data/web/inc/lib/vendor/twig/twig/src/Lexer.php index 9ff028c87..28feaa2c1 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Lexer.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Lexer.php @@ -19,6 +19,8 @@ use Twig\Error\SyntaxError; */ class Lexer { + private $isInitialized = false; + private $tokens; private $code; private $cursor; @@ -48,6 +50,14 @@ class Lexer public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As'; public const PUNCTUATION = '()[]{}?:.,|'; + private const SPECIAL_CHARS = [ + 'f' => "\f", + 'n' => "\n", + 'r' => "\r", + 't' => "\t", + 'v' => "\v", + ]; + public function __construct(Environment $env, array $options = []) { $this->env = $env; @@ -61,6 +71,13 @@ class Lexer 'whitespace_line_chars' => ' \t\0\x0B', 'interpolation' => ['#{', '}'], ], $options); + } + + private function initialize() + { + if ($this->isInitialized) { + return; + } // when PHP 7.3 is the min version, we will be able to remove the '#' part in preg_quote as it's part of the default $this->regexes = [ @@ -149,10 +166,14 @@ class Lexer 'interpolation_start' => '{'.preg_quote($this->options['interpolation'][0], '#').'\s*}A', 'interpolation_end' => '{\s*'.preg_quote($this->options['interpolation'][1], '#').'}A', ]; + + $this->isInitialized = true; } public function tokenize(Source $source): TokenStream { + $this->initialize(); + $this->source = $source; $this->code = str_replace(["\r\n", "\r"], "\n", $source->getCode()); $this->cursor = 0; @@ -194,11 +215,11 @@ class Lexer } } - $this->pushToken(/* Token::EOF_TYPE */ -1); + $this->pushToken(Token::EOF_TYPE); if (!empty($this->brackets)) { - list($expect, $lineno) = array_pop($this->brackets); - throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source); + [$expect, $lineno] = array_pop($this->brackets); + throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source); } return new TokenStream($this->tokens, $this->source); @@ -208,7 +229,7 @@ class Lexer { // if no matches are left we return the rest of the template as simple text token if ($this->position == \count($this->positions[0]) - 1) { - $this->pushToken(/* Token::TEXT_TYPE */ 0, substr($this->code, $this->cursor)); + $this->pushToken(Token::TEXT_TYPE, substr($this->code, $this->cursor)); $this->cursor = $this->end; return; @@ -237,7 +258,7 @@ class Lexer $text = rtrim($text, " \t\0\x0B"); } } - $this->pushToken(/* Token::TEXT_TYPE */ 0, $text); + $this->pushToken(Token::TEXT_TYPE, $text); $this->moveCursor($textContent.$position[0]); switch ($this->positions[1][$this->position][0]) { @@ -255,14 +276,14 @@ class Lexer $this->moveCursor($match[0]); $this->lineno = (int) $match[1]; } else { - $this->pushToken(/* Token::BLOCK_START_TYPE */ 1); + $this->pushToken(Token::BLOCK_START_TYPE); $this->pushState(self::STATE_BLOCK); $this->currentVarBlockLine = $this->lineno; } break; case $this->options['tag_variable'][0]: - $this->pushToken(/* Token::VAR_START_TYPE */ 2); + $this->pushToken(Token::VAR_START_TYPE); $this->pushState(self::STATE_VAR); $this->currentVarBlockLine = $this->lineno; break; @@ -272,7 +293,7 @@ class Lexer private function lexBlock(): void { if (empty($this->brackets) && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) { - $this->pushToken(/* Token::BLOCK_END_TYPE */ 3); + $this->pushToken(Token::BLOCK_END_TYPE); $this->moveCursor($match[0]); $this->popState(); } else { @@ -283,7 +304,7 @@ class Lexer private function lexVar(): void { if (empty($this->brackets) && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) { - $this->pushToken(/* Token::VAR_END_TYPE */ 4); + $this->pushToken(Token::VAR_END_TYPE); $this->moveCursor($match[0]); $this->popState(); } else { @@ -298,23 +319,28 @@ class Lexer $this->moveCursor($match[0]); if ($this->cursor >= $this->end) { - throw new SyntaxError(sprintf('Unclosed "%s".', self::STATE_BLOCK === $this->state ? 'block' : 'variable'), $this->currentVarBlockLine, $this->source); + throw new SyntaxError(\sprintf('Unclosed "%s".', self::STATE_BLOCK === $this->state ? 'block' : 'variable'), $this->currentVarBlockLine, $this->source); } } + // spread operator + if ('.' === $this->code[$this->cursor] && ($this->cursor + 2 < $this->end) && '.' === $this->code[$this->cursor + 1] && '.' === $this->code[$this->cursor + 2]) { + $this->pushToken(Token::SPREAD_TYPE, '...'); + $this->moveCursor('...'); + } // arrow function - if ('=' === $this->code[$this->cursor] && '>' === $this->code[$this->cursor + 1]) { + elseif ('=' === $this->code[$this->cursor] && ($this->cursor + 1 < $this->end) && '>' === $this->code[$this->cursor + 1]) { $this->pushToken(Token::ARROW_TYPE, '=>'); $this->moveCursor('=>'); } // operators elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) { - $this->pushToken(/* Token::OPERATOR_TYPE */ 8, preg_replace('/\s+/', ' ', $match[0])); + $this->pushToken(Token::OPERATOR_TYPE, preg_replace('/\s+/', ' ', $match[0])); $this->moveCursor($match[0]); } // names elseif (preg_match(self::REGEX_NAME, $this->code, $match, 0, $this->cursor)) { - $this->pushToken(/* Token::NAME_TYPE */ 5, $match[0]); + $this->pushToken(Token::NAME_TYPE, $match[0]); $this->moveCursor($match[0]); } // numbers @@ -323,33 +349,33 @@ class Lexer if (ctype_digit($match[0]) && $number <= \PHP_INT_MAX) { $number = (int) $match[0]; // integers lower than the maximum } - $this->pushToken(/* Token::NUMBER_TYPE */ 6, $number); + $this->pushToken(Token::NUMBER_TYPE, $number); $this->moveCursor($match[0]); } // punctuation - elseif (false !== strpos(self::PUNCTUATION, $this->code[$this->cursor])) { + elseif (str_contains(self::PUNCTUATION, $this->code[$this->cursor])) { // opening bracket - if (false !== strpos('([{', $this->code[$this->cursor])) { + if (str_contains('([{', $this->code[$this->cursor])) { $this->brackets[] = [$this->code[$this->cursor], $this->lineno]; } // closing bracket - elseif (false !== strpos(')]}', $this->code[$this->cursor])) { + elseif (str_contains(')]}', $this->code[$this->cursor])) { if (empty($this->brackets)) { - throw new SyntaxError(sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); + throw new SyntaxError(\sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); } - list($expect, $lineno) = array_pop($this->brackets); + [$expect, $lineno] = array_pop($this->brackets); if ($this->code[$this->cursor] != strtr($expect, '([{', ')]}')) { - throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source); + throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source); } } - $this->pushToken(/* Token::PUNCTUATION_TYPE */ 9, $this->code[$this->cursor]); + $this->pushToken(Token::PUNCTUATION_TYPE, $this->code[$this->cursor]); ++$this->cursor; } // strings elseif (preg_match(self::REGEX_STRING, $this->code, $match, 0, $this->cursor)) { - $this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes(substr($match[0], 1, -1))); + $this->pushToken(Token::STRING_TYPE, $this->stripcslashes(substr($match[0], 1, -1), substr($match[0], 0, 1))); $this->moveCursor($match[0]); } // opening double quoted string @@ -360,10 +386,67 @@ class Lexer } // unlexable else { - throw new SyntaxError(sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); + throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); } } + private function stripcslashes(string $str, string $quoteType): string + { + $result = ''; + $length = \strlen($str); + + $i = 0; + while ($i < $length) { + if (false === $pos = strpos($str, '\\', $i)) { + $result .= substr($str, $i); + break; + } + + $result .= substr($str, $i, $pos - $i); + $i = $pos + 1; + + if ($i >= $length) { + $result .= '\\'; + break; + } + + $nextChar = $str[$i]; + + if (isset(self::SPECIAL_CHARS[$nextChar])) { + $result .= self::SPECIAL_CHARS[$nextChar]; + } elseif ('\\' === $nextChar) { + $result .= $nextChar; + } elseif ("'" === $nextChar || '"' === $nextChar) { + if ($nextChar !== $quoteType) { + trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1); + } + $result .= $nextChar; + } elseif ('#' === $nextChar && $i + 1 < $length && '{' === $str[$i + 1]) { + $result .= '#{'; + ++$i; + } elseif ('x' === $nextChar && $i + 1 < $length && ctype_xdigit($str[$i + 1])) { + $hex = $str[++$i]; + if ($i + 1 < $length && ctype_xdigit($str[$i + 1])) { + $hex .= $str[++$i]; + } + $result .= \chr(hexdec($hex)); + } elseif (ctype_digit($nextChar) && $nextChar < '8') { + $octal = $nextChar; + while ($i + 1 < $length && ctype_digit($str[$i + 1]) && $str[$i + 1] < '8' && \strlen($octal) < 3) { + $octal .= $str[++$i]; + } + $result .= \chr(octdec($octal)); + } else { + trigger_deprecation('twig/twig', '3.12', 'Character "%s" at position %d should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', $nextChar, $i + 1); + $result .= $nextChar; + } + + ++$i; + } + + return $result; + } + private function lexRawData(): void { 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 @@ -401,23 +484,23 @@ class Lexer { if (preg_match($this->regexes['interpolation_start'], $this->code, $match, 0, $this->cursor)) { $this->brackets[] = [$this->options['interpolation'][0], $this->lineno]; - $this->pushToken(/* Token::INTERPOLATION_START_TYPE */ 10); + $this->pushToken(Token::INTERPOLATION_START_TYPE); $this->moveCursor($match[0]); $this->pushState(self::STATE_INTERPOLATION); - } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && \strlen($match[0]) > 0) { - $this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes($match[0])); + } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && '' !== $match[0]) { + $this->pushToken(Token::STRING_TYPE, $this->stripcslashes($match[0], '"')); $this->moveCursor($match[0]); } elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) { - list($expect, $lineno) = array_pop($this->brackets); + [$expect, $lineno] = array_pop($this->brackets); if ('"' != $this->code[$this->cursor]) { - throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source); + throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source); } $this->popState(); ++$this->cursor; } else { // unlexable - throw new SyntaxError(sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); + throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); } } @@ -426,7 +509,7 @@ class Lexer $bracket = end($this->brackets); if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, 0, $this->cursor)) { array_pop($this->brackets); - $this->pushToken(/* Token::INTERPOLATION_END_TYPE */ 11); + $this->pushToken(Token::INTERPOLATION_END_TYPE); $this->moveCursor($match[0]); $this->popState(); } else { @@ -437,7 +520,7 @@ class Lexer private function pushToken($type, $value = ''): void { // do not push empty text tokens - if (/* Token::TEXT_TYPE */ 0 === $type && '' === $value) { + if (Token::TEXT_TYPE === $type && '' === $value) { return; } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Loader/ArrayLoader.php b/data/web/inc/lib/vendor/twig/twig/src/Loader/ArrayLoader.php index 5d726c35a..2bb54b7a8 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Loader/ArrayLoader.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Loader/ArrayLoader.php @@ -28,14 +28,12 @@ use Twig\Source; */ final class ArrayLoader implements LoaderInterface { - private $templates = []; - /** * @param array $templates An array of templates (keys are the names, and values are the source code) */ - public function __construct(array $templates = []) - { - $this->templates = $templates; + public function __construct( + private array $templates = [], + ) { } public function setTemplate(string $name, string $template): void @@ -46,7 +44,7 @@ final class ArrayLoader implements LoaderInterface public function getSourceContext(string $name): Source { if (!isset($this->templates[$name])) { - throw new LoaderError(sprintf('Template "%s" is not defined.', $name)); + throw new LoaderError(\sprintf('Template "%s" is not defined.', $name)); } return new Source($this->templates[$name], $name); @@ -60,7 +58,7 @@ final class ArrayLoader implements LoaderInterface public function getCacheKey(string $name): string { if (!isset($this->templates[$name])) { - throw new LoaderError(sprintf('Template "%s" is not defined.', $name)); + throw new LoaderError(\sprintf('Template "%s" is not defined.', $name)); } return $name.':'.$this->templates[$name]; @@ -69,7 +67,7 @@ final class ArrayLoader implements LoaderInterface public function isFresh(string $name, int $time): bool { if (!isset($this->templates[$name])) { - throw new LoaderError(sprintf('Template "%s" is not defined.', $name)); + throw new LoaderError(\sprintf('Template "%s" is not defined.', $name)); } return true; diff --git a/data/web/inc/lib/vendor/twig/twig/src/Loader/ChainLoader.php b/data/web/inc/lib/vendor/twig/twig/src/Loader/ChainLoader.php index fbf4f3a06..6e4f9511c 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Loader/ChainLoader.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Loader/ChainLoader.php @@ -21,22 +21,28 @@ use Twig\Source; */ final class ChainLoader implements LoaderInterface { + /** + * @var array + */ private $hasSourceCache = []; - private $loaders = []; /** - * @param LoaderInterface[] $loaders + * @param iterable $loaders */ - public function __construct(array $loaders = []) - { - foreach ($loaders as $loader) { - $this->addLoader($loader); - } + public function __construct( + private iterable $loaders = [], + ) { } public function addLoader(LoaderInterface $loader): void { - $this->loaders[] = $loader; + $current = $this->loaders; + + $this->loaders = (static function () use ($current, $loader): \Generator { + yield from $current; + yield $loader; + })(); + $this->hasSourceCache = []; } @@ -45,13 +51,18 @@ final class ChainLoader implements LoaderInterface */ public function getLoaders(): array { + if (!\is_array($this->loaders)) { + $this->loaders = iterator_to_array($this->loaders, false); + } + return $this->loaders; } public function getSourceContext(string $name): Source { $exceptions = []; - foreach ($this->loaders as $loader) { + + foreach ($this->getLoaders() as $loader) { if (!$loader->exists($name)) { continue; } @@ -63,7 +74,7 @@ final class ChainLoader implements LoaderInterface } } - throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); + throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); } public function exists(string $name): bool @@ -72,7 +83,7 @@ final class ChainLoader implements LoaderInterface return $this->hasSourceCache[$name]; } - foreach ($this->loaders as $loader) { + foreach ($this->getLoaders() as $loader) { if ($loader->exists($name)) { return $this->hasSourceCache[$name] = true; } @@ -84,7 +95,8 @@ final class ChainLoader implements LoaderInterface public function getCacheKey(string $name): string { $exceptions = []; - foreach ($this->loaders as $loader) { + + foreach ($this->getLoaders() as $loader) { if (!$loader->exists($name)) { continue; } @@ -96,13 +108,14 @@ final class ChainLoader implements LoaderInterface } } - throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); + throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); } public function isFresh(string $name, int $time): bool { $exceptions = []; - foreach ($this->loaders as $loader) { + + foreach ($this->getLoaders() as $loader) { if (!$loader->exists($name)) { continue; } @@ -114,6 +127,6 @@ final class ChainLoader implements LoaderInterface } } - throw new LoaderError(sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); + throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Loader/FilesystemLoader.php b/data/web/inc/lib/vendor/twig/twig/src/Loader/FilesystemLoader.php index 62267a11c..c60964f5f 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Loader/FilesystemLoader.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Loader/FilesystemLoader.php @@ -34,9 +34,9 @@ class FilesystemLoader implements LoaderInterface * @param string|array $paths A path or an array of paths where to look for templates * @param string|null $rootPath The root path common to all relative paths (null for getcwd()) */ - public function __construct($paths = [], string $rootPath = null) + public function __construct($paths = [], ?string $rootPath = null) { - $this->rootPath = (null === $rootPath ? getcwd() : $rootPath).\DIRECTORY_SEPARATOR; + $this->rootPath = ($rootPath ?? getcwd()).\DIRECTORY_SEPARATOR; if (null !== $rootPath && false !== ($realPath = realpath($rootPath))) { $this->rootPath = $realPath.\DIRECTORY_SEPARATOR; } @@ -89,7 +89,7 @@ class FilesystemLoader implements LoaderInterface $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path; if (!is_dir($checkPath)) { - throw new LoaderError(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath)); + throw new LoaderError(\sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath)); } $this->paths[$namespace][] = rtrim($path, '/\\'); @@ -105,7 +105,7 @@ class FilesystemLoader implements LoaderInterface $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path; if (!is_dir($checkPath)) { - throw new LoaderError(sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath)); + throw new LoaderError(\sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath)); } $path = rtrim($path, '/\\'); @@ -183,7 +183,7 @@ class FilesystemLoader implements LoaderInterface } try { - list($namespace, $shortname) = $this->parseName($name); + [$namespace, $shortname] = $this->parseName($name); $this->validateName($shortname); } catch (LoaderError $e) { @@ -195,7 +195,7 @@ class FilesystemLoader implements LoaderInterface } if (!isset($this->paths[$namespace])) { - $this->errorCache[$name] = sprintf('There are no registered paths for namespace "%s".', $namespace); + $this->errorCache[$name] = \sprintf('There are no registered paths for namespace "%s".', $namespace); if (!$throw) { return null; @@ -218,7 +218,7 @@ class FilesystemLoader implements LoaderInterface } } - $this->errorCache[$name] = sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace])); + $this->errorCache[$name] = \sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace])); if (!$throw) { return null; @@ -236,7 +236,7 @@ class FilesystemLoader implements LoaderInterface { if (isset($name[0]) && '@' == $name[0]) { if (false === $pos = strpos($name, '/')) { - throw new LoaderError(sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name)); + throw new LoaderError(\sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name)); } $namespace = substr($name, 1, $pos - 1); @@ -250,7 +250,7 @@ class FilesystemLoader implements LoaderInterface private function validateName(string $name): void { - if (false !== strpos($name, "\0")) { + if (str_contains($name, "\0")) { throw new LoaderError('A template name cannot contain NUL bytes.'); } @@ -265,7 +265,7 @@ class FilesystemLoader implements LoaderInterface } if ($level < 0) { - throw new LoaderError(sprintf('Looks like you try to load a template outside configured directories (%s).', $name)); + throw new LoaderError(\sprintf('Looks like you try to load a template outside configured directories (%s).', $name)); } } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Markup.php b/data/web/inc/lib/vendor/twig/twig/src/Markup.php index 1788acc4f..4fae779ee 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Markup.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Markup.php @@ -16,10 +16,10 @@ namespace Twig; * * @author Fabien Potencier */ -class Markup implements \Countable, \JsonSerializable +class Markup implements \Countable, \JsonSerializable, \Stringable { private $content; - private $charset; + private ?string $charset; public function __construct($content, $charset) { diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/AutoEscapeNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/AutoEscapeNode.php index cd970411b..ee806396e 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/AutoEscapeNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/AutoEscapeNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -24,11 +25,12 @@ use Twig\Compiler; * * @author Fabien Potencier */ +#[YieldReady] class AutoEscapeNode extends Node { - public function __construct($value, Node $body, int $lineno, string $tag = 'autoescape') + public function __construct($value, Node $body, int $lineno) { - parent::__construct(['body' => $body], ['value' => $value], $lineno, $tag); + parent::__construct(['body' => $body], ['value' => $value], $lineno); } public function compile(Compiler $compiler): void diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/BlockNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/BlockNode.php index 0632ba747..b4f939cf6 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/BlockNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/BlockNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -19,24 +20,29 @@ use Twig\Compiler; * * @author Fabien Potencier */ +#[YieldReady] class BlockNode extends Node { - public function __construct(string $name, Node $body, int $lineno, string $tag = null) + public function __construct(string $name, Node $body, int $lineno) { - parent::__construct(['body' => $body], ['name' => $name], $lineno, $tag); + parent::__construct(['body' => $body], ['name' => $name], $lineno); } public function compile(Compiler $compiler): void { $compiler ->addDebugInfo($this) - ->write(sprintf("public function block_%s(\$context, array \$blocks = [])\n", $this->getAttribute('name')), "{\n") + ->write("/**\n") + ->write(" * @return iterable\n") + ->write(" */\n") + ->write(\sprintf("public function block_%s(array \$context, array \$blocks = []): iterable\n", $this->getAttribute('name')), "{\n") ->indent() ->write("\$macros = \$this->macros;\n") ; $compiler ->subcompile($this->getNode('body')) + ->write("yield from [];\n") ->outdent() ->write("}\n\n") ; diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/BlockReferenceNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/BlockReferenceNode.php index cc8af5b52..7c313a04c 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/BlockReferenceNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/BlockReferenceNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -19,18 +20,19 @@ use Twig\Compiler; * * @author Fabien Potencier */ +#[YieldReady] class BlockReferenceNode extends Node implements NodeOutputInterface { - public function __construct(string $name, int $lineno, string $tag = null) + public function __construct(string $name, int $lineno) { - parent::__construct([], ['name' => $name], $lineno, $tag); + parent::__construct([], ['name' => $name], $lineno); } public function compile(Compiler $compiler): void { $compiler ->addDebugInfo($this) - ->write(sprintf("\$this->displayBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name'))) + ->write(\sprintf("yield from \$this->unwrap()->yieldBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name'))) ; } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/BodyNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/BodyNode.php index 041cbf685..08115b3bd 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/BodyNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/BodyNode.php @@ -11,11 +11,14 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; + /** * Represents a body node. * * @author Fabien Potencier */ +#[YieldReady] class BodyNode extends Node { } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/CaptureNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/CaptureNode.php new file mode 100644 index 000000000..3b7f0b6d8 --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/CaptureNode.php @@ -0,0 +1,57 @@ + + */ +#[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(';'); + } + } +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/CheckSecurityCallNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/CheckSecurityCallNode.php index a78a38d80..9c162d129 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/CheckSecurityCallNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/CheckSecurityCallNode.php @@ -11,17 +11,19 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** * @author Fabien Potencier */ +#[YieldReady] class CheckSecurityCallNode extends Node { public function compile(Compiler $compiler) { $compiler - ->write("\$this->sandbox = \$this->env->getExtension('\Twig\Extension\SandboxExtension');\n") + ->write("\$this->sandbox = \$this->extensions[SandboxExtension::class];\n") ->write("\$this->checkSecurity();\n") ; } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/CheckSecurityNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/CheckSecurityNode.php index 472732796..6e591aad4 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/CheckSecurityNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/CheckSecurityNode.php @@ -11,17 +11,24 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** * @author Fabien Potencier */ +#[YieldReady] class CheckSecurityNode extends Node { private $usedFilters; private $usedTags; private $usedFunctions; + /** + * @param array $usedFilters + * @param array $usedTags + * @param array $usedFunctions + */ public function __construct(array $usedFilters, array $usedTags, array $usedFunctions) { $this->usedFilters = $usedFilters; @@ -33,32 +40,22 @@ class CheckSecurityNode extends Node public function compile(Compiler $compiler): void { - $tags = $filters = $functions = []; - foreach (['tags', 'filters', 'functions'] as $type) { - foreach ($this->{'used'.ucfirst($type)} as $name => $node) { - if ($node instanceof Node) { - ${$type}[$name] = $node->getTemplateLine(); - } else { - ${$type}[$node] = null; - } - } - } - $compiler ->write("\n") ->write("public function checkSecurity()\n") ->write("{\n") ->indent() - ->write('static $tags = ')->repr(array_filter($tags))->raw(";\n") - ->write('static $filters = ')->repr(array_filter($filters))->raw(";\n") - ->write('static $functions = ')->repr(array_filter($functions))->raw(";\n\n") + ->write('static $tags = ')->repr(array_filter($this->usedTags))->raw(";\n") + ->write('static $filters = ')->repr(array_filter($this->usedFilters))->raw(";\n") + ->write('static $functions = ')->repr(array_filter($this->usedFunctions))->raw(";\n\n") ->write("try {\n") ->indent() ->write("\$this->sandbox->checkSecurity(\n") ->indent() - ->write(!$tags ? "[],\n" : "['".implode("', '", array_keys($tags))."'],\n") - ->write(!$filters ? "[],\n" : "['".implode("', '", array_keys($filters))."'],\n") - ->write(!$functions ? "[]\n" : "['".implode("', '", array_keys($functions))."']\n") + ->write(!$this->usedTags ? "[],\n" : "['".implode("', '", array_keys($this->usedTags))."'],\n") + ->write(!$this->usedFilters ? "[],\n" : "['".implode("', '", array_keys($this->usedFilters))."'],\n") + ->write(!$this->usedFunctions ? "[],\n" : "['".implode("', '", array_keys($this->usedFunctions))."'],\n") + ->write("\$this->source\n") ->outdent() ->write(");\n") ->outdent() diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/CheckToStringNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/CheckToStringNode.php index c7a9d6984..937240c1d 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/CheckToStringNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/CheckToStringNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; @@ -24,11 +25,12 @@ use Twig\Node\Expression\AbstractExpression; * * @author Fabien Potencier */ +#[YieldReady] class CheckToStringNode extends AbstractExpression { public function __construct(AbstractExpression $expr) { - parent::__construct(['expr' => $expr], [], $expr->getTemplateLine(), $expr->getNodeTag()); + parent::__construct(['expr' => $expr], [], $expr->getTemplateLine()); } public function compile(Compiler $compiler): void diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/DeprecatedNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/DeprecatedNode.php index 5ff44307f..0772adfc3 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/DeprecatedNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/DeprecatedNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; @@ -20,11 +21,12 @@ use Twig\Node\Expression\ConstantExpression; * * @author Yonel Ceruto */ +#[YieldReady] class DeprecatedNode extends Node { - public function __construct(AbstractExpression $expr, int $lineno, string $tag = null) + public function __construct(AbstractExpression $expr, int $lineno) { - parent::__construct(['expr' => $expr], [], $lineno, $tag); + parent::__construct(['expr' => $expr], [], $lineno); } public function compile(Compiler $compiler): void @@ -33,21 +35,39 @@ class DeprecatedNode extends Node $expr = $this->getNode('expr'); - if ($expr instanceof ConstantExpression) { - $compiler->write('@trigger_error(') - ->subcompile($expr); - } else { + if (!$expr instanceof ConstantExpression) { $varName = $compiler->getVarName(); - $compiler->write(sprintf('$%s = ', $varName)) + $compiler + ->write(\sprintf('$%s = ', $varName)) ->subcompile($expr) ->raw(";\n") - ->write(sprintf('@trigger_error($%s', $varName)); + ; + } + + $compiler->write('trigger_deprecation('); + if ($this->hasNode('package')) { + $compiler->subcompile($this->getNode('package')); + } else { + $compiler->raw("''"); + } + $compiler->raw(', '); + if ($this->hasNode('version')) { + $compiler->subcompile($this->getNode('version')); + } else { + $compiler->raw("''"); + } + $compiler->raw(', '); + + if ($expr instanceof ConstantExpression) { + $compiler->subcompile($expr); + } else { + $compiler->write(\sprintf('$%s', $varName)); } $compiler ->raw('.') - ->string(sprintf(' ("%s" at line %d).', $this->getTemplateName(), $this->getTemplateLine())) - ->raw(", E_USER_DEPRECATED);\n") + ->string(\sprintf(' in "%s" at line %d.', $this->getTemplateName(), $this->getTemplateLine())) + ->raw(");\n") ; } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/DoNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/DoNode.php index f7783d19f..1593fd050 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/DoNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/DoNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; @@ -19,11 +20,12 @@ use Twig\Node\Expression\AbstractExpression; * * @author Fabien Potencier */ +#[YieldReady] class DoNode extends Node { - public function __construct(AbstractExpression $expr, int $lineno, string $tag = null) + public function __construct(AbstractExpression $expr, int $lineno) { - parent::__construct(['expr' => $expr], [], $lineno, $tag); + parent::__construct(['expr' => $expr], [], $lineno); } public function compile(Compiler $compiler): void diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/EmbedNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/EmbedNode.php index 903c3f6c7..4cd3b38f2 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/EmbedNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/EmbedNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; @@ -20,12 +21,13 @@ use Twig\Node\Expression\ConstantExpression; * * @author Fabien Potencier */ +#[YieldReady] class EmbedNode extends IncludeNode { // we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module) - public function __construct(string $name, int $index, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, string $tag = null) + public function __construct(string $name, int $index, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno) { - parent::__construct(new ConstantExpression('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno, $tag); + parent::__construct(new ConstantExpression('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno); $this->setAttribute('name', $name); $this->setAttribute('index', $index); diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/AbstractExpression.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/AbstractExpression.php index 42da0559d..1692f5671 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/AbstractExpression.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/AbstractExpression.php @@ -21,4 +21,8 @@ use Twig\Node\Node; */ abstract class AbstractExpression extends Node { + public function isGenerator(): bool + { + return $this->hasAttribute('is_generator') && $this->getAttribute('is_generator'); + } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ArrayExpression.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ArrayExpression.php index 0e25fe46a..5f8b0f63f 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ArrayExpression.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ArrayExpression.php @@ -55,7 +55,7 @@ class ArrayExpression extends AbstractExpression return false; } - public function addElement(AbstractExpression $value, AbstractExpression $key = null): void + public function addElement(AbstractExpression $value, ?AbstractExpression $key = null): void { if (null === $key) { $key = new ConstantExpression(++$this->index, $value->getTemplateLine()); @@ -66,20 +66,70 @@ class ArrayExpression extends AbstractExpression public function compile(Compiler $compiler): void { + $keyValuePairs = $this->getKeyValuePairs(); + $needsArrayMergeSpread = \PHP_VERSION_ID < 80100 && $this->hasSpreadItem($keyValuePairs); + + if ($needsArrayMergeSpread) { + $compiler->raw('CoreExtension::merge('); + } $compiler->raw('['); $first = true; - foreach ($this->getKeyValuePairs() as $pair) { + $reopenAfterMergeSpread = false; + $nextIndex = 0; + foreach ($keyValuePairs as $pair) { + if ($reopenAfterMergeSpread) { + $compiler->raw(', ['); + $reopenAfterMergeSpread = false; + } + + if ($needsArrayMergeSpread && $pair['value']->hasAttribute('spread')) { + $compiler->raw('], ')->subcompile($pair['value']); + $first = true; + $reopenAfterMergeSpread = true; + continue; + } if (!$first) { $compiler->raw(', '); } $first = false; - $compiler - ->subcompile($pair['key']) - ->raw(' => ') - ->subcompile($pair['value']) - ; + if ($pair['value']->hasAttribute('spread') && !$needsArrayMergeSpread) { + $compiler->raw('...')->subcompile($pair['value']); + ++$nextIndex; + } else { + $key = $pair['key'] instanceof ConstantExpression ? $pair['key']->getAttribute('value') : null; + + if ($nextIndex !== $key) { + if (\is_int($key)) { + $nextIndex = $key + 1; + } + $compiler + ->subcompile($pair['key']) + ->raw(' => ') + ; + } else { + ++$nextIndex; + } + + $compiler->subcompile($pair['value']); + } } - $compiler->raw(']'); + if (!$reopenAfterMergeSpread) { + $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; } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ArrowFunctionExpression.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ArrowFunctionExpression.php index eaad03c9c..2bae4edd7 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ArrowFunctionExpression.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ArrowFunctionExpression.php @@ -21,9 +21,9 @@ use Twig\Node\Node; */ class ArrowFunctionExpression extends AbstractExpression { - public function __construct(AbstractExpression $expr, Node $names, $lineno, $tag = null) + public function __construct(AbstractExpression $expr, Node $names, $lineno) { - parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno, $tag); + parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno); } public function compile(Compiler $compiler): void diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/EndsWithBinary.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/EndsWithBinary.php index c3516b853..a73a5608d 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/EndsWithBinary.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/EndsWithBinary.php @@ -20,11 +20,11 @@ class EndsWithBinary extends AbstractBinary $left = $compiler->getVarName(); $right = $compiler->getVarName(); $compiler - ->raw(sprintf('(is_string($%s = ', $left)) + ->raw(\sprintf('(is_string($%s = ', $left)) ->subcompile($this->getNode('left')) - ->raw(sprintf(') && is_string($%s = ', $right)) + ->raw(\sprintf(') && is_string($%s = ', $right)) ->subcompile($this->getNode('right')) - ->raw(sprintf(') && (\'\' === $%2$s || $%2$s === substr($%1$s, -strlen($%2$s))))', $left, $right)) + ->raw(\sprintf(') && str_ends_with($%1$s, $%2$s))', $left, $right)) ; } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/EqualBinary.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/EqualBinary.php index 6b48549ef..5f423196f 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/EqualBinary.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/EqualBinary.php @@ -24,7 +24,7 @@ class EqualBinary extends AbstractBinary } $compiler - ->raw('(0 === twig_compare(') + ->raw('(0 === CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/GreaterBinary.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/GreaterBinary.php index e1dd06780..f42de3f86 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/GreaterBinary.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/GreaterBinary.php @@ -24,7 +24,7 @@ class GreaterBinary extends AbstractBinary } $compiler - ->raw('(1 === twig_compare(') + ->raw('(1 === CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/GreaterEqualBinary.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/GreaterEqualBinary.php index df9bfcfbf..0c4f43fd9 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/GreaterEqualBinary.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/GreaterEqualBinary.php @@ -24,7 +24,7 @@ class GreaterEqualBinary extends AbstractBinary } $compiler - ->raw('(0 <= twig_compare(') + ->raw('(0 <= CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/HasEveryBinary.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/HasEveryBinary.php new file mode 100644 index 000000000..c57bb20e9 --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/HasEveryBinary.php @@ -0,0 +1,33 @@ +raw('CoreExtension::arrayEvery($this->env, ') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(')') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw(''); + } +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/HasSomeBinary.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/HasSomeBinary.php new file mode 100644 index 000000000..12293f84c --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/HasSomeBinary.php @@ -0,0 +1,33 @@ +raw('CoreExtension::arraySome($this->env, ') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(')') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw(''); + } +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/InBinary.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/InBinary.php index 6dbfa97f0..68a98fe15 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/InBinary.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/InBinary.php @@ -18,7 +18,7 @@ class InBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('twig_in_filter(') + ->raw('CoreExtension::inFilter(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/LessBinary.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/LessBinary.php index 598e62913..fb3264a2d 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/LessBinary.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/LessBinary.php @@ -24,7 +24,7 @@ class LessBinary extends AbstractBinary } $compiler - ->raw('(-1 === twig_compare(') + ->raw('(-1 === CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/LessEqualBinary.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/LessEqualBinary.php index e3c4af58d..8f3653892 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/LessEqualBinary.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/LessEqualBinary.php @@ -24,7 +24,7 @@ class LessEqualBinary extends AbstractBinary } $compiler - ->raw('(0 >= twig_compare(') + ->raw('(0 >= CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/MatchesBinary.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/MatchesBinary.php index bc97292cd..4669044e0 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/MatchesBinary.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/MatchesBinary.php @@ -18,7 +18,7 @@ class MatchesBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('preg_match(') + ->raw('CoreExtension::matches(') ->subcompile($this->getNode('right')) ->raw(', ') ->subcompile($this->getNode('left')) diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/NotEqualBinary.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/NotEqualBinary.php index db47a2890..d137ef627 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/NotEqualBinary.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/NotEqualBinary.php @@ -24,7 +24,7 @@ class NotEqualBinary extends AbstractBinary } $compiler - ->raw('(0 !== twig_compare(') + ->raw('(0 !== CoreExtension::compare(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/NotInBinary.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/NotInBinary.php index fcba6cca1..80c8755d8 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/NotInBinary.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/NotInBinary.php @@ -18,7 +18,7 @@ class NotInBinary extends AbstractBinary public function compile(Compiler $compiler): void { $compiler - ->raw('!twig_in_filter(') + ->raw('!CoreExtension::inFilter(') ->subcompile($this->getNode('left')) ->raw(', ') ->subcompile($this->getNode('right')) diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/StartsWithBinary.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/StartsWithBinary.php index d0df1c4b6..4519f30d9 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/StartsWithBinary.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Binary/StartsWithBinary.php @@ -20,11 +20,11 @@ class StartsWithBinary extends AbstractBinary $left = $compiler->getVarName(); $right = $compiler->getVarName(); $compiler - ->raw(sprintf('(is_string($%s = ', $left)) + ->raw(\sprintf('(is_string($%s = ', $left)) ->subcompile($this->getNode('left')) - ->raw(sprintf(') && is_string($%s = ', $right)) + ->raw(\sprintf(') && is_string($%s = ', $right)) ->subcompile($this->getNode('right')) - ->raw(sprintf(') && (\'\' === $%2$s || 0 === strpos($%1$s, $%2$s)))', $left, $right)) + ->raw(\sprintf(') && str_starts_with($%1$s, $%2$s))', $left, $right)) ; } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/BlockReferenceExpression.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/BlockReferenceExpression.php index b1e2a8f7b..acd231e15 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/BlockReferenceExpression.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/BlockReferenceExpression.php @@ -22,14 +22,14 @@ use Twig\Node\Node; */ 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]; if (null !== $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 @@ -40,8 +40,9 @@ class BlockReferenceExpression extends AbstractExpression if ($this->getAttribute('output')) { $compiler->addDebugInfo($this); + $compiler->write('yield from '); $this - ->compileTemplateCall($compiler, 'displayBlock') + ->compileTemplateCall($compiler, 'yieldBlock') ->raw(";\n"); } else { $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); } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/CallExpression.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/CallExpression.php index 28881066b..6fc6f66e0 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/CallExpression.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/CallExpression.php @@ -15,40 +15,49 @@ use Twig\Compiler; use Twig\Error\SyntaxError; use Twig\Extension\ExtensionInterface; 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 { - private $reflector; + private $reflector = null; 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); } else { - [$r, $callable] = $this->reflectCallable($callable); + $rc = $this->reflectCallable($twigCallable); + $r = $rc->getReflector(); + $callable = $rc->getCallable(); if (\is_string($callable)) { $compiler->raw($callable); } elseif (\is_array($callable) && \is_string($callable[0])) { 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 { - $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) { $class = \get_class($callable[0]); if (!$compiler->getEnvironment()->hasExtension($class)) { // 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 { - $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 { - $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 { + if (\func_num_args() >= 2) { + trigger_deprecation('twig/twig', '3.11', 'Passing a second argument to "%s()" is deprecated.', __METHOD__); + } + $compiler->raw($isArray ? '[' : '('); $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'); $first = false; } - if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) { + if ($twigCallable->needsContext()) { if (!$first) { $compiler->raw(', '); } @@ -74,14 +97,12 @@ abstract class CallExpression extends AbstractExpression $first = false; } - if ($this->hasAttribute('arguments')) { - foreach ($this->getAttribute('arguments') as $argument) { - if (!$first) { - $compiler->raw(', '); - } - $compiler->string($argument); - $first = false; + foreach ($twigCallable->getArguments() as $argument) { + if (!$first) { + $compiler->raw(', '); } + $compiler->string($argument); + $first = false; } if ($this->hasNode('node')) { @@ -93,8 +114,7 @@ abstract class CallExpression extends AbstractExpression } if ($this->hasNode('arguments')) { - $callable = $this->getAttribute('callable'); - $arguments = $this->getArguments($callable, $this->getNode('arguments')); + $arguments = (new CallableArgumentsExtractor($this, $this->getTwigCallable()))->extractArguments($this->getNode('arguments')); foreach ($arguments as $node) { if (!$first) { $compiler->raw(', '); @@ -107,8 +127,13 @@ abstract class CallExpression extends AbstractExpression $compiler->raw($isArray ? ']' : ')'); } + /** + * @deprecated since 3.12, use Twig\Util\CallableArgumentsExtractor::getArguments() instead + */ 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'); $callName = $this->getAttribute('name'); @@ -119,28 +144,28 @@ abstract class CallExpression extends AbstractExpression $named = true; $name = $this->normalizeName($name); } 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; } - $isVariadic = $this->hasAttribute('is_variadic') && $this->getAttribute('is_variadic'); + $isVariadic = $this->getAttribute('twig_callable')->isVariadic(); if (!$named && !$isVariadic) { return $parameters; } if (!$callable) { 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 { - $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); } - list($callableParameters, $isPhpVariadic) = $this->getCallableParameters($callable, $isVariadic); + [$callableParameters, $isPhpVariadic] = $this->getCallableParameters($callable, $isVariadic); $arguments = []; $names = []; $missingArguments = []; @@ -160,11 +185,11 @@ abstract class CallExpression extends AbstractExpression if (\array_key_exists($name, $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)) { - 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".', $name, $callType, $callName, implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) ), $this->getTemplateLine(), $this->getSourceContext()); @@ -189,7 +214,7 @@ abstract class CallExpression extends AbstractExpression $missingArguments[] = $name; } } 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( - sprintf( + \sprintf( 'Unknown argument%s "%s" for %s "%s(%s)".', \count($parameters) > 1 ? 's' : '', implode('", "', array_keys($parameters)), $callType, $callName, implode(', ', $names) ), @@ -232,88 +257,106 @@ abstract class CallExpression extends AbstractExpression return $arguments; } + /** + * @deprecated since 3.12 + */ 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)); } + // To be removed in 4.0 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(); if ($this->hasNode('node')) { array_shift($parameters); } - if ($this->hasAttribute('needs_environment') && $this->getAttribute('needs_environment')) { + if ($twigCallable->needsCharset()) { array_shift($parameters); } - if ($this->hasAttribute('needs_context') && $this->getAttribute('needs_context')) { + if ($twigCallable->needsEnvironment()) { array_shift($parameters); } - if ($this->hasAttribute('arguments') && null !== $this->getAttribute('arguments')) { - foreach ($this->getAttribute('arguments') as $argument) { - array_shift($parameters); - } + if ($twigCallable->needsContext()) { + array_shift($parameters); } + foreach ($twigCallable->getArguments() as $argument) { + array_shift($parameters); + } + $isPhpVariadic = false; if ($isVariadic) { $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()) { array_pop($parameters); } elseif ($argument && $argument->isVariadic()) { array_pop($parameters); $isPhpVariadic = true; } 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]; } - private function reflectCallable($callable) + private function reflectCallable(TwigCallableInterface $callable): ReflectionCallable { - if (null !== $this->reflector) { - return $this->reflector; + if (!$this->reflector) { + $this->reflector = new ReflectionCallable($callable); } - if (\is_string($callable) && false !== $pos = strpos($callable, '::')) { - $callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)]; - } + return $this->reflector; + } - if (\is_array($callable) && method_exists($callable[0], $callable[1])) { - $r = new \ReflectionMethod($callable[0], $callable[1]); + /** + * 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'); - return $this->reflector = [$r, $callable, $r->class.'::'.$r->name]; - } + $this->setAttribute('twig_callable', match ($this->getAttribute('type')) { + '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()), + }); - $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]; + return $this->getAttribute('twig_callable'); } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ConditionalExpression.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ConditionalExpression.php index 2c7bd0a27..d7db99357 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ConditionalExpression.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ConditionalExpression.php @@ -23,14 +23,23 @@ class ConditionalExpression extends AbstractExpression public function compile(Compiler $compiler): void { - $compiler - ->raw('((') - ->subcompile($this->getNode('expr1')) - ->raw(') ? (') - ->subcompile($this->getNode('expr2')) - ->raw(') : (') - ->subcompile($this->getNode('expr3')) - ->raw('))') - ; + // 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 + ->raw('((') + ->subcompile($this->getNode('expr1')) + ->raw(') ? (') + ->subcompile($this->getNode('expr2')) + ->raw(') : (') + ->subcompile($this->getNode('expr3')) + ->raw('))'); + } } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ConstantExpression.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ConstantExpression.php index 7ddbcc6fa..2a8909d54 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ConstantExpression.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ConstantExpression.php @@ -14,6 +14,9 @@ namespace Twig\Node\Expression; use Twig\Compiler; +/** + * @final + */ class ConstantExpression extends AbstractExpression { public function __construct($value, int $lineno) diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Filter/DefaultFilter.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Filter/DefaultFilter.php index 6a572d488..75b6d18c2 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Filter/DefaultFilter.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Filter/DefaultFilter.php @@ -11,7 +11,9 @@ namespace Twig\Node\Expression\Filter; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; +use Twig\Extension\CoreExtension; use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; @@ -19,6 +21,8 @@ use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\Test\DefinedTest; use Twig\Node\Node; +use Twig\TwigFilter; +use Twig\TwigTest; /** * Returns the value or the default value when it is undefined or empty. @@ -29,20 +33,27 @@ use Twig\Node\Node; */ class DefaultFilter extends FilterExpression { - public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, string $tag = null) + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno) { - $default = new FilterExpression($node, new ConstantExpression('default', $node->getTemplateLine()), $arguments, $node->getTemplateLine()); + if ($filter instanceof TwigFilter) { + $name = $filter->getName(); + $default = new FilterExpression($node, $filter, $arguments, $node->getTemplateLine()); + } else { + $name = $filter->getAttribute('value'); + $default = new FilterExpression($node, new TwigFilter('default', [CoreExtension::class, 'default']), $arguments, $node->getTemplateLine()); + } - if ('default' === $filterName->getAttribute('value') && ($node instanceof NameExpression || $node instanceof GetAttrExpression)) { - $test = new DefinedTest(clone $node, 'defined', new Node(), $node->getTemplateLine()); - $false = \count($arguments) ? $arguments->getNode(0) : new ConstantExpression('', $node->getTemplateLine()); + if ('default' === $name && ($node instanceof NameExpression || $node instanceof GetAttrExpression)) { + $test = new DefinedTest(clone $node, new TwigTest('defined'), new Node(), $node->getTemplateLine()); + $false = \count($arguments) ? $arguments->getNode('0') : new ConstantExpression('', $node->getTemplateLine()); $node = new ConditionalExpression($test, $default, $false, $node->getTemplateLine()); } else { $node = $default; } - parent::__construct($node, $filterName, $arguments, $lineno, $tag); + parent::__construct($node, $filter, $arguments, $lineno); } public function compile(Compiler $compiler): void diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Filter/RawFilter.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Filter/RawFilter.php new file mode 100644 index 000000000..e115ab194 --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Filter/RawFilter.php @@ -0,0 +1,36 @@ + + */ +class RawFilter extends FilterExpression +{ + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigFilter|ConstantExpression|null $filter = null, ?Node $arguments = null, int $lineno = 0) + { + parent::__construct($node, $filter ?: new TwigFilter('raw', null, ['is_safe' => ['all']]), $arguments ?: new Node(), $lineno ?: $node->getTemplateLine()); + } + + public function compile(Compiler $compiler): void + { + $compiler->subcompile($this->getNode('node')); + } +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/FilterExpression.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/FilterExpression.php index 0fc158869..efc91193e 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/FilterExpression.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/FilterExpression.php @@ -12,28 +12,61 @@ namespace Twig\Node\Expression; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; +use Twig\Node\NameDeprecation; use Twig\Node\Node; +use Twig\TwigFilter; class FilterExpression extends CallExpression { - public function __construct(Node $node, ConstantExpression $filterName, Node $arguments, int $lineno, string $tag = null) + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno) { - parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], [], $lineno, $tag); + if ($filter instanceof TwigFilter) { + $name = $filter->getName(); + $filterName = new ConstantExpression($name, $lineno); + } else { + $name = $filter->getAttribute('value'); + $filterName = $filter; + trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigFilter" when creating a "%s" filter of type "%s" is deprecated.', $name, static::class); + } + + parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], ['name' => $name, 'type' => 'filter'], $lineno); + + if ($filter instanceof TwigFilter) { + $this->setAttribute('twig_callable', $filter); + } + + $this->deprecateNode('filter', new NameDeprecation('twig/twig', '3.12')); + + $this->deprecateAttribute('needs_charset', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('needs_environment', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('needs_context', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('arguments', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('callable', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('is_variadic', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12')); } public function compile(Compiler $compiler): void { - $name = $this->getNode('filter')->getAttribute('value'); - $filter = $compiler->getEnvironment()->getFilter($name); + $name = $this->getNode('filter', false)->getAttribute('value'); + if ($name !== $this->getAttribute('name')) { + trigger_deprecation('twig/twig', '3.11', 'Changing the value of a "filter" node in a NodeVisitor class is not supported anymore.'); + $this->removeAttribute('twig_callable'); + } + if ('raw' === $name) { + trigger_deprecation('twig/twig', '3.11', 'Creating the "raw" filter via "FilterExpression" is deprecated; use "RawFilter" instead.'); - $this->setAttribute('name', $name); - $this->setAttribute('type', 'filter'); - $this->setAttribute('needs_environment', $filter->needsEnvironment()); - $this->setAttribute('needs_context', $filter->needsContext()); - $this->setAttribute('arguments', $filter->getArguments()); - $this->setAttribute('callable', $filter->getCallable()); - $this->setAttribute('is_variadic', $filter->isVariadic()); + $compiler->subcompile($this->getNode('node')); + + return; + } + + if (!$this->hasAttribute('twig_callable')) { + $this->setAttribute('twig_callable', $compiler->getEnvironment()->getFilter($name)); + } $this->compileCallable($compiler); } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/FunctionExpression.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/FunctionExpression.php index 71269775c..6215c6abf 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/FunctionExpression.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/FunctionExpression.php @@ -11,32 +11,57 @@ namespace Twig\Node\Expression; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; +use Twig\Node\NameDeprecation; use Twig\Node\Node; +use Twig\TwigFunction; class FunctionExpression extends CallExpression { - public function __construct(string $name, Node $arguments, int $lineno) + #[FirstClassTwigCallableReady] + public function __construct(TwigFunction|string $function, Node $arguments, int $lineno) { - parent::__construct(['arguments' => $arguments], ['name' => $name, 'is_defined_test' => false], $lineno); + if ($function instanceof TwigFunction) { + $name = $function->getName(); + } else { + $name = $function; + trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigFunction" when creating a "%s" function of type "%s" is deprecated.', $name, static::class); + } + + parent::__construct(['arguments' => $arguments], ['name' => $name, 'type' => 'function', 'is_defined_test' => false], $lineno); + + if ($function instanceof TwigFunction) { + $this->setAttribute('twig_callable', $function); + } + + $this->deprecateAttribute('needs_charset', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('needs_environment', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('needs_context', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('arguments', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('callable', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('is_variadic', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12')); } public function compile(Compiler $compiler) { $name = $this->getAttribute('name'); - $function = $compiler->getEnvironment()->getFunction($name); - - $this->setAttribute('name', $name); - $this->setAttribute('type', 'function'); - $this->setAttribute('needs_environment', $function->needsEnvironment()); - $this->setAttribute('needs_context', $function->needsContext()); - $this->setAttribute('arguments', $function->getArguments()); - $callable = $function->getCallable(); - if ('constant' === $name && $this->getAttribute('is_defined_test')) { - $callable = 'twig_constant_is_defined'; + if ($this->hasAttribute('twig_callable')) { + $name = $this->getAttribute('twig_callable')->getName(); + if ($name !== $this->getAttribute('name')) { + trigger_deprecation('twig/twig', '3.12', 'Changing the value of a "function" node in a NodeVisitor class is not supported anymore.'); + $this->removeAttribute('twig_callable'); + } + } + + if (!$this->hasAttribute('twig_callable')) { + $this->setAttribute('twig_callable', $compiler->getEnvironment()->getFunction($name)); + } + + if ('constant' === $name && $this->getAttribute('is_defined_test')) { + $this->getNode('arguments')->setNode('checkDefined', new ConstantExpression(true, $this->getTemplateLine())); } - $this->setAttribute('callable', $callable); - $this->setAttribute('is_variadic', $function->isVariadic()); $this->compileCallable($compiler); } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php new file mode 100644 index 000000000..7e5c25ff4 --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php @@ -0,0 +1,41 @@ +getNode('arguments'); + if ($arguments->hasNode('enum')) { + $firstArgument = $arguments->getNode('enum'); + } elseif ($arguments->hasNode('0')) { + $firstArgument = $arguments->getNode('0'); + } else { + $firstArgument = null; + } + + if (!$firstArgument instanceof ConstantExpression || 1 !== \count($arguments)) { + parent::compile($compiler); + + return; + } + + $value = $firstArgument->getAttribute('value'); + + if (!\is_string($value)) { + throw new SyntaxError('The first argument of the "enum_cases" function must be a string.', $this->getTemplateLine(), $this->getSourceContext()); + } + + if (!enum_exists($value)) { + throw new SyntaxError(\sprintf('The first argument of the "enum_cases" function must be the name of an enum, "%s" given.', $value), $this->getTemplateLine(), $this->getSourceContext()); + } + + $compiler->raw(\sprintf('%s::cases()', $value)); + } +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/GetAttrExpression.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/GetAttrExpression.php index e6a75ce94..29a446b88 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/GetAttrExpression.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/GetAttrExpression.php @@ -57,7 +57,7 @@ class GetAttrExpression extends AbstractExpression return; } - $compiler->raw('twig_get_attribute($this->env, $this->source, '); + $compiler->raw('CoreExtension::getAttribute($this->env, $this->source, '); if ($this->getAttribute('ignore_strict_check')) { $this->getNode('node')->setAttribute('ignore_strict_check', true); diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/InlinePrint.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/InlinePrint.php index 1ad4751e4..0a3c2e4f9 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/InlinePrint.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/InlinePrint.php @@ -27,9 +27,8 @@ final class InlinePrint extends AbstractExpression public function compile(Compiler $compiler): void { $compiler - ->raw('print (') + ->raw('yield ') ->subcompile($this->getNode('node')) - ->raw(')') ; } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/MethodCallExpression.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/MethodCallExpression.php index d5ec0b6ef..01806f91d 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/MethodCallExpression.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/MethodCallExpression.php @@ -39,14 +39,16 @@ class MethodCallExpression extends AbstractExpression } $compiler - ->raw('twig_call_macro($macros[') + ->raw('CoreExtension::callMacro($macros[') ->repr($this->getNode('node')->getAttribute('name')) ->raw('], ') ->repr($this->getAttribute('method')) ->raw(', [') ; $first = true; - foreach ($this->getNode('arguments')->getKeyValuePairs() as $pair) { + /** @var ArrayExpression */ + $args = $this->getNode('arguments'); + foreach ($args->getKeyValuePairs() as $pair) { if (!$first) { $compiler->raw(', '); } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/NameExpression.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/NameExpression.php index c3563f012..286aa5ae2 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/NameExpression.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/NameExpression.php @@ -34,7 +34,7 @@ class NameExpression extends AbstractExpression $compiler->addDebugInfo($this); if ($this->getAttribute('is_defined_test')) { - if ($this->isSpecial()) { + if (isset($this->specialVars[$name])) { $compiler->repr(true); } elseif (\PHP_VERSION_ID >= 70400) { $compiler @@ -51,7 +51,7 @@ class NameExpression extends AbstractExpression ->raw(', $context))') ; } - } elseif ($this->isSpecial()) { + } elseif (isset($this->specialVars[$name])) { $compiler->raw($this->specialVars[$name]); } elseif ($this->getAttribute('always_defined')) { $compiler @@ -85,13 +85,23 @@ class NameExpression extends AbstractExpression } } + /** + * @deprecated since Twig 3.11 (to be removed in 4.0) + */ public function isSpecial() { + trigger_deprecation('twig/twig', '3.11', 'The "%s()" method is deprecated and will be removed in Twig 4.0.', __METHOD__); + return isset($this->specialVars[$this->getAttribute('name')]); } + /** + * @deprecated since Twig 3.11 (to be removed in 4.0) + */ public function isSimple() { + trigger_deprecation('twig/twig', '3.11', 'The "%s()" method is deprecated and will be removed in Twig 4.0.', __METHOD__); + return !$this->isSpecial() && !$this->getAttribute('is_defined_test'); } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/NullCoalesceExpression.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/NullCoalesceExpression.php index a72bc4fc6..98630f7f0 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/NullCoalesceExpression.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/NullCoalesceExpression.php @@ -17,17 +17,18 @@ use Twig\Node\Expression\Test\DefinedTest; use Twig\Node\Expression\Test\NullTest; use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Node; +use Twig\TwigTest; class NullCoalesceExpression extends ConditionalExpression { public function __construct(Node $left, Node $right, int $lineno) { - $test = new DefinedTest(clone $left, 'defined', new Node(), $left->getTemplateLine()); + $test = new DefinedTest(clone $left, new TwigTest('defined'), new Node(), $left->getTemplateLine()); // for "block()", we don't need the null test as the return value is always a string if (!$left instanceof BlockReferenceExpression) { $test = new AndBinary( $test, - new NotUnary(new NullTest($left, 'null', new Node(), $left->getTemplateLine()), $left->getTemplateLine()), + new NotUnary(new NullTest($left, new TwigTest('null'), new Node(), $left->getTemplateLine()), $left->getTemplateLine()), $left->getTemplateLine() ); } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ParentExpression.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ParentExpression.php index 254919718..22fe38f6a 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ParentExpression.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/ParentExpression.php @@ -21,9 +21,9 @@ use Twig\Compiler; */ class ParentExpression extends AbstractExpression { - public function __construct(string $name, int $lineno, string $tag = null) + public function __construct(string $name, int $lineno) { - parent::__construct([], ['output' => false, 'name' => $name], $lineno, $tag); + parent::__construct([], ['output' => false, 'name' => $name], $lineno); } public function compile(Compiler $compiler): void @@ -31,7 +31,7 @@ class ParentExpression extends AbstractExpression if ($this->getAttribute('output')) { $compiler ->addDebugInfo($this) - ->write('$this->displayParentBlock(') + ->write('yield from $this->yieldParentBlock(') ->string($this->getAttribute('name')) ->raw(", \$context, \$blocks);\n") ; diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Test/ConstantTest.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Test/ConstantTest.php index 57e9319d5..867fd0951 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Test/ConstantTest.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Test/ConstantTest.php @@ -33,16 +33,16 @@ class ConstantTest extends TestExpression ->raw(' === constant(') ; - if ($this->getNode('arguments')->hasNode(1)) { + if ($this->getNode('arguments')->hasNode('1')) { $compiler ->raw('get_class(') - ->subcompile($this->getNode('arguments')->getNode(1)) + ->subcompile($this->getNode('arguments')->getNode('1')) ->raw(')."::".') ; } $compiler - ->subcompile($this->getNode('arguments')->getNode(0)) + ->subcompile($this->getNode('arguments')->getNode('0')) ->raw('))') ; } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Test/DefinedTest.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Test/DefinedTest.php index 3953bbbe2..24d3ee82c 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Test/DefinedTest.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Test/DefinedTest.php @@ -11,6 +11,7 @@ namespace Twig\Node\Expression\Test; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; use Twig\Error\SyntaxError; use Twig\Node\Expression\ArrayExpression; @@ -22,6 +23,7 @@ use Twig\Node\Expression\MethodCallExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\TestExpression; use Twig\Node\Node; +use Twig\TwigTest; /** * Checks if a variable is defined in the current context. @@ -35,7 +37,8 @@ use Twig\Node\Node; */ class DefinedTest extends TestExpression { - public function __construct(Node $node, string $name, ?Node $arguments, int $lineno) + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigTest|string $name, ?Node $arguments, int $lineno) { if ($node instanceof NameExpression) { $node->setAttribute('is_defined_test', true); @@ -54,6 +57,10 @@ class DefinedTest extends TestExpression throw new SyntaxError('The "defined" test only works with simple variables.', $lineno); } + if (\is_string($name) && 'defined' !== $name) { + trigger_deprecation('twig/twig', '3.12', 'Creating a "DefinedTest" instance with a test name that is not "defined" is deprecated.'); + } + parent::__construct($node, $name, $arguments, $lineno); } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Test/DivisiblebyTest.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Test/DivisiblebyTest.php index 4cb3ee096..90d58a49a 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Test/DivisiblebyTest.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Test/DivisiblebyTest.php @@ -29,7 +29,7 @@ class DivisiblebyTest extends TestExpression ->raw('(0 == ') ->subcompile($this->getNode('node')) ->raw(' % ') - ->subcompile($this->getNode('arguments')->getNode(0)) + ->subcompile($this->getNode('arguments')->getNode('0')) ->raw(')') ; } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Test/SameasTest.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Test/SameasTest.php index c96d2bc01..f1e24db6f 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Test/SameasTest.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/Test/SameasTest.php @@ -27,7 +27,7 @@ class SameasTest extends TestExpression ->raw('(') ->subcompile($this->getNode('node')) ->raw(' === ') - ->subcompile($this->getNode('arguments')->getNode(0)) + ->subcompile($this->getNode('arguments')->getNode('0')) ->raw(')') ; } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/TestExpression.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/TestExpression.php index e518bd8f1..080d85aaa 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/TestExpression.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Expression/TestExpression.php @@ -11,31 +11,55 @@ namespace Twig\Node\Expression; +use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Compiler; +use Twig\Node\NameDeprecation; use Twig\Node\Node; +use Twig\TwigTest; class TestExpression extends CallExpression { - public function __construct(Node $node, string $name, ?Node $arguments, int $lineno) + #[FirstClassTwigCallableReady] + public function __construct(Node $node, string|TwigTest $test, ?Node $arguments, int $lineno) { $nodes = ['node' => $node]; if (null !== $arguments) { $nodes['arguments'] = $arguments; } - parent::__construct($nodes, ['name' => $name], $lineno); + if ($test instanceof TwigTest) { + $name = $test->getName(); + } else { + $name = $test; + trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigTest" when creating a "%s" test of type "%s" is deprecated.', $name, static::class); + } + + parent::__construct($nodes, ['name' => $name, 'type' => 'test'], $lineno); + + if ($test instanceof TwigTest) { + $this->setAttribute('twig_callable', $test); + } + + $this->deprecateAttribute('arguments', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('callable', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('is_variadic', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12')); } public function compile(Compiler $compiler): void { $name = $this->getAttribute('name'); - $test = $compiler->getEnvironment()->getTest($name); + if ($this->hasAttribute('twig_callable')) { + $name = $this->getAttribute('twig_callable')->getName(); + if ($name !== $this->getAttribute('name')) { + trigger_deprecation('twig/twig', '3.12', 'Changing the value of a "test" node in a NodeVisitor class is not supported anymore.'); + $this->removeAttribute('twig_callable'); + } + } - $this->setAttribute('name', $name); - $this->setAttribute('type', 'test'); - $this->setAttribute('arguments', $test->getArguments()); - $this->setAttribute('callable', $test->getCallable()); - $this->setAttribute('is_variadic', $test->isVariadic()); + if (!$this->hasAttribute('twig_callable')) { + $this->setAttribute('twig_callable', $compiler->getEnvironment()->getTest($this->getAttribute('name'))); + } $this->compileCallable($compiler); } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/FlushNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/FlushNode.php index fa50a88ee..ff3bd1cf1 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/FlushNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/FlushNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,18 +19,22 @@ use Twig\Compiler; * * @author Fabien Potencier */ +#[YieldReady] class FlushNode extends Node { - public function __construct(int $lineno, string $tag) + public function __construct(int $lineno) { - parent::__construct([], [], $lineno, $tag); + parent::__construct([], [], $lineno); } public function compile(Compiler $compiler): void { - $compiler - ->addDebugInfo($this) - ->write("flush();\n") - ; + $compiler->addDebugInfo($this); + + if ($compiler->getEnvironment()->useYield()) { + $compiler->write("yield '';\n"); + } + + $compiler->write("flush();\n"); } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/ForLoopNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/ForLoopNode.php index d5ce845a7..1f0a4f321 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/ForLoopNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/ForLoopNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,11 +19,12 @@ use Twig\Compiler; * * @author Fabien Potencier */ +#[YieldReady] class ForLoopNode extends Node { - public function __construct(int $lineno, string $tag = null) + public function __construct(int $lineno) { - parent::__construct([], ['with_loop' => false, 'ifexpr' => false, 'else' => false], $lineno, $tag); + parent::__construct([], ['with_loop' => false, 'ifexpr' => false, 'else' => false], $lineno); } public function compile(Compiler $compiler): void @@ -36,7 +38,7 @@ class ForLoopNode extends Node ->write("++\$context['loop']['index0'];\n") ->write("++\$context['loop']['index'];\n") ->write("\$context['loop']['first'] = false;\n") - ->write("if (isset(\$context['loop']['length'])) {\n") + ->write("if (isset(\$context['loop']['revindex0'], \$context['loop']['revindex'])) {\n") ->indent() ->write("--\$context['loop']['revindex0'];\n") ->write("--\$context['loop']['revindex'];\n") diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/ForNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/ForNode.php index 04addfbfe..2fc014792 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/ForNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/ForNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\AssignNameExpression; @@ -21,20 +22,21 @@ use Twig\Node\Expression\AssignNameExpression; * * @author Fabien Potencier */ +#[YieldReady] class ForNode extends Node { private $loop; - public function __construct(AssignNameExpression $keyTarget, AssignNameExpression $valueTarget, AbstractExpression $seq, ?Node $ifexpr, Node $body, ?Node $else, int $lineno, string $tag = null) + public function __construct(AssignNameExpression $keyTarget, AssignNameExpression $valueTarget, AbstractExpression $seq, ?Node $ifexpr, Node $body, ?Node $else, int $lineno) { - $body = new Node([$body, $this->loop = new ForLoopNode($lineno, $tag)]); + $body = new Node([$body, $this->loop = new ForLoopNode($lineno)]); $nodes = ['key_target' => $keyTarget, 'value_target' => $valueTarget, 'seq' => $seq, 'body' => $body]; if (null !== $else) { $nodes['else'] = $else; } - parent::__construct($nodes, ['with_loop' => true], $lineno, $tag); + parent::__construct($nodes, ['with_loop' => true], $lineno); } public function compile(Compiler $compiler): void @@ -42,7 +44,7 @@ class ForNode extends Node $compiler ->addDebugInfo($this) ->write("\$context['_parent'] = \$context;\n") - ->write("\$context['_seq'] = twig_ensure_traversable(") + ->write("\$context['_seq'] = CoreExtension::ensureTraversable(") ->subcompile($this->getNode('seq')) ->raw(");\n") ; @@ -99,7 +101,14 @@ class ForNode extends Node $compiler->write("\$_parent = \$context['_parent'];\n"); // remove some "private" loop variables (needed for nested loops) - $compiler->write('unset($context[\'_seq\'], $context[\'_iterated\'], $context[\''.$this->getNode('key_target')->getAttribute('name').'\'], $context[\''.$this->getNode('value_target')->getAttribute('name').'\'], $context[\'_parent\'], $context[\'loop\']);'."\n"); + $compiler->write('unset($context[\'_seq\'], $context[\''.$this->getNode('key_target')->getAttribute('name').'\'], $context[\''.$this->getNode('value_target')->getAttribute('name').'\'], $context[\'_parent\']'); + if ($this->hasNode('else')) { + $compiler->raw(', $context[\'_iterated\']'); + } + if ($this->getAttribute('with_loop')) { + $compiler->raw(', $context[\'loop\']'); + } + $compiler->raw(");\n"); // keep the values set in the inner context for variables defined in the outer context $compiler->write("\$context = array_intersect_key(\$context, \$_parent) + \$_parent;\n"); diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/IfNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/IfNode.php index 5fa20082a..2af48fa81 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/IfNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/IfNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -19,16 +20,17 @@ use Twig\Compiler; * * @author Fabien Potencier */ +#[YieldReady] class IfNode extends Node { - public function __construct(Node $tests, ?Node $else, int $lineno, string $tag = null) + public function __construct(Node $tests, ?Node $else, int $lineno) { $nodes = ['tests' => $tests]; if (null !== $else) { $nodes['else'] = $else; } - parent::__construct($nodes, [], $lineno, $tag); + parent::__construct($nodes, [], $lineno); } public function compile(Compiler $compiler): void @@ -47,11 +49,14 @@ class IfNode extends Node } $compiler - ->subcompile($this->getNode('tests')->getNode($i)) + ->subcompile($this->getNode('tests')->getNode((string) $i)) ->raw(") {\n") ->indent() - ->subcompile($this->getNode('tests')->getNode($i + 1)) ; + // The node might not exists if the content is empty + if ($this->getNode('tests')->hasNode((string) ($i + 1))) { + $compiler->subcompile($this->getNode('tests')->getNode((string) ($i + 1))); + } } if ($this->hasNode('else')) { diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/ImportNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/ImportNode.php index 5378d799e..9a6033f21 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/ImportNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/ImportNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\NameExpression; @@ -20,11 +21,22 @@ use Twig\Node\Expression\NameExpression; * * @author Fabien Potencier */ +#[YieldReady] class ImportNode extends Node { - public function __construct(AbstractExpression $expr, AbstractExpression $var, int $lineno, string $tag = null, bool $global = true) + /** + * @param bool $global + */ + public function __construct(AbstractExpression $expr, AbstractExpression $var, int $lineno, $global = true) { - parent::__construct(['expr' => $expr, 'var' => $var], ['global' => $global], $lineno, $tag); + if (null === $global || \is_string($global)) { + trigger_deprecation('twig/twig', '3.12', 'Passing a tag to %s() is deprecated.', __METHOD__); + $global = \func_num_args() > 4 ? func_get_arg(4) : true; + } elseif (!\is_bool($global)) { + throw new \TypeError(\sprintf('Argument 4 passed to "%s()" must be a boolean, "%s" given.', __METHOD__, get_debug_type($global))); + } + + parent::__construct(['expr' => $expr, 'var' => $var], ['global' => $global], $lineno); } public function compile(Compiler $compiler): void diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/IncludeNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/IncludeNode.php index d540d6b23..1c18292c5 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/IncludeNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/IncludeNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; @@ -20,16 +21,17 @@ use Twig\Node\Expression\AbstractExpression; * * @author Fabien Potencier */ +#[YieldReady] class IncludeNode extends Node implements NodeOutputInterface { - public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno, string $tag = null) + public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno) { $nodes = ['expr' => $expr]; if (null !== $variables) { $nodes['variables'] = $variables; } - parent::__construct($nodes, ['only' => $only, 'ignore_missing' => $ignoreMissing], $lineno, $tag); + parent::__construct($nodes, ['only' => $only, 'ignore_missing' => $ignoreMissing], $lineno); } public function compile(Compiler $compiler): void @@ -40,10 +42,10 @@ class IncludeNode extends Node implements NodeOutputInterface $template = $compiler->getVarName(); $compiler - ->write(sprintf("$%s = null;\n", $template)) + ->write(\sprintf("$%s = null;\n", $template)) ->write("try {\n") ->indent() - ->write(sprintf('$%s = ', $template)) + ->write(\sprintf('$%s = ', $template)) ; $this->addGetTemplate($compiler); @@ -56,10 +58,11 @@ class IncludeNode extends Node implements NodeOutputInterface ->write("// ignore missing template\n") ->outdent() ->write("}\n") - ->write(sprintf("if ($%s) {\n", $template)) + ->write(\sprintf("if ($%s) {\n", $template)) ->indent() - ->write(sprintf('$%s->display(', $template)) + ->write(\sprintf('yield from $%s->unwrap()->yield(', $template)) ; + $this->addTemplateArguments($compiler); $compiler ->raw(");\n") @@ -67,8 +70,9 @@ class IncludeNode extends Node implements NodeOutputInterface ->write("}\n") ; } else { + $compiler->write('yield from '); $this->addGetTemplate($compiler); - $compiler->raw('->display('); + $compiler->raw('->unwrap()->yield('); $this->addTemplateArguments($compiler); $compiler->raw(");\n"); } @@ -93,12 +97,12 @@ class IncludeNode extends Node implements NodeOutputInterface $compiler->raw(false === $this->getAttribute('only') ? '$context' : '[]'); } elseif (false === $this->getAttribute('only')) { $compiler - ->raw('twig_array_merge($context, ') + ->raw('CoreExtension::merge($context, ') ->subcompile($this->getNode('variables')) ->raw(')') ; } else { - $compiler->raw('twig_to_array('); + $compiler->raw('CoreExtension::toArray('); $compiler->subcompile($this->getNode('variables')); $compiler->raw(')'); } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/MacroNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/MacroNode.php index 7f1b24d53..5a2543a9f 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/MacroNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/MacroNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Error\SyntaxError; @@ -19,26 +20,34 @@ use Twig\Error\SyntaxError; * * @author Fabien Potencier */ +#[YieldReady] class MacroNode extends Node { public const VARARGS_NAME = 'varargs'; - public function __construct(string $name, Node $body, Node $arguments, int $lineno, string $tag = null) + /** + * @param BodyNode $body + */ + public function __construct(string $name, Node $body, Node $arguments, int $lineno) { + if (!$body instanceof BodyNode) { + trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, static::class)); + } + foreach ($arguments as $argumentName => $argument) { if (self::VARARGS_NAME === $argumentName) { - throw new SyntaxError(sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $argument->getTemplateLine(), $argument->getSourceContext()); + throw new SyntaxError(\sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $argument->getTemplateLine(), $argument->getSourceContext()); } } - parent::__construct(['body' => $body, 'arguments' => $arguments], ['name' => $name], $lineno, $tag); + parent::__construct(['body' => $body, 'arguments' => $arguments], ['name' => $name], $lineno); } public function compile(Compiler $compiler): void { $compiler ->addDebugInfo($this) - ->write(sprintf('public function macro_%s(', $this->getAttribute('name'))) + ->write(\sprintf('public function macro_%s(', $this->getAttribute('name'))) ; $count = \count($this->getNode('arguments')); @@ -64,7 +73,7 @@ class MacroNode extends Node ->write("{\n") ->indent() ->write("\$macros = \$this->macros;\n") - ->write("\$context = \$this->env->mergeGlobals([\n") + ->write("\$context = [\n") ->indent() ; @@ -77,35 +86,19 @@ class MacroNode extends Node ; } + $node = new CaptureNode($this->getNode('body'), $this->getNode('body')->lineno); + $compiler ->write('') ->string(self::VARARGS_NAME) ->raw(' => ') - ; - - $compiler ->raw("\$__varargs__,\n") ->outdent() - ->write("]);\n\n") + ->write("] + \$this->env->getGlobals();\n\n") ->write("\$blocks = [];\n\n") - ; - if ($compiler->getEnvironment()->isDebug()) { - $compiler->write("ob_start();\n"); - } else { - $compiler->write("ob_start(function () { return ''; });\n"); - } - $compiler - ->write("try {\n") - ->indent() - ->subcompile($this->getNode('body')) + ->write('return ') + ->subcompile($node) ->raw("\n") - ->write("return ('' === \$tmp = ob_get_contents()) ? '' : new Markup(\$tmp, \$this->env->getCharset());\n") - ->outdent() - ->write("} finally {\n") - ->indent() - ->write("ob_end_clean();\n") - ->outdent() - ->write("}\n") ->outdent() ->write("}\n\n") ; diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/ModuleNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/ModuleNode.php index e972b6ba5..d2fb216b1 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/ModuleNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/ModuleNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; @@ -20,16 +21,24 @@ use Twig\Source; /** * Represents a module node. * - * Consider this class as being final. If you need to customize the behavior of - * the generated class, consider adding nodes to the following nodes: display_start, - * display_end, constructor_start, constructor_end, and class_end. + * If you need to customize the behavior of the generated class, add nodes to + * the following nodes: display_start, display_end, constructor_start, + * constructor_end, and class_end. * * @author Fabien Potencier */ +#[YieldReady] final class ModuleNode extends Node { + /** + * @param BodyNode $body + */ public function __construct(Node $body, ?AbstractExpression $parent, Node $blocks, Node $macros, Node $traits, $embeddedTemplates, Source $source) { + if (!$body instanceof BodyNode) { + trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, static::class)); + } + $nodes = [ 'body' => $body, 'blocks' => $blocks, @@ -106,7 +115,7 @@ final class ModuleNode extends Node $parent = $this->getNode('parent'); $compiler - ->write("protected function doGetParent(array \$context)\n", "{\n") + ->write("protected function doGetParent(array \$context): bool|string|Template|TemplateWrapper\n", "{\n") ->indent() ->addDebugInfo($parent) ->write('return ') @@ -143,6 +152,7 @@ final class ModuleNode extends Node ->write("use Twig\Environment;\n") ->write("use Twig\Error\LoaderError;\n") ->write("use Twig\Error\RuntimeError;\n") + ->write("use Twig\Extension\CoreExtension;\n") ->write("use Twig\Extension\SandboxExtension;\n") ->write("use Twig\Markup;\n") ->write("use Twig\Sandbox\SecurityError;\n") @@ -150,7 +160,9 @@ final class ModuleNode extends Node ->write("use Twig\Sandbox\SecurityNotAllowedFilterError;\n") ->write("use Twig\Sandbox\SecurityNotAllowedFunctionError;\n") ->write("use Twig\Source;\n") - ->write("use Twig\Template;\n\n") + ->write("use Twig\Template;\n") + ->write("use Twig\TemplateWrapper;\n") + ->write("\n") ; } $compiler @@ -160,8 +172,11 @@ final class ModuleNode extends Node ->raw(" extends Template\n") ->write("{\n") ->indent() - ->write("private \$source;\n") - ->write("private \$macros = [];\n\n") + ->write("private Source \$source;\n") + ->write("/**\n") + ->write(" * @var array\n") + ->write(" */\n") + ->write("private array \$macros = [];\n\n") ; } @@ -188,14 +203,14 @@ final class ModuleNode extends Node $compiler ->addDebugInfo($node) - ->write(sprintf('$_trait_%s = $this->loadTemplate(', $i)) + ->write(\sprintf('$_trait_%s = $this->loadTemplate(', $i)) ->subcompile($node) ->raw(', ') ->repr($node->getTemplateName()) ->raw(', ') ->repr($node->getTemplateLine()) ->raw(");\n") - ->write(sprintf("if (!\$_trait_%s->isTraitable()) {\n", $i)) + ->write(\sprintf("if (!\$_trait_%s->unwrap()->isTraitable()) {\n", $i)) ->indent() ->write("throw new RuntimeError('Template \"'.") ->subcompile($trait->getNode('template')) @@ -204,12 +219,12 @@ final class ModuleNode extends Node ->raw(", \$this->source);\n") ->outdent() ->write("}\n") - ->write(sprintf("\$_trait_%s_blocks = \$_trait_%s->getBlocks();\n\n", $i, $i)) + ->write(\sprintf("\$_trait_%s_blocks = \$_trait_%s->unwrap()->getBlocks();\n\n", $i, $i)) ; foreach ($trait->getNode('targets') as $key => $value) { $compiler - ->write(sprintf('if (!isset($_trait_%s_blocks[', $i)) + ->write(\sprintf('if (!isset($_trait_%s_blocks[', $i)) ->string($key) ->raw("])) {\n") ->indent() @@ -223,11 +238,11 @@ final class ModuleNode extends Node ->outdent() ->write("}\n\n") - ->write(sprintf('$_trait_%s_blocks[', $i)) + ->write(\sprintf('$_trait_%s_blocks[', $i)) ->subcompile($value) - ->raw(sprintf('] = $_trait_%s_blocks[', $i)) + ->raw(\sprintf('] = $_trait_%s_blocks[', $i)) ->string($key) - ->raw(sprintf(']; unset($_trait_%s_blocks[', $i)) + ->raw(\sprintf(']; unset($_trait_%s_blocks[', $i)) ->string($key) ->raw("]);\n\n") ; @@ -242,7 +257,7 @@ final class ModuleNode extends Node for ($i = 0; $i < $countTraits; ++$i) { $compiler - ->write(sprintf('$_trait_%s_blocks'.($i == $countTraits - 1 ? '' : ',')."\n", $i)) + ->write(\sprintf('$_trait_%s_blocks'.($i == $countTraits - 1 ? '' : ',')."\n", $i)) ; } @@ -275,7 +290,7 @@ final class ModuleNode extends Node foreach ($this->getNode('blocks') as $name => $node) { $compiler - ->write(sprintf("'%s' => [\$this, 'block_%s'],\n", $name, $name)) + ->write(\sprintf("'%s' => [\$this, 'block_%s'],\n", $name, $name)) ; } @@ -303,7 +318,7 @@ final class ModuleNode extends Node protected function compileDisplay(Compiler $compiler) { $compiler - ->write("protected function doDisplay(array \$context, array \$blocks = [])\n", "{\n") + ->write("protected function doDisplay(array \$context, array \$blocks = []): iterable\n", "{\n") ->indent() ->write("\$macros = \$this->macros;\n") ->subcompile($this->getNode('display_start')) @@ -324,15 +339,24 @@ final class ModuleNode extends Node ->repr($parent->getTemplateLine()) ->raw(");\n") ; - $compiler->write('$this->parent'); - } else { - $compiler->write('$this->getParent($context)'); } - $compiler->raw("->display(\$context, array_merge(\$this->blocks, \$blocks));\n"); + $compiler->write('yield from '); + + if ($parent instanceof ConstantExpression) { + $compiler->raw('$this->parent'); + } else { + $compiler->raw('$this->getParent($context)'); + } + $compiler->raw("->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks));\n"); + } + + $compiler->subcompile($this->getNode('display_end')); + + if (!$this->hasNode('parent')) { + $compiler->write("yield from [];\n"); } $compiler - ->subcompile($this->getNode('display_end')) ->outdent() ->write("}\n\n") ; @@ -355,7 +379,10 @@ final class ModuleNode extends Node protected function compileGetTemplateName(Compiler $compiler) { $compiler - ->write("public function getTemplateName()\n", "{\n") + ->write("/**\n") + ->write(" * @codeCoverageIgnore\n") + ->write(" */\n") + ->write("public function getTemplateName(): string\n", "{\n") ->indent() ->write('return ') ->repr($this->getSourceContext()->getName()) @@ -377,7 +404,7 @@ final class ModuleNode extends Node $traitable = !$this->hasNode('parent') && 0 === \count($this->getNode('macros')); if ($traitable) { if ($this->getNode('body') instanceof BodyNode) { - $nodes = $this->getNode('body')->getNode(0); + $nodes = $this->getNode('body')->getNode('0'); } else { $nodes = $this->getNode('body'); } @@ -391,14 +418,6 @@ final class ModuleNode extends Node continue; } - if ($node instanceof TextNode && ctype_space($node->getAttribute('data'))) { - continue; - } - - if ($node instanceof BlockReferenceNode) { - continue; - } - $traitable = false; break; } @@ -409,9 +428,12 @@ final class ModuleNode extends Node } $compiler - ->write("public function isTraitable()\n", "{\n") + ->write("/**\n") + ->write(" * @codeCoverageIgnore\n") + ->write(" */\n") + ->write("public function isTraitable(): bool\n", "{\n") ->indent() - ->write(sprintf("return %s;\n", $traitable ? 'true' : 'false')) + ->write("return false;\n") ->outdent() ->write("}\n\n") ; @@ -420,9 +442,12 @@ final class ModuleNode extends Node protected function compileDebugInfo(Compiler $compiler) { $compiler - ->write("public function getDebugInfo()\n", "{\n") + ->write("/**\n") + ->write(" * @codeCoverageIgnore\n") + ->write(" */\n") + ->write("public function getDebugInfo(): array\n", "{\n") ->indent() - ->write(sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true)))) + ->write(\sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true)))) ->outdent() ->write("}\n\n") ; @@ -431,7 +456,7 @@ final class ModuleNode extends Node protected function compileGetSourceContext(Compiler $compiler) { $compiler - ->write("public function getSourceContext()\n", "{\n") + ->write("public function getSourceContext(): Source\n", "{\n") ->indent() ->write('return new Source(') ->string($compiler->getEnvironment()->isDebug() ? $this->getSourceContext()->getCode() : '') @@ -449,7 +474,7 @@ final class ModuleNode extends Node { if ($node instanceof ConstantExpression) { $compiler - ->write(sprintf('%s = $this->loadTemplate(', $var)) + ->write(\sprintf('%s = $this->loadTemplate(', $var)) ->subcompile($node) ->raw(', ') ->repr($node->getTemplateName()) diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/NameDeprecation.php b/data/web/inc/lib/vendor/twig/twig/src/Node/NameDeprecation.php new file mode 100644 index 000000000..63ab28576 --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/NameDeprecation.php @@ -0,0 +1,46 @@ + + */ +class NameDeprecation +{ + private $package; + private $version; + private $newName; + + public function __construct(string $package = '', string $version = '', string $newName = '') + { + $this->package = $package; + $this->version = $version; + $this->newName = $newName; + } + + public function getPackage(): string + { + return $this->package; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getNewName(): string + { + return $this->newName; + } +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/Node.php b/data/web/inc/lib/vendor/twig/twig/src/Node/Node.php index c0558b9af..2ccbcf610 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/Node.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/Node.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Source; @@ -20,61 +21,82 @@ use Twig\Source; * * @author Fabien Potencier */ +#[YieldReady] class Node implements \Countable, \IteratorAggregate { + /** + * @var array + */ protected $nodes; protected $attributes; protected $lineno; protected $tag; - private $name; private $sourceContext; + /** @var array */ + private $nodeNameDeprecations = []; + /** @var array */ + private $attributeNameDeprecations = []; /** - * @param array $nodes An array of named nodes - * @param array $attributes An array of attributes (should not be nodes) - * @param int $lineno The line number - * @param string $tag The tag name associated with the Node + * @param array $nodes An array of named nodes + * @param array $attributes An array of attributes (should not be nodes) + * @param int $lineno The line number */ - public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0, string $tag = null) + public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0) { foreach ($nodes as $name => $node) { if (!$node instanceof self) { - throw new \InvalidArgumentException(sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', \is_object($node) ? \get_class($node) : (null === $node ? 'null' : \gettype($node)), $name, static::class)); + throw new \InvalidArgumentException(\sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', \is_object($node) ? $node::class : (null === $node ? 'null' : \gettype($node)), $name, static::class)); } } $this->nodes = $nodes; $this->attributes = $attributes; $this->lineno = $lineno; - $this->tag = $tag; + + if (\func_num_args() > 3) { + trigger_deprecation('twig/twig', '3.12', \sprintf('The "tag" constructor argument of the "%s" class is deprecated and ignored (check which TokenParser class set it to "%s"), the tag is now automatically set by the Parser when needed.', static::class, func_get_arg(3) ?: 'null')); + } } public function __toString() { - $attributes = []; - foreach ($this->attributes as $name => $value) { - $attributes[] = sprintf('%s: %s', $name, str_replace("\n", '', var_export($value, true))); + $repr = static::class; + + if ($this->tag) { + $repr .= \sprintf("\n tag: %s", $this->tag); } - $repr = [static::class.'('.implode(', ', $attributes)]; + $attributes = []; + foreach ($this->attributes as $name => $value) { + if (\is_callable($value)) { + $v = '\Closure'; + } elseif ($value instanceof \Stringable) { + $v = (string) $value; + } else { + $v = str_replace("\n", '', var_export($value, true)); + } + $attributes[] = \sprintf('%s: %s', $name, $v); + } + + if ($attributes) { + $repr .= \sprintf("\n attributes:\n %s", implode("\n ", $attributes)); + } if (\count($this->nodes)) { + $repr .= "\n nodes:"; foreach ($this->nodes as $name => $node) { - $len = \strlen($name) + 4; + $len = \strlen($name) + 6; $noderepr = []; foreach (explode("\n", (string) $node) as $line) { $noderepr[] = str_repeat(' ', $len).$line; } - $repr[] = sprintf(' %s: %s', $name, ltrim(implode("\n", $noderepr))); + $repr .= \sprintf("\n %s: %s", $name, ltrim(implode("\n", $noderepr))); } - - $repr[] = ')'; - } else { - $repr[0] .= ')'; } - return implode("\n", $repr); + return $repr; } /** @@ -83,7 +105,7 @@ class Node implements \Countable, \IteratorAggregate public function compile(Compiler $compiler) { foreach ($this->nodes as $node) { - $node->compile($compiler); + $compiler->subcompile($node); } } @@ -97,6 +119,18 @@ class Node implements \Countable, \IteratorAggregate return $this->tag; } + /** + * @internal + */ + public function setNodeTag(string $tag): void + { + if ($this->tag) { + throw new \LogicException('The tag of a node can only be set once.'); + } + + $this->tag = $tag; + } + public function hasAttribute(string $name): bool { return \array_key_exists($name, $this->attributes); @@ -105,7 +139,17 @@ class Node implements \Countable, \IteratorAggregate public function getAttribute(string $name) { if (!\array_key_exists($name, $this->attributes)) { - throw new \LogicException(sprintf('Attribute "%s" does not exist for Node "%s".', $name, static::class)); + throw new \LogicException(\sprintf('Attribute "%s" does not exist for Node "%s".', $name, static::class)); + } + + $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true; + if ($triggerDeprecation && isset($this->attributeNameDeprecations[$name])) { + $dep = $this->attributeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting attribute "%s" on a "%s" class is deprecated, get the "%s" attribute instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting attribute "%s" on a "%s" class is deprecated.', $name, static::class); + } } return $this->attributes[$name]; @@ -113,38 +157,96 @@ class Node implements \Countable, \IteratorAggregate public function setAttribute(string $name, $value): void { + $triggerDeprecation = \func_num_args() > 2 ? func_get_arg(2) : true; + if ($triggerDeprecation && isset($this->attributeNameDeprecations[$name])) { + $dep = $this->attributeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting attribute "%s" on a "%s" class is deprecated, set the "%s" attribute instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting attribute "%s" on a "%s" class is deprecated.', $name, static::class); + } + } + $this->attributes[$name] = $value; } + public function deprecateAttribute(string $name, NameDeprecation $dep): void + { + $this->attributeNameDeprecations[$name] = $dep; + } + public function removeAttribute(string $name): void { unset($this->attributes[$name]); } + /** + * @param string|int $name + */ public function hasNode(string $name): bool { return isset($this->nodes[$name]); } + /** + * @param string|int $name + */ public function getNode(string $name): self { if (!isset($this->nodes[$name])) { - throw new \LogicException(sprintf('Node "%s" does not exist for Node "%s".', $name, static::class)); + throw new \LogicException(\sprintf('Node "%s" does not exist for Node "%s".', $name, static::class)); + } + + $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true; + if ($triggerDeprecation && isset($this->nodeNameDeprecations[$name])) { + $dep = $this->nodeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting node "%s" on a "%s" class is deprecated, get the "%s" node instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting node "%s" on a "%s" class is deprecated.', $name, static::class); + } } return $this->nodes[$name]; } + /** + * @param string|int $name + */ public function setNode(string $name, self $node): void { + $triggerDeprecation = \func_num_args() > 2 ? func_get_arg(2) : true; + if ($triggerDeprecation && isset($this->nodeNameDeprecations[$name])) { + $dep = $this->nodeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting node "%s" on a "%s" class is deprecated, set the "%s" node instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting node "%s" on a "%s" class is deprecated.', $name, static::class); + } + } + + if (null !== $this->sourceContext) { + $node->setSourceContext($this->sourceContext); + } $this->nodes[$name] = $node; } + /** + * @param string|int $name + */ public function removeNode(string $name): void { unset($this->nodes[$name]); } + /** + * @param string|int $name + */ + public function deprecateNode(string $name, NameDeprecation $dep): void + { + $this->nodeNameDeprecations[$name] = $dep; + } + /** * @return int */ diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/PrintNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/PrintNode.php index 60386d299..e3c23bbfa 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/PrintNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/PrintNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\AbstractExpression; @@ -20,19 +21,23 @@ use Twig\Node\Expression\AbstractExpression; * * @author Fabien Potencier */ +#[YieldReady] class PrintNode extends Node implements NodeOutputInterface { - 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 { + /** @var AbstractExpression */ + $expr = $this->getNode('expr'); + $compiler ->addDebugInfo($this) - ->write('echo ') - ->subcompile($this->getNode('expr')) + ->write($expr->isGenerator() ? 'yield from ' : 'yield ') + ->subcompile($expr) ->raw(";\n") ; } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/SandboxNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/SandboxNode.php index 4d5666bff..d51cea44b 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/SandboxNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/SandboxNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,11 +19,12 @@ use Twig\Compiler; * * @author Fabien Potencier */ +#[YieldReady] class SandboxNode extends Node { - public function __construct(Node $body, int $lineno, string $tag = null) + public function __construct(Node $body, int $lineno) { - parent::__construct(['body' => $body], [], $lineno, $tag); + parent::__construct(['body' => $body], [], $lineno); } public function compile(Compiler $compiler): void diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/SetNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/SetNode.php index 96b6bd8bf..67725104e 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/SetNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/SetNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Expression\ConstantExpression; @@ -19,26 +20,28 @@ use Twig\Node\Expression\ConstantExpression; * * @author Fabien Potencier */ +#[YieldReady] class SetNode extends Node implements NodeCaptureInterface { - public function __construct(bool $capture, Node $names, Node $values, int $lineno, string $tag = null) + public function __construct(bool $capture, Node $names, Node $values, int $lineno) { - parent::__construct(['names' => $names, 'values' => $values], ['capture' => $capture, 'safe' => false], $lineno, $tag); - /* * Optimizes the node when capture is used for a large block of text. * * {% set foo %}foo{% endset %} is compiled to $context['foo'] = new Twig\Markup("foo"); */ - if ($this->getAttribute('capture')) { - $this->setAttribute('safe', true); - - $values = $this->getNode('values'); + $safe = false; + if ($capture) { + $safe = true; if ($values instanceof TextNode) { - $this->setNode('values', new ConstantExpression($values->getAttribute('data'), $values->getTemplateLine())); - $this->setAttribute('capture', false); + $values = new ConstantExpression($values->getAttribute('data'), $values->getTemplateLine()); + $capture = false; + } else { + $values = new CaptureNode($values, $values->getTemplateLine()); } } + + parent::__construct(['names' => $names, 'values' => $values], ['capture' => $capture, 'safe' => $safe], $lineno); } public function compile(Compiler $compiler): void @@ -46,7 +49,7 @@ class SetNode extends Node implements NodeCaptureInterface $compiler->addDebugInfo($this); if (\count($this->getNode('names')) > 1) { - $compiler->write('list('); + $compiler->write('['); foreach ($this->getNode('names') as $idx => $node) { if ($idx) { $compiler->raw(', '); @@ -54,29 +57,15 @@ class SetNode extends Node implements NodeCaptureInterface $compiler->subcompile($node); } - $compiler->raw(')'); + $compiler->raw(']'); } else { - if ($this->getAttribute('capture')) { - if ($compiler->getEnvironment()->isDebug()) { - $compiler->write("ob_start();\n"); - } else { - $compiler->write("ob_start(function () { return ''; });\n"); - } - $compiler - ->subcompile($this->getNode('values')) - ; - } - $compiler->subcompile($this->getNode('names'), false); - - if ($this->getAttribute('capture')) { - $compiler->raw(" = ('' === \$tmp = ob_get_clean()) ? '' : new Markup(\$tmp, \$this->env->getCharset())"); - } } + $compiler->raw(' = '); - if (!$this->getAttribute('capture')) { - $compiler->raw(' = '); - + if ($this->getAttribute('capture')) { + $compiler->subcompile($this->getNode('values')); + } else { if (\count($this->getNode('names')) > 1) { $compiler->write('['); foreach ($this->getNode('values') as $idx => $value) { @@ -98,8 +87,10 @@ class SetNode extends Node implements NodeCaptureInterface $compiler->subcompile($this->getNode('values')); } } + + $compiler->raw(';'); } - $compiler->raw(";\n"); + $compiler->raw("\n"); } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/TextNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/TextNode.php index d74ebe630..fae65fb2c 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/TextNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/TextNode.php @@ -12,6 +12,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -19,6 +20,7 @@ use Twig\Compiler; * * @author Fabien Potencier */ +#[YieldReady] class TextNode extends Node implements NodeOutputInterface { public function __construct(string $data, int $lineno) @@ -28,9 +30,10 @@ class TextNode extends Node implements NodeOutputInterface public function compile(Compiler $compiler): void { + $compiler->addDebugInfo($this); + $compiler - ->addDebugInfo($this) - ->write('echo ') + ->write('yield ') ->string($this->getAttribute('data')) ->raw(";\n") ; diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/TypesNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/TypesNode.php new file mode 100644 index 000000000..ebb304d49 --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/TypesNode.php @@ -0,0 +1,28 @@ + + */ +#[YieldReady] +class TypesNode extends Node +{ + /** + * @param array $types + */ + public function __construct(array $types, int $lineno) + { + parent::__construct([], ['mapping' => $types], $lineno); + } + + public function compile(Compiler $compiler) + { + // Don't compile anything. + } +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/Node/WithNode.php b/data/web/inc/lib/vendor/twig/twig/src/Node/WithNode.php index 56a334496..487e2800b 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Node/WithNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Node/WithNode.php @@ -11,6 +11,7 @@ namespace Twig\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; /** @@ -18,16 +19,17 @@ use Twig\Compiler; * * @author Fabien Potencier */ +#[YieldReady] class WithNode extends Node { - public function __construct(Node $body, ?Node $variables, bool $only, int $lineno, string $tag = null) + public function __construct(Node $body, ?Node $variables, bool $only, int $lineno) { $nodes = ['body' => $body]; if (null !== $variables) { $nodes['variables'] = $variables; } - parent::__construct($nodes, ['only' => $only], $lineno, $tag); + parent::__construct($nodes, ['only' => $only], $lineno); } public function compile(Compiler $compiler): void @@ -36,35 +38,35 @@ class WithNode extends Node $parentContextName = $compiler->getVarName(); - $compiler->write(sprintf("\$%s = \$context;\n", $parentContextName)); + $compiler->write(\sprintf("\$%s = \$context;\n", $parentContextName)); if ($this->hasNode('variables')) { $node = $this->getNode('variables'); $varsName = $compiler->getVarName(); $compiler - ->write(sprintf('$%s = ', $varsName)) + ->write(\sprintf('$%s = ', $varsName)) ->subcompile($node) ->raw(";\n") - ->write(sprintf("if (!twig_test_iterable(\$%s)) {\n", $varsName)) + ->write(\sprintf("if (!is_iterable(\$%s)) {\n", $varsName)) ->indent() - ->write("throw new RuntimeError('Variables passed to the \"with\" tag must be a hash.', ") + ->write("throw new RuntimeError('Variables passed to the \"with\" tag must be a mapping.', ") ->repr($node->getTemplateLine()) ->raw(", \$this->getSourceContext());\n") ->outdent() ->write("}\n") - ->write(sprintf("\$%s = twig_to_array(\$%s);\n", $varsName, $varsName)) + ->write(\sprintf("\$%s = CoreExtension::toArray(\$%s);\n", $varsName, $varsName)) ; if ($this->getAttribute('only')) { $compiler->write("\$context = [];\n"); } - $compiler->write(sprintf("\$context = \$this->env->mergeGlobals(array_merge(\$context, \$%s));\n", $varsName)); + $compiler->write(\sprintf("\$context = \$%s + \$context + \$this->env->getGlobals();\n", $varsName)); } $compiler ->subcompile($this->getNode('body')) - ->write(sprintf("\$context = \$%s;\n", $parentContextName)) + ->write(\sprintf("\$context = \$%s;\n", $parentContextName)) ; } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php b/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php index d7036ae55..5de35fd09 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php +++ b/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/AbstractNodeVisitor.php @@ -17,9 +17,9 @@ use Twig\Node\Node; /** * Used to make node visitors compatible with Twig 1.x and 2.x. * - * To be removed in Twig 3.1. - * * @author Fabien Potencier + * + * @deprecated since 3.9 (to be removed in 4.0) */ abstract class AbstractNodeVisitor implements NodeVisitorInterface { diff --git a/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php b/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php index fe56ea307..32f49ab1e 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php +++ b/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/EscaperNodeVisitor.php @@ -57,7 +57,7 @@ final class EscaperNodeVisitor implements NodeVisitorInterface } elseif ($node instanceof AutoEscapeNode) { $this->statusStack[] = $node->getAttribute('value'); } elseif ($node instanceof BlockNode) { - $this->statusStack[] = isset($this->blocks[$node->getAttribute('name')]) ? $this->blocks[$node->getAttribute('name')] : $this->needEscaping($env); + $this->statusStack[] = $this->blocks[$node->getAttribute('name')] ?? $this->needEscaping(); } elseif ($node instanceof ImportNode) { $this->safeVars[] = $node->getNode('var')->getAttribute('name'); } @@ -73,7 +73,7 @@ final class EscaperNodeVisitor implements NodeVisitorInterface $this->blocks = []; } elseif ($node instanceof FilterExpression) { return $this->preEscapeFilterNode($node, $env); - } elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping($env)) { + } elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping()) { $expression = $node->getNode('expr'); if ($expression instanceof ConditionalExpression && $this->shouldUnwrapConditional($expression, $env, $type)) { return new DoNode($this->unwrapConditional($expression, $env, $type), $expression->getTemplateLine()); @@ -85,7 +85,7 @@ final class EscaperNodeVisitor implements NodeVisitorInterface if ($node instanceof AutoEscapeNode || $node instanceof BlockNode) { array_pop($this->statusStack); } elseif ($node instanceof BlockReferenceNode) { - $this->blocks[$node->getAttribute('name')] = $this->needEscaping($env); + $this->blocks[$node->getAttribute('name')] = $this->needEscaping(); } return $node; @@ -126,15 +126,11 @@ final class EscaperNodeVisitor implements NodeVisitorInterface return $node; } - return new InlinePrint($this->getEscaperFilter($type, $expression), $node->getTemplateLine()); + return new InlinePrint($this->getEscaperFilter($env, $type, $expression), $node->getTemplateLine()); } private function escapePrintNode(PrintNode $node, Environment $env, string $type): Node { - if (false === $type) { - return $node; - } - $expression = $node->getNode('expr'); if ($this->isSafeFor($type, $expression, $env)) { @@ -143,14 +139,19 @@ final class EscaperNodeVisitor implements NodeVisitorInterface $class = \get_class($node); - return new $class($this->getEscaperFilter($type, $expression), $node->getTemplateLine()); + return new $class($this->getEscaperFilter($env, $type, $expression), $node->getTemplateLine()); } private function preEscapeFilterNode(FilterExpression $filter, Environment $env): FilterExpression { - $name = $filter->getNode('filter')->getAttribute('value'); + if ($filter->hasAttribute('twig_callable')) { + $type = $filter->getAttribute('twig_callable')->getPreEscape(); + } else { + // legacy + $name = $filter->getNode('filter', false)->getAttribute('value'); + $type = $env->getFilter($name)->getPreEscape(); + } - $type = $env->getFilter($name)->getPreEscape(); if (null === $type) { return $filter; } @@ -160,7 +161,7 @@ final class EscaperNodeVisitor implements NodeVisitorInterface return $filter; } - $filter->setNode('node', $this->getEscaperFilter($type, $node)); + $filter->setNode('node', $this->getEscaperFilter($env, $type, $node)); return $filter; } @@ -183,22 +184,22 @@ final class EscaperNodeVisitor implements NodeVisitorInterface return \in_array($type, $safe) || \in_array('all', $safe); } - private function needEscaping(Environment $env) + private function needEscaping() { if (\count($this->statusStack)) { return $this->statusStack[\count($this->statusStack) - 1]; } - return $this->defaultStrategy ? $this->defaultStrategy : false; + return $this->defaultStrategy ?: false; } - private function getEscaperFilter(string $type, Node $node): FilterExpression + private function getEscaperFilter(Environment $env, string $type, Node $node): FilterExpression { $line = $node->getTemplateLine(); - $name = new ConstantExpression('escape', $line); + $filter = $env->getFilter('escape'); $args = new Node([new ConstantExpression($type, $line), new ConstantExpression(null, $line), new ConstantExpression(true, $line)]); - return new FilterExpression($node, $name, $args, $line); + return new FilterExpression($node, $filter, $args, $line); } public function getPriority(): int diff --git a/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/MacroAutoImportNodeVisitor.php b/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/MacroAutoImportNodeVisitor.php index af477e653..01d5a997f 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/MacroAutoImportNodeVisitor.php +++ b/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/MacroAutoImportNodeVisitor.php @@ -46,14 +46,14 @@ final class MacroAutoImportNodeVisitor implements NodeVisitorInterface if ($node instanceof ModuleNode) { $this->inAModule = false; if ($this->hasMacroCalls) { - $node->getNode('constructor_end')->setNode('_auto_macro_import', new ImportNode(new NameExpression('_self', 0), new AssignNameExpression('_self', 0), 0, 'import', true)); + $node->getNode('constructor_end')->setNode('_auto_macro_import', new ImportNode(new NameExpression('_self', 0), new AssignNameExpression('_self', 0), 0, true)); } } elseif ($this->inAModule) { if ( - $node instanceof GetAttrExpression && - $node->getNode('node') instanceof NameExpression && - '_self' === $node->getNode('node')->getAttribute('name') && - $node->getNode('attribute') instanceof ConstantExpression + $node instanceof GetAttrExpression + && $node->getNode('node') instanceof NameExpression + && '_self' === $node->getNode('node')->getAttribute('name') + && $node->getNode('attribute') instanceof ConstantExpression ) { $this->hasMacroCalls = true; diff --git a/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php b/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php index 7ac75e41a..a943f45c3 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php +++ b/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/OptimizerNodeVisitor.php @@ -15,7 +15,6 @@ use Twig\Environment; use Twig\Node\BlockReferenceNode; use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\FunctionExpression; use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\NameExpression; @@ -24,6 +23,7 @@ use Twig\Node\ForNode; use Twig\Node\IncludeNode; use Twig\Node\Node; use Twig\Node\PrintNode; +use Twig\Node\TextNode; /** * Tries to optimize the AST. @@ -43,27 +43,34 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface public const OPTIMIZE_NONE = 0; public const OPTIMIZE_FOR = 2; public const OPTIMIZE_RAW_FILTER = 4; + public const OPTIMIZE_TEXT_NODES = 8; private $loops = []; private $loopsTargets = []; - private $optimizers; /** * @param int $optimizers The optimizer mode */ - public function __construct(int $optimizers = -1) - { - if ($optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER)) { - throw new \InvalidArgumentException(sprintf('Optimizer mode "%s" is not valid.', $optimizers)); + public function __construct( + private int $optimizers = -1, + ) { + if ($optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER | self::OPTIMIZE_TEXT_NODES)) { + throw new \InvalidArgumentException(\sprintf('Optimizer mode "%s" is not valid.', $optimizers)); } - $this->optimizers = $optimizers; + if (-1 !== $optimizers && self::OPTIMIZE_RAW_FILTER === (self::OPTIMIZE_RAW_FILTER & $optimizers)) { + trigger_deprecation('twig/twig', '3.11', 'The "Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER" option is deprecated and does nothing.'); + } + + if (-1 !== $optimizers && self::OPTIMIZE_TEXT_NODES === (self::OPTIMIZE_TEXT_NODES & $optimizers)) { + trigger_deprecation('twig/twig', '3.12', 'The "Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES" option is deprecated and does nothing.'); + } } public function enterNode(Node $node, Environment $env): Node { if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) { - $this->enterOptimizeFor($node, $env); + $this->enterOptimizeFor($node); } return $node; @@ -72,14 +79,10 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface public function leaveNode(Node $node, Environment $env): ?Node { if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) { - $this->leaveOptimizeFor($node, $env); + $this->leaveOptimizeFor($node); } - if (self::OPTIMIZE_RAW_FILTER === (self::OPTIMIZE_RAW_FILTER & $this->optimizers)) { - $node = $this->optimizeRawFilter($node, $env); - } - - $node = $this->optimizePrintNode($node, $env); + $node = $this->optimizePrintNode($node); return $node; } @@ -91,16 +94,21 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface * * * "echo $this->render(Parent)Block()" with "$this->display(Parent)Block()" */ - private function optimizePrintNode(Node $node, Environment $env): Node + private function optimizePrintNode(Node $node): Node { if (!$node instanceof PrintNode) { return $node; } $exprNode = $node->getNode('expr'); + + if ($exprNode instanceof ConstantExpression && \is_string($exprNode->getAttribute('value'))) { + return new TextNode($exprNode->getAttribute('value'), $exprNode->getTemplateLine()); + } + if ( - $exprNode instanceof BlockReferenceExpression || - $exprNode instanceof ParentExpression + $exprNode instanceof BlockReferenceExpression + || $exprNode instanceof ParentExpression ) { $exprNode->setAttribute('output', true); @@ -110,22 +118,10 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface return $node; } - /** - * Removes "raw" filters. - */ - private function optimizeRawFilter(Node $node, Environment $env): Node - { - if ($node instanceof FilterExpression && 'raw' == $node->getNode('filter')->getAttribute('value')) { - return $node->getNode('node'); - } - - return $node; - } - /** * Optimizes "for" tag by removing the "loop" variable creation whenever possible. */ - private function enterOptimizeFor(Node $node, Environment $env): void + private function enterOptimizeFor(Node $node): void { if ($node instanceof ForNode) { // disable the loop variable by default @@ -166,7 +162,7 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface && 'include' === $node->getAttribute('name') && (!$node->getNode('arguments')->hasNode('with_context') || false !== $node->getNode('arguments')->getNode('with_context')->getAttribute('value') - ) + ) ) { $this->addLoopToAll(); } @@ -175,12 +171,12 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface elseif ($node instanceof GetAttrExpression && (!$node->getNode('attribute') instanceof ConstantExpression || 'parent' === $node->getNode('attribute')->getAttribute('value') - ) + ) && (true === $this->loops[0]->getAttribute('with_loop') - || ($node->getNode('node') instanceof NameExpression - && 'loop' === $node->getNode('node')->getAttribute('name') - ) - ) + || ($node->getNode('node') instanceof NameExpression + && 'loop' === $node->getNode('node')->getAttribute('name') + ) + ) ) { $this->addLoopToAll(); } @@ -189,7 +185,7 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface /** * Optimizes "for" tag by removing the "loop" variable creation whenever possible. */ - private function leaveOptimizeFor(Node $node, Environment $env): void + private function leaveOptimizeFor(Node $node): void { if ($node instanceof ForNode) { array_shift($this->loops); diff --git a/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php b/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php index 90d6f2e0f..07672164e 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php +++ b/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php @@ -96,10 +96,15 @@ final class SafeAnalysisNodeVisitor implements NodeVisitorInterface $this->setSafe($node, $safe); } elseif ($node instanceof FilterExpression) { // filter expression is safe when the filter is safe - $name = $node->getNode('filter')->getAttribute('value'); - $args = $node->getNode('arguments'); - if ($filter = $env->getFilter($name)) { - $safe = $filter->getSafe($args); + if ($node->hasAttribute('twig_callable')) { + $filter = $node->getAttribute('twig_callable'); + } else { + // legacy + $filter = $env->getFilter($node->getAttribute('name')); + } + + if ($filter) { + $safe = $filter->getSafe($node->getNode('arguments')); if (null === $safe) { $safe = $this->intersectSafe($this->getSafe($node->getNode('node')), $filter->getPreservesSafety()); } @@ -109,10 +114,15 @@ final class SafeAnalysisNodeVisitor implements NodeVisitorInterface } } elseif ($node instanceof FunctionExpression) { // function expression is safe when the function is safe - $name = $node->getAttribute('name'); - $args = $node->getNode('arguments'); - if ($function = $env->getFunction($name)) { - $this->setSafe($node, $function->getSafe($args)); + if ($node->hasAttribute('twig_callable')) { + $function = $node->getAttribute('twig_callable'); + } else { + // legacy + $function = $env->getFunction($node->getAttribute('name')); + } + + if ($function) { + $this->setSafe($node, $function->getSafe($node->getNode('arguments'))); } else { $this->setSafe($node, []); } @@ -136,7 +146,7 @@ final class SafeAnalysisNodeVisitor implements NodeVisitorInterface return $node; } - private function intersectSafe(array $a = null, array $b = null): array + private function intersectSafe(?array $a = null, ?array $b = null): array { if (null === $a || null === $b) { return []; diff --git a/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php b/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php index 1446cee6b..37e184a3e 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php +++ b/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php @@ -34,8 +34,11 @@ use Twig\Node\SetNode; final class SandboxNodeVisitor implements NodeVisitorInterface { private $inAModule = false; + /** @var array */ private $tags; + /** @var array */ private $filters; + /** @var array */ private $functions; private $needsToStringWrap = false; @@ -51,22 +54,22 @@ final class SandboxNodeVisitor implements NodeVisitorInterface } elseif ($this->inAModule) { // look for tags if ($node->getNodeTag() && !isset($this->tags[$node->getNodeTag()])) { - $this->tags[$node->getNodeTag()] = $node; + $this->tags[$node->getNodeTag()] = $node->getTemplateLine(); } // look for filters - if ($node instanceof FilterExpression && !isset($this->filters[$node->getNode('filter')->getAttribute('value')])) { - $this->filters[$node->getNode('filter')->getAttribute('value')] = $node; + if ($node instanceof FilterExpression && !isset($this->filters[$node->getAttribute('name')])) { + $this->filters[$node->getAttribute('name')] = $node->getTemplateLine(); } // look for functions if ($node instanceof FunctionExpression && !isset($this->functions[$node->getAttribute('name')])) { - $this->functions[$node->getAttribute('name')] = $node; + $this->functions[$node->getAttribute('name')] = $node->getTemplateLine(); } // the .. operator is equivalent to the range() function if ($node instanceof RangeBinary && !isset($this->functions['range'])) { - $this->functions['range'] = $node; + $this->functions['range'] = $node->getTemplateLine(); } if ($node instanceof PrintNode) { @@ -116,7 +119,7 @@ final class SandboxNodeVisitor implements NodeVisitorInterface private function wrapNode(Node $node, string $name): void { $expr = $node->getNode($name); - if ($expr instanceof NameExpression || $expr instanceof GetAttrExpression) { + if (($expr instanceof NameExpression || $expr instanceof GetAttrExpression) && !$expr->isGenerator()) { $node->setNode($name, new CheckToStringNode($expr)); } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php b/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php new file mode 100644 index 000000000..4b190b414 --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php @@ -0,0 +1,59 @@ +yieldReadyNodes[$class])) { + return $node; + } + + if (!$this->yieldReadyNodes[$class] = (bool) (new \ReflectionClass($class))->getAttributes(YieldReady::class)) { + if ($this->useYield) { + throw new \LogicException(\sprintf('You cannot enable the "use_yield" option of Twig as node "%s" is not marked as ready for it; please make it ready and then flag it with the #[YieldReady] attribute.', $class)); + } + + trigger_deprecation('twig/twig', '3.9', 'Twig node "%s" is not marked as ready for using "yield" instead of "echo"; please make it ready and then flag it with the #[YieldReady] attribute.', $class); + } + + return $node; + } + + public function leaveNode(Node $node, Environment $env): ?Node + { + return $node; + } + + public function getPriority(): int + { + return 255; + } +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/Parser.php b/data/web/inc/lib/vendor/twig/twig/src/Parser.php index 4428208fe..40370bb1b 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Parser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Parser.php @@ -25,6 +25,7 @@ use Twig\Node\NodeOutputInterface; use Twig\Node\PrintNode; use Twig\Node\TextNode; use Twig\TokenParser\TokenParserInterface; +use Twig\Util\ReflectionCallable; /** * @author Fabien Potencier @@ -39,20 +40,19 @@ class Parser private $blocks; private $blockStack; private $macros; - private $env; private $importedSymbols; private $traits; private $embeddedTemplates = []; private $varNameSalt = 0; - public function __construct(Environment $env) - { - $this->env = $env; + public function __construct( + private Environment $env, + ) { } public function getVarName(): string { - return sprintf('__internal_parse_%d', $this->varNameSalt++); + return \sprintf('__internal_parse_%d', $this->varNameSalt++); } public function parse(TokenStream $stream, $test = null, bool $dropNeedle = false): ModuleNode @@ -91,7 +91,7 @@ class Parser } if (!$e->getTemplateLine()) { - $e->setTemplateLine($this->stream->getCurrent()->getLine()); + $e->setTemplateLine($this->getCurrentToken()->getLine()); } throw $e; @@ -101,6 +101,9 @@ class Parser $traverser = new NodeTraverser($this->env, $this->visitors); + /** + * @var ModuleNode $node + */ $node = $traverser->traverse($node); // restore previous stack so previous parse() call can resume working @@ -117,23 +120,23 @@ class Parser $rv = []; while (!$this->stream->isEOF()) { switch ($this->getCurrentToken()->getType()) { - case /* Token::TEXT_TYPE */ 0: + case Token::TEXT_TYPE: $token = $this->stream->next(); $rv[] = new TextNode($token->getValue(), $token->getLine()); break; - case /* Token::VAR_START_TYPE */ 2: + case Token::VAR_START_TYPE: $token = $this->stream->next(); $expr = $this->expressionParser->parseExpression(); - $this->stream->expect(/* Token::VAR_END_TYPE */ 4); + $this->stream->expect(Token::VAR_END_TYPE); $rv[] = new PrintNode($expr, $token->getLine()); break; - case /* Token::BLOCK_START_TYPE */ 1: + case Token::BLOCK_START_TYPE: $this->stream->next(); $token = $this->getCurrentToken(); - if (/* Token::NAME_TYPE */ 5 !== $token->getType()) { + if (Token::NAME_TYPE !== $token->getType()) { throw new SyntaxError('A block must start with a tag name.', $token->getLine(), $this->stream->getSourceContext()); } @@ -151,13 +154,14 @@ class Parser if (!$subparser = $this->env->getTokenParser($token->getValue())) { if (null !== $test) { - $e = new SyntaxError(sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); + $e = new SyntaxError(\sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); - if (\is_array($test) && isset($test[0]) && $test[0] instanceof TokenParserInterface) { - $e->appendMessage(sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $test[0]->getTag(), $lineno)); + $callable = (new ReflectionCallable(new TwigTest('decision', $test)))->getCallable(); + if (\is_array($callable) && $callable[0] instanceof TokenParserInterface) { + $e->appendMessage(\sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $callable[0]->getTag(), $lineno)); } } else { - $e = new SyntaxError(sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); + $e = new SyntaxError(\sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); $e->addSuggestions($token->getValue(), array_keys($this->env->getTokenParsers())); } @@ -168,13 +172,16 @@ class Parser $subparser->setParser($this); $node = $subparser->parse($token); - if (null !== $node) { + if (!$node) { + trigger_deprecation('twig/twig', '3.12', 'Returning "null" from "%s" is deprecated and forbidden by "TokenParserInterface".', $subparser::class); + } else { + $node->setNodeTag($subparser->getTag()); $rv[] = $node; } break; default: - throw new SyntaxError('Lexer or parser ended up in unsupported state.', $this->getCurrentToken()->getLine(), $this->stream->getSourceContext()); + throw new SyntaxError('The lexer or the parser ended up in an unsupported state.', $this->getCurrentToken()->getLine(), $this->stream->getSourceContext()); } } @@ -187,6 +194,8 @@ class Parser public function getBlockStack(): array { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return $this->blockStack; } @@ -207,21 +216,31 @@ class Parser public function hasBlock(string $name): bool { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return isset($this->blocks[$name]); } public function getBlock(string $name): Node { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return $this->blocks[$name]; } public function setBlock(string $name, BlockNode $value): void { + if (isset($this->blocks[$name])) { + throw new SyntaxError(\sprintf("The block '%s' has already been defined line %d.", $name, $this->blocks[$name]->getTemplateLine()), $this->getCurrentToken()->getLine(), $this->blocks[$name]->getSourceContext()); + } + $this->blocks[$name] = new BodyNode([$value], [], $value->getTemplateLine()); } public function hasMacro(string $name): bool { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return isset($this->macros[$name]); } @@ -237,6 +256,8 @@ class Parser public function hasTraits(): bool { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return \count($this->traits) > 0; } @@ -247,7 +268,7 @@ class Parser $this->embeddedTemplates[] = $template; } - public function addImportedSymbol(string $type, string $alias, string $name = null, AbstractExpression $node = null): void + public function addImportedSymbol(string $type, string $alias, ?string $name = null, ?AbstractExpression $node = null): void { $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $node]; } @@ -280,11 +301,26 @@ class Parser public function getParent(): ?Node { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + return $this->parent; } + public function hasInheritance() + { + return $this->parent || 0 < \count($this->traits); + } + public function setParent(?Node $parent): void { + if (null === $parent) { + trigger_deprecation('twig/twig', '3.12', 'Passing "null" to "%s()" is deprecated.', __METHOD__); + } + + if (null !== $this->parent) { + throw new SyntaxError('Multiple extends tags are forbidden.', $parent->getTemplateLine(), $parent->getSourceContext()); + } + $this->parent = $parent; } @@ -303,10 +339,9 @@ class Parser // check that the body does not contain non-empty output nodes if ( ($node instanceof TextNode && !ctype_space($node->getAttribute('data'))) - || - (!$node instanceof TextNode && !$node instanceof BlockReferenceNode && $node instanceof NodeOutputInterface) + || (!$node instanceof TextNode && !$node instanceof BlockReferenceNode && $node instanceof NodeOutputInterface) ) { - if (false !== strpos((string) $node, \chr(0xEF).\chr(0xBB).\chr(0xBF))) { + if (str_contains((string) $node, \chr(0xEF).\chr(0xBB).\chr(0xBF))) { $t = substr($node->getAttribute('data'), 3); if ('' === $t || ctype_space($t)) { // bypass empty nodes starting with a BOM diff --git a/data/web/inc/lib/vendor/twig/twig/src/Profiler/Dumper/BaseDumper.php b/data/web/inc/lib/vendor/twig/twig/src/Profiler/Dumper/BaseDumper.php index 4da43e475..267718c1f 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Profiler/Dumper/BaseDumper.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Profiler/Dumper/BaseDumper.php @@ -50,7 +50,7 @@ abstract class BaseDumper if ($profile->getDuration() * 1000 < 1) { $str = $start."\n"; } else { - $str = sprintf("%s %s\n", $start, $this->formatTime($profile, $percent)); + $str = \sprintf("%s %s\n", $start, $this->formatTime($profile, $percent)); } $nCount = \count($profile->getProfiles()); diff --git a/data/web/inc/lib/vendor/twig/twig/src/Profiler/Dumper/BlackfireDumper.php b/data/web/inc/lib/vendor/twig/twig/src/Profiler/Dumper/BlackfireDumper.php index 03abe0fa0..bb3fbb52a 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Profiler/Dumper/BlackfireDumper.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Profiler/Dumper/BlackfireDumper.php @@ -24,7 +24,7 @@ final class BlackfireDumper $this->dumpProfile('main()', $profile, $data); $this->dumpChildren('main()', $profile, $data); - $start = sprintf('%f', microtime(true)); + $start = \sprintf('%f', microtime(true)); $str = <<isTemplate()) { $name = $p->getTemplate(); } else { - $name = sprintf('%s::%s(%s)', $p->getTemplate(), $p->getType(), $p->getName()); + $name = \sprintf('%s::%s(%s)', $p->getTemplate(), $p->getType(), $p->getName()); } - $this->dumpProfile(sprintf('%s==>%s', $parent, $name), $p, $data); + $this->dumpProfile(\sprintf('%s==>%s', $parent, $name), $p, $data); $this->dumpChildren($name, $p, $data); } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Profiler/Dumper/HtmlDumper.php b/data/web/inc/lib/vendor/twig/twig/src/Profiler/Dumper/HtmlDumper.php index 1f2433b4d..cdab2de59 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Profiler/Dumper/HtmlDumper.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Profiler/Dumper/HtmlDumper.php @@ -32,16 +32,16 @@ final class HtmlDumper extends BaseDumper protected function formatTemplate(Profile $profile, $prefix): string { - return sprintf('%s└ %s', $prefix, self::$colors['template'], $profile->getTemplate()); + return \sprintf('%s└ %s', $prefix, self::$colors['template'], $profile->getTemplate()); } protected function formatNonTemplate(Profile $profile, $prefix): string { - return sprintf('%s└ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), isset(self::$colors[$profile->getType()]) ? self::$colors[$profile->getType()] : 'auto', $profile->getName()); + return \sprintf('%s└ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), self::$colors[$profile->getType()] ?? 'auto', $profile->getName()); } protected function formatTime(Profile $profile, $percent): string { - return sprintf('%.2fms/%.0f%%', $percent > 20 ? self::$colors['big'] : 'auto', $profile->getDuration() * 1000, $percent); + return \sprintf('%.2fms/%.0f%%', $percent > 20 ? self::$colors['big'] : 'auto', $profile->getDuration() * 1000, $percent); } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Profiler/Dumper/TextDumper.php b/data/web/inc/lib/vendor/twig/twig/src/Profiler/Dumper/TextDumper.php index 31561c466..1c1f77e94 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Profiler/Dumper/TextDumper.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Profiler/Dumper/TextDumper.php @@ -20,16 +20,16 @@ final class TextDumper extends BaseDumper { protected function formatTemplate(Profile $profile, $prefix): string { - return sprintf('%s└ %s', $prefix, $profile->getTemplate()); + return \sprintf('%s└ %s', $prefix, $profile->getTemplate()); } protected function formatNonTemplate(Profile $profile, $prefix): string { - return sprintf('%s└ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), $profile->getName()); + return \sprintf('%s└ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), $profile->getName()); } protected function formatTime(Profile $profile, $percent): string { - return sprintf('%.2fms/%.0f%%', $profile->getDuration() * 1000, $percent); + return \sprintf('%.2fms/%.0f%%', $profile->getDuration() * 1000, $percent); } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Profiler/Node/EnterProfileNode.php b/data/web/inc/lib/vendor/twig/twig/src/Profiler/Node/EnterProfileNode.php index 1494baf44..4d8e504d1 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Profiler/Node/EnterProfileNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Profiler/Node/EnterProfileNode.php @@ -11,6 +11,7 @@ namespace Twig\Profiler\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Node; @@ -19,6 +20,7 @@ use Twig\Node\Node; * * @author Fabien Potencier */ +#[YieldReady] class EnterProfileNode extends Node { public function __construct(string $extensionName, string $type, string $name, string $varName) @@ -29,10 +31,10 @@ class EnterProfileNode extends Node public function compile(Compiler $compiler): void { $compiler - ->write(sprintf('$%s = $this->extensions[', $this->getAttribute('var_name'))) + ->write(\sprintf('$%s = $this->extensions[', $this->getAttribute('var_name'))) ->repr($this->getAttribute('extension_name')) ->raw("];\n") - ->write(sprintf('$%s->enter($%s = new \Twig\Profiler\Profile($this->getTemplateName(), ', $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof')) + ->write(\sprintf('$%s->enter($%s = new \Twig\Profiler\Profile($this->getTemplateName(), ', $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof')) ->repr($this->getAttribute('type')) ->raw(', ') ->repr($this->getAttribute('name')) diff --git a/data/web/inc/lib/vendor/twig/twig/src/Profiler/Node/LeaveProfileNode.php b/data/web/inc/lib/vendor/twig/twig/src/Profiler/Node/LeaveProfileNode.php index 94cebbaa8..bd9227e52 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Profiler/Node/LeaveProfileNode.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Profiler/Node/LeaveProfileNode.php @@ -11,6 +11,7 @@ namespace Twig\Profiler\Node; +use Twig\Attribute\YieldReady; use Twig\Compiler; use Twig\Node\Node; @@ -19,6 +20,7 @@ use Twig\Node\Node; * * @author Fabien Potencier */ +#[YieldReady] class LeaveProfileNode extends Node { public function __construct(string $varName) @@ -30,7 +32,7 @@ class LeaveProfileNode extends Node { $compiler ->write("\n") - ->write(sprintf("\$%s->leave(\$%s);\n\n", $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof')) + ->write(\sprintf("\$%s->leave(\$%s);\n\n", $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof')) ; } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php b/data/web/inc/lib/vendor/twig/twig/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php index 91abee807..1458bc5fc 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php @@ -27,13 +27,12 @@ use Twig\Profiler\Profile; */ final class ProfilerNodeVisitor implements NodeVisitorInterface { - private $extensionName; private $varName; - public function __construct(string $extensionName) - { - $this->extensionName = $extensionName; - $this->varName = sprintf('__internal_%s', hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $extensionName)); + public function __construct( + private string $extensionName, + ) { + $this->varName = \sprintf('__internal_%s', hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $extensionName)); } public function enterNode(Node $node, Environment $env): Node diff --git a/data/web/inc/lib/vendor/twig/twig/src/Profiler/Profile.php b/data/web/inc/lib/vendor/twig/twig/src/Profiler/Profile.php index 252ca9b0c..2928e1646 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Profiler/Profile.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Profiler/Profile.php @@ -20,19 +20,16 @@ final class Profile implements \IteratorAggregate, \Serializable public const BLOCK = 'block'; public const TEMPLATE = 'template'; public const MACRO = 'macro'; - - private $template; - private $name; - private $type; private $starts = []; private $ends = []; private $profiles = []; - public function __construct(string $template = 'main', string $type = self::ROOT, string $name = 'main') - { - $this->template = $template; - $this->type = $type; - $this->name = 0 === strpos($name, '__internal_') ? 'INTERNAL' : $name; + public function __construct( + private string $template = 'main', + private string $type = self::ROOT, + private string $name = 'main', + ) { + $this->name = str_starts_with($name, '__internal_') ? 'INTERNAL' : $name; $this->enter(); } @@ -176,6 +173,6 @@ final class Profile implements \IteratorAggregate, \Serializable */ public function __unserialize(array $data): void { - list($this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles) = $data; + [$this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles] = $data; } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Resources/core.php b/data/web/inc/lib/vendor/twig/twig/src/Resources/core.php new file mode 100644 index 000000000..6e2fcfb07 --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Resources/core.php @@ -0,0 +1,541 @@ +getCharset(), $values, $max); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return $env->getExtension(CoreExtension::class)->formatDate($date, $format, $timezone); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_date_modify_filter(Environment $env, $date, $modifier) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return $env->getExtension(CoreExtension::class)->modifyDate($date, $modifier); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_sprintf($format, ...$values) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::sprintf($format, ...$values); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_date_converter(Environment $env, $date = null, $timezone = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return $env->getExtension(CoreExtension::class)->convertDate($date, $timezone); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_replace_filter($str, $from) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::replace($str, $from); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_round($value, $precision = 0, $method = 'common') +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::round($value, $precision, $method); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return $env->getExtension(CoreExtension::class)->formatNumber($number, $decimal, $decimalPoint, $thousandSep); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_urlencode_filter($url) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::urlencode($url); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_merge(...$arrays) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::merge(...$arrays); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::slice($env->getCharset(), $item, $start, $length, $preserveKeys); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_first(Environment $env, $item) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::first($env->getCharset(), $item); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_last(Environment $env, $item) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::last($env->getCharset(), $item); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_join_filter($value, $glue = '', $and = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::join($value, $glue, $and); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_split_filter(Environment $env, $value, $delimiter, $limit = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::split($env->getCharset(), $value, $delimiter, $limit); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_get_array_keys_filter($array) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::keys($array); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::reverse($env->getCharset(), $item, $preserveKeys); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_sort_filter(Environment $env, $array, $arrow = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::sort($env, $array, $arrow); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_matches(string $regexp, ?string $str) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::matches($regexp, $str); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_trim_filter($string, $characterMask = null, $side = 'both') +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::trim($string, $characterMask, $side); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_nl2br($string) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::nl2br($string); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_spaceless($content) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::spaceless($content); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_convert_encoding($string, $to, $from) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::convertEncoding($string, $to, $from); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_length_filter(Environment $env, $thing) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::length($env->getCharset(), $thing); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_upper_filter(Environment $env, $string) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::upper($env->getCharset(), $string); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_lower_filter(Environment $env, $string) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::lower($env->getCharset(), $string); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_striptags($string, $allowable_tags = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::striptags($string, $allowable_tags); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_title_string_filter(Environment $env, $string) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::titleCase($env->getCharset(), $string); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_capitalize_string_filter(Environment $env, $string) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::capitalize($env->getCharset(), $string); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_test_empty($value) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::testEmpty($value); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_test_iterable($value) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return is_iterable($value); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::include($env, $context, $template, $variables, $withContext, $ignoreMissing, $sandboxed); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_source(Environment $env, $name, $ignoreMissing = false) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::source($env, $name, $ignoreMissing); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_constant($constant, $object = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::constant($constant, $object); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_constant_is_defined($constant, $object = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::constant($constant, $object, true); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::batch($items, $size, $fill, $preserveKeys); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_column($array, $name, $index = null): array +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::column($array, $name, $index); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_filter(Environment $env, $array, $arrow) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::filter($env, $array, $arrow); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_map(Environment $env, $array, $arrow) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::map($env, $array, $arrow); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::reduce($env, $array, $arrow, $initial); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_some(Environment $env, $array, $arrow) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::arraySome($env, $array, $arrow); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_every(Environment $env, $array, $arrow) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::arrayEvery($env, $array, $arrow); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::checkArrowInSandbox($env, $arrow, $thing, $type); +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/Resources/debug.php b/data/web/inc/lib/vendor/twig/twig/src/Resources/debug.php new file mode 100644 index 000000000..104b4f4e0 --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Resources/debug.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Twig\Environment; +use Twig\Extension\DebugExtension; + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_var_dump(Environment $env, $context, ...$vars) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + DebugExtension::dump($env, $context, ...$vars); +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/Resources/escaper.php b/data/web/inc/lib/vendor/twig/twig/src/Resources/escaper.php new file mode 100644 index 000000000..a2ee8e7aa --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Resources/escaper.php @@ -0,0 +1,51 @@ +getRuntime(EscaperRuntime::class)->escape($string, $strategy, $charset, $autoescape); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_escape_filter_is_safe(Node $filterArgs) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return EscaperExtension::escapeFilterIsSafe($filterArgs); +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/Resources/string_loader.php b/data/web/inc/lib/vendor/twig/twig/src/Resources/string_loader.php new file mode 100644 index 000000000..8f0e6492a --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Resources/string_loader.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Twig\Environment; +use Twig\Extension\StringLoaderExtension; +use Twig\TemplateWrapper; + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_template_from_string(Environment $env, $template, ?string $name = null): TemplateWrapper +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return StringLoaderExtension::templateFromString($env, $template, $name); +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/Runtime/EscaperRuntime.php b/data/web/inc/lib/vendor/twig/twig/src/Runtime/EscaperRuntime.php new file mode 100644 index 000000000..a3ce17146 --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Runtime/EscaperRuntime.php @@ -0,0 +1,327 @@ + */ + private $escapers = []; + + /** @internal */ + public $safeClasses = []; + + /** @internal */ + public $safeLookup = []; + + public function __construct( + private $charset = 'UTF-8', + ) { + } + + /** + * Defines a new escaper to be used via the escape filter. + * + * @param string $strategy The strategy name that should be used as a strategy in the escape call + * @param callable(string $string, string $charset): string $callable A valid PHP callable + */ + public function setEscaper($strategy, callable $callable) + { + $this->escapers[$strategy] = $callable; + } + + /** + * Gets all defined escapers. + * + * @return array An array of escapers + */ + public function getEscapers() + { + return $this->escapers; + } + + public function setSafeClasses(array $safeClasses = []) + { + $this->safeClasses = []; + $this->safeLookup = []; + foreach ($safeClasses as $class => $strategies) { + $this->addSafeClass($class, $strategies); + } + } + + public function addSafeClass(string $class, array $strategies) + { + $class = ltrim($class, '\\'); + if (!isset($this->safeClasses[$class])) { + $this->safeClasses[$class] = []; + } + $this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies); + + foreach ($strategies as $strategy) { + $this->safeLookup[$strategy][$class] = true; + } + } + + /** + * Escapes a string. + * + * @param mixed $string The value to be escaped + * @param string $strategy The escaping strategy + * @param string|null $charset The charset + * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) + * + * @throws RuntimeError + */ + public function escape($string, string $strategy = 'html', ?string $charset = null, bool $autoescape = false) + { + if ($autoescape && $string instanceof Markup) { + return $string; + } + + if (!\is_string($string)) { + if ($string instanceof \Stringable) { + if ($autoescape) { + $c = \get_class($string); + if (!isset($this->safeClasses[$c])) { + $this->safeClasses[$c] = []; + foreach (class_parents($string) + class_implements($string) as $class) { + if (isset($this->safeClasses[$class])) { + $this->safeClasses[$c] = array_unique(array_merge($this->safeClasses[$c], $this->safeClasses[$class])); + foreach ($this->safeClasses[$class] as $s) { + $this->safeLookup[$s][$c] = true; + } + } + } + } + if (isset($this->safeLookup[$strategy][$c]) || isset($this->safeLookup['all'][$c])) { + return (string) $string; + } + } + + $string = (string) $string; + } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) { + // we return the input as is (which can be of any type) + return $string; + } + } + + if ('' === $string) { + return ''; + } + + $charset = $charset ?: $this->charset; + + 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 = $this->convertEncoding($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 = $this->convertEncoding($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 = $this->convertEncoding($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 = $this->convertEncoding($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 '�'; + } + + /* + * 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 => '"', /* quotation mark */ + 38 => '&', /* ampersand */ + 60 => '<', /* less-than sign */ + 62 => '>', /* 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: + if (\array_key_exists($strategy, $this->escapers)) { + return $this->escapers[$strategy]($string, $charset); + } + + $validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($this->escapers))); + + throw new RuntimeError(\sprintf('Invalid escaping strategy "%s" (valid ones: "%s").', $strategy, $validStrategies)); + } + } + + private function convertEncoding(string $string, string $to, string $from) + { + if (!\function_exists('iconv')) { + throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); + } + + return iconv($from, $to, $string); + } +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/RuntimeLoader/ContainerRuntimeLoader.php b/data/web/inc/lib/vendor/twig/twig/src/RuntimeLoader/ContainerRuntimeLoader.php index b360d7bea..05106680c 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/RuntimeLoader/ContainerRuntimeLoader.php +++ b/data/web/inc/lib/vendor/twig/twig/src/RuntimeLoader/ContainerRuntimeLoader.php @@ -23,11 +23,9 @@ use Psr\Container\ContainerInterface; */ class ContainerRuntimeLoader implements RuntimeLoaderInterface { - private $container; - - public function __construct(ContainerInterface $container) - { - $this->container = $container; + public function __construct( + private ContainerInterface $container, + ) { } public function load(string $class) diff --git a/data/web/inc/lib/vendor/twig/twig/src/RuntimeLoader/FactoryRuntimeLoader.php b/data/web/inc/lib/vendor/twig/twig/src/RuntimeLoader/FactoryRuntimeLoader.php index 130648392..5d4e70b92 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/RuntimeLoader/FactoryRuntimeLoader.php +++ b/data/web/inc/lib/vendor/twig/twig/src/RuntimeLoader/FactoryRuntimeLoader.php @@ -18,14 +18,12 @@ namespace Twig\RuntimeLoader; */ class FactoryRuntimeLoader implements RuntimeLoaderInterface { - private $map; - /** * @param array $map An array where keys are class names and values factory callables */ - public function __construct(array $map = []) - { - $this->map = $map; + public function __construct( + private array $map = [], + ) { } public function load(string $class) diff --git a/data/web/inc/lib/vendor/twig/twig/src/Sandbox/SecurityPolicy.php b/data/web/inc/lib/vendor/twig/twig/src/Sandbox/SecurityPolicy.php index 2fc0d0131..988e37216 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Sandbox/SecurityPolicy.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Sandbox/SecurityPolicy.php @@ -68,19 +68,25 @@ final class SecurityPolicy implements SecurityPolicyInterface { foreach ($tags as $tag) { if (!\in_array($tag, $this->allowedTags)) { - throw new SecurityNotAllowedTagError(sprintf('Tag "%s" is not allowed.', $tag), $tag); + if ('extends' === $tag) { + trigger_deprecation('twig/twig', '3.12', 'The "extends" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.'); + } elseif ('use' === $tag) { + trigger_deprecation('twig/twig', '3.12', 'The "use" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed.'); + } else { + throw new SecurityNotAllowedTagError(\sprintf('Tag "%s" is not allowed.', $tag), $tag); + } } } foreach ($filters as $filter) { if (!\in_array($filter, $this->allowedFilters)) { - throw new SecurityNotAllowedFilterError(sprintf('Filter "%s" is not allowed.', $filter), $filter); + throw new SecurityNotAllowedFilterError(\sprintf('Filter "%s" is not allowed.', $filter), $filter); } } foreach ($functions as $function) { if (!\in_array($function, $this->allowedFunctions)) { - throw new SecurityNotAllowedFunctionError(sprintf('Function "%s" is not allowed.', $function), $function); + throw new SecurityNotAllowedFunctionError(\sprintf('Function "%s" is not allowed.', $function), $function); } } } @@ -94,16 +100,15 @@ final class SecurityPolicy implements SecurityPolicyInterface $allowed = false; $method = strtr($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); foreach ($this->allowedMethods as $class => $methods) { - if ($obj instanceof $class) { - $allowed = \in_array($method, $methods); - + if ($obj instanceof $class && \in_array($method, $methods)) { + $allowed = true; break; } } if (!$allowed) { $class = \get_class($obj); - throw new SecurityNotAllowedMethodError(sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method); + throw new SecurityNotAllowedMethodError(\sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method); } } @@ -111,16 +116,15 @@ final class SecurityPolicy implements SecurityPolicyInterface { $allowed = false; foreach ($this->allowedProperties as $class => $properties) { - if ($obj instanceof $class) { - $allowed = \in_array($property, \is_array($properties) ? $properties : [$properties]); - + if ($obj instanceof $class && \in_array($property, \is_array($properties) ? $properties : [$properties])) { + $allowed = true; break; } } if (!$allowed) { $class = \get_class($obj); - throw new SecurityNotAllowedPropertyError(sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property); + throw new SecurityNotAllowedPropertyError(\sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property); } } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Sandbox/SourcePolicyInterface.php b/data/web/inc/lib/vendor/twig/twig/src/Sandbox/SourcePolicyInterface.php new file mode 100644 index 000000000..b952f1ea6 --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Sandbox/SourcePolicyInterface.php @@ -0,0 +1,24 @@ +code = $code; - $this->name = $name; - $this->path = $path; + public function __construct( + private string $code, + private string $name, + private string $path = '', + ) { } public function getCode(): string diff --git a/data/web/inc/lib/vendor/twig/twig/src/Template.php b/data/web/inc/lib/vendor/twig/twig/src/Template.php index e04bd04a6..7b3ce8161 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Template.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Template.php @@ -35,38 +35,36 @@ abstract class Template protected $parent; protected $parents = []; - protected $env; protected $blocks = []; protected $traits = []; protected $extensions = []; protected $sandbox; - public function __construct(Environment $env) - { - $this->env = $env; + private $useYield; + + public function __construct( + protected Environment $env, + ) { + $this->useYield = $env->useYield(); $this->extensions = $env->getExtensions(); } /** * Returns the template name. - * - * @return string The template name */ - abstract public function getTemplateName(); + abstract public function getTemplateName(): string; /** * Returns debug information about the template. * - * @return array Debug information + * @return array Debug information */ - abstract public function getDebugInfo(); + abstract public function getDebugInfo(): array; /** * Returns information about the original template source code. - * - * @return Source */ - abstract public function getSourceContext(); + abstract public function getSourceContext(): Source; /** * Returns the parent template. @@ -74,18 +72,16 @@ abstract class Template * This method is for internal use only and should never be called * directly. * - * @return Template|TemplateWrapper|false The parent template or false if there is no parent + * @return self|TemplateWrapper|false The parent template or false if there is no parent */ - public function getParent(array $context) + public function getParent(array $context): self|TemplateWrapper|false { if (null !== $this->parent) { return $this->parent; } try { - $parent = $this->doGetParent($context); - - if (false === $parent) { + if (!$parent = $this->doGetParent($context)) { return false; } @@ -106,12 +102,12 @@ abstract class Template return $this->parents[$parent]; } - protected function doGetParent(array $context) + protected function doGetParent(array $context): bool|string|self|TemplateWrapper { return false; } - public function isTraitable() + public function isTraitable(): bool { return true; } @@ -126,14 +122,10 @@ abstract class Template * @param array $context The context * @param array $blocks The current set of blocks */ - public function displayParentBlock($name, array $context, array $blocks = []) + public function displayParentBlock($name, array $context, array $blocks = []): void { - if (isset($this->traits[$name])) { - $this->traits[$name][0]->displayBlock($name, $context, $blocks, false); - } elseif (false !== $parent = $this->getParent($context)) { - $parent->displayBlock($name, $context, $blocks, false); - } else { - throw new RuntimeError(sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext()); + foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { + echo $data; } } @@ -148,51 +140,10 @@ abstract class Template * @param array $blocks The current set of blocks * @param bool $useBlocks Whether to use the current set of blocks */ - public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, self $templateContext = null) + public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): void { - if ($useBlocks && isset($blocks[$name])) { - $template = $blocks[$name][0]; - $block = $blocks[$name][1]; - } elseif (isset($this->blocks[$name])) { - $template = $this->blocks[$name][0]; - $block = $this->blocks[$name][1]; - } else { - $template = null; - $block = null; - } - - // avoid RCEs when sandbox is enabled - if (null !== $template && !$template instanceof self) { - throw new \LogicException('A block must be a method on a \Twig\Template instance.'); - } - - if (null !== $template) { - try { - $template->$block($context, $blocks); - } catch (Error $e) { - if (!$e->getSourceContext()) { - $e->setSourceContext($template->getSourceContext()); - } - - // this is mostly useful for \Twig\Error\LoaderError exceptions - // see \Twig\Error\LoaderError - if (-1 === $e->getTemplateLine()) { - $e->guess(); - } - - throw $e; - } catch (\Exception $e) { - $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e); - $e->guess(); - - throw $e; - } - } elseif (false !== $parent = $this->getParent($context)) { - $parent->displayBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this); - } elseif (isset($blocks[$name])) { - throw new RuntimeError(sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext()); - } else { - throw new RuntimeError(sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext()); + foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks, $templateContext) as $data) { + echo $data; } } @@ -208,16 +159,25 @@ abstract class Template * * @return string The rendered block */ - public function renderParentBlock($name, array $context, array $blocks = []) + public function renderParentBlock($name, array $context, array $blocks = []): string { - if ($this->env->isDebug()) { - ob_start(); - } else { - ob_start(function () { return ''; }); - } - $this->displayParentBlock($name, $context, $blocks); + if (!$this->useYield) { + if ($this->env->isDebug()) { + ob_start(); + } else { + ob_start(function () { return ''; }); + } + $this->displayParentBlock($name, $context, $blocks); - return ob_get_clean(); + return ob_get_clean(); + } + + $content = ''; + foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { + $content .= $data; + } + + return $content; } /** @@ -233,16 +193,34 @@ abstract class Template * * @return string The rendered block */ - public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true) + public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true): string { - if ($this->env->isDebug()) { - ob_start(); - } else { - ob_start(function () { return ''; }); - } - $this->displayBlock($name, $context, $blocks, $useBlocks); + if (!$this->useYield) { + $level = ob_get_level(); + if ($this->env->isDebug()) { + ob_start(); + } else { + ob_start(function () { return ''; }); + } + try { + $this->displayBlock($name, $context, $blocks, $useBlocks); + } catch (\Throwable $e) { + while (ob_get_level() > $level) { + ob_end_clean(); + } - return ob_get_clean(); + throw $e; + } + + return ob_get_clean(); + } + + $content = ''; + foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks) as $data) { + $content .= $data; + } + + return $content; } /** @@ -257,7 +235,7 @@ abstract class Template * * @return bool true if the block exists, false otherwise */ - public function hasBlock($name, array $context, array $blocks = []) + public function hasBlock($name, array $context, array $blocks = []): bool { if (isset($blocks[$name])) { return $blocks[$name][0] instanceof self; @@ -267,7 +245,7 @@ abstract class Template return true; } - if (false !== $parent = $this->getParent($context)) { + if ($parent = $this->getParent($context)) { return $parent->hasBlock($name, $context); } @@ -283,13 +261,13 @@ abstract class Template * @param array $context The context * @param array $blocks The current set of blocks * - * @return array An array of block names + * @return array An array of block names */ - public function getBlockNames(array $context, array $blocks = []) + public function getBlockNames(array $context, array $blocks = []): array { $names = array_merge(array_keys($blocks), array_keys($this->blocks)); - if (false !== $parent = $this->getParent($context)) { + if ($parent = $this->getParent($context)) { $names = array_merge($names, $parent->getBlockNames($context)); } @@ -297,16 +275,22 @@ abstract class Template } /** - * @return Template|TemplateWrapper + * @param string|TemplateWrapper|array $template */ - protected function loadTemplate($template, $templateName = null, $line = null, $index = null) + protected function loadTemplate($template, $templateName = null, $line = null, $index = null): self|TemplateWrapper { try { if (\is_array($template)) { return $this->env->resolveTemplate($template); } - if ($template instanceof self || $template instanceof TemplateWrapper) { + if ($template instanceof TemplateWrapper) { + return $template; + } + + if ($template instanceof self) { + trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', self::class, __METHOD__); + return $template; } @@ -341,10 +325,8 @@ abstract class Template /** * @internal - * - * @return Template */ - public function unwrap() + public function unwrap(): self { return $this; } @@ -357,41 +339,58 @@ abstract class Template * * @return array An array of blocks */ - public function getBlocks() + public function getBlocks(): array { return $this->blocks; } - public function display(array $context, array $blocks = []) + public function display(array $context, array $blocks = []): void { - $this->displayWithErrorHandling($this->env->mergeGlobals($context), array_merge($this->blocks, $blocks)); + foreach ($this->yield($context, $blocks) as $data) { + echo $data; + } } - public function render(array $context) + public function render(array $context): string { - $level = ob_get_level(); - if ($this->env->isDebug()) { - ob_start(); - } else { - ob_start(function () { return ''; }); - } - try { - $this->display($context); - } catch (\Throwable $e) { - while (ob_get_level() > $level) { - ob_end_clean(); + if (!$this->useYield) { + $level = ob_get_level(); + if ($this->env->isDebug()) { + ob_start(); + } else { + ob_start(function () { return ''; }); + } + try { + $this->display($context); + } catch (\Throwable $e) { + while (ob_get_level() > $level) { + ob_end_clean(); + } + + throw $e; } - throw $e; + return ob_get_clean(); } - return ob_get_clean(); + $content = ''; + foreach ($this->yield($context) as $data) { + $content .= $data; + } + + return $content; } - protected function displayWithErrorHandling(array $context, array $blocks = []) + /** + * @return iterable + */ + public function yield(array $context, array $blocks = []): iterable { + $context += $this->env->getGlobals(); + $blocks = array_merge($this->blocks, $blocks); + try { - $this->doDisplay($context, $blocks); + yield from $this->doDisplay($context, $blocks); } catch (Error $e) { if (!$e->getSourceContext()) { $e->setSourceContext($this->getSourceContext()); @@ -404,19 +403,95 @@ abstract class Template } throw $e; - } catch (\Exception $e) { - $e = new RuntimeError(sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e); + } catch (\Throwable $e) { + $e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e); $e->guess(); throw $e; } } + /** + * @return iterable + */ + public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): iterable + { + if ($useBlocks && isset($blocks[$name])) { + $template = $blocks[$name][0]; + $block = $blocks[$name][1]; + } elseif (isset($this->blocks[$name])) { + $template = $this->blocks[$name][0]; + $block = $this->blocks[$name][1]; + } else { + $template = null; + $block = null; + } + + // avoid RCEs when sandbox is enabled + if (null !== $template && !$template instanceof self) { + throw new \LogicException('A block must be a method on a \Twig\Template instance.'); + } + + if (null !== $template) { + try { + yield from $template->$block($context, $blocks); + } catch (Error $e) { + if (!$e->getSourceContext()) { + $e->setSourceContext($template->getSourceContext()); + } + + // this is mostly useful for \Twig\Error\LoaderError exceptions + // see \Twig\Error\LoaderError + if (-1 === $e->getTemplateLine()) { + $e->guess(); + } + + throw $e; + } catch (\Throwable $e) { + $e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e); + $e->guess(); + + throw $e; + } + } elseif ($parent = $this->getParent($context)) { + yield from $parent->unwrap()->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this); + } elseif (isset($blocks[$name])) { + throw new RuntimeError(\sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext()); + } else { + throw new RuntimeError(\sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext()); + } + } + + /** + * Yields a parent block. + * + * This method is for internal use only and should never be called + * directly. + * + * @param string $name The block name to display from the parent + * @param array $context The context + * @param array $blocks The current set of blocks + * + * @return iterable + */ + public function yieldParentBlock($name, array $context, array $blocks = []): iterable + { + if (isset($this->traits[$name])) { + yield from $this->traits[$name][0]->yieldBlock($name, $context, $blocks, false); + } elseif ($parent = $this->getParent($context)) { + yield from $parent->unwrap()->yieldBlock($name, $context, $blocks, false); + } else { + throw new RuntimeError(\sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext()); + } + } + /** * Auto-generated method to display the template with the given context. * * @param array $context An array of parameters to pass to the template * @param array $blocks An array of blocks to pass to the template + * + * @return iterable */ - abstract protected function doDisplay(array $context, array $blocks = []); + abstract protected function doDisplay(array $context, array $blocks = []): iterable; } diff --git a/data/web/inc/lib/vendor/twig/twig/src/TemplateWrapper.php b/data/web/inc/lib/vendor/twig/twig/src/TemplateWrapper.php index c9c6b07c6..135c59188 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TemplateWrapper.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TemplateWrapper.php @@ -18,26 +18,21 @@ namespace Twig; */ final class TemplateWrapper { - private $env; - private $template; - /** * This method is for internal use only and should never be called * directly (use Twig\Environment::load() instead). * * @internal */ - public function __construct(Environment $env, Template $template) - { - $this->env = $env; - $this->template = $template; + public function __construct( + private Environment $env, + private Template $template, + ) { } public function render(array $context = []): string { - // using func_get_args() allows to not expose the blocks argument - // as it should only be used by internal code - return $this->template->render($context, \func_get_args()[1] ?? []); + return $this->template->render($context); } public function display(array $context = []) @@ -62,29 +57,15 @@ final class TemplateWrapper public function renderBlock(string $name, array $context = []): string { - $context = $this->env->mergeGlobals($context); - $level = ob_get_level(); - if ($this->env->isDebug()) { - ob_start(); - } else { - ob_start(function () { return ''; }); - } - try { - $this->template->displayBlock($name, $context); - } catch (\Throwable $e) { - while (ob_get_level() > $level) { - ob_end_clean(); - } - - throw $e; - } - - return ob_get_clean(); + return $this->template->renderBlock($name, $context + $this->env->getGlobals()); } public function displayBlock(string $name, array $context = []) { - $this->template->displayBlock($name, $this->env->mergeGlobals($context)); + $context += $this->env->getGlobals(); + foreach ($this->template->yieldBlock($name, $context) as $data) { + echo $data; + } } public function getSourceContext(): Source diff --git a/data/web/inc/lib/vendor/twig/twig/src/Test/IntegrationTestCase.php b/data/web/inc/lib/vendor/twig/twig/src/Test/IntegrationTestCase.php index 307302bb6..8690a809e 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Test/IntegrationTestCase.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Test/IntegrationTestCase.php @@ -30,9 +30,19 @@ use Twig\TwigTest; abstract class IntegrationTestCase extends TestCase { /** + * @deprecated since Twig 3.13, use getFixturesDirectory() instead. + * * @return string */ - abstract protected function getFixturesDir(); + protected function getFixturesDir() + { + throw new \BadMethodCallException('Not implemented.'); + } + + protected static function getFixturesDirectory(): string + { + throw new \BadMethodCallException('Not implemented.'); + } /** * @return RuntimeLoaderInterface[] @@ -84,6 +94,7 @@ abstract class IntegrationTestCase extends TestCase /** * @dataProvider getLegacyTests + * * @group legacy */ public function testLegacyIntegration($file, $message, $condition, $templates, $exception, $outputs, $deprecation = '') @@ -91,9 +102,19 @@ abstract class IntegrationTestCase extends TestCase $this->doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs, $deprecation); } + /** + * @final since Twig 3.13 + */ public function getTests($name, $legacyTests = false) { - $fixturesDir = realpath($this->getFixturesDir()); + try { + $fixturesDir = static::getFixturesDirectory(); + } catch (\BadMethodCallException) { + trigger_deprecation('twig/twig', '3.13', 'Not overriding "%s::getFixturesDirectory()" in "%s" is deprecated. This method will be abstract in 4.0.', self::class, static::class); + $fixturesDir = $this->getFixturesDir(); + } + + $fixturesDir = realpath($fixturesDir); $tests = []; foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($fixturesDir), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { @@ -101,7 +122,7 @@ abstract class IntegrationTestCase extends TestCase continue; } - if ($legacyTests xor false !== strpos($file->getRealpath(), '.legacy.test')) { + if ($legacyTests xor str_contains($file->getRealpath(), '.legacy.test')) { continue; } @@ -122,7 +143,7 @@ abstract class IntegrationTestCase extends TestCase $exception = false; preg_match_all('/--DATA--(.*?)(?:--CONFIG--(.*?))?--EXPECT--(.*?)(?=\-\-DATA\-\-|$)/s', $test, $outputs, \PREG_SET_ORDER); } else { - throw new \InvalidArgumentException(sprintf('Test "%s" is not valid.', str_replace($fixturesDir.'/', '', $file))); + throw new \InvalidArgumentException(\sprintf('Test "%s" is not valid.', str_replace($fixturesDir.'/', '', $file))); } $tests[] = [str_replace($fixturesDir.'/', '', $file), $message, $condition, $templates, $exception, $outputs, $deprecation]; @@ -136,6 +157,9 @@ abstract class IntegrationTestCase extends TestCase return $tests; } + /** + * @final since Twig 3.13 + */ public function getLegacyTests() { return $this->getTests('testLegacyIntegration', true); @@ -148,19 +172,23 @@ abstract class IntegrationTestCase extends TestCase } if ($condition) { + $ret = ''; eval('$ret = '.$condition.';'); if (!$ret) { $this->markTestSkipped($condition); } } - $loader = new ArrayLoader($templates); - foreach ($outputs as $i => $match) { $config = array_merge([ 'cache' => false, 'strict_variables' => true, ], $match[2] ? eval($match[2].';') : []); + // make sure that template are always compiled even if they are the same (useful when testing with more than one data/expect sections) + foreach ($templates as $j => $template) { + $templates[$j] = $template.str_repeat(' ', $i); + } + $loader = new ArrayLoader($templates); $twig = new Environment($loader, $config); $twig->addGlobal('global', 'global'); foreach ($this->getRuntimeLoaders() as $runtimeLoader) { @@ -183,11 +211,6 @@ abstract class IntegrationTestCase extends TestCase $twig->addFunction($function); } - // avoid using the same PHP class name for different cases - $p = new \ReflectionProperty($twig, 'templateClassPrefix'); - $p->setAccessible(true); - $p->setValue($twig, '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', uniqid(mt_rand(), true), false).'_'); - $deprecations = []; try { $prevHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$deprecations, &$prevHandler) { @@ -204,14 +227,14 @@ abstract class IntegrationTestCase extends TestCase } catch (\Exception $e) { if (false !== $exception) { $message = $e->getMessage(); - $this->assertSame(trim($exception), trim(sprintf('%s: %s', \get_class($e), $message))); + $this->assertSame(trim($exception), trim(\sprintf('%s: %s', \get_class($e), $message))); $last = substr($message, \strlen($message) - 1); $this->assertTrue('.' === $last || '?' === $last, 'Exception message must end with a dot or a question mark.'); return; } - throw new Error(sprintf('%s: %s', \get_class($e), $e->getMessage()), -1, null, $e); + throw new Error(\sprintf('%s: %s', \get_class($e), $e->getMessage()), -1, null, $e); } finally { restore_error_handler(); } @@ -222,18 +245,18 @@ abstract class IntegrationTestCase extends TestCase $output = trim($template->render(eval($match[1].';')), "\n "); } catch (\Exception $e) { if (false !== $exception) { - $this->assertSame(trim($exception), trim(sprintf('%s: %s', \get_class($e), $e->getMessage()))); + $this->assertSame(trim($exception), trim(\sprintf('%s: %s', \get_class($e), $e->getMessage()))); return; } - $e = new Error(sprintf('%s: %s', \get_class($e), $e->getMessage()), -1, null, $e); + $e = new Error(\sprintf('%s: %s', \get_class($e), $e->getMessage()), -1, null, $e); - $output = trim(sprintf('%s: %s', \get_class($e), $e->getMessage())); + $output = trim(\sprintf('%s: %s', \get_class($e), $e->getMessage())); } if (false !== $exception) { - list($class) = explode(':', $exception); + [$class] = explode(':', $exception); $constraintClass = class_exists('PHPUnit\Framework\Constraint\Exception') ? 'PHPUnit\Framework\Constraint\Exception' : 'PHPUnit_Framework_Constraint_Exception'; $this->assertThat(null, new $constraintClass($class)); } @@ -257,7 +280,7 @@ abstract class IntegrationTestCase extends TestCase $templates = []; preg_match_all('/--TEMPLATE(?:\((.*?)\))?--(.*?)(?=\-\-TEMPLATE|$)/s', $test, $matches, \PREG_SET_ORDER); foreach ($matches as $match) { - $templates[($match[1] ?: 'index.twig')] = $match[2]; + $templates[$match[1] ?: 'index.twig'] = $match[2]; } return $templates; diff --git a/data/web/inc/lib/vendor/twig/twig/src/Test/NodeTestCase.php b/data/web/inc/lib/vendor/twig/twig/src/Test/NodeTestCase.php index 3b8b2c86c..bac0ea6d0 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Test/NodeTestCase.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Test/NodeTestCase.php @@ -11,6 +11,8 @@ namespace Twig\Test; +use PHPUnit\Framework\Attributes\BeforeClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Twig\Compiler; use Twig\Environment; @@ -19,17 +21,37 @@ use Twig\Node\Node; abstract class NodeTestCase extends TestCase { - abstract public function getTests(); + /** + * @var Environment + */ + private $currentEnv; + + public function getTests() + { + return []; + } + + /** + * @return iterable + */ + public static function provideTests(): iterable + { + trigger_deprecation('twig/twig', '3.13', 'Not implementing "%s()" in "%s" is deprecated. This method will be abstract in 4.0.', __METHOD__, static::class); + + return []; + } /** * @dataProvider getTests + * @dataProvider provideTests */ + #[DataProvider('getTests'), DataProvider('provideTests')] public function testCompile($node, $source, $environment = null, $isPattern = false) { $this->assertNodeCompilation($source, $node, $environment, $isPattern); } - public function assertNodeCompilation($source, Node $node, Environment $environment = null, $isPattern = false) + public function assertNodeCompilation($source, Node $node, ?Environment $environment = null, $isPattern = false) { $compiler = $this->getCompiler($environment); $compiler->compile($node); @@ -41,25 +63,63 @@ abstract class NodeTestCase extends TestCase } } - protected function getCompiler(Environment $environment = null) + protected function getCompiler(?Environment $environment = null) { - return new Compiler(null === $environment ? $this->getEnvironment() : $environment); + return new Compiler($environment ?? $this->getEnvironment()); } + /** + * @final since Twig 3.13 + */ protected function getEnvironment() { - return new Environment(new ArrayLoader([])); + return $this->currentEnv ??= static::createEnvironment(); } + protected static function createEnvironment(): Environment + { + return new Environment(new ArrayLoader()); + } + + /** + * @deprecated since Twig 3.13, use createVariableGetter() instead. + */ protected function getVariableGetter($name, $line = false) + { + trigger_deprecation('twig/twig', '3.13', 'Method "%s()" is deprecated, use "createVariableGetter()" instead.', __METHOD__); + + return self::createVariableGetter($name, $line); + } + + final protected static function createVariableGetter(string $name, bool $line = false): string { $line = $line > 0 ? "// line $line\n" : ''; - return sprintf('%s($context["%s"] ?? null)', $line, $name); + return \sprintf('%s($context["%s"] ?? null)', $line, $name); } + /** + * @deprecated since Twig 3.13, use createAttributeGetter() instead. + */ protected function getAttributeGetter() { - return 'twig_get_attribute($this->env, $this->source, '; + trigger_deprecation('twig/twig', '3.13', 'Method "%s()" is deprecated, use "createAttributeGetter()" instead.', __METHOD__); + + return self::createAttributeGetter(); + } + + final protected static function createAttributeGetter(): string + { + return 'CoreExtension::getAttribute($this->env, $this->source, '; + } + + /** @beforeClass */ + #[BeforeClass] + final public static function checkDataProvider(): void + { + $r = new \ReflectionMethod(static::class, 'getTests'); + if (self::class !== $r->getDeclaringClass()->getName()) { + trigger_deprecation('twig/twig', '3.13', 'Implementing "%s::getTests()" in "%s" is deprecated, implement "provideTests()" instead.', self::class, static::class); + } } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Token.php b/data/web/inc/lib/vendor/twig/twig/src/Token.php index 53a6cafc3..237634ad1 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Token.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Token.php @@ -17,10 +17,6 @@ namespace Twig; */ final class Token { - private $value; - private $type; - private $lineno; - public const EOF_TYPE = -1; public const TEXT_TYPE = 0; public const BLOCK_START_TYPE = 1; @@ -35,17 +31,18 @@ final class Token public const INTERPOLATION_START_TYPE = 10; public const INTERPOLATION_END_TYPE = 11; public const ARROW_TYPE = 12; + public const SPREAD_TYPE = 13; - public function __construct(int $type, $value, int $lineno) - { - $this->type = $type; - $this->value = $value; - $this->lineno = $lineno; + public function __construct( + private int $type, + private $value, + private int $lineno, + ) { } public function __toString() { - return sprintf('%s(%s)', self::typeToString($this->type, true), $this->value); + return \sprintf('%s(%s)', self::typeToString($this->type, true), $this->value); } /** @@ -67,9 +64,9 @@ final class Token } return ($this->type === $type) && ( - null === $values || - (\is_array($values) && \in_array($this->value, $values)) || - $this->value == $values + null === $values + || (\is_array($values) && \in_array($this->value, $values)) + || $this->value == $values ); } @@ -133,8 +130,11 @@ final class Token case self::ARROW_TYPE: $name = 'ARROW_TYPE'; break; + case self::SPREAD_TYPE: + $name = 'SPREAD_TYPE'; + break; default: - throw new \LogicException(sprintf('Token of type "%s" does not exist.', $type)); + throw new \LogicException(\sprintf('Token of type "%s" does not exist.', $type)); } return $short ? $name : 'Twig\Token::'.$name; @@ -171,8 +171,10 @@ final class Token return 'end of string interpolation'; case self::ARROW_TYPE: return 'arrow function'; + case self::SPREAD_TYPE: + return 'spread operator'; default: - throw new \LogicException(sprintf('Token of type "%s" does not exist.', $type)); + throw new \LogicException(\sprintf('Token of type "%s" does not exist.', $type)); } } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/ApplyTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/ApplyTokenParser.php index 4dbf30406..0a6c1afb5 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/ApplyTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/ApplyTokenParser.php @@ -36,16 +36,16 @@ final class ApplyTokenParser extends AbstractTokenParser $ref = new TempNameExpression($name, $lineno); $ref->setAttribute('always_defined', true); - $filter = $this->parser->getExpressionParser()->parseFilterExpressionRaw($ref, $this->getTag()); + $filter = $this->parser->getExpressionParser()->parseFilterExpressionRaw($ref); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideApplyEnd'], true); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); return new Node([ - new SetNode(true, $ref, $body, $lineno, $this->getTag()), - new PrintNode($filter, $lineno, $this->getTag()), - ]); + new SetNode(true, $ref, $body, $lineno), + new PrintNode($filter, $lineno), + ], [], $lineno); } public function decideApplyEnd(Token $token): bool diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/AutoEscapeTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/AutoEscapeTokenParser.php index b674bea4a..b50b29e65 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/AutoEscapeTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/AutoEscapeTokenParser.php @@ -29,7 +29,7 @@ final class AutoEscapeTokenParser extends AbstractTokenParser $lineno = $token->getLine(); $stream = $this->parser->getStream(); - if ($stream->test(/* Token::BLOCK_END_TYPE */ 3)) { + if ($stream->test(Token::BLOCK_END_TYPE)) { $value = 'html'; } else { $expr = $this->parser->getExpressionParser()->parseExpression(); @@ -39,11 +39,11 @@ final class AutoEscapeTokenParser extends AbstractTokenParser $value = $expr->getAttribute('value'); } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); - return new AutoEscapeNode($value, $body, $lineno, $this->getTag()); + return new AutoEscapeNode($value, $body, $lineno); } public function decideBlockEnd(Token $token): bool diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/BlockTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/BlockTokenParser.php index 5878131be..81d675db0 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/BlockTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/BlockTokenParser.php @@ -35,21 +35,18 @@ final class BlockTokenParser extends AbstractTokenParser { $lineno = $token->getLine(); $stream = $this->parser->getStream(); - $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); - if ($this->parser->hasBlock($name)) { - throw new SyntaxError(sprintf("The block '%s' has already been defined line %d.", $name, $this->parser->getBlock($name)->getTemplateLine()), $stream->getCurrent()->getLine(), $stream->getSourceContext()); - } + $name = $stream->expect(Token::NAME_TYPE)->getValue(); $this->parser->setBlock($name, $block = new BlockNode($name, new Node([]), $lineno)); $this->parser->pushLocalScope(); $this->parser->pushBlockStack($name); - if ($stream->nextIf(/* Token::BLOCK_END_TYPE */ 3)) { + if ($stream->nextIf(Token::BLOCK_END_TYPE)) { $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); - if ($token = $stream->nextIf(/* Token::NAME_TYPE */ 5)) { + if ($token = $stream->nextIf(Token::NAME_TYPE)) { $value = $token->getValue(); if ($value != $name) { - throw new SyntaxError(sprintf('Expected endblock for block "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('Expected endblock for block "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } } } else { @@ -57,13 +54,13 @@ final class BlockTokenParser extends AbstractTokenParser new PrintNode($this->parser->getExpressionParser()->parseExpression(), $lineno), ]); } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $block->setNode('body', $body); $this->parser->popBlockStack(); $this->parser->popLocalScope(); - return new BlockReferenceNode($name, $lineno, $this->getTag()); + return new BlockReferenceNode($name, $lineno); } public function decideBlockEnd(Token $token): bool diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/DeprecatedTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/DeprecatedTokenParser.php index 31416c79c..164ef26ee 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/DeprecatedTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/DeprecatedTokenParser.php @@ -11,6 +11,7 @@ namespace Twig\TokenParser; +use Twig\Error\SyntaxError; use Twig\Node\DeprecatedNode; use Twig\Node\Node; use Twig\Token; @@ -21,6 +22,8 @@ use Twig\Token; * {% deprecated 'The "base.twig" template is deprecated, use "layout.twig" instead.' %} * {% extends 'layout.html.twig' %} * + * {% deprecated 'The "base.twig" template is deprecated, use "layout.twig" instead.' package="foo/bar" version="1.1" %} + * * @author Yonel Ceruto * * @internal @@ -29,11 +32,31 @@ final class DeprecatedTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { - $expr = $this->parser->getExpressionParser()->parseExpression(); + $stream = $this->parser->getStream(); + $expressionParser = $this->parser->getExpressionParser(); + $expr = $expressionParser->parseExpression(); + $node = new DeprecatedNode($expr, $token->getLine()); - $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); + while ($stream->test(Token::NAME_TYPE)) { + $k = $stream->getCurrent()->getValue(); + $stream->next(); + $stream->expect(Token::OPERATOR_TYPE, '='); - return new DeprecatedNode($expr, $token->getLine(), $this->getTag()); + switch ($k) { + case 'package': + $node->setNode('package', $expressionParser->parseExpression()); + break; + case 'version': + $node->setNode('version', $expressionParser->parseExpression()); + break; + default: + throw new SyntaxError(\sprintf('Unknown "%s" option.', $k), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } + } + + $stream->expect(Token::BLOCK_END_TYPE); + + return $node; } public function getTag(): string diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/DoTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/DoTokenParser.php index 32c8f12ff..8afd48559 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/DoTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/DoTokenParser.php @@ -26,9 +26,9 @@ final class DoTokenParser extends AbstractTokenParser { $expr = $this->parser->getExpressionParser()->parseExpression(); - $this->parser->getStream()->expect(/* Token::BLOCK_END_TYPE */ 3); + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); - return new DoNode($expr, $token->getLine(), $this->getTag()); + return new DoNode($expr, $token->getLine()); } public function getTag(): string diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/EmbedTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/EmbedTokenParser.php index 64b4f296f..7bf3233e2 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/EmbedTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/EmbedTokenParser.php @@ -30,21 +30,21 @@ final class EmbedTokenParser extends IncludeTokenParser $parent = $this->parser->getExpressionParser()->parseExpression(); - list($variables, $only, $ignoreMissing) = $this->parseArguments(); + [$variables, $only, $ignoreMissing] = $this->parseArguments(); - $parentToken = $fakeParentToken = new Token(/* Token::STRING_TYPE */ 7, '__parent__', $token->getLine()); + $parentToken = $fakeParentToken = new Token(Token::STRING_TYPE, '__parent__', $token->getLine()); if ($parent instanceof ConstantExpression) { - $parentToken = new Token(/* Token::STRING_TYPE */ 7, $parent->getAttribute('value'), $token->getLine()); + $parentToken = new Token(Token::STRING_TYPE, $parent->getAttribute('value'), $token->getLine()); } elseif ($parent instanceof NameExpression) { - $parentToken = new Token(/* Token::NAME_TYPE */ 5, $parent->getAttribute('name'), $token->getLine()); + $parentToken = new Token(Token::NAME_TYPE, $parent->getAttribute('name'), $token->getLine()); } // inject a fake parent to make the parent() function work $stream->injectTokens([ - new Token(/* Token::BLOCK_START_TYPE */ 1, '', $token->getLine()), - new Token(/* Token::NAME_TYPE */ 5, 'extends', $token->getLine()), + new Token(Token::BLOCK_START_TYPE, '', $token->getLine()), + new Token(Token::NAME_TYPE, 'extends', $token->getLine()), $parentToken, - new Token(/* Token::BLOCK_END_TYPE */ 3, '', $token->getLine()), + new Token(Token::BLOCK_END_TYPE, '', $token->getLine()), ]); $module = $this->parser->parse($stream, [$this, 'decideBlockEnd'], true); @@ -56,9 +56,9 @@ final class EmbedTokenParser extends IncludeTokenParser $this->parser->embedTemplate($module); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); - return new EmbedNode($module->getTemplateName(), $module->getAttribute('index'), $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag()); + return new EmbedNode($module->getTemplateName(), $module->getAttribute('index'), $variables, $only, $ignoreMissing, $token->getLine()); } public function decideBlockEnd(Token $token): bool diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/ExtendsTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/ExtendsTokenParser.php index 0ca46dd29..86ddfdfba 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/ExtendsTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/ExtendsTokenParser.php @@ -35,14 +35,11 @@ final class ExtendsTokenParser extends AbstractTokenParser throw new SyntaxError('Cannot use "extend" in a macro.', $token->getLine(), $stream->getSourceContext()); } - if (null !== $this->parser->getParent()) { - throw new SyntaxError('Multiple extends tags are forbidden.', $token->getLine(), $stream->getSourceContext()); - } $this->parser->setParent($this->parser->getExpressionParser()->parseExpression()); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); - return new Node(); + return new Node([], [], $token->getLine()); } public function getTag(): string diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/FlushTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/FlushTokenParser.php index 02c74aa13..0d2388745 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/FlushTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/FlushTokenParser.php @@ -26,9 +26,9 @@ final class FlushTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { - $this->parser->getStream()->expect(/* Token::BLOCK_END_TYPE */ 3); + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); - return new FlushNode($token->getLine(), $this->getTag()); + return new FlushNode($token->getLine()); } public function getTag(): string diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/ForTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/ForTokenParser.php index bac8ba2da..cf655f842 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/ForTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/ForTokenParser.php @@ -35,30 +35,30 @@ final class ForTokenParser extends AbstractTokenParser $lineno = $token->getLine(); $stream = $this->parser->getStream(); $targets = $this->parser->getExpressionParser()->parseAssignmentExpression(); - $stream->expect(/* Token::OPERATOR_TYPE */ 8, 'in'); + $stream->expect(Token::OPERATOR_TYPE, 'in'); $seq = $this->parser->getExpressionParser()->parseExpression(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideForFork']); if ('else' == $stream->next()->getValue()) { - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $else = $this->parser->subparse([$this, 'decideForEnd'], true); } else { $else = null; } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); if (\count($targets) > 1) { - $keyTarget = $targets->getNode(0); + $keyTarget = $targets->getNode('0'); $keyTarget = new AssignNameExpression($keyTarget->getAttribute('name'), $keyTarget->getTemplateLine()); - $valueTarget = $targets->getNode(1); + $valueTarget = $targets->getNode('1'); } else { $keyTarget = new AssignNameExpression('_key', $lineno); - $valueTarget = $targets->getNode(0); + $valueTarget = $targets->getNode('0'); } $valueTarget = new AssignNameExpression($valueTarget->getAttribute('name'), $valueTarget->getTemplateLine()); - return new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, $lineno, $this->getTag()); + return new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, $lineno); } public function decideForFork(Token $token): bool diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/FromTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/FromTokenParser.php index 35098c267..2ccff5fbe 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/FromTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/FromTokenParser.php @@ -29,28 +29,28 @@ final class FromTokenParser extends AbstractTokenParser { $macro = $this->parser->getExpressionParser()->parseExpression(); $stream = $this->parser->getStream(); - $stream->expect(/* Token::NAME_TYPE */ 5, 'import'); + $stream->expect(Token::NAME_TYPE, 'import'); $targets = []; - do { - $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + while (true) { + $name = $stream->expect(Token::NAME_TYPE)->getValue(); $alias = $name; if ($stream->nextIf('as')) { - $alias = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + $alias = $stream->expect(Token::NAME_TYPE)->getValue(); } $targets[$name] = $alias; - if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; } - } while (true); + } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $var = new AssignNameExpression($this->parser->getVarName(), $token->getLine()); - $node = new ImportNode($macro, $var, $token->getLine(), $this->getTag(), $this->parser->isMainScope()); + $node = new ImportNode($macro, $var, $token->getLine(), $this->parser->isMainScope()); foreach ($targets as $name => $alias) { $this->parser->addImportedSymbol('function', $alias, 'macro_'.$name, $var); diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/IfTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/IfTokenParser.php index c0fe6df0d..4ea6f3df9 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/IfTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/IfTokenParser.php @@ -37,7 +37,7 @@ final class IfTokenParser extends AbstractTokenParser $lineno = $token->getLine(); $expr = $this->parser->getExpressionParser()->parseExpression(); $stream = $this->parser->getStream(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideIfFork']); $tests = [$expr, $body]; $else = null; @@ -46,13 +46,13 @@ final class IfTokenParser extends AbstractTokenParser while (!$end) { switch ($stream->next()->getValue()) { case 'else': - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $else = $this->parser->subparse([$this, 'decideIfEnd']); break; case 'elseif': $expr = $this->parser->getExpressionParser()->parseExpression(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideIfFork']); $tests[] = $expr; $tests[] = $body; @@ -63,13 +63,13 @@ final class IfTokenParser extends AbstractTokenParser break; default: - throw new SyntaxError(sprintf('Unexpected end of template. Twig was looking for the following tags "else", "elseif", or "endif" to close the "if" block started at line %d).', $lineno), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('Unexpected end of template. Twig was looking for the following tags "else", "elseif", or "endif" to close the "if" block started at line %d).', $lineno), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); - return new IfNode(new Node($tests), $else, $lineno, $this->getTag()); + return new IfNode(new Node($tests), $else, $lineno); } public function decideIfFork(Token $token): bool diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/ImportTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/ImportTokenParser.php index 44cb4dad7..f20f35ab3 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/ImportTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/ImportTokenParser.php @@ -28,13 +28,13 @@ final class ImportTokenParser extends AbstractTokenParser public function parse(Token $token): Node { $macro = $this->parser->getExpressionParser()->parseExpression(); - $this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5, 'as'); - $var = new AssignNameExpression($this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5)->getValue(), $token->getLine()); - $this->parser->getStream()->expect(/* Token::BLOCK_END_TYPE */ 3); + $this->parser->getStream()->expect(Token::NAME_TYPE, 'as'); + $var = new AssignNameExpression($this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(), $token->getLine()); + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); $this->parser->addImportedSymbol('template', $var->getAttribute('name')); - return new ImportNode($macro, $var, $token->getLine(), $this->getTag(), $this->parser->isMainScope()); + return new ImportNode($macro, $var, $token->getLine(), $this->parser->isMainScope()); } public function getTag(): string diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/IncludeTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/IncludeTokenParser.php index 28beb8ae4..466f2288c 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/IncludeTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/IncludeTokenParser.php @@ -31,9 +31,9 @@ class IncludeTokenParser extends AbstractTokenParser { $expr = $this->parser->getExpressionParser()->parseExpression(); - list($variables, $only, $ignoreMissing) = $this->parseArguments(); + [$variables, $only, $ignoreMissing] = $this->parseArguments(); - return new IncludeNode($expr, $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag()); + return new IncludeNode($expr, $variables, $only, $ignoreMissing, $token->getLine()); } protected function parseArguments() @@ -41,23 +41,23 @@ class IncludeTokenParser extends AbstractTokenParser $stream = $this->parser->getStream(); $ignoreMissing = false; - if ($stream->nextIf(/* Token::NAME_TYPE */ 5, 'ignore')) { - $stream->expect(/* Token::NAME_TYPE */ 5, 'missing'); + if ($stream->nextIf(Token::NAME_TYPE, 'ignore')) { + $stream->expect(Token::NAME_TYPE, 'missing'); $ignoreMissing = true; } $variables = null; - if ($stream->nextIf(/* Token::NAME_TYPE */ 5, 'with')) { + if ($stream->nextIf(Token::NAME_TYPE, 'with')) { $variables = $this->parser->getExpressionParser()->parseExpression(); } $only = false; - if ($stream->nextIf(/* Token::NAME_TYPE */ 5, 'only')) { + if ($stream->nextIf(Token::NAME_TYPE, 'only')) { $only = true; } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); return [$variables, $only, $ignoreMissing]; } diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/MacroTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/MacroTokenParser.php index f584927e9..c7762075c 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/MacroTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/MacroTokenParser.php @@ -32,26 +32,26 @@ final class MacroTokenParser extends AbstractTokenParser { $lineno = $token->getLine(); $stream = $this->parser->getStream(); - $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + $name = $stream->expect(Token::NAME_TYPE)->getValue(); $arguments = $this->parser->getExpressionParser()->parseArguments(true, true); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $this->parser->pushLocalScope(); $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); - if ($token = $stream->nextIf(/* Token::NAME_TYPE */ 5)) { + if ($token = $stream->nextIf(Token::NAME_TYPE)) { $value = $token->getValue(); if ($value != $name) { - throw new SyntaxError(sprintf('Expected endmacro for macro "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + throw new SyntaxError(\sprintf('Expected endmacro for macro "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext()); } } $this->parser->popLocalScope(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); - $this->parser->setMacro($name, new MacroNode($name, new BodyNode([$body]), $arguments, $lineno, $this->getTag())); + $this->parser->setMacro($name, new MacroNode($name, new BodyNode([$body]), $arguments, $lineno)); - return new Node(); + return new Node([], [], $lineno); } public function decideBlockEnd(Token $token): bool diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/SandboxTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/SandboxTokenParser.php index c919556ec..70869fbc5 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/SandboxTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/SandboxTokenParser.php @@ -34,9 +34,9 @@ final class SandboxTokenParser extends AbstractTokenParser public function parse(Token $token): Node { $stream = $this->parser->getStream(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); // in a sandbox tag, only include tags are allowed if (!$body instanceof IncludeNode) { @@ -51,7 +51,7 @@ final class SandboxTokenParser extends AbstractTokenParser } } - return new SandboxNode($body, $token->getLine(), $this->getTag()); + return new SandboxNode($body, $token->getLine()); } public function decideBlockEnd(Token $token): bool diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/SetTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/SetTokenParser.php index 2fbdfe090..bb43907bd 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/SetTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/SetTokenParser.php @@ -37,10 +37,10 @@ final class SetTokenParser extends AbstractTokenParser $names = $this->parser->getExpressionParser()->parseAssignmentExpression(); $capture = false; - if ($stream->nextIf(/* Token::OPERATOR_TYPE */ 8, '=')) { + if ($stream->nextIf(Token::OPERATOR_TYPE, '=')) { $values = $this->parser->getExpressionParser()->parseMultitargetExpression(); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); if (\count($names) !== \count($values)) { throw new SyntaxError('When using set, you must have the same number of variables and assignments.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); @@ -52,13 +52,13 @@ final class SetTokenParser extends AbstractTokenParser throw new SyntaxError('When using set with a block, you cannot have a multi-target.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $values = $this->parser->subparse([$this, 'decideBlockEnd'], true); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); } - return new SetNode($capture, $names, $values, $lineno, $this->getTag()); + return new SetNode($capture, $names, $values, $lineno); } public function decideBlockEnd(Token $token): bool diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/TypesTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/TypesTokenParser.php new file mode 100644 index 000000000..2e0850e7e --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/TypesTokenParser.php @@ -0,0 +1,86 @@ + + * + * @internal + */ +final class TypesTokenParser extends AbstractTokenParser +{ + public function parse(Token $token): Node + { + $stream = $this->parser->getStream(); + + $types = $this->parseSimpleMappingExpression($stream); + + $stream->expect(Token::BLOCK_END_TYPE); + + return new TypesNode($types, $token->getLine()); + } + + /** + * @return array + * + * @throws SyntaxError + */ + private function parseSimpleMappingExpression(TokenStream $stream): array + { + $stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected'); + + $types = []; + + $first = true; + while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) { + if (!$first) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A type string must be followed by a comma'); + + // trailing ,? + if ($stream->test(Token::PUNCTUATION_TYPE, '}')) { + break; + } + } + $first = false; + + $nameToken = $stream->expect(Token::NAME_TYPE); + $isOptional = null !== $stream->nextIf(Token::PUNCTUATION_TYPE, '?'); + + $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A type name must be followed by a colon (:)'); + + $valueToken = $stream->expect(Token::STRING_TYPE); + + $types[$nameToken->getValue()] = [ + 'type' => $valueToken->getValue(), + 'optional' => $isOptional, + ]; + } + $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed'); + + return $types; + } + + public function getTag(): string + { + return 'types'; + } +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/UseTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/UseTokenParser.php index d0a2de41a..1b96b4047 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/UseTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/UseTokenParser.php @@ -43,27 +43,27 @@ final class UseTokenParser extends AbstractTokenParser $targets = []; if ($stream->nextIf('with')) { - do { - $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + while (true) { + $name = $stream->expect(Token::NAME_TYPE)->getValue(); $alias = $name; if ($stream->nextIf('as')) { - $alias = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue(); + $alias = $stream->expect(Token::NAME_TYPE)->getValue(); } $targets[$name] = new ConstantExpression($alias, -1); - if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) { + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; } - } while (true); + } } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $this->parser->addTrait(new Node(['template' => $template, 'targets' => new Node($targets)])); - return new Node(); + return new Node([], [], $token->getLine()); } public function getTag(): string diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/WithTokenParser.php b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/WithTokenParser.php index 7d8cbe261..8ce4f02b2 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenParser/WithTokenParser.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenParser/WithTokenParser.php @@ -30,18 +30,18 @@ final class WithTokenParser extends AbstractTokenParser $variables = null; $only = false; - if (!$stream->test(/* Token::BLOCK_END_TYPE */ 3)) { + if (!$stream->test(Token::BLOCK_END_TYPE)) { $variables = $this->parser->getExpressionParser()->parseExpression(); - $only = (bool) $stream->nextIf(/* Token::NAME_TYPE */ 5, 'only'); + $only = (bool) $stream->nextIf(Token::NAME_TYPE, 'only'); } - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideWithEnd'], true); - $stream->expect(/* Token::BLOCK_END_TYPE */ 3); + $stream->expect(Token::BLOCK_END_TYPE); - return new WithNode($body, $variables, $only, $token->getLine(), $this->getTag()); + return new WithNode($body, $variables, $only, $token->getLine()); } public function decideWithEnd(Token $token): bool diff --git a/data/web/inc/lib/vendor/twig/twig/src/TokenStream.php b/data/web/inc/lib/vendor/twig/twig/src/TokenStream.php index 1eac11a02..c91701bfe 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TokenStream.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TokenStream.php @@ -21,13 +21,12 @@ use Twig\Error\SyntaxError; */ final class TokenStream { - private $tokens; private $current = 0; - private $source; - public function __construct(array $tokens, Source $source = null) - { - $this->tokens = $tokens; + public function __construct( + private array $tokens, + private ?Source $source = null, + ) { $this->source = $source ?: new Source('', ''); } @@ -60,24 +59,22 @@ final class TokenStream */ public function nextIf($primary, $secondary = null) { - if ($this->tokens[$this->current]->test($primary, $secondary)) { - return $this->next(); - } + return $this->tokens[$this->current]->test($primary, $secondary) ? $this->next() : null; } /** * Tests a token and returns it or throws a syntax error. */ - public function expect($type, $value = null, string $message = null): Token + public function expect($type, $value = null, ?string $message = null): Token { $token = $this->tokens[$this->current]; if (!$token->test($type, $value)) { $line = $token->getLine(); - throw new SyntaxError(sprintf('%sUnexpected token "%s"%s ("%s" expected%s).', + throw new SyntaxError(\sprintf('%sUnexpected token "%s"%s ("%s" expected%s).', $message ? $message.'. ' : '', Token::typeToEnglish($token->getType()), - $token->getValue() ? sprintf(' of value "%s"', $token->getValue()) : '', - Token::typeToEnglish($type), $value ? sprintf(' with value "%s"', $value) : ''), + $token->getValue() ? \sprintf(' of value "%s"', $token->getValue()) : '', + Token::typeToEnglish($type), $value ? \sprintf(' with value "%s"', $value) : ''), $line, $this->source ); @@ -112,7 +109,7 @@ final class TokenStream */ public function isEOF(): bool { - return /* Token::EOF_TYPE */ -1 === $this->tokens[$this->current]->getType(); + return Token::EOF_TYPE === $this->tokens[$this->current]->getType(); } public function getCurrent(): Token diff --git a/data/web/inc/lib/vendor/twig/twig/src/TwigCallableInterface.php b/data/web/inc/lib/vendor/twig/twig/src/TwigCallableInterface.php new file mode 100644 index 000000000..2a8ff6116 --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/TwigCallableInterface.php @@ -0,0 +1,53 @@ + + */ +interface TwigCallableInterface extends \Stringable +{ + public function getName(): string; + + public function getType(): string; + + public function getDynamicName(): string; + + /** + * @return callable|array{class-string, string}|null + */ + public function getCallable(); + + public function getNodeClass(): string; + + public function needsCharset(): bool; + + public function needsEnvironment(): bool; + + public function needsContext(): bool; + + public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self; + + public function getArguments(): array; + + public function isVariadic(): bool; + + public function isDeprecated(): bool; + + public function getDeprecatingPackage(): string; + + public function getDeprecatedVersion(): string; + + public function getAlternative(): ?string; + + public function getMinimalNumberOfRequiredArguments(): int; +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/TwigFilter.php b/data/web/inc/lib/vendor/twig/twig/src/TwigFilter.php index 94e5f9b01..70b1f8f3f 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TwigFilter.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TwigFilter.php @@ -21,72 +21,27 @@ use Twig\Node\Node; * * @see https://twig.symfony.com/doc/templates.html#filters */ -final class TwigFilter +final class TwigFilter extends AbstractTwigCallable { - private $name; - private $callable; - private $options; - private $arguments = []; - /** - * @param callable|null $callable A callable implementing the filter. If null, you need to overwrite the "node_class" option to customize compilation. + * @param callable|array{class-string, string}|null $callable A callable implementing the filter. If null, you need to overwrite the "node_class" option to customize compilation. */ public function __construct(string $name, $callable = null, array $options = []) { - $this->name = $name; - $this->callable = $callable; + parent::__construct($name, $callable, $options); + $this->options = array_merge([ - 'needs_environment' => false, - 'needs_context' => false, - 'is_variadic' => false, 'is_safe' => null, 'is_safe_callback' => null, 'pre_escape' => null, 'preserves_safety' => null, 'node_class' => FilterExpression::class, - 'deprecated' => false, - 'alternative' => null, - ], $options); + ], $this->options); } - public function getName(): string + public function getType(): string { - return $this->name; - } - - /** - * Returns the callable to execute for this filter. - * - * @return callable|null - */ - public function getCallable() - { - return $this->callable; - } - - public function getNodeClass(): string - { - return $this->options['node_class']; - } - - public function setArguments(array $arguments): void - { - $this->arguments = $arguments; - } - - public function getArguments(): array - { - return $this->arguments; - } - - public function needsEnvironment(): bool - { - return $this->options['needs_environment']; - } - - public function needsContext(): bool - { - return $this->options['needs_context']; + return 'filter'; } public function getSafe(Node $filterArgs): ?array @@ -112,23 +67,8 @@ final class TwigFilter return $this->options['pre_escape']; } - public function isVariadic(): bool + public function getMinimalNumberOfRequiredArguments(): int { - return $this->options['is_variadic']; - } - - public function isDeprecated(): bool - { - return (bool) $this->options['deprecated']; - } - - public function getDeprecatedVersion(): string - { - return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated']; - } - - public function getAlternative(): ?string - { - return $this->options['alternative']; + return parent::getMinimalNumberOfRequiredArguments() + 1; } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/TwigFunction.php b/data/web/inc/lib/vendor/twig/twig/src/TwigFunction.php index 494d45b08..4a10df95e 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TwigFunction.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TwigFunction.php @@ -21,70 +21,31 @@ use Twig\Node\Node; * * @see https://twig.symfony.com/doc/templates.html#functions */ -final class TwigFunction +final class TwigFunction extends AbstractTwigCallable { - private $name; - private $callable; - private $options; - private $arguments = []; - /** - * @param callable|null $callable A callable implementing the function. If null, you need to overwrite the "node_class" option to customize compilation. + * @param callable|array{class-string, string}|null $callable A callable implementing the function. If null, you need to overwrite the "node_class" option to customize compilation. */ public function __construct(string $name, $callable = null, array $options = []) { - $this->name = $name; - $this->callable = $callable; + parent::__construct($name, $callable, $options); + $this->options = array_merge([ - 'needs_environment' => false, - 'needs_context' => false, - 'is_variadic' => false, 'is_safe' => null, 'is_safe_callback' => null, 'node_class' => FunctionExpression::class, - 'deprecated' => false, - 'alternative' => null, - ], $options); + 'parser_callable' => null, + ], $this->options); } - public function getName(): string + public function getType(): string { - return $this->name; + return 'function'; } - /** - * Returns the callable to execute for this function. - * - * @return callable|null - */ - public function getCallable() + public function getParserCallable(): ?callable { - return $this->callable; - } - - public function getNodeClass(): string - { - return $this->options['node_class']; - } - - public function setArguments(array $arguments): void - { - $this->arguments = $arguments; - } - - public function getArguments(): array - { - return $this->arguments; - } - - public function needsEnvironment(): bool - { - return $this->options['needs_environment']; - } - - public function needsContext(): bool - { - return $this->options['needs_context']; + return $this->options['parser_callable']; } public function getSafe(Node $functionArgs): ?array @@ -99,24 +60,4 @@ final class TwigFunction return []; } - - public function isVariadic(): bool - { - return (bool) $this->options['is_variadic']; - } - - public function isDeprecated(): bool - { - return (bool) $this->options['deprecated']; - } - - public function getDeprecatedVersion(): string - { - return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated']; - } - - public function getAlternative(): ?string - { - return $this->options['alternative']; - } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/TwigTest.php b/data/web/inc/lib/vendor/twig/twig/src/TwigTest.php index 4c18632f5..5e58ad8b0 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/TwigTest.php +++ b/data/web/inc/lib/vendor/twig/twig/src/TwigTest.php @@ -20,81 +20,48 @@ use Twig\Node\Expression\TestExpression; * * @see https://twig.symfony.com/doc/templates.html#test-operator */ -final class TwigTest +final class TwigTest extends AbstractTwigCallable { - private $name; - private $callable; - private $options; - private $arguments = []; - /** - * @param callable|null $callable A callable implementing the test. If null, you need to overwrite the "node_class" option to customize compilation. + * @param callable|array{class-string, string}|null $callable A callable implementing the test. If null, you need to overwrite the "node_class" option to customize compilation. */ public function __construct(string $name, $callable = null, array $options = []) { - $this->name = $name; - $this->callable = $callable; + parent::__construct($name, $callable, $options); + $this->options = array_merge([ - 'is_variadic' => false, 'node_class' => TestExpression::class, - 'deprecated' => false, - 'alternative' => null, 'one_mandatory_argument' => false, - ], $options); + ], $this->options); } - public function getName(): string + public function getType(): string { - return $this->name; + return 'test'; } - /** - * Returns the callable to execute for this test. - * - * @return callable|null - */ - public function getCallable() + public function needsCharset(): bool { - return $this->callable; + return false; } - public function getNodeClass(): string + public function needsEnvironment(): bool { - return $this->options['node_class']; + return false; } - public function setArguments(array $arguments): void + public function needsContext(): bool { - $this->arguments = $arguments; - } - - public function getArguments(): array - { - return $this->arguments; - } - - public function isVariadic(): bool - { - return (bool) $this->options['is_variadic']; - } - - public function isDeprecated(): bool - { - return (bool) $this->options['deprecated']; - } - - public function getDeprecatedVersion(): string - { - return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated']; - } - - public function getAlternative(): ?string - { - return $this->options['alternative']; + return false; } public function hasOneMandatoryArgument(): bool { return (bool) $this->options['one_mandatory_argument']; } + + public function getMinimalNumberOfRequiredArguments(): int + { + return parent::getMinimalNumberOfRequiredArguments() + 1; + } } diff --git a/data/web/inc/lib/vendor/twig/twig/src/Util/CallableArgumentsExtractor.php b/data/web/inc/lib/vendor/twig/twig/src/Util/CallableArgumentsExtractor.php new file mode 100644 index 000000000..8811ca9c8 --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Util/CallableArgumentsExtractor.php @@ -0,0 +1,204 @@ + + * + * @internal + */ +final class CallableArgumentsExtractor +{ + private ReflectionCallable $rc; + + public function __construct( + private Node $node, + private TwigCallableInterface $twigCallable, + ) { + $this->rc = new ReflectionCallable($twigCallable); + } + + /** + * @return array + */ + public function extractArguments(Node $arguments): array + { + $extractedArguments = []; + $named = false; + foreach ($arguments as $name => $node) { + if (!\is_int($name)) { + $named = true; + $name = $this->normalizeName($name); + } elseif ($named) { + throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + + $extractedArguments[$name] = $node; + } + + if (!$named && !$this->twigCallable->isVariadic()) { + $min = $this->twigCallable->getMinimalNumberOfRequiredArguments(); + if (\count($extractedArguments) < $this->rc->getReflector()->getNumberOfRequiredParameters() - $min) { + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $this->rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName(), $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + + return $extractedArguments; + } + + if (!$callable = $this->twigCallable->getCallable()) { + if ($named) { + throw new SyntaxError(\sprintf('Named arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName())); + } + + throw new SyntaxError(\sprintf('Arbitrary positional arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName())); + } + + [$callableParameters, $isPhpVariadic] = $this->getCallableParameters(); + $arguments = []; + $names = []; + $missingArguments = []; + $optionalArguments = []; + $pos = 0; + foreach ($callableParameters as $callableParameter) { + $name = $this->normalizeName($callableParameter->name); + if (\PHP_VERSION_ID >= 80000 && 'range' === $callable) { + if ('start' === $name) { + $name = 'low'; + } elseif ('end' === $name) { + $name = 'high'; + } + } + + $names[] = $name; + + if (\array_key_exists($name, $extractedArguments)) { + if (\array_key_exists($pos, $extractedArguments)) { + throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $name, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + + if (\count($missingArguments)) { + 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".', + $name, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) + ), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $extractedArguments[$name]; + unset($extractedArguments[$name]); + $optionalArguments = []; + } elseif (\array_key_exists($pos, $extractedArguments)) { + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $extractedArguments[$pos]; + unset($extractedArguments[$pos]); + $optionalArguments = []; + ++$pos; + } elseif ($callableParameter->isDefaultValueAvailable()) { + $optionalArguments[] = new ConstantExpression($callableParameter->getDefaultValue(), $this->node->getTemplateLine()); + } elseif ($callableParameter->isOptional()) { + if (!$extractedArguments) { + break; + } + + $missingArguments[] = $name; + } else { + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + } + + if ($this->twigCallable->isVariadic()) { + $arbitraryArguments = $isPhpVariadic ? new VariadicExpression([], $this->node->getTemplateLine()) : new ArrayExpression([], $this->node->getTemplateLine()); + foreach ($extractedArguments as $key => $value) { + if (\is_int($key)) { + $arbitraryArguments->addElement($value); + } else { + $arbitraryArguments->addElement($value, new ConstantExpression($key, $this->node->getTemplateLine())); + } + unset($extractedArguments[$key]); + } + + if ($arbitraryArguments->count()) { + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $arbitraryArguments; + } + } + + if ($extractedArguments) { + $unknownArgument = null; + foreach ($extractedArguments as $extractedArgument) { + if ($extractedArgument instanceof Node) { + $unknownArgument = $extractedArgument; + break; + } + } + + throw new SyntaxError( + \sprintf( + 'Unknown argument%s "%s" for %s "%s(%s)".', + \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $names) + ), + $unknownArgument ? $unknownArgument->getTemplateLine() : $this->node->getTemplateLine(), + $unknownArgument ? $unknownArgument->getSourceContext() : $this->node->getSourceContext() + ); + } + + return $arguments; + } + + private function normalizeName(string $name): string + { + return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)); + } + + private function getCallableParameters(): array + { + $parameters = $this->rc->getReflector()->getParameters(); + if ($this->node->hasNode('node')) { + array_shift($parameters); + } + if ($this->twigCallable->needsCharset()) { + array_shift($parameters); + } + if ($this->twigCallable->needsEnvironment()) { + array_shift($parameters); + } + if ($this->twigCallable->needsContext()) { + array_shift($parameters); + } + foreach ($this->twigCallable->getArguments() as $argument) { + array_shift($parameters); + } + + $isPhpVariadic = false; + if ($this->twigCallable->isVariadic()) { + $argument = end($parameters); + $isArray = $argument && $argument->hasType() && $argument->getType() instanceof \ReflectionNamedType && 'array' === $argument->getType()->getName(); + if ($isArray && $argument->isDefaultValueAvailable() && [] === $argument->getDefaultValue()) { + array_pop($parameters); + } elseif ($argument && $argument->isVariadic()) { + array_pop($parameters); + $isPhpVariadic = true; + } else { + throw new SyntaxError(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $this->rc->getName(), $this->twigCallable->getType(), $this->twigCallable->getName())); + } + } + + return [$parameters, $isPhpVariadic]; + } +} diff --git a/data/web/inc/lib/vendor/twig/twig/src/Util/DeprecationCollector.php b/data/web/inc/lib/vendor/twig/twig/src/Util/DeprecationCollector.php index 378b666bd..0ea26ed4b 100644 --- a/data/web/inc/lib/vendor/twig/twig/src/Util/DeprecationCollector.php +++ b/data/web/inc/lib/vendor/twig/twig/src/Util/DeprecationCollector.php @@ -20,11 +20,9 @@ use Twig\Source; */ final class DeprecationCollector { - private $twig; - - public function __construct(Environment $twig) - { - $this->twig = $twig; + public function __construct( + private Environment $twig, + ) { } /** @@ -60,6 +58,8 @@ final class DeprecationCollector if (\E_USER_DEPRECATED === $type) { $deprecations[] = $msg; } + + return false; }); foreach ($iterator as $name => $contents) { diff --git a/data/web/inc/lib/vendor/twig/twig/src/Util/ReflectionCallable.php b/data/web/inc/lib/vendor/twig/twig/src/Util/ReflectionCallable.php new file mode 100644 index 000000000..16734d9df --- /dev/null +++ b/data/web/inc/lib/vendor/twig/twig/src/Util/ReflectionCallable.php @@ -0,0 +1,92 @@ + + * + * @internal + */ +final class ReflectionCallable +{ + private $reflector; + private $callable; + private $name; + + public function __construct( + TwigCallableInterface $twigCallable, + ) { + $callable = $twigCallable->getCallable(); + if (\is_string($callable) && false !== $pos = strpos($callable, '::')) { + $callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)]; + } + + if (\is_array($callable) && method_exists($callable[0], $callable[1])) { + $this->reflector = $r = new \ReflectionMethod($callable[0], $callable[1]); + $this->callable = $callable; + $this->name = $r->class.'::'.$r->name; + + return; + } + + $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.', $twigCallable->getType(), $twigCallable->getName()), 0, $e); + } + $this->reflector = $r = new \ReflectionFunction($closure); + + if (str_contains($r->name, '{closure')) { + $this->callable = $callable; + $this->name = 'Closure'; + + return; + } + + if ($object = $r->getClosureThis()) { + $callable = [$object, $r->name]; + $this->name = get_debug_type($object).'::'.$r->name; + } elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) { + $callable = [$class->name, $r->name]; + $this->name = $class->name.'::'.$r->name; + } elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) { + $callable = [\is_array($callable) ? $callable[0] : $class->name, $r->name]; + $this->name = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name; + } else { + $callable = $this->name = $r->name; + } + + if ($checkVisibility && \is_array($callable) && method_exists(...$callable) && !(new \ReflectionMethod(...$callable))->isPublic()) { + $callable = $r->getClosure(); + } + + $this->callable = $callable; + } + + public function getReflector(): \ReflectionFunctionAbstract + { + return $this->reflector; + } + + public function getCallable() + { + return $this->callable; + } + + public function getName(): string + { + return $this->name; + } +} From 38907b50323c91b16cec1e2597e84b6ea9f890d6 Mon Sep 17 00:00:00 2001 From: Niklas Meyer Date: Wed, 16 Oct 2024 15:56:40 +0200 Subject: [PATCH 23/37] dovecot: activate lazy_expunge plugin per default (unconfigured) (#6112) --- data/Dockerfiles/dovecot/docker-entrypoint.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 3cfdd77a6..35c4691c5 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -114,15 +114,15 @@ if [[ "${FLATCURVE_EXPERIMENTAL}" =~ ^([yY][eE][sS]|[yY]) ]]; then echo -e "\e[33mActivating Flatcurve as FTS Backend...\e[0m" echo -e "\e[33mDepending on your previous setup a full reindex might be needed... \e[0m" echo -e "\e[34mVisit https://docs.mailcow.email/manual-guides/Dovecot/u_e-dovecot-fts/#fts-related-dovecot-commands to learn how to reindex\e[0m" -echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_flatcurve listescape replication' > /etc/dovecot/mail_plugins +echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_flatcurve listescape replication lazy_expunge' > /etc/dovecot/mail_plugins echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify mail_log fts fts_flatcurve listescape replication' > /etc/dovecot/mail_plugins_imap echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl fts fts_flatcurve notify listescape replication' > /etc/dovecot/mail_plugins_lmtp elif [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then -echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication' > /etc/dovecot/mail_plugins +echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication lazy_expunge' > /etc/dovecot/mail_plugins echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify listescape replication mail_log' > /etc/dovecot/mail_plugins_imap echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl notify listescape replication' > /etc/dovecot/mail_plugins_lmtp else -echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_solr listescape replication' > /etc/dovecot/mail_plugins +echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_solr listescape replication lazy_expunge' > /etc/dovecot/mail_plugins echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify mail_log fts fts_solr listescape replication' > /etc/dovecot/mail_plugins_imap echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl fts fts_solr notify listescape replication' > /etc/dovecot/mail_plugins_lmtp fi From fce93609dda7ba209cac1f30f72ac262ce6356d8 Mon Sep 17 00:00:00 2001 From: Patrik Kernstock Date: Thu, 17 Oct 2024 09:11:55 +0200 Subject: [PATCH 24/37] Update mime_types.conf configuration (#6013) In the last months and years, the default `mime_types.conf` of rspamd has changed and it might be also useful to make some adjustments to the weight of certain file extensions. This PR is removing all file extensions from `mime_types.conf` which are already in rspamd's default configuration at [rspamd/src/plugins/lua/mime_types.lua](https://github.com/rspamd/rspamd/blob/master/src/plugins/lua/mime_types.lua). If file extension is not present or has a different score compared to rspamd default, it is still in the list. There are also a few major differences to certain file extensions, which might be useful to discuss and carefully adjust. For example, `.exe` files are rated very 'badly' due to high chance of being malicious, so are other extensions like `bat`, `cmd`, etc. Current suggestion: ```lua # Extensions that are treated as 'bad' # Number is score multiply factor bad_extensions = { apk = 4, appx = 4, appxbundle = 4, bat = 8, cab = 20, cmd = 8, com = 20, diagcfg = 4, diagpack = 4, dmg = 8, ex = 20, ex_ = 20, exe = 20, img = 4, jar = 8, jnlp = 8, js = 8, jse = 8, lnk = 20, mjs = 8, msi = 4, msix = 4, msixbundle = 4, ps1 = 8, scr = 20, sct = 20, vb = 20, vbe = 20, vbs = 20, vhd = 4, py = 4, reg = 8, scf = 8, vhdx = 4, }; # Extensions that are particularly penalized for archives bad_archive_extensions = { pptx = 0.5, docx = 0.5, xlsx = 0.5, pdf = 1.0, jar = 12, jnlp = 12, bat = 12, cmd = 12, }; # Used to detect another archive in archive archive_extensions = { tar = 1, ['tar.gz'] = 1, }; ``` **As a important reminder**: For all remaining and additional file extensions and score weights, please check above default rspamd configuration! --- data/conf/rspamd/local.d/mime_types.conf | 72 ++++++++++++++---------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/data/conf/rspamd/local.d/mime_types.conf b/data/conf/rspamd/local.d/mime_types.conf index 08a08b699..d149e588a 100644 --- a/data/conf/rspamd/local.d/mime_types.conf +++ b/data/conf/rspamd/local.d/mime_types.conf @@ -1,27 +1,45 @@ +############################################################################### +# This list is added/merged with defined defaults in LUA module: +# https://github.com/rspamd/rspamd/blob/master/src/plugins/lua/mime_types.lua +############################################################################### + # Extensions that are treated as 'bad' # Number is score multiply factor bad_extensions = { - scr = 20, - lnk = 20, - exe = 20, - msi = 1, - msp = 1, - msu = 1, - jar = 2, - com = 20, - bat = 4, - cmd = 4, - ps1 = 4, - ace = 4, - arj = 4, + apk = 4, + appx = 4, + appxbundle = 4, + bat = 8, cab = 20, + cmd = 8, + com = 20, + diagcfg = 4, + diagpack = 4, + dmg = 8, + ex = 20, + ex_ = 20, + exe = 20, + img = 4, + jar = 8, + jnlp = 8, + js = 8, + jse = 8, + lnk = 20, + mjs = 8, + msi = 4, + msix = 4, + msixbundle = 4, + ps1 = 8, + scr = 20, + sct = 20, + vb = 20, + vbe = 20, vbs = 20, - hta = 4, - shs = 4, - wsc = 4, - wsf = 4, - iso = 8, - img = 8 + vhd = 4, + py = 4, + reg = 8, + scf = 8, + vhdx = 4, }; # Extensions that are particularly penalized for archives @@ -30,18 +48,14 @@ bad_archive_extensions = { docx = 0.5, xlsx = 0.5, pdf = 1.0, - jar = 3, - js = 0.5, - vbs = 20, - exe = 20 + jar = 12, + jnlp = 12, + bat = 12, + cmd = 12, }; # Used to detect another archive in archive archive_extensions = { - zip = 1, - arj = 1, - rar = 1, - ace = 1, - 7z = 1, - cab = 1 + tar = 1, + ['tar.gz'] = 1, }; From 399630cf3443c2adf91cc0b55948bdcce1a8a608 Mon Sep 17 00:00:00 2001 From: SamWang8891 Date: Thu, 17 Oct 2024 20:50:05 +0800 Subject: [PATCH 25/37] Update lang.zh-tw.json (#6114) --- data/web/lang/lang.zh-tw.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/web/lang/lang.zh-tw.json b/data/web/lang/lang.zh-tw.json index c3c2cf714..6dcd519e6 100644 --- a/data/web/lang/lang.zh-tw.json +++ b/data/web/lang/lang.zh-tw.json @@ -511,7 +511,7 @@ "architecture": "結構", "current_time": "系統時間", "container_running": "正在執行", - "memory": "記憶", + "memory": "記憶體", "container_disabled": "容器停止或停用", "container_stopped": "已停止", "cores": "核心", From ee2791d93af2335a759d46f855994ef4824480d1 Mon Sep 17 00:00:00 2001 From: Niklas Meyer Date: Fri, 18 Oct 2024 15:50:45 +0200 Subject: [PATCH 26/37] rspamd: update to 3.10.1 (#6115) * rspamd: upgrade to 3.10.1 * rspamd: adapt 30s task timeout per default now --- data/Dockerfiles/rspamd/Dockerfile | 4 ++-- data/conf/rspamd/local.d/options.inc | 1 + docker-compose.yml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index df15a0bed..ca4f9f71d 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -1,8 +1,8 @@ FROM debian:bookworm-slim -LABEL maintainer = "The Infrastructure Company GmbH " +LABEL maintainer="The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive -ARG RSPAMD_VER=rspamd_3.9.1-1~82f43560f +ARG RSPAMD_VER=rspamd_3.10.1-1~9de95c2b6 ARG CODENAME=bookworm ENV LC_ALL=C diff --git a/data/conf/rspamd/local.d/options.inc b/data/conf/rspamd/local.d/options.inc index 105bbdcd8..99197ff55 100644 --- a/data/conf/rspamd/local.d/options.inc +++ b/data/conf/rspamd/local.d/options.inc @@ -2,6 +2,7 @@ dns { enable_dnssec = true; } map_watch_interval = 30s; +task_timeout = 30s; disable_monitoring = true; # In case a task times out (like DNS lookup), soft reject the message # instead of silently accepting the message without further processing. diff --git a/docker-compose.yml b/docker-compose.yml index 01f8ee90b..110de288d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -80,7 +80,7 @@ services: - clamd rspamd-mailcow: - image: mailcow/rspamd:1.97 + image: mailcow/rspamd:1.98 stop_grace_period: 30s depends_on: - dovecot-mailcow From 502a7100ca18adaf4a6f3bbf0cfc5a38c91e168a Mon Sep 17 00:00:00 2001 From: milkmaker Date: Sat, 19 Oct 2024 22:24:45 +0200 Subject: [PATCH 27/37] [Web] Updated lang.zh-cn.json (#6120) Co-authored-by: SamWang8891 --- data/web/lang/lang.zh-cn.json | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/data/web/lang/lang.zh-cn.json b/data/web/lang/lang.zh-cn.json index 0d660c189..bac42e5d7 100644 --- a/data/web/lang/lang.zh-cn.json +++ b/data/web/lang/lang.zh-cn.json @@ -28,7 +28,8 @@ "spam_score": "垃圾邮件分数", "syncjobs": "同步任务", "tls_policy": "TLS 策略", - "unlimited_quota": "无限邮箱容量配额" + "unlimited_quota": "无限邮箱容量配额", + "pw_reset": "允许重置mailcow使用者密码" }, "add": { "activate_filter_warn": "当“启用”选项被勾选后,其它所有的过滤器都会被禁用。", @@ -338,7 +339,26 @@ "yes": "✓", "options": "选项", "f2b_max_ban_time": "最长封禁时间(秒)", - "copy_to_clipboard": "复制到粘贴板" + "copy_to_clipboard": "复制到粘贴板", + "reset_password_vars": "{{link}} 生成的密码重置链接
{{username}} 要求重置密码的使用者的邮箱名称
{{username2}} 复原邮箱名称
{{date}} 要求重置密码之日期
{{token_lifetime}} 令牌(token)过期时间(以分钟计)
{{hostname}} mailcow 主机名", + "logo_normal_label": "正常", + "f2b_manage_external_info": "Fail2Ban 还会持续维护封禁列表,但不会主动设置规则阻挡流量。请使用以下生成的封禁列表在外部阻止流量。", + "password_reset_info": "若无提供复原电子邮箱,此功能将无法使用。", + "f2b_ban_time_increment": "禁止时间会随着禁止次数增加", + "logo_dark_label": "为深色模式反色", + "cors_settings": "CORS 设定", + "f2b_manage_external": "外部管理Fail2Ban", + "ip_check_opt_in": "使用第三方服务 ipv4.mailcow.emailipv6.mailcow.email 以解析外部IP地址。", + "password_reset_settings": "密码复原设置", + "password_reset_tmpl_html": "HTML 模版", + "password_reset_tmpl_text": "文字模版", + "password_settings": "密码设定", + "restore_template": "留空以恢复预设模版", + "ip_check": "IP 检查", + "ip_check_disabled": "IP 检查已禁用。你可透过以下路径启用
系统 > 配置 > 选项 > 页面自定义", + "queue_unban": "解除封禁", + "allowed_methods": "访问控制允许方式", + "allowed_origins": "访问控制允许原" }, "danger": { "access_denied": "访问被拒绝或者表单数据无效", @@ -459,7 +479,9 @@ "value_missing": "请填入所有值", "yotp_verification_failed": "Yubico OTP 认证失败: %s", "template_exists": "模板 %s 已存在", - "template_name_invalid": "模板名称无效" + "template_name_invalid": "模板名称无效", + "cors_invalid_method": "制定的允许模式无效", + "cors_invalid_origin": "指定的允许原无效" }, "debug": { "chart_this_server": "图表 (此服务器)", From b106945c73e8d00df2ad912870fb7744535cb399 Mon Sep 17 00:00:00 2001 From: Niklas Meyer Date: Mon, 21 Oct 2024 16:03:51 +0200 Subject: [PATCH 28/37] Feat/rspamd 3.10.2 (#6122) * rspamd: update to 3.10.2 * rspamd: fix broken archive_extension gz --- data/Dockerfiles/rspamd/Dockerfile | 2 +- data/conf/rspamd/local.d/mime_types.conf | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index ca4f9f71d..64376bbd3 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -2,7 +2,7 @@ FROM debian:bookworm-slim LABEL maintainer="The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive -ARG RSPAMD_VER=rspamd_3.10.1-1~9de95c2b6 +ARG RSPAMD_VER=rspamd_3.10.2-1~b8a232043 ARG CODENAME=bookworm ENV LC_ALL=C diff --git a/data/conf/rspamd/local.d/mime_types.conf b/data/conf/rspamd/local.d/mime_types.conf index d149e588a..83561862f 100644 --- a/data/conf/rspamd/local.d/mime_types.conf +++ b/data/conf/rspamd/local.d/mime_types.conf @@ -57,5 +57,5 @@ bad_archive_extensions = { # Used to detect another archive in archive archive_extensions = { tar = 1, - ['tar.gz'] = 1, -}; + gz = 1, +}; \ No newline at end of file From 5c88030b5a39e8368bdfb564f4bfcde87b2e6ace Mon Sep 17 00:00:00 2001 From: milkmaker Date: Tue, 22 Oct 2024 21:52:42 +0200 Subject: [PATCH 29/37] Translations update from Weblate (#6123) * [Web] Updated lang.lv-lv.json Co-authored-by: Edgars Andersons * [Web] Updated lang.zh-tw.json [Web] Updated lang.zh-tw.json Co-authored-by: SamWang8891 Co-authored-by: milkmaker --------- Co-authored-by: Edgars Andersons Co-authored-by: SamWang8891 --- data/web/lang/lang.lv-lv.json | 38 +++++++++++++++--------- data/web/lang/lang.zh-tw.json | 55 ++++++++++++++++++++++++++++++----- 2 files changed, 71 insertions(+), 22 deletions(-) diff --git a/data/web/lang/lang.lv-lv.json b/data/web/lang/lang.lv-lv.json index 5f486d909..31c31301c 100644 --- a/data/web/lang/lang.lv-lv.json +++ b/data/web/lang/lang.lv-lv.json @@ -40,7 +40,7 @@ "exclude": "Izslēgt objektus (regex)", "full_name": "Pilns vārds", "goto_null": "Klusām dzēst pastu", - "hostname": "Hosta nosaukums", + "hostname": "Saimniekdators", "kind": "Veids", "mailbox_quota_m": "Maks. kvota pastkastei (MiB)", "mailbox_username": "Lietotājvārds (kriesā daļa no epasta adreses)", @@ -69,7 +69,8 @@ "username": "Lietotājvārds", "validate": "Apstiprināt", "validation_success": "Apstiprināts veiksmīgi", - "bcc_dest_format": "BCC galamērķim ir jābūt vienai derīgai e-pasta adresei.
Ja ir nepieciešams nosūtīt kopiju vairākām adresēm, jāizveido aizstājvārds un jāizmanto tas šeit." + "bcc_dest_format": "BCC galamērķim ir jābūt vienai derīgai e-pasta adresei.
Ja ir nepieciešams nosūtīt kopiju vairākām adresēm, jāizveido aizstājvārds un jāizmanto tas šeit.", + "domain_matches_hostname": "Domēns %s atbilst saimniekdatora nosaukumam" }, "admin": { "access": "Pieeja", @@ -165,7 +166,8 @@ "f2b_regex_info": "Vērā ņemtie žurnāli: SOGO, Postfix, Dovecot, PHP-FPM.", "sys_mails": "Sistēmas pasts", "ip_check_disabled": "IP pārbaude ir atspējota. To var iespējot
Sistēma > Konfigurācija > iespējas > Pielāgot", - "rspamd_com_settings": "Iestatījuma nosaukums tiks izveidots automātiski. Lūgums zemāk skatīt priekšiestatījumu piemērus. Vairāk informācijas ir Rspamd dokumentācijā" + "rspamd_com_settings": "Iestatījuma nosaukums tiks izveidots automātiski. Lūgums zemāk skatīt priekšiestatījumu piemērus. Vairāk informācijas ir Rspamd dokumentācijā", + "reset_password_vars": "{{link}} Izveidotā paroles atiestatīšanas saite
{{username}} Lietotāja, kurš pieprasīja paroles atiestatīšanu, pastkastes nosaukums
{{username2}} Atkopšanas pastkastes nosaukums
{{date}} Paroles atiestatīšanas pieprasījuma veikšanas datums
{{token_lifetime}} Pilnvaras derīgums minūtēs
{{hostname}} mailcow saimniekdatora nosaukums" }, "danger": { "access_denied": "Piekļuve liegta, vai nepareizi dati", @@ -216,7 +218,8 @@ "target_domain_invalid": "Goto domēns ir nepareizs", "targetd_not_found": "Mērķa domēns nav atrasts", "username_invalid": "Lietotājvārds nevar tikt izmantots", - "validity_missing": "Lūdzu piešķiriet derīguma termiņu" + "validity_missing": "Lūdzu piešķiriet derīguma termiņu", + "domain_cannot_match_hostname": "Domēns nevar atbilst saimniekdatora nosaukumam" }, "diagnostics": { "cname_from_a": "Vērtība, kas iegūta no A/AAAA ieraksta. Tas tiek atbalstīts tik ilgi, kamēr ieraksts norāda uz pareizo resursu.", @@ -248,14 +251,14 @@ "force_pw_update": "Piespiedu paroles atjaunošana pie nākošās pieslēgšanās", "force_pw_update_info": "Šis lietotājs varēs pieslēgties tikai %s.", "full_name": "Pilns vārds", - "hostname": "Hosta nosaukums", + "hostname": "Saimniekdatora nosaukums", "inactive": "Neaktīvs", "kind": "Veids", "mailbox": "Rediģēt pastkasti", "max_aliases": "Lielākais aizstājvārdu skaits", "max_mailboxes": "Maks. iespējamās pastkastes", "max_quota": "Maks. kvota uz pastkasti (MiB)", - "maxage": "Maximum age of messages in days that will be polled from remote
(0 = ignore age)", + "maxage": "Lielākais ziņojumu, kuri tiks vaicāti attālajā serverī, vecums dienās
(0 = neņemt vērā vecumu)", "maxbytespersecond": "Maks. baiti sekundē (0 ir vienāds ar neierobežotu skaitu)", "mins_interval": "Intervāls (min)", "multiple_bookings": "Vairāki rezervējumi", @@ -325,7 +328,7 @@ }, "mailbox": { "action": "Rīcība", - "activate": "Activate", + "activate": "Aktivizēt", "active": "Aktīvs", "add": "Pievienot", "add_alias": "Pievienot aizstājvārdu", @@ -449,11 +452,11 @@ "qinfo": "Karantīnas sistēma datubāzē saglabās noraidīto pastu (sūtītājam netiks radīts iespaids par piegādātu pastu), kā arī pastu, kas tiek piegādāts kā kopija pastkastes mēstuļu mapē.\n
\"Apgūt kā surogātpastu un izdzēst\" apgūs ziņojumu kā surogātpastu ar Bajesa teorēmu un aprēķinās arī nestriktas jaucējvērtības, lai nākotnē noraidītu līdzīgus ziņojumus.\n
Lūgums apzināties, ka vairāku ziņojumu apgūšana var būt laikietilpīga atkarībā no sistēmas.
Melnā saraksta vienumi karantīnā netiek iekļauti." }, "queue": { - "queue_manager": "Queue Manager", + "queue_manager": "Rindas pārvaldnieks", "info": "Pasta rinda satur visus e-pastus, kas gaida piegādi. Ja e-pasts ir iestrēdzis pasta rindā ilgu laiku, sistēma to automātiski izdzēš.
Attiecīgā pasta kļūdas ziņojums sniedz informāciju par to, kāpēc pastu neizdevās piegādāt." }, "start": { - "help": "Rādīt/Paslēp palīdzības paneli", + "help": "Rādīt/Paslēpt palīdzības paneli", "imap_smtp_server_auth_info": "Lūgums izmantot pilnu e-pasta adresi un PLAIN autentifikācijas mehānismu.
\nPieteikšanās dati tiks šifrēti ar servera puses obligātu šifrēšanu.", "mailcow_apps_detail": "Izmantojiet lietotni mailcow, lai piekļūtu savam pastam, kalendāram, kontaktiem un citām lietām.", "mailcow_panel_detail": "Domēna pārvaldītāji izveido, maina vai izdzēš pastkastes un aizstājvārdus, maina domēnus un lasa papildu informāciju par piešķirtajiem domēniem.
\nPastkastes lietotāji var izveidot laikā ierobežotus aizstājvārdus (surogātpasta aizstājvārdus), mainīt savu paroli un surogātpasta atlasīšanas iestatījumus." @@ -489,7 +492,7 @@ "relayhost_added": "Relejhosts %s pievienots", "relayhost_removed": "Relejhosts %s noņemts", "reset_main_logo": "Atjaunot noklusējuma logotipu", - "resource_added": "Resource %s has been added", + "resource_added": "Avots %s tika pievienots", "resource_modified": "Izmaiņas %s ir saglabātas", "resource_removed": "Resurs %s tika noņemts", "ui_texts": "Saglabāt UI izmaiņas tekstiem", @@ -513,7 +516,8 @@ "webauthn": "WebAuthn autentifikācija", "waiting_usb_auth": "Gaida USB ierīci...

Lūdzu, tagad nospiežiet pogu uz Jūsu WebAuthn USB ierīces.", "waiting_usb_register": "Gaida USB ierīci...

Lūdzu augšā ievadiet Jūsu paroli un apstipriniet WebAuthn reģistrāciju nospiežot pogu uz Jūsu WebAuthn USB ierīces.", - "yubi_otp": "Yubico OTP autentifikators" + "yubi_otp": "Yubico OTP autentifikators", + "authenticators": "Autentificētāji" }, "user": { "action": "Rīcība", @@ -557,7 +561,7 @@ "no_record": "Nav ieraksta", "password_now": "Pašreizējā parole (Apstiprināt izmaiņas)", "remove": "Noņemt", - "running": "Running", + "running": "Darbojas", "save_changes": "Saglabāt izmaiņas", "shared_aliases": "Kopīgotās aizstājadreses", "shared_aliases_desc": "Tādi lietotāja iestatījumi kā surogātpasta atlasīšana vai šifrēšanas nosacījumi neietekmē kopīgotos aizstājvārdus. Atbilstošu surogātpasta atlasi var izveidot tikai pārvaldītājs kā domēnu aptverošu nosacījumu.", @@ -594,7 +598,7 @@ "tls_policy_warning": "Brīdinājums: Ja tiek izlemts ieviest šifrēta pasta nosūtīšanu, var tikt pazaudēti e-pasti.
Ziņojumi, kas neatbilst nosacījumiem, pasta sistēma atmetīs ar kļūdu.
Šī iespēja attiecas uz galveno e-pasta adresi (pieteikšanās vārdu), visām adresēm, kas atvasinātas no aizsājdomēniem, kā arī aizstājadreses, kas norāda tikai uz šo pastkasti.", "user_settings": "Lietotāja iestatījumi", "username": "Lietotājvārds", - "waiting": "Waiting", + "waiting": "Gaida", "week": "Nedēļa", "weeks": "Nedēļas", "open_logs": "Atvērt žurnālus", @@ -615,7 +619,13 @@ "system_containers": "Sistēma un konteineri", "current_time": "Sistēmas laiks", "external_logs": "Ārējie žurnāli", - "logs": "Žurnāli" + "logs": "Žurnāli", + "architecture": "Arhitektūra", + "disk_usage": "Diska lietojums", + "jvm_memory_solr": "JVM atmiņas lietojums", + "memory": "Atmiņa", + "timezone": "Laika josla", + "uptime": "Darbošanās laiks" }, "warning": { "domain_added_sogo_failed": "Domēns pievienots, bet neizdevās pārsāknēt SOGO. Lūgums pārbaudīt servera žurnālus.", diff --git a/data/web/lang/lang.zh-tw.json b/data/web/lang/lang.zh-tw.json index 6dcd519e6..2017d1ddd 100644 --- a/data/web/lang/lang.zh-tw.json +++ b/data/web/lang/lang.zh-tw.json @@ -28,7 +28,8 @@ "spam_score": "垃圾郵件分數", "syncjobs": "同步任務", "tls_policy": "TLS 規則", - "unlimited_quota": "信箱容量配額無上限" + "unlimited_quota": "信箱容量配額無上限", + "pw_reset": "允許重置mailcow使用者密碼" }, "add": { "activate_filter_warn": "當\"啟用\"被勾選,所有其他過濾器都會被消除。", @@ -351,7 +352,14 @@ "ip_check_opt_in": "選擇使用第三方服務 ipv4.mailcow.emailipv6.mailcow.email 來解析外部 IP 位址。", "ip_check_disabled": "IP 檢查已停用。 您可以在
系統 > 配置 > 選項 > 自訂下啟用它", "options": "選項", - "queue_unban": "解除禁令" + "queue_unban": "解除禁令", + "reset_password_vars": "{{link}} 生成密碼重置連結
{{username}} 請求密碼重置的使用者信箱
{{username2}} 備援電子信箱名稱
{{date}} 密碼重置要求之日期
{{token_lifetime}} 權杖(token)壽命(以分鐘計)
{{hostname}} mailcow 主機名稱(hostname)", + "password_reset_info": "若無提供備援電子郵件,此功能將無法使用。", + "password_reset_settings": "密碼復原設定", + "password_reset_tmpl_html": "HTML 範本", + "password_reset_tmpl_text": "文字範本", + "password_settings": "密碼設定", + "restore_template": "留白以還原預設範本" }, "danger": { "access_denied": "存取拒絕或表單資料有誤", @@ -480,7 +488,14 @@ "extended_sender_acl_denied": "缺少設定外部寄件者地址的 ACL", "template_exists": "模板 %s 已存在", "template_id_invalid": "範本 ID %s 無效", - "template_name_invalid": "模板名稱無效" + "template_name_invalid": "模板名稱無效", + "password_reset_invalid_user": "查無此信箱或無設定備援郵件", + "password_reset_na": "密碼恢復目前無法使用。請聯繫您的管理員。", + "to_invalid": "收件者不能為空", + "img_size_exceeded": "圖片超出最大檔案大小", + "recovery_email_failed": "無法寄送還原電子郵件。請聯絡您的管理員。", + "reset_token_limit_exceeded": "重置權杖(token)已達上限。請稍後再試。", + "img_dimensions_exceeded": "圖片超出最大圖片大小" }, "debug": { "chart_this_server": "圖表 (此伺服器)", @@ -650,7 +665,19 @@ "validate_save": "驗證並儲存", "domain_footer_info": "網域範圍的頁尾將會新增至與該網域內的位址關聯的所有外發電子郵件。
以下變數可用於頁尾:", "custom_attributes": "自訂屬性", - "pushover_sound": "聲音" + "pushover_sound": "聲音", + "domain_footer": "全域頁腳", + "domain_footer_info_vars": { + "from_name": "{= from_name =} - 從信封中的名稱部分,例如:\"Mailcow <moo@mailcow.tld>\" 則回傳 \"Mailcow\"", + "auth_user": "{= auth_user =} - MTA所認證之指定使用者名稱", + "from_user": "{= from_user =} - 從信封中的使用者部分。例如:\"moo@mailcow.tld\" 則回傳\"moo\"", + "from_addr": "{= from_addr =} - 從信封中的地址部分", + "from_domain": "{= from_domain =} - 從信封中的域名(domain)部分" + }, + "domain_footer_skip_replies": "於回信中忽落頁腳", + "domain_footer_html": "HTML 頁腳", + "password_recovery_email": "密碼重置備援信箱", + "domain_footer_plain": "純文字頁腳" }, "fido2": { "confirm": "確認", @@ -706,7 +733,14 @@ "mobileconfig_info": "請使用信箱使用者登入以下載 Apple 連接描述檔案。", "other_logins": "金鑰登入", "password": "密碼", - "username": "使用者名稱" + "username": "使用者名稱", + "back_to_mailcow": "返回至mailcow", + "invalid_pass_reset_token": "密碼重置權杖(token)無效或已過期
請重新請求新的重置連結。", + "forgot_password": "> 忘記密碼?", + "new_password": "新密碼", + "new_password_confirm": "確認新密碼", + "reset_password": "重置密碼", + "request_reset_password": "要求重置密碼" }, "mailbox": { "action": "操作", @@ -1042,7 +1076,9 @@ "cors_headers_edited": "CORS 設定已儲存", "domain_footer_modified": "網域頁尾 %s 的變更已儲存", "f2b_banlist_refreshed": "禁止清單 ID 已成功刷新。", - "ip_check_opt_in_modified": "IP檢查已成功儲存" + "ip_check_opt_in_modified": "IP檢查已成功儲存", + "password_changed_success": "密碼重置成功", + "recovery_email_sent": "已寄送復原郵件至 %s" }, "tfa": { "api_register": "%s 使用 Yubico Cloud API,請在這裡為這把金鑰獲取 API 金鑰", @@ -1069,7 +1105,8 @@ "webauthn": "WebAuthn 認證", "waiting_usb_auth": "等待 USB 裝置...

請觸碰 USB 裝置上的按鈕。", "waiting_usb_register": "等待 USB 裝置...

請輸入密碼並觸碰 USB 裝置上的按鈕來確認註冊。", - "yubi_otp": "Yubico OTP 認證" + "yubi_otp": "Yubico OTP 認證", + "authenticators": "驗證器(Authenticators)" }, "user": { "action": "操作", @@ -1233,7 +1270,9 @@ "years": "年", "attribute": "屬性", "pushover_sound": "聲音", - "value": "數值" + "value": "數值", + "password_reset_info": "若無提供密碼備援電子信箱,此功能將無法使用。", + "pw_recovery_email": "密碼備援電子信箱" }, "warning": { "cannot_delete_self": "不能刪除已登入的使用者", From 672bb345fd755ce3aeb13ca584fde8a5ec9eec68 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Fri, 25 Oct 2024 10:47:53 +0200 Subject: [PATCH 30/37] Fix mailbox_rename de-de translation --- data/web/lang/lang.de-de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index d22187c6a..807393d77 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -641,7 +641,7 @@ "mailbox": "Mailbox bearbeiten", "mailbox_quota_def": "Standard-Quota einer Mailbox", "mailbox_relayhost_info": "Wird auf eine Mailbox und direkte Alias-Adressen angewendet. Überschreibt die Einstellung einer Domain.", - "mailbox_rename": "Mailbox umbennnen", + "mailbox_rename": "Mailbox umbenennen", "mailbox_rename_agree": "Ich habe ein Backup erstellt.", "mailbox_rename_warning": "WICHTIG! Vor dem Umbenennen der Mailbox ein Backup erstellen.", "mailbox_rename_alias": "Alias automatisch erstellen", From d8c8e4ab1bc9168f0ed5ecb4074dcd42176ad7ea Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Thu, 31 Oct 2024 11:00:03 +0100 Subject: [PATCH 31/37] [DockerApi] Fix IMAP ACL migration issue when renaming mailbox --- .../dockerapi/modules/DockerApi.py | 46 ++++++++++++------- data/web/inc/functions.mailbox.inc.php | 6 ++- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/data/Dockerfiles/dockerapi/modules/DockerApi.py b/data/Dockerfiles/dockerapi/modules/DockerApi.py index 64bcc4d95..4701cbf51 100644 --- a/data/Dockerfiles/dockerapi/modules/DockerApi.py +++ b/data/Dockerfiles/dockerapi/modules/DockerApi.py @@ -429,24 +429,38 @@ class DockerApi: formatted_acls = [] mailbox_seen = [] for shared_folder in shared_folders: - if "Shared" not in shared_folder and "/" not in shared_folder: - continue - shared_folder = shared_folder.split("/") - if len(shared_folder) < 3: - continue + if "Shared" not in shared_folder: + mailbox = shared_folder.replace("'", "'\\''") + if mailbox in mailbox_seen: + 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 '{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 - 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:]: - _, rights = acls[1].split(maxsplit=1) - mailbox_seen.append(mailbox) - formatted_acls.append({ 'user': user, 'id': id, 'mailbox': mailbox, 'rights': rights.split() }) + 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 diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 9dec594b5..9de665675 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3364,12 +3364,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // 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' => $imap_acl['user'], + 'user' => $user, 'mailbox' => $imap_acl['mailbox'], - 'id' => $new_username, + 'id' => $user_id, 'rights' => $imap_acl['rights'] ); docker('post', 'dovecot-mailcow', 'exec', $exec_fields); From 93cf99cc9e62c7eb9d0d08ce41e8ba2787e07e7b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 2 Nov 2024 20:38:18 +0100 Subject: [PATCH 32/37] chore(deps): update thollander/actions-comment-pull-request action to v3.0.1 (#6130) Signed-off-by: milkmaker Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/check_prs_if_on_staging.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_prs_if_on_staging.yml b/.github/workflows/check_prs_if_on_staging.yml index 6ba3b89c0..7f840c568 100644 --- a/.github/workflows/check_prs_if_on_staging.yml +++ b/.github/workflows/check_prs_if_on_staging.yml @@ -10,7 +10,7 @@ jobs: if: github.event.pull_request.base.ref != 'staging' #check if the target branch is not staging steps: - name: Send message - uses: thollander/actions-comment-pull-request@v3.0.0 + uses: thollander/actions-comment-pull-request@v3.0.1 with: GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }} message: | From 8b2f71f97ee31e36d5544c79accf4cac14b2041e Mon Sep 17 00:00:00 2001 From: milkmaker Date: Tue, 5 Nov 2024 16:20:57 +0100 Subject: [PATCH 33/37] update postscreen_access.cidr (#6129) --- data/conf/postfix/postscreen_access.cidr | 39 +++++++++++++++--------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/data/conf/postfix/postscreen_access.cidr b/data/conf/postfix/postscreen_access.cidr index b9a185aa5..7a1cbf6ae 100644 --- a/data/conf/postfix/postscreen_access.cidr +++ b/data/conf/postfix/postscreen_access.cidr @@ -1,6 +1,6 @@ -# Whitelist generated by Postwhite v3.4 on Tue Oct 1 00:18:56 UTC 2024 +# Whitelist generated by Postwhite v3.4 on Fri Nov 1 00:18:49 UTC 2024 # https://github.com/stevejenkins/postwhite/ -# 1970 total rules +# 2013 total rules 2a00:1450:4000::/36 permit 2a01:111:f400::/48 permit 2a01:111:f403:8000::/50 permit @@ -21,7 +21,6 @@ 8.25.196.0/23 permit 8.39.54.0/23 permit 8.40.222.0/23 permit -10.162.0.0/16 permit 12.130.86.238 permit 13.110.208.0/21 permit 13.110.209.0/24 permit @@ -32,11 +31,10 @@ 15.200.21.50 permit 15.200.44.248 permit 15.200.201.185 permit -17.41.0.0/16 permit 17.57.155.0/24 permit 17.57.156.0/24 permit 17.58.0.0/16 permit -17.142.0.0/15 permit +17.143.234.140/30 permit 18.156.89.250 permit 18.157.243.190 permit 18.194.95.56 permit @@ -123,6 +121,7 @@ 44.217.45.156 permit 44.236.56.93 permit 44.238.220.251 permit +45.14.148.0/22 permit 46.19.170.16 permit 46.226.48.0/21 permit 46.228.36.37 permit @@ -184,7 +183,9 @@ 50.18.126.162 permit 50.31.32.0/19 permit 50.31.36.205 permit -50.56.130.220/30 permit +50.56.130.220 permit +50.56.130.221 permit +50.56.130.222 permit 52.1.14.157 permit 52.5.230.59 permit 52.27.5.72 permit @@ -244,16 +245,12 @@ 54.244.54.130 permit 54.244.242.0/24 permit 54.255.61.23 permit +57.103.64.0/18 permit 62.13.128.0/24 permit -62.13.128.196 permit 62.13.129.128/25 permit -62.13.136.0/22 permit -62.13.140.0/22 permit -62.13.144.0/22 permit -62.13.148.0/23 permit -62.13.150.0/23 permit -62.13.152.0/23 permit -62.13.159.196 permit +62.13.136.0/21 permit +62.13.144.0/21 permit +62.13.152.0/21 permit 62.17.146.128/26 permit 62.179.121.0/24 permit 62.201.172.0/27 permit @@ -1319,7 +1316,9 @@ 129.41.77.70 permit 129.41.169.249 permit 129.80.5.164 permit +129.80.64.36 permit 129.80.67.121 permit +129.80.145.156 permit 129.145.74.12 permit 129.146.88.28 permit 129.146.147.105 permit @@ -1330,6 +1329,9 @@ 129.153.168.146 permit 129.153.190.200 permit 129.153.194.228 permit +129.154.255.129 permit +129.158.56.255 permit +129.159.22.159 permit 129.159.87.137 permit 129.213.195.191 permit 130.61.9.72 permit @@ -1353,6 +1355,7 @@ 135.84.216.0/22 permit 136.143.160.0/24 permit 136.143.161.0/24 permit +136.143.162.0/24 permit 136.143.178.49 permit 136.143.182.0/23 permit 136.143.184.0/24 permit @@ -1383,6 +1386,7 @@ 141.193.185.32/27 permit 141.193.185.64/26 permit 141.193.185.128/25 permit +143.47.120.152 permit 143.55.224.0/21 permit 143.55.232.0/22 permit 143.55.236.0/22 permit @@ -1396,7 +1400,10 @@ 144.178.38.0/24 permit 145.253.228.160/29 permit 145.253.239.128/29 permit -146.20.14.104/30 permit +146.20.14.104 permit +146.20.14.105 permit +146.20.14.106 permit +146.20.14.107 permit 146.20.112.0/26 permit 146.20.113.0/24 permit 146.20.191.0/24 permit @@ -1737,6 +1744,8 @@ 204.92.114.187 permit 204.92.114.203 permit 204.92.114.204/31 permit +204.141.32.0/23 permit +204.141.42.0/23 permit 204.220.160.0/21 permit 204.220.168.0/21 permit 204.220.176.0/20 permit From 6ec1e357c3027a39cb3016f36ba1e56295205919 Mon Sep 17 00:00:00 2001 From: Niklas Meyer Date: Tue, 5 Nov 2024 16:21:14 +0100 Subject: [PATCH 34/37] fix: broken sogo cron notifications (for appointments etc.) (#6128) --- .gitignore | 1 + data/Dockerfiles/dovecot/docker-entrypoint.sh | 2 ++ docker-compose.yml | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index e25639a70..b22abfd6e 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ data/conf/rspamd/override.d/* data/conf/sogo/custom-theme.js data/conf/sogo/plist_ldap data/conf/sogo/sieve.creds +data/conf/sogo/cron.creds data/conf/sogo/sogo-full.svg data/gitea/ data/gogs/ diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 35c4691c5..55eed7956 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -371,6 +371,8 @@ EOF # Create random master Password for SOGo SSO RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1) echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass +# Creating additional creds file for SOGo notify crons (calendars, etc) +echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds cat < /etc/dovecot/sogo-sso.conf # Autogenerated by mailcow passdb { diff --git a/docker-compose.yml b/docker-compose.yml index 4e7bfb2ed..c462ba88c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -211,9 +211,9 @@ services: ofelia.job-exec.sogo_sessions.schedule: "@every 1m" ofelia.job-exec.sogo_sessions.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool -v expire-sessions $${SOGO_EXPIRE_SESSION} || exit 0\"" ofelia.job-exec.sogo_ealarms.schedule: "@every 1m" - ofelia.job-exec.sogo_ealarms.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-ealarms-notify -p /etc/sogo/sieve.creds || exit 0\"" + ofelia.job-exec.sogo_ealarms.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-ealarms-notify -p /etc/sogo/cron.creds || exit 0\"" ofelia.job-exec.sogo_eautoreply.schedule: "@every 5m" - ofelia.job-exec.sogo_eautoreply.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/sieve.creds || exit 0\"" + ofelia.job-exec.sogo_eautoreply.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/cron.creds || exit 0\"" ofelia.job-exec.sogo_backup.schedule: "@every 24h" ofelia.job-exec.sogo_backup.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool backup /sogo_backup ALL || exit 0\"" restart: always From be79f320d28cbdd1e6b19d8807d2b93367ec1a69 Mon Sep 17 00:00:00 2001 From: milkmaker Date: Wed, 6 Nov 2024 19:08:53 +0100 Subject: [PATCH 35/37] Translations update from Weblate (#6140) * [Web] Updated lang.lv-lv.json Co-authored-by: Edgars Andersons * [Web] Updated lang.tr-tr.json Co-authored-by: Furkan --------- Co-authored-by: Edgars Andersons Co-authored-by: Furkan --- data/web/lang/lang.lv-lv.json | 65 +++++++++++++++++++++++++---------- data/web/lang/lang.tr-tr.json | 15 ++++++-- 2 files changed, 58 insertions(+), 22 deletions(-) diff --git a/data/web/lang/lang.lv-lv.json b/data/web/lang/lang.lv-lv.json index 31c31301c..98cb04ae8 100644 --- a/data/web/lang/lang.lv-lv.json +++ b/data/web/lang/lang.lv-lv.json @@ -70,7 +70,8 @@ "validate": "Apstiprināt", "validation_success": "Apstiprināts veiksmīgi", "bcc_dest_format": "BCC galamērķim ir jābūt vienai derīgai e-pasta adresei.
Ja ir nepieciešams nosūtīt kopiju vairākām adresēm, jāizveido aizstājvārds un jāizmanto tas šeit.", - "domain_matches_hostname": "Domēns %s atbilst saimniekdatora nosaukumam" + "domain_matches_hostname": "Domēns %s atbilst saimniekdatora nosaukumam", + "disable_login": "Neļaut pieteikšanos (ienākošais pasts joprojām tiks pieņemts)" }, "admin": { "access": "Pieeja", @@ -115,7 +116,7 @@ "forwarding_hosts": "Hostu pārsūtīšana", "forwarding_hosts_add_hint": "Var norādīt vai nu IPv4/IPv6 addreses, tīklu ar CIDR apzīmējumu, saimniekdatoru nosaukumus (kas tiks atrisināti IP adresēs) vai arī domēna vārdus (kas tiks atrisināti IP adresēs, vaicājot SPF ierakstus, vai, ja tādu nav, MX ierakstus).", "forwarding_hosts_hint": "Ienākošie ziņojumi tiek bez nosacījumiem pieņemti no visiem šeit norādītajiem saimniekdatoriem. Tie tad netiek pārbaudīti pret DNSBL vai pakļauti ievietošanai pelēkajā sarakstā. No tiem saņemtās mēstules nekad netiek noraidītas, bet pēc izvēles tās var pārvietot mapē \"Nevēlams\". Visbiežāk to izmanto, lai norādītu pasta serverus, kuros ir uzstādīts nosacījums, kas pārsūta ienākošās e-pasta vēstules uz Tavu mailcow serveri.", - "help_text": "Ignorēt palīdzības tekstu zem pieteikšanās maskas (HTML ir atļauta)", + "help_text": "Pārrakstīt palīdzības tekstu zem pieteikšanās maskas (var izmantot HTML)", "host": "Hosts", "import": "Importēt", "import_private_key": "Importēt privātu atslēgu", @@ -167,7 +168,9 @@ "sys_mails": "Sistēmas pasts", "ip_check_disabled": "IP pārbaude ir atspējota. To var iespējot
Sistēma > Konfigurācija > iespējas > Pielāgot", "rspamd_com_settings": "Iestatījuma nosaukums tiks izveidots automātiski. Lūgums zemāk skatīt priekšiestatījumu piemērus. Vairāk informācijas ir Rspamd dokumentācijā", - "reset_password_vars": "{{link}} Izveidotā paroles atiestatīšanas saite
{{username}} Lietotāja, kurš pieprasīja paroles atiestatīšanu, pastkastes nosaukums
{{username2}} Atkopšanas pastkastes nosaukums
{{date}} Paroles atiestatīšanas pieprasījuma veikšanas datums
{{token_lifetime}} Pilnvaras derīgums minūtēs
{{hostname}} mailcow saimniekdatora nosaukums" + "reset_password_vars": "{{link}} Izveidotā paroles atiestatīšanas saite
{{username}} Lietotāja, kurš pieprasīja paroles atiestatīšanu, pastkastes nosaukums
{{username2}} Atkopšanas pastkastes nosaukums
{{date}} Paroles atiestatīšanas pieprasījuma veikšanas datums
{{token_lifetime}} Pilnvaras derīgums minūtēs
{{hostname}} mailcow saimniekdatora nosaukums", + "ui_header_announcement_help": "Paziņojums ir redzams visiem lietotājiem, kuri ir pieteikušies, un pieteikšanās ekrānā saskarnē.", + "login_time": "Pieteikšanās laiks" }, "danger": { "access_denied": "Piekļuve liegta, vai nepareizi dati", @@ -194,7 +197,7 @@ "is_alias_or_mailbox": "%s jau ir zināms kā aizstājvārds, pastkaste vai aizstājadrese, kas ir izvērsta no aizstājdomēna.", "is_spam_alias": "%s jau ir zināma kā pagaidu aizstājadrese (mēstuļu aizstājadrese)", "last_key": "Pēdējo atslēgu nevar izdzēst, tā vietā jāatspējo divpakāpju pārbaude.", - "login_failed": "Ielogošanās neveiksmīga", + "login_failed": "Pieteikšanās neizdevās", "mailbox_invalid": "Pastkastes vārds ir nederīgs", "mailbox_quota_exceeded": "Kvota pārsniedz domēna limitu (max. %d MiB)", "mailbox_quota_exceeds_domain_quota": "Pastkastes izmērs pārsniedz maksimāli pieļaujamo", @@ -248,8 +251,8 @@ "edit_alias_domain": "Labot aizstājdomēnu", "encryption": "Šifrēšana", "exclude": "Neiekļaut objektus (regex)", - "force_pw_update": "Piespiedu paroles atjaunošana pie nākošās pieslēgšanās", - "force_pw_update_info": "Šis lietotājs varēs pieslēgties tikai %s.", + "force_pw_update": "Piespiedu paroles atjaunošana nākamajā pieteikšanās reizē", + "force_pw_update_info": "Šis lietotājs varēs pieteikties tikai %s. Lietotņu paroles joprojām būs izmantojamas.", "full_name": "Pilns vārds", "hostname": "Saimniekdatora nosaukums", "inactive": "Neaktīvs", @@ -295,16 +298,19 @@ "mailbox_relayhost_info": "Tiek piemērots tikai pastkastei un tiešajiem aizstājvārdiem, aizstāj domēna retranslācijas saimniekdatoru.", "sender_acl_info": "Ja pastkastes lietotājam A ir ļauts sūtīt kā pastkastes lietotājam B, sūtītāja adrese SOGo netiek automātiski parādīta kā atlasāms lauks \"no\".
.\n Pastkastes lietotājam B SOGo ir jāizveido pilnvarojums, lai pastkastes lietotājs A varētu izvēlēties tā adresi kā sūtītāja. Lai SOGo pilnvarotu pastkasti, pasta skatā jāizmanto izvēlne (trīs punkti) pa labi no pastkastes nosaukuma augšējā kreisajā pusē. Šī darbība neattiecas uz aizstājadresēm.", "sogo_visible": "Aizstājvārds ir redzams SOGo", - "sogo_visible_info": "Šī iespēja ietekmē tikai tos objektus, kurus var parādīt SOGo (koplietojamās vai nekoplietojamās aizstājadreses, kas norāda uz vismaz vienu vietējo pastkasti). Ja paslēpts, netiks parādīts SOGo kā atlasāms sūtītājs." + "sogo_visible_info": "Šī iespēja ietekmē tikai tos objektus, kurus var parādīt SOGo (koplietojamās vai nekoplietojamās aizstājadreses, kas norāda uz vismaz vienu vietējo pastkasti). Ja paslēpts, netiks parādīts SOGo kā atlasāms sūtītājs.", + "mbox_rl_info": "Šis pieprasījumu ierobežojums tiek piemērots SASL pieteikšanās vārdam, tas atbilst jebkurai \"from\" adresei, ko izmanto lietotājs, kurš ir pieteicies. Pastkastes pieprasījumu ierobežojums pārraksta domēna mēroga pieprasījumu ierobežojumu.", + "sogo_access": "Nodrošināt tiešu pieteikšanās piekļuvi SOGo", + "disable_login": "Neļaut pieteikšanos (ienākošais pasts joprojām tiks pieņemts)" }, "footer": { "cancel": "Atcelt", "confirm_delete": "Apstiprināt dzēšanu", "delete_now": "Dzēst tagad", "delete_these_items": "Lūgums apstiprināt izmaiņas šim objekta Id", - "loading": "Lūdzu uzgaidiet...", + "loading": "Lūgums uzgaidīt...", "restart_container": "Restartēt konteineri", - "restart_container_info": "Important: Piespiedu restartēšana var aizņemt ilgu laiku, lūdzu uzgaidiet.", + "restart_container_info": "Svarīgi: nesteidzīga pārsāknēšana var aizņemt ilgāku laiku. Lūgums uzgaidīt, līdz tā tiek pabeigta.", "restart_now": "Pārsāknēt tagad" }, "header": { @@ -321,10 +327,13 @@ "no_action": "No action applicable" }, "login": { - "delayed": "Pieteikšanās tika aizkavēta %s sekundēm.", - "login": "Pieslēgties", + "delayed": "Pieteikšanās tika aizkavēta %s sekundes.", + "login": "Pieteikties", "password": "Parole", - "username": "Lietotājvārds" + "username": "Lietotājvārds", + "fido2_webauthn": "FIDO/WebAuthn pieteikšanās", + "mobileconfig_info": "Lūgums pieteikties kā pastkastes lietotājam, lai lejupielādētu pieprasīto Apple savienojuma profilu.", + "other_logins": "Pieteikšanās ar atslēgu" }, "mailbox": { "action": "Rīcība", @@ -370,7 +379,7 @@ "filter_table": "Filtra tabula", "filters": "Filtri", "fname": "Pilns vārds", - "force_pw_update": "Piespiedu paroles atjaunošana pie nākošās pieslēgšanās", + "force_pw_update": "Piespiedu paroles atjaunināšana nākamajā pieteikšanās reizē", "in_use": "Lietošanā (%)", "inactive": "Neaktīvs", "kind": "Veids", @@ -423,7 +432,8 @@ "sogo_visible_y": "Rādīt aizstājvārdu SOGo", "add_alias_expand": "Izvērst aizstājvārdu pār aizstājdomēniem", "alias_domain_alias_hint": "Aizstājvārdi netiek automātiski piemēroti domēnu aizstājvārdiem. Aizstājadrese my-alias@domain nenosedz adresi my-alias@alias-domain (kur \"alias-domain\" ir iedomāts \"domain\" aizstājdomēns).
Lūgums izmantot sieta atlasi, lai pārvirzītu pastu uz ārēju pastkasti (skatīt cilti \"Atlasīšana\" vai izmantot SOGo -> Pārsūtītājs). \"Izvērst aizstājvārdu pār aizstājdomēniem\" ir izmantojams, lai automātiski pievienotu trūkstošos aiztājvārdus.", - "alias_domain_backupmx": "Aizstājdomēns ir neaktīvs retranslācijas domēnam" + "alias_domain_backupmx": "Aizstājdomēns ir neaktīvs retranslācijas domēnam", + "disable_login": "Neļaut pieteikšanos (ienākošais pasts joprojām tiks pieņemts)" }, "quarantine": { "action": "Darbības", @@ -457,7 +467,7 @@ }, "start": { "help": "Rādīt/Paslēpt palīdzības paneli", - "imap_smtp_server_auth_info": "Lūgums izmantot pilnu e-pasta adresi un PLAIN autentifikācijas mehānismu.
\nPieteikšanās dati tiks šifrēti ar servera puses obligātu šifrēšanu.", + "imap_smtp_server_auth_info": "Lūgums izmantot pilnu e-pasta adresi un PLAIN autentificēšanās mehānismu.
\nPieteikšanās dati tiks šifrēti ar servera puses obligātu šifrēšanu.", "mailcow_apps_detail": "Izmantojiet lietotni mailcow, lai piekļūtu savam pastam, kalendāram, kontaktiem un citām lietām.", "mailcow_panel_detail": "Domēna pārvaldītāji izveido, maina vai izdzēš pastkastes un aizstājvārdus, maina domēnus un lasa papildu informāciju par piešķirtajiem domēniem.
\nPastkastes lietotāji var izveidot laikā ierobežotus aizstājvārdus (surogātpasta aizstājvārdus), mainīt savu paroli un surogātpasta atlasīšanas iestatījumus." }, @@ -496,14 +506,18 @@ "resource_modified": "Izmaiņas %s ir saglabātas", "resource_removed": "Resurs %s tika noņemts", "ui_texts": "Saglabāt UI izmaiņas tekstiem", - "upload_success": "Faila augšupielāde veiksmīga" + "upload_success": "Faila augšupielāde veiksmīga", + "verified_fido2_login": "Apliecināta FIDO2 pieteikšanās", + "verified_webauthn_login": "Apliecināta WebAuthn pieteikšanās", + "verified_totp_login": "Apliecināta TOTP pieteikšanās", + "verified_yotp_login": "Apliecināta Yubico OTP pieteikšanās" }, "tfa": { "api_register": "%s izmanto Yubico Cloud API. Lūdzu iegūstiet API atslēgu priekš Jūsu atslēgashere", "confirm": "Apstiprināt", "confirm_totp_token": "Lūdzu apstipriniet Jūsu izmaiņas ievadot uzģenerēto tekstu", "delete_tfa": "Atspējot TFA", - "disable_tfa": "Atspējot TFA līdz nākamajai veiksmīgai pieteikšanās", + "disable_tfa": "Atspējot TFA līdz nākamajai veiksmīgajai pieteikšanās reizei", "enter_qr_code": "TOTP kods, ja Tava ierīce nevar nolasīt kvadrātkodus", "key_id": "Jūsu YubiKey identifikators", "key_id_totp": "Identifikators Jūsu atslēgai", @@ -595,7 +609,7 @@ "tls_enforce_in": "Piespiest TLS ienākošajiem", "tls_enforce_out": "Piespiest TLS izejošajiem", "tls_policy": "Šifrēšanas politika", - "tls_policy_warning": "Brīdinājums: Ja tiek izlemts ieviest šifrēta pasta nosūtīšanu, var tikt pazaudēti e-pasti.
Ziņojumi, kas neatbilst nosacījumiem, pasta sistēma atmetīs ar kļūdu.
Šī iespēja attiecas uz galveno e-pasta adresi (pieteikšanās vārdu), visām adresēm, kas atvasinātas no aizsājdomēniem, kā arī aizstājadreses, kas norāda tikai uz šo pastkasti.", + "tls_policy_warning": "Brīdinājums: ja tiek izlemts ieviest šifrēta pasta nosūtīšanu, var tikt pazaudēti e-pasta ziņojumi.
Ziņojumi, kas neatbilst nosacījumiem, pasta sistēma atmetīs ar kļūdu.
Šī iespēja attiecas uz galveno e-pasta adresi (pieteikšanās vārdu), visām adresēm, kas atvasinātas no aizstājdomēniem, kā arī aizstājadreses, kas norāda tikai uz šo pastkasti.", "user_settings": "Lietotāja iestatījumi", "username": "Lietotājvārds", "waiting": "Gaida", @@ -603,7 +617,14 @@ "weeks": "Nedēļas", "open_logs": "Atvērt žurnālus", "apple_connection_profile_mailonly": "Šis savienojuma profils iekļauj IMAP un SMTP konfigurācijas parametrus Apple ierīcei.", - "pushover_info": "Pašpiegādes paziņojumu iestatījumi attieksies uz visu tīro (ne surogātpasta) pastu, kas piegādāts uz %s, ieskaitot aizstājvārdus (kopīgotus, nekopīgotus, ar birkām)." + "pushover_info": "Pašpiegādes paziņojumu iestatījumi attieksies uz visu tīro (ne surogātpasta) pastu, kas piegādāts uz %s, ieskaitot aizstājvārdus (kopīgotus, nekopīgotus, ar birkām).", + "app_hint": "Lietotņu paroles ir aizstājējparoles, lai pieteiktos IMAP, SMTP, CalDAV, CardDAV un EAS. Lietotājvārds paliek nemainīgs. SOGo tīmekļa pasts nav pieejams ar lietotņu parolēm.", + "direct_protocol_access": "Šim pastkastes lietotājam ir tieša, ārēja piekļuve zemāk uzskaitītajiem protokoliem un lietotnēm. Šo iestatījumu pārrauga pārvaldītājs. Lietotņu paroles var izveidot, lai nodrošinātu piekļuvi atsevišķiem protokoliem un lietotnēm.
Poga \"Pieteikties tīmekļa pastā\" nodrošina vienotu pieteikšanos SOGo un vienmēr ir pieejama.", + "last_ui_login": "Pēdējā pieteikšanās saskarnē", + "login_history": "Pieteikšanās vēsture", + "no_last_login": "Nav informācijas par pēdējām pieteikšanās saskarnē reizēm", + "open_webmail_sso": "Pieteikšanās tīmekļa pastā", + "last_mail_login": "Pēdējā pasta pieteikšanās" }, "datatables": { "paginate": { @@ -631,5 +652,11 @@ "domain_added_sogo_failed": "Domēns pievienots, bet neizdevās pārsāknēt SOGO. Lūgums pārbaudīt servera žurnālus.", "dovecot_restart_failed": "Dovecot neizdevās pārsāknēties. Lūgums pārbaudīt žurnālus", "is_not_primary_alias": "Izlaists aizstājvārds %s, kas nav galvenais" + }, + "oauth2": { + "access_denied": "Lūgums pieteikties kā pastkastes īpašniekam, lai nodrošinātu piekļuvi ar OAuth2." + }, + "fido2": { + "fido2_auth": "Pieteikties ar FIDO2" } } diff --git a/data/web/lang/lang.tr-tr.json b/data/web/lang/lang.tr-tr.json index e7fb623ce..7049046c2 100644 --- a/data/web/lang/lang.tr-tr.json +++ b/data/web/lang/lang.tr-tr.json @@ -28,7 +28,8 @@ "spam_score": "Spam skoru", "syncjobs": "Görevleri senkronize et", "tls_policy": "TLS ilkesi", - "unlimited_quota": "E-postalar için sınırsız kota" + "unlimited_quota": "E-postalar için sınırsız kota", + "pw_reset": "Mailcow kullanıcı şifresini sıfırlamaya izin ver" }, "add": { "activate_filter_warn": "Aktif edilirse diğer tüm filtreler devre dışı bırakılacak.", @@ -350,7 +351,13 @@ "reset_limit": "Hashi kaldır", "routing": "Yönlendirme", "rsetting_add_rule": "Kural ekle", - "rsetting_content": "Kural içeriği" + "rsetting_content": "Kural içeriği", + "password_reset_info": "Herhangi bir kurtarma e-postası sağlanmamışsa, bu işlev kullanılamaz.", + "restore_template": "Varsayılan şablonu geri yüklemek için boş bırakın.", + "password_reset_settings": "Parola Kurtarma Ayarları", + "password_reset_tmpl_html": "HTML Şablonu", + "password_reset_tmpl_text": "Metin Şablonu", + "password_settings": "Şifre Ayarları" }, "warning": { "cannot_delete_self": "Cannot delete logged in user", @@ -661,7 +668,9 @@ "tls_policy_map_entry_exists": "Bir TLS ilke haritası girişi \\\"%s\\\" mevcut", "tls_policy_map_parameter_invalid": "Politika parametresi geçersiz", "totp_verification_failed": "TOTP doğrulaması başarısız oldu", - "transport_dest_exists": "\\\"%s\\\" aktarım hedefi mevcut" + "transport_dest_exists": "\\\"%s\\\" aktarım hedefi mevcut", + "password_reset_invalid_user": "Posta kutusu bulunamadı veya kurtarma e-postası ayarlanmadı", + "invalid_reset_token": "Geçersiz sıfırlama token'ı" }, "debug": { "container_disabled": "Container durduruldu veya devre dışı bırakıldı", From 52f3f93aeef939dcf831ba26e82d1c9e34aa54b8 Mon Sep 17 00:00:00 2001 From: Niklas Meyer Date: Mon, 11 Nov 2024 16:50:14 +0100 Subject: [PATCH 36/37] update.sh: precaution ask for deletion of dns_blocklists.cf if old format (#6154) --- update.sh | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/update.sh b/update.sh index fe8aee72f..cf94eb71c 100755 --- a/update.sh +++ b/update.sh @@ -275,6 +275,34 @@ detect_bad_asn() { fi } +fix_broken_dnslist_conf() { + +# Fixing issue: #6143. To be removed in a later patch + + local file="${SCRIPT_DIR}/data/conf/postfix/dns_blocklists.cf" + # Check if the file exists + if [[ ! -f "$file" ]]; then + return 1 + fi + + # Check if the file contains the autogenerated comment + if grep -q "# Autogenerated by mailcow" "$file"; then + # Ask the user if custom changes were made + echo -e "\e[91mWARNING!!! \e[31mAn old version of dns_blocklists.cnf has been detected which may cause a broken postfix upon startup (see: https://github.com/mailcow/mailcow-dockerized/issues/6143)...\e[0m" + echo -e "\e[31mIf you have any custom settings in there you might copy it away and adapt the changes after the file is regenerated...\e[0m" + read -p "Do you want to delete the file now and let mailcow regenerate it properly? " response + if [[ "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + rm "$file" + echo -e "\e[32mdns_blocklists.cf has been deleted and will be properly regenerated" + return 0 + else + echo -e "\e[35mOk, not deleting it! Please make sure you take a look at postfix upon start then..." + return 2 + fi + fi + +} + ############## End Function Section ############## # Check permissions @@ -437,6 +465,8 @@ source mailcow.conf detect_docker_compose_command +fix_broken_dnslist_conf + DOTS=${MAILCOW_HOSTNAME//[^.]}; if [ ${#DOTS} -lt 1 ]; then echo -e "\e[31mMAILCOW_HOSTNAME (${MAILCOW_HOSTNAME}) is not a FQDN!\e[0m" From dc5a28111d1d77915234d1e4d9f2853fb9e4b59a Mon Sep 17 00:00:00 2001 From: milkmaker Date: Mon, 11 Nov 2024 21:39:15 +0100 Subject: [PATCH 37/37] [Web] Updated lang.zh-cn.json (#6151) [Web] Updated lang.zh-cn.json Co-authored-by: Easton Man --- data/web/lang/lang.zh-cn.json | 145 ++++++++++++++++++++++++++++------ 1 file changed, 123 insertions(+), 22 deletions(-) diff --git a/data/web/lang/lang.zh-cn.json b/data/web/lang/lang.zh-cn.json index bac42e5d7..ca6c9aaf2 100644 --- a/data/web/lang/lang.zh-cn.json +++ b/data/web/lang/lang.zh-cn.json @@ -63,7 +63,7 @@ "exclude": "拒绝对象 (Regex)", "full_name": "全称", "gal": "全球地址簿", - "gal_info": "全球地址簿包含了域名下的所有对象,并且此行为不能被用户更改。如果关闭,用户的 \"空闲/繁忙\" 的状态将无法在 SOGo 中显示。 重启 SOGo 服务以应用更改。", + "gal_info": "全球地址簿包含了域名下的所有对象,并且此行为不能被用户更改。如果关闭,用户的 \"空闲/繁忙\" 的状态将无法在 SOGo 中显示。 重启 SOGo 服务以应用更改。", "generate": "生成", "goto_ham": "学习为非垃圾邮件", "goto_null": "静默丢弃邮件", @@ -146,7 +146,7 @@ "arrival_time": "到达时间 (服务器时间)", "authed_user": "已认证用户", "ays": "确定继续操作?", - "ban_list_info": "以下为被封禁的 IP 列表: 网络 (剩余封禁时间) - [操作]
被取消封禁的 IP 将会在几秒之内从封禁列表中移除
红色标签表示因黑名单而导致的永久封禁", + "ban_list_info": "以下为被封禁的 IP 列表: 网络 (剩余封禁时间) - [操作]
被取消封禁的 IP 将会在几秒之内从封禁列表中移除
红色标签表示因黑名单而导致的永久封禁。", "change_logo": "更改 Logo", "configuration": "配置", "convert_html_to_text": "将 HTML 转换为纯文本内容", @@ -292,7 +292,7 @@ "rsettings_preset_2": "允许管理员接收垃圾邮件", "rsettings_preset_3": "只允许指定的发件人发信 (例如只允许内部邮箱发送)", "rsettings_preset_4": "禁用域名的 Rspamd 服务", - "rspamd_com_settings": "设置名称将会自动生成,请看参考下方的示例预设。查看Rspamd 文档以了解更多的细节。", + "rspamd_com_settings": "设置名称将会自动生成,请看参考下方的示例预设。查看Rspamd 文档以了解更多的细节", "rspamd_global_filters": "全局过滤规则", "rspamd_global_filters_agree": "我会小心谨慎的!", "rspamd_global_filters_info": "全局过滤规则包含了不同类型的全局黑名单和白名单。", @@ -353,7 +353,7 @@ "password_reset_tmpl_html": "HTML 模版", "password_reset_tmpl_text": "文字模版", "password_settings": "密码设定", - "restore_template": "留空以恢复预设模版", + "restore_template": "留空以恢复预设模版。", "ip_check": "IP 检查", "ip_check_disabled": "IP 检查已禁用。你可透过以下路径启用
系统 > 配置 > 选项 > 页面自定义", "queue_unban": "解除封禁", @@ -481,7 +481,21 @@ "template_exists": "模板 %s 已存在", "template_name_invalid": "模板名称无效", "cors_invalid_method": "制定的允许模式无效", - "cors_invalid_origin": "指定的允许原无效" + "cors_invalid_origin": "指定的允许原无效", + "reset_token_limit_exceeded": "Reset token 数量已达上限。请稍后再尝试。", + "extended_sender_acl_denied": "缺少设置外部发送者地址的 ACL", + "img_dimensions_exceeded": "图片超出最大图片大小", + "img_size_exceeded": "图片超过最大文件大小", + "invalid_reset_token": "不合法的 reset token", + "password_reset_invalid_user": "未找到此信箱或未设置恢复邮箱", + "password_reset_na": "密码恢复目前无法使用。请联系您的管理员。", + "recovery_email_failed": "无法发送恢复邮件。请联系您的管理员。", + "template_id_invalid": "Template ID %s 无效", + "to_invalid": "收件人不能为空", + "webauthn_authenticator_failed": "找不到所选的 authenticator", + "webauthn_publickey_failed": "没有为选定的身份验证器保存公钥", + "webauthn_username_failed": "所选的 authenticator 属于另一个账户", + "demo_mode_enabled": "演示模式已开启" }, "debug": { "chart_this_server": "图表 (此服务器)", @@ -516,7 +530,13 @@ "error_show_ip": "无法解析公网IP地址", "show_ip": "显示公网IP", "update_available": "有可用更新", - "update_failed": "无法检查更新" + "update_failed": "无法检查更新", + "architecture": "结构", + "container_stopped": "已停止", + "current_time": "系统时间", + "timezone": "时区", + "no_update_available": "系统已经是最新版本", + "wip": "正在施工中" }, "diagnostics": { "cname_from_a": "来自 A/AAAA 记录的值。但只要记录指向正确的资源即可。", @@ -640,7 +660,31 @@ "title": "编辑对象", "unchanged_if_empty": "如果不更改则留空", "username": "用户名", - "validate_save": "验证并保存" + "validate_save": "验证并保存", + "domain_footer_info_vars": { + "from_name": "{= from_name =} - 信件中的 From name 字段,例如对于 \"Mailcow <moo@mailcow.tld>\" 来说它是 \"Mailcow\"", + "auth_user": "{= auth_user =} - MTA 指定的经过认证的用户名", + "from_user": "{= from_user =} - 信件中的 From user 字段,例如对于 \"moo@mailcow.tld\" 来说它是 \"moo\"", + "from_addr": "{= from_addr =} - 信件中的 From address 字段", + "from_domain": "{= from_domain =} - 信件中的 From domain 字段", + "custom": "{= foo =} - 如果信箱有一个自定义属性 \"foo\" 的值为 \"bar\", 它将返回 \"bar\"" + }, + "created_on": "创建于", + "custom_attributes": "自定义属性", + "mailbox_rename": "重命名信箱", + "mailbox_rename_agree": "我已经创建了一个备份。", + "mailbox_rename_warning": "重要! 重命名信箱之前请先创建备份。", + "mailbox_rename_alias": "自动创建别名", + "mailbox_rename_title": "新本地信箱的名字", + "password_recovery_email": "密码重置邮箱", + "domain_footer": "域页脚", + "domain_footer_html": "HTML footer", + "domain_footer_info": "Domain-wide footers 会被加入到该 domain 下的地址发出的所有邮件中。
下列变量可以在 footer 中使用:", + "domain_footer_plain": "纯文字 footer", + "domain_footer_skip_replies": "在回信中忽略 footer", + "footer_exclude": "从 footer 中排除", + "last_modified": "上次修改时间", + "pushover_sound": "声音" }, "fido2": { "confirm": "确认", @@ -675,13 +719,14 @@ "header": { "administration": "配置和管理", "apps": "应用", - "debug": "系统信息", + "debug": "信息", "email": "E-Mail", "mailcow_config": "配置", "quarantine": "隔离", "restart_netfilter": "重启 netfilter", "restart_sogo": "重启 SOGo", - "user_settings": "用户设置" + "user_settings": "用户设置", + "mailcow_system": "系统" }, "info": { "awaiting_tfa_confirmation": "等待 TFA 确认", @@ -695,7 +740,14 @@ "mobileconfig_info": "请使用邮箱用户登录以下载 Apple 连接描述文件。", "other_logins": "Key 登录", "password": "密码", - "username": "用户名" + "username": "用户名", + "forgot_password": "> 忘记密码?", + "back_to_mailcow": "返回到 mailcow", + "new_password": "新密码", + "new_password_confirm": "确认新密码", + "reset_password": "重置密码", + "request_reset_password": "请求重置密码", + "invalid_pass_reset_token": "密码重置 token 无效或已过期。
请重新获取新的密码重置链接。" }, "mailbox": { "action": "操作", @@ -799,9 +851,9 @@ "recipient_map": "收件人映射", "recipient_map_info": "收件人映射用于在邮件被发送前替换收件人的地址。", "recipient_map_new": "新收件人", - "recipient_map_new_info": "新收件人必须为合法的邮箱地址", + "recipient_map_new_info": "新收件人必须为合法的邮箱地址。", "recipient_map_old": "原收件人", - "recipient_map_old_info": "原收件人必须为合法的邮箱地址", + "recipient_map_old_info": "原收件人必须为合法的邮箱地址。", "recipient_maps": "收件人映射", "relay_all": "中继所有收件人", "remove": "删除", @@ -818,7 +870,7 @@ "sieve_preset_5": "自动回复 (休假)", "sieve_preset_6": "拒绝接收邮件并通知", "sieve_preset_7": "重定向邮件并保留或删除", - "sieve_preset_8": "删除发件人发送给自己别名地址的邮件", + "sieve_preset_8": "重定向来自特定发件人的邮件,标记为已读并放入子文件夹中", "sieve_preset_header": "请看下方的示例预设。 查看 Sieve Wikipedia 页面 (英文)以了解更多细节。", "sogo_visible": "SOGo 别名显示", "sogo_visible_n": "在 SOGo 中隐藏别名", @@ -860,10 +912,20 @@ "mailbox_templates": "邮箱模板", "gal": "全局地址列表", "max_aliases": "最大别名数", - "max_mailboxes": "最大可能的邮箱数" + "max_mailboxes": "最大可能的邮箱数", + "created_on": "建立于", + "force_pw_update": "强制在下一次登陆时更新密码", + "add_template": "新增模板", + "goto_ham": "学习为 非垃圾邮件 ", + "goto_spam": "学习为 垃圾邮件 ", + "last_modified": "上次修改时间", + "max_quota": "每个信箱的最大容量配额", + "relay_unknown": "转发未知信箱", + "templates": "模板", + "template": "模板" }, "oauth2": { - "access_denied": "请作为邮箱所有者登录以使用 OAuth2 授权", + "access_denied": "请作为邮箱所有者登录以使用 OAuth2 授权。", "authorize_app": "授权应用", "deny": "拒绝", "permit": "授权应用", @@ -926,7 +988,19 @@ }, "queue": { "queue_manager": "队列管理器", - "delete": "全部删除" + "delete": "全部删除", + "info": "邮件队列包含所有等待投递的邮件。如果邮件长时间停留在邮件队列中,系统会自动将其删除。
对应的邮件错误信息会提供有关邮件无法送达的原因。", + "flush": "刷新队列", + "legend": "邮件队列操作功能:", + "ays": "请确认您要删除当前队列中的所有项目。", + "deliver_mail": "投递", + "deliver_mail_legend": "尝试重新投递选中的邮件。", + "hold_mail": "保留", + "hold_mail_legend": "保持选中的邮件。(不继续投递)", + "show_message": "显示内容", + "unban": "队列解除限制", + "unhold_mail": "取消保持", + "unhold_mail_legend": "允许选中的邮件继续投递。(需要在保持状态)" }, "ratelimit": { "disabled": "禁用", @@ -994,7 +1068,7 @@ "nginx_reloaded": "Nginx 已重新启动", "object_modified": "已保存对象 %s 更改", "password_policy_saved": "已成功保存密码规则", - "pushover_settings_edited": "已成功设置 Pushover,请重新校验凭证", + "pushover_settings_edited": "Pushover 设置已保存,请重新校验凭证。", "qlearn_spam": "消息 ID %s 已被学习为垃圾邮件并被删除", "queue_command_success": "成功执行配额命令", "recipient_map_entry_deleted": "已删除接收人映射 ID %s", @@ -1018,7 +1092,17 @@ "verified_fido2_login": "FIDO2 登录验证成功", "verified_totp_login": "TOTP 登录验证成功", "verified_webauthn_login": "WebAuthn 登录验证成功", - "verified_yotp_login": "Yubico OTP 登录验证成功" + "verified_yotp_login": "Yubico OTP 登录验证成功", + "domain_footer_modified": "对域 footer %s 的修改已保存", + "cors_headers_edited": "CORS 设置已保存", + "ip_check_opt_in_modified": "IP 检查已保存", + "f2b_banlist_refreshed": "黑名单 ID 已成功刷新。", + "mailbox_renamed": "信箱 %s 已被重命名为 %s", + "password_changed_success": "密码重置成功", + "recovery_email_sent": "重置邮件已发送至 %s", + "template_added": "新增了模板 %s", + "template_modified": "模板 %s 的修改已保存", + "template_removed": "模板 ID %s 已删除" }, "tfa": { "api_register": "%s 使用了 Yubico Cloud API,请在此为你的密钥获取 API 密钥", @@ -1045,7 +1129,8 @@ "webauthn": "WebAuthn 认证", "waiting_usb_auth": "等待 USB 设备中...

现在请触碰你的 WebAuthn USB 设备上的按钮。", "waiting_usb_register": "等待 USB 设备中...

请在上方输入你的密码并请触碰你的 WebAuthn USB 设备上的按钮以确认注册该 WebAuthn 设备。", - "yubi_otp": "Yubico OTP 认证" + "yubi_otp": "Yubico OTP 认证", + "authenticators": "验证器(Authenticators)" }, "user": { "action": "操作", @@ -1062,7 +1147,7 @@ "alias_valid_until": "有效至", "aliases_also_send_as": "同时允许发送为", "aliases_send_as_all": "已关闭发件人可访性检查的域名和域名别名", - "app_hint": "应用密码是你登录 IMAP 和 SMTP 时的可选替代密码,用户名仍然保持不变。
应用密码不适用于 SOGo (包括 ActiveSync) ", + "app_hint": "应用密码是你登录 IMAP 和 SMTP 时的可选替代密码,用户名仍然保持不变。
应用密码不适用于 SOGo (包括 ActiveSync)。", "allowed_protocols": "允许使用的协议", "app_name": "应用名称", "app_passwds": "应用密码", @@ -1206,7 +1291,12 @@ "weeks": "周", "with_app_password": "包含应用密码", "year": "年", - "years": "年" + "years": "年", + "pw_recovery_email": "密码重置邮箱", + "password_reset_info": "如果不提供密码重置邮箱,此功能将无法使用。", + "pushover_sound": "声音", + "value": "值", + "attribute": "属性" }, "warning": { "cannot_delete_self": "不能删除已登录的用户", @@ -1233,6 +1323,17 @@ "last": "最后一页", "previous": "上一页", "next": "下一页" - } + }, + "aria": { + "sortDescending": ": 激活以降序对列进行排序", + "sortAscending": ": 激活以升序对列进行排序" + }, + "decimal": ".", + "emptyTable": "表中没有可用的数据", + "infoFiltered": "(从 _MAX_ 个总条目中过滤)", + "thousands": ",", + "lengthMenu": "显示 _MENU_ 条目", + "loadingRecords": "加载中...", + "zeroRecords": "未找到符合条件的记录" } }