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 = "&nbsp;<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