1<?php 2// Copyright (C) 2010-2017 Combodo SARL 3// 4// This file is part of iTop. 5// 6// iTop is free software; you can redistribute it and/or modify 7// it under the terms of the GNU Affero General Public License as published by 8// the Free Software Foundation, either version 3 of the License, or 9// (at your option) any later version. 10// 11// iTop is distributed in the hope that it will be useful, 12// but WITHOUT ANY WARRANTY; without even the implied warranty of 13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14// GNU Affero General Public License for more details. 15// 16// You should have received a copy of the GNU Affero General Public License 17// along with iTop. If not, see <http://www.gnu.org/licenses/> 18 19 20/** 21 * Class DisplayTemplate 22 * 23 * @copyright Copyright (C) 2010-2017 Combodo SARL 24 * @license http://opensource.org/licenses/AGPL-3.0 25 */ 26 27require_once(APPROOT.'/application/displayblock.class.inc.php'); 28/** 29 * This class manages the special template format used internally to build the iTop web pages 30 */ 31class DisplayTemplate 32{ 33 protected $m_sTemplate; 34 protected $m_aTags; 35 static protected $iBlockCount = 0; 36 37 public function __construct($sTemplate) 38 { 39 $this->m_aTags = array ( 40 'itopblock', 41 'itopcheck', 42 'itoptabs', 43 'itoptab', 44 'itoptoggle', 45 'itopstring', 46 'sqlblock' 47 ); 48 $this->m_sTemplate = $sTemplate; 49 } 50 51 public function Render(WebPage $oPage, $aParams = array()) 52 { 53 $this->m_sTemplate = MetaModel::ApplyParams($this->m_sTemplate, $aParams); 54 $iStart = 0; 55 $iEnd = strlen($this->m_sTemplate); 56 $iCount = 0; 57 $iBeforeTagPos = $iStart; 58 $iAfterTagPos = $iStart; 59 while($sTag = $this->GetNextTag($iStart, $iEnd)) 60 { 61 $sContent = $this->GetTagContent($sTag, $iStart, $iEnd); 62 $iAfterTagPos = $iEnd + strlen('</'.$sTag.'>'); 63 $sOuterTag = substr($this->m_sTemplate, $iStart, $iAfterTagPos - $iStart); 64 $oPage->add(substr($this->m_sTemplate, $iBeforeTagPos, $iStart - $iBeforeTagPos)); 65 if ($sTag == DisplayBlock::TAG_BLOCK) 66 { 67 try 68 { 69 $oBlock = DisplayBlock::FromTemplate($sOuterTag); 70 if (is_object($oBlock)) 71 { 72 $oBlock->Display($oPage, 'block_'.self::$iBlockCount, $aParams); 73 } 74 } 75 catch(OQLException $e) 76 { 77 $oPage->p('Error in template (please contact your administrator) - Invalid query<!--'.$sOuterTag.'-->'); 78 } 79 catch(Exception $e) 80 { 81 $oPage->p('Error in template (please contact your administrator)<!--'.$e->getMessage().'--><!--'.$sOuterTag.'-->'); 82 } 83 84 self::$iBlockCount++; 85 } 86 else 87 { 88 $aAttributes = $this->GetTagAttributes($sTag, $iStart, $iEnd); 89 //$oPage->p("Tag: $sTag - ($iStart, $iEnd)"); 90 $this->RenderTag($oPage, $sTag, $aAttributes, $sContent); 91 92 } 93 $iAfterTagPos = $iEnd + strlen('</'.$sTag.'>'); 94 $iBeforeTagPos = $iAfterTagPos; 95 $iStart = $iEnd; 96 $iEnd = strlen($this->m_sTemplate); 97 $iCount++; 98 } 99 $oPage->add(substr($this->m_sTemplate, $iAfterTagPos)); 100 } 101 102 public function GetNextTag(&$iStartPos, &$iEndPos) 103 { 104 $iChunkStartPos = $iStartPos; 105 $sNextTag = null; 106 $iStartPos = $iEndPos; 107 foreach($this->m_aTags as $sTag) 108 { 109 // Search for the opening tag 110 $iOpeningPos = stripos($this->m_sTemplate, '<'.$sTag.' ', $iChunkStartPos); 111 if ($iOpeningPos === false) 112 { 113 $iOpeningPos = stripos($this->m_sTemplate, '<'.$sTag.'>', $iChunkStartPos); 114 } 115 if ($iOpeningPos !== false) 116 { 117 $iClosingPos = stripos($this->m_sTemplate, '</'.$sTag.'>', $iOpeningPos); 118 } 119 if ( ($iOpeningPos !== false) && ($iClosingPos !== false)) 120 { 121 if ($iOpeningPos < $iStartPos) 122 { 123 // This is the next tag 124 $iStartPos = $iOpeningPos; 125 $iEndPos = $iClosingPos; 126 $sNextTag = $sTag; 127 } 128 } 129 } 130 return $sNextTag; 131 } 132 133 public function GetTagContent($sTag, $iStartPos, $iEndPos) 134 { 135 $sContent = ""; 136 $iContentStart = strpos($this->m_sTemplate, '>', $iStartPos); // Content of tag start immediatly after the first closing bracket 137 if ($iContentStart !== false) 138 { 139 $sContent = substr($this->m_sTemplate, 1+$iContentStart, $iEndPos - $iContentStart - 1); 140 } 141 return $sContent; 142 } 143 144 public function GetTagAttributes($sTag, $iStartPos, $iEndPos) 145 { 146 $aAttr = array(); 147 $iAttrStart = strpos($this->m_sTemplate, ' ', $iStartPos); // Attributes start just after the first space 148 $iAttrEnd = strpos($this->m_sTemplate, '>', $iStartPos); // Attributes end just before the first closing bracket 149 if ( ($iAttrStart !== false) && ($iAttrEnd !== false) && ($iAttrEnd > $iAttrStart)) 150 { 151 $sAttributes = substr($this->m_sTemplate, 1+$iAttrStart, $iAttrEnd - $iAttrStart - 1); 152 $aAttributes = explode(' ', $sAttributes); 153 foreach($aAttributes as $sAttr) 154 { 155 if ( preg_match('/(.+) *= *"(.+)"$/', $sAttr, $aMatches) ) 156 { 157 $aAttr[strtolower($aMatches[1])] = $aMatches[2]; 158 } 159 } 160 } 161 return $aAttr; 162 } 163 164 protected function RenderTag($oPage, $sTag, $aAttributes, $sContent) 165 { 166 static $iTabContainerCount = 0; 167 switch($sTag) 168 { 169 case 'itoptabs': 170 $oPage->AddTabContainer('Tabs_'.$iTabContainerCount); 171 $oPage->SetCurrentTabContainer('Tabs_'.$iTabContainerCount); 172 $iTabContainerCount++; 173 //$oPage->p('Content:<pre>'.htmlentities($sContent, ENT_QUOTES, 'UTF-8').'</pre>'); 174 $oTemplate = new DisplayTemplate($sContent); 175 $oTemplate->Render($oPage, array()); // no params to apply, they have already been applied 176 $oPage->SetCurrentTabContainer(''); 177 break; 178 179 case 'itopcheck': 180 $sClassName = $aAttributes['class']; 181 if (MetaModel::IsValidClass($sClassName) && UserRights::IsActionAllowed($sClassName, UR_ACTION_READ)) 182 { 183 $oTemplate = new DisplayTemplate($sContent); 184 $oTemplate->Render($oPage, array()); // no params to apply, they have already been applied 185 } 186 else 187 { 188 // Leave a trace for those who'd like to understand why nothing is displayed 189 $oPage->add("<!-- class $sClassName does not exist, skipping some part of the template -->\n"); 190 } 191 break; 192 193 case 'itoptab': 194 $oPage->SetCurrentTab(Dict::S(str_replace('_', ' ', $aAttributes['name']))); 195 $oTemplate = new DisplayTemplate($sContent); 196 $oTemplate->Render($oPage, array()); // no params to apply, they have already been applied 197 //$oPage->p('iTop Tab Content:<pre>'.htmlentities($sContent, ENT_QUOTES, 'UTF-8').'</pre>'); 198 $oPage->SetCurrentTab(''); 199 break; 200 201 case 'itoptoggle': 202 $sName = isset($aAttributes['name']) ? $aAttributes['name'] : 'Tagada'; 203 $bOpen = isset($aAttributes['open']) ? $aAttributes['open'] : true; 204 $oPage->StartCollapsibleSection(Dict::S($sName), $bOpen); 205 $oTemplate = new DisplayTemplate($sContent); 206 $oTemplate->Render($oPage, array()); // no params to apply, they have already been applied 207 //$oPage->p('iTop Tab Content:<pre>'.htmlentities($sContent, ENT_QUOTES, 'UTF-8').'</pre>'); 208 $oPage->EndCollapsibleSection(); 209 break; 210 211 case 'itopstring': 212 $oPage->add(Dict::S($sContent)); 213 break; 214 215 case 'sqlblock': 216 $oBlock = SqlBlock::FromTemplate($sContent); 217 $oBlock->RenderContent($oPage); 218 break; 219 220 case 'itopblock': // No longer used, handled by DisplayBlock::FromTemplate see above 221 $oPage->add("<!-- Application Error: should be handled by DisplayBlock::FromTemplate -->"); 222 break; 223 224 default: 225 // Unknown tag, just ignore it or now -- output an HTML comment 226 $oPage->add("<!-- unsupported tag: $sTag -->"); 227 } 228 } 229 230 /** 231 * Unit test 232 */ 233 static public function UnitTest() 234 { 235 require_once(APPROOT.'/application/startup.inc.php'); 236 require_once(APPROOT."/application/itopwebpage.class.inc.php"); 237 238 $sTemplate = '<div class="page_header"> 239 <div class="actions_details"><a href="#"><span>Actions</span></a></div> 240 <h1>$class$: <span class="hilite">$name$</span></h1> 241 <itopblock blockclass="HistoryBlock" type="toggle" encoding="text/oql">SELECT CMDBChangeOp WHERE objkey = $id$ AND objclass = \'$class$\'</itopblock> 242 </div> 243 <img src="../../images/connect_to_network.png" style="margin-top:-10px; margin-right:10px; float:right"> 244 <itoptabs> 245 <itoptab name="Interfaces"> 246 <itopblock blockclass="DisplayBlock" type="list" encoding="text/oql">SELECT Interface AS i WHERE i.device_id = $id$</itopblock> 247 </itoptab> 248 <itoptab name="Contacts"> 249 <itopblock blockclass="DisplayBlock" type="list" encoding="text/oql">SELECT Contact AS c JOIN lnkContactToCI AS l ON l.contact_id = c.id WHERE l.ci_id = $id$</itopblock> 250 </itoptab> 251 <itoptab name="Documents"> 252 <itopblock blockclass="DisplayBlock" type="list" encoding="text/oql">SELECT Document AS d JOIN lnkDocumentToCI as l ON l.document_id = d.id WHERE l.ci_id = $id$)</itopblock> 253 </itoptab> 254 </itoptabs>'; 255 256 $oPage = new iTopWebPage('Unit Test'); 257 //$oPage->add("Template content: <pre>".htmlentities($sTemplate, ENT_QUOTES, 'UTF-8')."</pre>\n"); 258 $oTemplate = new DisplayTemplate($sTemplate); 259 $oTemplate->Render($oPage, array('class'=>'Network device','pkey'=> 271, 'name' => 'deliversw01.mecanorama.fr', 'org_id' => 3)); 260 $oPage->output(); 261 } 262} 263 264/** 265 * Special type of template for displaying the details of an object 266 * On top of the defaut 'blocks' managed by the parent class, the following placeholders 267 * are available in such a template: 268 * $attribute_code$ An attribute of the object (in edit mode this is the input for the attribute) 269 * $attribute_code->label()$ The label of an attribute 270 * $PlugIn:plugInClass->properties()$ The ouput of OnDisplayProperties of the specified plugInClass 271 */ 272class ObjectDetailsTemplate extends DisplayTemplate 273{ 274 public function __construct($sTemplate, $oObj, $sFormPrefix = '') 275 { 276 parent::__construct($sTemplate); 277 $this->m_oObj = $oObj; 278 $this->m_sPrefix = $sFormPrefix; 279 } 280 281 public function Render(WebPage $oPage, $aParams = array(), $bEditMode = false) 282 { 283 $sStateAttCode = MetaModel :: GetStateAttributeCode(get_class($this->m_oObj)); 284 $aTemplateFields = array(); 285 preg_match_all('/\\$this->([a-z0-9_]+)\\$/', $this->m_sTemplate, $aMatches); 286 foreach ($aMatches[1] as $sAttCode) 287 { 288 if (MetaModel::IsValidAttCode(get_class($this->m_oObj), $sAttCode)) 289 { 290 $aTemplateFields[] = $sAttCode; 291 } 292 else 293 { 294 $aParams['this->'.$sAttCode] = "<!--Unknown attribute: $sAttCode-->"; 295 } 296 } 297 preg_match_all('/\\$this->field\\(([a-z0-9_]+)\\)\\$/', $this->m_sTemplate, $aMatches); 298 foreach ($aMatches[1] as $sAttCode) 299 { 300 if (MetaModel::IsValidAttCode(get_class($this->m_oObj), $sAttCode)) 301 { 302 $aTemplateFields[] = $sAttCode; 303 } 304 else 305 { 306 $aParams['this->field('.$sAttCode.')'] = "<!--Unknown attribute: $sAttCode-->"; 307 } 308 } 309 $aFieldsComments = (isset($aParams['fieldsComments'])) ? $aParams['fieldsComments'] : array(); 310 $aFieldsMap = array(); 311 312 $sClass = get_class($this->m_oObj); 313 // Renders the fields used in the template 314 foreach(MetaModel::ListAttributeDefs(get_class($this->m_oObj)) as $sAttCode => $oAttDef) 315 { 316 $aParams['this->label('.$sAttCode.')'] = $oAttDef->GetLabel(); 317 $aParams['this->comments('.$sAttCode.')'] = isset($aFieldsComments[$sAttCode]) ? $aFieldsComments[$sAttCode] : ''; 318 $iInputId = '2_'.$sAttCode; // TODO: generate a real/unique prefix... 319 if (in_array($sAttCode, $aTemplateFields)) 320 { 321 if ($this->m_oObj->IsNew()) 322 { 323 $iFlags = $this->m_oObj->GetInitialStateAttributeFlags($sAttCode); 324 } 325 else 326 { 327 $iFlags = $this->m_oObj->GetAttributeFlags($sAttCode); 328 } 329 if (($iFlags & OPT_ATT_MANDATORY) && $this->m_oObj->IsNew()) 330 { 331 $iFlags = $iFlags & ~OPT_ATT_READONLY; // Mandatory fields cannot be read-only when creating an object 332 } 333 334 if ((!$oAttDef->IsWritable()) || ($sStateAttCode == $sAttCode)) 335 { 336 $iFlags = $iFlags | OPT_ATT_READONLY; 337 } 338 339 if ($iFlags & OPT_ATT_HIDDEN) 340 { 341 $aParams['this->label('.$sAttCode.')'] = ''; 342 $aParams['this->field('.$sAttCode.')'] = ''; 343 $aParams['this->comments('.$sAttCode.')'] = ''; 344 $aParams['this->'.$sAttCode] = ''; 345 } 346 else 347 { 348 if ($bEditMode && ($iFlags & (OPT_ATT_READONLY|OPT_ATT_SLAVE))) 349 { 350 // Check if the attribute is not read-only because of a synchro... 351 $aReasons = array(); 352 $sSynchroIcon = ''; 353 if ($iFlags & OPT_ATT_SLAVE) 354 { 355 $iSynchroFlags = $this->m_oObj->GetSynchroReplicaFlags($sAttCode, $aReasons); 356 $sSynchroIcon = " <img id=\"synchro_$iInputId\" src=\"../images/transp-lock.png\" style=\"vertical-align:middle\"/>"; 357 $sTip = ''; 358 foreach($aReasons as $aRow) 359 { 360 $sDescription = htmlentities($aRow['description'], ENT_QUOTES, 'UTF-8'); 361 $sDescription = str_replace(array("\r\n", "\n"), "<br/>", $sDescription); 362 $sTip .= "<div class='synchro-source'>"; 363 $sTip .= "<div class='synchro-source-title'>Synchronized with {$aRow['name']}</div>"; 364 $sTip .= "<div class='synchro-source-description'>$sDescription</div>"; 365 } 366 $oPage->add_ready_script("$('#synchro_$iInputId').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );"); 367 } 368 369 // Attribute is read-only 370 $sHTMLValue = "<span id=\"field_{$iInputId}\">".$this->m_oObj->GetAsHTML($sAttCode); 371 $sHTMLValue .= '<input type="hidden" id="'.$iInputId.'" name="attr_'.$sAttCode.'" value="'.htmlentities($this->m_oObj->Get($sAttCode), ENT_QUOTES, 'UTF-8').'"/></span>'; 372 $aFieldsMap[$sAttCode] = $iInputId; 373 $aParams['this->comments('.$sAttCode.')'] = $sSynchroIcon; 374 } 375 376 if ($bEditMode && !($iFlags & OPT_ATT_READONLY)) //TODO: check the data synchro status... 377 { 378 $aParams['this->field('.$sAttCode.')'] = "<span id=\"field_{$iInputId}\">".$this->m_oObj->GetFormElementForField($oPage, $sClass, $sAttCode, $oAttDef, 379 $this->m_oObj->Get($sAttCode), 380 $this->m_oObj->GetEditValue($sAttCode), 381 $iInputId, // InputID 382 '', 383 $iFlags, 384 array('this' => $this->m_oObj) // aArgs 385 ).'</span>'; 386 $aFieldsMap[$sAttCode] = $iInputId; 387 } 388 else 389 { 390 $aParams['this->field('.$sAttCode.')'] = $this->m_oObj->GetAsHTML($sAttCode); 391 } 392 $aParams['this->'.$sAttCode] = "<table class=\"field\"><tr><td class=\"label\">".$aParams['this->label('.$sAttCode.')'].":</td><td>".$aParams['this->field('.$sAttCode.')']."</td><td>".$aParams['this->comments('.$sAttCode.')']."</td></tr></table>"; 393 } 394 } 395 } 396 397 // Renders the PlugIns used in the template 398 preg_match_all('/\\$PlugIn:([A-Za-z0-9_]+)->properties\\(\\)\\$/', $this->m_sTemplate, $aMatches); 399 $aPlugInProperties = $aMatches[1]; 400 foreach($aPlugInProperties as $sPlugInClass) 401 { 402 $oInstance = MetaModel::GetPlugins('iApplicationUIExtension', $sPlugInClass); 403 if ($oInstance != null) // Safety check... 404 { 405 $offset = $oPage->start_capture(); 406 $oInstance->OnDisplayProperties($this->m_oObj, $oPage, $bEditMode); 407 $sContent = $oPage->end_capture($offset); 408 $aParams["PlugIn:{$sPlugInClass}->properties()"]= $sContent; 409 } 410 else 411 { 412 $aParams["PlugIn:{$sPlugInClass}->properties()"]= "Missing PlugIn: $sPlugInClass"; 413 } 414 } 415 416 $offset = $oPage->start_capture(); 417 parent::Render($oPage, $aParams); 418 $sContent = $oPage->end_capture($offset); 419 // Remove empty table rows in case some attributes are hidden... 420 $sContent = preg_replace('/<tr[^>]*>\s*(<td[^>]*>\s*<\\/td>)+\s*<\\/tr>/im', '', $sContent); 421 $oPage->add($sContent); 422 return $aFieldsMap; 423 } 424} 425 426//DisplayTemplate::UnitTest(); 427?> 428