diff --git a/application/Html2Text.php b/application/Html2Text.php new file mode 100644 index 000000000..71a1ef493 --- /dev/null +++ b/application/Html2Text.php @@ -0,0 +1,321 @@ + + * @copyright Copyright 2012 Sean Murphy. All rights reserved. + * @license http://creativecommons.org/publicdomain/zero/1.0/ + * @link http://php.net/manual/function.str-replace.php + * + * @param mixed $search + * @param mixed $replace + * @param mixed $subject + * @param int $count + * @return mixed + */ +function mb_str_replace($search, $replace, $subject, &$count = 0) { + if (!is_array($subject)) { + // Normalize $search and $replace so they are both arrays of the same length + $searches = is_array($search) ? array_values($search) : array($search); + $replacements = is_array($replace) ? array_values($replace) : array($replace); + $replacements = array_pad($replacements, count($searches), ''); + foreach ($searches as $key => $search) { + $parts = mb_split(preg_quote($search), $subject); + $count += count($parts) - 1; + $subject = implode($replacements[$key], $parts); + } + } else { + // Call mb_str_replace for each subject in array, recursively + foreach ($subject as $key => $value) { + $subject[$key] = mb_str_replace($search, $replace, $value, $count); + } + } + return $subject; +} + +/****************************************************************************** + * Copyright (c) 2010 Jevon Wright and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * or + * + * LGPL which is available at http://www.gnu.org/licenses/lgpl.html + * + * + * Contributors: + * Jevon Wright - initial API and implementation + * Denis Flaven - some fixes for properly handling UTF-8 characters + ****************************************************************************/ + +class Html2Text { + + /** + * Tries to convert the given HTML into a plain text format - best suited for + * e-mail display, etc. + * + *

In particular, it tries to maintain the following features: + *

+ * + * @param string html the input HTML + * @return string the HTML converted, as best as possible, to text + * @throws Html2TextException if the HTML could not be loaded as a {@link DOMDocument} + */ + static function convert($html) { + // replace   with spaces + + $html = str_replace(" ", " ", $html); + $html = mb_str_replace("\xa0", " ", $html); // DO NOT USE str_replace since it breaks the "à" character which is \xc3 \xa0 in UTF-8 + + $html = static::fixNewlines($html); + + $doc = new \DOMDocument(); + if (!@$doc->loadHTML(''.$html)) // Forces the UTF-8 character set for HTML fragments + { + throw new Html2TextException("Could not load HTML - badly formed?", $html); + } + + $output = static::iterateOverNode($doc); + + // remove leading and trailing spaces on each line + $output = preg_replace("/[ \t]*\n[ \t]*/im", "\n", $output); + $output = preg_replace("/ *\t */im", "\t", $output); + + // remove unnecessary empty lines + $output = preg_replace("/\n\n\n*/im", "\n\n", $output); + + // remove leading and trailing whitespace + $output = trim($output); + + return $output; + } + + /** + * Unify newlines; in particular, \r\n becomes \n, and + * then \r becomes \n. This means that all newlines (Unix, Windows, Mac) + * all become \ns. + * + * @param string text text with any number of \r, \r\n and \n combinations + * @return string the fixed text + */ + static function fixNewlines($text) { + // replace \r\n to \n + $text = str_replace("\r\n", "\n", $text); + // remove \rs + $text = str_replace("\r", "\n", $text); + + return $text; + } + + static function nextChildName($node) { + // get the next child + $nextNode = $node->nextSibling; + while ($nextNode != null) { + if ($nextNode instanceof \DOMElement) { + break; + } + $nextNode = $nextNode->nextSibling; + } + $nextName = null; + if ($nextNode instanceof \DOMElement && $nextNode != null) { + $nextName = strtolower($nextNode->nodeName); + } + + return $nextName; + } + + static function prevChildName($node) { + // get the previous child + $nextNode = $node->previousSibling; + while ($nextNode != null) { + if ($nextNode instanceof \DOMElement) { + break; + } + $nextNode = $nextNode->previousSibling; + } + $nextName = null; + if ($nextNode instanceof \DOMElement && $nextNode != null) { + $nextName = strtolower($nextNode->nodeName); + } + + return $nextName; + } + + static function iterateOverNode($node) { + if ($node instanceof \DOMText) { + // Replace whitespace characters with a space (equivilant to \s) + return preg_replace("/[\\t\\n\\f\\r ]+/im", " ", $node->wholeText); + } + if ($node instanceof \DOMDocumentType) { + // ignore + return ""; + } + + $nextName = static::nextChildName($node); + $prevName = static::prevChildName($node); + + $name = strtolower($node->nodeName); + + // start whitespace + switch ($name) { + case "hr": + return "---------------------------------------------------------------\n"; + + case "style": + case "head": + case "title": + case "meta": + case "script": + // ignore these tags + return ""; + + case "h1": + case "h2": + case "h3": + case "h4": + case "h5": + case "h6": + case "ol": + case "ul": + // add two newlines, second line is added below + $output = "\n"; + break; + + case "td": + case "th": + // add tab char to separate table fields + $output = "\t"; + break; + + case "tr": + case "p": + case "div": + // add one line + $output = "\n"; + break; + + case "li": + $output = "- "; + break; + + default: + // print out contents of unknown tags + $output = ""; + break; + } + + // debug + //$output .= "[$name,$nextName]"; + + if (isset($node->childNodes)) { + for ($i = 0; $i < $node->childNodes->length; $i++) { + $n = $node->childNodes->item($i); + + $text = static::iterateOverNode($n); + + $output .= $text; + } + } + + // end whitespace + switch ($name) { + case "h1": + case "h2": + case "h3": + case "h4": + case "h5": + case "h6": + $output .= "\n"; + break; + + case "p": + case "br": + // add one line + if ($nextName != "div") + $output .= "\n"; + break; + + case "div": + // add one line only if the next child isn't a div + if ($nextName != "div" && $nextName != null) + $output .= "\n"; + break; + + case "a": + // links are returned in [text](link) format + $href = $node->getAttribute("href"); + + $output = trim($output); + + // remove double [[ ]] s from linking images + if (substr($output, 0, 1) == "[" && substr($output, -1) == "]") { + $output = substr($output, 1, strlen($output) - 2); + + // for linking images, the title of the overrides the title of the + if ($node->getAttribute("title")) { + $output = $node->getAttribute("title"); + } + } + + // if there is no link text, but a title attr + if (!$output && $node->getAttribute("title")) { + $output = $node->getAttribute("title"); + } + + if ($href == null) { + // it doesn't link anywhere + if ($node->getAttribute("name") != null) { + $output = "[$output]"; + } + } else { + if ($href == $output || $href == "mailto:$output" || $href == "http://$output" || $href == "https://$output") { + // link to the same address: just use link + $output; + } else { + // replace it + if ($output) { + $output = "[$output]($href)"; + } else { + // empty string + $output = $href; + } + } + } + + // does the next node require additional whitespace? + switch ($nextName) { + case "h1": case "h2": case "h3": case "h4": case "h5": case "h6": + $output .= "\n"; + break; + } + break; + + case "img": + if ($node->getAttribute("title")) { + $output = "[" . $node->getAttribute("title") . "]"; + } elseif ($node->getAttribute("alt")) { + $output = "[" . $node->getAttribute("alt") . "]"; + } else { + $output = ""; + } + break; + + case "li": + $output .= "\n"; + break; + + default: + // do nothing + } + + return $output; + } + +} diff --git a/application/Html2TextException.php b/application/Html2TextException.php new file mode 100644 index 000000000..ddfa86586 --- /dev/null +++ b/application/Html2TextException.php @@ -0,0 +1,28 @@ +more_info = $more_info; + } +} diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index f2178c641..16413344c 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -1825,7 +1825,7 @@ EOF $sPreviousLog = is_object($value) ? $value->GetAsHTML($oPage, true /* bEditMode */, array('AttributeText', 'RenderWikiHtml')) : ''; $iEntriesCount = is_object($value) ? count($value->GetIndex()) : 0; $sHidden = ""; // To know how many entries the case log already contains - $sHTMLValue = "
$sHeader$sPreviousLog{$sValidationField}
$sHidden
"; + $sHTMLValue = "
$sHeader$sPreviousLog{$sValidationField}
$sHidden
"; $oPage->add_ready_script("$('#$iId').bind('keyup change validate', function(evt, sFormId) { return ValidateCaseLogField('$iId', $bMandatory, sFormId) } );"); // Custom validation function break; @@ -3364,9 +3364,23 @@ EOF { $sHTMLValue = ''.$sComment.'
'; } - $sHTMLValue .= "".self::GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, $sValue, $sDisplayValue, $sInputId, '', $iFlags, $aArgs).''; + $sHTMLValue .= "".self::GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, $sValue, $sDisplayValue, $sInputId, '', $iFlags, $aArgs).''; $aFieldsMap[$sAttCode] = $sInputId; + + // Replace the text area with CKEditor + // To change the default settings of the editor, + // a) edit the file /js/ckeditor/config.js + // b) or override some of the configuration settings, using the second parameter of ckeditor() + $aConfig = array(); + $sLanguage = strtolower(trim(UserRights::GetUserLanguage())); + $aConfig['font_style'] = $sLanguage; + $aConfig['language'] = $sLanguage; + $aConfig['contentsLanguage'] = $sLanguage; + $aConfig['extraPlugins'] = 'disabler'; + $sConfigJS = json_encode($aConfig); + $oPage->add_ready_script("$('#$sInputId').ckeditor(function() { /* callback code */ }, $sConfigJS);"); // Transform $iId into a CKEdit + } //$aVal = array('label' => ''.$oAttDef->GetLabel().'', 'value' => $sHTMLValue, 'comments' => $sComments, 'infos' => $sInfos); $oPage->add('
'.$oAttDef->GetLabel().''); diff --git a/application/displayblock.class.inc.php b/application/displayblock.class.inc.php index aaf599a45..446d4eb5e 100644 --- a/application/displayblock.class.inc.php +++ b/application/displayblock.class.inc.php @@ -1,5 +1,5 @@ GetHistoryTable($oPage, $oSet); - } + } + $sMaxWidth = MetaModel::GetModuleSetting('itop-attachment', 'inline_image_max_width', '450px'); + $oPage->add_ready_script("$('.case-log-history-entry-toggle').on('click', function () { $(this).closest('.case-log-history-entry').toggleClass('expanded');});"); + $oPage->add_ready_script( +<<'); + jMe.find('.history_truncated_toggler').on('click', function() { + jMe.toggleClass('history_entry_truncated'); + }); + } +}); +EOF + ); } return $sHtml; } @@ -1319,7 +1345,7 @@ class HistoryBlock extends DisplayBlock } $aAttribs = array('date' => 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'), 'description' => Dict::S('UI:History:Changes+')), + 'log' => array('label' => Dict::S('UI:History:Changes').'Expand All / Collapse All', '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 8f4e80c18..f0b63bb7f 100644 --- a/application/itopwebpage.class.inc.php +++ b/application/itopwebpage.class.inc.php @@ -1,5 +1,5 @@ ul').popupmenu(); - $('.caselog_header').click( function () { $(this).toggleClass('open').next('.caselog_entry').toggle(); }); + $('.caselog_header').click( function () { $(this).toggleClass('open').next('.caselog_entry,.caselog_entry_html').toggle(); }); $(document).ajaxSend(function(event, jqxhr, options) { jqxhr.setRequestHeader('X-Combodo-Ajax', 'true'); diff --git a/application/utils.inc.php b/application/utils.inc.php index 03e2cea77..b91a80449 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -1,4 +1,5 @@ '.$sHtml; + return \Html2Text\Html2Text::convert(''.$sHtml); + } + catch(Exception $e) + { + return $e->getMessage(); + } + } + + /** + * Convert (?) plain text to some HTML markup by replacing newlines by
tags + * and escaping HTML entities + * @param string $sText + * @return string + */ + public static function TextToHtml($sText) + { + $sText = str_replace("\r\n", "\n", $sText); + $sText = str_replace("\r", "\n", $sText); + return str_replace("\n", '
', 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/action.class.inc.php b/core/action.class.inc.php index cc158e013..adf8898f7 100644 --- a/core/action.class.inc.php +++ b/core/action.class.inc.php @@ -1,5 +1,5 @@ null, "sql"=>"cc", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeOQL("bcc", array("allowed_values"=>null, "sql"=>"bcc", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeTemplateString("subject", array("allowed_values"=>null, "sql"=>"subject", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); - MetaModel::Init_AddAttribute(new AttributeTemplateText("body", array("allowed_values"=>null, "sql"=>"body", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); + MetaModel::Init_AddAttribute(new AttributeTemplateHTML("body", array("allowed_values"=>null, "sql"=>"body", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeEnum("importance", array("allowed_values"=>new ValueSetEnum('low,normal,high'), "sql"=>"importance", "default_value"=>'normal', "is_null_allowed"=>false, "depends_on"=>array()))); // Display lists diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index 529c11c11..0792b16fd 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -30,6 +30,7 @@ require_once('ormdocument.class.inc.php'); require_once('ormstopwatch.class.inc.php'); require_once('ormpassword.class.inc.php'); require_once('ormcaselog.class.inc.php'); +require_once('htmlsanitizer.class.inc.php'); /** * MissingColumnException - sent if an attribute is being created but the column is missing in the row @@ -436,6 +437,15 @@ abstract class AttributeDefinition { return (string)$sValue; } + + /** + * For fields containing a potential markup, return the value without this markup + * @return string + */ + public function GetAsPlainText($sValue, $oHostObj = null) + { + return (string) $this->GetEditValue($sValue, $oHostObj); + } /** * Helper to get a value that will be JSON encoded @@ -476,7 +486,7 @@ abstract class AttributeDefinition /** * Override to escape the value when read by DBObject::GetAsCSV() */ - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { return (string)$sValue; } @@ -499,6 +509,7 @@ abstract class AttributeDefinition '' => 'Plain text (unlocalized) representation', 'html' => 'HTML representation', 'label' => 'Localized representation', + 'text' => 'Plain text representation (without any markup)', ); } @@ -524,6 +535,10 @@ abstract class AttributeDefinition case 'label': return $this->GetEditValue($value); + case 'text': + return $this->GetAsPlainText($value); + break; + default: throw new Exception("Unknown verb '$sVerb' for attribute ".$this->GetCode().' in class '.get_class($oHostObj)); } @@ -780,7 +795,7 @@ class AttributeLinkedSet extends AttributeDefinition return $sRes; } - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { $sSepItem = MetaModel::GetConfig()->Get('link_set_item_separator'); $sSepAttribute = MetaModel::GetConfig()->Get('link_set_attribute_separator'); @@ -1676,7 +1691,7 @@ class AttributeBoolean extends AttributeInteger { return $sValue ? '1' : '0'; } - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { return $sValue ? '1' : '0'; } @@ -1806,7 +1821,7 @@ class AttributeString extends AttributeDBField return $value; } - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { $sFrom = array("\r\n", $sTextQualifier); $sTo = array("\n", $sTextQualifier.$sTextQualifier); @@ -1972,7 +1987,7 @@ class AttributeFinalClass extends AttributeString return $value; } - public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) + public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { if ($bLocalize && $value != '') { @@ -1982,7 +1997,7 @@ class AttributeFinalClass extends AttributeString { $sRawValue = $value; } - return parent::GetAsCSV($sRawValue, $sSeparator, $sTextQualifier, null, false); + return parent::GetAsCSV($sRawValue, $sSeparator, $sTextQualifier, null, false, $bConvertToPlainText); } public function GetAsXML($value, $oHostObject = null, $bLocalize = true) @@ -2162,9 +2177,43 @@ define('WIKI_OBJECT_REGEXP', '/\[\[(.+):(.+)\]\]/U'); */ class AttributeText extends AttributeString { - public function GetEditClass() {return "Text";} + public function GetEditClass() {return ($this->GetFormat() == 'text') ? 'Text' : "HTML";} + protected function GetSQLCol($bFullSpec = false) {return "TEXT";} + public function GetSQLColumns($bFullSpec = false) + { + $aColumns = array(); + $aColumns[$this->Get('sql')] = $this->GetSQLCol($bFullSpec); + if ($this->GetOptional('format', null) != null ) + { + // Add the extra column only if the property 'format' is specified for the attribute + $aColumns[$this->Get('sql').'_format'] = "ENUM('text','html')"; + if ($bFullSpec) + { + $aColumns[$this->Get('sql').'_format'].= " DEFAULT 'text'"; // default 'text' is for migrating old records + } + } + return $aColumns; + } + + public function GetSQLExpressions($sPrefix = '') + { + if ($sPrefix == '') + { + $sPrefix = $this->Get('sql'); + } + $aColumns = array(); + // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix + $aColumns[''] = $sPrefix; + if ($this->GetOptional('format', null) != null ) + { + // Add the extra column only if the property 'format' is specified for the attribute + $aColumns['_format'] = $sPrefix.'_format'; + } + return $aColumns; + } + public function GetMaxSize() { // Is there a way to know the current limitation for mysql? @@ -2224,8 +2273,6 @@ class AttributeText extends AttributeString public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) { - $sValue = parent::GetAsHTML($sValue, $oHostObject, $bLocalize); - $sValue = self::RenderWikiHtml($sValue); $aStyles = array(); if ($this->GetWidth() != '') { @@ -2241,44 +2288,83 @@ class AttributeText extends AttributeString $aStyles[] = 'overflow:auto'; $sStyle = 'style="'.implode(';', $aStyles).'"'; } - return "
".str_replace("\n", "
\n", $sValue).'
'; + + if ($this->GetFormat() == 'text') + { + $sValue = parent::GetAsHTML($sValue, $oHostObject, $bLocalize); + $sValue = self::RenderWikiHtml($sValue); + return "
".str_replace("\n", "
\n", $sValue).'
'; + } + else + { + return "
".utils::FixInlineAttachments($sValue).'
'; + } + } public function GetEditValue($sValue, $oHostObj = null) { - if (preg_match_all(WIKI_OBJECT_REGEXP, $sValue, $aAllMatches, PREG_SET_ORDER)) + if ($this->GetFormat() == 'text') { - foreach($aAllMatches as $iPos => $aMatches) + if (preg_match_all(WIKI_OBJECT_REGEXP, $sValue, $aAllMatches, PREG_SET_ORDER)) { - $sClass = $aMatches[1]; - $sName = $aMatches[2]; - - if (MetaModel::IsValidClass($sClass)) + foreach($aAllMatches as $iPos => $aMatches) { - $sClassLabel = MetaModel::GetName($sClass); - $sValue = str_replace($aMatches[0], "[[$sClassLabel:$sName]]", $sValue); + $sClass = $aMatches[1]; + $sName = $aMatches[2]; + + if (MetaModel::IsValidClass($sClass)) + { + $sClassLabel = MetaModel::GetName($sClass); + $sValue = str_replace($aMatches[0], "[[$sClassLabel:$sName]]", $sValue); + } } } } return $sValue; } + /** + * For fields containing a potential markup, return the value without this markup + * @return string + */ + public function GetAsPlainText($sValue, $oHostObj = null) + { + if ($this->GetFormat() == 'html') + { + return (string) utils::HtmlToText($this->GetEditValue($sValue, $oHostObj)); + } + else + { + return parent::GetAsPlainText($sValue, $oHostObj); + } + } + public function MakeRealValue($proposedValue, $oHostObj) { $sValue = $proposedValue; - if (preg_match_all(WIKI_OBJECT_REGEXP, $sValue, $aAllMatches, PREG_SET_ORDER)) + switch ($this->GetFormat()) { - foreach($aAllMatches as $iPos => $aMatches) + case 'html': + $sValue = HTMLSanitizer::Sanitize($sValue); + break; + + case 'text': + default: + if (preg_match_all(WIKI_OBJECT_REGEXP, $sValue, $aAllMatches, PREG_SET_ORDER)) { - $sClassLabel = $aMatches[1]; - $sName = $aMatches[2]; - - if (!MetaModel::IsValidClass($sClassLabel)) + foreach($aAllMatches as $iPos => $aMatches) { - $sClass = MetaModel::GetClassFromLabel($sClassLabel); - if ($sClass) + $sClassLabel = $aMatches[1]; + $sName = $aMatches[2]; + + if (!MetaModel::IsValidClass($sClassLabel)) { - $sValue = str_replace($aMatches[0], "[[$sClass:$sName]]", $sValue); + $sClass = MetaModel::GetClassFromLabel($sClassLabel); + if ($sClass) + { + $sValue = str_replace($aMatches[0], "[[$sClass:$sName]]", $sValue); + } } } } @@ -2300,6 +2386,92 @@ class AttributeText extends AttributeString { return $this->GetOptional('height', ''); } + + /** + * The actual formatting of the field: either text (=plain text) or html (= text with HTML markup) + * @return string + */ + public function GetFormat() + { + return $this->GetOptional('format', 'text'); + } + + /** + * Read the value from the row returned by the SQL query and transorms it to the appropriate + * internal format (either text or html) + * @see AttributeDBFieldVoid::FromSQLToValue() + */ + public function FromSQLToValue($aCols, $sPrefix = '') + { + $value = $aCols[$sPrefix.'']; + if ($this->GetOptional('format', null) != null ) + { + // Read from the extra column only if the property 'format' is specified for the attribute + $sFormat = $aCols[$sPrefix.'_format']; + } + else + { + $sFormat = $this->GetFormat(); + } + + switch($sFormat) + { + case 'text': + if ($this->GetFormat() == 'html') + { + $value = utils::TextToHtml($value); + } + break; + + case 'html': + if ($this->GetFormat() == 'text') + { + $value = utils::HtmlToText($value); + } + else + { + $value = utils::FixInlineAttachments((string)$value); + } + break; + + default: + // unknown format ?? + } + return $value; + } + + public function GetSQLValues($value) + { + $aValues = array(); + $aValues[$this->Get("sql")] = $this->ScalarToSQL($value); + if ($this->GetOptional('format', null) != null ) + { + // Add the extra column only if the property 'format' is specified for the attribute + $aValues[$this->Get("sql").'_format'] = $this->GetFormat(); + } + return $aValues; + } + + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) + { + switch($this->GetFormat()) + { + case 'html': + if ($bConvertToPlainText) + { + $sValue = utils::HtmlToText((string)$sValue); + } + $sFrom = array("\r\n", $sTextQualifier); + $sTo = array("\n", $sTextQualifier.$sTextQualifier); + $sEscaped = str_replace($sFrom, $sTo, (string)$sValue); + return $sTextQualifier.$sEscaped.$sTextQualifier; + break; + + case 'text': + default: + return parent::GetAsCSV($sValue, $sSeparator, $sTextQualifier, $oHostObject, $bLocalize, $bConvertToPlainText); + } + } } /** @@ -2358,7 +2530,25 @@ class AttributeCaseLog extends AttributeLongText } return $sValue->GetModifiedEntry(); } - + + /** + * For fields containing a potential markup, return the value without this markup + * @return string + */ + public function GetAsPlainText($value, $oHostObj = null) + { + $value = $oObj->Get($sAttCode); + if ($value instanceOf ormCaseLog) + { + + return $value->GetAsPlainText(); + } + else + { + return (string) $value; + } + } + public function GetDefaultValue() {return new ormCaseLog();} public function Equals($val1, $val2) {return ($val1->GetText() == $val2->GetText());} @@ -2402,7 +2592,7 @@ class AttributeCaseLog extends AttributeLongText { if (strlen($proposedValue) > 0) { - $oCaseLog->AddLogEntry(parent::MakeRealValue($proposedValue, $oHostObj)); + $oCaseLog->AddLogEntry($proposedValue); } } $ret = $oCaseLog; @@ -2414,7 +2604,7 @@ class AttributeCaseLog extends AttributeLongText { if ($sPrefix == '') { - $sPrefix = $this->GetCode(); + $sPrefix = $this->Get('sql'); } $aColumns = array(); // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix @@ -2502,11 +2692,11 @@ class AttributeCaseLog extends AttributeLongText return "
".$sContent.'
'; } - public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) + public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { if ($value instanceOf ormCaseLog) { - return parent::GetAsCSV($value->GetText(), $sSeparator, $sTextQualifier, $oHostObject, $bLocalize); + return parent::GetAsCSV($value->GetText($bConvertToPlainText), $sSeparator, $sTextQualifier, $oHostObject, $bLocalize, $bConvertToPlainText); } else { @@ -2615,6 +2805,15 @@ class AttributeCaseLog extends AttributeLongText } return $sFingerprint; } + + /** + * The actual formatting of the text: either text (=plain text) or html (= text with HTML markup) + * @return string + */ + public function GetFormat() + { + return $this->GetOptional('format', 'html'); // default format for case logs is now HTML + } } /** @@ -2624,11 +2823,29 @@ class AttributeCaseLog extends AttributeLongText */ class AttributeHTML extends AttributeLongText { - public function GetEditClass() {return "HTML";} - - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) + public function GetSQLColumns($bFullSpec = false) { - return $sValue; + $aColumns = array(); + $aColumns[$this->GetCode()] = $this->GetSQLCol(); + if ($this->GetOptional('format', null) != null ) + { + // Add the extra column only if the property 'format' is specified for the attribute + $aColumns[$this->Get('sql').'_format'] = "ENUM('text','html')"; + if ($bFullSpec) + { + $aColumns[$this->Get('sql').'_format'].= " DEFAULT 'html'"; // default 'html' is for migrating old records + } + } + return $aColumns; + } + + /** + * The actual formatting of the text: either text (=plain text) or html (= text with HTML markup) + * @return string + */ + public function GetFormat() + { + return $this->GetOptional('format', 'html'); // Defaults to HTML } } @@ -2706,11 +2923,29 @@ class AttributeTemplateText extends AttributeText */ class AttributeTemplateHTML extends AttributeText { - public function GetEditClass() {return "HTML";} - - public function GetAsHTML($sValue, $oHostObject = null, $bLocalize = true) + public function GetSQLColumns($bFullSpec = false) { - return $sValue; + $aColumns = array(); + $aColumns[$this->GetCode()] = $this->GetSQLCol(); + if ($this->GetOptional('format', null) != null ) + { + // Add the extra column only if the property 'format' is specified for the attribute + $aColumns[$this->Get('sql').'_format'] = "ENUM('text','html')"; + if ($bFullSpec) + { + $aColumns[$this->Get('sql').'_format'].= " DEFAULT 'html'"; // default 'html' is for migrating old records + } + } + return $aColumns; + } + + /** + * The actual formatting of the text: either text (=plain text) or html (= text with HTML markup) + * @return string + */ + public function GetFormat() + { + return $this->GetOptional('format', 'html'); // Defaults to HTML } } @@ -2885,7 +3120,7 @@ class AttributeEnum extends AttributeString return $sRes; } - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { if (is_null($sValue)) { @@ -3257,7 +3492,7 @@ class AttributeDateTime extends AttributeDBField return Str::pure2xml($value); } - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { $sFrom = array("\r\n", $sTextQualifier); $sTo = array("\n", $sTextQualifier.$sTextQualifier); @@ -3818,7 +4053,7 @@ class AttributeExternalField extends AttributeDefinition { if ($sPrefix == '') { - return array('' => $this->GetCode()); + return array('' => $this->GetCode()); // Warning: Use GetCode() since AttributeExternalField does not have any 'sql' property } else { @@ -4013,10 +4248,10 @@ class AttributeExternalField extends AttributeDefinition $oExtAttDef = $this->GetExtAttDef(); return $oExtAttDef->GetAsXML($value, null, $bLocalize); } - public function GetAsCSV($value, $sSeparator = ',', $sTestQualifier = '"', $oHostObject = null, $bLocalize = true) + public function GetAsCSV($value, $sSeparator = ',', $sTestQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { $oExtAttDef = $this->GetExtAttDef(); - return $oExtAttDef->GetAsCSV($value, $sSeparator, $sTestQualifier, null, $bLocalize); + return $oExtAttDef->GetAsCSV($value, $sSeparator, $sTestQualifier, null, $bLocalize, $bConvertToPlainText); } public function IsPartOfFingerprint() { return false; } @@ -4257,7 +4492,7 @@ class AttributeBlob extends AttributeDefinition } } - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { return ''; // Not exportable in CSV ! } @@ -4379,7 +4614,7 @@ class AttributeStopWatch extends AttributeDefinition { if ($sPrefix == '') { - $sPrefix = $this->GetCode(); + $sPrefix = $this->GetCode(); // Warning: a stopwatch does not have any 'sql' property, so its SQL column is equal to its attribute code !! } $aColumns = array(); // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix @@ -4551,7 +4786,7 @@ class AttributeStopWatch extends AttributeDefinition } } - public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) + public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { return $value->GetTimeSpent(); } @@ -4773,7 +5008,7 @@ class AttributeStopWatch extends AttributeDefinition return $sHtml; } - public function GetSubItemAsCSV($sItemCode, $value, $sSeparator = ',', $sTextQualifier = '"') + public function GetSubItemAsCSV($sItemCode, $value, $sSeparator = ',', $sTextQualifier = '"', $bConvertToPlainText = false) { $sFrom = array("\r\n", $sTextQualifier); $sTo = array("\n", $sTextQualifier.$sTextQualifier); @@ -5026,10 +5261,10 @@ class AttributeSubItem extends AttributeDefinition return $res; } - public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) + public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { $oParent = $this->GetTargetAttDef(); - $res = $oParent->GetSubItemAsCSV($this->Get('item_code'), $value, $sSeparator, $sTextQualifier); + $res = $oParent->GetSubItemAsCSV($this->Get('item_code'), $value, $sSeparator, $sTextQualifier, $bConvertToPlainText); return $res; } @@ -5087,7 +5322,7 @@ class AttributeOneWayPassword extends AttributeDefinition { if ($sPrefix == '') { - $sPrefix = $this->GetCode(); + $sPrefix = $this->GetCode(); // Warning: AttributeOneWayPassword does not have any sql property so code = sql ! } $aColumns = array(); // Note: to optimize things, the existence of the attribute is determined by the existence of one column with an empty suffix @@ -5196,7 +5431,7 @@ class AttributeOneWayPassword extends AttributeDefinition } } - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { return ''; // Not exportable in CSV } @@ -5300,7 +5535,7 @@ class AttributeTable extends AttributeDBField return $sRes; } - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { // Not implemented return ''; @@ -5372,7 +5607,7 @@ class AttributePropertySet extends AttributeTable return $sRes; } - public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) + public function GetAsCSV($value, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { if (count($value) == 0) { @@ -5451,7 +5686,7 @@ class AttributeComputedFieldVoid extends AttributeDefinition { if ($sPrefix == '') { - $sPrefix = $this->GetCode(); + $sPrefix = $this->GetCode(); // Warning AttributeComputedFieldVoid does not have any sql property } return array('' => $sPrefix); } @@ -5602,7 +5837,7 @@ class AttributeFriendlyName extends AttributeComputedFieldVoid return Str::pure2html((string)$sValue); } - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { $sFrom = array("\r\n", $sTextQualifier); $sTo = array("\n", $sTextQualifier.$sTextQualifier); @@ -5769,7 +6004,7 @@ class AttributeRedundancySettings extends AttributeDBField return sprintf($this->GetUserOptionFormat($sCurrentOption), $this->GetMinUpValue($sValue), MetaModel::GetName($sClass)); } - public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true) + public function GetAsCSV($sValue, $sSeparator = ',', $sTextQualifier = '"', $oHostObject = null, $bLocalize = true, $bConvertToPlainText = false) { $sFrom = array("\r\n", $sTextQualifier); $sTo = array("\n", $sTextQualifier.$sTextQualifier); diff --git a/core/cmdbchangeop.class.inc.php b/core/cmdbchangeop.class.inc.php index 31877e392..2937df71c 100644 --- a/core/cmdbchangeop.class.inc.php +++ b/core/cmdbchangeop.class.inc.php @@ -1,5 +1,5 @@ Get('objclass'); $oTargetObjectKey = $this->Get('objkey'); @@ -560,6 +557,66 @@ class CMDBChangeOpSetAttributeLongText extends CMDBChangeOpSetAttribute } } +/** + * Record the modification of a multiline string (text) containing some HTML markup + * + * @package iTopORM + */ +class CMDBChangeOpSetAttributeHTML extends CMDBChangeOpSetAttributeLongText +{ + public static function Init() + { + $aParams = array + ( + "category" => "core/cmdb", + "key_type" => "", + "name_attcode" => "change", + "state_attcode" => "", + "reconc_keys" => array(), + "db_table" => "priv_changeop_setatt_html", + "db_key_field" => "id", + "db_finalclass_field" => "", + ); + MetaModel::Init_Params($aParams); + MetaModel::Init_InheritAttributes(); + + // Display lists + MetaModel::Init_SetZListItems('details', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for the complete details + MetaModel::Init_SetZListItems('list', array('date', 'userinfo', 'attcode')); // Attributes to be displayed for a list + } + /** + * Describe (as a text string) the modifications corresponding to this change + */ + public function GetDescription() + { + $sResult = ''; + $oTargetObjectClass = $this->Get('objclass'); + $oTargetObjectKey = $this->Get('objkey'); + $oTargetSearch = new DBObjectSearch($oTargetObjectClass); + $oTargetSearch->AddCondition('id', $oTargetObjectKey, '='); + + $oMonoObjectSet = new DBObjectSet($oTargetSearch); + if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES) + { + if (MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) + { + $oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode')); + $sAttName = $oAttDef->GetLabel(); + } + else + { + // The attribute was renamed or removed from the object ? + $sAttName = $this->Get('attcode'); + } + $sTextView = '
'.$this->Get('prevdata').'
'; + + //$sDocView = $oPrevDoc->GetDisplayInline(get_class($this), $this->GetKey(), 'prevdata'); + $sResult = Dict::Format('Change:AttName_Changed_PreviousValue_OldValue', $sAttName, $sTextView); + } + return $sResult; + } +} + /** * Record the modification of a caselog (text) * since the caselog itself stores the history @@ -622,27 +679,8 @@ class CMDBChangeOpSetAttributeCaseLog extends CMDBChangeOpSetAttribute $oObj = $oMonoObjectSet->Fetch(); $oCaseLog = $oObj->Get($this->Get('attcode')); $iMaxVisibleLength = MetaModel::getConfig()->Get('max_history_case_log_entry_length', 0); - $sTextEntry = $oCaseLog->GetEntryAt($this->Get('lastentry')); - if (($iMaxVisibleLength > 0) && (strlen($sTextEntry) > $iMaxVisibleLength)) - { - if (function_exists('mb_strcut')) - { - // Safe with multi-byte strings - $sBefore = $this->ToHtml(mb_strcut($sTextEntry, 0, $iMaxVisibleLength, 'UTF-8')); - $sAfter = $this->ToHtml(mb_strcut($sTextEntry, $iMaxVisibleLength, null, 'UTF-8')); - } - else - { - // Let's hope we have no multi-byte characters around the cuttting point... - $sBefore = $this->ToHtml(substr($sTextEntry, 0, $iMaxVisibleLength)); - $sAfter = $this->ToHtml(substr($sTextEntry, $iMaxVisibleLength)); - } - $sTextEntry = ''.$sBefore.''.$sAfter.'...'; - } - else - { - $sTextEntry = $this->ToHtml($sTextEntry); - } + $sTextEntry = '
'.$oCaseLog->GetEntryAt($this->Get('lastentry')).'
'; + $sResult = Dict::Format('Change:AttName_EntryAdded', $sAttName, $sTextEntry); } return $sResult; diff --git a/core/cmdbobject.class.inc.php b/core/cmdbobject.class.inc.php index 981571fe8..bdae0c013 100644 --- a/core/cmdbobject.class.inc.php +++ b/core/cmdbobject.class.inc.php @@ -1,5 +1,5 @@ GetFormat() == 'html') + { + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeHTML"); + } + else + { + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeLongText"); + } $oMyChangeOp->Set("objclass", get_class($this)); $oMyChangeOp->Set("objkey", $this->GetKey()); $oMyChangeOp->Set("attcode", $sAttCode); @@ -352,7 +359,14 @@ abstract class CMDBObject extends DBObject elseif ($oAttDef instanceOf AttributeText) { // Data blobs - $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeText"); + if ($oAttDef->GetFormat() == 'html') + { + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeHTML"); + } + else + { + $oMyChangeOp = MetaModel::NewObject("CMDBChangeOpSetAttributeText"); + } $oMyChangeOp->Set("objclass", get_class($this)); $oMyChangeOp->Set("objkey", $this->GetKey()); $oMyChangeOp->Set("attcode", $sAttCode); diff --git a/core/config.class.inc.php b/core/config.class.inc.php index a742ebabf..0463c7e8f 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -1,5 +1,5 @@ '', 'show_in_conf_sample' => false, ), + 'html_sanitizer' => array( + 'type' => 'string', + 'description' => 'The class to use for HTML sanitization: HTMLDOMSanitizer, HTMLPurifierSanitizer or HTMLNullSanitizer', + 'default' => 'HTMLDOMSanitizer', + 'value' => '', + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), ); public function IsProperty($sPropCode) diff --git a/core/csvbulkexport.class.inc.php b/core/csvbulkexport.class.inc.php index c5031d909..c0461a50e 100644 --- a/core/csvbulkexport.class.inc.php +++ b/core/csvbulkexport.class.inc.php @@ -1,5 +1,5 @@ p(" *\tcharset: (optional) character set for encoding the result (default is 'UTF-8')."); $oP->p(" *\ttext-qualifier: (optional) character to be used around text strings (default is '\"')."); $oP->p(" *\tno_localize: set to 1 to retrieve non-localized values (for instance for ENUM values). Default is 0 (= localized values)"); + $oP->p(" *\tformatted_text: set to 1 to export case logs and formatted text fields with their HTML markup. Default is 0 (= plain text)"); } public function ReadParameters() @@ -55,6 +56,7 @@ class CSVBulkExport extends TabularBulkExport } $this->aStatusInfo['charset'] = strtoupper(utils::ReadParam('charset', 'UTF-8', true, 'raw_data')); + $this->aStatusInfo['formatted_text'] = (bool)utils::ReadParam('formatted_text', 0, true); } @@ -79,7 +81,7 @@ class CSVBulkExport extends TabularBulkExport public function EnumFormParts() { - return array_merge(parent::EnumFormParts(), array('csv_options' => array('separator', 'charset', 'text-qualifier', 'no_localize') ,'interactive_fields_csv' => array('interactive_fields_csv'))); + return array_merge(parent::EnumFormParts(), array('csv_options' => array('separator', 'charset', 'text-qualifier', 'no_localize', 'formatted_text') ,'interactive_fields_csv' => array('interactive_fields_csv'))); } public function DisplayFormPart(WebPage $oP, $sPartId) @@ -157,6 +159,10 @@ class CSVBulkExport extends TabularBulkExport } $oP->add(''); + $sChecked = (utils::ReadParam('formatted_text', 0) == 1) ? ' checked ' : ''; + $oP->add('

'.Dict::S('Core:BulkExport:TextFormat').'

'); + $oP->add(''); + $oP->add(''); $oP->add('
'); @@ -182,7 +188,7 @@ class CSVBulkExport extends TabularBulkExport break; default: - $sRet = trim($oObj->GetAsCSV($sAttCode), '"'); + $sRet = trim($oObj->GetAsCSV($sAttCode), '"'); } return $sRet; } @@ -251,7 +257,7 @@ class CSVBulkExport extends TabularBulkExport break; default: - $sField = $oObj->GetAsCSV($sAttCode, $this->aStatusInfo['separator'], $this->aStatusInfo['text_qualifier'], $this->bLocalizeOutput); + $sField = $oObj->GetAsCSV($sAttCode, $this->aStatusInfo['separator'], $this->aStatusInfo['text_qualifier'], $this->bLocalizeOutput, !$this->aStatusInfo['formatted_text']); } } if ($this->aStatusInfo['charset'] != 'UTF-8') diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 041f136b8..e7c41e281 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -754,10 +754,10 @@ abstract class DBObject implements iDisplay return $oAtt->GetAsXML($this->Get($sAttCode), $this, $bLocalize); } - public function GetAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true) + public function GetAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true, $bConvertToPlainText = false) { $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - return $oAtt->GetAsCSV($this->Get($sAttCode), $sSeparator, $sTextQualifier, $this, $bLocalize); + return $oAtt->GetAsCSV($this->Get($sAttCode), $sSeparator, $sTextQualifier, $this, $bLocalize, $bConvertToPlainText); } public function GetOriginalAsHTML($sAttCode, $bLocalize = true) @@ -772,10 +772,10 @@ abstract class DBObject implements iDisplay return $oAtt->GetAsXML($this->GetOriginal($sAttCode), $this, $bLocalize); } - public function GetOriginalAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true) + public function GetOriginalAsCSV($sAttCode, $sSeparator = ',', $sTextQualifier = '"', $bLocalize = true, $bConvertToPlainText = false) { $oAtt = MetaModel::GetAttributeDef(get_class($this), $sAttCode); - return $oAtt->GetAsCSV($this->GetOriginal($sAttCode), $sSeparator, $sTextQualifier, $this, $bLocalize); + return $oAtt->GetAsCSV($this->GetOriginal($sAttCode), $sSeparator, $sTextQualifier, $this, $bLocalize, $bConvertToPlainText); } public static function MakeHyperLink($sObjClass, $sObjKey, $sLabel = '', $sUrlMakerClass = null, $bWithNavigationContext = true) diff --git a/core/email.class.inc.php b/core/email.class.inc.php index 78acb6069..735de04fc 100644 --- a/core/email.class.inc.php +++ b/core/email.class.inc.php @@ -1,5 +1,5 @@ EmbedInlineImages(); + $this->LoadConfig(); $sTransport = self::$m_oConfig->Get('email_transport'); @@ -208,9 +211,46 @@ class EMail return EMAIL_SEND_OK; } } + + /** + * Reprocess the body of the message (if it is an HTML message) + * to replace the URL of images based on attachments by a link + * to an embedded image (i.e. cid:....) + */ + protected function EmbedInlineImages() + { + if ($this->m_aData['body']['mimeType'] == 'text/html') + { + $oDOMDoc = new DOMDocument(); + $oDOMDoc->preserveWhitespace = true; + @$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]"; + $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 */); + if ($oAttachment) + { + $oDoc = $oAttachment->Get('contents'); + $oSwiftImage = new Swift_Image($oDoc->GetData(), $oDoc->GetFileName(), $oDoc->GetMimeType()); + $sCid = $this->m_oMessage->embed($oSwiftImage); + $oImg->setAttribute('src', $sCid); + } + } + } + $sHtmlBody = $oDOMDoc->saveHTML(); + $this->m_oMessage->setBody($sHtmlBody, 'text/html', 'UTF-8'); + } + } public function Send(&$aIssues, $bForceSynchronous = false, $oLog = null) - { + { if ($bForceSynchronous) { return $this->SendSynchronous($aIssues, $oLog); diff --git a/core/excelbulkexport.class.inc.php b/core/excelbulkexport.class.inc.php index 223391bc3..0c852cd15 100644 --- a/core/excelbulkexport.class.inc.php +++ b/core/excelbulkexport.class.inc.php @@ -1,5 +1,5 @@ p(" * xlsx format options:"); $oP->p(" *\tfields: the comma separated list of field codes to export (e.g: name,org_id,service_name...)."); + $oP->p(" *\tformatted_text: set to 1 to export case logs and formatted text fields with their HTML markup. Default is 0 (= plain text)"); } - + public function ReadParameters() + { + parent::ReadParameters(); + $this->aStatusInfo['formatted_text'] = (bool)utils::ReadParam('formatted_text', 0, true); + } + public function EnumFormParts() { - return array_merge(parent::EnumFormParts(), array('interactive_fields_xlsx' => array('interactive_fields_xlsx'))); + return array_merge(parent::EnumFormParts(), array('xlsx_options' => array('formatted_text') ,'interactive_fields_xlsx' => array('interactive_fields_xlsx'))); } public function DisplayFormPart(WebPage $oP, $sPartId) @@ -62,7 +68,20 @@ class ExcelBulkExport extends TabularBulkExport $this->GetInteractiveFieldsWidget($oP, 'interactive_fields_xlsx'); break; - default: + case 'xlsx_options': + $oP->add('
'.Dict::S('Core:BulkExport:XLSXOptions').''); + $oP->add('
'); + + $sChecked = (utils::ReadParam('formatted_text', 0) == 1) ? ' checked ' : ''; + $oP->add('

'.Dict::S('Core:BulkExport:TextFormat').'

'); + $oP->add(''); + + $oP->add('
'); + + $oP->add('
'); + break; + + default: return parent:: DisplayFormPart($oP, $sPartId); } } @@ -103,8 +122,16 @@ class ExcelBulkExport extends TabularBulkExport $value = $oObj->Get($sAttCode); if ($value instanceOf ormCaseLog) { + if (array_key_exists('formatted_text', $this->aStatusInfo) && $this->aStatusInfo['formatted_text']) + { + $sText = $value->GetText(); + } + else + { + $sText = $value->GetAsPlainText(); + } // Extract the case log as text and remove the "===" which make Excel think that the cell contains a formula the next time you edit it! - $sRet = trim(preg_replace('/========== ([^=]+) ============/', '********** $1 ************', $value->GetText())); + $sRet = trim(preg_replace('/========== ([^=]+) ============/', '********** $1 ************', $sText)); } else if ($value instanceOf DBObjectSet) { @@ -114,7 +141,14 @@ class ExcelBulkExport extends TabularBulkExport else { $oAttDef = MetaModel::GetAttributeDef(get_class($oObj), $sAttCode); - $sRet = $oAttDef->GetEditValue($value, $oObj); + if (array_key_exists('formatted_text', $this->aStatusInfo) && $this->aStatusInfo['formatted_text']) + { + $sRet = $oAttDef->GetEditValue($value, $oObj); + } + else + { + $sRet = $oAttDef->GetAsPlainText($value, $oObj); + } } } return $sRet; diff --git a/core/htmlsanitizer.class.inc.php b/core/htmlsanitizer.class.inc.php new file mode 100644 index 000000000..4e0e85e3f --- /dev/null +++ b/core/htmlsanitizer.class.inc.php @@ -0,0 +1,338 @@ +Get('html_sanitizer'); + if(!class_exists($sSanitizerClass)) + { + IssueLog::Warning('The configured "html_sanitizer" class "'.$sSanitizerClass.'" is not a valid class. Will use HTMLDOMSanitizer as the default sanitizer.'); + $sSanitizerClass = 'HTMLDOMSanitizer'; + } + else if(!is_subclass_of($sSanitizerClass, 'HTMLSanitizer')) + { + IssueLog::Warning('The configured "html_sanitizer" class "'.$sSanitizerClass.'" is not a subclass of HTMLSanitizer. Will use HTMLDOMSanitizer as the default sanitizer.'); + $sSanitizerClass = 'HTMLDOMSanitizer'; + } + + try + { + $oSanitizer = new $sSanitizerClass(); + $sCleanHTML = $oSanitizer->DoSanitize($sHTML); + } + catch(Exception $e) + { + if($sSanitizerClass != 'HTMLDOMSanitizer') + { + IssueLog::Warning('Failed to sanitize an HTML string with "'.$sSanitizerClass.'". The following exception occured: '.$e->getMessage()); + IssueLog::Warning('Will try to sanitize with HTMLDOMSanitizer.'); + // try again with the HTMLDOMSanitizer + $oSanitizer = new HTMLDOMSanitizer(); + $sCleanHTML = $oSanitizer->DoSanitize($sHTML); + } + else + { + IssueLog::Error('Failed to sanitize an HTML string with "HTMLDOMSanitizer". The following exception occured: '.$e->getMessage()); + IssueLog::Error('The HTML will NOT be sanitized.'); + $sCleanHTML = $sHTML; + } + } + return $sCleanHTML; + } +} + +/** + * Dummy HTMLSanitizer which does nothing at all! + * Can be used if HTML Sanitization is not important + * (for example when importing "safe" data during an on-boarding) + * and performance is at stake + * + */ +class HTMLNullSanitizer extends HTMLSanitizer +{ + /** + * (non-PHPdoc) + * @see HTMLSanitizer::Sanitize() + */ + public function DoSanitize($sHTML) + { + return $sHTML; + } + +} + +/** + * A standard-compliant HTMLSanitizer based on the HTMLPurifier library by Edward Z. Yang + * Complete but quite slow + * http://htmlpurifier.org + */ +/* +class HTMLPurifierSanitizer extends HTMLSanitizer +{ + protected static $oPurifier = null; + + public function __construct() + { + if (self::$oPurifier == null) + { + $sLibPath = APPROOT.'lib/htmlpurifier/HTMLPurifier.auto.php'; + if (!file_exists($sLibPath)) + { + throw new Exception("Missing library '$sLibPath', cannot use HTMLPurifierSanitizer."); + } + require_once($sLibPath); + + $oPurifierConfig = HTMLPurifier_Config::createDefault(); + $oPurifierConfig->set('Core.Encoding', 'UTF-8'); // defaults to 'UTF-8' + $oPurifierConfig->set('HTML.Doctype', 'XHTML 1.0 Strict'); // defaults to 'XHTML 1.0 Transitional' + $oPurifierConfig->set('URI.AllowedSchemes', array ( + 'http' => true, + 'https' => true, + 'data' => true, // This one is not present by default + )); + $sPurifierCache = APPROOT.'data/HTMLPurifier'; + if (!is_dir($sPurifierCache)) + { + mkdir($sPurifierCache); + } + if (!is_dir($sPurifierCache)) + { + throw new Exception("Could not create the cache directory '$sPurifierCache'"); + } + $oPurifierConfig->set('Cache.SerializerPath', $sPurifierCache); // no trailing slash + self::$oPurifier = new HTMLPurifier($oPurifierConfig); + } + } + + public function DoSanitize($sHTML) + { + $sCleanHtml = self::$oPurifier->purify($sHTML); + return $sCleanHtml; + } +} +*/ + +class HTMLDOMSanitizer extends HTMLSanitizer +{ + protected $oDoc; + protected static $aTagsWhiteList = array( + 'html' => array(), + 'body' => array(), + 'a' => array('href', 'name', 'style'), + 'p' => array('style'), + 'br' => array(), + 'span' => array('style'), + 'div' => array('style'), + 'b' => array(), + 'i' => array(), + 'em' => array(), + 'strong' => array(), + 'img' => array('src','style'), + 'ul' => array('style'), + 'ol' => array('style'), + 'li' => array('style'), + 'h1' => array('style'), + 'h2' => array('style'), + 'h3' => array('style'), + 'h4' => array('style'), + 'nav' => array('style'), + 'section' => array('style'), + 'code' => array('style'), + 'table' => array('style', 'width'), + 'thead' => array('style'), + 'tbody' => array('style'), + 'tr' => array('style'), + 'td' => array('style', 'colspan'), + 'th' => array('style'), + 'fieldset' => array('style'), + 'legend' => array('style'), + 'font' => array('face', 'color', 'style', 'size'), + 'big' => array(), + 'small' => array(), + 'tt' => array(), + 'code' => array(), + 'kbd' => array(), + 'samp' => array(), + 'var' => array(), + 'del' => array(), + 's' => array(), // strikethrough + 'ins' => array(), + 'cite' => array(), + 'q' => array(), + 'hr' => array('style'), + 'pre' => array(), + 'center' => array(), + ); + + protected static $aAttrsWhiteList = array( + 'href' => '/^(http:|https:)/i', + 'src' => '/^(http:|https:|data:)/i', + ); + + protected static $aStylesWhiteList = array( + 'background-color', 'color', 'font', 'font-style', 'font-size', 'font-family', 'padding', 'margin', 'border', 'cellpadding', 'cellspacing', 'bordercolor', 'border-collapse', 'width', 'height', + ); + + public function DoSanitize($sHTML) + { + $this->oDoc = new DOMDocument(); + $this->oDoc->preserveWhitespace = true; + @$this->oDoc->loadHTML(''.$sHTML); // For loading HTML chunks where the character set is not specified + + $this->CleanNode($this->oDoc); + + $oXPath = new DOMXPath($this->oDoc); + $sXPath = "//body"; + $oNodesList = $oXPath->query($sXPath); + + if ($oNodesList->length == 0) + { + // No body, save the whole document + $sCleanHtml = $this->oDoc->saveHTML(); + } + else + { + // Export only the content of the body tag + $sCleanHtml = $this->oDoc->saveHTML($oNodesList->item(0)); + // remove the body tag itself + $sCleanHtml = str_replace( array('', ''), '', $sCleanHtml); + } + + return $sCleanHtml; + } + + protected function CleanNode(DOMNode $oElement) + { + $aAttrToRemove = array(); + // Gather the attributes to remove + if ($oElement->hasAttributes()) + { + foreach($oElement->attributes as $oAttr) + { + $sAttr = strtolower($oAttr->name); + if (!in_array($sAttr, self::$aTagsWhiteList[strtolower($oElement->tagName)])) + { + // Forbidden (or unknown) attribute + $aAttrToRemove[] = $oAttr->name; + } + else if (!$this->IsValidAttributeContent($sAttr, $oAttr->value)) + { + // Invalid content + $aAttrToRemove[] = $oAttr->name; + } + else if ($sAttr == 'style') + { + // Special processing for style tags + $sCleanStyle = $this->CleanStyle($oAttr->value); + if ($sCleanStyle == '') + { + // Invalid content + $aAttrToRemove[] = $oAttr->name; + } + else + { + $oElement->setAttribute($oAttr->name, $sCleanStyle); + } + } + } + // Now remove them + foreach($aAttrToRemove as $sName) + { + $oElement->removeAttribute($sName); + } + } + + if ($oElement->hasChildNodes()) + { + $aChildElementsToRemove = array(); + // Gather the child noes to remove + foreach($oElement->childNodes as $oNode) + { + if (($oNode instanceof DOMElement) && (!array_key_exists(strtolower($oNode->tagName), self::$aTagsWhiteList))) + { + $aChildElementsToRemove[] = $oNode; + } + else if ($oNode instanceof DOMComment) + { + $aChildElementsToRemove[] = $oNode; + } + else + { + // Recurse + $this->CleanNode($oNode); + if (($oNode instanceof DOMElement) && (strtolower($oNode->tagName) == 'img')) + { + $this->ProcessImage($oNode); + } + } + } + // Now remove them + foreach($aChildElementsToRemove as $oDomElement) + { + $oElement->removeChild($oDomElement); + } + } + } + + /** + * Add an extra attribute data-att-id for images which are based on an actual attachment + * 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 ? + $sUrlPattern = '|'.$sDownloadUrl.'([0-9]+)|'; + if (preg_match($sUrlPattern, $sSrc, $aMatches)) + { + $oElement->setAttribute('data-att-id', $aMatches[1]); + } + } + + protected function CleanStyle($sStyle) + { + $aAllowedStyles = array(); + $aItems = explode(';', $sStyle); + { + foreach($aItems as $sItem) + { + $aElements = explode(':', trim($sItem)); + if (in_array(trim(strtolower($aElements[0])), static::$aStylesWhiteList)) + { + $aAllowedStyles[] = trim($sItem); + } + } + } + return implode(';', $aAllowedStyles); + } + + protected function IsValidAttributeContent($sAttributeName, $sValue) + { + if (array_key_exists($sAttributeName, self::$aAttrsWhiteList)) + { + return preg_match(self::$aAttrsWhiteList[$sAttributeName], $sValue); + } + return true; + } +} \ No newline at end of file diff --git a/core/metamodel.class.php b/core/metamodel.class.php index 641bda9e8..4006fa0ec 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -3538,7 +3538,6 @@ abstract class MetaModel { // Skip this attribute if not originaly defined in this class if (self::$m_aAttribOrigins[$sClass][$sAttCode] != $sClass) continue; - foreach($oAttDef->GetSQLColumns(true) as $sField => $sDBFieldSpec) { // Keep track of columns used by iTop @@ -4893,9 +4892,9 @@ abstract class MetaModel { // Expand the parameters for the object $sName = substr($sSearch, 0, $iPos); - if (preg_match_all('/\\$'.$sName.'->([^\\$]+)\\$/', $sInput, $aMatches)) + if (preg_match_all('/\\$'.$sName.'-(>|>)([^\\$]+)\\$/', $sInput, $aMatches)) // Support both syntaxes: $this->xxx$ or $this->xxx$ for HTML compatibility { - foreach($aMatches[1] as $sPlaceholderAttCode) + foreach($aMatches[2] as $idx => $sPlaceholderAttCode) { try { @@ -4903,7 +4902,7 @@ abstract class MetaModel if ($sReplacement !== null) { $aReplacements[] = $sReplacement; - $aSearches[] = '$'.$sName.'->'.$sPlaceholderAttCode.'$'; + $aSearches[] = '$'.$sName.'-'.$aMatches[1][$idx].$sPlaceholderAttCode.'$'; } } catch(Exception $e) diff --git a/core/ormcaselog.class.inc.php b/core/ormcaselog.class.inc.php index 5ea44ea3d..b08003470 100644 --- a/core/ormcaselog.class.inc.php +++ b/core/ormcaselog.class.inc.php @@ -1,5 +1,5 @@ m_bModified = false; } - public function GetText() + public function GetText($bConvertToPlainText = false) { - return $this->m_sLog; + if ($bConvertToPlainText) + { + // Rebuild the log, but filtering any HTML markup for the all 'html' entries in the log + return $this->GetAsPlainText(); + } + else + { + return $this->m_sLog; + } } public static function FromJSON($oJson) @@ -97,11 +105,24 @@ class ormCaseLog { $sDate = ''; } } + $sFormat = array_key_exists('format', $this->m_aIndex[$index]) ? $this->m_aIndex[$index]['format'] : 'text'; + switch($sFormat) + { + case 'text': + $sHtmlEntry = utils::TextToHtml($sTextEntry); + break; + + case 'html': + $sHtmlEntry = $sTextEntry; + $sTextEntry = utils::HtmlToText($sHtmlEntry); + break; + } $aEntries[] = array( 'date' => $sDate, 'user_login' => $this->m_aIndex[$index]['user_name'], 'user_id' => $this->m_aIndex[$index]['user_id'], - 'message' => $sTextEntry + 'message' => $sTextEntry, + 'message_html' => $sHtmlEntry, ); } @@ -113,7 +134,8 @@ class ormCaseLog { $aEntries[] = array( 'date' => '', 'user_login' => '', - 'message' => $sTextEntry + 'message' => $sTextEntry, + 'message_html' => utils::TextToHtml($sTextEntry), ); } @@ -122,6 +144,22 @@ class ormCaseLog { return $aRet; } + /** + * Returns a "plain text" version of the log (equivalent to $this->m_sLog) where all the HTML markup from the 'html' entries have been removed + * @return string + */ + public function GetAsPlainText() + { + $sPlainText = ''; + $aJSON = $this->GetForJSON(); + foreach($aJSON['entries'] as $aData) + { + $sSeparator = sprintf(CASELOG_SEPARATOR, $aData['date'], $aData['user_login'], $aData['user_id']); + $sPlainText .= $sSeparator.$aData['message']; + } + return $sPlainText; + } + public function GetIndex() { return $this->m_aIndex; @@ -152,7 +190,16 @@ class ormCaseLog { { $iPos += $aIndex[$index]['separator_length']; $sTextEntry = substr($this->m_sLog, $iPos, $aIndex[$index]['text_length']); - $sTextEntry = str_replace(array("\r\n", "\n", "\r"), "
", htmlentities($sTextEntry, ENT_QUOTES, 'UTF-8')); + $sCSSClass = 'caselog_entry_html'; + if (!array_key_exists('format', $aIndex[$index]) || ($aIndex[$index]['format'] == 'text')) + { + $sCSSClass = 'caselog_entry'; + $sTextEntry = str_replace(array("\r\n", "\n", "\r"), "
", htmlentities($sTextEntry, ENT_QUOTES, 'UTF-8')); + } + else + { + $sTextEntry = utils::FixInlineAttachments($sTextEntry); + } $iPos += $aIndex[$index]['text_length']; $sEntry = '
'; @@ -180,7 +227,7 @@ class ormCaseLog { } $sEntry .= sprintf(Dict::S('UI:CaseLog:Header_Date_UserName'), ''.$sDate.'', ''.$aIndex[$index]['user_name'].''); $sEntry .= '
'; - $sEntry .= '
'; + $sEntry .= '
'; $sEntry .= $sTextEntry; $sEntry .= '
'; $sHtml = $sHtml.$sEntry; @@ -227,7 +274,20 @@ class ormCaseLog { { $iPos += $aIndex[$index]['separator_length']; $sTextEntry = substr($this->m_sLog, $iPos, $aIndex[$index]['text_length']); - $sTextEntry = str_replace(array("\r\n", "\n", "\r"), "
", htmlentities($sTextEntry, ENT_QUOTES, 'UTF-8')); + $sCSSClass = 'case_log_simple_html_entry_html'; + if (!array_key_exists('format', $aIndex[$index]) || ($aIndex[$index]['format'] == 'text')) + { + $sCSSClass = 'case_log_simple_html_entry'; + $sTextEntry = str_replace(array("\r\n", "\n", "\r"), "
", htmlentities($sTextEntry, ENT_QUOTES, 'UTF-8')); + if (!is_null($aTransfoHandler)) + { + $sTextEntry = call_user_func($aTransfoHandler, $sTextEntry); + } + } + else + { + $sTextEntry = utils::FixInlineAttachments($sTextEntry); + } $iPos += $aIndex[$index]['text_length']; $sEntry = '
  • '; @@ -254,7 +314,7 @@ class ormCaseLog { } } $sEntry .= sprintf(Dict::S('UI:CaseLog:Header_Date_UserName'), ''.$sDate.'', ''.$aIndex[$index]['user_name'].''); - $sEntry .= '
    '; + $sEntry .= '
    '; $sEntry .= $sTextEntry; $sEntry .= '
    '; $sEntry .= '
  • '; @@ -317,10 +377,19 @@ class ormCaseLog { } $iPos += $aIndex[$index]['separator_length']; $sTextEntry = substr($this->m_sLog, $iPos, $aIndex[$index]['text_length']); - $sTextEntry = str_replace(array("\r\n", "\n", "\r"), "
    ", htmlentities($sTextEntry, ENT_QUOTES, 'UTF-8')); - if (!is_null($aTransfoHandler)) + $sCSSClass= 'caselog_entry_html'; + if (!array_key_exists('format', $aIndex[$index]) || ($aIndex[$index]['format'] == 'text')) { - $sTextEntry = call_user_func($aTransfoHandler, $sTextEntry); + $sCSSClass= 'caselog_entry'; + $sTextEntry = str_replace(array("\r\n", "\n", "\r"), "
    ", htmlentities($sTextEntry, ENT_QUOTES, 'UTF-8')); + if (!is_null($aTransfoHandler)) + { + $sTextEntry = call_user_func($aTransfoHandler, $sTextEntry); + } + } + else + { + $sTextEntry = utils::FixInlineAttachments($sTextEntry); } $iPos += $aIndex[$index]['text_length']; @@ -349,7 +418,7 @@ class ormCaseLog { } $sEntry .= sprintf(Dict::S('UI:CaseLog:Header_Date_UserName'), $sDate, $aIndex[$index]['user_name']); $sEntry .= '
    '; - $sEntry .= '
    '; + $sEntry .= '
    '; $sEntry .= $sTextEntry; $sEntry .= '
    '; $sHtml = $sHtml.$sEntry; @@ -358,6 +427,7 @@ class ormCaseLog { // Process the case of an eventual remainder (quick migration of AttributeText fields) if ($iPos < (strlen($this->m_sLog) - 1)) { + // In this case the format is always "text" $sTextEntry = substr($this->m_sLog, $iPos); $sTextEntry = str_replace(array("\r\n", "\n", "\r"), "
    ", htmlentities($sTextEntry, ENT_QUOTES, 'UTF-8')); if (!is_null($aTransfoHandler)) @@ -402,6 +472,7 @@ class ormCaseLog { */ public function AddLogEntry($sText, $sOnBehalfOf = '') { + $sText = HTMLSanitizer::Sanitize($sText); $bMergeEntries = false; $sDate = date(Dict::S('UI:CaseLog:DateFormat')); if ($sOnBehalfOf == '') @@ -439,7 +510,8 @@ class ormCaseLog { 'user_id' => $iUserId, 'date' => time(), 'text_length' => $aLatestEntry['text_length'] + $iTextlength, - 'separator_length' => $iSepLength, + 'separator_length' => $iSepLength, + 'format' => 'html', ); } @@ -455,6 +527,7 @@ class ormCaseLog { 'date' => time(), 'text_length' => $iTextlength, 'separator_length' => $iSepLength, + 'format' => 'html', ); } $this->m_bModified = true; @@ -463,7 +536,7 @@ class ormCaseLog { public function AddLogEntryFromJSON($oJson, $bCheckUserId = true) { - $sText = isset($oJson->message) ? $oJson->message : ''; + $sText = HTMLSanitizer::Sanitize(isset($oJson->message) ? $oJson->message : ''); if (isset($oJson->user_id)) { @@ -505,6 +578,16 @@ class ormCaseLog { { $iDate = time(); } + if (isset($oJson->format)) + { + $sFormat = $oJson->format; + } + else + { + // TODO: what is the default format ? text ? + $sFormat = 'html'; + } + $sDate = date(Dict::S('UI:CaseLog:DateFormat'), $iDate); $sSeparator = sprintf(CASELOG_SEPARATOR, $sDate, $sOnBehalfOf, $iUserId); @@ -516,7 +599,8 @@ class ormCaseLog { 'user_id' => $iUserId, 'date' => $iDate, 'text_length' => $iTextlength, - 'separator_length' => $iSepLength, + 'separator_length' => $iSepLength, + 'format' => $sFormat, ); $this->m_bModified = true; diff --git a/css/light-grey.css b/css/light-grey.css index a70e8eff9..3084f10ac 100644 --- a/css/light-grey.css +++ b/css/light-grey.css @@ -280,16 +280,33 @@ legend.transparent { } -.ui-widget-content td a.cke_toolbox_collapser { +.ui-widget-content td a.cke_button, .ui-widget-content td a.cke_combo_button, .ui-widget-content td a.cke_toolbox_collapser, cke_dialog a { padding-left: 0; + background-image: none; } -p a:hover, td a:hover { +.ui-widget-content td a:hover, p a:hover, td a:hover { text-decoration: underline; color: #e87c1e; - padding-left: 14px; - background: url(../images/mini-arrow-orange.gif) no-repeat left; +} + + +.cke_reset_all *:hover { + text-decoration: none; + color: black; +} + + +table.cke_dialog_contents a.cke_dialog_ui_button_ok { + color: black; + border-color: #e87c1e; + background: #e87c1e; +} + + +.cke_notifications_area { + display: none; } @@ -2154,3 +2171,33 @@ span.refresh-button { } +.history_entry { + position: relative !important; + max-width: 100%; +} + + +.history_entry_truncated { + max-height: 7em; + overflow: hidden; +} + + +.history_truncated_toggler { + position: absolute !important; + bottom: 0; + right: 0; + display: block; + cursor: pointer; + width: 16px; + height: 16px; + background-image: url(ui-lightness/images/ui-icons_222222_256x240.png); + background-position: -16px -192px; +} + + +.history_entry_truncated .history_truncated_toggler { + background-position: 0 -192px; +} + + diff --git a/css/light-grey.scss b/css/light-grey.scss index 99dd8528b..3ccb0bbac 100644 --- a/css/light-grey.scss +++ b/css/light-grey.scss @@ -224,16 +224,27 @@ legend.transparent { padding-left:14px; background: url(../images/mini-arrow-orange.gif) no-repeat left; } -.ui-widget-content td a.cke_toolbox_collapser { +.ui-widget-content td a.cke_button, .ui-widget-content td a.cke_toolbox_collapser, .ui-widget-content td a.cke_combo_button, cke_dialog a { padding-left: 0; -} -p a:hover, td a:hover { - text-decoration:underline; - color:$highlight-color; - padding-left:14px; - background: url(../images/mini-arrow-orange.gif) no-repeat left; + background-image: none; } +.ui-widget-content td a:hover, p a:hover, td a:hover { + text-decoration:underline; + color:$highlight-color; +} +.cke_reset_all *:hover { + text-decoration: none; + color: #000; +} +table.cke_dialog_contents a.cke_dialog_ui_button_ok { + color: #000; + border-color: $highlight-color; + background: $highlight-color; +} +.cke_notifications_area { + display: none; +} td a.no-arrow, td a.no-arrow:visited, .SearchDrawer a.no-arrow, .SearchDrawer a.no-arrow:visited { text-decoration:none; color:#000000; @@ -1583,3 +1594,25 @@ span.refresh-button { .printable-tab .case-log-history-entry .case-log-history-entry-toggle { display: none; } +.history_entry { + position: relative !important; + max-width: 100%; +} +.history_entry_truncated { + max-height: 7em; + overflow: hidden; +} +.history_truncated_toggler { + position: absolute !important; + bottom: 0; + right: 0; + display: block; + cursor: pointer; + width: 16px; + height: 16px; + background-image: url(ui-lightness/images/ui-icons_222222_256x240.png); + background-position: -16px -192px; +} +.history_entry_truncated .history_truncated_toggler { + background-position: 0 -192px; +} \ No newline at end of file diff --git a/datamodels/1.x/itop-attachments/main.attachments.php b/datamodels/1.x/itop-attachments/main.attachments.php index 4c833568f..75442a217 100644 --- a/datamodels/1.x/itop-attachments/main.attachments.php +++ b/datamodels/1.x/itop-attachments/main.attachments.php @@ -1,5 +1,5 @@ add('

    '.$sFileName.'

      
    '); - $oPage->add_ready_script("$('#attachment_plugin').trigger('add_attachment', [$iAttId, '".addslashes($sFileName)."']);"); + $oPage->add_ready_script("$('#attachment_plugin').trigger('add_attachment', [$iAttId, '".addslashes($sFileName)."', false /* not an line image */]);"); } } } diff --git a/datamodels/2.x/itop-attachments/ajax.attachment.php b/datamodels/2.x/itop-attachments/ajax.attachment.php index 5f261f9a8..418edb790 100755 --- a/datamodels/2.x/itop-attachments/ajax.attachment.php +++ b/datamodels/2.x/itop-attachments/ajax.attachment.php @@ -1,5 +1,5 @@ 0, + 'fileName' => '', + 'url' => '', + 'icon' => '', + 'msg' => '', + 'att_id' => 0, + 'preview' => 'false', + ); + + $sObjClass = stripslashes(utils::ReadParam('obj_class', '', false, 'class')); + $sTempId = utils::ReadParam('temp_id', ''); + if (empty($sObjClass)) + { + $aResult['error'] = "Missing argument 'obj_class'"; + } + elseif (empty($sTempId)) + { + $aResult['error'] = "Missing argument 'temp_id'"; + } + else + { + try + { + $oDoc = utils::ReadPostedDocument('upload'); + $oAttachment = MetaModel::NewObject('Attachment'); + $oAttachment->Set('expire', time() + 3600); // one hour... + $oAttachment->Set('temp_id', $sTempId); + $oAttachment->Set('item_class', $sObjClass); + $oAttachment->SetDefaultOrgId(); + $oAttachment->Set('contents', $oDoc); + $iAttId = $oAttachment->DBInsert(); + + $aResult['uploaded'] = 1; + $aResult['msg'] = $oDoc->GetFileName(); + $aResult['fileName'] = $oDoc->GetFileName(); + $aResult['url'] = utils::GetAbsoluteUrlAppRoot().ATTACHMENT_DOWNLOAD_URL.$iAttId; + $aResult['icon'] = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($oDoc->GetFileName()); + $aResult['att_id'] = $iAttId; + $aResult['preview'] = $oDoc->IsPreviewAvailable() ? 'true' : 'false'; + } + catch (FileUploadException $e) + { + $aResult['error'] = $e->GetMessage(); + } + } + $oPage->add(json_encode($aResult)); + break; + + case 'cke_browse': + $oPage = new NiceWebPage('Browse for image...'); + $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'); + $sImgUrl = utils::GetAbsoluteUrlAppRoot().ATTACHMENT_DOWNLOAD_URL; + $oPage->add_script( +<< 1 ) ? match[1] : null; + } + // Simulate user action of selecting a file to be returned to CKEditor. + function returnFileUrl(iAttId, sAltText) { + + var funcNum = getUrlParam( 'CKEditorFuncNum' ); + var fileUrl = '$sImgUrl'+iAttId; + window.opener.CKEDITOR.tools.callFunction( funcNum, fileUrl, function() { + // Get the reference to a dialog window. + var dialog = this.getDialog(); + // Check if this is the Image Properties dialog window. + if ( dialog.getName() == 'image' ) { + // Get the reference to a text field that stores the "alt" attribute. + var element = dialog.getContentElement( 'info', 'txtAlt' ); + // Assign the new value. + if ( element ) + element.setValue(sAltText); + } + // Return "false" to stop further execution. In such case CKEditor will ignore the second argument ("fileUrl") + // and the "onSelect" function assigned to the button that called the file manager (if defined). + // return false; + } ); + window.close(); + } +EOF + ); + $oPage->add_ready_script( +<< $sTempId, 'obj_class' => $sClass, 'obj_id' => $iObjectId)); + while($oAttachment = $oSet->Fetch()) + { + $oDoc = $oAttachment->Get('contents'); + if ($oDoc->GetMainMimeType() == 'image') + { + $sDocName = addslashes(htmlentities($oDoc->GetFileName(), ENT_QUOTES, 'UTF-8')); + $iAttId = $oAttachment->GetKey(); + $oPage->add("
    \"$sDocName\"
    "); + } + } + break; + default: $oPage->p("Missing argument 'operation'"); } diff --git a/datamodels/2.x/itop-attachments/css/magnific-popup.css b/datamodels/2.x/itop-attachments/css/magnific-popup.css new file mode 100644 index 000000000..a530c65ae --- /dev/null +++ b/datamodels/2.x/itop-attachments/css/magnific-popup.css @@ -0,0 +1,374 @@ +/* Magnific Popup CSS */ +.mfp-bg { + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1042; + overflow: hidden; + position: fixed; + background: #0b0b0b; + opacity: 0.8; + filter: alpha(opacity=80); } + +.mfp-wrap { + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1043; + position: fixed; + outline: none !important; + -webkit-backface-visibility: hidden; } + +.mfp-container { + text-align: center; + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + padding: 0 8px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + +.mfp-container:before { + content: ''; + display: inline-block; + height: 100%; + vertical-align: middle; } + +.mfp-align-top .mfp-container:before { + display: none; } + +.mfp-content { + position: relative; + display: inline-block; + vertical-align: middle; + margin: 0 auto; + text-align: left; + z-index: 1045; } + +.mfp-inline-holder .mfp-content, .mfp-ajax-holder .mfp-content { + width: 100%; + cursor: auto; } + +.mfp-ajax-cur { + cursor: progress; } + +.mfp-zoom-out-cur, .mfp-zoom-out-cur .mfp-image-holder .mfp-close { + cursor: -moz-zoom-out; + cursor: -webkit-zoom-out; + cursor: zoom-out; } + +.mfp-zoom { + cursor: pointer; + cursor: -webkit-zoom-in; + cursor: -moz-zoom-in; + cursor: zoom-in; } + +.mfp-auto-cursor .mfp-content { + cursor: auto; } + +.mfp-close, .mfp-arrow, .mfp-preloader, .mfp-counter { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; } + +.mfp-loading.mfp-figure { + display: none; } + +.mfp-hide { + display: none !important; } + +.mfp-preloader { + color: #CCC; + position: absolute; + top: 50%; + width: auto; + text-align: center; + margin-top: -0.8em; + left: 8px; + right: 8px; + z-index: 1044; } + .mfp-preloader a { + color: #CCC; } + .mfp-preloader a:hover { + color: #FFF; } + +.mfp-s-ready .mfp-preloader { + display: none; } + +.mfp-s-error .mfp-content { + display: none; } + +button.mfp-close, button.mfp-arrow { + overflow: visible; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; + display: block; + outline: none; + padding: 0; + z-index: 1046; + -webkit-box-shadow: none; + box-shadow: none; } +button::-moz-focus-inner { + padding: 0; + border: 0; } + +.mfp-close { + width: 44px; + height: 44px; + line-height: 44px; + position: absolute; + right: 0; + top: 0; + text-decoration: none; + text-align: center; + opacity: 0.65; + filter: alpha(opacity=65); + padding: 0 0 18px 10px; + color: #FFF; + font-style: normal; + font-size: 28px; + font-family: Arial, Baskerville, monospace; } + .mfp-close:hover, .mfp-close:focus { + opacity: 1; + filter: alpha(opacity=100); } + .mfp-close:active { + top: 1px; } + +.mfp-close-btn-in .mfp-close { + color: #333; } + +.mfp-image-holder .mfp-close, .mfp-iframe-holder .mfp-close { + color: #FFF; + right: -6px; + text-align: right; + padding-right: 6px; + width: 100%; } + +.mfp-counter { + position: absolute; + top: 0; + right: 0; + color: #CCC; + font-size: 12px; + line-height: 18px; + white-space: nowrap; } + +.mfp-arrow { + position: absolute; + opacity: 0.65; + filter: alpha(opacity=65); + margin: 0; + top: 50%; + margin-top: -55px; + padding: 0; + width: 90px; + height: 110px; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } + .mfp-arrow:active { + margin-top: -54px; } + .mfp-arrow:hover, .mfp-arrow:focus { + opacity: 1; + filter: alpha(opacity=100); } + .mfp-arrow:before, .mfp-arrow:after, .mfp-arrow .mfp-b, .mfp-arrow .mfp-a { + content: ''; + display: block; + width: 0; + height: 0; + position: absolute; + left: 0; + top: 0; + margin-top: 35px; + margin-left: 35px; + border: medium inset transparent; } + .mfp-arrow:after, .mfp-arrow .mfp-a { + border-top-width: 13px; + border-bottom-width: 13px; + top: 8px; } + .mfp-arrow:before, .mfp-arrow .mfp-b { + border-top-width: 21px; + border-bottom-width: 21px; + opacity: 0.7; } + +.mfp-arrow-left { + left: 0; } + .mfp-arrow-left:after, .mfp-arrow-left .mfp-a { + border-right: 17px solid #FFF; + margin-left: 31px; } + .mfp-arrow-left:before, .mfp-arrow-left .mfp-b { + margin-left: 25px; + border-right: 27px solid #3F3F3F; } + +.mfp-arrow-right { + right: 0; } + .mfp-arrow-right:after, .mfp-arrow-right .mfp-a { + border-left: 17px solid #FFF; + margin-left: 39px; } + .mfp-arrow-right:before, .mfp-arrow-right .mfp-b { + border-left: 27px solid #3F3F3F; } + +.mfp-iframe-holder { + padding-top: 40px; + padding-bottom: 40px; } + .mfp-iframe-holder .mfp-content { + line-height: 0; + width: 100%; + max-width: 900px; } + .mfp-iframe-holder .mfp-close { + top: -40px; } + +.mfp-iframe-scaler { + width: 100%; + height: 0; + overflow: hidden; + padding-top: 56.25%; } + .mfp-iframe-scaler iframe { + position: absolute; + display: block; + top: 0; + left: 0; + width: 100%; + height: 100%; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.6); + background: #000; } + +/* Main image in popup */ +img.mfp-img { + width: auto; + max-width: 100%; + height: auto; + display: block; + line-height: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 40px 0 40px; + margin: 0 auto; } + +/* The shadow behind the image */ +.mfp-figure { + line-height: 0; } + .mfp-figure:after { + content: ''; + position: absolute; + left: 0; + top: 40px; + bottom: 40px; + display: block; + right: 0; + width: auto; + height: auto; + z-index: -1; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.6); + background: #444; } + .mfp-figure small { + color: #BDBDBD; + display: block; + font-size: 12px; + line-height: 14px; } + .mfp-figure figure { + margin: 0; } + +.mfp-bottom-bar { + margin-top: -36px; + position: absolute; + top: 100%; + left: 0; + width: 100%; + cursor: auto; } + +.mfp-title { + text-align: left; + line-height: 18px; + color: #F3F3F3; + word-wrap: break-word; + padding-right: 36px; } + +.mfp-image-holder .mfp-content { + max-width: 100%; } + +.mfp-gallery .mfp-image-holder .mfp-figure { + cursor: pointer; } + +@media screen and (max-width: 800px) and (orientation: landscape), screen and (max-height: 300px) { + /** + * Remove all paddings around the image on small screen + */ + .mfp-img-mobile .mfp-image-holder { + padding-left: 0; + padding-right: 0; } + .mfp-img-mobile img.mfp-img { + padding: 0; } + .mfp-img-mobile .mfp-figure:after { + top: 0; + bottom: 0; } + .mfp-img-mobile .mfp-figure small { + display: inline; + margin-left: 5px; } + .mfp-img-mobile .mfp-bottom-bar { + background: rgba(0, 0, 0, 0.6); + bottom: 0; + margin: 0; + top: auto; + padding: 3px 5px; + position: fixed; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } + .mfp-img-mobile .mfp-bottom-bar:empty { + padding: 0; } + .mfp-img-mobile .mfp-counter { + right: 5px; + top: 3px; } + .mfp-img-mobile .mfp-close { + top: 0; + right: 0; + width: 35px; + height: 35px; + line-height: 35px; + background: rgba(0, 0, 0, 0.6); + position: fixed; + text-align: center; + padding: 0; } + } + +@media all and (max-width: 900px) { + .mfp-arrow { + -webkit-transform: scale(0.75); + transform: scale(0.75); } + + .mfp-arrow-left { + -webkit-transform-origin: 0; + transform-origin: 0; } + + .mfp-arrow-right { + -webkit-transform-origin: 100%; + transform-origin: 100%; } + + .mfp-container { + padding-left: 6px; + padding-right: 6px; } + } + +.mfp-ie7 .mfp-img { + padding: 0; } +.mfp-ie7 .mfp-bottom-bar { + width: 600px; + left: 50%; + margin-left: -300px; + margin-top: 5px; + padding-bottom: 5px; } +.mfp-ie7 .mfp-container { + padding: 0; } +.mfp-ie7 .mfp-content { + padding-top: 44px; } +.mfp-ie7 .mfp-close { + top: 0; + right: 0; + padding-top: 0; } diff --git a/datamodels/2.x/itop-attachments/js/jquery.magnific-popup.js b/datamodels/2.x/itop-attachments/js/jquery.magnific-popup.js new file mode 100644 index 000000000..01f6f38d9 --- /dev/null +++ b/datamodels/2.x/itop-attachments/js/jquery.magnific-popup.js @@ -0,0 +1,2060 @@ +/*! Magnific Popup - v1.0.0 - 2015-01-03 +* http://dimsemenov.com/plugins/magnific-popup/ +* Copyright (c) 2015 Dmitry Semenov; */ +;(function (factory) { +if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS + factory(require('jquery')); + } else { + // Browser globals + factory(window.jQuery || window.Zepto); + } + }(function($) { + +/*>>core*/ +/** + * + * Magnific Popup Core JS file + * + */ + + +/** + * Private static constants + */ +var CLOSE_EVENT = 'Close', + BEFORE_CLOSE_EVENT = 'BeforeClose', + AFTER_CLOSE_EVENT = 'AfterClose', + BEFORE_APPEND_EVENT = 'BeforeAppend', + MARKUP_PARSE_EVENT = 'MarkupParse', + OPEN_EVENT = 'Open', + CHANGE_EVENT = 'Change', + NS = 'mfp', + EVENT_NS = '.' + NS, + READY_CLASS = 'mfp-ready', + REMOVING_CLASS = 'mfp-removing', + PREVENT_CLOSE_CLASS = 'mfp-prevent-close'; + + +/** + * Private vars + */ +/*jshint -W079 */ +var mfp, // As we have only one instance of MagnificPopup object, we define it locally to not to use 'this' + MagnificPopup = function(){}, + _isJQ = !!(window.jQuery), + _prevStatus, + _window = $(window), + _document, + _prevContentType, + _wrapClasses, + _currPopupType; + + +/** + * Private functions + */ +var _mfpOn = function(name, f) { + mfp.ev.on(NS + name + EVENT_NS, f); + }, + _getEl = function(className, appendTo, html, raw) { + var el = document.createElement('div'); + el.className = 'mfp-'+className; + if(html) { + el.innerHTML = html; + } + if(!raw) { + el = $(el); + if(appendTo) { + el.appendTo(appendTo); + } + } else if(appendTo) { + appendTo.appendChild(el); + } + return el; + }, + _mfpTrigger = function(e, data) { + mfp.ev.triggerHandler(NS + e, data); + + if(mfp.st.callbacks) { + // converts "mfpEventName" to "eventName" callback and triggers it if it's present + e = e.charAt(0).toLowerCase() + e.slice(1); + if(mfp.st.callbacks[e]) { + mfp.st.callbacks[e].apply(mfp, $.isArray(data) ? data : [data]); + } + } + }, + _getCloseBtn = function(type) { + if(type !== _currPopupType || !mfp.currTemplate.closeBtn) { + mfp.currTemplate.closeBtn = $( mfp.st.closeMarkup.replace('%title%', mfp.st.tClose ) ); + _currPopupType = type; + } + return mfp.currTemplate.closeBtn; + }, + // Initialize Magnific Popup only when called at least once + _checkInstance = function() { + if(!$.magnificPopup.instance) { + /*jshint -W020 */ + mfp = new MagnificPopup(); + mfp.init(); + $.magnificPopup.instance = mfp; + } + }, + // CSS transition detection, http://stackoverflow.com/questions/7264899/detect-css-transitions-using-javascript-and-without-modernizr + supportsTransitions = function() { + var s = document.createElement('p').style, // 's' for style. better to create an element if body yet to exist + v = ['ms','O','Moz','Webkit']; // 'v' for vendor + + if( s['transition'] !== undefined ) { + return true; + } + + while( v.length ) { + if( v.pop() + 'Transition' in s ) { + return true; + } + } + + return false; + }; + + + +/** + * Public functions + */ +MagnificPopup.prototype = { + + constructor: MagnificPopup, + + /** + * Initializes Magnific Popup plugin. + * This function is triggered only once when $.fn.magnificPopup or $.magnificPopup is executed + */ + init: function() { + var appVersion = navigator.appVersion; + mfp.isIE7 = appVersion.indexOf("MSIE 7.") !== -1; + mfp.isIE8 = appVersion.indexOf("MSIE 8.") !== -1; + mfp.isLowIE = mfp.isIE7 || mfp.isIE8; + mfp.isAndroid = (/android/gi).test(appVersion); + mfp.isIOS = (/iphone|ipad|ipod/gi).test(appVersion); + mfp.supportsTransition = supportsTransitions(); + + // We disable fixed positioned lightbox on devices that don't handle it nicely. + // If you know a better way of detecting this - let me know. + mfp.probablyMobile = (mfp.isAndroid || mfp.isIOS || /(Opera Mini)|Kindle|webOS|BlackBerry|(Opera Mobi)|(Windows Phone)|IEMobile/i.test(navigator.userAgent) ); + _document = $(document); + + mfp.popupsCache = {}; + }, + + /** + * Opens popup + * @param data [description] + */ + open: function(data) { + + var i; + + if(data.isObj === false) { + // convert jQuery collection to array to avoid conflicts later + mfp.items = data.items.toArray(); + + mfp.index = 0; + var items = data.items, + item; + for(i = 0; i < items.length; i++) { + item = items[i]; + if(item.parsed) { + item = item.el[0]; + } + if(item === data.el[0]) { + mfp.index = i; + break; + } + } + } else { + mfp.items = $.isArray(data.items) ? data.items : [data.items]; + mfp.index = data.index || 0; + } + + // if popup is already opened - we just update the content + if(mfp.isOpen) { + mfp.updateItemHTML(); + return; + } + + mfp.types = []; + _wrapClasses = ''; + if(data.mainEl && data.mainEl.length) { + mfp.ev = data.mainEl.eq(0); + } else { + mfp.ev = _document; + } + + if(data.key) { + if(!mfp.popupsCache[data.key]) { + mfp.popupsCache[data.key] = {}; + } + mfp.currTemplate = mfp.popupsCache[data.key]; + } else { + mfp.currTemplate = {}; + } + + + + mfp.st = $.extend(true, {}, $.magnificPopup.defaults, data ); + mfp.fixedContentPos = mfp.st.fixedContentPos === 'auto' ? !mfp.probablyMobile : mfp.st.fixedContentPos; + + if(mfp.st.modal) { + mfp.st.closeOnContentClick = false; + mfp.st.closeOnBgClick = false; + mfp.st.showCloseBtn = false; + mfp.st.enableEscapeKey = false; + } + + + // Building markup + // main containers are created only once + if(!mfp.bgOverlay) { + + // Dark overlay + mfp.bgOverlay = _getEl('bg').on('click'+EVENT_NS, function() { + mfp.close(); + }); + + mfp.wrap = _getEl('wrap').attr('tabindex', -1).on('click'+EVENT_NS, function(e) { + if(mfp._checkIfClose(e.target)) { + mfp.close(); + } + }); + + mfp.container = _getEl('container', mfp.wrap); + } + + mfp.contentContainer = _getEl('content'); + if(mfp.st.preloader) { + mfp.preloader = _getEl('preloader', mfp.container, mfp.st.tLoading); + } + + + // Initializing modules + var modules = $.magnificPopup.modules; + for(i = 0; i < modules.length; i++) { + var n = modules[i]; + n = n.charAt(0).toUpperCase() + n.slice(1); + mfp['init'+n].call(mfp); + } + _mfpTrigger('BeforeOpen'); + + + if(mfp.st.showCloseBtn) { + // Close button + if(!mfp.st.closeBtnInside) { + mfp.wrap.append( _getCloseBtn() ); + } else { + _mfpOn(MARKUP_PARSE_EVENT, function(e, template, values, item) { + values.close_replaceWith = _getCloseBtn(item.type); + }); + _wrapClasses += ' mfp-close-btn-in'; + } + } + + if(mfp.st.alignTop) { + _wrapClasses += ' mfp-align-top'; + } + + + + if(mfp.fixedContentPos) { + mfp.wrap.css({ + overflow: mfp.st.overflowY, + overflowX: 'hidden', + overflowY: mfp.st.overflowY + }); + } else { + mfp.wrap.css({ + top: _window.scrollTop(), + position: 'absolute' + }); + } + if( mfp.st.fixedBgPos === false || (mfp.st.fixedBgPos === 'auto' && !mfp.fixedContentPos) ) { + mfp.bgOverlay.css({ + height: _document.height(), + position: 'absolute' + }); + } + + + + if(mfp.st.enableEscapeKey) { + // Close on ESC key + _document.on('keyup' + EVENT_NS, function(e) { + if(e.keyCode === 27) { + mfp.close(); + } + }); + } + + _window.on('resize' + EVENT_NS, function() { + mfp.updateSize(); + }); + + + if(!mfp.st.closeOnContentClick) { + _wrapClasses += ' mfp-auto-cursor'; + } + + if(_wrapClasses) + mfp.wrap.addClass(_wrapClasses); + + + // this triggers recalculation of layout, so we get it once to not to trigger twice + var windowHeight = mfp.wH = _window.height(); + + + var windowStyles = {}; + + if( mfp.fixedContentPos ) { + if(mfp._hasScrollBar(windowHeight)){ + var s = mfp._getScrollbarSize(); + if(s) { + windowStyles.marginRight = s; + } + } + } + + if(mfp.fixedContentPos) { + if(!mfp.isIE7) { + windowStyles.overflow = 'hidden'; + } else { + // ie7 double-scroll bug + $('body, html').css('overflow', 'hidden'); + } + } + + + + var classesToadd = mfp.st.mainClass; + if(mfp.isIE7) { + classesToadd += ' mfp-ie7'; + } + if(classesToadd) { + mfp._addClassToMFP( classesToadd ); + } + + // add content + mfp.updateItemHTML(); + + _mfpTrigger('BuildControls'); + + // remove scrollbar, add margin e.t.c + $('html').css(windowStyles); + + // add everything to DOM + mfp.bgOverlay.add(mfp.wrap).prependTo( mfp.st.prependTo || $(document.body) ); + + // Save last focused element + mfp._lastFocusedEl = document.activeElement; + + // Wait for next cycle to allow CSS transition + setTimeout(function() { + + if(mfp.content) { + mfp._addClassToMFP(READY_CLASS); + mfp._setFocus(); + } else { + // if content is not defined (not loaded e.t.c) we add class only for BG + mfp.bgOverlay.addClass(READY_CLASS); + } + + // Trap the focus in popup + _document.on('focusin' + EVENT_NS, mfp._onFocusIn); + + }, 16); + + mfp.isOpen = true; + mfp.updateSize(windowHeight); + _mfpTrigger(OPEN_EVENT); + + return data; + }, + + /** + * Closes the popup + */ + close: function() { + if(!mfp.isOpen) return; + _mfpTrigger(BEFORE_CLOSE_EVENT); + + mfp.isOpen = false; + // for CSS3 animation + if(mfp.st.removalDelay && !mfp.isLowIE && mfp.supportsTransition ) { + mfp._addClassToMFP(REMOVING_CLASS); + setTimeout(function() { + mfp._close(); + }, mfp.st.removalDelay); + } else { + mfp._close(); + } + }, + + /** + * Helper for close() function + */ + _close: function() { + _mfpTrigger(CLOSE_EVENT); + + var classesToRemove = REMOVING_CLASS + ' ' + READY_CLASS + ' '; + + mfp.bgOverlay.detach(); + mfp.wrap.detach(); + mfp.container.empty(); + + if(mfp.st.mainClass) { + classesToRemove += mfp.st.mainClass + ' '; + } + + mfp._removeClassFromMFP(classesToRemove); + + if(mfp.fixedContentPos) { + var windowStyles = {marginRight: ''}; + if(mfp.isIE7) { + $('body, html').css('overflow', ''); + } else { + windowStyles.overflow = ''; + } + $('html').css(windowStyles); + } + + _document.off('keyup' + EVENT_NS + ' focusin' + EVENT_NS); + mfp.ev.off(EVENT_NS); + + // clean up DOM elements that aren't removed + mfp.wrap.attr('class', 'mfp-wrap').removeAttr('style'); + mfp.bgOverlay.attr('class', 'mfp-bg'); + mfp.container.attr('class', 'mfp-container'); + + // remove close button from target element + if(mfp.st.showCloseBtn && + (!mfp.st.closeBtnInside || mfp.currTemplate[mfp.currItem.type] === true)) { + if(mfp.currTemplate.closeBtn) + mfp.currTemplate.closeBtn.detach(); + } + + + if(mfp._lastFocusedEl) { + $(mfp._lastFocusedEl).focus(); // put tab focus back + } + mfp.currItem = null; + mfp.content = null; + mfp.currTemplate = null; + mfp.prevHeight = 0; + + _mfpTrigger(AFTER_CLOSE_EVENT); + }, + + updateSize: function(winHeight) { + + if(mfp.isIOS) { + // fixes iOS nav bars https://github.com/dimsemenov/Magnific-Popup/issues/2 + var zoomLevel = document.documentElement.clientWidth / window.innerWidth; + var height = window.innerHeight * zoomLevel; + mfp.wrap.css('height', height); + mfp.wH = height; + } else { + mfp.wH = winHeight || _window.height(); + } + // Fixes #84: popup incorrectly positioned with position:relative on body + if(!mfp.fixedContentPos) { + mfp.wrap.css('height', mfp.wH); + } + + _mfpTrigger('Resize'); + + }, + + /** + * Set content of popup based on current index + */ + updateItemHTML: function() { + var item = mfp.items[mfp.index]; + + // Detach and perform modifications + mfp.contentContainer.detach(); + + if(mfp.content) + mfp.content.detach(); + + if(!item.parsed) { + item = mfp.parseEl( mfp.index ); + } + + var type = item.type; + + _mfpTrigger('BeforeChange', [mfp.currItem ? mfp.currItem.type : '', type]); + // BeforeChange event works like so: + // _mfpOn('BeforeChange', function(e, prevType, newType) { }); + + mfp.currItem = item; + + + + + + if(!mfp.currTemplate[type]) { + var markup = mfp.st[type] ? mfp.st[type].markup : false; + + // allows to modify markup + _mfpTrigger('FirstMarkupParse', markup); + + if(markup) { + mfp.currTemplate[type] = $(markup); + } else { + // if there is no markup found we just define that template is parsed + mfp.currTemplate[type] = true; + } + } + + if(_prevContentType && _prevContentType !== item.type) { + mfp.container.removeClass('mfp-'+_prevContentType+'-holder'); + } + + var newContent = mfp['get' + type.charAt(0).toUpperCase() + type.slice(1)](item, mfp.currTemplate[type]); + mfp.appendContent(newContent, type); + + item.preloaded = true; + + _mfpTrigger(CHANGE_EVENT, item); + _prevContentType = item.type; + + // Append container back after its content changed + mfp.container.prepend(mfp.contentContainer); + + _mfpTrigger('AfterChange'); + }, + + + /** + * Set HTML content of popup + */ + appendContent: function(newContent, type) { + mfp.content = newContent; + + if(newContent) { + if(mfp.st.showCloseBtn && mfp.st.closeBtnInside && + mfp.currTemplate[type] === true) { + // if there is no markup, we just append close button element inside + if(!mfp.content.find('.mfp-close').length) { + mfp.content.append(_getCloseBtn()); + } + } else { + mfp.content = newContent; + } + } else { + mfp.content = ''; + } + + _mfpTrigger(BEFORE_APPEND_EVENT); + mfp.container.addClass('mfp-'+type+'-holder'); + + mfp.contentContainer.append(mfp.content); + }, + + + + + /** + * Creates Magnific Popup data object based on given data + * @param {int} index Index of item to parse + */ + parseEl: function(index) { + var item = mfp.items[index], + type; + + if(item.tagName) { + item = { el: $(item) }; + } else { + type = item.type; + item = { data: item, src: item.src }; + } + + if(item.el) { + var types = mfp.types; + + // check for 'mfp-TYPE' class + for(var i = 0; i < types.length; i++) { + if( item.el.hasClass('mfp-'+types[i]) ) { + type = types[i]; + break; + } + } + + item.src = item.el.attr('data-mfp-src'); + if(!item.src) { + item.src = item.el.attr('href'); + } + } + + item.type = type || mfp.st.type || 'inline'; + item.index = index; + item.parsed = true; + mfp.items[index] = item; + _mfpTrigger('ElementParse', item); + + return mfp.items[index]; + }, + + + /** + * Initializes single popup or a group of popups + */ + addGroup: function(el, options) { + var eHandler = function(e) { + e.mfpEl = this; + mfp._openClick(e, el, options); + }; + + if(!options) { + options = {}; + } + + var eName = 'click.magnificPopup'; + options.mainEl = el; + + if(options.items) { + options.isObj = true; + el.off(eName).on(eName, eHandler); + } else { + options.isObj = false; + if(options.delegate) { + el.off(eName).on(eName, options.delegate , eHandler); + } else { + options.items = el; + el.off(eName).on(eName, eHandler); + } + } + }, + _openClick: function(e, el, options) { + var midClick = options.midClick !== undefined ? options.midClick : $.magnificPopup.defaults.midClick; + + + if(!midClick && ( e.which === 2 || e.ctrlKey || e.metaKey ) ) { + return; + } + + var disableOn = options.disableOn !== undefined ? options.disableOn : $.magnificPopup.defaults.disableOn; + + if(disableOn) { + if($.isFunction(disableOn)) { + if( !disableOn.call(mfp) ) { + return true; + } + } else { // else it's number + if( _window.width() < disableOn ) { + return true; + } + } + } + + if(e.type) { + e.preventDefault(); + + // This will prevent popup from closing if element is inside and popup is already opened + if(mfp.isOpen) { + e.stopPropagation(); + } + } + + + options.el = $(e.mfpEl); + if(options.delegate) { + options.items = el.find(options.delegate); + } + mfp.open(options); + }, + + + /** + * Updates text on preloader + */ + updateStatus: function(status, text) { + + if(mfp.preloader) { + if(_prevStatus !== status) { + mfp.container.removeClass('mfp-s-'+_prevStatus); + } + + if(!text && status === 'loading') { + text = mfp.st.tLoading; + } + + var data = { + status: status, + text: text + }; + // allows to modify status + _mfpTrigger('UpdateStatus', data); + + status = data.status; + text = data.text; + + mfp.preloader.html(text); + + mfp.preloader.find('a').on('click', function(e) { + e.stopImmediatePropagation(); + }); + + mfp.container.addClass('mfp-s-'+status); + _prevStatus = status; + } + }, + + + /* + "Private" helpers that aren't private at all + */ + // Check to close popup or not + // "target" is an element that was clicked + _checkIfClose: function(target) { + + if($(target).hasClass(PREVENT_CLOSE_CLASS)) { + return; + } + + var closeOnContent = mfp.st.closeOnContentClick; + var closeOnBg = mfp.st.closeOnBgClick; + + if(closeOnContent && closeOnBg) { + return true; + } else { + + // We close the popup if click is on close button or on preloader. Or if there is no content. + if(!mfp.content || $(target).hasClass('mfp-close') || (mfp.preloader && target === mfp.preloader[0]) ) { + return true; + } + + // if click is outside the content + if( (target !== mfp.content[0] && !$.contains(mfp.content[0], target)) ) { + if(closeOnBg) { + // last check, if the clicked element is in DOM, (in case it's removed onclick) + if( $.contains(document, target) ) { + return true; + } + } + } else if(closeOnContent) { + return true; + } + + } + return false; + }, + _addClassToMFP: function(cName) { + mfp.bgOverlay.addClass(cName); + mfp.wrap.addClass(cName); + }, + _removeClassFromMFP: function(cName) { + this.bgOverlay.removeClass(cName); + mfp.wrap.removeClass(cName); + }, + _hasScrollBar: function(winHeight) { + return ( (mfp.isIE7 ? _document.height() : document.body.scrollHeight) > (winHeight || _window.height()) ); + }, + _setFocus: function() { + (mfp.st.focus ? mfp.content.find(mfp.st.focus).eq(0) : mfp.wrap).focus(); + }, + _onFocusIn: function(e) { + if( e.target !== mfp.wrap[0] && !$.contains(mfp.wrap[0], e.target) ) { + mfp._setFocus(); + return false; + } + }, + _parseMarkup: function(template, values, item) { + var arr; + if(item.data) { + values = $.extend(item.data, values); + } + _mfpTrigger(MARKUP_PARSE_EVENT, [template, values, item] ); + + $.each(values, function(key, value) { + if(value === undefined || value === false) { + return true; + } + arr = key.split('_'); + if(arr.length > 1) { + var el = template.find(EVENT_NS + '-'+arr[0]); + + if(el.length > 0) { + var attr = arr[1]; + if(attr === 'replaceWith') { + if(el[0] !== value[0]) { + el.replaceWith(value); + } + } else if(attr === 'img') { + if(el.is('img')) { + el.attr('src', value); + } else { + el.replaceWith( '' ); + } + } else { + el.attr(arr[1], value); + } + } + + } else { + template.find(EVENT_NS + '-'+key).html(value); + } + }); + }, + + _getScrollbarSize: function() { + // thx David + if(mfp.scrollbarSize === undefined) { + var scrollDiv = document.createElement("div"); + scrollDiv.style.cssText = 'width: 99px; height: 99px; overflow: scroll; position: absolute; top: -9999px;'; + document.body.appendChild(scrollDiv); + mfp.scrollbarSize = scrollDiv.offsetWidth - scrollDiv.clientWidth; + document.body.removeChild(scrollDiv); + } + return mfp.scrollbarSize; + } + +}; /* MagnificPopup core prototype end */ + + + + +/** + * Public static functions + */ +$.magnificPopup = { + instance: null, + proto: MagnificPopup.prototype, + modules: [], + + open: function(options, index) { + _checkInstance(); + + if(!options) { + options = {}; + } else { + options = $.extend(true, {}, options); + } + + + options.isObj = true; + options.index = index || 0; + return this.instance.open(options); + }, + + close: function() { + return $.magnificPopup.instance && $.magnificPopup.instance.close(); + }, + + registerModule: function(name, module) { + if(module.options) { + $.magnificPopup.defaults[name] = module.options; + } + $.extend(this.proto, module.proto); + this.modules.push(name); + }, + + defaults: { + + // Info about options is in docs: + // http://dimsemenov.com/plugins/magnific-popup/documentation.html#options + + disableOn: 0, + + key: null, + + midClick: false, + + mainClass: '', + + preloader: true, + + focus: '', // CSS selector of input to focus after popup is opened + + closeOnContentClick: false, + + closeOnBgClick: true, + + closeBtnInside: true, + + showCloseBtn: true, + + enableEscapeKey: true, + + modal: false, + + alignTop: false, + + removalDelay: 0, + + prependTo: null, + + fixedContentPos: 'auto', + + fixedBgPos: 'auto', + + overflowY: 'auto', + + closeMarkup: '', + + tClose: 'Close (Esc)', + + tLoading: 'Loading...' + + } +}; + + + +$.fn.magnificPopup = function(options) { + _checkInstance(); + + var jqEl = $(this); + + // We call some API method of first param is a string + if (typeof options === "string" ) { + + if(options === 'open') { + var items, + itemOpts = _isJQ ? jqEl.data('magnificPopup') : jqEl[0].magnificPopup, + index = parseInt(arguments[1], 10) || 0; + + if(itemOpts.items) { + items = itemOpts.items[index]; + } else { + items = jqEl; + if(itemOpts.delegate) { + items = items.find(itemOpts.delegate); + } + items = items.eq( index ); + } + mfp._openClick({mfpEl:items}, jqEl, itemOpts); + } else { + if(mfp.isOpen) + mfp[options].apply(mfp, Array.prototype.slice.call(arguments, 1)); + } + + } else { + // clone options obj + options = $.extend(true, {}, options); + + /* + * As Zepto doesn't support .data() method for objects + * and it works only in normal browsers + * we assign "options" object directly to the DOM element. FTW! + */ + if(_isJQ) { + jqEl.data('magnificPopup', options); + } else { + jqEl[0].magnificPopup = options; + } + + mfp.addGroup(jqEl, options); + + } + return jqEl; +}; + + +//Quick benchmark +/* +var start = performance.now(), + i, + rounds = 1000; + +for(i = 0; i < rounds; i++) { + +} +console.log('Test #1:', performance.now() - start); + +start = performance.now(); +for(i = 0; i < rounds; i++) { + +} +console.log('Test #2:', performance.now() - start); +*/ + + +/*>>core*/ + +/*>>inline*/ + +var INLINE_NS = 'inline', + _hiddenClass, + _inlinePlaceholder, + _lastInlineElement, + _putInlineElementsBack = function() { + if(_lastInlineElement) { + _inlinePlaceholder.after( _lastInlineElement.addClass(_hiddenClass) ).detach(); + _lastInlineElement = null; + } + }; + +$.magnificPopup.registerModule(INLINE_NS, { + options: { + hiddenClass: 'hide', // will be appended with `mfp-` prefix + markup: '', + tNotFound: 'Content not found' + }, + proto: { + + initInline: function() { + mfp.types.push(INLINE_NS); + + _mfpOn(CLOSE_EVENT+'.'+INLINE_NS, function() { + _putInlineElementsBack(); + }); + }, + + getInline: function(item, template) { + + _putInlineElementsBack(); + + if(item.src) { + var inlineSt = mfp.st.inline, + el = $(item.src); + + if(el.length) { + + // If target element has parent - we replace it with placeholder and put it back after popup is closed + var parent = el[0].parentNode; + if(parent && parent.tagName) { + if(!_inlinePlaceholder) { + _hiddenClass = inlineSt.hiddenClass; + _inlinePlaceholder = _getEl(_hiddenClass); + _hiddenClass = 'mfp-'+_hiddenClass; + } + // replace target inline element with placeholder + _lastInlineElement = el.after(_inlinePlaceholder).detach().removeClass(_hiddenClass); + } + + mfp.updateStatus('ready'); + } else { + mfp.updateStatus('error', inlineSt.tNotFound); + el = $('
    '); + } + + item.inlineElement = el; + return el; + } + + mfp.updateStatus('ready'); + mfp._parseMarkup(template, {}, item); + return template; + } + } +}); + +/*>>inline*/ + +/*>>ajax*/ +var AJAX_NS = 'ajax', + _ajaxCur, + _removeAjaxCursor = function() { + if(_ajaxCur) { + $(document.body).removeClass(_ajaxCur); + } + }, + _destroyAjaxRequest = function() { + _removeAjaxCursor(); + if(mfp.req) { + mfp.req.abort(); + } + }; + +$.magnificPopup.registerModule(AJAX_NS, { + + options: { + settings: null, + cursor: 'mfp-ajax-cur', + tError: 'The content could not be loaded.' + }, + + proto: { + initAjax: function() { + mfp.types.push(AJAX_NS); + _ajaxCur = mfp.st.ajax.cursor; + + _mfpOn(CLOSE_EVENT+'.'+AJAX_NS, _destroyAjaxRequest); + _mfpOn('BeforeChange.' + AJAX_NS, _destroyAjaxRequest); + }, + getAjax: function(item) { + + if(_ajaxCur) { + $(document.body).addClass(_ajaxCur); + } + + mfp.updateStatus('loading'); + + var opts = $.extend({ + url: item.src, + success: function(data, textStatus, jqXHR) { + var temp = { + data:data, + xhr:jqXHR + }; + + _mfpTrigger('ParseAjax', temp); + + mfp.appendContent( $(temp.data), AJAX_NS ); + + item.finished = true; + + _removeAjaxCursor(); + + mfp._setFocus(); + + setTimeout(function() { + mfp.wrap.addClass(READY_CLASS); + }, 16); + + mfp.updateStatus('ready'); + + _mfpTrigger('AjaxContentAdded'); + }, + error: function() { + _removeAjaxCursor(); + item.finished = item.loadError = true; + mfp.updateStatus('error', mfp.st.ajax.tError.replace('%url%', item.src)); + } + }, mfp.st.ajax.settings); + + mfp.req = $.ajax(opts); + + return ''; + } + } +}); + + + + + + + +/*>>ajax*/ + +/*>>image*/ +var _imgInterval, + _getTitle = function(item) { + if(item.data && item.data.title !== undefined) + return item.data.title; + + var src = mfp.st.image.titleSrc; + + if(src) { + if($.isFunction(src)) { + return src.call(mfp, item); + } else if(item.el) { + return item.el.attr(src) || ''; + } + } + return ''; + }; + +$.magnificPopup.registerModule('image', { + + options: { + markup: '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    ', + cursor: 'mfp-zoom-out-cur', + titleSrc: 'title', + verticalFit: true, + tError: 'The image could not be loaded.' + }, + + proto: { + initImage: function() { + var imgSt = mfp.st.image, + ns = '.image'; + + mfp.types.push('image'); + + _mfpOn(OPEN_EVENT+ns, function() { + if(mfp.currItem.type === 'image' && imgSt.cursor) { + $(document.body).addClass(imgSt.cursor); + } + }); + + _mfpOn(CLOSE_EVENT+ns, function() { + if(imgSt.cursor) { + $(document.body).removeClass(imgSt.cursor); + } + _window.off('resize' + EVENT_NS); + }); + + _mfpOn('Resize'+ns, mfp.resizeImage); + if(mfp.isLowIE) { + _mfpOn('AfterChange', mfp.resizeImage); + } + }, + resizeImage: function() { + var item = mfp.currItem; + if(!item || !item.img) return; + + if(mfp.st.image.verticalFit) { + var decr = 0; + // fix box-sizing in ie7/8 + if(mfp.isLowIE) { + decr = parseInt(item.img.css('padding-top'), 10) + parseInt(item.img.css('padding-bottom'),10); + } + item.img.css('max-height', mfp.wH-decr); + } + }, + _onImageHasSize: function(item) { + if(item.img) { + + item.hasSize = true; + + if(_imgInterval) { + clearInterval(_imgInterval); + } + + item.isCheckingImgSize = false; + + _mfpTrigger('ImageHasSize', item); + + if(item.imgHidden) { + if(mfp.content) + mfp.content.removeClass('mfp-loading'); + + item.imgHidden = false; + } + + } + }, + + /** + * Function that loops until the image has size to display elements that rely on it asap + */ + findImageSize: function(item) { + + var counter = 0, + img = item.img[0], + mfpSetInterval = function(delay) { + + if(_imgInterval) { + clearInterval(_imgInterval); + } + // decelerating interval that checks for size of an image + _imgInterval = setInterval(function() { + if(img.naturalWidth > 0) { + mfp._onImageHasSize(item); + return; + } + + if(counter > 200) { + clearInterval(_imgInterval); + } + + counter++; + if(counter === 3) { + mfpSetInterval(10); + } else if(counter === 40) { + mfpSetInterval(50); + } else if(counter === 100) { + mfpSetInterval(500); + } + }, delay); + }; + + mfpSetInterval(1); + }, + + getImage: function(item, template) { + + var guard = 0, + + // image load complete handler + onLoadComplete = function() { + if(item) { + if (item.img[0].complete) { + item.img.off('.mfploader'); + + if(item === mfp.currItem){ + mfp._onImageHasSize(item); + + mfp.updateStatus('ready'); + } + + item.hasSize = true; + item.loaded = true; + + _mfpTrigger('ImageLoadComplete'); + + } + else { + // if image complete check fails 200 times (20 sec), we assume that there was an error. + guard++; + if(guard < 200) { + setTimeout(onLoadComplete,100); + } else { + onLoadError(); + } + } + } + }, + + // image error handler + onLoadError = function() { + if(item) { + item.img.off('.mfploader'); + if(item === mfp.currItem){ + mfp._onImageHasSize(item); + mfp.updateStatus('error', imgSt.tError.replace('%url%', item.src) ); + } + + item.hasSize = true; + item.loaded = true; + item.loadError = true; + } + }, + imgSt = mfp.st.image; + + + var el = template.find('.mfp-img'); + if(el.length) { + var img = document.createElement('img'); + img.className = 'mfp-img'; + if(item.el && item.el.find('img').length) { + img.alt = item.el.find('img').attr('alt'); + } + item.img = $(img).on('load.mfploader', onLoadComplete).on('error.mfploader', onLoadError); + img.src = item.src; + + // without clone() "error" event is not firing when IMG is replaced by new IMG + // TODO: find a way to avoid such cloning + if(el.is('img')) { + item.img = item.img.clone(); + } + + img = item.img[0]; + if(img.naturalWidth > 0) { + item.hasSize = true; + } else if(!img.width) { + item.hasSize = false; + } + } + + mfp._parseMarkup(template, { + title: _getTitle(item), + img_replaceWith: item.img + }, item); + + mfp.resizeImage(); + + if(item.hasSize) { + if(_imgInterval) clearInterval(_imgInterval); + + if(item.loadError) { + template.addClass('mfp-loading'); + mfp.updateStatus('error', imgSt.tError.replace('%url%', item.src) ); + } else { + template.removeClass('mfp-loading'); + mfp.updateStatus('ready'); + } + return template; + } + + mfp.updateStatus('loading'); + item.loading = true; + + if(!item.hasSize) { + item.imgHidden = true; + template.addClass('mfp-loading'); + mfp.findImageSize(item); + } + + return template; + } + } +}); + + + +/*>>image*/ + +/*>>zoom*/ +var hasMozTransform, + getHasMozTransform = function() { + if(hasMozTransform === undefined) { + hasMozTransform = document.createElement('p').style.MozTransform !== undefined; + } + return hasMozTransform; + }; + +$.magnificPopup.registerModule('zoom', { + + options: { + enabled: false, + easing: 'ease-in-out', + duration: 300, + opener: function(element) { + return element.is('img') ? element : element.find('img'); + } + }, + + proto: { + + initZoom: function() { + var zoomSt = mfp.st.zoom, + ns = '.zoom', + image; + + if(!zoomSt.enabled || !mfp.supportsTransition) { + return; + } + + var duration = zoomSt.duration, + getElToAnimate = function(image) { + var newImg = image.clone().removeAttr('style').removeAttr('class').addClass('mfp-animated-image'), + transition = 'all '+(zoomSt.duration/1000)+'s ' + zoomSt.easing, + cssObj = { + position: 'fixed', + zIndex: 9999, + left: 0, + top: 0, + '-webkit-backface-visibility': 'hidden' + }, + t = 'transition'; + + cssObj['-webkit-'+t] = cssObj['-moz-'+t] = cssObj['-o-'+t] = cssObj[t] = transition; + + newImg.css(cssObj); + return newImg; + }, + showMainContent = function() { + mfp.content.css('visibility', 'visible'); + }, + openTimeout, + animatedImg; + + _mfpOn('BuildControls'+ns, function() { + if(mfp._allowZoom()) { + + clearTimeout(openTimeout); + mfp.content.css('visibility', 'hidden'); + + // Basically, all code below does is clones existing image, puts in on top of the current one and animated it + + image = mfp._getItemToZoom(); + + if(!image) { + showMainContent(); + return; + } + + animatedImg = getElToAnimate(image); + + animatedImg.css( mfp._getOffset() ); + + mfp.wrap.append(animatedImg); + + openTimeout = setTimeout(function() { + animatedImg.css( mfp._getOffset( true ) ); + openTimeout = setTimeout(function() { + + showMainContent(); + + setTimeout(function() { + animatedImg.remove(); + image = animatedImg = null; + _mfpTrigger('ZoomAnimationEnded'); + }, 16); // avoid blink when switching images + + }, duration); // this timeout equals animation duration + + }, 16); // by adding this timeout we avoid short glitch at the beginning of animation + + + // Lots of timeouts... + } + }); + _mfpOn(BEFORE_CLOSE_EVENT+ns, function() { + if(mfp._allowZoom()) { + + clearTimeout(openTimeout); + + mfp.st.removalDelay = duration; + + if(!image) { + image = mfp._getItemToZoom(); + if(!image) { + return; + } + animatedImg = getElToAnimate(image); + } + + + animatedImg.css( mfp._getOffset(true) ); + mfp.wrap.append(animatedImg); + mfp.content.css('visibility', 'hidden'); + + setTimeout(function() { + animatedImg.css( mfp._getOffset() ); + }, 16); + } + + }); + + _mfpOn(CLOSE_EVENT+ns, function() { + if(mfp._allowZoom()) { + showMainContent(); + if(animatedImg) { + animatedImg.remove(); + } + image = null; + } + }); + }, + + _allowZoom: function() { + return mfp.currItem.type === 'image'; + }, + + _getItemToZoom: function() { + if(mfp.currItem.hasSize) { + return mfp.currItem.img; + } else { + return false; + } + }, + + // Get element postion relative to viewport + _getOffset: function(isLarge) { + var el; + if(isLarge) { + el = mfp.currItem.img; + } else { + el = mfp.st.zoom.opener(mfp.currItem.el || mfp.currItem); + } + + var offset = el.offset(); + var paddingTop = parseInt(el.css('padding-top'),10); + var paddingBottom = parseInt(el.css('padding-bottom'),10); + offset.top -= ( $(window).scrollTop() - paddingTop ); + + + /* + + Animating left + top + width/height looks glitchy in Firefox, but perfect in Chrome. And vice-versa. + + */ + var obj = { + width: el.width(), + // fix Zepto height+padding issue + height: (_isJQ ? el.innerHeight() : el[0].offsetHeight) - paddingBottom - paddingTop + }; + + // I hate to do this, but there is no another option + if( getHasMozTransform() ) { + obj['-moz-transform'] = obj['transform'] = 'translate(' + offset.left + 'px,' + offset.top + 'px)'; + } else { + obj.left = offset.left; + obj.top = offset.top; + } + return obj; + } + + } +}); + + + +/*>>zoom*/ + +/*>>iframe*/ + +var IFRAME_NS = 'iframe', + _emptyPage = '//about:blank', + + _fixIframeBugs = function(isShowing) { + if(mfp.currTemplate[IFRAME_NS]) { + var el = mfp.currTemplate[IFRAME_NS].find('iframe'); + if(el.length) { + // reset src after the popup is closed to avoid "video keeps playing after popup is closed" bug + if(!isShowing) { + el[0].src = _emptyPage; + } + + // IE8 black screen bug fix + if(mfp.isIE8) { + el.css('display', isShowing ? 'block' : 'none'); + } + } + } + }; + +$.magnificPopup.registerModule(IFRAME_NS, { + + options: { + markup: '
    '+ + '
    '+ + ''+ + '
    ', + + srcAction: 'iframe_src', + + // we don't care and support only one default type of URL by default + patterns: { + youtube: { + index: 'youtube.com', + id: 'v=', + src: '//www.youtube.com/embed/%id%?autoplay=1' + }, + vimeo: { + index: 'vimeo.com/', + id: '/', + src: '//player.vimeo.com/video/%id%?autoplay=1' + }, + gmaps: { + index: '//maps.google.', + src: '%id%&output=embed' + } + } + }, + + proto: { + initIframe: function() { + mfp.types.push(IFRAME_NS); + + _mfpOn('BeforeChange', function(e, prevType, newType) { + if(prevType !== newType) { + if(prevType === IFRAME_NS) { + _fixIframeBugs(); // iframe if removed + } else if(newType === IFRAME_NS) { + _fixIframeBugs(true); // iframe is showing + } + }// else { + // iframe source is switched, don't do anything + //} + }); + + _mfpOn(CLOSE_EVENT + '.' + IFRAME_NS, function() { + _fixIframeBugs(); + }); + }, + + getIframe: function(item, template) { + var embedSrc = item.src; + var iframeSt = mfp.st.iframe; + + $.each(iframeSt.patterns, function() { + if(embedSrc.indexOf( this.index ) > -1) { + if(this.id) { + if(typeof this.id === 'string') { + embedSrc = embedSrc.substr(embedSrc.lastIndexOf(this.id)+this.id.length, embedSrc.length); + } else { + embedSrc = this.id.call( this, embedSrc ); + } + } + embedSrc = this.src.replace('%id%', embedSrc ); + return false; // break; + } + }); + + var dataObj = {}; + if(iframeSt.srcAction) { + dataObj[iframeSt.srcAction] = embedSrc; + } + mfp._parseMarkup(template, dataObj, item); + + mfp.updateStatus('ready'); + + return template; + } + } +}); + + + +/*>>iframe*/ + +/*>>gallery*/ +/** + * Get looped index depending on number of slides + */ +var _getLoopedId = function(index) { + var numSlides = mfp.items.length; + if(index > numSlides - 1) { + return index - numSlides; + } else if(index < 0) { + return numSlides + index; + } + return index; + }, + _replaceCurrTotal = function(text, curr, total) { + return text.replace(/%curr%/gi, curr + 1).replace(/%total%/gi, total); + }; + +$.magnificPopup.registerModule('gallery', { + + options: { + enabled: false, + arrowMarkup: '', + preload: [0,2], + navigateByImgClick: true, + arrows: true, + + tPrev: 'Previous (Left arrow key)', + tNext: 'Next (Right arrow key)', + tCounter: '%curr% of %total%' + }, + + proto: { + initGallery: function() { + + var gSt = mfp.st.gallery, + ns = '.mfp-gallery', + supportsFastClick = Boolean($.fn.mfpFastClick); + + mfp.direction = true; // true - next, false - prev + + if(!gSt || !gSt.enabled ) return false; + + _wrapClasses += ' mfp-gallery'; + + _mfpOn(OPEN_EVENT+ns, function() { + + if(gSt.navigateByImgClick) { + mfp.wrap.on('click'+ns, '.mfp-img', function() { + if(mfp.items.length > 1) { + mfp.next(); + return false; + } + }); + } + + _document.on('keydown'+ns, function(e) { + if (e.keyCode === 37) { + mfp.prev(); + } else if (e.keyCode === 39) { + mfp.next(); + } + }); + }); + + _mfpOn('UpdateStatus'+ns, function(e, data) { + if(data.text) { + data.text = _replaceCurrTotal(data.text, mfp.currItem.index, mfp.items.length); + } + }); + + _mfpOn(MARKUP_PARSE_EVENT+ns, function(e, element, values, item) { + var l = mfp.items.length; + values.counter = l > 1 ? _replaceCurrTotal(gSt.tCounter, item.index, l) : ''; + }); + + _mfpOn('BuildControls' + ns, function() { + if(mfp.items.length > 1 && gSt.arrows && !mfp.arrowLeft) { + var markup = gSt.arrowMarkup, + arrowLeft = mfp.arrowLeft = $( markup.replace(/%title%/gi, gSt.tPrev).replace(/%dir%/gi, 'left') ).addClass(PREVENT_CLOSE_CLASS), + arrowRight = mfp.arrowRight = $( markup.replace(/%title%/gi, gSt.tNext).replace(/%dir%/gi, 'right') ).addClass(PREVENT_CLOSE_CLASS); + + var eName = supportsFastClick ? 'mfpFastClick' : 'click'; + arrowLeft[eName](function() { + mfp.prev(); + }); + arrowRight[eName](function() { + mfp.next(); + }); + + // Polyfill for :before and :after (adds elements with classes mfp-a and mfp-b) + if(mfp.isIE7) { + _getEl('b', arrowLeft[0], false, true); + _getEl('a', arrowLeft[0], false, true); + _getEl('b', arrowRight[0], false, true); + _getEl('a', arrowRight[0], false, true); + } + + mfp.container.append(arrowLeft.add(arrowRight)); + } + }); + + _mfpOn(CHANGE_EVENT+ns, function() { + if(mfp._preloadTimeout) clearTimeout(mfp._preloadTimeout); + + mfp._preloadTimeout = setTimeout(function() { + mfp.preloadNearbyImages(); + mfp._preloadTimeout = null; + }, 16); + }); + + + _mfpOn(CLOSE_EVENT+ns, function() { + _document.off(ns); + mfp.wrap.off('click'+ns); + + if(mfp.arrowLeft && supportsFastClick) { + mfp.arrowLeft.add(mfp.arrowRight).destroyMfpFastClick(); + } + mfp.arrowRight = mfp.arrowLeft = null; + }); + + }, + next: function() { + mfp.direction = true; + mfp.index = _getLoopedId(mfp.index + 1); + mfp.updateItemHTML(); + }, + prev: function() { + mfp.direction = false; + mfp.index = _getLoopedId(mfp.index - 1); + mfp.updateItemHTML(); + }, + goTo: function(newIndex) { + mfp.direction = (newIndex >= mfp.index); + mfp.index = newIndex; + mfp.updateItemHTML(); + }, + preloadNearbyImages: function() { + var p = mfp.st.gallery.preload, + preloadBefore = Math.min(p[0], mfp.items.length), + preloadAfter = Math.min(p[1], mfp.items.length), + i; + + for(i = 1; i <= (mfp.direction ? preloadAfter : preloadBefore); i++) { + mfp._preloadItem(mfp.index+i); + } + for(i = 1; i <= (mfp.direction ? preloadBefore : preloadAfter); i++) { + mfp._preloadItem(mfp.index-i); + } + }, + _preloadItem: function(index) { + index = _getLoopedId(index); + + if(mfp.items[index].preloaded) { + return; + } + + var item = mfp.items[index]; + if(!item.parsed) { + item = mfp.parseEl( index ); + } + + _mfpTrigger('LazyLoad', item); + + if(item.type === 'image') { + item.img = $('').on('load.mfploader', function() { + item.hasSize = true; + }).on('error.mfploader', function() { + item.hasSize = true; + item.loadError = true; + _mfpTrigger('LazyLoadError', item); + }).attr('src', item.src); + } + + + item.preloaded = true; + } + } +}); + +/* +Touch Support that might be implemented some day + +addSwipeGesture: function() { + var startX, + moved, + multipleTouches; + + return; + + var namespace = '.mfp', + addEventNames = function(pref, down, move, up, cancel) { + mfp._tStart = pref + down + namespace; + mfp._tMove = pref + move + namespace; + mfp._tEnd = pref + up + namespace; + mfp._tCancel = pref + cancel + namespace; + }; + + if(window.navigator.msPointerEnabled) { + addEventNames('MSPointer', 'Down', 'Move', 'Up', 'Cancel'); + } else if('ontouchstart' in window) { + addEventNames('touch', 'start', 'move', 'end', 'cancel'); + } else { + return; + } + _window.on(mfp._tStart, function(e) { + var oE = e.originalEvent; + multipleTouches = moved = false; + startX = oE.pageX || oE.changedTouches[0].pageX; + }).on(mfp._tMove, function(e) { + if(e.originalEvent.touches.length > 1) { + multipleTouches = e.originalEvent.touches.length; + } else { + //e.preventDefault(); + moved = true; + } + }).on(mfp._tEnd + ' ' + mfp._tCancel, function(e) { + if(moved && !multipleTouches) { + var oE = e.originalEvent, + diff = startX - (oE.pageX || oE.changedTouches[0].pageX); + + if(diff > 20) { + mfp.next(); + } else if(diff < -20) { + mfp.prev(); + } + } + }); +}, +*/ + + +/*>>gallery*/ + +/*>>retina*/ + +var RETINA_NS = 'retina'; + +$.magnificPopup.registerModule(RETINA_NS, { + options: { + replaceSrc: function(item) { + return item.src.replace(/\.\w+$/, function(m) { return '@2x' + m; }); + }, + ratio: 1 // Function or number. Set to 1 to disable. + }, + proto: { + initRetina: function() { + if(window.devicePixelRatio > 1) { + + var st = mfp.st.retina, + ratio = st.ratio; + + ratio = !isNaN(ratio) ? ratio : ratio(); + + if(ratio > 1) { + _mfpOn('ImageHasSize' + '.' + RETINA_NS, function(e, item) { + item.img.css({ + 'max-width': item.img[0].naturalWidth / ratio, + 'width': '100%' + }); + }); + _mfpOn('ElementParse' + '.' + RETINA_NS, function(e, item) { + item.src = st.replaceSrc(item, ratio); + }); + } + } + + } + } +}); + +/*>>retina*/ + +/*>>fastclick*/ +/** + * FastClick event implementation. (removes 300ms delay on touch devices) + * Based on https://developers.google.com/mobile/articles/fast_buttons + * + * You may use it outside the Magnific Popup by calling just: + * + * $('.your-el').mfpFastClick(function() { + * console.log('Clicked!'); + * }); + * + * To unbind: + * $('.your-el').destroyMfpFastClick(); + * + * + * Note that it's a very basic and simple implementation, it blocks ghost click on the same element where it was bound. + * If you need something more advanced, use plugin by FT Labs https://github.com/ftlabs/fastclick + * + */ + +(function() { + var ghostClickDelay = 1000, + supportsTouch = 'ontouchstart' in window, + unbindTouchMove = function() { + _window.off('touchmove'+ns+' touchend'+ns); + }, + eName = 'mfpFastClick', + ns = '.'+eName; + + + // As Zepto.js doesn't have an easy way to add custom events (like jQuery), so we implement it in this way + $.fn.mfpFastClick = function(callback) { + + return $(this).each(function() { + + var elem = $(this), + lock; + + if( supportsTouch ) { + + var timeout, + startX, + startY, + pointerMoved, + point, + numPointers; + + elem.on('touchstart' + ns, function(e) { + pointerMoved = false; + numPointers = 1; + + point = e.originalEvent ? e.originalEvent.touches[0] : e.touches[0]; + startX = point.clientX; + startY = point.clientY; + + _window.on('touchmove'+ns, function(e) { + point = e.originalEvent ? e.originalEvent.touches : e.touches; + numPointers = point.length; + point = point[0]; + if (Math.abs(point.clientX - startX) > 10 || + Math.abs(point.clientY - startY) > 10) { + pointerMoved = true; + unbindTouchMove(); + } + }).on('touchend'+ns, function(e) { + unbindTouchMove(); + if(pointerMoved || numPointers > 1) { + return; + } + lock = true; + e.preventDefault(); + clearTimeout(timeout); + timeout = setTimeout(function() { + lock = false; + }, ghostClickDelay); + callback(); + }); + }); + + } + + elem.on('click' + ns, function() { + if(!lock) { + callback(); + } + }); + }); + }; + + $.fn.destroyMfpFastClick = function() { + $(this).off('touchstart' + ns + ' click' + ns); + if(supportsTouch) _window.off('touchmove'+ns+' touchend'+ns); + }; +})(); + +/*>>fastclick*/ + _checkInstance(); })); \ No newline at end of file diff --git a/datamodels/2.x/itop-attachments/js/jquery.magnific-popup.min.js b/datamodels/2.x/itop-attachments/js/jquery.magnific-popup.min.js new file mode 100644 index 000000000..ad353b97e --- /dev/null +++ b/datamodels/2.x/itop-attachments/js/jquery.magnific-popup.min.js @@ -0,0 +1,4 @@ +/*! Magnific Popup - v1.0.0 - 2015-01-03 +* http://dimsemenov.com/plugins/magnific-popup/ +* Copyright (c) 2015 Dmitry Semenov; */ +!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):window.jQuery||window.Zepto)}(function(a){var b,c,d,e,f,g,h="Close",i="BeforeClose",j="AfterClose",k="BeforeAppend",l="MarkupParse",m="Open",n="Change",o="mfp",p="."+o,q="mfp-ready",r="mfp-removing",s="mfp-prevent-close",t=function(){},u=!!window.jQuery,v=a(window),w=function(a,c){b.ev.on(o+a+p,c)},x=function(b,c,d,e){var f=document.createElement("div");return f.className="mfp-"+b,d&&(f.innerHTML=d),e?c&&c.appendChild(f):(f=a(f),c&&f.appendTo(c)),f},y=function(c,d){b.ev.triggerHandler(o+c,d),b.st.callbacks&&(c=c.charAt(0).toLowerCase()+c.slice(1),b.st.callbacks[c]&&b.st.callbacks[c].apply(b,a.isArray(d)?d:[d]))},z=function(c){return c===g&&b.currTemplate.closeBtn||(b.currTemplate.closeBtn=a(b.st.closeMarkup.replace("%title%",b.st.tClose)),g=c),b.currTemplate.closeBtn},A=function(){a.magnificPopup.instance||(b=new t,b.init(),a.magnificPopup.instance=b)},B=function(){var a=document.createElement("p").style,b=["ms","O","Moz","Webkit"];if(void 0!==a.transition)return!0;for(;b.length;)if(b.pop()+"Transition"in a)return!0;return!1};t.prototype={constructor:t,init:function(){var c=navigator.appVersion;b.isIE7=-1!==c.indexOf("MSIE 7."),b.isIE8=-1!==c.indexOf("MSIE 8."),b.isLowIE=b.isIE7||b.isIE8,b.isAndroid=/android/gi.test(c),b.isIOS=/iphone|ipad|ipod/gi.test(c),b.supportsTransition=B(),b.probablyMobile=b.isAndroid||b.isIOS||/(Opera Mini)|Kindle|webOS|BlackBerry|(Opera Mobi)|(Windows Phone)|IEMobile/i.test(navigator.userAgent),d=a(document),b.popupsCache={}},open:function(c){var e;if(c.isObj===!1){b.items=c.items.toArray(),b.index=0;var g,h=c.items;for(e=0;e(a||v.height())},_setFocus:function(){(b.st.focus?b.content.find(b.st.focus).eq(0):b.wrap).focus()},_onFocusIn:function(c){return c.target===b.wrap[0]||a.contains(b.wrap[0],c.target)?void 0:(b._setFocus(),!1)},_parseMarkup:function(b,c,d){var e;d.data&&(c=a.extend(d.data,c)),y(l,[b,c,d]),a.each(c,function(a,c){if(void 0===c||c===!1)return!0;if(e=a.split("_"),e.length>1){var d=b.find(p+"-"+e[0]);if(d.length>0){var f=e[1];"replaceWith"===f?d[0]!==c[0]&&d.replaceWith(c):"img"===f?d.is("img")?d.attr("src",c):d.replaceWith(''):d.attr(e[1],c)}}else b.find(p+"-"+a).html(c)})},_getScrollbarSize:function(){if(void 0===b.scrollbarSize){var a=document.createElement("div");a.style.cssText="width: 99px; height: 99px; overflow: scroll; position: absolute; top: -9999px;",document.body.appendChild(a),b.scrollbarSize=a.offsetWidth-a.clientWidth,document.body.removeChild(a)}return b.scrollbarSize}},a.magnificPopup={instance:null,proto:t.prototype,modules:[],open:function(b,c){return A(),b=b?a.extend(!0,{},b):{},b.isObj=!0,b.index=c||0,this.instance.open(b)},close:function(){return a.magnificPopup.instance&&a.magnificPopup.instance.close()},registerModule:function(b,c){c.options&&(a.magnificPopup.defaults[b]=c.options),a.extend(this.proto,c.proto),this.modules.push(b)},defaults:{disableOn:0,key:null,midClick:!1,mainClass:"",preloader:!0,focus:"",closeOnContentClick:!1,closeOnBgClick:!0,closeBtnInside:!0,showCloseBtn:!0,enableEscapeKey:!0,modal:!1,alignTop:!1,removalDelay:0,prependTo:null,fixedContentPos:"auto",fixedBgPos:"auto",overflowY:"auto",closeMarkup:'',tClose:"Close (Esc)",tLoading:"Loading..."}},a.fn.magnificPopup=function(c){A();var d=a(this);if("string"==typeof c)if("open"===c){var e,f=u?d.data("magnificPopup"):d[0].magnificPopup,g=parseInt(arguments[1],10)||0;f.items?e=f.items[g]:(e=d,f.delegate&&(e=e.find(f.delegate)),e=e.eq(g)),b._openClick({mfpEl:e},d,f)}else b.isOpen&&b[c].apply(b,Array.prototype.slice.call(arguments,1));else c=a.extend(!0,{},c),u?d.data("magnificPopup",c):d[0].magnificPopup=c,b.addGroup(d,c);return d};var C,D,E,F="inline",G=function(){E&&(D.after(E.addClass(C)).detach(),E=null)};a.magnificPopup.registerModule(F,{options:{hiddenClass:"hide",markup:"",tNotFound:"Content not found"},proto:{initInline:function(){b.types.push(F),w(h+"."+F,function(){G()})},getInline:function(c,d){if(G(),c.src){var e=b.st.inline,f=a(c.src);if(f.length){var g=f[0].parentNode;g&&g.tagName&&(D||(C=e.hiddenClass,D=x(C),C="mfp-"+C),E=f.after(D).detach().removeClass(C)),b.updateStatus("ready")}else b.updateStatus("error",e.tNotFound),f=a("
    ");return c.inlineElement=f,f}return b.updateStatus("ready"),b._parseMarkup(d,{},c),d}}});var H,I="ajax",J=function(){H&&a(document.body).removeClass(H)},K=function(){J(),b.req&&b.req.abort()};a.magnificPopup.registerModule(I,{options:{settings:null,cursor:"mfp-ajax-cur",tError:'The content could not be loaded.'},proto:{initAjax:function(){b.types.push(I),H=b.st.ajax.cursor,w(h+"."+I,K),w("BeforeChange."+I,K)},getAjax:function(c){H&&a(document.body).addClass(H),b.updateStatus("loading");var d=a.extend({url:c.src,success:function(d,e,f){var g={data:d,xhr:f};y("ParseAjax",g),b.appendContent(a(g.data),I),c.finished=!0,J(),b._setFocus(),setTimeout(function(){b.wrap.addClass(q)},16),b.updateStatus("ready"),y("AjaxContentAdded")},error:function(){J(),c.finished=c.loadError=!0,b.updateStatus("error",b.st.ajax.tError.replace("%url%",c.src))}},b.st.ajax.settings);return b.req=a.ajax(d),""}}});var L,M=function(c){if(c.data&&void 0!==c.data.title)return c.data.title;var d=b.st.image.titleSrc;if(d){if(a.isFunction(d))return d.call(b,c);if(c.el)return c.el.attr(d)||""}return""};a.magnificPopup.registerModule("image",{options:{markup:'
    ',cursor:"mfp-zoom-out-cur",titleSrc:"title",verticalFit:!0,tError:'The image could not be loaded.'},proto:{initImage:function(){var c=b.st.image,d=".image";b.types.push("image"),w(m+d,function(){"image"===b.currItem.type&&c.cursor&&a(document.body).addClass(c.cursor)}),w(h+d,function(){c.cursor&&a(document.body).removeClass(c.cursor),v.off("resize"+p)}),w("Resize"+d,b.resizeImage),b.isLowIE&&w("AfterChange",b.resizeImage)},resizeImage:function(){var a=b.currItem;if(a&&a.img&&b.st.image.verticalFit){var c=0;b.isLowIE&&(c=parseInt(a.img.css("padding-top"),10)+parseInt(a.img.css("padding-bottom"),10)),a.img.css("max-height",b.wH-c)}},_onImageHasSize:function(a){a.img&&(a.hasSize=!0,L&&clearInterval(L),a.isCheckingImgSize=!1,y("ImageHasSize",a),a.imgHidden&&(b.content&&b.content.removeClass("mfp-loading"),a.imgHidden=!1))},findImageSize:function(a){var c=0,d=a.img[0],e=function(f){L&&clearInterval(L),L=setInterval(function(){return d.naturalWidth>0?void b._onImageHasSize(a):(c>200&&clearInterval(L),c++,void(3===c?e(10):40===c?e(50):100===c&&e(500)))},f)};e(1)},getImage:function(c,d){var e=0,f=function(){c&&(c.img[0].complete?(c.img.off(".mfploader"),c===b.currItem&&(b._onImageHasSize(c),b.updateStatus("ready")),c.hasSize=!0,c.loaded=!0,y("ImageLoadComplete")):(e++,200>e?setTimeout(f,100):g()))},g=function(){c&&(c.img.off(".mfploader"),c===b.currItem&&(b._onImageHasSize(c),b.updateStatus("error",h.tError.replace("%url%",c.src))),c.hasSize=!0,c.loaded=!0,c.loadError=!0)},h=b.st.image,i=d.find(".mfp-img");if(i.length){var j=document.createElement("img");j.className="mfp-img",c.el&&c.el.find("img").length&&(j.alt=c.el.find("img").attr("alt")),c.img=a(j).on("load.mfploader",f).on("error.mfploader",g),j.src=c.src,i.is("img")&&(c.img=c.img.clone()),j=c.img[0],j.naturalWidth>0?c.hasSize=!0:j.width||(c.hasSize=!1)}return b._parseMarkup(d,{title:M(c),img_replaceWith:c.img},c),b.resizeImage(),c.hasSize?(L&&clearInterval(L),c.loadError?(d.addClass("mfp-loading"),b.updateStatus("error",h.tError.replace("%url%",c.src))):(d.removeClass("mfp-loading"),b.updateStatus("ready")),d):(b.updateStatus("loading"),c.loading=!0,c.hasSize||(c.imgHidden=!0,d.addClass("mfp-loading"),b.findImageSize(c)),d)}}});var N,O=function(){return void 0===N&&(N=void 0!==document.createElement("p").style.MozTransform),N};a.magnificPopup.registerModule("zoom",{options:{enabled:!1,easing:"ease-in-out",duration:300,opener:function(a){return a.is("img")?a:a.find("img")}},proto:{initZoom:function(){var a,c=b.st.zoom,d=".zoom";if(c.enabled&&b.supportsTransition){var e,f,g=c.duration,j=function(a){var b=a.clone().removeAttr("style").removeAttr("class").addClass("mfp-animated-image"),d="all "+c.duration/1e3+"s "+c.easing,e={position:"fixed",zIndex:9999,left:0,top:0,"-webkit-backface-visibility":"hidden"},f="transition";return e["-webkit-"+f]=e["-moz-"+f]=e["-o-"+f]=e[f]=d,b.css(e),b},k=function(){b.content.css("visibility","visible")};w("BuildControls"+d,function(){if(b._allowZoom()){if(clearTimeout(e),b.content.css("visibility","hidden"),a=b._getItemToZoom(),!a)return void k();f=j(a),f.css(b._getOffset()),b.wrap.append(f),e=setTimeout(function(){f.css(b._getOffset(!0)),e=setTimeout(function(){k(),setTimeout(function(){f.remove(),a=f=null,y("ZoomAnimationEnded")},16)},g)},16)}}),w(i+d,function(){if(b._allowZoom()){if(clearTimeout(e),b.st.removalDelay=g,!a){if(a=b._getItemToZoom(),!a)return;f=j(a)}f.css(b._getOffset(!0)),b.wrap.append(f),b.content.css("visibility","hidden"),setTimeout(function(){f.css(b._getOffset())},16)}}),w(h+d,function(){b._allowZoom()&&(k(),f&&f.remove(),a=null)})}},_allowZoom:function(){return"image"===b.currItem.type},_getItemToZoom:function(){return b.currItem.hasSize?b.currItem.img:!1},_getOffset:function(c){var d;d=c?b.currItem.img:b.st.zoom.opener(b.currItem.el||b.currItem);var e=d.offset(),f=parseInt(d.css("padding-top"),10),g=parseInt(d.css("padding-bottom"),10);e.top-=a(window).scrollTop()-f;var h={width:d.width(),height:(u?d.innerHeight():d[0].offsetHeight)-g-f};return O()?h["-moz-transform"]=h.transform="translate("+e.left+"px,"+e.top+"px)":(h.left=e.left,h.top=e.top),h}}});var P="iframe",Q="//about:blank",R=function(a){if(b.currTemplate[P]){var c=b.currTemplate[P].find("iframe");c.length&&(a||(c[0].src=Q),b.isIE8&&c.css("display",a?"block":"none"))}};a.magnificPopup.registerModule(P,{options:{markup:'
    ',srcAction:"iframe_src",patterns:{youtube:{index:"youtube.com",id:"v=",src:"//www.youtube.com/embed/%id%?autoplay=1"},vimeo:{index:"vimeo.com/",id:"/",src:"//player.vimeo.com/video/%id%?autoplay=1"},gmaps:{index:"//maps.google.",src:"%id%&output=embed"}}},proto:{initIframe:function(){b.types.push(P),w("BeforeChange",function(a,b,c){b!==c&&(b===P?R():c===P&&R(!0))}),w(h+"."+P,function(){R()})},getIframe:function(c,d){var e=c.src,f=b.st.iframe;a.each(f.patterns,function(){return e.indexOf(this.index)>-1?(this.id&&(e="string"==typeof this.id?e.substr(e.lastIndexOf(this.id)+this.id.length,e.length):this.id.call(this,e)),e=this.src.replace("%id%",e),!1):void 0});var g={};return f.srcAction&&(g[f.srcAction]=e),b._parseMarkup(d,g,c),b.updateStatus("ready"),d}}});var S=function(a){var c=b.items.length;return a>c-1?a-c:0>a?c+a:a},T=function(a,b,c){return a.replace(/%curr%/gi,b+1).replace(/%total%/gi,c)};a.magnificPopup.registerModule("gallery",{options:{enabled:!1,arrowMarkup:'',preload:[0,2],navigateByImgClick:!0,arrows:!0,tPrev:"Previous (Left arrow key)",tNext:"Next (Right arrow key)",tCounter:"%curr% of %total%"},proto:{initGallery:function(){var c=b.st.gallery,e=".mfp-gallery",g=Boolean(a.fn.mfpFastClick);return b.direction=!0,c&&c.enabled?(f+=" mfp-gallery",w(m+e,function(){c.navigateByImgClick&&b.wrap.on("click"+e,".mfp-img",function(){return b.items.length>1?(b.next(),!1):void 0}),d.on("keydown"+e,function(a){37===a.keyCode?b.prev():39===a.keyCode&&b.next()})}),w("UpdateStatus"+e,function(a,c){c.text&&(c.text=T(c.text,b.currItem.index,b.items.length))}),w(l+e,function(a,d,e,f){var g=b.items.length;e.counter=g>1?T(c.tCounter,f.index,g):""}),w("BuildControls"+e,function(){if(b.items.length>1&&c.arrows&&!b.arrowLeft){var d=c.arrowMarkup,e=b.arrowLeft=a(d.replace(/%title%/gi,c.tPrev).replace(/%dir%/gi,"left")).addClass(s),f=b.arrowRight=a(d.replace(/%title%/gi,c.tNext).replace(/%dir%/gi,"right")).addClass(s),h=g?"mfpFastClick":"click";e[h](function(){b.prev()}),f[h](function(){b.next()}),b.isIE7&&(x("b",e[0],!1,!0),x("a",e[0],!1,!0),x("b",f[0],!1,!0),x("a",f[0],!1,!0)),b.container.append(e.add(f))}}),w(n+e,function(){b._preloadTimeout&&clearTimeout(b._preloadTimeout),b._preloadTimeout=setTimeout(function(){b.preloadNearbyImages(),b._preloadTimeout=null},16)}),void w(h+e,function(){d.off(e),b.wrap.off("click"+e),b.arrowLeft&&g&&b.arrowLeft.add(b.arrowRight).destroyMfpFastClick(),b.arrowRight=b.arrowLeft=null})):!1},next:function(){b.direction=!0,b.index=S(b.index+1),b.updateItemHTML()},prev:function(){b.direction=!1,b.index=S(b.index-1),b.updateItemHTML()},goTo:function(a){b.direction=a>=b.index,b.index=a,b.updateItemHTML()},preloadNearbyImages:function(){var a,c=b.st.gallery.preload,d=Math.min(c[0],b.items.length),e=Math.min(c[1],b.items.length);for(a=1;a<=(b.direction?e:d);a++)b._preloadItem(b.index+a);for(a=1;a<=(b.direction?d:e);a++)b._preloadItem(b.index-a)},_preloadItem:function(c){if(c=S(c),!b.items[c].preloaded){var d=b.items[c];d.parsed||(d=b.parseEl(c)),y("LazyLoad",d),"image"===d.type&&(d.img=a('').on("load.mfploader",function(){d.hasSize=!0}).on("error.mfploader",function(){d.hasSize=!0,d.loadError=!0,y("LazyLoadError",d)}).attr("src",d.src)),d.preloaded=!0}}}});var U="retina";a.magnificPopup.registerModule(U,{options:{replaceSrc:function(a){return a.src.replace(/\.\w+$/,function(a){return"@2x"+a})},ratio:1},proto:{initRetina:function(){if(window.devicePixelRatio>1){var a=b.st.retina,c=a.ratio;c=isNaN(c)?c():c,c>1&&(w("ImageHasSize."+U,function(a,b){b.img.css({"max-width":b.img[0].naturalWidth/c,width:"100%"})}),w("ElementParse."+U,function(b,d){d.src=a.replaceSrc(d,c)}))}}}}),function(){var b=1e3,c="ontouchstart"in window,d=function(){v.off("touchmove"+f+" touchend"+f)},e="mfpFastClick",f="."+e;a.fn.mfpFastClick=function(e){return a(this).each(function(){var g,h=a(this);if(c){var i,j,k,l,m,n;h.on("touchstart"+f,function(a){l=!1,n=1,m=a.originalEvent?a.originalEvent.touches[0]:a.touches[0],j=m.clientX,k=m.clientY,v.on("touchmove"+f,function(a){m=a.originalEvent?a.originalEvent.touches:a.touches,n=m.length,m=m[0],(Math.abs(m.clientX-j)>10||Math.abs(m.clientY-k)>10)&&(l=!0,d())}).on("touchend"+f,function(a){d(),l||n>1||(g=!0,a.preventDefault(),clearTimeout(i),i=setTimeout(function(){g=!1},b),e())})})}h.on("click"+f,function(){g||e()})})},a.fn.destroyMfpFastClick=function(){a(this).off("touchstart"+f+" click"+f),c&&v.off("touchmove"+f+" touchend"+f)}}(),A()}); \ No newline at end of file diff --git a/datamodels/2.x/itop-attachments/main.attachments.php b/datamodels/2.x/itop-attachments/main.attachments.php index dd891f203..b78054fa3 100755 --- a/datamodels/2.x/itop-attachments/main.attachments.php +++ b/datamodels/2.x/itop-attachments/main.attachments.php @@ -1,5 +1,5 @@ - class AttachmentPlugIn implements iApplicationUIExtension, iApplicationObjectExtension { protected static $m_bIsModified = false; @@ -204,6 +203,7 @@ class AttachmentPlugIn implements iApplicationUIExtension, iApplicationObjectExt $sTitle = ($oSet->Count() > 0)? Dict::Format('Attachments:TabTitle_Count', $oSet->Count()) : Dict::S('Attachments:EmptyTabTitle'); $oPage->SetCurrentTab($sTitle); } + $sMaxWidth = MetaModel::GetModuleSetting('itop-attachment', 'inline_image_max_width', '450px'); $oPage->add_style( <<add('
    '); @@ -243,15 +246,24 @@ EOF $sIsDeleteEnabled = $this->m_bDeleteEnabled ? 'true' : 'false'; $iTransactionId = $oPage->GetTransactionId(); $sClass = get_class($oObject); + $iObjectId = $oObject->Getkey(); $sTempId = session_id().'_'.$iTransactionId; $sDeleteBtn = Dict::S('Attachments:DeleteBtn'); $oPage->add_script( <<GetFileName(); $sIcon = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($sFileName); $sPreview = $oDoc->IsPreviewAvailable() ? 'true' : 'false'; - $sDownloadLink = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=download_document&class=Attachment&id='.$iAttId.'&field=contents'; + $sDownloadLink = utils::GetAbsoluteUrlAppRoot().ATTACHMENT_DOWNLOAD_URL.$iAttId; $oPage->add(''); } @@ -291,10 +303,10 @@ EOF $oDoc = $oAttachment->Get('contents'); $sFileName = $oDoc->GetFileName(); $sIcon = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($sFileName); - $sDownloadLink = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=download_document&class=Attachment&id='.$iAttId.'&field=contents'; + $sDownloadLink = utils::GetAbsoluteUrlAppRoot().ATTACHMENT_DOWNLOAD_URL.$iAttId; $sPreview = $oDoc->IsPreviewAvailable() ? 'true' : 'false'; $oPage->add(''); - $oPage->add_ready_script("$('#attachment_plugin').trigger('add_attachment', [$iAttId, '".addslashes($sFileName)."']);"); + $oPage->add_ready_script("$('#attachment_plugin').trigger('add_attachment', [$iAttId, '".addslashes($sFileName)."', false /* not an line image */]);"); } } } @@ -305,9 +317,21 @@ EOF $oPage->p(Dict::S('Attachments:AddAttachment').' '.$sMaxUpload); $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.iframe-transport.js'); - $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.fileupload.js'); - -$oPage->add_ready_script( + $oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.fileupload.js'); + + $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'); + $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 $('#file').fileupload({ url: GetAbsoluteUrlModulesRoot()+'itop-attachments/ajax.attachment.php', @@ -323,13 +347,13 @@ $oPage->add_ready_script( } else { - var sDownloadLink = GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?operation=download_document&class=Attachment&id='+data.result.att_id+'&field=contents'; + var sDownloadLink = '$sDownloadLink'+data.result.att_id; $('#attachments').append(''); if($sIsDeleteEnabled) { $('#display_attachment_'+data.result.att_id).hover( function() { $(this).children(':button').toggleClass('btn_hidden'); } ); } - $('#attachment_plugin').trigger('add_attachment', [data.result.att_id, data.result.msg]); + $('#attachment_plugin').trigger('add_attachment', [data.result.att_id, data.result.msg, false /* inline image */]); } } }, @@ -380,7 +404,65 @@ $oPage->add_ready_script( window.dropZoneTimeout = null; 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() { + $('.attachment a').each(function() { + var sUrl = $(this).attr('href'); + if($('img[src="'+sUrl+'"]').length > 0) + { + $(this).addClass('image-in-use').find('img').wrap('
    '); + } + }); + $('.htmlEditor').each(function() { + var oEditor = $(this).ckeditorGet(); + var sHtml = oEditor.getData(); + var jElement = $('
    ').html(sHtml).contents(); + jElement.find('img').each(function() { + var sSrc = $(this).attr('src'); + $('.attachment a[href="'+sSrc+'"]').parent().addClass('image-in-use').find('img').wrap('
    '); + }); + }); + $('.image-in-use-wrapper').append('
    '); + }, 200 ); EOF ); $oPage->p(''); @@ -406,7 +488,7 @@ EOF $sFileName = $oDoc->GetFileName(); $sIcon = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($sFileName); $sPreview = $oDoc->IsPreviewAvailable() ? 'true' : 'false'; - $sDownloadLink = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php?operation=download_document&class=Attachment&id='.$iAttId.'&field=contents'; + $sDownloadLink = utils::GetAbsoluteUrlAppRoot().ATTACHMENT_DOWNLOAD_URL.$iAttId; $oPage->add(''); } } @@ -415,7 +497,24 @@ EOF $oPage->add('
    '); $sPreviewNotAvailable = addslashes(Dict::S('Attachments:PreviewNotAvailable')); $iMaxWidth = MetaModel::GetModuleSetting('itop-attachments', 'preview_max_width', 290); - $oPage->add_ready_script("$(document).tooltip({ items: '.attachment a', 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'; }}});"); + $oPage->add_ready_script( +<<');} 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 + ); } protected static function UpdateAttachments($oObject, $oChange = null) diff --git a/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml b/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml index 277591027..8b894bf6d 100755 --- a/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml +++ b/datamodels/2.x/itop-tickets/datamodel.itop-tickets.xml @@ -130,6 +130,7 @@ description false + html true diff --git a/dictionaries/cs.dictionary.itop.core.php b/dictionaries/cs.dictionary.itop.core.php index e77405e94..eb21a0d36 100755 --- a/dictionaries/cs.dictionary.itop.core.php +++ b/dictionaries/cs.dictionary.itop.core.php @@ -1,6 +1,6 @@ 'Možnosti tabulky', 'Core:BulkExport:OptionNoLocalize' => 'Nepřekládat hodnoty číselníků', 'Core:BulkExport:OptionLinkSets' => 'Zahrnout odkazované objekty', + 'Core:BulkExport:OptionFormattedText' => 'Preserve text formatting~~', 'Core:BulkExport:ScopeDefinition' => 'Definice objektů k exportu', 'Core:BulkExportLabelOQLExpression' => 'Dotaz OQL:', 'Core:BulkExportLabelPhrasebookEntry' => 'Query Phrasebook Entry:~~', diff --git a/dictionaries/da.dictionary.itop.core.php b/dictionaries/da.dictionary.itop.core.php index e041b6a93..530d3cd26 100644 --- a/dictionaries/da.dictionary.itop.core.php +++ b/dictionaries/da.dictionary.itop.core.php @@ -1,5 +1,5 @@ 'Core:BulkExport:SpreadsheetOptions' => 'Spreadsheet Options~~', 'Core:BulkExport:OptionNoLocalize' => 'Do not localize the values (for Enumerated fields)~~', 'Core:BulkExport:OptionLinkSets' => 'Include linked objects~~', + 'Core:BulkExport:OptionFormattedText' => 'Preserve text formatting~~', 'Core:BulkExport:ScopeDefinition' => 'Definition of the objects to export~~', 'Core:BulkExportLabelOQLExpression' => 'OQL Query:~~', 'Core:BulkExportLabelPhrasebookEntry' => 'Query Phrasebook Entry:~~', diff --git a/dictionaries/de.dictionary.itop.core.php b/dictionaries/de.dictionary.itop.core.php index 2dd668430..a23c1c253 100644 --- a/dictionaries/de.dictionary.itop.core.php +++ b/dictionaries/de.dictionary.itop.core.php @@ -1,5 +1,5 @@ - * @copyright Copyright (C) 2010-2012 Combodo SARL + * @copyright Copyright (C) 2010-2016 Combodo SARL * @licence http://opensource.org/licenses/AGPL-3.0 */ @@ -590,6 +590,7 @@ Operatoren:
    'Core:BulkExport:SpreadsheetOptions' => 'Spreadsheet-Optionen', 'Core:BulkExport:OptionNoLocalize' => 'Werte von Aufzählungsfeldern nicht lokalisieren', 'Core:BulkExport:OptionLinkSets' => 'Include linked objects~~', + 'Core:BulkExport:OptionFormattedText' => 'Preserve Textformatierung', 'Core:BulkExport:ScopeDefinition' => 'Definition der zu exportierenden Objekte', 'Core:BulkExportLabelOQLExpression' => 'OQL-Abfrage', 'Core:BulkExportLabelPhrasebookEntry' => 'Query-Bibliotheks-Eintrag:', diff --git a/dictionaries/dictionary.itop.core.php b/dictionaries/dictionary.itop.core.php index 8a4acfd7a..e30f736bc 100644 --- a/dictionaries/dictionary.itop.core.php +++ b/dictionaries/dictionary.itop.core.php @@ -1,5 +1,5 @@ 'Spreadsheet Options', 'Core:BulkExport:OptionNoLocalize' => 'Do not localize the values (for Enumerated fields)', 'Core:BulkExport:OptionLinkSets' => 'Include linked objects', + 'Core:BulkExport:OptionFormattedText' => 'Preserve text formatting', 'Core:BulkExport:ScopeDefinition' => 'Definition of the objects to export', 'Core:BulkExportLabelOQLExpression' => 'OQL Query:', 'Core:BulkExportLabelPhrasebookEntry' => 'Query Phrasebook Entry:', diff --git a/dictionaries/es_cr.dictionary.itop.core.php b/dictionaries/es_cr.dictionary.itop.core.php index d12b89959..fc5e5ff1b 100644 --- a/dictionaries/es_cr.dictionary.itop.core.php +++ b/dictionaries/es_cr.dictionary.itop.core.php @@ -1,5 +1,5 @@ 'Spreadsheet Options~~', 'Core:BulkExport:OptionNoLocalize' => 'Do not localize the values (for Enumerated fields)~~', 'Core:BulkExport:OptionLinkSets' => 'Include linked objects~~', + 'Core:BulkExport:OptionFormattedText' => 'Preserve text formatting~~', 'Core:BulkExport:ScopeDefinition' => 'Definition of the objects to export~~', 'Core:BulkExportLabelOQLExpression' => 'OQL Query:~~', 'Core:BulkExportLabelPhrasebookEntry' => 'Query Phrasebook Entry:~~', diff --git a/dictionaries/fr.dictionary.itop.core.php b/dictionaries/fr.dictionary.itop.core.php index 9b0696401..99d5d2595 100644 --- a/dictionaries/fr.dictionary.itop.core.php +++ b/dictionaries/fr.dictionary.itop.core.php @@ -1,5 +1,5 @@ 'Core:BulkExport:SpreadsheetOptions' => 'Options du format HTML pour Excel', 'Core:BulkExport:OptionNoLocalize' => 'Ne pas traduire les valeurs (pour les champs de type "Enum")', 'Core:BulkExport:OptionLinkSets' => 'Inclure les objets liés', + 'Core:BulkExport:OptionFormattedText' => 'Préserver le formatage du texte', 'Core:BulkExport:ScopeDefinition' => 'Définition des objets à exporter', 'Core:BulkExportLabelOQLExpression' => 'Requête OQL:', 'Core:BulkExportLabelPhrasebookEntry' => 'Entrée du livre des requêtes:', diff --git a/dictionaries/hu.dictionary.itop.core.php b/dictionaries/hu.dictionary.itop.core.php index e508d63d3..7ebe279db 100755 --- a/dictionaries/hu.dictionary.itop.core.php +++ b/dictionaries/hu.dictionary.itop.core.php @@ -1,5 +1,5 @@ /** - * @copyright Copyright (C) 2010-2012 Combodo SARL + * @copyright Copyright (C) 2010-2016 Combodo SARL * @license http://opensource.org/licenses/AGPL-3.0 */ @@ -593,6 +593,7 @@ Operators:
    'Core:BulkExport:SpreadsheetOptions' => 'Spreadsheet Options~~', 'Core:BulkExport:OptionNoLocalize' => 'Do not localize the values (for Enumerated fields)~~', 'Core:BulkExport:OptionLinkSets' => 'Include linked objects~~', + 'Core:BulkExport:OptionFormattedText' => 'Preserve text formatting~~', 'Core:BulkExport:ScopeDefinition' => 'Definition of the objects to export~~', 'Core:BulkExportLabelOQLExpression' => 'OQL Query:~~', 'Core:BulkExportLabelPhrasebookEntry' => 'Query Phrasebook Entry:~~', diff --git a/dictionaries/it.dictionary.itop.core.php b/dictionaries/it.dictionary.itop.core.php index 41b6ec51c..49820eb7f 100644 --- a/dictionaries/it.dictionary.itop.core.php +++ b/dictionaries/it.dictionary.itop.core.php @@ -1,5 +1,5 @@ 'Spreadsheet Options~~', 'Core:BulkExport:OptionNoLocalize' => 'Do not localize the values (for Enumerated fields)~~', 'Core:BulkExport:OptionLinkSets' => 'Include linked objects~~', + 'Core:BulkExport:OptionFormattedText' => 'Preserve text formatting~~', 'Core:BulkExport:ScopeDefinition' => 'Definition of the objects to export~~', 'Core:BulkExportLabelOQLExpression' => 'OQL Query:~~', 'Core:BulkExportLabelPhrasebookEntry' => 'Query Phrasebook Entry:~~', diff --git a/dictionaries/ja.dictionary.itop.core.php b/dictionaries/ja.dictionary.itop.core.php index 44150057c..880d26bbf 100644 --- a/dictionaries/ja.dictionary.itop.core.php +++ b/dictionaries/ja.dictionary.itop.core.php @@ -1,5 +1,5 @@ /** - * @copyright Copyright (C) 2010-2012 Combodo SARL + * @copyright Copyright (C) 2010-2016 Combodo SARL * @licence http://opensource.org/licenses/AGPL-3.0 */ @@ -615,6 +615,7 @@ Operators:
    'Core:BulkExport:SpreadsheetOptions' => 'Spreadsheet Options~~', 'Core:BulkExport:OptionLinkSets' => 'Include linked objects~~', 'Core:BulkExport:OptionNoLocalize' => 'Do not localize the values (for Enumerated fields)~~', + 'Core:BulkExport:OptionFormattedText' => 'Preserve text formatting~~', 'Core:BulkExport:ScopeDefinition' => 'Definition of the objects to export~~', 'Core:BulkExportLabelOQLExpression' => 'OQL Query:~~', 'Core:BulkExportLabelPhrasebookEntry' => 'Query Phrasebook Entry:~~', diff --git a/dictionaries/nl.dictionary.itop.core.php b/dictionaries/nl.dictionary.itop.core.php index 9cffc4e54..e162531c8 100644 --- a/dictionaries/nl.dictionary.itop.core.php +++ b/dictionaries/nl.dictionary.itop.core.php @@ -1,5 +1,5 @@ 'Spreadsheet Options~~', 'Core:BulkExport:OptionLinkSets' => 'Include linked objects~~', 'Core:BulkExport:OptionNoLocalize' => 'Do not localize the values (for Enumerated fields)~~', + 'Core:BulkExport:OptionFormattedText' => 'Preserve text formatting~~', 'Core:BulkExport:ScopeDefinition' => 'Definition of the objects to export~~', 'Core:BulkExportLabelOQLExpression' => 'OQL Query:~~', 'Core:BulkExportLabelPhrasebookEntry' => 'Query Phrasebook Entry:~~', diff --git a/dictionaries/pt_br.dictionary.itop.core.php b/dictionaries/pt_br.dictionary.itop.core.php index dc738ce02..334991334 100644 --- a/dictionaries/pt_br.dictionary.itop.core.php +++ b/dictionaries/pt_br.dictionary.itop.core.php @@ -1,5 +1,5 @@ 'Spreadsheet Options~~', 'Core:BulkExport:OptionLinkSets' => 'Include linked objects~~', 'Core:BulkExport:OptionNoLocalize' => 'Do not localize the values (for Enumerated fields)~~', + 'Core:BulkExport:OptionFormattedText' => 'Preserve text formatting~~', 'Core:BulkExport:ScopeDefinition' => 'Definition of the objects to export~~', 'Core:BulkExportLabelOQLExpression' => 'OQL Query:~~', 'Core:BulkExportLabelPhrasebookEntry' => 'Query Phrasebook Entry:~~', diff --git a/dictionaries/ru.dictionary.itop.core.php b/dictionaries/ru.dictionary.itop.core.php index 8eb3511c1..176b11123 100644 --- a/dictionaries/ru.dictionary.itop.core.php +++ b/dictionaries/ru.dictionary.itop.core.php @@ -833,6 +833,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', array( 'Core:BulkExport:SpreadsheetOptions' => 'Spreadsheet Options~~', 'Core:BulkExport:OptionLinkSets' => 'Include linked objects~~', 'Core:BulkExport:OptionNoLocalize' => 'Do not localize the values (for Enumerated fields)~~', + 'Core:BulkExport:OptionFormattedText' => 'Preserve text formatting~~', 'Core:BulkExport:ScopeDefinition' => 'Definition of the objects to export~~', 'Core:BulkExportLabelOQLExpression' => 'OQL Query:~~', 'Core:BulkExportLabelPhrasebookEntry' => 'Query Phrasebook Entry:~~', diff --git a/dictionaries/tr.dictionary.itop.core.php b/dictionaries/tr.dictionary.itop.core.php index 0e9eb1310..f46fd5feb 100644 --- a/dictionaries/tr.dictionary.itop.core.php +++ b/dictionaries/tr.dictionary.itop.core.php @@ -1,5 +1,5 @@ - * @copyright Copyright (C) 2010-2012 Combodo SARL + * @copyright Copyright (C) 2010-2016 Combodo SARL * @license http://opensource.org/licenses/AGPL-3.0 */ @@ -765,6 +765,7 @@ Operators:
    'Core:BulkExport:SpreadsheetOptions' => 'Spreadsheet Options~~', 'Core:BulkExport:OptionLinkSets' => 'Include linked objects~~', 'Core:BulkExport:OptionNoLocalize' => 'Do not localize the values (for Enumerated fields)~~', + 'Core:BulkExport:OptionFormattedText' => 'Preserve text formatting~~', 'Core:BulkExport:ScopeDefinition' => 'Definition of the objects to export~~', 'Core:BulkExportLabelOQLExpression' => 'OQL Query:~~', 'Core:BulkExportLabelPhrasebookEntry' => 'Query Phrasebook Entry:~~', diff --git a/dictionaries/zh.dictionary.itop.core.php b/dictionaries/zh.dictionary.itop.core.php index 6f1315724..24e206b9c 100644 --- a/dictionaries/zh.dictionary.itop.core.php +++ b/dictionaries/zh.dictionary.itop.core.php @@ -1,5 +1,5 @@ - * @copyright Copyright (C) 2010-2012 Combodo SARL + * @copyright Copyright (C) 2010-2016 Combodo SARL * @license http://opensource.org/licenses/AGPL-3.0 */ @@ -764,6 +764,7 @@ Operators:
    'Core:BulkExport:SpreadsheetOptions' => 'Spreadsheet Options~~', 'Core:BulkExport:OptionLinkSets' => 'Include linked objects~~', 'Core:BulkExport:OptionNoLocalize' => 'Do not localize the values (for Enumerated fields)~~', + 'Core:BulkExport:OptionFormattedText' => 'Preserve text formatting~~', 'Core:BulkExport:ScopeDefinition' => 'Definition of the objects to export~~', 'Core:BulkExportLabelOQLExpression' => 'OQL Query:~~', 'Core:BulkExportLabelPhrasebookEntry' => 'Query Phrasebook Entry:~~', diff --git a/js/forms-json-utils.js b/js/forms-json-utils.js index fd82aec51..d87f4ae9b 100644 --- a/js/forms-json-utils.js +++ b/js/forms-json-utils.js @@ -249,6 +249,16 @@ function ValidateCKEditField(sFieldId, sPattern, bMandatory, sFormId, nullValue) else { sTextContent = oFormattedContents.contents().find("body").text(); + + if (sTextContent == '') + { + // No plain text, maybe there is just an image... + var oImg = oFormattedContents.contents().find("body img"); + if (oImg.length != 0) + { + sTextContent = 'image'; + } + } } if (bMandatory && (sTextContent == '')) diff --git a/js/jquery.tablesorter.pager.js b/js/jquery.tablesorter.pager.js index f32310641..1db8728fc 100644 --- a/js/jquery.tablesorter.pager.js +++ b/js/jquery.tablesorter.pager.js @@ -15,19 +15,22 @@ function sprintf(format, etc) { function setPageSize(table,size, bReload) { var c = table.config; - c.selectedSize = size; - if (size == -1) + if (c != undefined) { - size = c.totalRows; + c.selectedSize = size; + if (size == -1) + { + size = c.totalRows; + } + c.size = size; + c.totalPages = Math.ceil(c.totalRows / c.size); + c.pagerPositionSet = false; + if (bReload) + { + moveToPage(table); + } + fixPosition(table); } - c.size = size; - c.totalPages = Math.ceil(c.totalRows / c.size); - c.pagerPositionSet = false; - if (bReload) - { - moveToPage(table); - } - fixPosition(table); } function fixPosition(table) { @@ -246,6 +249,8 @@ function sprintf(format, etc) { function applySelection(table) { var c = table.config; + if (c == undefined) return; + if (c.selectionMode == 'negative') { $(table).find(':checkbox[name^=selectObj]').attr('checked', true); diff --git a/js/simple_graph.js b/js/simple_graph.js index a2817742a..15c6915bd 100644 --- a/js/simple_graph.js +++ b/js/simple_graph.js @@ -834,7 +834,7 @@ $(function() jTab.find('span').html(sTabText+' '); } $.post(sUrl, oParams, function(data) { - var sDownloadLink = GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?operation=download_document&class=Attachment&id='+data.att_id+'&field=contents'; + var sDownloadLink = GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?operation=download_document&class=Attachment&field=contents&id='+data.att_id; var sIcon = GetAbsoluteUrlModulesRoot()+'itop-attachments/icons/pdf.png'; if (jTab != null) { diff --git a/portal/images/company_logo.png b/portal/images/company_logo.png index 8f9f434f8..ecc258fb4 100644 Binary files a/portal/images/company_logo.png and b/portal/images/company_logo.png differ diff --git a/setup/compiler.class.inc.php b/setup/compiler.class.inc.php index 6ba3580fd..460c58d47 100644 --- a/setup/compiler.class.inc.php +++ b/setup/compiler.class.inc.php @@ -1219,6 +1219,7 @@ EOF; // Added if present... // $aParameters['validation_pattern'] = $this->GetPropString($oField, 'validation_pattern'); + $aParameters['format'] = $this->GetPropString($oField, 'format'); $aParameters['width'] = $this->GetPropString($oField, 'width'); $aParameters['height'] = $this->GetPropString($oField, 'height'); $aParameters['digits'] = $this->GetPropNumber($oField, 'digits');