Commit 785fea3c authored by Alexandre Delaunay's avatar Alexandre Delaunay Committed by Johan Cwiklinski

review image/upload in richtext editor (#1519)

* review image/upload in richtext editor
* condition tinymce paste_data_images param with presence of fileupload placeholder
* condition tinymce paste plugin on presence of tinymce
* delay tinymce loading
* Do not follow links using "onclick" (on dialog open only)
parent 29c30f12
......@@ -13,5 +13,6 @@ The present file will list all changes made to the project; according to the
### Changed
- Display knowledge base category items in tickets using a popup instead of a
new whole window
- Reviewed all richtext editor (tinymce) and their upload parts, now more simpler and intuitive
For older entries, please check [GLPI website](http://glpi-project.org).
......@@ -63,7 +63,6 @@ We are working on a [markdown version](https://github.com/glpi-project/doc)
* [Update](http://glpi-project.org/spip.php?article172)
## Additional resources
* [Official website](http://glpi-project.org)
......
<?php
/*
* @version $Id$
-------------------------------------------------------------------------
GLPI - Gestionnaire Libre de Parc Informatique
Copyright (C) 2015-2016 Teclib'.
Copyright (C) 2017 Teclib'.
http://glpi-project.org
......@@ -31,80 +32,43 @@
*/
/** @file
* @brief
* @since version 0.85
**/
* @brief
* @since version 9.2
*/
include ('../inc/includes.php');
if (!defined('GLPI_ROOT')) {
define('GLPI_ROOT', dirname(__DIR__));
}
include_once (GLPI_ROOT . "/inc/autoload.function.php");
include_once (GLPI_ROOT . "/inc/db.function.php");
include_once (GLPI_ROOT . "/inc/config.php");
Session::checkLoginUser();
// Load Language file
Session::loadLanguage();
include_once(GLPI_ROOT.'/lib/jqueryplugins/jquery-file-upload/server/php/UploadHandler.php');
$errors = array(
1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
3 => __('The uploaded file was only partially uploaded'),
4 => __('No file was uploaded'),
6 => __('Missing a temporary folder'),
7 => __('Failed to write file to disk'),
8 => __('A PHP extension stopped the file upload'),
'post_max_size' => __('The uploaded file exceeds the post_max_size directive in php.ini'),
'max_file_size' => __('File is too big'),
'min_file_size' => __('File is too small'),
'accept_file_types' => __('Filetype not allowed'),
'max_number_of_files' => __('Maximum number of files exceeded'),
'max_width' => __('Image exceeds maximum width'),
'min_width' => __('Image requires a minimum width'),
'max_height' => __('Image exceeds maximum height'),
'min_height' => __('Image requires a minimum height')
);
$upload_dir = GLPI_TMP_DIR.'/';
$upload_handler = new UploadHandler(array('upload_dir' => $upload_dir,
'param_name' => $_GET['name'],
'orient_image' => false,
'image_versions' => array()),
false, $errors);
$response = $upload_handler->post(false);
$upload_dir = GLPI_TMP_DIR.'/';
$upload_handler = new GLPIUploadHandler(['upload_dir' => $upload_dir,
'param_name' => $_REQUEST['name'] ,
'orient_image' => false,
'image_versions' => []],
false);
$response = $upload_handler->post(false);
// clean compute display filesize
if (isset($response[$_GET['name']]) && is_array($response[$_GET['name']])) {
foreach ($response[$_GET['name']] as $key => &$val) {
if (isset($response[$_REQUEST['name']]) && is_array($response[$_REQUEST['name']])) {
foreach ($response[$_REQUEST['name']] as $key => &$val) {
if (Document::isValidDoc(addslashes($val->name))) {
if (isset($val->name)) {
$val->display = $val->name;
}
if (isset($val->size)) {
$val->filesize = Toolbox::getSize($val->size);
if (isset($_GET['showfilesize']) && $_GET['showfilesize']) {
if (isset($_REQUEST['showfilesize']) && $_REQUEST['showfilesize']) {
$val->display = sprintf('%1$s %2$s', $val->display, $val->filesize);
}
}
} else { // Unlink file
$val->error = $errors['accept_file_types'];
} else {
// Unlink file
$val->error = __('Filetype not allowed');
if (file_exists($upload_dir.$val->name)) {
@unlink($upload_dir.$val->name);
unlink($upload_dir.$val->name);
}
}
$val->id = 'doc'.$_GET['name'].mt_rand();
$val->id = 'doc'.$_REQUEST['name'].mt_rand();
}
}
// Ajout du Doc + generation tag + autre traitement
// send answer
$upload_handler->generate_response($response);
......@@ -48,7 +48,7 @@ Session::checkLoginUser();
if (isset($_POST['users_id']) && ($_POST['users_id'] > 0)) {
$rand = mt_rand();
echo " <a href='#' onClick=\"".Html::jsGetElementbyID('planningcheck'.$rand).".dialog('open');\">";
echo " <a href='#' onClick=\"".Html::jsGetElementbyID('planningcheck'.$rand).".dialog('open');return false;\">";
echo "<img src='".$CFG_GLPI["root_doc"]."/pics/reservation-3.png'
title=\"".__s('Availability')."\" alt=\"".__s('Availability')."\"
class='calendrier'>";
......
......@@ -1672,6 +1672,19 @@ a.icon_nav_move:hover img {
max-width: 950px;
margin: .5em auto;
padding: .5em;
margin-top: 5px;
}
.fileupload_info {
margin-bottom: 10px;
}
.upload_rich_text {
display : none;
}
.draghover {
background: #FBF8DF;
}
......
......@@ -135,3 +135,9 @@ if (!defined("GLPI_FONT_FREESANS")) {
// if FreeSans.ttf available in system, use (in config_path.php)
// define("GLPI_FONT_FREESANS", '/usr/share/fonts/gnu-free/FreeSans.ttf');
}
// Default path to juqery file upload handler
if (!defined("GLPI_JQUERY_UPLOADHANDLER")) {
define("GLPI_JQUERY_UPLOADHANDLER",
GLPI_ROOT.'/lib/jqueryplugins/jquery-file-upload/server/php/UploadHandler.php');
}
......@@ -881,7 +881,7 @@ class Bookmark extends CommonDBTM {
static function showSaveButton($type, $itemtype=0) {
global $CFG_GLPI;
echo " <a href='#' onClick=\"".Html::jsGetElementbyID('bookmarksave').".dialog('open');\">";
echo " <a href='#' onClick=\"".Html::jsGetElementbyID('bookmarksave').".dialog('open'); return false;\">";
echo "<img src='".$CFG_GLPI["root_doc"]."/pics/bookmark_record.png'
title=\"".__s('Save as bookmark')."\" alt=\"".__s('Save as bookmark')."\"
class='calendrier pointer'>";
......
......@@ -4529,4 +4529,144 @@ class CommonDBTM extends CommonGLPI {
static function generateLinkContents($link, CommonDBTM $item) {
return Link::generateLinkContents($link, $item);
}
/**
* add files (from $this->input['_filename']) to an CommonDBTM object
* create document if needed
* create link from document to CommonDBTM object
*
* @param $input array
* @param $options array with theses keys
* - force_update (default false) update the content field of the object
* - content_field (default content) the field who receive the main text
* (with images)
*
* @since version 9.2
*
* @return array the input param transformed
**/
function addFiles(array $input, $options = []) {
global $CFG_GLPI;
$default_options = [
'force_update' => false,
'content_field' => 'content',
];
$options = array_merge($default_options, $options);
if (!isset($input['_filename'])
|| (count($input['_filename']) == 0)) {
return $input;
}
$docadded = array();
$donotif = isset($input['_donotif']) ? $input['_donotif'] : 0;
$disablenotif = isset($input['_disablenotif']) ? $input['_disablenotif'] : 0;
foreach ($input['_filename'] as $key => $file) {
$doc = new Document();
$docitem = new Document_Item();
$docID = 0;
$filename = GLPI_TMP_DIR."/".$file;
$input2 = array();
//If file tag is present
if (isset($input['_tag_filename'])
&& !empty($input['_tag_filename'][$key])) {
$input['_tag'][$key] = $input['_tag_filename'][$key];
}
//retrieve entity
$entities_id = isset($this->fields["entities_id"])
? $this->fields["entities_id"]
: $_SESSION['glpiactive_entity'];
// Check for duplicate
if ($doc->getFromDBbyContent($entities_id, $filename)) {
if (!$doc->fields['is_blacklisted']) {
$docID = $doc->fields["id"];
}
// File already exist, we replace the tag by the existing one
if (isset($input['_tag'][$key])
&& ($docID > 0)
&& isset($input[$options['content_field']])) {
$input[$options['content_field']]
= preg_replace('/'.Document::getImageTag($input['_tag'][$key]).'/',
Document::getImageTag($doc->fields["tag"]),
$input[$options['content_field']]);
$docadded[$docID]['tag'] = $doc->fields["tag"];
}
} else {
if ($this->getType() == 'Ticket') {
//TRANS: Default document to files attached to tickets : %d is the ticket id
$input2["name"] = addslashes(sprintf(__('Document Ticket %d'), $this->getID()));
$input2["tickets_id"] = $this->getID();
}
// Insert image tag
$input2["tag"] = $input['_tag'][$key];
$input2["entities_id"] = $entities_id;
$input2["documentcategories_id"] = $CFG_GLPI["documentcategories_id_forticket"];
$input2["_only_if_upload_succeed"] = 1;
$input2["_filename"] = array($file);
$docID = $doc->add($input2);
$docadded[$docID]['tag'] = $input['_tag'][$key];
}
if ($docID > 0) {
// complete doc information
$docadded[$docID]['data'] = sprintf(__('%1$s - %2$s'),
stripslashes($doc->fields["name"]),
stripslashes($doc->fields["filename"]));
// for sub item, attach to document to parent item
$item_fordocitem = $this;
$skip_docitem = false;
if (isset($input['_job'])) {
$item_fordocitem = $input['_job'];
}
// if doc is an image and already inserted in content, do not attach in docitem
if (isset($input[$options['content_field']])
&& strpos($input[$options['content_field']], $doc->fields["tag"]) !== false
&& strpos($doc->fields['mime'], 'image/') !== false) {
$skip_docitem = true;
}
// add doc - item ling
if (!$skip_docitem) {
$docitem->add(array('documents_id' => $docID,
'_do_notif' => $donotif,
'_disablenotif' => $disablenotif,
'itemtype' => $item_fordocitem->getType(),
'items_id' => $item_fordocitem->getID()));
}
}
// Only notification for the first New doc
$donotif = false;
}
// manage content transformation
if (isset($input[$options['content_field']])) {
if ($CFG_GLPI["use_rich_text"]) {
$input[$options['content_field']]
= Toolbox::convertTagToImage($input[$options['content_field']],
$this,
$docadded);
$input['_forcenotif'] = true;
} else {
$input[$options['content_field']]
= Html::setSimpleTextContent($input[$options['content_field']]);
}
// force update of content on add process (we are in post_addItem function)
if ($options['force_update']) {
$this->fields[$options['content_field']] = $input[$options['content_field']];
$this->updateInDB(array($options['content_field']));
}
}
return $input;
}
}
......@@ -582,14 +582,7 @@ abstract class CommonITILObject extends CommonDBTM {
// Add document if needed
$this->getFromDB($input["id"]); // entities_id field required
if (!isset($input['_donotadddocs']) || !$input['_donotadddocs']) {
$docadded = $this->addFiles(1, isset($input['_disablenotif'])?$input['_disablenotif']:0);
if (isset($this->input['_forcenotif'])) {
$input['_forcenotif'] = $this->input['_forcenotif'];
unset($input['_disablenotif']);
}
if (isset($this->input['content'])) {
$input['content'] = $this->input['content'];
}
$input = $this->addFiles($input);
}
if (isset($input["document"]) && ($input["document"] > 0)) {
......@@ -1242,7 +1235,7 @@ abstract class CommonITILObject extends CommonDBTM {
function post_addItem() {
// Add document if needed, without notification
$this->addFiles(0, 1);
$this->input = $this->addFiles($this->input, ['force_update' => true]);
// Add default document if set in template
if (isset($this->input['_documents_id'])
......@@ -1654,129 +1647,6 @@ abstract class CommonITILObject extends CommonDBTM {
}
/**
* add files (from $this->input['_filename']) to an ITIL object
* create document if needed
* create link from document to ITIL object
*
* @param $donotif Boolean if we want to raise notification (default 1)
* @param $disablenotif (default 0)
*
* @return array of doc added name
**/
function addFiles($donotif=1, $disablenotif=0) {
global $CFG_GLPI;
if (!isset($this->input['_filename']) || (count($this->input['_filename']) == 0)) {
return array();
}
$docadded = array();
foreach ($this->input['_filename'] as $key => $file) {
$doc = new Document();
$docitem = new Document_Item();
$docID = 0;
$filename = GLPI_TMP_DIR."/".$file;
$input2 = array();
// Crop/Resize image file if needed
if (isset($this->input['_coordinates']) && !empty($this->input['_coordinates'][$key])) {
$image_coordinates = json_decode(urldecode($this->input['_coordinates'][$key]), true);
Toolbox::resizePicture($filename,
$filename,
$image_coordinates['img_w'],
$image_coordinates['img_h'],
$image_coordinates['img_y'],
$image_coordinates['img_x'],
$image_coordinates['img_w'],
$image_coordinates['img_h'],
0);
} else {
Toolbox::resizePicture($filename, $filename, 0, 0, 0, 0, 0, 0, 0);
}
//If file tag is present
if (isset($this->input['_tag_filename']) && !empty($this->input['_tag_filename'][$key])) {
$this->input['_tag'][$key] = $this->input['_tag_filename'][$key];
}
// Check for duplicate
if ($doc->getFromDBbyContent($this->fields["entities_id"],
$filename)) {
if (!$doc->fields['is_blacklisted']) {
$docID = $doc->fields["id"];
}
// File already exist, we replace the tag by the existing one
if (isset($this->input['_tag'][$key])
&& ($docID > 0)
&& isset($this->input['content'])) {
$this->input['content'] = preg_replace('/'.Document::getImageTag($this->input['_tag'][$key]).'/',
Document::getImageTag($doc->fields["tag"]),
$this->input['content']);
$docadded[$docID]['tag'] = $doc->fields["tag"];
}
} else {
//TRANS: Default document to files attached to tickets : %d is the ticket id
$input2["name"] = addslashes(sprintf(__('Document Ticket %d'), $this->getID()));
if ($this->getType() == 'Ticket') {
$input2["tickets_id"] = $this->getID();
// Insert image tag
if (isset($this->input['_tag'][$key])) {
$input2["tag"] = $this->input['_tag'][$key];
}
}
$input2["entities_id"] = $this->fields["entities_id"];
$input2["documentcategories_id"] = $CFG_GLPI["documentcategories_id_forticket"];
$input2["_only_if_upload_succeed"] = 1;
$input2["entities_id"] = $this->fields["entities_id"];
$input2["_filename"] = array($file);
$docID = $doc->add($input2);
}
if ($docID > 0) {
if ($docitem->add(array('documents_id' => $docID,
'_do_notif' => $donotif,
'_disablenotif' => $disablenotif,
'itemtype' => $this->getType(),
'items_id' => $this->getID()))) {
$docadded[$docID]['data'] = sprintf(__('%1$s - %2$s'),
stripslashes($doc->fields["name"]),
stripslashes($doc->fields["filename"]));
if (isset($input2["tag"])) {
$docadded[$docID]['tag'] = $input2["tag"];
unset($this->input['_filename'][$key]);
unset($this->input['_tag'][$key]);
}
if (isset($this->input['_coordinates'][$key])) {
unset($this->input['_coordinates'][$key]);
}
}
}
// Only notification for the first New doc
$donotif = 0;
}
// Ticket update
if (isset($this->input['content'])) {
if ($CFG_GLPI["use_rich_text"]) {
$this->input['content'] = $this->convertTagToImage($this->input['content'], true,
$docadded);
$this->input['_forcenotif'] = true;
} else {
$this->fields['content'] = $this->setSimpleTextContent($this->input['content']);
$this->updateInDB(array('content'));
}
}
return $docadded;
}
/**
* Compute Priority
*
......
......@@ -295,6 +295,8 @@ abstract class CommonITILTask extends CommonDBTM {
}
}
$input = $this->addFiles($input);
return $input;
}
......@@ -367,7 +369,6 @@ abstract class CommonITILTask extends CommonDBTM {
function prepareInputForAdd($input) {
$itemtype = $this->getItilObjectItemType();
Toolbox::manageBeginAndEndPlanDates($input['plan']);
......@@ -411,14 +412,6 @@ abstract class CommonITILTask extends CommonDBTM {
$input['is_private'] = 0;
}
// Manage File attached (from mailgate)
// Pass filename if set to ticket
if (isset($input['_filename'])) {
$input["_job"]->input['_filename'] = $input['_filename'];
}
// Add docs without notif
$docadded = $input["_job"]->addFiles(0, 1);
return $input;
}
......@@ -426,6 +419,9 @@ abstract class CommonITILTask extends CommonDBTM {
function post_addItem() {
global $CFG_GLPI;
// Add document if needed, without notification
$this->input = $this->addFiles($this->input, ['force_update' => true]);
if (isset($this->input['_planningrecall'])) {
$this->input['_planningrecall']['items_id'] = $this->fields['id'];
PlanningRecall::manageDatas($this->input['_planningrecall']);
......@@ -1329,10 +1325,27 @@ abstract class CommonITILTask extends CommonDBTM {
}
echo "<tr class='tab_bg_1'>";
echo "<td rowspan='$rowspan' style='width:100px'>".__('Description')."</td>";
echo "<td rowspan='$rowspan' style='width:50%' id='content$rand_text'>".
"<textarea name='content' style='width: 95%; height: 160px' id='task$rand_text'>".$this->fields["content"].
"</textarea>";
echo Html::scriptBlock("$(function() { $('#content$rand').autogrow(); });");
echo "<td rowspan='$rowspan' style='width:65%' id='content$rand_text'>";
$rand_text = mt_rand();
$content_id = "content$rand";
$cols = 90;
$rows = 6;
if ($CFG_GLPI["use_rich_text"]) {
$cols = 100;
$rows = 10;
}
Html::textarea(['name' => 'content',
'value' => $this->fields["content"],
'rand' => $rand_text,
'editor_id' => $content_id,
'enable_fileupload' => true,
'enable_richtext' => $CFG_GLPI["use_rich_text"],
'cols' => $cols,
'rows' => $rows]);
echo "</td>";
echo "<input type='hidden' name='$fkfield' value='".$this->fields[$fkfield]."'>";
echo "</td></tr>\n";
......@@ -1421,9 +1434,6 @@ abstract class CommonITILTask extends CommonDBTM {
echo "</td></tr>\n";
if ($ID <= 0) {
Document_Item::showSimpleAddForItem($item);
}
echo "<tr class='tab_bg_1'>";
echo "<td>".__('By')."</td>";
echo "<td colspan='2'>";
......@@ -1445,7 +1455,7 @@ abstract class CommonITILTask extends CommonDBTM {
'url' => $CFG_GLPI["root_doc"]."/ajax/planningcheck.php");
User::dropdown($params);
echo " <a href='#' onClick=\"".Html::jsGetElementbyID('planningcheck'.$rand).".dialog('open');\">";
echo " <a href='#' onClick=\"".Html::jsGetElementbyID('planningcheck'.$rand).".dialog('open'); return false;\">";
echo "&nbsp;<img src='".$CFG_GLPI["root_doc"]."/pics/reservation-3.png'
title=\"".__s('Availability')."\" alt=\"".__s('Availability')."\"
class='calendrier'>";
......
......@@ -366,7 +366,7 @@ $CFG_GLPI['javascript'] = [
],
'helpdesk' => [
'planning' => ['fullcalendar', 'colorpicker'],
'ticket' => ['rateit', 'tinymce', 'imageupload'],
'ticket' => ['rateit', 'tinymce'],
'problem' => ['tinymce'],
'change' => ['tinymce']
],
......
......@@ -425,7 +425,7 @@ class Document extends CommonDBTM {
echo "<td>".sprintf(__('%1$s (%2$s)'), __('File'), self::getMaxUploadSize())."</td>";
echo "<td>";
echo Html::file();
Html::file();
echo "</td></tr>";
$this->showFormButtons($options);
......
......@@ -188,7 +188,7 @@ class Document_Item extends CommonDBRelation{
$doc->getFromDB($this->fields['documents_id']);
if (!empty($doc->fields['tag'])) {
$ticket->getFromDB($this->fields['items_id']);
$input['content'] = $ticket->cleanTagOrImage($ticket->fields['content'],
$input['content'] = Toolbox::cleanTagOrImage($ticket->fields['content'],
array($doc->fields['tag']));
}
......@@ -611,7 +611,7 @@ class Document_Item extends CommonDBRelation{
if ($item->getType() == 'Ticket') {
echo "<input type='hidden' name='tickets_id' value='".$item->getID()."'>";
}
echo Html::file(array('multiple' => true));
Html::file();
echo "</td><td class='left'>(".Document::getMaxUploadSize().")&nbsp;</td>";
echo "<td></td></tr>";
}
......@@ -707,7 +707,7 @@ class Document_Item extends CommonDBRelation{
if ($item->getType() == 'Ticket') {
echo "<input type='hidden' name='tickets_id' value='".$item->getID()."'>";
}
echo Html::file(array('multiple' => true));
Html::file();
echo "</td><td class='left'>(".Document::getMaxUploadSize().")&nbsp;</td>";
echo "<td class='center' width='20%'>";
echo "<input type='submit' name='add' value=\""._sx('button', 'Add a new file')."\"
......
......@@ -161,16 +161,33 @@ class DocumentType extends CommonDropdown {
/**
* @since version 0.85
*
* @param array $options list of options with theses possible keys:
* - bool 'display', echo the generated html or return it
**/
static function showAvailableTypesLink() {
static function showAvailableTypesLink($options = array()) {
global $CFG_GLPI;
echo " <a href='#' onClick=\"".Html::jsGetElementbyID('documenttypelist').".dialog('open');\">";
echo "<img src='".$CFG_GLPI["root_doc"]."/pics/info-small.png' title=\"".__s('Help')."\"
alt=\"".__s('Help')."\" class='calendrier pointer'>";
echo "</a>";
Ajax::createIframeModalWindow('documenttypelist',
$CFG_GLPI["root_doc"]."/front/documenttype.list.php",
array('title' => static::getTypeName(Session::getPluralNumber())));
$p['display'] = true;
//merge default options with options parameter
$p = array_merge($p, $options);
$display = "&nbsp;";
$display .= "<a href='#' onClick=\"".Html::jsGetElementbyID('documenttypelist').
".dialog('open'); return false;\">";
$display .= "<img src='".$CFG_GLPI["root_doc"]."/pics/info-small.png' title=\"".__s('Help')."\"
alt=\"".__s('Help')."\" class='calendrier pointer'>";
$display .= "</a>";
$display .= Ajax::createIframeModalWindow('documenttypelist',
$CFG_GLPI["root_doc"]."/front/documenttype.list.php",
['title' => static::getTypeName(Session::getPluralNumber()),
'display' => false]);
if ($p['display']) {
echo $display;
} else {
return $display;
}
}
}
<?php
/*
-------------------------------------------------------------------------
GLPI - Gestionnaire Libre de Parc Informatique
Copyright (C) 2015-2016 Teclib'.
http://glpi-project.org
based on GLPI - Gestionnaire Libre de Parc Informatique
Copyright (C) 2003-2014 by the INDEPNET Development Team.
-------------------------------------------------------------------------
LICENSE
This file is part of GLPI.
GLPI is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
GLPI is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with GLPI. If not, see <http://www.gnu.org/licenses/>.
--------------------------------------------------------------------------
*/
/** @file
* @brief
*/
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
include_once(GLPI_JQUERY_UPLOADHANDLER);
/** GLPIUploadHandler class
*
* @since version 9.2
**/
class GLPIUploadHandler extends UploadHandler {
protected function get_error_message