Commit ea7a2fac authored by Cédric Anne's avatar Cédric Anne Committed by Johan Cwiklinski
Browse files

Enhanced rich text

parent 65df212e
......@@ -28,16 +28,23 @@ The present file will list all changes made to the project; according to the
#### Deprecated
- Usage of `GLPI_FORCE_EMPTY_SQL_MODE` constant
- Usage of `CommonDBTM::notificationqueueonaction` property
- Usage of `NotificationTarget::html_tags` property
- `DBmysql::getTableSchema()`
- `Calendar::duplicate()`
- `CommonDBTM::clone()`
- `CommonDBTM::prepareInputForClone()`
- `CommonDBTM::post_clone()`
- `Config::getCache()`
- `Html::clean()`
- `Html::setSimpleTextContent()`
- `Html::setRichTextContent()`
- `Html::weblink_extract()`
- `RuleImportComputer` class
- `RuleImportComputerCollection` class
- `Toolbox::doubleEncodeEmails()`
- `Toolbox::getHtmlToDisplay()`
- `Toolbox::useCache()`
- `Toolbox::unclean_html_cross_side_scripting_deep()`
#### Removed
- `Update::declareOldItems()`
......
......@@ -34,6 +34,8 @@
* @since 9.1
*/
use Glpi\Toolbox\RichText;
include ('../inc/includes.php');
header("Content-Type: application/json; charset=UTF-8");
Html::header_nocache();
......@@ -52,14 +54,14 @@ $revision = new KnowbaseItem_Revision();
$revision->getFromDB($oldid);
$old = [
'name' => $revision->fields['name'],
'answer' => Toolbox::unclean_html_cross_side_scripting_deep($revision->fields['answer'])
'answer' => RichText::getSafeHtml($revision->fields['answer'], true)
];
$revision = $diffid == 0 ? new KnowbaseItem() : new KnowbaseItem_Revision();
$revision->getFromDB($diffid == 0 ? $kbid : $diffid);
$diff = [
'name' => $revision->fields['name'],
'answer' => Toolbox::unclean_html_cross_side_scripting_deep($revision->fields['answer'])
'answer' => RichText::getSafeHtml($revision->fields['answer'], true)
];
echo json_encode([
......
......@@ -34,6 +34,8 @@
* @since 9.1
*/
use Glpi\Toolbox\RichText;
include ('../inc/includes.php');
header("Content-Type: application/json; charset=UTF-8");
Html::header_nocache();
......@@ -50,7 +52,7 @@ $revision = new KnowbaseItem_Revision();
$revision->getFromDB($revid);
$rev = [
'name' => $revision->fields['name'],
'answer' => html_entity_decode($revision->fields['answer'])
'answer' => RichText::getSafeHtml($revision->fields['answer'], true)
];
echo json_encode($rev);
......@@ -34,6 +34,8 @@
* @since 9.5
*/
use Glpi\Toolbox\RichText;
$AJAX_INCLUDE = 1;
include ('../inc/includes.php');
......@@ -47,6 +49,11 @@ if (isset($_POST['itilfollowuptemplates_id'])
$template = new ITILFollowupTemplate();
$template->getFromDB($_POST['itilfollowuptemplates_id']);
$template->fields = array_map('html_entity_decode', $template->fields);
echo json_encode($template->fields);
echo json_encode(
[
'content' => RichText::getSafeHtml($template->fields['content'], true),
'requesttypes_id' => $template->fields['requesttypes_id'],
'is_private' => $template->fields['is_private'],
]
);
}
......@@ -30,6 +30,8 @@
* ---------------------------------------------------------------------
*/
use Glpi\Toolbox\RichText;
$AJAX_INCLUDE = 1;
include ('../inc/includes.php');
......@@ -44,7 +46,7 @@ if (isset($_POST['solutiontemplates_id']) && $_POST['solutiontemplates_id'] > 0)
echo json_encode(
[
'content' => Toolbox::unclean_cross_side_scripting_deep($template->fields['content']),
'content' => RichText::getSafeHtml($template->fields['content'], true),
'solutiontypes_id' => $template->fields['solutiontypes_id'],
]
);
......
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "5acd42ee96b63ebc1502a2ad3ceeb813",
"content-hash": "087449288541ba61dffffff4cc7ce073",
"packages": [
{
"name": "blueimp/jquery-file-upload",
......@@ -499,6 +499,47 @@
},
"time": "2021-04-26T09:17:50+00:00"
},
{
"name": "html2text/html2text",
"version": "4.3.1",
"source": {
"type": "git",
"url": "https://github.com/mtibben/html2text.git",
"reference": "61ad68e934066a6f8df29a3d23a6460536d0855c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mtibben/html2text/zipball/61ad68e934066a6f8df29a3d23a6460536d0855c",
"reference": "61ad68e934066a6f8df29a3d23a6460536d0855c",
"shasum": ""
},
"require-dev": {
"phpunit/phpunit": "~4"
},
"suggest": {
"ext-mbstring": "For best performance",
"symfony/polyfill-mbstring": "If you can't install ext-mbstring"
},
"type": "library",
"autoload": {
"psr-4": {
"Html2Text\\": [
"src/",
"test/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "Converts HTML to formatted plain text",
"support": {
"issues": "https://github.com/mtibben/html2text/issues",
"source": "https://github.com/mtibben/html2text/tree/4.3.1"
},
"time": "2020-04-16T23:44:31+00:00"
},
{
"name": "htmlawed/htmlawed",
"version": "1.2.5",
......
......@@ -502,10 +502,6 @@ li.auto_comp {
width:550px;
}
#kbanswer ul {
padding-left: 15px;
}
.tdkb_result {
vertical-align:top;
text-align:left;
......
......@@ -311,6 +311,53 @@ hr {
/* ################--------------- Rich Text ---------------#################### */
.rich_text_container {
p {
margin: 1.12em 0;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
}
h2 {
font-size: 1.5em;
margin: .75em 0;
font-weight: bolder;
}
h3 {
font-size: 1.17em;
margin: .83em 0;
font-weight: bolder;
}
h4 {
margin: 1.12em 0;
font-weight: bolder;
}
h5 {
font-size: .83em;
margin: 1.5em 0;
font-weight: bolder;
}
h6 {
font-size: .75em;
margin: 1.67em 0;
font-weight: bolder;
}
address {
font-style: italic;
}
pre {
font-family: monospace;
white-space: pre;
}
ul {
list-style-type: disc;
margin: 1em 0;
......@@ -347,6 +394,10 @@ hr {
}
}
img {
max-width: 100%;
}
.user-mention, [data-user-mention="true"] {
border-radius: 3px;
padding: 5px;
......@@ -1804,7 +1855,13 @@ td, th {
/* ################--------------- User Picture ---------------#################### */
.qtip {
max-width: 380px !important;
max-width: none;
.qtip-content {
max-height: 250px;
max-width: 400px;
overflow: auto;
}
}
.tooltip {
......@@ -2010,54 +2067,6 @@ img.picture_square {
}
#kbanswer {
ul {
padding-left: 15px;
list-style-type: circle;
}
ol {
padding-left: 15px;
list-style-type: decimal;
}
p {
margin: 1.12em 0;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
}
h2 {
font-size: 1.5em;
margin: .75em 0;
font-weight: bolder;
}
h3 {
font-size: 1.17em;
margin: .83em 0;
font-weight: bolder;
}
h4 {
margin: 1.12em 0;
font-weight: bolder;
}
h5 {
font-size: .83em;
margin: 1.5em 0;
font-weight: bolder;
}
h6 {
font-size: .75em;
margin: 1.67em 0;
font-weight: bolder;
}
h1:target, h2:target, h3:target, h4:target, h5:target, h6:target {
background-color: #fff2a8;
}
......@@ -2075,15 +2084,6 @@ img.picture_square {
h1:hover svg, h2:hover svg, h3:hover svg, h4:hover svg, h5:hover svg, h6:hover svg {
visibility: visible;
}
address {
font-style: italic;
}
pre {
font-family: monospace;
white-space: pre;
}
}
.tdkb_result {
......@@ -7133,11 +7133,6 @@ div.progress {
}
/** /style for relations **/
// Fix line breaks in pasted rich text
.rich_text_container pre, .rich_text_container span {
white-space: pre-wrap;
}
.log-toolbar {
padding-right: 8px !important;
......
......@@ -266,7 +266,7 @@ abstract class API {
// login on glpi
if (!$auth->login($params['login'], $params['password'], $noAuto, false, $params['auth'])) {
$err = Html::clean($auth->getErr());
$err = Toolbox::stripTags($auth->getErr());
if (isset($params['user_token'])
&& !empty($params['user_token'])) {
return $this->returnError(__("parameter user_token seems invalid"), 401, "ERROR_GLPI_LOGIN_USER_TOKEN", false);
......@@ -2192,7 +2192,7 @@ abstract class API {
// clean html
foreach ($messages_after_redirect as $messages) {
foreach ($messages as $message) {
$all_messages[] = Html::clean($message);
$all_messages[] = Toolbox::stripTags($message);
}
}
......@@ -2341,11 +2341,7 @@ abstract class API {
// expand dropdown
if ($params['expand_dropdowns']) {
$value = Dropdown::getDropdownName($tablename, $value);
// fix value for inexistent items
if ($value == " ") {
$value = "";
}
$value = Dropdown::getDropdownName($tablename, $value, false, true, false, '');
}
}
}
......
......@@ -323,8 +323,7 @@ class Calendar extends AbstractBackend {
$input['uuid'] = Uuid::uuid4();
}
$input = \Html::entities_deep($input);
$input = \Toolbox::addslashes_deep($input);
$input = \Toolbox::sanitize($input);
if ($item->isNewItem()) {
// Auto set entities_id if exists and not set
......
......@@ -32,6 +32,7 @@
namespace Glpi\CalDAV\Traits;
use Glpi\Toolbox\RichText;
use RRule\RRule;
use Sabre\VObject\Component;
use Sabre\VObject\Component\VCalendar;
......@@ -95,7 +96,7 @@ trait VobjectConverterTrait {
$vcomp = $vcalendar->add($component_type);
}
$fields = \Html::entity_decode_deep($item->fields);
$fields = \Toolbox::unclean_cross_side_scripting_deep($item->fields);
$utc_tz = new \DateTimeZone('UTC');
if (array_key_exists('uuid', $fields)) {
......@@ -117,10 +118,15 @@ trait VobjectConverterTrait {
$vcomp->SUMMARY = $fields['name'];
}
$description = null;
if (array_key_exists('content', $fields)) {
$vcomp->DESCRIPTION = $fields['content'];
$description = $fields['content'];
} else if (array_key_exists('text', $fields)) {
$vcomp->DESCRIPTION = $fields['text'];
$description = $fields['text'];
}
if ($description !== null) {
// Transform HTML text to plain text
$vcomp->DESCRIPTION = RichText::getTextFromHtml($description, true, false);
}
$vcomp->URL = $CFG_GLPI['url_base'] . $this->getFormURLWithID($fields['id'], false);
......@@ -243,6 +249,9 @@ trait VobjectConverterTrait {
}
$input['rrule'] = $this->getRRuleInputFromVComponent($vcomponent);
if ($input['rrule'] === null) {
$input['rrule'] = 'NULL'; // Ensure rrule is set to null on update.
}
$state = $this->getStateInputFromVComponent($vcomponent);
if ($state !== null) {
......@@ -267,8 +276,11 @@ trait VobjectConverterTrait {
}
$content = $vcomponent->DESCRIPTION->getValue();
$content = nl2br($content);
$content = str_replace(["\n", "\r"], ['', ''], $content);
// Content is handled as plain text in CalDAV client and will be handled as rich text on GLPI side,
// so special chars have to be encoded in html entities.
$content = \Html::entities_deep($content);
return $content;
}
......
......@@ -521,7 +521,7 @@ class Certificate extends CommonDBTM {
echo "<tr class='tab_bg_1'>";
echo "<td>" . __('Expiration date');
echo "&nbsp;";
Html::showToolTip(nl2br(__('Empty for infinite')));
Html::showToolTip(__('Empty for infinite'));
echo "&nbsp;</td>";
echo "<td>";
Html::showDateField('date_expiration', ['value' => $this->fields["date_expiration"]]);
......
......@@ -34,6 +34,8 @@ if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Glpi\Toolbox\RichText;
/**
* Change Class
**/
......@@ -1174,8 +1176,6 @@ class Change extends CommonITILObject {
$content = Html::cleanPostForTextArea($content);
}
$content = Toolbox::getHtmlToDisplay($content);
echo "<div id='content$rand_text'>";
if ($canupdate) {
$uploads = [];
......@@ -1190,12 +1190,14 @@ class Change extends CommonITILObject {
'required' => $tt->isMandatoryField('content'),
'rows' => $rows,
'enable_richtext' => true,
'value' => Html::entities_deep($content), // Re-encode entities for textarea
'value' => RichText::getSafeHtml($content, true, true),
'uploads' => $uploads,
]);
Html::activateUserMentions($content_id);
} else {
echo $content;
echo '<div class="rich_text_container">';
echo RichText::getSafeHtml($content, true);
echo '</div>';
}
echo "</div>";
......
......@@ -32,6 +32,7 @@
use Glpi\Event;
use Glpi\Features\CacheableListInterface;
use Glpi\Toolbox\RichText;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
......@@ -4673,16 +4674,11 @@ class CommonDBTM extends CommonGLPI {
return $value;
case "text" :
if ($options['html']) {
$text = nl2br($value);
} else {
$text = $value;
}
if (isset($searchoptions['htmltext']) && $searchoptions['htmltext']) {
$text = Html::clean(Toolbox::unclean_cross_side_scripting_deep($text));
$value = RichText::getTextFromHtml($value, true, true);
}
return $text;
return $options['html'] ? nl2br($value) : $value;
case "bool" :
return Dropdown::getYesNo($value);
......@@ -4909,13 +4905,18 @@ class CommonDBTM extends CommonGLPI {
return Html::autocompletionTextField($this, $name, $options);
case "text" :
$is_htmltext = isset($searchoptions['htmltext']) && $searchoptions['htmltext'];
if ($is_htmltext) {
$value = RichText::getSafeHtml($value, true, true);
}
return Html::textarea(
[
'display' => false,
'name' => $name,
'value' => $value,
'enable_fileupload' => false,
'enable_richtext' => isset($searchoptions['htmltext']) && $searchoptions['htmltext'],
'enable_richtext' => $is_htmltext,
// For now, this textarea is displayed only in the "update" massive action form, for fields
// corresponding to a search option having "htmltext" property.
// Uploaded images processing is not able to handle multiple use of same uploaded file, so until this is fixed,
......
......@@ -34,6 +34,8 @@ if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Glpi\Toolbox\RichText;
/// CommonDropdown class - generic dropdown
abstract class CommonDropdown extends CommonDBTM {
......@@ -473,7 +475,7 @@ abstract class CommonDropdown extends CommonDBTM {
case 'tinymce':
Html::textarea([
'name' => $field['name'],
'value' => $this->fields[$field['name']],
'value' => RichText::getSafeHtml($this->fields[$field['name']], true, true),
'enable_richtext' => true,
'enable_images' => !($field['disable_images'] ?? false),
]);
......
......@@ -34,6 +34,8 @@ if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Glpi\Toolbox\RichText;
/**
* CommonITILObject Class
**/
......@@ -116,18 +118,13 @@ abstract class CommonITILObject extends CommonDBTM {
/**
* Retrieve an item from the database with datas associated (hardwares)
*
* @param integer $ID ID of the item to get
* @param boolean $purecontent true : nothing change / false : convert to HTML display
* @param integer $ID ID of the item to get
*
* @return boolean true if succeed else false
**/
function getFromDBwithData($ID, $purecontent) {
function getFromDBwithData($ID) {
if ($this->getFromDB($ID)) {
if (!$purecontent) {
$this->fields["content"] = nl2br(preg_replace("/\r\n\r\n/", "\r\n",
$this->fields["content"]));
}
$this->getAdditionalDatas();
return true;
}
......@@ -1579,15 +1576,23 @@ abstract class CommonITILObject extends CommonDBTM {
$input["name"] = ltrim($input["name"]);
$input['content'] = ltrim($input['content']);
if (empty($input["name"])) {
$input['name'] = Html::clean(Html::entity_decode_deep($input['content']));
$input["name"] = preg_replace('/\\r\\n/', ' ', $input['name']);
$input["name"] = preg_replace('/\\n/', ' ', $input['name']);
// For mailcollector
$input["name"] = preg_replace('/\\\\r\\\\n/', ' ', $input['name']);
$input["name"] = preg_replace('/\\\\n/', ' ', $input['name']);
$input['name'] = Toolbox::stripslashes_deep($input['name']);
$input["name"] = Toolbox::substr($input['name'], 0, 70);
$input['name'] = Toolbox::addslashes_deep($input['name']);
// Build name based on content
// Unsanitize
//
// Using `Toolbox::stripslashes_deep()` on sanitized content will produce "r" and "n" instead of "\r" and \n",
// so newlines have to be removed before calling it.
$content = str_replace(['\r', '\n'], ' ', $input['content']);
$content = Toolbox::stripslashes_deep(Toolbox::unclean_cross_side_scripting_deep($content));
// Get unformatted text
$name = RichText::getTextFromHtml($content, false);
// Shorten result
$name = Toolbox::substr(preg_replace('/\s{2,}/', ' ', $name), 0, 70);
// Sanitize result
$input['name'] = Toolbox::clean_cross_side_scripting_deep(Toolbox::addslashes_deep($name));
}
// Set default dropdown
......@@ -6242,7 +6247,7 @@ abstract class CommonITILObject extends CommonDBTM {
if ($p['output_type'] == Search::HTML_OUTPUT) {
$name_column = sprintf(__('%1$s %2$s'), $name_column,
Html::showToolTip(Html::clean(