1<?php
2/**
3 * @package     FrameworkOnFramework
4 * @subpackage  template
5 * @copyright   Copyright (C) 2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd. All rights reserved.
6 * @license     GNU General Public License version 2 or later; see LICENSE.txt
7 * @note        This file has been modified by the Joomla! Project and no longer reflects the original work of its author.
8 */
9
10// Protect from unauthorized access
11defined('FOF_INCLUDED') or die;
12
13/**
14 * A utility class to load view templates, media files and modules.
15 *
16 * @package  FrameworkOnFramework
17 * @since    1.0
18 */
19class FOFTemplateUtils
20{
21	/**
22	 * Add a CSS file to the page generated by the CMS
23	 *
24	 * @param   string  $path  A fancy path definition understood by parsePath
25	 *
26	 * @see FOFTemplateUtils::parsePath
27	 *
28	 * @return  void
29	 */
30	public static function addCSS($path)
31	{
32		$document = FOFPlatform::getInstance()->getDocument();
33
34		if ($document instanceof JDocument)
35		{
36			if (method_exists($document, 'addStyleSheet'))
37			{
38				$url = self::parsePath($path);
39				$document->addStyleSheet($url);
40			}
41		}
42	}
43
44	/**
45	 * Add a JS script file to the page generated by the CMS.
46	 *
47	 * There are three combinations of defer and async (see http://www.w3schools.com/tags/att_script_defer.asp):
48	 * * $defer false, $async true: The script is executed asynchronously with the rest of the page
49	 *   (the script will be executed while the page continues the parsing)
50	 * * $defer true, $async false: The script is executed when the page has finished parsing.
51	 * * $defer false, $async false. (default) The script is loaded and executed immediately. When it finishes
52	 *   loading the browser continues parsing the rest of the page.
53	 *
54	 * When you are using $defer = true there is no guarantee about the load order of the scripts. Whichever
55	 * script loads first will be executed first. The order they appear on the page is completely irrelevant.
56	 *
57	 * @param   string   $path   A fancy path definition understood by parsePath
58	 * @param   boolean  $defer  Adds the defer attribute, meaning that your script
59	 *                           will only load after the page has finished parsing.
60	 * @param   boolean  $async  Adds the async attribute, meaning that your script
61	 *                           will be executed while the rest of the page
62	 *                           continues parsing.
63	 *
64	 * @see FOFTemplateUtils::parsePath
65	 *
66	 * @return  void
67	 */
68	public static function addJS($path, $defer = false, $async = false)
69	{
70		$document = FOFPlatform::getInstance()->getDocument();
71
72		if ($document instanceof JDocument)
73		{
74			if (method_exists($document, 'addScript'))
75			{
76				$url = self::parsePath($path);
77				$document->addScript($url, "text/javascript", $defer, $async);
78			}
79		}
80	}
81
82	/**
83	 * Compile a LESS file into CSS and add it to the page generated by the CMS.
84	 * This method has integrated cache support. The compiled LESS files will be
85	 * written to the media/lib_fof/compiled directory of your site. If the file
86	 * cannot be written we will use the $altPath, if specified
87	 *
88	 * @param   string   $path        A fancy path definition understood by parsePath pointing to the source LESS file
89	 * @param   string   $altPath     A fancy path definition understood by parsePath pointing to a precompiled CSS file,
90	 *                                used when we can't write the generated file to the output directory
91	 * @param   boolean  $returnPath  Return the URL of the generated CSS file but do not include it. If it can't be
92	 *                                generated, false is returned and the alt files are not included
93	 *
94	 * @see FOFTemplateUtils::parsePath
95	 *
96	 * @since 2.0
97	 *
98	 * @return  mixed  True = successfully included generated CSS, False = the alternate CSS file was used, null = the source file does not exist
99	 */
100	public static function addLESS($path, $altPath = null, $returnPath = false)
101	{
102		// Does the cache directory exists and is writeable
103		static $sanityCheck = null;
104
105		// Get the local LESS file
106		$localFile = self::parsePath($path, true);
107
108		$filesystem   = FOFPlatform::getInstance()->getIntegrationObject('filesystem');
109        $platformDirs = FOFPlatform::getInstance()->getPlatformBaseDirs();
110
111		if (is_null($sanityCheck))
112		{
113			// Make sure the cache directory exists
114			if (!is_dir($platformDirs['public'] . '/media/lib_fof/compiled/'))
115			{
116				$sanityCheck = $filesystem->folderCreate($platformDirs['public'] . '/media/lib_fof/compiled/');
117			}
118			else
119			{
120				$sanityCheck = true;
121			}
122		}
123
124		// No point continuing if the source file is not there or we can't write to the cache
125
126		if (!$sanityCheck || !is_file($localFile))
127		{
128			if (!$returnPath)
129			{
130				if (is_string($altPath))
131				{
132					self::addCSS($altPath);
133				}
134				elseif (is_array($altPath))
135				{
136					foreach ($altPath as $anAltPath)
137					{
138						self::addCSS($anAltPath);
139					}
140				}
141			}
142
143			return false;
144		}
145
146		// Get the source file's unique ID
147		$id = md5(filemtime($localFile) . filectime($localFile) . $localFile);
148
149		// Get the cached file path
150		$cachedPath = $platformDirs['public'] . '/media/lib_fof/compiled/' . $id . '.css';
151
152		// Get the LESS compiler
153		$lessCompiler = new FOFLess;
154		$lessCompiler->formatterName = 'compressed';
155
156		// Should I add an alternative import path?
157		$altFiles = self::getAltPaths($path);
158
159		if (isset($altFiles['alternate']))
160		{
161			$currentLocation = realpath(dirname($localFile));
162			$normalLocation = realpath(dirname($altFiles['normal']));
163			$alternateLocation = realpath(dirname($altFiles['alternate']));
164
165			if ($currentLocation == $normalLocation)
166			{
167				$lessCompiler->importDir = array($alternateLocation, $currentLocation);
168			}
169			else
170			{
171				$lessCompiler->importDir = array($currentLocation, $normalLocation);
172			}
173		}
174
175		// Compile the LESS file
176		$lessCompiler->checkedCompile($localFile, $cachedPath);
177
178		// Add the compiled CSS to the page
179		$base_url = rtrim(FOFPlatform::getInstance()->URIbase(), '/');
180
181		if (substr($base_url, -14) == '/administrator')
182		{
183			$base_url = substr($base_url, 0, -14);
184		}
185
186		$url = $base_url . '/media/lib_fof/compiled/' . $id . '.css';
187
188		if ($returnPath)
189		{
190			return $url;
191		}
192		else
193		{
194			$document = FOFPlatform::getInstance()->getDocument();
195
196			if ($document instanceof JDocument)
197			{
198				if (method_exists($document, 'addStyleSheet'))
199				{
200					$document->addStyleSheet($url);
201				}
202			}
203			return true;
204		}
205	}
206
207	/**
208	 * Creates a SEF compatible sort header. Standard Joomla function will add a href="#" tag, so with SEF
209	 * enabled, the browser will follow the fake link instead of processing the onSubmit event; so we
210	 * need a fix.
211	 *
212	 * @param   string          $text   Header text
213	 * @param   string          $field  Field used for sorting
214	 * @param   FOFUtilsObject  $list   Object holding the direction and the ordering field
215	 *
216	 * @return  string  HTML code for sorting
217	 */
218	public static function sefSort($text, $field, $list)
219	{
220		$sort = JHTML::_('grid.sort', JText::_(strtoupper($text)) . '&nbsp;', $field, $list->order_Dir, $list->order);
221
222		return str_replace('href="#"', 'href="javascript:void(0);"', $sort);
223	}
224
225	/**
226	 * Parse a fancy path definition into a path relative to the site's root,
227	 * respecting template overrides, suitable for inclusion of media files.
228	 * For example, media://com_foobar/css/test.css is parsed into
229	 * media/com_foobar/css/test.css if no override is found, or
230	 * templates/mytemplate/media/com_foobar/css/test.css if the current
231	 * template is called mytemplate and there's a media override for it.
232	 *
233	 * The valid protocols are:
234	 * media://		The media directory or a media override
235	 * admin://		Path relative to administrator directory (no overrides)
236	 * site://		Path relative to site's root (no overrides)
237	 *
238	 * @param   string   $path       Fancy path
239	 * @param   boolean  $localFile  When true, it returns the local path, not the URL
240	 *
241	 * @return  string  Parsed path
242	 */
243	public static function parsePath($path, $localFile = false)
244	{
245        $platformDirs = FOFPlatform::getInstance()->getPlatformBaseDirs();
246
247		if ($localFile)
248		{
249			$url = rtrim($platformDirs['root'], DIRECTORY_SEPARATOR) . '/';
250		}
251		else
252		{
253			$url = FOFPlatform::getInstance()->URIroot();
254		}
255
256		$altPaths = self::getAltPaths($path);
257		$filePath = $altPaths['normal'];
258
259		// If JDEBUG is enabled, prefer that path, else prefer an alternate path if present
260		if (defined('JDEBUG') && JDEBUG && isset($altPaths['debug']))
261		{
262			if (file_exists($platformDirs['public'] . '/' . $altPaths['debug']))
263			{
264				$filePath = $altPaths['debug'];
265			}
266		}
267		elseif (isset($altPaths['alternate']))
268		{
269			if (file_exists($platformDirs['public'] . '/' . $altPaths['alternate']))
270			{
271				$filePath = $altPaths['alternate'];
272			}
273		}
274
275		$url .= $filePath;
276
277		return $url;
278	}
279
280	/**
281	 * Parse a fancy path definition into a path relative to the site's root.
282	 * It returns both the normal and alternative (template media override) path.
283	 * For example, media://com_foobar/css/test.css is parsed into
284	 * array(
285	 *   'normal' => 'media/com_foobar/css/test.css',
286	 *   'alternate' => 'templates/mytemplate/media/com_foobar/css//test.css'
287	 * );
288	 *
289	 * The valid protocols are:
290	 * media://		The media directory or a media override
291	 * admin://		Path relative to administrator directory (no alternate)
292	 * site://		Path relative to site's root (no alternate)
293	 *
294	 * @param   string  $path  Fancy path
295	 *
296	 * @return  array  Array of normal and alternate parsed path
297	 */
298	public static function getAltPaths($path)
299	{
300		$protoAndPath = explode('://', $path, 2);
301
302		if (count($protoAndPath) < 2)
303		{
304			$protocol = 'media';
305		}
306		else
307		{
308			$protocol = $protoAndPath[0];
309			$path = $protoAndPath[1];
310		}
311
312		$path = ltrim($path, '/' . DIRECTORY_SEPARATOR);
313
314		switch ($protocol)
315		{
316			case 'media':
317				// Do we have a media override in the template?
318				$pathAndParams = explode('?', $path, 2);
319
320				$ret = array(
321					'normal'	 => 'media/' . $pathAndParams[0],
322					'alternate'	 => FOFPlatform::getInstance()->getTemplateOverridePath('media:/' . $pathAndParams[0], false),
323				);
324				break;
325
326			case 'admin':
327				$ret = array(
328					'normal' => 'administrator/' . $path
329				);
330				break;
331
332			default:
333			case 'site':
334				$ret = array(
335					'normal' => $path
336				);
337				break;
338		}
339
340		// For CSS and JS files, add a debug path if the supplied file is compressed
341		$filesystem = FOFPlatform::getInstance()->getIntegrationObject('filesystem');
342		$ext        = $filesystem->getExt($ret['normal']);
343
344		if (in_array($ext, array('css', 'js')))
345		{
346			$file = basename($filesystem->stripExt($ret['normal']));
347
348			/*
349			 * Detect if we received a file in the format name.min.ext
350			 * If so, strip the .min part out, otherwise append -uncompressed
351			 */
352
353			if (strlen($file) > 4 && strrpos($file, '.min', '-4'))
354			{
355				$position = strrpos($file, '.min', '-4');
356				$filename = str_replace('.min', '.', $file, $position) . $ext;
357			}
358			else
359			{
360				$filename = $file . '-uncompressed.' . $ext;
361			}
362
363			// Clone the $ret array so we can manipulate the 'normal' path a bit
364			$t1 = (object) $ret;
365			$temp = clone $t1;
366			unset($t1);
367			$temp = (array)$temp;
368			$normalPath = explode('/', $temp['normal']);
369			array_pop($normalPath);
370			$normalPath[] = $filename;
371			$ret['debug'] = implode('/', $normalPath);
372		}
373
374		return $ret;
375	}
376
377	/**
378	 * Returns the contents of a module position
379	 *
380	 * @param   string  $position  The position name, e.g. "position-1"
381	 * @param   int     $style     Rendering style; please refer to Joomla!'s code for more information
382	 *
383	 * @return  string  The contents of the module position
384	 */
385	public static function loadPosition($position, $style = -2)
386	{
387		$document = FOFPlatform::getInstance()->getDocument();
388
389		if (!($document instanceof JDocument))
390		{
391			return '';
392		}
393
394		if (!method_exists($document, 'loadRenderer'))
395		{
396			return '';
397		}
398
399		try
400		{
401			$renderer = $document->loadRenderer('module');
402		}
403		catch (Exception $exc)
404		{
405			return '';
406		}
407
408		$params = array('style' => $style);
409
410		$contents = '';
411
412		foreach (JModuleHelper::getModules($position) as $mod)
413		{
414			$contents .= $renderer->render($mod, $params);
415		}
416
417		return $contents;
418	}
419
420	/**
421	 * Merges the current url with new or changed parameters.
422	 *
423	 * This method merges the route string with the url parameters defined
424	 * in current url. The parameters defined in current url, but not given
425	 * in route string, will automatically reused in the resulting url.
426	 * But only these following parameters will be reused:
427	 *
428	 * option, view, layout, format
429	 *
430	 * Example:
431	 *
432	 * Assuming that current url is:
433	 * http://fobar.com/index.php?option=com_foo&view=cpanel
434	 *
435	 * <code>
436	 * <?php echo FOFTemplateutils::route('view=categories&layout=tree'); ?>
437	 * </code>
438	 *
439	 * Result:
440	 * http://fobar.com/index.php?option=com_foo&view=categories&layout=tree
441	 *
442	 * @param   string  $route  The parameters string
443	 *
444	 * @return  string  The human readable, complete url
445	 */
446	public static function route($route = '')
447	{
448		$route = trim($route);
449
450		// Special cases
451
452		if ($route == 'index.php' || $route == 'index.php?')
453		{
454			$result = $route;
455		}
456		elseif (substr($route, 0, 1) == '&')
457		{
458			$url = JURI::getInstance();
459			$vars = array();
460			parse_str($route, $vars);
461
462			$url->setQuery(array_merge($url->getQuery(true), $vars));
463
464			$result = 'index.php?' . $url->getQuery();
465		}
466		else
467		{
468			$url = JURI::getInstance();
469			$props = $url->getQuery(true);
470
471			// Strip 'index.php?'
472			if (substr($route, 0, 10) == 'index.php?')
473			{
474				$route = substr($route, 10);
475			}
476
477			// Parse route
478			$parts = array();
479			parse_str($route, $parts);
480			$result = array();
481
482			// Check to see if there is component information in the route if not add it
483
484			if (!isset($parts['option']) && isset($props['option']))
485			{
486				$result[] = 'option=' . $props['option'];
487			}
488
489			// Add the layout information to the route only if it's not 'default'
490
491			if (!isset($parts['view']) && isset($props['view']))
492			{
493				$result[] = 'view=' . $props['view'];
494
495				if (!isset($parts['layout']) && isset($props['layout']))
496				{
497					$result[] = 'layout=' . $props['layout'];
498				}
499			}
500
501			// Add the format information to the URL only if it's not 'html'
502
503			if (!isset($parts['format']) && isset($props['format']) && $props['format'] != 'html')
504			{
505				$result[] = 'format=' . $props['format'];
506			}
507
508			// Reconstruct the route
509
510			if (!empty($route))
511			{
512				$result[] = $route;
513			}
514
515			$result = 'index.php?' . implode('&', $result);
516		}
517
518		return JRoute::_($result);
519	}
520}
521