PageRenderTime 55ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/plugins/system/jch_optimize/jch_optimize.php

https://bitbucket.org/organicdevelopment/joomla-2.5
PHP | 677 lines | 436 code | 70 blank | 171 comment | 82 complexity | 0174009d3ec2b5412227c769b9b89af2 MD5 | raw file
Possible License(s): LGPL-3.0, GPL-2.0, MIT, BSD-3-Clause, LGPL-2.1
  1. <?php
  2. /**
  3. * JCH Optimize - Joomla! plugin to aggregate and minify external resources for
  4. * optmized downloads
  5. * @author Samuel Marshall <sdmarshall73@gmail.com>
  6. * @copyright Copyright (c) 2010 Samuel Marshall
  7. * @license GNU/GPLv3, See LICENSE file
  8. * This program is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License as published by
  10. * the Free Software Foundation, either version 3 of the License, or
  11. * (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * If LICENSE file missing, see <http://www.gnu.org/licenses/>.
  19. *
  20. * This plugin, inspired by CssJsCompress <http://www.joomlatags.org>, was
  21. * created in March 2010 and includes other copyrighted works. See individual
  22. * files for details.
  23. */
  24. /**
  25. * Modified for Joomla 1.6 by Branislav Maksin - www.maksin.ms
  26. */
  27. // no direct access
  28. defined('_JEXEC') or die('Restricted access');
  29. jimport('joomla.plugin.plugin');
  30. require_once ( dirname(__FILE__) . DS . 'cache' . DS . 'CSS.php' );
  31. require_once ( dirname(__FILE__) . DS . 'cache' . DS . 'HTML.php' );
  32. require_once ( dirname(__FILE__) . DS . 'cache' . DS . 'jsmin.php' );
  33. class plgSystemJCH_Optimize extends JPlugin {
  34. /** @var object Holds reference to JCACHE object */
  35. protected $oCache = '';
  36. /** @var string Head section of html */
  37. protected $sHead = '';
  38. /** @var string Html of full page */
  39. protected $sBody = '';
  40. /** @var array Array of css or js urls taken from head */
  41. protected $aLinks = array();
  42. /** @var array Array of css media types */
  43. protected $aMedia = array();
  44. /** @var array Array of ordered js files */
  45. protected $aOrder = array();
  46. /** @var integer First index for $alinks */
  47. protected $iCnt = 2000;
  48. /** @var array Array of arguments to be used in callback functions */
  49. protected $aCallbackArgs = array();
  50. /** @var object Holds reference to JURI object */
  51. protected $oUri = '';
  52. /**
  53. * Triggered by onAfterRender event; will remove all urls of css and js
  54. * files in head tags (except those excluded) and aggregate in single css or
  55. * js file
  56. *
  57. */
  58. public function onAfterRender() {
  59. $iCss = $this->params->get('css', 1);
  60. $iJavaScript = $this->params->get('javascript', 1);
  61. $iHtmlMin = $this->params->get('html_minify', 0);
  62. $sExComp = $this->params->get('excludeComponents', '');
  63. $iExExtensions = $this->params->get('excludeAllExtensions', '1');
  64. if (!$iCss && !$iJavaScript && !$iHtmlMin) {
  65. return true;
  66. }
  67. $oApplication = JFactory::getApplication();
  68. if ($oApplication->getName() != 'site') {
  69. return true;
  70. }
  71. $oDocument = JFactory::getDocument();
  72. $sDocType = $oDocument->getType();
  73. $sLnEnd = $oDocument->_getLineEnd();
  74. $sTab = $oDocument->_getTab();
  75. if ($sDocType != 'html') {
  76. return;
  77. }
  78. $sExCompRegex = '';
  79. $sExExtensionsRegex = '';
  80. if ($iExExtensions) {
  81. $sExExtensionsRegex = '|(?:/components/)|(?:/modules/)|(?:/plugins/)';
  82. }
  83. if (isset($sExComp) && $sExComp) {
  84. $aComponents = $this->getArray($sExComp);
  85. foreach ($aComponents as $sComponent) {
  86. $sExCompRegex .= '|(?:/' . $sComponent . '/)';
  87. }
  88. }
  89. $this->sBody = JResponse::getBody();
  90. $sHeadRegex = '~<head>.*?</head>~msi';
  91. preg_match($sHeadRegex, $this->sBody, $aHeadMatches);
  92. $this->sHead = $aHeadMatches[0];
  93. $this->oUri = clone JURI::getInstance();
  94. $sCacheGroup = 'plg_jch_optimize';
  95. $this->oCache = JFactory::getCache($sCacheGroup, 'callback', 'file');
  96. $this->oCache->setCaching(1);
  97. $this->oCache->setLifetime((int) $this->params->get('lifetime', '30') * 24 * 60 * 60);
  98. if ($iJavaScript) {
  99. $this->aOrder = $this->getArray($this->params->get('customOrder', ''));
  100. $this->aCallbackArgs['excludes'] = $this->getArray($this->params->get('excludeJs', ''));
  101. unset($this->aLinks);
  102. $sType = 'js';
  103. $this->excludeIf($sType);
  104. $iDefer = $this->params->get('defer_js', 0);
  105. $iJsPosition = $this->params->get('bottom_js', 1);
  106. $this->aCallbackArgs['type'] = $sType;
  107. $this->aCallbackArgs['counter'] = 0;
  108. $sJsRegexStart = '~<script
  109. (?=[^>]+?src\s?=\s?["\']([^"\']+?/([^/]+\.js)(?:\?[^"\']*?)?)["\'])
  110. (?=[^>]+?type\s?=\s?["\']text/javascript["\'])
  111. (?:(?!(?:\.php)|(?:/editors/)';
  112. $sJsRegexEnd = ')[^>])+>(?:(?:[^<]*?)</script>)?~ix';
  113. $sJsRegex = $sJsRegexStart . $sExExtensionsRegex . $sExCompRegex . $sJsRegexEnd;
  114. $this->sHead = preg_replace_callback($sJsRegex, array($this, 'replaceScripts'), $this->sHead);
  115. $sLink = '<script type="text/javascript" src="URL"';
  116. $sLink .= $iDefer ? ' defer="defer"' : '';
  117. $sLink .= '></script>';
  118. $sNewJsLink = $iJsPosition == 1 ? $sTab . $sLink . $sLnEnd . '</body>' : $sLink;
  119. $iCnt = $this->aCallbackArgs['counter'];
  120. if (!empty($this->aLinks)) {
  121. $iJsId = $this->processLink($sType, $sCacheGroup, $sNewJsLink, $sLnEnd, $iCnt);
  122. }
  123. }
  124. if ($iCss || $this->params->get('csg_enable', 0)) {
  125. $this->aCallbackArgs['excludes'] = $this->getArray($this->params->get('excludeCss', ''));
  126. unset($this->aLinks);
  127. $sType = 'css';
  128. $this->excludeIf($sType);
  129. $this->aCallbackArgs['type'] = $sType;
  130. $this->aCallbackArgs['counter'] = 0;
  131. $sCssRegexStart = '~<link
  132. (?=[^>]+?href\s?=\s?["\']([^"\']+?/([^/]+\.css)(?:\?[^"\']*?)?)["\'])
  133. (?=[^>]+?type\s?=\s?["\']text/css["\'])
  134. (?:(?!(?:\.php)|(?:title\s?=\s?["\'])';
  135. $sCssRegexEnd = ')[^>])+>(?:(?:[^<]*?)</[^>]+>)?~ix';
  136. $sCssRegex = $sCssRegexStart . $sExExtensionsRegex . $sExCompRegex . $sCssRegexEnd;
  137. $this->sHead = preg_replace_callback($sCssRegex, array($this, 'replaceScripts'), $this->sHead);
  138. //print_r($this->aLinks);
  139. $sNewCssLink = '</title>' . $sLnEnd . $sTab . '<link rel="stylesheet" type="text/css" ';
  140. $sNewCssLink .= 'href="URL"/>';
  141. if (!empty($this->aLinks)) {
  142. $iCssId = $this->processLink('css', $sCacheGroup, $sNewCssLink, $sLnEnd);
  143. }
  144. }
  145. $sBody = preg_replace($sHeadRegex, $this->sHead, $this->sBody);
  146. $aOptions = array();
  147. if ($this->params->get('css_minify', 0)) {
  148. $aOptions['cssMinifier'] = array('Minify_CSS', 'process');
  149. }
  150. if ($this->params->get('js_minify', 0)) {
  151. $aOptions['jsMinifier'] = array('JSMin', 'minify');
  152. }
  153. if ($iHtmlMin) {
  154. $sBody = Minify_HTML::minify($sBody, $aOptions);
  155. }
  156. JResponse::setBody($sBody);
  157. }
  158. /**
  159. * Add js and css urls in conditional tags to excludes list
  160. *
  161. * @param string $sType css or js
  162. */
  163. protected function excludeIf($sType) {
  164. if (preg_match_all('~<\!--.*?-->~is', $this->sHead, $aMatches)) {
  165. foreach ($aMatches[0] as $sMatch) {
  166. preg_match_all('~.*?/([^/]+\.' . $sType . ').*?~', $sMatch, $aExcludesMatches);
  167. $this->aCallbackArgs['excludes'][] = @$aExcludesMatches[1][0];
  168. }
  169. }
  170. }
  171. /**
  172. * Use generated id to cache aggregated file
  173. *
  174. * @param string $sType css or js
  175. * @param string $sCacheGroup Name of cache group
  176. * @param string $sLink Url for aggregated file
  177. * @param string $sLnEnd Line end
  178. */
  179. protected function processLink($sType, $sCacheGroup, $sLink, $sLnEnd, $iCnt='') {
  180. if ($sType == 'js') {
  181. $iMinify = $this->params->get('js_minify', 0);
  182. } elseif ($sType == 'css') {
  183. $iMinify = $this->params->get('css_minify', 0);
  184. }
  185. $iImport = $this->params->get('import', 0);
  186. $iSprite = $this->params->get('csg_enable', 0);
  187. $sId = md5(serialize(implode('', $this->aLinks) . $this->params));
  188. $aArgs = array($this->aLinks, $sType, $sLnEnd, $iMinify, $iImport, $iSprite, $sId);
  189. $aFunction = array($this, 'getContents');
  190. $bCached = $this->loadCache($aFunction, $aArgs, $sId);
  191. $sFileName = $this->getFilename($sId, $sCacheGroup);
  192. $iTime = (int) $this->params->get('lifetime', '30');
  193. if ($bCached) {
  194. $sUrl = $this->buildUrl($sFileName, $sType, $iTime, $this->isGZ());
  195. $sNewLink = str_replace('URL', $sUrl, $sLink);
  196. $this->replaceLink($sNewLink, $sType, $iCnt);
  197. }
  198. //print_r($sFile);
  199. return $sId;
  200. }
  201. /**
  202. * Returns url of aggregated file
  203. *
  204. * @param string $sFile Aggregated file name
  205. * @param string $sType css or js
  206. * @param mixed $bGz True (or 1) if gzip set and enabled
  207. * @param number $sTime Expire header time
  208. * @return string Url of aggregated file
  209. */
  210. protected function buildUrl($sFile, $sType, $iTime, $bGz=false) {
  211. $sPath = JURI::base(true) . '/plugins/system/jch_optimize/cache/';
  212. if ($this->params->get('htaccess', 0)) {
  213. $sUrl = $sPath . ($bGz ? 'gz/' : 'nz/') . $iTime . '/' . $sFile . '.' . $sType;
  214. } else {
  215. $oUri = $this->oUri;
  216. $oUri->setPath($sPath . 'jscss.php');
  217. $aVar = array();
  218. $aVar['f'] = $sFile;
  219. $aVar['type'] = $sType;
  220. if ($bGz) {
  221. $aVar['gz'] = 'gz';
  222. }
  223. $aVar['d'] = $iTime;
  224. $oUri->setQuery($aVar);
  225. $sUrl = htmlentities($oUri->toString(array('path', 'query')));
  226. }
  227. return ($sUrl);
  228. }
  229. /**
  230. * Insert url of aggregated file in html
  231. *
  232. * @param string $sNewLink Url of aggregated file
  233. */
  234. protected function replaceLink($sNewLink, $sType, $iCnt='') {
  235. if ($sType == 'css') {
  236. $this->sHead = str_replace('</title>', $sNewLink, $this->sHead);
  237. }
  238. if ($sType == 'js') {
  239. switch ($this->params->get('bottom_js', 1)) {
  240. case 0: //First found javascript tag
  241. $this->sHead = preg_replace('~<JCH_SCRIPT>~', $sNewLink, $this->sHead, 1);
  242. $this->sHead = str_replace('<JCH_SCRIPT>', '', $this->sHead);
  243. break;
  244. case 2: //Last found javascript tag
  245. $iCnt--;
  246. $this->sHead = preg_replace('~<JCH_SCRIPT>~', '', $this->sHead, $iCnt);
  247. $this->sHead = str_replace('<JCH_SCRIPT>', $sNewLink, $this->sHead);
  248. break;
  249. case 1: //Bottom of page
  250. $this->sHead = str_replace('<JCH_SCRIPT>', '', $this->sHead);
  251. $this->sBody = str_replace('</body>', $sNewLink, $this->sBody);
  252. break;
  253. default:
  254. break;
  255. }
  256. }
  257. }
  258. /**
  259. * Create and cache aggregated file if it doesn't exists, file will have
  260. * lifetime set in global configurations.
  261. *
  262. * @param array $aFunction Name of function used to aggregate files
  263. * @param array $aArgs Arguments used by function above
  264. * @param string $sId Generated id to identify cached file
  265. * @return boolean True on success
  266. */
  267. protected function loadCache($aFunction, $aArgs, $sId) {
  268. unset($bCached);
  269. $bCached = $this->oCache->get($aFunction, $aArgs, $sId);
  270. if (isset($bCached)) {
  271. return true;
  272. }
  273. return false;
  274. }
  275. /**
  276. * Gets name of aggregated file
  277. *
  278. * @param string $sId Id of cached file
  279. * @param string $sCacheGroup Name of cache group
  280. * @return string Cache file name
  281. */
  282. protected function getFilename($sId, $sCacheGroup) {
  283. $oStorage = $this->oCache->_getStorage();
  284. $sName1 = md5($oStorage->_application . '-' . $sId . '-' . $oStorage->_language);
  285. $sName = $oStorage->_hash . '-cache-' . $sCacheGroup . '-' . $sName1;
  286. return $sName;
  287. }
  288. /**
  289. * Check if gzip is set or enabled
  290. *
  291. * @return boolean True if gzip parameter set and server is enabled
  292. */
  293. public function isGZ() {
  294. $iIsGz = $this->params->get('gzip', 0);
  295. if ($iIsGz) {
  296. if (!isset($_SERVER['HTTP_ACCEPT_ENCODING'])) {
  297. return false;
  298. } elseif (!extension_loaded('zlib') || ini_get('zlib.output_compression')) {
  299. return false;
  300. } elseif (false !== strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip')) {
  301. return true;
  302. }
  303. } else {
  304. return false;
  305. }
  306. }
  307. /**
  308. * Callback function used to remove urls of css and js files in head tags
  309. *
  310. * @param array $aMatches Array of all matches
  311. * @return string Returns the url if excluded, empty string otherwise
  312. */
  313. protected function replaceScripts($aMatches) {
  314. $sUrl = $aMatches[1];
  315. if ($this->oUri->isInternal($sUrl)) {
  316. $sFile = $aMatches[2];
  317. $aExcludes = array();
  318. if ($this->aCallbackArgs) {
  319. $aExcludes = $this->aCallbackArgs['excludes'];
  320. }
  321. if (in_array($sFile, $aExcludes)) {
  322. return $aMatches[0];
  323. } else {
  324. if (in_array($sFile, $this->aOrder)) {
  325. $sKey = array_search($sFile, $this->aOrder);
  326. $this->aLinks[$sKey] = $sUrl;
  327. } else {
  328. $this->aLinks[$this->iCnt++] = $sUrl;
  329. }
  330. $iResult = preg_match('~media\s?=\s?["\']([^"\']+?)["\']~i', $aMatches[0], $aMediaTypes);
  331. if ($iResult > 0) {
  332. $this->aMedia[$sUrl] = $aMediaTypes[1];
  333. }
  334. if ($this->aCallbackArgs['type'] == 'js') {
  335. $this->aCallbackArgs['counter']++;
  336. return '<JCH_SCRIPT>';
  337. } else {
  338. return '';
  339. }
  340. }
  341. }
  342. return $aMatches[0];
  343. }
  344. /**
  345. * Prepare css or js files for aggregation and minification
  346. *
  347. * @param array $aUrlArray Array of urls of css or js files for aggregation
  348. * @param string $sType css or js
  349. * @param string $sLnEnd line end
  350. * @return string Aggregated (and possibly minified) contents of files
  351. */
  352. public function getContents($aUrlArray, $sType, $sLnEnd, $iMinify, $iImport, $iSprite, $sId) {
  353. ksort($aUrlArray);
  354. $sContents = $this->combineFiles($aUrlArray, $sLnEnd, $sType);
  355. if ($sType == 'css') {
  356. if ($iImport) {
  357. $sContents = $this->replaceImports($sContents, $sLnEnd);
  358. }
  359. $sContents = trim($this->sortImports($sContents, $sLnEnd));
  360. if ($iSprite) {
  361. $sContents = $this->generateSprite($sContents, $sLnEnd);
  362. }
  363. if ($iMinify) {
  364. $sContents = Minify_CSS::process($sContents);
  365. }
  366. }
  367. if ($iMinify && $sType == 'js') {
  368. try{
  369. $sContents = JSMin::minify($sContents);
  370. }catch (JSMinException $e){
  371. //Need to test how this handles
  372. JError::raiseWarning(101, $e->getMessage());
  373. //return false;
  374. }
  375. }
  376. $sContents = str_replace('LINE_END', $sLnEnd, $sContents);
  377. return $sContents;
  378. }
  379. /**
  380. * Aggregate contents of CSS and JS files
  381. *
  382. * @param array $aUrlArray Array of links of files
  383. * @param string $sType CSS or js
  384. * @return string Aggregarted contents
  385. */
  386. protected function combineFiles($aUrlArray, $sLnEnd, $sType='') {
  387. $iJQueryNoConflict = $this->params->get('jqueryNoConflict', '');
  388. $sJQueryFile = $this->params->get('jquery', '');
  389. $sContents = '';
  390. foreach ($aUrlArray as $sUrl) {
  391. $sPath = $this->getFilepath($sUrl);
  392. $aPath = explode(DS, $sPath);
  393. $sFile = end($aPath);
  394. preg_match('~.*\.[A-Za-z]{2,3}~', $sPath, $aMatches);
  395. if (file_exists($aMatches[0])) {
  396. if (preg_match('~.*?\.php~', $sPath)) {
  397. $sContent = $this->evalCss($sPath, $sUrl);
  398. } else {
  399. $sContent = file_get_contents($sPath);
  400. }
  401. if ($iJQueryNoConflict && $sJQueryFile == $sFile && $sType == 'js') {
  402. $sContent.="\n jQuery.noConflict();\n";
  403. }
  404. if ($sType == 'css') {
  405. unset($this->aCallbackArgs['css_url']);
  406. $this->aCallbackArgs['css_url'] = $sUrl;
  407. $sContent = preg_replace('~@import\s?[\'"]([^\'"]+?)[\'"];~', '@import url("$1");', $sContent);
  408. $sContent = preg_replace_callback('~url\s?\([\'"]?(?![a-z]+:|/+)([^\'")]+)[\'"]?\)~i', array($this, 'correctUrl'), $sContent);
  409. if (@$this->aMedia[$sUrl]) {
  410. $sContent = '@media ' . $this->aMedia[$sUrl] . ' {' . $sLnEnd . $sContent . $sLnEnd . ' }';
  411. }
  412. }
  413. $sContents .= $sContent . 'LINE_END';
  414. }
  415. }
  416. return $sContents;
  417. }
  418. /**
  419. * Get local path of file from the url
  420. *
  421. * @param string $sUrl Url of file
  422. * @return string File path
  423. */
  424. static public function getFilepath($sUrl) {
  425. $sUriBase = str_replace('~', '\~', JURI::base());
  426. $sUriPath = str_replace('~', '\~', JURI::base(true));
  427. $sUrl = preg_replace(array('~^' . $sUriBase . '~', '~^' . $sUriPath . '/~', '~\?.*?$~'), '', $sUrl);
  428. $sUrl = str_replace('/', DS, $sUrl);
  429. $sFilePath = JPATH_ROOT . DS . $sUrl;
  430. return $sFilePath;
  431. }
  432. /**
  433. * Splits a string into an array using any regular delimiter or whitespace
  434. *
  435. * @param string $sString Delimited string of components
  436. * @return array An array of the components
  437. */
  438. static public function getArray($sString) {
  439. return $aArray = preg_split('~[\s,;:]+~', $sString);
  440. }
  441. /**
  442. * Returns url of current host
  443. *
  444. * @return string Url of current host
  445. */
  446. public function getHost() {
  447. $sWww = $this->oUri->toString(array('scheme', 'user', 'pass', 'host', 'port'));
  448. return $sWww;
  449. }
  450. /**
  451. * Callback function to correct urls in aggregated css files
  452. *
  453. * @param array $aMatches Array of all matches
  454. * @return string Correct url of images from aggregated css file
  455. */
  456. protected function correctUrl($aMatches) {
  457. if (!preg_match('~^(/|http)~', $aMatches[1])) {
  458. $sCssRootPath = preg_replace('~/[^/]+\.css~', '/', $this->aCallbackArgs['css_url']);
  459. $sImagePath = $sCssRootPath . $aMatches[1];
  460. $oUri = clone JURI::getInstance($sImagePath);
  461. $oUri->setPath($oUri->getPath());
  462. $sCleanPath = $oUri->getPath();
  463. $sCleanPath = preg_replace('~^(?!/)~', '/', $sCleanPath);
  464. return 'url(' . $sCleanPath . ')';
  465. } else {
  466. return $aMatches[0];
  467. }
  468. }
  469. /**
  470. * Replace all @import with internal urls with the file contents
  471. *
  472. * @param string $sCss Combined css file
  473. * @return string CSS file with contents from import prepended.
  474. */
  475. protected function replaceImports($sCss, $sLnEnd) {
  476. unset($this->aLinks);
  477. $this->aCallbackArgs = array();
  478. // print_r($sCss);
  479. $sImportCss = '';
  480. $sCss = preg_replace_callback('~@import.*?url\([\'"]?(.*?/([^/]+\.css))[\'"]?\);~i', array($this, 'replaceScripts'), $sCss);
  481. if (!empty($this->aLinks)) {
  482. $sImportCss = $this->combineFiles($this->aLinks, $sLnEnd, 'css');
  483. }
  484. return $sImportCss . $sCss;
  485. }
  486. /**
  487. * Sorts @import and @charset as according to w3C <http://www.w3.org/TR/CSS2/cascade.html> Section 6.3
  488. *
  489. * @param string $sCss Combined css
  490. * @param string $sLnEnd Line ending
  491. * @return string CSS with @import and @charset properly sorted
  492. */
  493. protected function sortImports($sCss, $sLnEnd) {
  494. $sImportRegex = '~@import[^;]+?;~i';
  495. $sCharsetRegex = '~@charset[^;]+?;~i';
  496. $n = preg_match_all($sImportRegex, $sCss, $aImportMatches);
  497. $aExcludesMatches = preg_match($sCharsetRegex, $sCss, $charset);
  498. $sImports = '';
  499. foreach ($aImportMatches[0] as $sImport) {
  500. $sImports .= $sImport . $sLnEnd;
  501. }
  502. preg_replace(array($sImportRegex, $sCharsetRegex), '', $sCss);
  503. $charset = $aExcludesMatches <= 0 ? '' : $charset[0];
  504. return $sCss = trim($charset . $sLnEnd . $sImports . $sCss);
  505. }
  506. /**
  507. * //Not implemented yet
  508. * @param <type> $sPath
  509. * @param <type> $sUrl
  510. */
  511. protected function evalCss($sPath, $sUrl) {
  512. $oUri = JURI::getInstance($sUrl);
  513. $sQuery = $oUri->getQuery(true);
  514. eval('?>' . $sContent = file_get_contents($sPath) . '<?');
  515. }
  516. /**
  517. * Grabs background images with no-repeat attribute from css and merge them in one file called a sprite.
  518. * Css is updated with sprite url and correct background positions for affected images.
  519. * Sprite saved in {Joomla! base}/images/jch-optimize/
  520. *
  521. * @param string $sCss Aggregated css file before sprite generation
  522. * @param string $sLnEnd Document line end
  523. * @return string Css updated with sprite information on success. Original css on failure
  524. */
  525. protected function generateSprite($sCss, $sLnEnd) {
  526. if (extension_loaded('imagick') && extension_loaded('exif')) {
  527. $sImageLibrary = 'imagick';
  528. } else {
  529. if (!extension_loaded('gd') || !extension_loaded('exif')) {
  530. return $sCss;
  531. }
  532. $sImageLibrary = 'gd';
  533. }
  534. $iMinMaxImages = $this->params->get('csg_min_max_images', 0);
  535. $sDelStart = '~';
  536. $sRegexStart = '(?:(?<=^|})
  537. (?=([^{]+?)({[^}]+?(url\(([^}]+?\.(?:png|gif|jpe?g))[^}]*?\))[^}]+?}))
  538. (?:(?!(?:\s(?<!no-)repeat(?:-(?:x|y))?)|(?:background[^;]+?(?:\s(?:left|right|center|top|bottom)';
  539. $sRegexMin = '|(?:\s0)|(?:\s\d{1,5}(?:%|in|(?:c|m)m|e(?:m|x)|p(?:t|c|x)))){1,2}[^;]*?;)';
  540. $sRegexMax = '|(?:\s[1-9]\d{0,4}(?:%|in|(?:c|m)m|e(?:m|x)|p(?:t|c|x)))){1,2}[^;]*?;)';
  541. $sRegexEnd = ')[^}])*?})';
  542. $sDelEnd = '~sx';
  543. $aIncludeImages = $this->getArray($this->params->get('csg_include_images'));
  544. $aExcludeImages = $this->getArray($this->params->get('csg_exclude_images'));
  545. $sIncImagesRegex = '';
  546. if (!empty($aIncludeImages[0]) && !$iMinMaxImages) {
  547. foreach ($aIncludeImages as $sIncImage) {
  548. $sIncImage = str_replace('.', '\.', $sIncImage);
  549. $sIncImagesRegex .= '|(?:(?<=^|})([^{]+?){[^}]+?(url\(([^}]+?' . $sIncImage . ')[^}]*?\))[^}]*?})';
  550. }
  551. }
  552. $sExImagesRegex = '';
  553. if (!empty($aExcludeImages[0]) && $iMinMaxImages) {
  554. foreach ($aExcludeImages as $sExImage) {
  555. $sExImage = str_replace('.', '\.', $sExImage);
  556. $sExImagesRegex .= '|(?:\b' . $sExImage . ')';
  557. }
  558. }
  559. $sMinMaxRegex = $iMinMaxImages ? $sRegexMax : $sRegexMin;
  560. $sRegex = $sDelStart . $sRegexStart . $sMinMaxRegex . $sExImagesRegex . $sRegexEnd . $sIncImagesRegex . $sDelEnd;
  561. $iResult = preg_match_all($sRegex, $sCss, $aMatches);
  562. //print_r($aMatches);
  563. if ($iResult <= 0) {
  564. return $sCss;
  565. }
  566. require_once ( dirname(__FILE__) . DS . 'cache' . DS . 'css-sprite-gen.inc.php' );
  567. $aDeclaration = $aMatches[2];
  568. $aImages = $aMatches[4];
  569. //print_r($aDeclaration);
  570. //print_r($sImages);
  571. $aFormValues = array();
  572. $aFormValues['wrap-columns'] = $this->params->get('csg_wrap_images', 'off');
  573. $aFormValues['build-direction'] = $this->params->get('csg_direction', 'vertical');
  574. $aFormValues['image-output'] = $this->params->get('csg_file_output', 'PNG');
  575. $oSpriteGen = new CssSpriteGen($sImageLibrary, $aFormValues);
  576. $aImageTypes = $oSpriteGen->GetImageTypes();
  577. $oSpriteGen->CreateSprite($aImages);
  578. $aSpriteCss = $oSpriteGen->GetCssBackground();
  579. //print_r($aSpriteCss);
  580. $aNeedles = array();
  581. $aReplacements = array();
  582. $sImageSelector = '';
  583. $sBaseUrl = JURI::getInstance()->base();
  584. // $sBaseUrl = clone JURI::base(true);
  585. $sBaseUrl = $sBaseUrl == '/' ? $sBaseUrl : $sBaseUrl . '/';
  586. for ($i = 0; $i < count($aSpriteCss); $i++) {
  587. if (@$aSpriteCss[$i]) {
  588. $aNeedles[] = $aDeclaration[$i];
  589. preg_match('~(?<=background)(?:[^;]+?)(
  590. (?<!/)\b(?:aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow)\b(?!/)
  591. |\#[a-fA-F\d]{3,6}
  592. |rgb\([^\)]+?\))
  593. (?:[^;]*?);~ix', $aDeclaration[$i], $aBgColor);
  594. preg_match('~(?<=background)(?:[^;]+?)\s(
  595. scroll|fixed
  596. )\s(?:(?:[^;]+?)?;)~ix', $aDeclaration[$i], $aBgAttach);
  597. $sBgImage = 'url(' . $sBaseUrl . 'images/jch-optimize/' . $oSpriteGen->GetSpriteFilename() . ')';
  598. $sBackground = 'background: ' . @$aBgColor[1] . ' ' . $sBgImage . ' ' . @$aBgAttach[1] . ' ' . $aSpriteCss[$i] . ' no-repeat; ';
  599. $sDecUnique = preg_replace('~background[^;]+?;~sx', '', $aDeclaration[$i]);
  600. $aReplacements[] = str_replace('{', '{' . $sLnEnd . $sBackground, $sDecUnique);
  601. }
  602. }
  603. $sCss = str_replace($aNeedles, $aReplacements, $sCss);
  604. //$sCss = $sImageSelector.'{background-image:'.$sBgImage.');}'.$sCss;
  605. return $sCss;
  606. }
  607. }