From 608e94a613d9df46650bc809c7e18bd764c2a610 Mon Sep 17 00:00:00 2001 From: Denis Flaven Date: Thu, 25 Feb 2016 15:06:04 +0000 Subject: [PATCH] Inline images in formatted case log & descriptions: beta version fixperms js The inline images are now no longer stored stored as Attachments but using a specific object InlineImage... SVN:trunk[3926] --- application/cmdbabstract.class.inc.php | 8 + application/displayblock.class.inc.php | 15 +- application/itopwebpage.class.inc.php | 8 +- application/utils.inc.php | 31 -- core/attributedef.class.inc.php | 4 +- core/config.class.inc.php | 19 +- core/email.class.inc.php | 6 +- core/htmlsanitizer.class.inc.php | 6 +- core/inlineimage.class.inc.php | 467 ++++++++++++++++++ core/ormcaselog.class.inc.php | 6 +- .../css => css}/magnific-popup.css | 0 .../2.x/itop-attachments/main.attachments.php | 60 +-- dictionaries/dictionary.itop.ui.php | 7 + dictionaries/fr.dictionary.itop.ui.php | 7 + js/ckeditor/build-config.js | 5 +- js/ckeditor/ckeditor.js | 145 +++--- js/ckeditor/lang/de.js | 2 +- js/ckeditor/lang/en.js | 2 +- js/ckeditor/lang/es.js | 2 +- js/ckeditor/lang/fr.js | 2 +- js/ckeditor/lang/it.js | 2 +- js/ckeditor/lang/pt-br.js | 2 +- .../plugins/colorbutton/icons/bgcolor.png | Bin 0 -> 906 bytes .../colorbutton/icons/hidpi/bgcolor.png | Bin 0 -> 2169 bytes .../colorbutton/icons/hidpi/textcolor.png | Bin 0 -> 1725 bytes .../plugins/colorbutton/icons/textcolor.png | Bin 0 -> 813 bytes js/ckeditor/plugins/colorbutton/lang/af.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/ar.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/bg.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/bn.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/bs.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/ca.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/cs.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/cy.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/da.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/de-ch.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/de.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/el.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/en-au.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/en-ca.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/en-gb.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/en.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/eo.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/es.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/et.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/eu.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/fa.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/fi.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/fo.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/fr-ca.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/fr.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/gl.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/gu.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/he.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/hi.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/hr.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/hu.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/id.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/is.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/it.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/ja.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/ka.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/km.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/ko.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/ku.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/lt.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/lv.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/mk.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/mn.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/ms.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/nb.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/nl.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/no.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/pl.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/pt-br.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/pt.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/ro.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/ru.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/si.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/sk.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/sl.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/sq.js | 53 ++ .../plugins/colorbutton/lang/sr-latn.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/sr.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/sv.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/th.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/tr.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/tt.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/ug.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/uk.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/vi.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/zh-cn.js | 53 ++ js/ckeditor/plugins/colorbutton/lang/zh.js | 53 ++ js/ckeditor/plugins/colorbutton/plugin.js | 301 +++++++++++ js/ckeditor/plugins/icons.png | Bin 10227 -> 11167 bytes js/ckeditor/plugins/icons_hidpi.png | Bin 34465 -> 36988 bytes js/ckeditor/skins/flat/editor.css | 2 +- js/ckeditor/skins/flat/editor_gecko.css | 2 +- js/ckeditor/skins/flat/editor_ie.css | 2 +- js/ckeditor/skins/flat/editor_ie7.css | 2 +- js/ckeditor/skins/flat/editor_ie8.css | 2 +- js/ckeditor/skins/flat/editor_iequirks.css | 2 +- js/ckeditor/skins/flat/icons.png | Bin 10227 -> 11167 bytes js/ckeditor/skins/flat/icons_hidpi.png | Bin 34465 -> 36988 bytes .../js => js}/jquery.magnific-popup.js | 0 .../js => js}/jquery.magnific-popup.min.js | 0 pages/ajax.render.php | 202 ++++++++ 107 files changed, 4676 insertions(+), 196 deletions(-) create mode 100644 core/inlineimage.class.inc.php rename {datamodels/2.x/itop-attachments/css => css}/magnific-popup.css (100%) create mode 100644 js/ckeditor/plugins/colorbutton/icons/bgcolor.png create mode 100644 js/ckeditor/plugins/colorbutton/icons/hidpi/bgcolor.png create mode 100644 js/ckeditor/plugins/colorbutton/icons/hidpi/textcolor.png create mode 100644 js/ckeditor/plugins/colorbutton/icons/textcolor.png create mode 100644 js/ckeditor/plugins/colorbutton/lang/af.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/ar.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/bg.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/bn.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/bs.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/ca.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/cs.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/cy.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/da.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/de-ch.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/de.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/el.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/en-au.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/en-ca.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/en-gb.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/en.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/eo.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/es.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/et.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/eu.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/fa.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/fi.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/fo.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/fr-ca.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/fr.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/gl.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/gu.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/he.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/hi.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/hr.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/hu.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/id.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/is.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/it.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/ja.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/ka.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/km.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/ko.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/ku.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/lt.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/lv.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/mk.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/mn.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/ms.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/nb.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/nl.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/no.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/pl.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/pt-br.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/pt.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/ro.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/ru.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/si.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/sk.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/sl.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/sq.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/sr-latn.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/sr.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/sv.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/th.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/tr.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/tt.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/ug.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/uk.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/vi.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/zh-cn.js create mode 100644 js/ckeditor/plugins/colorbutton/lang/zh.js create mode 100644 js/ckeditor/plugins/colorbutton/plugin.js rename {datamodels/2.x/itop-attachments/js => js}/jquery.magnific-popup.js (100%) rename {datamodels/2.x/itop-attachments/js => js}/jquery.magnific-popup.min.js (100%) diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index 16413344c..bba3790f8 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -2216,6 +2216,8 @@ EOF $sJsonFieldsMap = json_encode($aFieldsMap); $sState = $this->GetState(); $sSessionStorageKey = $sClass.'_'.$iKey; + $sTempId = session_id().'_'.$iTransactionId; + $oPage->add_ready_script(InlineImage::EnableCKEditorImageUpload($this, $sTempId)); $oPage->add_script( <<UpdateObjectFromArray($aFinalValues); + if (!$this->IsNew()) // for new objects this is performed in DBInsertNoReload() + { + InlineImage::FinalizeInlineImages($this); + } // Invoke extensions after the update of the object from the form foreach (MetaModel::EnumPlugins('iApplicationUIExtension') as $oExtensionInstance) @@ -3172,6 +3178,8 @@ EOF { $res = parent::DBInsertNoReload(); + InlineImage::FinalizeInlineImages($this); + // Invoke extensions after insertion (the object must exist, have an id, etc.) foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance) { diff --git a/application/displayblock.class.inc.php b/application/displayblock.class.inc.php index 446d4eb5e..3b9d2ece4 100644 --- a/application/displayblock.class.inc.php +++ b/application/displayblock.class.inc.php @@ -1290,22 +1290,13 @@ class HistoryBlock extends DisplayBlock { $sHtml .= $this->GetHistoryTable($oPage, $oSet); } - $sMaxWidth = MetaModel::GetModuleSetting('itop-attachment', 'inline_image_max_width', '450px'); + $oPage->add_ready_script(InlineImage::FixImagesWidth()); $oPage->add_ready_script("$('.case-log-history-entry-toggle').on('click', function () { $(this).closest('.case-log-history-entry').toggleClass('expanded');});"); $oPage->add_ready_script( << array('label' => Dict::S('UI:History:Date'), 'description' => Dict::S('UI:History:Date+')), 'userinfo' => array('label' => Dict::S('UI:History:User'), 'description' => Dict::S('UI:History:User+')), - 'log' => array('label' => Dict::S('UI:History:Changes').'Expand All / Collapse All', 'description' => Dict::S('UI:History:Changes+')), + 'log' => array('label' => Dict::S('UI:History:Changes') , 'description' => Dict::S('UI:History:Changes+')), ); $aValues = array(); foreach($aChanges as $aChange) diff --git a/application/itopwebpage.class.inc.php b/application/itopwebpage.class.inc.php index f0b63bb7f..cb5e8a25a 100644 --- a/application/itopwebpage.class.inc.php +++ b/application/itopwebpage.class.inc.php @@ -54,6 +54,8 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage $this->add_linked_stylesheet("../css/jquery.autocomplete.css"); $this->add_linked_stylesheet("../css/fg.menu.css"); $this->add_linked_stylesheet("../css/jquery.multiselect.css"); + $this->add_linked_stylesheet("../css/magnific-popup.css"); + $this->add_linked_script('../js/jquery.layout.min.js'); $this->add_linked_script('../js/jquery.ba-bbq.min.js'); $this->add_linked_script("../js/jquery.treeview.js"); @@ -76,6 +78,8 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage $this->add_linked_script('../js/jquery.multiselect.js'); $this->add_linked_script('../js/ajaxfileupload.js'); $this->add_linked_script('../js/jquery.mousewheel.js'); + $this->add_linked_script('../js/jquery.magnific-popup.min.js'); + $sSearchAny = addslashes(Dict::S('UI:SearchValue:Any')); $sSearchNbSelected = addslashes(Dict::S('UI:SearchValue:NbSelected')); @@ -436,9 +440,9 @@ EOF }); } }); - EOF ); + $this->add_ready_script(InlineImage::FixImagesWidth()); $sUserPrefs = appUserPreferences::GetAsJSON(); $this->add_script( <<', htmlentities($sText, ENT_QUOTES, 'UTF-8')); } - - /** - * Parses the supplied HTML fragment to rebuild the attribute src="" for images - * that refer to an attachment (detected via the attribute data-att-id="") so that - * the URL is consistent with the current URL of the application. - * @param string $sHtml The HTML fragment to process - * @return string The modified HTML - */ - public static function FixInlineAttachments($sHtml) - { - $aNeedles = array(); - $aReplacements = array(); - // Find img tags with an attribute data-att-id - if (preg_match_all('/]*)data-att-id="([0-9]+)"([^>]*)>/i', $sHtml, $aMatches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) - { - $sUrl = utils::GetAbsoluteUrlAppRoot().ATTACHMENT_DOWNLOAD_URL; - foreach($aMatches as $aImgInfo) - { - $sImgTag = $aImgInfo[0][0]; - $sAttId = $aImgInfo[2][0]; - - $sNewImgTag = preg_replace('/src="[^"]+"/', 'src="'.$sUrl.$sAttId.'"', $sImgTag); // preserve other attributes - $aNeedles[] = $sImgTag; - $aReplacements[] = $sNewImgTag; - } - $sHtml = str_replace($aNeedles, $aReplacements, $sHtml); - } - return $sHtml; - } - } diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 0792b16fd..f9b99f46b 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -2297,7 +2297,7 @@ class AttributeText extends AttributeString } else { - return "
".utils::FixInlineAttachments($sValue).'
'; + return "
".InlineImage::FixUrls($sValue).'
'; } } @@ -2430,7 +2430,7 @@ class AttributeText extends AttributeString } else { - $value = utils::FixInlineAttachments((string)$value); + $value = InlineImage::FixUrls((string)$value); } break; diff --git a/core/config.class.inc.php b/core/config.class.inc.php index 0463c7e8f..11f88ae25 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -882,7 +882,23 @@ class Config 'value' => '', 'source_of_value' => '', 'show_in_conf_sample' => false, - ), + ), + 'inline_image_max_display_width' => array( + 'type' => 'integer', + 'description' => 'The maximum width (in pixels) when displaying images inside an HTML formatted attribute. Images will be displayed using this this maximum width.', + 'default' => '600', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'inline_image_max_storage_width' => array( + 'type' => 'integer', + 'description' => 'The maximum width (in pixels) when uploading images to be used inside an HTML formatted attribute. Images larger than the given size will be downsampled before storing them in the database.', + 'default' => '1600', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), ); public function IsProperty($sPropCode) @@ -1027,6 +1043,7 @@ class Config 'core/ownershiplock.class.inc.php', 'synchro/synchrodatasource.class.inc.php', 'core/backgroundtask.class.inc.php', + 'core/inlineimage.class.inc.php', ); $this->m_aDataModels = array(); $this->m_aWebServiceCategories = array( diff --git a/core/email.class.inc.php b/core/email.class.inc.php index 735de04fc..677615b63 100644 --- a/core/email.class.inc.php +++ b/core/email.class.inc.php @@ -226,15 +226,15 @@ class EMail @$oDOMDoc->loadHTML(''.$this->m_aData['body']['body']); // For loading HTML chunks where the character set is not specified $oXPath = new DOMXPath($oDOMDoc); - $sXPath = "//img[@data-att-id]"; + $sXPath = "//img[@data-img-id]"; $oImagesList = $oXPath->query($sXPath); if ($oImagesList->length != 0) { foreach($oImagesList as $oImg) { - $iAttId = $oImg->getAttribute('data-att-id'); - $oAttachment = MetaModel::GetObject('Attachment', $iAttId, false, true /* Allow All Data */); + $iAttId = $oImg->getAttribute('data-img-id'); + $oAttachment = MetaModel::GetObject('InlineImage', $iAttId, false, true /* Allow All Data */); if ($oAttachment) { $oDoc = $oAttachment->Get('contents'); diff --git a/core/htmlsanitizer.class.inc.php b/core/htmlsanitizer.class.inc.php index 4e0e85e3f..9451ad156 100644 --- a/core/htmlsanitizer.class.inc.php +++ b/core/htmlsanitizer.class.inc.php @@ -295,18 +295,18 @@ class HTMLDOMSanitizer extends HTMLSanitizer } /** - * Add an extra attribute data-att-id for images which are based on an actual attachment + * Add an extra attribute data-img-id for images which are based on an actual InlineImage * so that we can later reconstruct the full "src" URL when needed * @param DOMNode $oElement */ protected function ProcessImage(DOMNode $oElement) { $sSrc = $oElement->getAttribute('src'); - $sDownloadUrl = str_replace(array('.', '?'), array('\.', '\?'), ATTACHMENT_DOWNLOAD_URL); // Escape . and ? + $sDownloadUrl = str_replace(array('.', '?'), array('\.', '\?'), INLINEIMAGE_DOWNLOAD_URL); // Escape . and ? $sUrlPattern = '|'.$sDownloadUrl.'([0-9]+)|'; if (preg_match($sUrlPattern, $sSrc, $aMatches)) { - $oElement->setAttribute('data-att-id', $aMatches[1]); + $oElement->setAttribute('data-img-id', $aMatches[1]); } } diff --git a/core/inlineimage.class.inc.php b/core/inlineimage.class.inc.php new file mode 100644 index 000000000..543b3b87e --- /dev/null +++ b/core/inlineimage.class.inc.php @@ -0,0 +1,467 @@ + + +define('INLINEIMAGE_DOWNLOAD_URL', 'pages/ajax.render.php?operation=download_document&class=InlineImage&field=contents&id='); + +/** + * Persistent classes (internal): store images referenced inside HTML formatted text fields + * + * @copyright Copyright (C) 2016 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +class InlineImage extends DBObject +{ + public static function Init() + { + $aParams = array + ( + 'category' => 'addon', + 'key_type' => 'autoincrement', + 'name_attcode' => array('item_class', 'temp_id'), + 'state_attcode' => '', + 'reconc_keys' => array(''), + 'db_table' => 'inline_image', + 'db_key_field' => 'id', + 'db_finalclass_field' => '', + 'indexes' => array( + array('temp_id'), + array('item_class', 'item_id'), + array('item_org_id'), + ), + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + MetaModel::Init_AddAttribute(new AttributeDateTime("expire", array("allowed_values"=>null, "sql"=>'expire', "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array(), "always_load_in_tables"=>false))); + MetaModel::Init_AddAttribute(new AttributeString("temp_id", array("allowed_values"=>null, "sql"=>'temp_id', "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array(), "always_load_in_tables"=>false))); + MetaModel::Init_AddAttribute(new AttributeString("item_class", array("allowed_values"=>null, "sql"=>'item_class', "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array(), "always_load_in_tables"=>false))); + MetaModel::Init_AddAttribute(new AttributeObjectKey("item_id", array("class_attcode"=>'item_class', "allowed_values"=>null, "sql"=>'item_id', "is_null_allowed"=>true, "depends_on"=>array(), "always_load_in_tables"=>false))); + MetaModel::Init_AddAttribute(new AttributeInteger("item_org_id", array("allowed_values"=>null, "sql"=>'item_org_id', "default_value"=>'0', "is_null_allowed"=>true, "depends_on"=>array(), "always_load_in_tables"=>false))); + MetaModel::Init_AddAttribute(new AttributeBlob("contents", array("is_null_allowed"=>false, "depends_on"=>array(), "always_load_in_tables"=>false))); + + + + MetaModel::Init_SetZListItems('details', array('temp_id', 'item_class', 'item_id', 'item_org_id')); + MetaModel::Init_SetZListItems('standard_search', array('temp_id', 'item_class', 'item_id')); + MetaModel::Init_SetZListItems('list', array('temp_id', 'item_class', 'item_id' )); + } + + + /** + * Maps the given context parameter name to the appropriate filter/search code for this class + * @param string $sContextParam Name of the context parameter, e.g. 'org_id' + * @return string Filter code, e.g. 'customer_id' + */ + public static function MapContextParam($sContextParam) + { + if ($sContextParam == 'org_id') + { + return 'item_org_id'; + } + else + { + return null; + } + } + + /** + * Set/Update all of the '_item' fields + * @param DBObject $oItem Container item + * @return void + */ + public function SetItem(DBObject $oItem, $bUpdateOnChange = false) + { + $sClass = get_class($oItem); + $iItemId = $oItem->GetKey(); + + $this->Set('item_class', $sClass); + $this->Set('item_id', $iItemId); + + $aCallSpec = array($sClass, 'MapContextParam'); + if (is_callable($aCallSpec)) + { + $sAttCode = call_user_func($aCallSpec, 'org_id'); // Returns null when there is no mapping for this parameter + if (MetaModel::IsValidAttCode($sClass, $sAttCode)) + { + $iOrgId = $oItem->Get($sAttCode); + if ($iOrgId > 0) + { + if ($iOrgId != $this->Get('item_org_id')) + { + $this->Set('item_org_id', $iOrgId); + if ($bUpdateOnChange) + { + $this->DBUpdate(); + } + } + } + } + } + } + + /** + * Give a default value for item_org_id (if relevant...) + * @return void + */ + public function SetDefaultOrgId() + { + // First check that the organization CAN be fetched from the target class + // + $sClass = $this->Get('item_class'); + $aCallSpec = array($sClass, 'MapContextParam'); + if (is_callable($aCallSpec)) + { + $sAttCode = call_user_func($aCallSpec, 'org_id'); // Returns null when there is no mapping for this parameter + if (MetaModel::IsValidAttCode($sClass, $sAttCode)) + { + // Second: check that the organization CAN be fetched from the current user + // + if (MetaModel::IsValidClass('Person')) + { + $aCallSpec = array($sClass, 'MapContextParam'); + if (is_callable($aCallSpec)) + { + $sAttCode = call_user_func($aCallSpec, 'org_id'); // Returns null when there is no mapping for this parameter + if (MetaModel::IsValidAttCode($sClass, $sAttCode)) + { + // OK - try it + // + $oCurrentPerson = MetaModel::GetObject('Person', UserRights::GetContactId(), false); + if ($oCurrentPerson) + { + $this->Set('item_org_id', $oCurrentPerson->Get($sAttCode)); + } + } + } + } + } + } + } + + /** + * When posting a form, finalize the creation of the inline images + * related to the specified object + * + * @param DBObject $oObject + */ + public static function FinalizeInlineImages(DBObject $oObject) + { + $iTransactionId = utils::ReadParam('transaction_id', null); + if (!is_null($iTransactionId)) + { + // Attach new (temporary) inline images + + $sTempId = session_id().'_'.$iTransactionId; + // The object is being created from a form, check if there are pending inline images for this object + $sOQL = 'SELECT InlineImage WHERE temp_id = :temp_id'; + $oSearch = DBObjectSearch::FromOQL($sOQL); + $oSet = new DBObjectSet($oSearch, array(), array('temp_id' => $sTempId)); + while($oInlineImage = $oSet->Fetch()) + { + $oInlineImage->SetItem($oObject); + $oInlineImage->Set('temp_id', ''); + $oInlineImage->DBUpdate(); + } + } + } + + /** + * Cleanup the pending images if the form is not submitted + * @param string $sTempId + */ + public static function OnFormCancel($sTempId) + { + // Delete all "pending" InlineImages for this form + $sOQL = 'SELECT InlineImage WHERE temp_id = :temp_id'; + $oSearch = DBObjectSearch::FromOQL($sOQL); + $oSet = new DBObjectSet($oSearch, array(), array('temp_id' => $sTempId)); + while($oInlineImage = $oSet->Fetch()) + { + $oInlineImage->DBDelete(); + } + } + + /** + * Parses the supplied HTML fragment to rebuild the attribute src="" for images + * that refer to an InlineImage (detected via the attribute data-img-id="") so that + * the URL is consistent with the current URL of the application. + * @param string $sHtml The HTML fragment to process + * @return string The modified HTML + */ + public static function FixUrls($sHtml) + { + $aNeedles = array(); + $aReplacements = array(); + // Find img tags with an attribute data-img-id + if (preg_match_all('/]*)data-img-id="([0-9]+)"([^>]*)>/i', $sHtml, $aMatches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) + { + $sUrl = utils::GetAbsoluteUrlAppRoot().INLINEIMAGE_DOWNLOAD_URL; + foreach($aMatches as $aImgInfo) + { + $sImgTag = $aImgInfo[0][0]; + $sAttId = $aImgInfo[2][0]; + + $sNewImgTag = preg_replace('/src="[^"]+"/', 'src="'.$sUrl.$sAttId.'"', $sImgTag); // preserve other attributes + $aNeedles[] = $sImgTag; + $aReplacements[] = $sNewImgTag; + } + $sHtml = str_replace($aNeedles, $aReplacements, $sHtml); + } + return $sHtml; + } + + /** + * Get the javascript fragment - to be added to "on document ready" - to adjust (on the fly) the width on Inline Images + */ + public static function FixImagesWidth() + { + $iMaxWidth = (int)MetaModel::GetConfig()->Get('inline_image_max_display_width', 0); + $sJS = ''; + if ($iMaxWidth != 0) + { + $sJS = +<< $iMaxWidth) + { + $(this).css({'max-width': '{$iMaxWidth}px', width: '', height: '', 'max-height': ''}); + } + $(this).addClass('inline-image').attr('href', $(this).attr('src')); +}).magnificPopup({type: 'image', closeOnContentClick: true }); +EOF + ; + } + + return $sJS; + } + + /** + * Check if an the given mimeType is an image that can be processed by the system + * @param string $sMimeType + * @return boolean + */ + public static function IsImage($sMimeType) + { + if (!function_exists('gd_info')) return false; // no image processing capability on this system + + $bRet = false; + $aInfo = gd_info(); // What are the capabilities + switch($sMimeType) + { + case 'image/gif': + return $aInfo['GIF Read Support']; + break; + + case 'image/jpeg': + return $aInfo['JPEG Support']; + break; + + case 'image/png': + return $aInfo['PNG Support']; + break; + + } + return $bRet; + } + + /** + * Resize an image so that it fits the maximum width/height defined in the config file + * @param ormDocument $oImage The original image stored as an array (content / mimetype / filename) + * @return ormDocument The resampled image (or the original one if it already fit) + */ + public static function ResizeImageToFit(ormDocument $oImage, &$aDimensions = null) + { + $img = false; + switch($oImage->GetMimeType()) + { + case 'image/gif': + case 'image/jpeg': + case 'image/png': + $img = @imagecreatefromstring($oImage->GetData()); + break; + + default: + // Unsupported image type, return the image as-is + $aDimensions = null; + return $oImage; + } + if ($img === false) + { + $aDimensions = null; + return $oImage; + } + else + { + // Let's scale the image, preserving the transparency for GIFs and PNGs + $iWidth = imagesx($img); + $iHeight = imagesy($img); + $aDimensions = array('width' => $iWidth, 'height' => $iHeight); + $iMaxImageSize = (int)MetaModel::GetConfig()->Get('inline_image_max_storage_width', 0); + + if (($iMaxImageSize > 0) && ($iWidth <= $iMaxImageSize) && ($iHeight <= $iMaxImageSize)) + { + // No need to resize + return $oImage; + } + + $fScale = min($iMaxImageSize / $iWidth, $iMaxImageSize / $iHeight); + + $iNewWidth = $iWidth * $fScale; + $iNewHeight = $iHeight * $fScale; + + $aDimensions['width'] = $iNewWidth; + $aDimensions['height'] = $iNewHeight; + + $new = imagecreatetruecolor($iNewWidth, $iNewHeight); + + // Preserve transparency + if(($oImage->GetMimeType() == "image/gif") || ($oImage->GetMimeType() == "image/png")) + { + imagecolortransparent($new, imagecolorallocatealpha($new, 0, 0, 0, 127)); + imagealphablending($new, false); + imagesavealpha($new, true); + } + + imagecopyresampled($new, $img, 0, 0, 0, 0, $iNewWidth, $iNewHeight, $iWidth, $iHeight); + + ob_start(); + switch ($oImage->GetMimeType()) + { + case 'image/gif': + imagegif($new); // send image to output buffer + break; + + case 'image/jpeg': + imagejpeg($new, null, 80); // null = send image to output buffer, 80 = good quality + break; + + case 'image/png': + imagepng($new, null, 5); // null = send image to output buffer, 5 = medium compression + break; + } + $oNewImage = new ormDocument(ob_get_contents(), $oImage->GetMimeType(), $oImage->GetFileName()); + @ob_end_clean(); + + imagedestroy($img); + imagedestroy($new); + + return $oNewImage; + } + + } + + /** + * Get the (localized) textual representation of the max upload size + * @return string + */ + public static function GetMaxUpload() + { + $iMaxUpload = ini_get('upload_max_filesize'); + if (!$iMaxUpload) + { + $sRet = Dict::S('Attachments:UploadNotAllowedOnThisSystem'); + } + else + { + $iMaxUpload = utils::ConvertToBytes($iMaxUpload); + if ($iMaxUpload > 1024*1024*1024) + { + $sRet = Dict::Format('Attachment:Max_Go', sprintf('%0.2f', $iMaxUpload/(1024*1024*1024))); + } + else if ($iMaxUpload > 1024*1024) + { + $sRet = Dict::Format('Attachment:Max_Mo', sprintf('%0.2f', $iMaxUpload/(1024*1024))); + } + else + { + $sRet = Dict::Format('Attachment:Max_Ko', sprintf('%0.2f', $iMaxUpload/(1024))); + } + } + return $sRet; + } + + /** + * Get the fragment of javascript needed to complete the initialization of + * CKEditor when creating/modifying an object + * + * @param DBObject $oObject The object being edited + * @param string $sTempId The concatenation of session_id().'_'.$iTransactionId. + * @return string The JS fragment to insert in "on document ready" + */ + public static function EnableCKEditorImageUpload(DBObject $oObject, $sTempId) + { + $sObjClass = get_class($oObject); + $iObjKey = $oObject->GetKey(); + + return +<<' ); + } + }); + }); +EOF + ; + } +} \ No newline at end of file diff --git a/core/ormcaselog.class.inc.php b/core/ormcaselog.class.inc.php index b08003470..8a22ebfbb 100644 --- a/core/ormcaselog.class.inc.php +++ b/core/ormcaselog.class.inc.php @@ -198,7 +198,7 @@ class ormCaseLog { } else { - $sTextEntry = utils::FixInlineAttachments($sTextEntry); + $sTextEntry = InlineImage::FixUrls($sTextEntry); } $iPos += $aIndex[$index]['text_length']; @@ -286,7 +286,7 @@ class ormCaseLog { } else { - $sTextEntry = utils::FixInlineAttachments($sTextEntry); + $sTextEntry = InlineImage::FixUrls($sTextEntry); } $iPos += $aIndex[$index]['text_length']; @@ -389,7 +389,7 @@ class ormCaseLog { } else { - $sTextEntry = utils::FixInlineAttachments($sTextEntry); + $sTextEntry = InlineImage::FixUrls($sTextEntry); } $iPos += $aIndex[$index]['text_length']; diff --git a/datamodels/2.x/itop-attachments/css/magnific-popup.css b/css/magnific-popup.css similarity index 100% rename from datamodels/2.x/itop-attachments/css/magnific-popup.css rename to css/magnific-popup.css diff --git a/datamodels/2.x/itop-attachments/main.attachments.php b/datamodels/2.x/itop-attachments/main.attachments.php index 2ab248fe4..0483c3428 100755 --- a/datamodels/2.x/itop-attachments/main.attachments.php +++ b/datamodels/2.x/itop-attachments/main.attachments.php @@ -16,6 +16,8 @@ // You should have received a copy of the GNU Affero General Public License // along with iTop. If not, see +define('ATTACHMENT_DOWNLOAD_URL', 'pages/ajax.render.php?operation=download_document&class=Attachment&field=contents&id='); + class AttachmentPlugIn implements iApplicationUIExtension, iApplicationObjectExtension { protected static $m_bIsModified = false; @@ -241,9 +243,6 @@ EOF $oPage->add('
'); $oPage->add(''.Dict::S('Attachments:FieldsetTitle').''); - $oPage->add_linked_stylesheet(utils::GetAbsoluteUrlModulesRoot().'itop-attachments/css/magnific-popup.css'); - $oPage->add_linked_script(utils::GetAbsoluteUrlModulesRoot().'itop-attachments/js/jquery.magnific-popup.min.js'); - if ($bEditMode) { $sIsDeleteEnabled = $this->m_bDeleteEnabled ? 'true' : 'false'; @@ -322,15 +321,6 @@ EOF $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.iframe-transport.js'); $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.fileupload.js'); - $maxWidth = MetaModel::GetModuleSetting('itop-standard-email-synchro', 'inline_image_max_width', ''); - if ($maxWidth !== '') - { - $sStyle = "style=\"max-width:{$maxWidth}px;cursor:zoom-in;\""; - } - else - { - $sStyle = "style=\"cursor:zoom-in;\""; - } $sDownloadLink = utils::GetAbsoluteUrlAppRoot().ATTACHMENT_DOWNLOAD_URL; $oPage->add_ready_script( <<< EOF @@ -406,43 +396,6 @@ EOF dropZone.removeClass('drag_in'); }, 300); }); - - // Hook the file upload of all CKEditor instances - $('.htmlEditor').each(function() { - var oEditor = $(this).ckeditorGet(); - oEditor.config.extraPlugins = 'uploadimage'; - oEditor.config.uploadUrl = GetAbsoluteUrlModulesRoot()+'itop-attachments/ajax.attachment.php'; - oEditor.config.filebrowserBrowseUrl = GetAbsoluteUrlModulesRoot()+'itop-attachments/ajax.attachment.php?operation=cke_browse&temp_id=$sTempId&obj_class=$sClass&obj_key=$iObjectId'; - oEditor.on( 'fileUploadResponse', function( evt ) { - // Get XHR and response. - var data = evt.data, - xhr = data.fileLoader.xhr, - response = xhr.responseText.split( '|' ); - - var oValues = JSON.parse(response[0]); - - var sDownloadLink = '$sDownloadLink'+oValues.att_id; - $('#attachments').append(''); - if(true) - { - $('#display_attachment_'+oValues.att_id).hover( function() { $(this).children(':button').toggleClass('btn_hidden'); } ); - } - $('#attachment_plugin').trigger('add_attachment', [oValues.att_id, oValues.msg, true /* inline image */]); - } ); - - oEditor.on( 'fileUploadRequest', function( evt ) { - evt.data.fileLoader.uploadUrl += '?operation=cke_img_upload&temp_id=$sTempId&obj_class=$sClass'; - }, null, null, 4 ); // Listener with priority 4 will be executed before priority 5. - - }); - - $('img[data-att-id]').each(function() { - if ('$sMaxWidth' != '') - { - $(this).css({'max-width': '$sMaxWidth', width: '', height: '', 'max-height': ''}); - } - $(this).addClass('inline-image').attr('href', $(this).attr('src')); - }).magnificPopup({type: 'image', closeOnContentClick: true }); // check if the attachments are used by inline images window.setTimeout( function() { @@ -505,15 +458,6 @@ EOF position: { my: 'left top', at: 'right top', using: function( position, feedback ) { $( this ).css( position ); }}, content: function() { if ($(this).attr('data-preview') == 'true') { return('');} else { return '$sPreviewNotAvailable'; }} }); - - $('img[data-att-id]').each(function() { - if ('$sMaxWidth' != '') - { - $(this).css({'max-width': '$sMaxWidth', width: '', height: '', 'max-height': ''}); - } - $(this).addClass('inline-image'); - $(this).attr('href', $(this).attr('src')); - }).magnificPopup({type: 'image', closeOnContentClick: true }); EOF ); } diff --git a/dictionaries/dictionary.itop.ui.php b/dictionaries/dictionary.itop.ui.php index 2ec2691a5..df45ca591 100644 --- a/dictionaries/dictionary.itop.ui.php +++ b/dictionaries/dictionary.itop.ui.php @@ -383,6 +383,7 @@ Dict::Add('EN US', 'English', 'English', array( 'UI:Button:Rename' => ' Rename... ', 'UI:Button:ChangePassword' => ' Change Password ', 'UI:Button:ResetPassword' => ' Reset Password ', + 'UI:Button:Insert' => 'Insert', 'UI:SearchToggle' => 'Search', 'UI:ClickToCreateNew' => 'Create a new %1$s', @@ -1293,5 +1294,11 @@ When associated with a trigger, each action is given an "order" number, specifyi 'UI:Menu:ExportPDF' => 'Export as PDF...', 'UI:Menu:PrintableVersion' => 'Printer friendly version', + + 'UI:BrowseInlineImages' => 'Browse images...', + 'UI:UploadInlineImageLegend' => 'Upload a new image', + 'UI:SelectInlineImageToUpload' => 'Select the image to upload', + 'UI:AvailableInlineImagesLegend' => 'Available images', + 'UI:NoInlineImage' => 'There is no image available on the server. Use the "Browse" button above to select an image from your computer and upload it to the server.', )); ?> diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php index d5a2be922..8261c6f2f 100644 --- a/dictionaries/fr.dictionary.itop.ui.php +++ b/dictionaries/fr.dictionary.itop.ui.php @@ -263,6 +263,7 @@ Dict::Add('FR FR', 'French', 'Français', array( 'UI:Button:Rename' => ' Renommer... ', 'UI:Button:ChangePassword' => ' Changer ! ', 'UI:Button:ResetPassword' => ' Ràz du mot de passe ', + 'UI:Button:Insert' => 'Insérer', 'UI:SearchToggle' => 'Recherche', 'UI:ClickToCreateNew' => 'Créer un nouvel objet de type %1$s', 'UI:SearchFor_Class' => 'Rechercher des objets de type %1$s', @@ -1135,4 +1136,10 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé 'UI:Menu:ExportPDF' => 'Exporter en PDF...', 'UI:Menu:PrintableVersion' => 'Version imprimable', + + 'UI:BrowseInlineImages' => 'Parcourir les images...', + 'UI:UploadInlineImageLegend' => 'Ajouter une image', + 'UI:SelectInlineImageToUpload' => 'Sélectionnez l\'image à ajouter', + 'UI:AvailableInlineImagesLegend' => 'Images disponibles', + 'UI:NoInlineImage' => 'Il n\'y a aucune image de disponible sur le serveur. Utilisez le bouton "Parcourir" (ci-dessus) pour sélectionner une image sur votre ordinateur et la télécharger sur le serveur.', )); \ No newline at end of file diff --git a/js/ckeditor/build-config.js b/js/ckeditor/build-config.js index 3e4e58cd3..a407cd087 100644 --- a/js/ckeditor/build-config.js +++ b/js/ckeditor/build-config.js @@ -13,10 +13,10 @@ * (1) http://ckeditor.com/builder * Visit online builder to build CKEditor from scratch. * - * (2) http://ckeditor.com/builder/1de7fad5b64ec63d0b3f735553a4b527 + * (2) http://ckeditor.com/builder/e55cbf89ec0d2a5ac973d24e8726b9f1 * Visit online builder to build CKEditor, starting with the same setup as before. * - * (3) http://ckeditor.com/builder/download/1de7fad5b64ec63d0b3f735553a4b527 + * (3) http://ckeditor.com/builder/download/e55cbf89ec0d2a5ac973d24e8726b9f1 * Straight download link to the latest version of CKEditor (Optimized) with the same setup as before. * * NOTE: @@ -55,6 +55,7 @@ var CKBUILDER_CONFIG = { 'basicstyles' : 1, 'blockquote' : 1, 'clipboard' : 1, + 'colorbutton' : 1, 'contextmenu' : 1, 'elementspath' : 1, 'enterkey' : 1, diff --git a/js/ckeditor/ckeditor.js b/js/ckeditor/ckeditor.js index cfad4fb75..b0f45c806 100644 --- a/js/ckeditor/ckeditor.js +++ b/js/ckeditor/ckeditor.js @@ -598,8 +598,8 @@ arguments;return function(){var b=this&&this.getValue?this.getValue():a[0],c,d=C notEmpty:function(b){return this.regex(a,b)},integer:function(a){return this.regex(b,a)},number:function(a){return this.regex(c,a)},cssLength:function(a){return this.functions(function(a){return e.test(CKEDITOR.tools.trim(a))},a)},htmlLength:function(a){return this.functions(function(a){return d.test(CKEDITOR.tools.trim(a))},a)},inlineStyle:function(a){return this.functions(function(a){return f.test(CKEDITOR.tools.trim(a))},a)},equals:function(a,b){return this.functions(function(b){return b==a},b)}, notEqual:function(a,b){return this.functions(function(b){return b!=a},b)}};CKEDITOR.on("instanceDestroyed",function(a){if(CKEDITOR.tools.isEmpty(CKEDITOR.instances)){for(var b;b=CKEDITOR.dialog._.currentTop;)b.hide();for(var c in z)z[c].remove();z={}}a=a.editor._.storedDialogs;for(var d in a)a[d].destroy()})})();CKEDITOR.tools.extend(CKEDITOR.editor.prototype,{openDialog:function(a,b){var c=null,d=CKEDITOR.dialog._.dialogDefinitions[a];null===CKEDITOR.dialog._.currentTop&&N(this);if("function"==typeof d)c= this._.storedDialogs||(this._.storedDialogs={}),c=c[a]||(c[a]=new CKEDITOR.dialog(this,a)),b&&b.call(c,c),c.show();else{if("failed"==d)throw O(this),Error('[CKEDITOR.dialog.openDialog] Dialog "'+a+'" failed when loading definition.');"string"==typeof d&&CKEDITOR.scriptLoader.load(CKEDITOR.getUrl(d),function(){"function"!=typeof CKEDITOR.dialog._.dialogDefinitions[a]&&(CKEDITOR.dialog._.dialogDefinitions[a]="failed");this.openDialog(a,b)},this,0,1)}CKEDITOR.skin.loadPart("dialog");return c}})})(); -CKEDITOR.plugins.add("dialog",{requires:"dialogui",init:function(x){x.on("doubleclick",function(A){A.data.dialog&&x.openDialog(A.data.dialog)},null,null,999)}});CKEDITOR.plugins.add("about",{requires:"dialog",init:function(a){var b=a.addCommand("about",new CKEDITOR.dialogCommand("about"));b.modes={wysiwyg:1,source:1};b.canUndo=!1;b.readOnly=1;a.ui.addButton&&a.ui.addButton("About",{label:a.lang.about.title,command:"about",toolbar:"about"});CKEDITOR.dialog.add("about",this.path+"dialogs/about.js")}});(function(){CKEDITOR.plugins.add("a11yhelp",{requires:"dialog",availableLangs:{af:1,ar:1,bg:1,ca:1,cs:1,cy:1,da:1,de:1,"de-ch":1,el:1,en:1,"en-gb":1,eo:1,es:1,et:1,eu:1,fa:1,fi:1,fo:1,fr:1,"fr-ca":1,gl:1,gu:1,he:1,hi:1,hr:1,hu:1,id:1,it:1,ja:1,km:1,ko:1,ku:1,lt:1,lv:1,mk:1,mn:1,nb:1,nl:1,no:1,pl:1,pt:1,"pt-br":1,ro:1,ru:1,si:1,sk:1,sl:1,sq:1,sr:1,"sr-latn":1,sv:1,th:1,tr:1,tt:1,ug:1,uk:1,vi:1,zh:1,"zh-cn":1},init:function(b){var c=this;b.addCommand("a11yHelp",{exec:function(){var a=b.langCode,a=c.availableLangs[a]? -a:c.availableLangs[a.replace(/-.*/,"")]?a.replace(/-.*/,""):"en";CKEDITOR.scriptLoader.load(CKEDITOR.getUrl(c.path+"dialogs/lang/"+a+".js"),function(){b.lang.a11yhelp=c.langEntries[a];b.openDialog("a11yHelp")})},modes:{wysiwyg:1,source:1},readOnly:1,canUndo:!1});b.setKeystroke(CKEDITOR.ALT+48,"a11yHelp");CKEDITOR.dialog.add("a11yHelp",this.path+"dialogs/a11yhelp.js");b.on("ariaEditorHelpLabel",function(a){a.data.label=b.lang.common.editorHelp})}})})();CKEDITOR.plugins.add("basicstyles",{init:function(c){var e=0,d=function(g,d,b,a){if(a){a=new CKEDITOR.style(a);var f=h[b];f.unshift(a);c.attachStyleStateChange(a,function(a){!c.readOnly&&c.getCommand(b).setState(a)});c.addCommand(b,new CKEDITOR.styleCommand(a,{contentForms:f}));c.ui.addButton&&c.ui.addButton(g,{label:d,command:b,toolbar:"basicstyles,"+(e+=10)})}},h={bold:["strong","b",["span",function(a){a=a.styles["font-weight"];return"bold"==a||700<=+a}]],italic:["em","i",["span",function(a){return"italic"== +CKEDITOR.plugins.add("dialog",{requires:"dialogui",init:function(x){x.on("doubleclick",function(A){A.data.dialog&&x.openDialog(A.data.dialog)},null,null,999)}});(function(){CKEDITOR.plugins.add("a11yhelp",{requires:"dialog",availableLangs:{af:1,ar:1,bg:1,ca:1,cs:1,cy:1,da:1,de:1,"de-ch":1,el:1,en:1,"en-gb":1,eo:1,es:1,et:1,eu:1,fa:1,fi:1,fo:1,fr:1,"fr-ca":1,gl:1,gu:1,he:1,hi:1,hr:1,hu:1,id:1,it:1,ja:1,km:1,ko:1,ku:1,lt:1,lv:1,mk:1,mn:1,nb:1,nl:1,no:1,pl:1,pt:1,"pt-br":1,ro:1,ru:1,si:1,sk:1,sl:1,sq:1,sr:1,"sr-latn":1,sv:1,th:1,tr:1,tt:1,ug:1,uk:1,vi:1,zh:1,"zh-cn":1},init:function(b){var c=this;b.addCommand("a11yHelp",{exec:function(){var a=b.langCode,a=c.availableLangs[a]? +a:c.availableLangs[a.replace(/-.*/,"")]?a.replace(/-.*/,""):"en";CKEDITOR.scriptLoader.load(CKEDITOR.getUrl(c.path+"dialogs/lang/"+a+".js"),function(){b.lang.a11yhelp=c.langEntries[a];b.openDialog("a11yHelp")})},modes:{wysiwyg:1,source:1},readOnly:1,canUndo:!1});b.setKeystroke(CKEDITOR.ALT+48,"a11yHelp");CKEDITOR.dialog.add("a11yHelp",this.path+"dialogs/a11yhelp.js");b.on("ariaEditorHelpLabel",function(a){a.data.label=b.lang.common.editorHelp})}})})();CKEDITOR.plugins.add("about",{requires:"dialog",init:function(a){var b=a.addCommand("about",new CKEDITOR.dialogCommand("about"));b.modes={wysiwyg:1,source:1};b.canUndo=!1;b.readOnly=1;a.ui.addButton&&a.ui.addButton("About",{label:a.lang.about.title,command:"about",toolbar:"about"});CKEDITOR.dialog.add("about",this.path+"dialogs/about.js")}});CKEDITOR.plugins.add("basicstyles",{init:function(c){var e=0,d=function(g,d,b,a){if(a){a=new CKEDITOR.style(a);var f=h[b];f.unshift(a);c.attachStyleStateChange(a,function(a){!c.readOnly&&c.getCommand(b).setState(a)});c.addCommand(b,new CKEDITOR.styleCommand(a,{contentForms:f}));c.ui.addButton&&c.ui.addButton(g,{label:d,command:b,toolbar:"basicstyles,"+(e+=10)})}},h={bold:["strong","b",["span",function(a){a=a.styles["font-weight"];return"bold"==a||700<=+a}]],italic:["em","i",["span",function(a){return"italic"== a.styles["font-style"]}]],underline:["u",["span",function(a){return"underline"==a.styles["text-decoration"]}]],strike:["s","strike",["span",function(a){return"line-through"==a.styles["text-decoration"]}]],subscript:["sub"],superscript:["sup"]},b=c.config,a=c.lang.basicstyles;d("Bold",a.bold,"bold",b.coreStyles_bold);d("Italic",a.italic,"italic",b.coreStyles_italic);d("Underline",a.underline,"underline",b.coreStyles_underline);d("Strike",a.strike,"strike",b.coreStyles_strike);d("Subscript",a.subscript, "subscript",b.coreStyles_subscript);d("Superscript",a.superscript,"superscript",b.coreStyles_superscript);c.setKeystroke([[CKEDITOR.CTRL+66,"bold"],[CKEDITOR.CTRL+73,"italic"],[CKEDITOR.CTRL+85,"underline"]])}});CKEDITOR.config.coreStyles_bold={element:"strong",overrides:"b"};CKEDITOR.config.coreStyles_italic={element:"em",overrides:"i"};CKEDITOR.config.coreStyles_underline={element:"u"};CKEDITOR.config.coreStyles_strike={element:"s",overrides:"strike"};CKEDITOR.config.coreStyles_subscript={element:"sub"}; CKEDITOR.config.coreStyles_superscript={element:"sup"};(function(){var m={exec:function(g){var a=g.getCommand("blockquote").state,k=g.getSelection(),c=k&&k.getRanges()[0];if(c){var h=k.createBookmarks();if(CKEDITOR.env.ie){var e=h[0].startNode,b=h[0].endNode,d;if(e&&"blockquote"==e.getParent().getName())for(d=e;d=d.getNext();)if(d.type==CKEDITOR.NODE_ELEMENT&&d.isBlockBoundary()){e.move(d,!0);break}if(b&&"blockquote"==b.getParent().getName())for(d=b;d=d.getPrevious();)if(d.type==CKEDITOR.NODE_ELEMENT&&d.isBlockBoundary()){b.move(d);break}}var f=c.createIterator(); @@ -686,33 +686,7 @@ this.icon);a={id:h,name:this.name,iconName:g,label:this.label,cls:this.className p.output(a,c)}}})})();CKEDITOR.config.menu_groups="clipboard,form,tablecell,tablecellproperties,tablerow,tablecolumn,table,anchor,link,image,flash,checkbox,radio,textfield,hiddenfield,imagebutton,button,select,textarea,div";CKEDITOR.plugins.add("contextmenu",{requires:"menu",onLoad:function(){CKEDITOR.plugins.contextMenu=CKEDITOR.tools.createClass({base:CKEDITOR.menu,$:function(a){this.base.call(this,a,{panel:{className:"cke_menu_panel",attributes:{"aria-label":a.lang.contextmenu.options}}})},proto:{addTarget:function(a,e){a.on("contextmenu",function(a){a=a.data;var c=CKEDITOR.env.webkit?f:CKEDITOR.env.mac?a.$.metaKey:a.$.ctrlKey;if(!e||!c){a.preventDefault();if(CKEDITOR.env.mac&&CKEDITOR.env.webkit){var c=this.editor, b=(new CKEDITOR.dom.elementPath(a.getTarget(),c.editable())).contains(function(a){return a.hasAttribute("contenteditable")},!0);b&&"false"==b.getAttribute("contenteditable")&&c.getSelection().fake(b)}var b=a.getTarget().getDocument(),d=a.getTarget().getDocument().getDocumentElement(),c=!b.equals(CKEDITOR.document),b=b.getWindow().getScrollPosition(),g=c?a.$.clientX:a.$.pageX||b.x+a.$.clientX,h=c?a.$.clientY:a.$.pageY||b.y+a.$.clientY;CKEDITOR.tools.setTimeout(function(){this.open(d,null,g,h)},CKEDITOR.env.ie? 200:0,this)}},this);if(CKEDITOR.env.webkit){var f,d=function(){f=0};a.on("keydown",function(a){f=CKEDITOR.env.mac?a.data.$.metaKey:a.data.$.ctrlKey});a.on("keyup",d);a.on("contextmenu",d)}},open:function(a,e,f,d){this.editor.focus();a=a||CKEDITOR.document.getDocumentElement();this.editor.selectionChange(1);this.show(a,e,f,d)}}})},beforeInit:function(a){var e=a.contextMenu=new CKEDITOR.plugins.contextMenu(a);a.on("contentDom",function(){e.addTarget(a.editable(),!1!==a.config.browserContextMenuOnCtrl)}); -a.addCommand("contextMenu",{exec:function(){a.contextMenu.open(a.document.getBody())}});a.setKeystroke(CKEDITOR.SHIFT+121,"contextMenu");a.setKeystroke(CKEDITOR.CTRL+CKEDITOR.SHIFT+121,"contextMenu")}});CKEDITOR.plugins.add("resize",{init:function(b){function f(d){var e=c.width,m=c.height,f=e+(d.data.$.screenX-n.x)*("rtl"==g?-1:1);d=m+(d.data.$.screenY-n.y);h&&(e=Math.max(a.resize_minWidth,Math.min(f,a.resize_maxWidth)));p&&(m=Math.max(a.resize_minHeight,Math.min(d,a.resize_maxHeight)));b.resize(h?e:null,m)}function k(){CKEDITOR.document.removeListener("mousemove",f);CKEDITOR.document.removeListener("mouseup",k);b.document&&(b.document.removeListener("mousemove",f),b.document.removeListener("mouseup", -k))}var a=b.config,r=b.ui.spaceId("resizer"),g=b.element?b.element.getDirection(1):"ltr";!a.resize_dir&&(a.resize_dir="vertical");void 0===a.resize_maxWidth&&(a.resize_maxWidth=3E3);void 0===a.resize_maxHeight&&(a.resize_maxHeight=3E3);void 0===a.resize_minWidth&&(a.resize_minWidth=750);void 0===a.resize_minHeight&&(a.resize_minHeight=250);if(!1!==a.resize_enabled){var l=null,n,c,h=("both"==a.resize_dir||"horizontal"==a.resize_dir)&&a.resize_minWidth!=a.resize_maxWidth,p=("both"==a.resize_dir||"vertical"== -a.resize_dir)&&a.resize_minHeight!=a.resize_maxHeight,q=CKEDITOR.tools.addFunction(function(d){l||(l=b.getResizable());c={width:l.$.offsetWidth||0,height:l.$.offsetHeight||0};n={x:d.screenX,y:d.screenY};a.resize_minWidth>c.width&&(a.resize_minWidth=c.width);a.resize_minHeight>c.height&&(a.resize_minHeight=c.height);CKEDITOR.document.on("mousemove",f);CKEDITOR.document.on("mouseup",k);b.document&&(b.document.on("mousemove",f),b.document.on("mouseup",k));d.preventDefault&&d.preventDefault()});b.on("destroy", -function(){CKEDITOR.tools.removeFunction(q)});b.on("uiSpace",function(a){if("bottom"==a.data.space){var e="";h&&!p&&(e=" cke_resizer_horizontal");!h&&p&&(e=" cke_resizer_vertical");var c='\x3cspan id\x3d"'+r+'" class\x3d"cke_resizer'+e+" cke_resizer_"+g+'" title\x3d"'+CKEDITOR.tools.htmlEncode(b.lang.common.resize)+'" onmousedown\x3d"CKEDITOR.tools.callFunction('+q+', event)"\x3e'+("ltr"==g?"◢":"◣")+"\x3c/span\x3e";"ltr"==g&&"ltr"==e?a.data.html+=c:a.data.html=c+a.data.html}},b,null,100);b.on("maximize", -function(a){b.ui.space("resizer")[a.data==CKEDITOR.TRISTATE_ON?"hide":"show"]()})}}});(function(){var c='\x3ca id\x3d"{id}" class\x3d"cke_button cke_button__{name} cke_button_{state} {cls}"'+(CKEDITOR.env.gecko&&!CKEDITOR.env.hc?"":" href\x3d\"javascript:void('{titleJs}')\"")+' title\x3d"{title}" tabindex\x3d"-1" hidefocus\x3d"true" role\x3d"button" aria-labelledby\x3d"{id}_label" aria-haspopup\x3d"{hasArrow}" aria-disabled\x3d"{ariaDisabled}"';CKEDITOR.env.gecko&&CKEDITOR.env.mac&&(c+=' onkeypress\x3d"return false;"');CKEDITOR.env.gecko&&(c+=' onblur\x3d"this.style.cssText \x3d this.style.cssText;"'); -var c=c+(' onkeydown\x3d"return CKEDITOR.tools.callFunction({keydownFn},event);" onfocus\x3d"return CKEDITOR.tools.callFunction({focusFn},event);" '+(CKEDITOR.env.ie?'onclick\x3d"return false;" onmouseup':"onclick")+'\x3d"CKEDITOR.tools.callFunction({clickFn},this);return false;"\x3e\x3cspan class\x3d"cke_button_icon cke_button__{iconName}_icon" style\x3d"{style}"'),c=c+'\x3e\x26nbsp;\x3c/span\x3e\x3cspan id\x3d"{id}_label" class\x3d"cke_button_label cke_button__{name}_label" aria-hidden\x3d"false"\x3e{label}\x3c/span\x3e{arrowHtml}\x3c/a\x3e', -r=CKEDITOR.addTemplate("buttonArrow",'\x3cspan class\x3d"cke_button_arrow"\x3e'+(CKEDITOR.env.hc?"\x26#9660;":"")+"\x3c/span\x3e"),t=CKEDITOR.addTemplate("button",c);CKEDITOR.plugins.add("button",{beforeInit:function(a){a.ui.addHandler(CKEDITOR.UI_BUTTON,CKEDITOR.ui.button.handler)}});CKEDITOR.UI_BUTTON="button";CKEDITOR.ui.button=function(a){CKEDITOR.tools.extend(this,a,{title:a.label,click:a.click||function(b){b.execCommand(a.command)}});this._={}};CKEDITOR.ui.button.handler={create:function(a){return new CKEDITOR.ui.button(a)}}; -CKEDITOR.ui.button.prototype={render:function(a,b){function c(){var e=a.mode;e&&(e=this.modes[e]?void 0!==k[e]?k[e]:CKEDITOR.TRISTATE_OFF:CKEDITOR.TRISTATE_DISABLED,e=a.readOnly&&!this.readOnly?CKEDITOR.TRISTATE_DISABLED:e,this.setState(e),this.refresh&&this.refresh())}var l=CKEDITOR.env,m=this._.id=CKEDITOR.tools.getNextId(),f="",g=this.command,n;this._.editor=a;var d={id:m,button:this,editor:a,focus:function(){CKEDITOR.document.getById(m).focus()},execute:function(){this.button.click(a)},attach:function(a){this.button.attach(a)}}, -u=CKEDITOR.tools.addFunction(function(a){if(d.onkey)return a=new CKEDITOR.dom.event(a),!1!==d.onkey(d,a.getKeystroke())}),v=CKEDITOR.tools.addFunction(function(a){var b;d.onfocus&&(b=!1!==d.onfocus(d,new CKEDITOR.dom.event(a)));return b}),p=0;d.clickFn=n=CKEDITOR.tools.addFunction(function(){p&&(a.unlockSelection(1),p=0);d.execute();l.iOS&&a.focus()});if(this.modes){var k={};a.on("beforeModeUnload",function(){a.mode&&this._.state!=CKEDITOR.TRISTATE_DISABLED&&(k[a.mode]=this._.state)},this);a.on("activeFilterChange", -c,this);a.on("mode",c,this);!this.readOnly&&a.on("readOnly",c,this)}else g&&(g=a.getCommand(g))&&(g.on("state",function(){this.setState(g.state)},this),f+=g.state==CKEDITOR.TRISTATE_ON?"on":g.state==CKEDITOR.TRISTATE_DISABLED?"disabled":"off");if(this.directional)a.on("contentDirChanged",function(b){var c=CKEDITOR.document.getById(this._.id),d=c.getFirst();b=b.data;b!=a.lang.dir?c.addClass("cke_"+b):c.removeClass("cke_ltr").removeClass("cke_rtl");d.setAttribute("style",CKEDITOR.skin.getIconStyle(h, -"rtl"==b,this.icon,this.iconOffset))},this);g||(f+="off");var q=this.name||this.command,h=q;this.icon&&!/\./.test(this.icon)&&(h=this.icon,this.icon=null);f={id:m,name:q,iconName:h,label:this.label,cls:this.className||"",state:f,ariaDisabled:"disabled"==f?"true":"false",title:this.title,titleJs:l.gecko&&!l.hc?"":(this.title||"").replace("'",""),hasArrow:this.hasArrow?"true":"false",keydownFn:u,focusFn:v,clickFn:n,style:CKEDITOR.skin.getIconStyle(h,"rtl"==a.lang.dir,this.icon,this.iconOffset),arrowHtml:this.hasArrow? -r.output():""};t.output(f,b);if(this.onRender)this.onRender();return d},setState:function(a){if(this._.state==a)return!1;this._.state=a;var b=CKEDITOR.document.getById(this._.id);return b?(b.setState(a,"cke_button"),a==CKEDITOR.TRISTATE_DISABLED?b.setAttribute("aria-disabled",!0):b.removeAttribute("aria-disabled"),this.hasArrow?(a=a==CKEDITOR.TRISTATE_ON?this._.editor.lang.button.selectedLabel.replace(/%1/g,this.label):this.label,CKEDITOR.document.getById(this._.id+"_label").setText(a)):a==CKEDITOR.TRISTATE_ON? -b.setAttribute("aria-pressed",!0):b.removeAttribute("aria-pressed"),!0):!1},getState:function(){return this._.state},toFeature:function(a){if(this._.feature)return this._.feature;var b=this;this.allowedContent||this.requiredContent||!this.command||(b=a.getCommand(this.command)||b);return this._.feature=b}};CKEDITOR.ui.prototype.addButton=function(a,b){this.add(a,CKEDITOR.UI_BUTTON,b)}})();(function(){function B(a){function d(){for(var b=g(),e=CKEDITOR.tools.clone(a.config.toolbarGroups)||q(a),f=0;fa.order?-1:0>b.order?1:b.orderthis.$.offsetHeight){var d=b.createRange();d[33==c?"moveToElementEditStart":"moveToElementEditEnd"](this);d.select();a.data.preventDefault()}});CKEDITOR.env.ie&&this.attachListener(c,"blur",function(){try{c.$.selection.empty()}catch(a){}});CKEDITOR.env.iOS&&this.attachListener(c,"touchend",function(){a.focus()});d=b.document.getElementsByTag("title").getItem(0);d.data("cke-title",d.getText());CKEDITOR.env.ie&&(b.document.$.title=this._.docTitle);CKEDITOR.tools.setTimeout(function(){"unloaded"== -this.status&&(this.status="ready");b.fire("contentDom");this._.isPendingFocus&&(b.focus(),this._.isPendingFocus=!1);setTimeout(function(){b.fire("dataReady")},0)},0,this)}function n(a){function f(){var c;a.editable().attachListener(a,"selectionChange",function(){var d=a.getSelection().getSelectedElement();d&&(c&&(c.detachEvent("onresizestart",b),c=null),d.$.attachEvent("onresizestart",b),c=d.$)})}function b(a){a.returnValue=!1}if(CKEDITOR.env.gecko)try{var c=a.document.$;c.execCommand("enableObjectResizing", -!1,!a.config.disableObjectResizing);c.execCommand("enableInlineTableEditing",!1,!a.config.disableNativeTableHandles)}catch(d){}else CKEDITOR.env.ie&&11>CKEDITOR.env.version&&a.config.disableObjectResizing&&f(a)}function p(){var a=[];if(8<=CKEDITOR.document.$.documentMode){a.push("html.CSS1Compat [contenteditable\x3dfalse]{min-height:0 !important}");var f=[],b;for(b in CKEDITOR.dtd.$removeEmpty)f.push("html.CSS1Compat "+b+"[contenteditable\x3dfalse]");a.push(f.join(",")+"{display:inline-block}")}else CKEDITOR.env.gecko&& -(a.push("html{height:100% !important}"),a.push("img:-moz-broken{-moz-force-broken-image-icon:1;min-width:24px;min-height:24px}"));a.push("html{cursor:text;*cursor:auto}");a.push("img,input,textarea{cursor:default}");return a.join("\n")}CKEDITOR.plugins.add("wysiwygarea",{init:function(a){a.config.fullPage&&a.addFeature({allowedContent:"html head title; style [media,type]; body (*)[id]; meta link [*]",requiredContent:"body"});a.addMode("wysiwyg",function(f){function b(b){b&&b.removeListener();a.editable(new l(a, -d.$.contentWindow.document.body));a.setData(a.getData(1),f)}var c="document.open();"+(CKEDITOR.env.ie?"("+CKEDITOR.tools.fixDomain+")();":"")+"document.close();",c=CKEDITOR.env.air?"javascript:void(0)":CKEDITOR.env.ie&&!CKEDITOR.env.edge?"javascript:void(function(){"+encodeURIComponent(c)+"}())":"",d=CKEDITOR.dom.element.createFromHtml('\x3ciframe src\x3d"'+c+'" frameBorder\x3d"0"\x3e\x3c/iframe\x3e');d.setStyles({width:"100%",height:"100%"});d.addClass("cke_wysiwyg_frame").addClass("cke_reset"); -c=a.ui.space("contents");c.append(d);var e=CKEDITOR.env.ie&&!CKEDITOR.env.edge||CKEDITOR.env.gecko;if(e)d.on("load",b);var g=a.title,h=a.fire("ariaEditorHelpLabel",{}).label;g&&(CKEDITOR.env.ie&&h&&(g+=", "+h),d.setAttribute("title",g));if(h){var g=CKEDITOR.tools.getNextId(),k=CKEDITOR.dom.element.createFromHtml('\x3cspan id\x3d"'+g+'" class\x3d"cke_voice_label"\x3e'+h+"\x3c/span\x3e");c.append(k,1);d.setAttribute("aria-describedby",g)}a.on("beforeModeUnload",function(a){a.removeListener();k&&k.remove()}); -d.setAttributes({tabIndex:a.tabIndex,allowTransparency:"true"});!e&&b();a.fire("ariaWidget",d)})}});CKEDITOR.editor.prototype.addContentsCss=function(a){var f=this.config,b=f.contentsCss;CKEDITOR.tools.isArray(b)||(f.contentsCss=b?[b]:[]);f.contentsCss.push(a)};var l=CKEDITOR.tools.createClass({$:function(){this.base.apply(this,arguments);this._.frameLoadedHandler=CKEDITOR.tools.addFunction(function(a){CKEDITOR.tools.setTimeout(m,0,this,a)},this);this._.docTitle=this.getWindow().getFrame().getAttribute("title")}, -base:CKEDITOR.editable,proto:{setData:function(a,f){var b=this.editor;if(f)this.setHtml(a),this.fixInitialSelection(),b.fire("dataReady");else{this._.isLoadingData=!0;b._.dataStore={id:1};var c=b.config,d=c.fullPage,e=c.docType,g=CKEDITOR.tools.buildStyleHtml(p()).replace(/