Merge branch 'develop' into template-v2

This commit is contained in:
Kijin Sung 2023-10-10 14:23:39 +09:00
commit c777b59afb
296 changed files with 2392 additions and 1676 deletions

View file

@ -164,7 +164,10 @@ class Config
}
// Remove the backup file.
Storage::delete($backup_filename);
if (isset($backup_filename))
{
Storage::delete($backup_filename);
}
// Save XE-compatible config files.
$warning = '// THIS FILE IS NOT USED IN RHYMIX.' . "\n" . '// TO MODIFY SYSTEM CONFIGURATION, EDIT config.php INSTEAD.';

View file

@ -595,7 +595,7 @@ class Debug
);
self::$_remote_requests[] = $request_object;
if ($request_object->elapsed_time && $request_object->elapsed_time >= (self::$_config['log_slow_remote_requests'] ?? 1))
if ($request_object->elapsed_time && is_numeric($request_object->elapsed_time) && $request_object->elapsed_time >= (self::$_config['log_slow_remote_requests'] ?? 1))
{
self::$_slow_remote_requests[] = $request_object;
}

View file

@ -364,7 +364,7 @@ class Session
$_SESSION['RHYMIX']['ipaddress'] = $_SESSION['ipaddress'] = \RX_CLIENT_IP;
$_SESSION['RHYMIX']['useragent'] = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
$_SESSION['RHYMIX']['language'] = \Context::getLangType();
$_SESSION['RHYMIX']['timezone'] = DateTime::getTimezoneForCurrentUser();
// $_SESSION['RHYMIX']['timezone'] = DateTime::getTimezoneForCurrentUser();
$_SESSION['RHYMIX']['secret'] = Security::getRandom(32, 'alnum');
$_SESSION['RHYMIX']['domains'] = array();
$_SESSION['RHYMIX']['tokens'] = array();

View file

@ -38,6 +38,18 @@ class HTMLFilter
'web-share' => true,
);
/**
* List of tags where data-* attributes are allowed.
*/
protected static $_data_allowed = array(
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'p',
'a', 'span', 'img', 'picture', 'b', 'i', 'strong', 'em', 'u', 's', 'sub', 'sup',
'header', 'footer', 'nav', 'main', 'section', 'article', 'aside', 'details', 'summary',
'ul', 'ol', 'li', 'mark', 'wbr', 'figure', 'figcaption', 'caption',
'table', 'thead', 'tbody', 'tr', 'th', 'td', 'ins', 'del',
'iframe', 'video', 'audio', 'source', 'track', 'blockquote', 'code',
);
/**
* Prepend a pre-processing filter.
*
@ -93,11 +105,6 @@ class HTMLFilter
*/
public static function clean(string $input, $allow_classes = false, bool $allow_editor_components = true, bool $allow_widgets = false): string
{
foreach (self::$_preproc as $callback)
{
$input = $callback($input);
}
if ($allow_classes === true)
{
$allowed_classes = null;
@ -119,13 +126,20 @@ class HTMLFilter
}
}
$input = self::_preprocess($input, $allow_editor_components, $allow_widgets);
$output = self::getHTMLPurifier($allowed_classes)->purify($input);
$output = self::_postprocess($output, $allow_editor_components, $allow_widgets);
$purifier = self::getHTMLPurifier($allowed_classes);
foreach (self::$_preproc as $callback)
{
$input = $callback($input, $purifier, $allow_editor_components, $allow_widgets);
}
$input = self::_preprocess($input, $purifier, $allow_editor_components, $allow_widgets);
$output = $purifier->purify($input);
$output = self::_postprocess($output, $purifier, $allow_editor_components, $allow_widgets);
foreach (self::$_postproc as $callback)
{
$output = $callback($output);
$output = $callback($output, $purifier, $allow_editor_components, $allow_widgets);
}
return $output;
@ -214,13 +228,6 @@ class HTMLFilter
$config->set('Cache.SerializerPath', \RX_BASEDIR . 'files/cache/htmlpurifier');
Storage::createDirectory(\RX_BASEDIR . 'files/cache/htmlpurifier');
// Modify the HTML definition to support editor components and widgets.
$def = $config->getHTMLDefinition(true);
$def->addAttribute('img', 'editor_component', 'Text');
$def->addAttribute('div', 'editor_component', 'Text');
$def->addAttribute('img', 'rx_encoded_properties', 'Text');
$def->addAttribute('div', 'rx_encoded_properties', 'Text');
// Support HTML5 and CSS3.
self::_supportHTML5($config);
self::_supportCSS3($config);
@ -255,6 +262,8 @@ class HTMLFilter
$def->addElement('section', 'Block', 'Flow', 'Common');
$def->addElement('article', 'Block', 'Flow', 'Common');
$def->addElement('aside', 'Block', 'Flow', 'Common');
$def->addElement('details', 'Block', 'Flow', 'Common');
$def->addElement('summary', 'Block', 'Flow', 'Common');
// Add various inline tags.
$def->addElement('s', 'Inline', 'Inline', 'Common');
@ -313,15 +322,28 @@ class HTMLFilter
));
// Support additional properties.
$def->addAttribute('details', 'open', 'Bool');
$def->addAttribute('i', 'aria-hidden', 'Text');
$def->addAttribute('img', 'srcset', 'Text');
$def->addAttribute('img', 'data-file-srl', 'Number');
$def->addAttribute('iframe', 'allow', 'Text');
$def->addAttribute('iframe', 'allowfullscreen', 'Bool');
$def->addAttribute('iframe', 'referrerpolicy', 'Enum#no-referrer,no-referrer-when-downgrade,origin,origin-when-cross-origin,same-origin,strict-origin,strict-origin-when-cross-origin,unsafe-url');
// Support contenteditable="false" (#1710)
$def->addAttribute('div', 'contenteditable', 'Enum#false');
// Support editor components and widgets.
$def->addAttribute('img', 'data-file-srl', 'Number');
$def->addAttribute('img', 'editor_component', 'Text');
$def->addAttribute('div', 'editor_component', 'Text');
$def->addAttribute('img', 'rx_encoded_properties', 'Text');
$def->addAttribute('div', 'rx_encoded_properties', 'Text');
// Support encoded data-* attributes for some tags.
foreach (self::$_data_allowed as $tag)
{
$def->addAttribute($tag, 'rx_encoded_datas', 'Text');
}
}
/**
@ -480,17 +502,21 @@ class HTMLFilter
* Rhymix-specific preprocessing method.
*
* @param string $content
* @param \HTMLPurifier $purifier
* @param bool $allow_editor_components (optional)
* @param bool $allow_widgets (optional)
* @return string
*/
protected static function _preprocess(string $content, bool $allow_editor_components = true, bool $allow_widgets = false): string
protected static function _preprocess(string $content, \HTMLPurifier $purifier, bool $allow_editor_components = true, bool $allow_widgets = false): string
{
// Encode widget and editor component properties so that they are not removed by HTMLPurifier.
if ($allow_editor_components || $allow_widgets)
{
$content = self::_encodeWidgetsAndEditorComponents($content, $allow_editor_components, $allow_widgets);
}
// Encode data-* attributes.
$content = self::_encodeDataAttributes($content);
return $content;
}
@ -498,11 +524,12 @@ class HTMLFilter
* Rhymix-specific postprocessing method.
*
* @param string $content
* @param \HTMLPurifier $purifier
* @param bool $allow_editor_components (optional)
* @param bool $allow_widgets (optional)
* @return string
*/
protected static function _postprocess(string $content, bool $allow_editor_components = true, bool $allow_widgets = false): string
protected static function _postprocess(string $content, \HTMLPurifier $purifier, bool $allow_editor_components = true, bool $allow_widgets = false): string
{
// Define acts to allow and deny.
$allow_acts = array('procFileDownload');
@ -558,6 +585,9 @@ class HTMLFilter
// Restore widget and editor component properties.
$content = self::_decodeWidgetsAndEditorComponents($content, $allow_editor_components, $allow_widgets);
// Restore data-* attributes.
$content = self::_decodeDataAttributes($content);
return $content;
}
@ -598,7 +628,7 @@ class HTMLFilter
{
return $attr[0];
}
$attrval = utf8_normalize_spaces(utf8_clean(html_entity_decode($attr[2])));
$attrval = trim(utf8_normalize_spaces(utf8_clean(html_entity_decode($attr[2]))));
if (preg_match('/^javascript:/i', preg_replace('/\s+/', '', $attrval)))
{
return '';
@ -653,7 +683,69 @@ class HTMLFilter
}
foreach ($decoded_properties as $key => $val)
{
$attrs[] = $key . '="' . htmlspecialchars($val) . '"';
$attrs[] = $key . '="' . htmlspecialchars($val, ENT_QUOTES, 'UTF-8') . '"';
}
return str_replace($match[3], ' ' . implode(' ', $attrs), $match[0]);
}, $content);
}
/**
* Encode data-* attributes so that they will survive being passed through HTMLPurifier.
*
* @param string $content
* @return string
*/
protected static function _encodeDataAttributes(string $content): string
{
$tags = implode('|', self::$_data_allowed);
return preg_replace_callback('!<(' . $tags . ')\s([^>]+)>!i', function($match) {
$attrs = array();
$html = preg_replace_callback('!\s(data-[a-zA-Z0-9_-]+)="([^"]*)"!', function($attr) use(&$attrs) {
$attrkey = strtolower($attr[1]);
$attrval = trim(utf8_normalize_spaces(utf8_clean(html_entity_decode($attr[2]))));
if (preg_match('/^(data-file-srl)$/', $attrkey))
{
return $attr[0];
}
if (preg_match('/^javascript:/i', preg_replace('/\s+/', '', $attrval)))
{
return '';
}
$attrs[$attrkey] = $attrval;
return '';
}, $match[0]);
$encoded_datas = base64_encode(json_encode($attrs));
$encoded_datas = $encoded_datas . ':' . Security::createSignature($encoded_datas);
return rtrim($html, ' />') . ' rx_encoded_datas="' . $encoded_datas . '"' . (preg_match('!/>$!', $html) ? ' />' : '>');
}, $content);
}
/**
* Decode data-* attributes after processing.
*
* @param string $content
* @param bool $allow_editor_components (optional)
* @param bool $allow_widgets (optional)
* @return string
*/
protected static function _decodeDataAttributes(string $content): string
{
$tags = implode('|', self::$_data_allowed);
return preg_replace_callback('!<(' . $tags . ')([^>]*)(\srx_encoded_datas="([^"]+)")!i', function($match) {
$attrs = array();
list($encoded_datas, $signature) = explode(':', $match[4]);
if (!Security::verifySignature($encoded_datas, $signature))
{
return str_replace($match[3], '', $match[0]);
}
$encoded_datas = json_decode(base64_decode($encoded_datas));
if (!$encoded_datas)
{
return str_replace($match[3], '', $match[0]);
}
foreach ($encoded_datas as $key => $val)
{
$attrs[] = $key . '="' . htmlspecialchars($val, ENT_QUOTES, 'UTF-8') . '"';
}
return str_replace($match[3], ' ' . implode(' ', $attrs), $match[0]);
}, $content);

View file

@ -280,7 +280,7 @@ class DBQueryParser extends BaseParser
{
$group = new DBQuery\ConditionGroup;
$group->conditions = self::_parseConditions($tag);
$group->pipe = strtoupper($attribs['pipe'] ?? null) ?: 'AND';
$group->pipe = strtoupper($attribs['pipe'] ?? '') ?: 'AND';
$group->ifvar = $attribs['if'] ?? null;
$result[] = $group;
}

View file

@ -55,7 +55,7 @@ class VariableBase
{
if ($this->not_null)
{
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . $this->column . ' must not be null');
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . ($this->column ?? ($this->name ?? 'unknown')) . ' must not be null');
}
if ($this instanceof Condition && in_array($this->operation, ['equal', 'notequal', 'not_equal']))
{
@ -89,7 +89,7 @@ class VariableBase
}
elseif ($this->not_null)
{
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . ($this->column ?? 'unknown') . ' is not set');
throw new \Rhymix\Framework\Exceptions\QueryError('Variable ' . $this->var . ' for column ' . ($this->column ?? ($this->name ?? 'unknown')) . ' is not set');
}
elseif (!in_array($this->operation, ['null', 'notnull', 'not_null']))
{