table = 'mail_template'; } /** * @return int */ public function get_count() { $row = Database::select( 'count(*) as count', $this->table, ['where' => ['url_id = ? ' => api_get_current_access_url_id()]], 'first' ); return $row['count']; } /** * Displays the title + grid. * * @return string html code */ public function display() { // Action links $html = '
'; $html .= ''. Display::return_icon( 'back.png', get_lang('Back'), '', '32' ) .''; $html .= ''. Display::return_icon( 'add.png', get_lang('Add'), '', '32' ).''; $html .= '
'; $html .= Display::grid_html('mail_template'); return $html; } /** * Returns a Form validator Obj. * * @param string $url * @param string $action * * @return FormValidator */ public function returnForm($url, $action = 'add') { $form = new FormValidator('template', 'post', $url); // Setting the form elements $header = get_lang('Add'); if ($action === 'edit') { $header = get_lang('Modify'); } $id = isset($_GET['id']) ? (int) $_GET['id'] : ''; $form->addElement('header', '', $header); $form->addElement('hidden', 'id', $id); $form->addElement( 'text', 'name', get_lang('Name'), ['size' => '70', 'id' => 'name'] ); /*$form->addHtmlEditor( 'email_template', get_lang('Template'), false, false, [ 'ToolbarSet' => 'Careers', 'Width' => '100%', 'Height' => '250', ] );*/ $form->addTextarea( 'email_template', get_lang('Template'), ['rows' => 20] ); $form->addLabel( get_lang('Allowed template syntax'), $this->getAllowedSyntaxHelp() ); $finder = new Finder(); $files = $finder ->files() ->in(api_get_path(SYS_CODE_PATH).'template/default/mail') ->sort( function ($a, $b) { return strcmp($a->getRealpath(), $b->getRealpath()); } ); $options = []; /** @var SplFileInfo $file */ foreach ($files as $file) { $options[$file->getFilename()] = $file->getFilename(); } $form->addSelect( 'type', get_lang('Type'), $options ); $defaults = $this->get($id); if ($action === 'edit') { $form->addLabel(get_lang('CreatedAt'), Display::dateToStringAgoAndLongDate($defaults['created_at'])); $form->addLabel(get_lang('UpdatedAt'), Display::dateToStringAgoAndLongDate($defaults['updated_at'])); $form->addButtonSave(get_lang('Modify'), 'submit'); } else { $form->addButtonCreate(get_lang('Add'), 'submit'); } // Setting the defaults if (!empty($defaults)) { $defaults['email_template'] = $defaults['template']; } $form->setDefaults($defaults); // Setting the rules $form->addRule('name', get_lang('ThisFieldIsRequired'), 'required'); return $form; } /** * @param int $id * * @return bool */ public function setDefault($id) { $template = $this->get($id); if (empty($template)) { return false; } $type = $template['type']; $urlId = api_get_current_access_url_id(); $sql = "UPDATE {$this->table} SET default_template = 0 WHERE type = '$type' AND url_id = $urlId"; Database::query($sql); $sql = "UPDATE {$this->table} SET default_template = 1 WHERE id = $id"; Database::query($sql); return true; } /** * @param int $templateId * @param array $userInfo * * @return string|false */ public function parseTemplate($templateId, $userInfo) { $templateInfo = $this->get($templateId); if (!empty($templateInfo)) { $emailTemplate = nl2br($templateInfo['template']); $keys = array_keys($userInfo); foreach ($keys as $key) { $emailTemplate = str_replace("{{user.$key}}", $userInfo[$key], $emailTemplate); } $template = new Template(); $template->twig->setLoader(new \Twig_Loader_String()); $emailBody = $template->twig->render($emailTemplate); return $emailBody; } return false; } /** * Gets a custom mail template by the name of the template it replaces. * * @param string $templateType Name of the template file it replaces * * @return string */ public function getTemplateByType($templateType) { if (empty($templateType)) { return ''; } $result = Database::select( 'template', $this->table, ['where' => ['type = ? ' => $templateType, ' AND url_id = ? ' => api_get_current_access_url_id()]], 'first' ); if (empty($result)) { return ''; } return $result['template']; } /** * Builds the help block describing the Twig syntax allowed in a mail * template, derived from the same allowlists enforced by * renderSandboxedTemplate() so the form and the sandbox never drift apart. * * @return string */ public function getAllowedSyntaxHelp(): string { $tags = api_htmlentities(implode(', ', self::ALLOWED_TAGS)); $filters = api_htmlentities(implode(', ', self::ALLOWED_FILTERS)); $functions = api_htmlentities(implode(', ', self::ALLOWED_FUNCTIONS)); $html = '
'; $html .= '

'.api_htmlentities(get_lang('Tags')).': '.$tags.'

'; $html .= '

'.api_htmlentities(get_lang('Filters')).': '.$filters.'

'; $html .= '

'.api_htmlentities(get_lang('Functions')).': '.$functions.'

'; $html .= '

'.api_htmlentities(get_lang('Available variables depend on the template type, for example {{ user.getUsername() }} or {{ user.getEmail() }}.')).'

'; $html .= '

'.api_htmlentities(get_lang('For security reasons, any other Twig function, filter (such as filter, map, reduce or sort) or PHP call is blocked.')).'

'; $html .= '
'; return $html; } /** * Renders an admin-stored mail template body through a sandboxed Twig * environment. * * Stored mail templates are untrusted content (any platform admin can edit * them) and must never be compiled with the full application Twig: the * non-sandboxed environment exposes the callable-accepting filters * (filter/map/reduce/sort) that turn a template body into a Server-Side * Template Injection → Remote Code Execution gadget. * * The sandbox here uses an explicit allow-list of tags, filters, functions * and entity getters; everything else — including the RCE gadget filters — * is rejected. When rendering is refused or fails, an empty string is * returned so the caller falls back to the default file-based template. * * @param string $templateText The admin-stored Twig template body * @param array $params The render context (template variables) * * @return string The rendered body, or '' when rendering is rejected */ public static function renderSandboxedTemplate(string $templateText, array $params): string { if ('' === trim($templateText)) { return ''; } $allowedMethods = [ User::class => [ 'getId', 'getUsername', 'getFirstname', 'getLastname', 'getEmail', 'getStatus', 'getOfficialCode', 'getPhone', ], ]; $allowedProperties = []; $policy = new SecurityPolicy( self::ALLOWED_TAGS, self::ALLOWED_FILTERS, $allowedMethods, $allowedProperties, self::ALLOWED_FUNCTIONS ); $twig = new Environment( new ArrayLoader(['mail_template' => $templateText]), ['autoescape' => 'html', 'cache' => false, 'strict_variables' => false] ); $twig->addExtension(new SandboxExtension($policy, true)); $twig->addFilter(new TwigFilter('get_lang', 'get_lang')); $twig->addFunction(new TwigFunction('get_lang', 'get_lang')); try { return $twig->render('mail_template', $params); } catch (Throwable $e) { error_log('Refused to render stored mail template in sandbox: '.$e->getMessage()); return ''; } } }