Fuente: https://github.com/chamilo/chamilo-lms/releases/download/v1.11.40/chamilo-1.11.40.zip sha256: 1cf4bf2cc7bae1ef1a1eff643235db1d552f78ddf4b6dd1e2d2dac9868679439 Snapshot independiente (rama huerfana); diffable vs 1.11.38. vendor incluido.
357 lines
11 KiB
PHP
357 lines
11 KiB
PHP
<?php
|
|
/* For licensing terms, see /license.txt */
|
|
|
|
use Chamilo\UserBundle\Entity\User;
|
|
use Symfony\Component\Finder\Finder;
|
|
use Twig\Environment;
|
|
use Twig\Extension\SandboxExtension;
|
|
use Twig\Loader\ArrayLoader;
|
|
use Twig\Sandbox\SecurityPolicy;
|
|
use Twig\TwigFilter;
|
|
use Twig\TwigFunction;
|
|
|
|
/**
|
|
* Class MailTemplateManager.
|
|
*/
|
|
class MailTemplateManager extends Model
|
|
{
|
|
/**
|
|
* Twig tags allowed inside an admin-stored mail template body.
|
|
*/
|
|
public const ALLOWED_TAGS = ['if', 'for', 'set', 'apply', 'spaceless', 'autoescape', 'with'];
|
|
|
|
/**
|
|
* Twig filters allowed inside an admin-stored mail template body. The
|
|
* callable-accepting gadget filters (filter/map/reduce/sort) are
|
|
* deliberately excluded to prevent SSTI → RCE.
|
|
*/
|
|
public const ALLOWED_FILTERS = [
|
|
'abs', 'capitalize', 'date', 'date_modify', 'default', 'escape', 'e',
|
|
'first', 'format', 'join', 'json_encode', 'keys', 'last', 'length',
|
|
'lower', 'merge', 'nl2br', 'number_format', 'raw', 'replace', 'reverse',
|
|
'round', 'slice', 'split', 'striptags', 'title', 'trim', 'upper',
|
|
'url_encode', 'get_lang',
|
|
];
|
|
|
|
/**
|
|
* Twig functions allowed inside an admin-stored mail template body.
|
|
*/
|
|
public const ALLOWED_FUNCTIONS = ['max', 'min', 'range', 'get_lang'];
|
|
|
|
public $columns = [
|
|
'id',
|
|
'name',
|
|
'template',
|
|
'type',
|
|
'system',
|
|
'url_id',
|
|
'default_template',
|
|
'created_at',
|
|
'updated_at',
|
|
'author_id',
|
|
];
|
|
|
|
public function __construct()
|
|
{
|
|
parent::__construct();
|
|
|
|
$this->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 = '<div class="actions" style="margin-bottom:20px">';
|
|
$html .= '<a href="'.api_get_path(WEB_CODE_PATH).'admin">'.
|
|
Display::return_icon(
|
|
'back.png',
|
|
get_lang('Back'),
|
|
'',
|
|
'32'
|
|
)
|
|
.'</a>';
|
|
$html .= '<a href="'.api_get_self().'?action=add">'.
|
|
Display::return_icon(
|
|
'add.png',
|
|
get_lang('Add'),
|
|
'',
|
|
'32'
|
|
).'</a>';
|
|
$html .= '</div>';
|
|
$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 = '<div class="space-y-2 mt-2 rounded-lg border border-gray-25 bg-gray-15 p-4 text-sm text-gray-70">';
|
|
$html .= '<p><strong>'.api_htmlentities(get_lang('Tags')).':</strong> <code>'.$tags.'</code></p>';
|
|
$html .= '<p><strong>'.api_htmlentities(get_lang('Filters')).':</strong> <code>'.$filters.'</code></p>';
|
|
$html .= '<p><strong>'.api_htmlentities(get_lang('Functions')).':</strong> <code>'.$functions.'</code></p>';
|
|
$html .= '<p>'.api_htmlentities(get_lang('Available variables depend on the template type, for example {{ user.getUsername() }} or {{ user.getEmail() }}.')).'</p>';
|
|
$html .= '<p>'.api_htmlentities(get_lang('For security reasons, any other Twig function, filter (such as filter, map, reduce or sort) or PHP call is blocked.')).'</p>';
|
|
$html .= '</div>';
|
|
|
|
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 '';
|
|
}
|
|
}
|
|
}
|