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