1<?php
2// Copyright (C) 2010-2015 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 WebPage
22 *
23 * @copyright   Copyright (C) 2010-2015 Combodo SARL
24 * @license     http://opensource.org/licenses/AGPL-3.0
25 */
26
27
28/**
29 * Generic interface common to CLI and Web pages
30 */
31Interface Page
32{
33	public function output();
34
35	public function add($sText);
36
37	public function p($sText);
38
39	public function pre($sText);
40
41	public function add_comment($sText);
42
43	public function table($aConfig, $aData, $aParams = array());
44}
45
46
47/**
48 * <p>Simple helper class to ease the production of HTML pages
49 *
50 * <p>This class provide methods to add content, scripts, includes... to a web page
51 * and renders the full web page by putting the elements in the proper place & order
52 * when the output() method is called.
53 *
54 * <p>Usage:
55 * ```php
56 *    $oPage = new WebPage("Title of my page");
57 *    $oPage->p("Hello World !");
58 *    $oPage->output();
59 * ```
60 */
61class WebPage implements Page
62{
63	protected $s_title;
64	protected $s_content;
65	protected $s_deferred_content;
66	protected $a_scripts;
67	protected $a_dict_entries;
68	protected $a_dict_entries_prefixes;
69	protected $a_styles;
70	protected $a_linked_scripts;
71	protected $a_linked_stylesheets;
72	protected $a_headers;
73	protected $a_base;
74	protected $iNextId;
75	protected $iTransactionId;
76	protected $sContentType;
77	protected $sContentDisposition;
78	protected $sContentFileName;
79	protected $bTrashUnexpectedOutput;
80	protected $s_sOutputFormat;
81	protected $a_OutputOptions;
82	protected $bPrintable;
83
84	public function __construct($s_title, $bPrintable = false)
85	{
86		$this->s_title = $s_title;
87		$this->s_content = "";
88		$this->s_deferred_content = '';
89		$this->a_scripts = array();
90		$this->a_dict_entries = array();
91		$this->a_dict_entries_prefixes = array();
92		$this->a_styles = array();
93		$this->a_linked_scripts = array();
94		$this->a_linked_stylesheets = array();
95		$this->a_headers = array();
96		$this->a_base = array('href' => '', 'target' => '');
97		$this->iNextId = 0;
98		$this->iTransactionId = 0;
99		$this->sContentType = '';
100		$this->sContentDisposition = '';
101		$this->sContentFileName = '';
102		$this->bTrashUnexpectedOutput = false;
103		$this->s_OutputFormat = utils::ReadParam('output_format', 'html');
104		$this->a_OutputOptions = array();
105		$this->bPrintable = $bPrintable;
106		ob_start(); // Start capturing the output
107	}
108
109	/**
110	 * Change the title of the page after its creation
111	 */
112	public function set_title($s_title)
113	{
114		$this->s_title = $s_title;
115	}
116
117	/**
118	 * Specify a default URL and a default target for all links on a page
119	 */
120	public function set_base($s_href = '', $s_target = '')
121	{
122		$this->a_base['href'] = $s_href;
123		$this->a_base['target'] = $s_target;
124	}
125
126	/**
127	 * Add any text or HTML fragment to the body of the page
128	 */
129	public function add($s_html)
130	{
131		$this->s_content .= $s_html;
132	}
133
134	/**
135	 * Add any text or HTML fragment (identified by an ID) at the end of the body of the page
136	 * This is useful to add hidden content, DIVs or FORMs that should not
137	 * be embedded into each other.
138	 */
139	public function add_at_the_end($s_html, $sId = '')
140	{
141		$this->s_deferred_content .= $s_html;
142	}
143
144	/**
145	 * Add a paragraph to the body of the page
146	 */
147	public function p($s_html)
148	{
149		$this->add($this->GetP($s_html));
150	}
151
152	/**
153	 * Add a pre-formatted text to the body of the page
154	 */
155	public function pre($s_html)
156	{
157		$this->add('<pre>'.$s_html.'</pre>');
158	}
159
160	/**
161	 * Add a comment
162	 */
163	public function add_comment($sText)
164	{
165		$this->add('<!--'.$sText.'-->');
166	}
167
168	/**
169	 * Add a paragraph to the body of the page
170	 */
171	public function GetP($s_html)
172	{
173		return "<p>$s_html</p>\n";
174	}
175
176	/**
177	 * Adds a tabular content to the web page
178	 *
179	 * @param string[] $aConfig Configuration of the table: hash array of 'column_id' => 'Column Label'
180	 * @param string[] $aData Hash array. Data to display in the table: each row is made of 'column_id' => Data. A
181	 *     column 'pkey' is expected for each row
182	 * @param array $aParams Hash array. Extra parameters for the table.
183	 *
184	 * @return void
185	 */
186	public function table($aConfig, $aData, $aParams = array())
187	{
188		$this->add($this->GetTable($aConfig, $aData, $aParams));
189	}
190
191	public function GetTable($aConfig, $aData, $aParams = array())
192	{
193		$oAppContext = new ApplicationContext();
194
195		static $iNbTables = 0;
196		$iNbTables++;
197		$sHtml = "";
198		$sHtml .= "<table class=\"listResults\">\n";
199		$sHtml .= "<thead>\n";
200		$sHtml .= "<tr>\n";
201		foreach ($aConfig as $sName => $aDef)
202		{
203			$sHtml .= "<th title=\"".$aDef['description']."\">".$aDef['label']."</th>\n";
204		}
205		$sHtml .= "</tr>\n";
206		$sHtml .= "</thead>\n";
207		$sHtml .= "<tbody>\n";
208		foreach ($aData as $aRow)
209		{
210			$sHtml .= $this->GetTableRow($aRow, $aConfig);
211		}
212		$sHtml .= "</tbody>\n";
213		$sHtml .= "</table>\n";
214
215		return $sHtml;
216	}
217
218	public function GetTableRow($aRow, $aConfig)
219	{
220		$sHtml = '';
221		if (isset($aRow['@class'])) // Row specific class, for hilighting certain rows
222		{
223			$sHtml .= "<tr class=\"{$aRow['@class']}\">";
224		}
225		else
226		{
227			$sHtml .= "<tr>";
228		}
229		foreach ($aConfig as $sName => $aAttribs)
230		{
231			$sClass = isset($aAttribs['class']) ? 'class="'.$aAttribs['class'].'"' : '';
232			$sValue = ($aRow[$sName] === '') ? '&nbsp;' : $aRow[$sName];
233			$sHtml .= "<td $sClass>$sValue</td>";
234		}
235		$sHtml .= "</tr>";
236
237		return $sHtml;
238	}
239
240	/**
241	 * Add some Javascript to the header of the page
242	 */
243	public function add_script($s_script)
244	{
245		$this->a_scripts[] = $s_script;
246	}
247
248	/**
249	 * Add some Javascript to the header of the page
250	 */
251	public function add_ready_script($s_script)
252	{
253		// Do nothing silently... this is not supported by this type of page...
254	}
255
256	/**
257	 * Allow a dictionnary entry to be used client side with Dict.S()
258	 *
259	 * @param string $s_entryId a translation label key
260	 *
261	 * @see \WebPage::add_dict_entries()
262	 * @see utils.js
263	 */
264	public function add_dict_entry($s_entryId)
265	{
266		$this->a_dict_entries[] = $s_entryId;
267	}
268
269	/**
270	 * Add a set of dictionary entries (based on the given prefix) for the Javascript side
271	 *
272	 * @param string $s_entriesPrefix translation label prefix (eg 'UI:Button:' to add all keys beginning with this)
273	 *
274	 * @see \WebPage::add_dict_entry()
275	 * @see utils.js
276	 */
277	public function add_dict_entries($s_entriesPrefix)
278	{
279		$this->a_dict_entries_prefixes[] = $s_entriesPrefix;
280	}
281
282	protected function get_dict_signature()
283	{
284		return str_replace('_', '', Dict::GetUserLanguage()).'-'.md5(implode(',',
285					$this->a_dict_entries).'|'.implode(',', $this->a_dict_entries_prefixes));
286	}
287
288	protected function get_dict_file_content()
289	{
290		$aEntries = array();
291		foreach ($this->a_dict_entries as $sCode)
292		{
293			$aEntries[$sCode] = Dict::S($sCode);
294		}
295		foreach ($this->a_dict_entries_prefixes as $sPrefix)
296		{
297			$aEntries = array_merge($aEntries, Dict::ExportEntries($sPrefix));
298		}
299		$sJSFile = 'var aDictEntries = '.json_encode($aEntries);
300
301		return $sJSFile;
302	}
303
304
305	/**
306	 * Add some CSS definitions to the header of the page
307	 */
308	public function add_style($s_style)
309	{
310		$this->a_styles[] = $s_style;
311	}
312
313	/**
314	 * Add a script (as an include, i.e. link) to the header of the page.<br>
315	 * Handles duplicates : calling twice with the same script will add the script only once
316	 *
317	 * @param string $s_linked_script
318	 */
319	public function add_linked_script($s_linked_script)
320	{
321		$this->a_linked_scripts[$s_linked_script] = $s_linked_script;
322	}
323
324	/**
325	 * Add a CSS stylesheet (as an include, i.e. link) to the header of the page
326	 */
327	public function add_linked_stylesheet($s_linked_stylesheet, $s_condition = "")
328	{
329		$this->a_linked_stylesheets[] = array('link' => $s_linked_stylesheet, 'condition' => $s_condition);
330	}
331
332	public function add_saas($sSaasRelPath)
333	{
334		$sCssRelPath = utils::GetCSSFromSASS($sSaasRelPath);
335		$sRootUrl = utils::GetAbsoluteUrlAppRoot();
336		if ($sRootUrl === '')
337		{
338			// We're running the setup of the first install...
339			$sRootUrl = '../';
340		}
341		$sCSSUrl = $sRootUrl.$sCssRelPath;
342		$this->add_linked_stylesheet($sCSSUrl);
343	}
344
345	/**
346	 * Add some custom header to the page
347	 */
348	public function add_header($s_header)
349	{
350		$this->a_headers[] = $s_header;
351	}
352
353	/**
354	 * Add needed headers to the page so that it will no be cached
355	 */
356	public function no_cache()
357	{
358		$this->add_header("Cache-Control: no-cache, must-revalidate");  // HTTP/1.1
359		$this->add_header("Expires: Fri, 17 Jul 1970 05:00:00 GMT");    // Date in the past
360	}
361
362	/**
363	 * Build a special kind of TABLE useful for displaying the details of an object from a hash array of data
364	 */
365	public function details($aFields)
366	{
367
368		$this->add($this->GetDetails($aFields));
369	}
370
371	/**
372	 * Whether or not the page is a PDF page
373	 *
374	 * @return boolean
375	 */
376	public function is_pdf()
377	{
378		return false;
379	}
380
381	/**
382	 * Records the current state of the 'html' part of the page output
383	 *
384	 * @return mixed The current state of the 'html' output
385	 */
386	public function start_capture()
387	{
388		return strlen($this->s_content);
389	}
390
391	/**
392	 * Returns the part of the html output that occurred since the call to start_capture
393	 * and removes this part from the current html output
394	 *
395	 * @param $offset mixed The value returned by start_capture
396	 *
397	 * @return string The part of the html output that was added since the call to start_capture
398	 */
399	public function end_capture($offset)
400	{
401		$sCaptured = substr($this->s_content, $offset);
402		$this->s_content = substr($this->s_content, 0, $offset);
403
404		return $sCaptured;
405	}
406
407	/**
408	 * Build a special kind of TABLE useful for displaying the details of an object from a hash array of data
409	 */
410	public function GetDetails($aFields)
411	{
412		$sHtml = "<div class=\"details\" id='search-widget-results-outer'>\n";
413		foreach ($aFields as $aAttrib)
414		{
415			$sDataAttCode = isset($aAttrib['attcode']) ? "data-attcode=\"{$aAttrib['attcode']}\"" : '';
416			$sLayout = isset($aAttrib['layout']) ? $aAttrib['layout'] : 'small';
417			$sHtml .= "<div class=\"field_container field_{$sLayout}\" $sDataAttCode>\n";
418			$sHtml .= "<div class=\"field_label label\">{$aAttrib['label']}</div>\n";
419
420			$sHtml .= "<div class=\"field_data\">\n";
421			// By Rom, for csv import, proposed to show several values for column selection
422			if (is_array($aAttrib['value']))
423			{
424				$sHtml .= "<div class=\"field_value\">".implode("</div><div>", $aAttrib['value'])."</div>\n";
425			}
426			else
427			{
428				$sHtml .= "<div class=\"field_value\">".$aAttrib['value']."</div>\n";
429			}
430			// Checking if we should add comments & infos
431			$sComment = (isset($aAttrib['comments'])) ? $aAttrib['comments'] : '';
432			$sInfo = (isset($aAttrib['infos'])) ? $aAttrib['infos'] : '';
433			if ($sComment !== '')
434			{
435				$sHtml .= "<div class=\"field_comments\">$sComment</div>\n";
436			}
437			if ($sInfo !== '')
438			{
439				$sHtml .= "<div class=\"field_infos\">$sInfo</div>\n";
440			}
441			$sHtml .= "</div>\n";
442
443			$sHtml .= "</div>\n";
444		}
445		$sHtml .= "</div>\n";
446
447		return $sHtml;
448	}
449
450	/**
451	 * Build a set of radio buttons suitable for editing a field/attribute of an object (including its validation)
452	 *
453	 * @param $aAllowedValues hash Array of value => display_value
454	 * @param $value mixed Current value for the field/attribute
455	 * @param $iId mixed Unique Id for the input control in the page
456	 * @param $sFieldName string The name of the field, attr_<$sFieldName> will hold the value for the field
457	 * @param $bMandatory bool Whether or not the field is mandatory
458	 * @param $bVertical bool Disposition of the radio buttons vertical or horizontal
459	 * @param $sValidationField string HTML fragment holding the validation field (exclamation icon...)
460	 *
461	 * @return string The HTML fragment corresponding to the radio buttons
462	 */
463	public function GetRadioButtons(
464		$aAllowedValues, $value, $iId, $sFieldName, $bMandatory, $bVertical, $sValidationField
465	) {
466		$idx = 0;
467		$sHTMLValue = '';
468		foreach ($aAllowedValues as $key => $display_value)
469		{
470			if ((count($aAllowedValues) == 1) && ($bMandatory == 'true'))
471			{
472				// When there is only once choice, select it by default
473				$sSelected = ' checked';
474			}
475			else
476			{
477				$sSelected = ($value == $key) ? ' checked' : '';
478			}
479			$sHTMLValue .= "<input type=\"radio\" id=\"{$iId}_{$key}\" name=\"radio_$sFieldName\" onChange=\"$('#{$iId}').val(this.value).trigger('change');\" value=\"$key\"$sSelected><label class=\"radio\" for=\"{$iId}_{$key}\">&nbsp;$display_value</label>&nbsp;";
480			if ($bVertical)
481			{
482				if ($idx == 0)
483				{
484					// Validation icon at the end of the first line
485					$sHTMLValue .= "&nbsp;{$sValidationField}\n";
486				}
487				$sHTMLValue .= "<br>\n";
488			}
489			$idx++;
490		}
491		$sHTMLValue .= "<input type=\"hidden\" id=\"$iId\" name=\"$sFieldName\" value=\"$value\"/>";
492		if (!$bVertical)
493		{
494			// Validation icon at the end of the line
495			$sHTMLValue .= "&nbsp;{$sValidationField}\n";
496		}
497
498		return $sHTMLValue;
499	}
500
501	/**
502	 * Discard unexpected output data (such as PHP warnings)
503	 * This is a MUST when the Page output is DATA (download of a document, download CSV export, download ...)
504	 */
505	public function TrashUnexpectedOutput()
506	{
507		$this->bTrashUnexpectedOutput = true;
508	}
509
510	/**
511	 * Read the output buffer and deal with its contents:
512	 * - trash unexpected output if the flag has been set
513	 * - report unexpected behaviors such as the output buffering being stopped
514	 *
515	 * Possible improvement: I've noticed that several output buffers are stacked,
516	 * if they are not empty, the output will be corrupted. The solution would
517	 * consist in unstacking all of them (and concatenate the contents).
518	 */
519	protected function ob_get_clean_safe()
520	{
521		$sOutput = ob_get_contents();
522		if ($sOutput === false)
523		{
524			$sMsg = "Design/integration issue: No output buffer. Some piece of code has called ob_get_clean() or ob_end_clean() without calling ob_start()";
525			if ($this->bTrashUnexpectedOutput)
526			{
527				IssueLog::Error($sMsg);
528				$sOutput = '';
529			}
530			else
531			{
532				$sOutput = $sMsg;
533			}
534		}
535		else
536		{
537			ob_end_clean(); // on some versions of PHP doing so when the output buffering is stopped can cause a notice
538			if ($this->bTrashUnexpectedOutput)
539			{
540				if (trim($sOutput) != '')
541				{
542					if (Utils::GetConfig() && Utils::GetConfig()->Get('debug_report_spurious_chars'))
543					{
544						IssueLog::Error("Trashing unexpected output:'$sOutput'\n");
545					}
546				}
547				$sOutput = '';
548			}
549		}
550
551		return $sOutput;
552	}
553
554	/**
555	 * Outputs (via some echo) the complete HTML page by assembling all its elements
556	 */
557	public function output()
558	{
559		foreach ($this->a_headers as $s_header)
560		{
561			header($s_header);
562		}
563
564		$s_captured_output = $this->ob_get_clean_safe();
565		echo "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n";
566		echo "<html>\n";
567		echo "<head>\n";
568		echo "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n";
569		echo "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, shrink-to-fit=no\" />";
570		echo "<title>".htmlentities($this->s_title, ENT_QUOTES, 'UTF-8')."</title>\n";
571		echo $this->get_base_tag();
572
573		$this->output_dict_entries();
574
575		foreach ($this->a_linked_scripts as $s_script)
576		{
577			// Make sure that the URL to the script contains the application's version number
578			// so that the new script do NOT get reloaded from the cache when the application is upgraded
579			if (strpos($s_script, '?') === false)
580			{
581				$s_script .= "?t=".utils::GetCacheBusterTimestamp();
582			}
583			else
584			{
585				$s_script .= "&t=".utils::GetCacheBusterTimestamp();
586			}
587			echo "<script type=\"text/javascript\" src=\"$s_script\"></script>\n";
588		}
589		if (count($this->a_scripts) > 0)
590		{
591			echo "<script type=\"text/javascript\">\n";
592			foreach ($this->a_scripts as $s_script)
593			{
594				echo "$s_script\n";
595			}
596			echo "</script>\n";
597		}
598		foreach ($this->a_linked_stylesheets as $a_stylesheet)
599		{
600			if (strpos($a_stylesheet['link'], '?') === false)
601			{
602				$s_stylesheet = $a_stylesheet['link']."?t=".utils::GetCacheBusterTimestamp();
603			}
604			else
605			{
606				$s_stylesheet = $a_stylesheet['link']."&t=".utils::GetCacheBusterTimestamp();
607			}
608			if ($a_stylesheet['condition'] != "")
609			{
610				echo "<!--[if {$a_stylesheet['condition']}]>\n";
611			}
612			echo "<link rel=\"stylesheet\" type=\"text/css\" href=\"{$s_stylesheet}\" />\n";
613			if ($a_stylesheet['condition'] != "")
614			{
615				echo "<![endif]-->\n";
616			}
617		}
618
619		if (count($this->a_styles) > 0)
620		{
621			echo "<style>\n";
622			foreach ($this->a_styles as $s_style)
623			{
624				echo "$s_style\n";
625			}
626			echo "</style>\n";
627		}
628		if (class_exists('MetaModel') && MetaModel::GetConfig())
629		{
630			echo "<link rel=\"shortcut icon\" href=\"".utils::GetAbsoluteUrlAppRoot()."images/favicon.ico?t=".utils::GetCacheBusterTimestamp()."\" />\n";
631		}
632		echo "</head>\n";
633		echo "<body>\n";
634		echo self::FilterXSS($this->s_content);
635		if (trim($s_captured_output) != "")
636		{
637			echo "<div class=\"raw_output\">".self::FilterXSS($s_captured_output)."</div>\n";
638		}
639		echo '<div id="at_the_end">'.self::FilterXSS($this->s_deferred_content).'</div>';
640		echo "</body>\n";
641		echo "</html>\n";
642
643		if (class_exists('DBSearch'))
644		{
645			DBSearch::RecordQueryTrace();
646		}
647		if (class_exists('ExecutionKPI'))
648		{
649			ExecutionKPI::ReportStats();
650		}
651	}
652
653	/**
654	 * Build a series of hidden field[s] from an array
655	 */
656	public function add_input_hidden($sLabel, $aData)
657	{
658		foreach ($aData as $sKey => $sValue)
659		{
660			// Note: protection added to protect against the Notice 'array to string conversion' that appeared with PHP 5.4
661			// (this function seems unused though!)
662			if (is_scalar($sValue))
663			{
664				$this->add("<input type=\"hidden\" name=\"".$sLabel."[$sKey]\" value=\"$sValue\">");
665			}
666		}
667	}
668
669	protected function get_base_tag()
670	{
671		$sTag = '';
672		if (($this->a_base['href'] != '') || ($this->a_base['target'] != ''))
673		{
674			$sTag = '<base ';
675			if (($this->a_base['href'] != ''))
676			{
677				$sTag .= "href =\"{$this->a_base['href']}\" ";
678			}
679			if (($this->a_base['target'] != ''))
680			{
681				$sTag .= "target =\"{$this->a_base['target']}\" ";
682			}
683			$sTag .= " />\n";
684		}
685
686		return $sTag;
687	}
688
689	/**
690	 * Get an ID (for any kind of HTML tag) that is guaranteed unique in this page
691	 *
692	 * @return int The unique ID (in this page)
693	 */
694	public function GetUniqueId()
695	{
696		return $this->iNextId++;
697	}
698
699	/**
700	 * Set the content-type (mime type) for the page's content
701	 *
702	 * @param $sContentType string
703	 *
704	 * @return void
705	 */
706	public function SetContentType($sContentType)
707	{
708		$this->sContentType = $sContentType;
709	}
710
711	/**
712	 * Set the content-disposition (mime type) for the page's content
713	 *
714	 * @param $sDisposition string The disposition: 'inline' or 'attachment'
715	 * @param $sFileName string The original name of the file
716	 *
717	 * @return void
718	 */
719	public function SetContentDisposition($sDisposition, $sFileName)
720	{
721		$this->sContentDisposition = $sDisposition;
722		$this->sContentFileName = $sFileName;
723	}
724
725	/**
726	 * Set the transactionId of the current form
727	 *
728	 * @param $iTransactionId integer
729	 *
730	 * @return void
731	 */
732	public function SetTransactionId($iTransactionId)
733	{
734		$this->iTransactionId = $iTransactionId;
735	}
736
737	/**
738	 * Returns the transactionId of the current form
739	 *
740	 * @return integer The current transactionID
741	 */
742	public function GetTransactionId()
743	{
744		return $this->iTransactionId;
745	}
746
747	public static function FilterXSS($sHTML)
748	{
749		return str_ireplace('<script', '&lt;script', $sHTML);
750	}
751
752	/**
753	 * What is the currently selected output format
754	 *
755	 * @return string The selected output format: html, pdf...
756	 */
757	public function GetOutputFormat()
758	{
759		return $this->s_OutputFormat;
760	}
761
762	/**
763	 * Check whether the desired output format is possible or not
764	 *
765	 * @param string $sOutputFormat The desired output format: html, pdf...
766	 *
767	 * @return bool True if the format is Ok, false otherwise
768	 */
769	function IsOutputFormatAvailable($sOutputFormat)
770	{
771		$bResult = false;
772		switch ($sOutputFormat)
773		{
774			case 'html':
775				$bResult = true; // Always supported
776				break;
777
778			case 'pdf':
779				$bResult = @is_readable(APPROOT.'lib/MPDF/mpdf.php');
780				break;
781		}
782
783		return $bResult;
784	}
785
786	/**
787	 * Check whether the output must be printable (using print.css, for sure!)
788	 *
789	 * @return bool ...
790	 */
791	public function IsPrintableVersion()
792	{
793		return $this->bPrintable;
794	}
795
796	/**
797	 * Retrieves the value of a named output option for the given format
798	 *
799	 * @param string $sFormat The format: html or pdf
800	 * @param string $sOptionName The name of the option
801	 *
802	 * @return mixed false if the option was never set or the options's value
803	 */
804	public function GetOutputOption($sFormat, $sOptionName)
805	{
806		if (isset($this->a_OutputOptions[$sFormat][$sOptionName]))
807		{
808			return $this->a_OutputOptions[$sFormat][$sOptionName];
809		}
810
811		return false;
812	}
813
814	/**
815	 * Sets a named output option for the given format
816	 *
817	 * @param string $sFormat The format for which to set the option: html or pdf
818	 * @param string $sOptionName the name of the option
819	 * @param mixed $sValue The value of the option
820	 */
821	public function SetOutputOption($sFormat, $sOptionName, $sValue)
822	{
823		if (!isset($this->a_OutputOptions[$sFormat]))
824		{
825			$this->a_OutputOptions[$sFormat] = array($sOptionName => $sValue);
826		}
827		else
828		{
829			$this->a_OutputOptions[$sFormat][$sOptionName] = $sValue;
830		}
831	}
832
833	public function RenderPopupMenuItems($aActions, $aFavoriteActions = array())
834	{
835		$sPrevUrl = '';
836		$sHtml = '';
837		if (!$this->IsPrintableVersion())
838		{
839			foreach ($aActions as $aAction)
840			{
841				$sClass = isset($aAction['css_classes']) ? ' class="'.implode(' ', $aAction['css_classes']).'"' : '';
842				$sOnClick = isset($aAction['onclick']) ? ' onclick="'.htmlspecialchars($aAction['onclick'], ENT_QUOTES,
843						"UTF-8").'"' : '';
844				$sTarget = isset($aAction['target']) ? " target=\"{$aAction['target']}\"" : "";
845				if (empty($aAction['url']))
846				{
847					if ($sPrevUrl != '') // Don't output consecutively two separators...
848					{
849						$sHtml .= "<li>{$aAction['label']}</li>";
850					}
851					$sPrevUrl = '';
852				}
853				else
854				{
855					$sHtml .= "<li><a $sTarget href=\"{$aAction['url']}\"$sClass $sOnClick>{$aAction['label']}</a></li>";
856					$sPrevUrl = $aAction['url'];
857				}
858			}
859			$sHtml .= "</ul></li></ul></div>";
860			foreach (array_reverse($aFavoriteActions) as $aAction)
861			{
862				$sTarget = isset($aAction['target']) ? " target=\"{$aAction['target']}\"" : "";
863				$sHtml .= "<div class=\"actions_button\"><a $sTarget href='{$aAction['url']}'>{$aAction['label']}</a></div>";
864			}
865		}
866
867		return $sHtml;
868	}
869
870	protected function output_dict_entries($bReturnOutput = false)
871	{
872		if ((count($this->a_dict_entries) > 0) || (count($this->a_dict_entries_prefixes) > 0))
873		{
874			if (class_exists('Dict'))
875			{
876				// The dictionary may not be available for example during the setup...
877				// Create a specific dictionary file and load it as a JS script
878				$sSignature = $this->get_dict_signature();
879				$sJSFileName = utils::GetCachePath().$sSignature.'.js';
880				if (!file_exists($sJSFileName) && is_writable(utils::GetCachePath()))
881				{
882					file_put_contents($sJSFileName, $this->get_dict_file_content());
883				}
884				// Load the dictionary as the first javascript file, so that other JS file benefit from the translations
885				array_unshift($this->a_linked_scripts,
886					utils::GetAbsoluteUrlAppRoot().'pages/ajax.document.php?operation=dict&s='.$sSignature);
887			}
888		}
889	}
890}
891
892
893interface iTabbedPage
894{
895	public function AddTabContainer($sTabContainer, $sPrefix = '');
896
897	public function AddToTab($sTabContainer, $sTabLabel, $sHtml);
898
899	public function SetCurrentTabContainer($sTabContainer = '');
900
901	public function SetCurrentTab($sTabLabel = '');
902
903	/**
904	 * Add a tab which content will be loaded asynchronously via the supplied URL
905	 *
906	 * Limitations:
907	 * Cross site scripting is not not allowed for security reasons. Use a normal tab with an IFRAME if you want to
908	 * pull content from another server. Static content cannot be added inside such tabs.
909	 *
910	 * @param string $sTabLabel The (localised) label of the tab
911	 * @param string $sUrl The URL to load (on the same server)
912	 * @param boolean $bCache Whether or not to cache the content of the tab once it has been loaded. flase will cause
913	 *     the tab to be reloaded upon each activation.
914	 *
915	 * @since 2.0.3
916	 */
917	public function AddAjaxTab($sTabLabel, $sUrl, $bCache = true);
918
919	public function GetCurrentTab();
920
921	public function RemoveTab($sTabLabel, $sTabContainer = null);
922
923	/**
924	 * Finds the tab whose title matches a given pattern
925	 *
926	 * @return mixed The name of the tab as a string or false if not found
927	 */
928	public function FindTab($sPattern, $sTabContainer = null);
929}
930
931/**
932 * Helper class to implement JQueryUI tabs inside a page
933 */
934class TabManager
935{
936	protected $m_aTabs;
937	protected $m_sCurrentTabContainer;
938	protected $m_sCurrentTab;
939
940	public function __construct()
941	{
942		$this->m_aTabs = array();
943		$this->m_sCurrentTabContainer = '';
944		$this->m_sCurrentTab = '';
945	}
946
947	public function AddTabContainer($sTabContainer, $sPrefix = '')
948	{
949		$this->m_aTabs[$sTabContainer] = array('prefix' => $sPrefix, 'tabs' => array());
950
951		return "\$Tabs:$sTabContainer\$";
952	}
953
954	public function AddToCurrentTab($sHtml)
955	{
956		$this->AddToTab($this->m_sCurrentTabContainer, $this->m_sCurrentTab, $sHtml);
957	}
958
959	public function GetCurrentTabLength($sHtml)
960	{
961		$iLength = isset($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html']) ? strlen($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html']) : 0;
962
963		return $iLength;
964	}
965
966	/**
967	 * Truncates the given tab to the specifed length and returns the truncated part
968	 *
969	 * @param string $sTabContainer The tab container in which to truncate the tab
970	 * @param string $sTab The name/identifier of the tab to truncate
971	 * @param integer $iLength The length/offset at which to truncate the tab
972	 *
973	 * @return string The truncated part
974	 */
975	public function TruncateTab($sTabContainer, $sTab, $iLength)
976	{
977		$sResult = substr($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html'],
978			$iLength);
979		$this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html'] = substr($this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$this->m_sCurrentTab]['html'],
980			0, $iLength);
981
982		return $sResult;
983	}
984
985	public function TabExists($sTabContainer, $sTab)
986	{
987		return isset($this->m_aTabs[$sTabContainer]['tabs'][$sTab]);
988	}
989
990	public function TabsContainerCount()
991	{
992		return count($this->m_aTabs);
993	}
994
995	public function AddToTab($sTabContainer, $sTabLabel, $sHtml)
996	{
997		if (!isset($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]))
998		{
999			// Set the content of the tab
1000			$this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel] = array(
1001				'type' => 'html',
1002				'html' => $sHtml,
1003			);
1004		}
1005		else
1006		{
1007			if ($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]['type'] != 'html')
1008			{
1009				throw new Exception("Cannot add HTML content to the tab '$sTabLabel' of type '{$this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]['type']}'");
1010			}
1011			// Append to the content of the tab
1012			$this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]['html'] .= $sHtml;
1013		}
1014
1015		return ''; // Nothing to add to the page for now
1016	}
1017
1018	public function SetCurrentTabContainer($sTabContainer = '')
1019	{
1020		$sPreviousTabContainer = $this->m_sCurrentTabContainer;
1021		$this->m_sCurrentTabContainer = $sTabContainer;
1022
1023		return $sPreviousTabContainer;
1024	}
1025
1026	public function SetCurrentTab($sTabLabel = '')
1027	{
1028		$sPreviousTab = $this->m_sCurrentTab;
1029		$this->m_sCurrentTab = $sTabLabel;
1030
1031		return $sPreviousTab;
1032	}
1033
1034	/**
1035	 * Add a tab which content will be loaded asynchronously via the supplied URL
1036	 *
1037	 * Limitations:
1038	 * Cross site scripting is not not allowed for security reasons. Use a normal tab with an IFRAME if you want to
1039	 * pull content from another server. Static content cannot be added inside such tabs.
1040	 *
1041	 * @param string $sTabLabel The (localised) label of the tab
1042	 * @param string $sUrl The URL to load (on the same server)
1043	 * @param boolean $bCache Whether or not to cache the content of the tab once it has been loaded. flase will cause
1044	 *     the tab to be reloaded upon each activation.
1045	 *
1046	 * @since 2.0.3
1047	 */
1048	public function AddAjaxTab($sTabLabel, $sUrl, $bCache = true)
1049	{
1050		// Set the content of the tab
1051		$this->m_aTabs[$this->m_sCurrentTabContainer]['tabs'][$sTabLabel] = array(
1052			'type' => 'ajax',
1053			'url' => $sUrl,
1054			'cache' => $bCache,
1055		);
1056
1057		return ''; // Nothing to add to the page for now
1058	}
1059
1060
1061	public function GetCurrentTabContainer()
1062	{
1063		return $this->m_sCurrentTabContainer;
1064	}
1065
1066	public function GetCurrentTab()
1067	{
1068		return $this->m_sCurrentTab;
1069	}
1070
1071	public function RemoveTab($sTabLabel, $sTabContainer = null)
1072	{
1073		if ($sTabContainer == null)
1074		{
1075			$sTabContainer = $this->m_sCurrentTabContainer;
1076		}
1077		if (isset($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]))
1078		{
1079			// Delete the content of the tab
1080			unset($this->m_aTabs[$sTabContainer]['tabs'][$sTabLabel]);
1081
1082			// If we just removed the active tab, let's reset the active tab
1083			if (($this->m_sCurrentTabContainer == $sTabContainer) && ($this->m_sCurrentTab == $sTabLabel))
1084			{
1085				$this->m_sCurrentTab = '';
1086			}
1087		}
1088	}
1089
1090	/**
1091	 * Finds the tab whose title matches a given pattern
1092	 *
1093	 * @return mixed The actual name of the tab (as a string) or false if not found
1094	 */
1095	public function FindTab($sPattern, $sTabContainer = null)
1096	{
1097		$result = false;
1098		if ($sTabContainer == null)
1099		{
1100			$sTabContainer = $this->m_sCurrentTabContainer;
1101		}
1102		foreach ($this->m_aTabs[$sTabContainer]['tabs'] as $sTabLabel => $void)
1103		{
1104			if (preg_match($sPattern, $sTabLabel))
1105			{
1106				$result = $sTabLabel;
1107				break;
1108			}
1109		}
1110
1111		return $result;
1112	}
1113
1114	/**
1115	 * Make the given tab the active one, as if it were clicked
1116	 * DOES NOT WORK: apparently in the *old* version of jquery
1117	 * that we are using this is not supported... TO DO upgrade
1118	 * the whole jquery bundle...
1119	 */
1120	public function SelectTab($sTabContainer, $sTabLabel)
1121	{
1122		$container_index = 0;
1123		$tab_index = 0;
1124		foreach ($this->m_aTabs as $sCurrentTabContainerName => $aTabs)
1125		{
1126			if ($sTabContainer == $sCurrentTabContainerName)
1127			{
1128				foreach ($aTabs['tabs'] as $sCurrentTabLabel => $void)
1129				{
1130					if ($sCurrentTabLabel == $sTabLabel)
1131					{
1132						break;
1133					}
1134					$tab_index++;
1135				}
1136				break;
1137			}
1138			$container_index++;
1139		}
1140		$sSelector = '#tabbedContent_'.$container_index.' > ul';
1141
1142		return "window.setTimeout(\"$('$sSelector').tabs('select', $tab_index);\", 100);"; // Let the time to the tabs widget to initialize
1143	}
1144
1145	public function RenderIntoContent($sContent, WebPage $oPage)
1146	{
1147		// Render the tabs in the page (if any)
1148		foreach ($this->m_aTabs as $sTabContainerName => $aTabs)
1149		{
1150			$sTabs = '';
1151			$sPrefix = $aTabs['prefix'];
1152			$container_index = 0;
1153			if (count($aTabs['tabs']) > 0)
1154			{
1155				if ($oPage->IsPrintableVersion())
1156				{
1157					$oPage->add_ready_script(
1158						<<< EOF
1159oHiddeableChapters = {};
1160EOF
1161					);
1162					$sTabs = "<!-- tabs -->\n<div id=\"tabbedContent_{$sPrefix}{$container_index}\" class=\"light\">\n";
1163					$i = 0;
1164					foreach ($aTabs['tabs'] as $sTabName => $aTabData)
1165					{
1166						$sTabNameEsc = addslashes($sTabName);
1167						$sTabId = "tab_{$sPrefix}{$container_index}$i";
1168						switch ($aTabData['type'])
1169						{
1170							case 'ajax':
1171								$sTabHtml = '';
1172								$sUrl = $aTabData['url'];
1173								$oPage->add_ready_script(
1174									<<< EOF
1175$.post('$sUrl', {printable: '1'}, function(data){
1176	$('#$sTabId > .printable-tab-content').append(data);
1177});
1178EOF
1179								);
1180								break;
1181
1182							case 'html':
1183							default:
1184								$sTabHtml = $aTabData['html'];
1185						}
1186						$sTabs .= "<div class=\"printable-tab\" id=\"$sTabId\"><h2 class=\"printable-tab-title\">".htmlentities($sTabName,
1187								ENT_QUOTES,
1188								'UTF-8')."</h2><div class=\"printable-tab-content\">".$sTabHtml."</div></div>\n";
1189						$oPage->add_ready_script(
1190							<<< EOF
1191oHiddeableChapters['$sTabId'] = '$sTabNameEsc';
1192EOF
1193						);
1194						$i++;
1195					}
1196					$sTabs .= "</div>\n<!-- end of tabs-->\n";
1197				}
1198				else
1199				{
1200					$sTabs = "<!-- tabs -->\n<div id=\"tabbedContent_{$sPrefix}{$container_index}\" class=\"light\">\n";
1201					$sTabs .= "<ul>\n";
1202					// Display the unordered list that will be rendered as the tabs
1203					$i = 0;
1204					foreach ($aTabs['tabs'] as $sTabName => $aTabData)
1205					{
1206						switch ($aTabData['type'])
1207						{
1208							case 'ajax':
1209								$sTabs .= "<li data-cache=\"".($aTabData['cache'] ? 'true' : 'false')."\"><a href=\"{$aTabData['url']}\" class=\"tab\"><span>".htmlentities($sTabName,
1210										ENT_QUOTES, 'UTF-8')."</span></a></li>\n";
1211								break;
1212
1213							case 'html':
1214							default:
1215								$sTabs .= "<li><a href=\"#tab_{$sPrefix}{$container_index}$i\" class=\"tab\"><span>".htmlentities($sTabName,
1216										ENT_QUOTES, 'UTF-8')."</span></a></li>\n";
1217						}
1218						$i++;
1219					}
1220					$sTabs .= "</ul>\n";
1221					// Now add the content of the tabs themselves
1222					$i = 0;
1223					foreach ($aTabs['tabs'] as $sTabName => $aTabData)
1224					{
1225						switch ($aTabData['type'])
1226						{
1227							case 'ajax':
1228								// Nothing to add
1229								break;
1230
1231							case 'html':
1232							default:
1233								$sTabs .= "<div id=\"tab_{$sPrefix}{$container_index}$i\">".$aTabData['html']."</div>\n";
1234						}
1235						$i++;
1236					}
1237					$sTabs .= "</div>\n<!-- end of tabs-->\n";
1238				}
1239			}
1240			$sContent = str_replace("\$Tabs:$sTabContainerName\$", $sTabs, $sContent);
1241			$container_index++;
1242		}
1243
1244		return $sContent;
1245	}
1246}