1<?php if (!defined('BASEPATH')) { 2 exit('No direct script access allowed'); 3} 4/* 5 * LimeSurvey 6 * Copyright (C) 2007-2011 The LimeSurvey Project Team / Carsten Schmitz 7 * All rights reserved. 8 * License: GNU/GPL License v2 or later, see LICENSE.php 9 * LimeSurvey is free software. This version may have been modified pursuant 10 * to the GNU General Public License, and as distributed it includes or 11 * is derivative of works licensed under the GNU General Public License or 12 * other free or open source software licenses. 13 * See COPYRIGHT.php for copyright notices and details. 14 * 15 */ 16 17 /* 18 * NOTE 1 : To refresh the assets, the base directory of the template must be updated. 19 * NOTE 2: By default, Asset Manager is off when debug mode is on. 20 * 21 * Developers should then think about : 22 * 1. refreshing their brower's cache (ctrl + F5) to see their changes 23 * 2. update the config.xml last_update before pushing, to be sure that end users will have the new version 24 * 25 * 26 * For more detail, see : 27 * http://www.yiiframework.com/doc/api/1.1/CClientScript#addPackage-detail 28 * http://www.yiiframework.com/doc/api/1.1/YiiBase#setPathOfAlias-detail 29 */ 30 31class LSYii_ClientScript extends CClientScript 32{ 33 34 const POS_POSTSCRIPT = 5; 35 const POS_PREBEGIN = 6; 36 /** 37 * cssFiles is protected on CClientScript. It can be useful to access it for debugin purpose 38 * @return array 39 */ 40 public function getCssFiles() 41 { 42 return $this->cssFiles; 43 } 44 45 46 public function recordCachingAction($context, $method, $params) 47 { 48 if(($controller=Yii::app()->getController())!==null && (get_class($controller)!=='ConsoleApplication' )){ 49 $controller->recordCachingAction($context,$method,$params); 50 } 51 52 } 53 54 public function getScriptFiles() 55 { 56 return $this->scriptFiles; 57 } 58 59 /** 60 * cssFicoreScripts is protected on CClientScript. It can be useful to access it for debugin purpose 61 * @return array 62 */ 63 public function getCoreScripts() 64 { 65 return $this->coreScripts; 66 } 67 68 /** 69 * Remove a package from coreScript. 70 * It can be useful when mixing backend/frontend rendering (see: template editor) 71 * 72 * @var string $sName of the package to remove 73 */ 74 public function unregisterPackage($sName) 75 { 76 if (!empty($this->coreScripts[$sName])) { 77 unset($this->coreScripts[$sName]); 78 } 79 } 80 81 public function unregisterScriptFile($sName) 82 { 83 if (!empty($this->scriptFiles[0]["$sName"])) { 84 unset($this->scriptFiles[0]["$sName"]); 85 } 86 } 87 88 /** 89 * Check if a file is in a given package 90 * @var $sPackageName string name of the package 91 * @var $sType string css/js 92 * @var $sFileName string name of the file to remove 93 * @return boolean 94 */ 95 public function IsFileInPackage($sPackageName, $sType, $sFileName) 96 { 97 if (!empty(Yii::app()->clientScript->packages[$sPackageName])) { 98 if (!empty(Yii::app()->clientScript->packages[$sPackageName][$sType])) { 99 $key = array_search($sFileName, Yii::app()->clientScript->packages[$sPackageName][$sType]); 100 return $key !== false; 101 } 102 } 103 return false; 104 } 105 106 107 /** 108 * Add a file to a given package 109 * 110 * @var $sPackageName string name of the package 111 * @var $sType string css/js 112 * @var $sFileName string name of the file to add 113 */ 114 public function addFileToPackage($sPackageName, $sType, $sFileName) 115 { 116 if (!empty(Yii::app()->clientScript->packages[$sPackageName])) { 117 118 119 if (empty(Yii::app()->clientScript->packages[$sPackageName][$sType])) { 120 Yii::app()->clientScript->packages[$sPackageName][$sType] = array(); 121 } 122 123 $sFilePath = Yii::getPathOfAlias( Yii::app()->clientScript->packages[$sPackageName]["basePath"] ) . DIRECTORY_SEPARATOR . $sFileName; 124 Yii::app()->clientScript->packages[$sPackageName][$sType][] = $sFileName; 125 } 126 } 127 128 129 130 /** 131 * Remove a file from a given package 132 * 133 * @var $sPackageName string name of the package 134 * @var $sType string css/js 135 * @var $sFileName string name of the file to remove 136 */ 137 public function removeFileFromPackage($sPackageName, $sType, $sFileName) 138 { 139 if (!empty(Yii::app()->clientScript->packages[$sPackageName])) { 140 if (!empty(Yii::app()->clientScript->packages[$sPackageName][$sType])) { 141 $key = array_search($sFileName, Yii::app()->clientScript->packages[$sPackageName][$sType]); 142 unset(Yii::app()->clientScript->packages[$sPackageName][$sType][$key]); 143 } 144 } 145 } 146 147 /** 148 * In LimeSurvey, if debug mode is OFF we use the asset manager (so participants never needs to update their webbrowser cache). 149 * If debug mode is ON, we don't use the asset manager, so developpers just have to refresh their browser cache to reload the new scripts. 150 * To make developper life easier, if they want to register a single script file, they can use App()->getClientScript()->registerScriptFile({url to script file}) 151 * if the file exist in local file system and debug mode is off, it will find the path to the file, and it will publish it via the asset manager 152 * @param string $url 153 * @param string $position 154 * @param array $htmlOptions 155 * @return void|static 156 */ 157 public function registerScriptFile($url, $position = null, array $htmlOptions = array()) 158 { 159 // If possible, we publish the asset: it moves the file to the tmp/asset directory and return the url to access it 160 if ((!YII_DEBUG || Yii::app()->getConfig('use_asset_manager'))) { 161 $aUrlDatas = $this->analyzeUrl($url); 162 if ($aUrlDatas['toPublish']) { 163 $url = App()->assetManager->publish($aUrlDatas['sPathToFile']); 164 } 165 } 166 167 parent::registerScriptFile($url, $position, $htmlOptions); // We publish the script 168 } 169 170 171 public function registerCssFile($url, $media = '') 172 { 173 // If possible, we publish the asset: it moves the file to the tmp/asset directory and return the url to access it 174 if ((!YII_DEBUG || Yii::app()->getConfig('use_asset_manager'))) { 175 $aUrlDatas = $this->analyzeUrl($url); 176 if ($aUrlDatas['toPublish']) { 177 $url = App()->assetManager->publish($aUrlDatas['sPathToFile']); 178 } 179 } 180 parent::registerCssFile($url, $media); // We publish the script 181 } 182 183 /** 184 * The method will first check if a devbaseUrl parameter is provided, 185 * so when debug mode is on, it doens't use the asset manager 186 * @param string $name 187 * @return void|static 188 */ 189 public function registerPackage($name) 190 { 191 if (!YII_DEBUG || Yii::app()->getConfig('use_asset_manager')) { 192 parent::registerPackage($name); 193 } else { 194 195 // We first convert the current package to devBaseUrl 196 $this->convertDevBaseUrl($name); 197 198 // Then we do the same for all its dependencies 199 $aDepends = $this->getRecursiveDependencies($name); 200 foreach ($aDepends as $package) { 201 $this->convertDevBaseUrl($package); 202 } 203 204 parent::registerPackage($name); 205 } 206 } 207 208 /** 209 * Return a list of all the recursive dependencies of a packages 210 * eg: If a package A depends on B, and B depends on C, getRecursiveDependencies('A') will return {B,C} 211 * @param string $sPackageName 212 */ 213 public function getRecursiveDependencies($sPackageName) 214 { 215 $aPackages = Yii::app()->clientScript->packages; 216 if (array_key_exists('depends', $aPackages[$sPackageName])) { 217 $aDependencies = $aPackages[$sPackageName]['depends']; 218 219 foreach ($aDependencies as $sDpackageName) { 220 if ($aPackages[$sPackageName]['depends']) { 221 $aRDependencies = $this->getRecursiveDependencies($sDpackageName); // Recursive call 222 if (is_array($aRDependencies)) { 223 $aDependencies = array_unique(array_merge($aDependencies, $aRDependencies)); 224 } 225 } 226 } 227 return $aDependencies; 228 } 229 return array(); 230 } 231 232 233 /** 234 * Convert one package to baseUrl 235 * Overwrite the package definition using a base url instead of a base path 236 * The package must have a devBaseUrl, else it will remain unchanged (for core/external package); so third party package are not concerned 237 * @param string $package 238 */ 239 private function convertDevBaseUrl($package) 240 { 241 // We retreive the old package 242 $aOldPackageDefinition = Yii::app()->clientScript->packages[$package]; 243 244 // If it has an entry 'devBaseUrl', we use it to replace basePath (it will turn off asset manager for this package) 245 if (is_array($aOldPackageDefinition) && array_key_exists('devBaseUrl', $aOldPackageDefinition)) { 246 247 $aNewPackageDefinition = array(); 248 249 // Take all the values of the oldPackage to add it to the new one 250 foreach ($aOldPackageDefinition as $key => $value) { 251 252 // Remove basePath 253 if ($key != 'basePath') { 254 255 // Convert devBaseUrl 256 if ($key == 'devBaseUrl') { 257 $aNewPackageDefinition['baseUrl'] = $value; 258 } else { 259 $aNewPackageDefinition[$key] = $value; 260 } 261 } 262 } 263 Yii::app()->clientScript->addPackage($package, $aNewPackageDefinition); 264 } 265 } 266 267 /** 268 * This function will analyze the url of a file (css/js) to register 269 * It will check if it can be published via the asset manager and if so will retreive its path 270 * @param $sUrl 271 * @return array 272 */ 273 private function analyzeUrl($sUrl) 274 { 275 $sCleanUrl = str_replace(Yii::app()->baseUrl, '', $sUrl); // we remove the base url to be sure that the first parameter is the one we want 276 $aUrlParams = explode('/', $sCleanUrl); 277 $sFilePath = Yii::app()->getConfig('rootdir').$sCleanUrl; 278 $sPath = ''; 279 280 // TODO: check if tmp directory can be named differently via config 281 if (isset($aUrlParams[1]) && $aUrlParams[1] == 'tmp') { 282 $sType = 'published'; 283 } else { 284 if (file_exists($sFilePath)) { 285 $sType = 'toPublish'; 286 $sPath = $sFilePath; 287 } else { 288 $sType = 'cantPublish'; 289 } 290 } 291 292 return array('toPublish'=>($sType == 'toPublish'), 'sPathToFile' => $sPath); 293 } 294 295 /** 296 * Registers a script package that is listed in {@link packages}. 297 * @param string $name the name of the script package. 298 * @return static the CClientScript object itself (to support method chaining, available since version 1.1.5). 299 * @see renderCoreScript 300 * @throws CException 301 */ 302 public function registerPackageScriptOnPosition($name, $position) 303 { 304 if (isset($this->coreScripts[$name])) { 305 $this->coreScripts[$name]['position'] = $position; 306 return $this; 307 } 308 309 if (isset($this->packages[$name])) { 310 $package = $this->packages[$name]; 311 } else { 312 if ($this->corePackages === null) { 313 $this->corePackages = require(YII_PATH.'/web/js/packages.php'); 314 } 315 if (isset($this->corePackages[$name])) { 316 $package = $this->corePackages[$name]; 317 } 318 } 319 320 if (isset($package)) { 321 $package['position'] = $position; 322 323 if (!empty($package['depends'])) { 324 foreach ($package['depends'] as $p) { 325 $this->registerPackageScriptOnPosition($p, $position); 326 } 327 } 328 329 $this->coreScripts[$name] = $package; 330 $this->hasScripts = true; 331 $params = func_get_args(); 332 $this->recordCachingAction('clientScript', 'registerPackageScriptOnPosition', $params); 333 } elseif (YII_DEBUG) { 334 throw new CException('There is no LSYii_ClientScript package: '.$name); 335 } else { 336 Yii::log('There is no LSYii_ClientScript package: '.$name, CLogger::LEVEL_WARNING, 'system.web.LSYii_ClientScript'); 337 } 338 339 return $this; 340 } 341 342 /** 343 * Renders the specified core javascript library. 344 */ 345 public function renderCoreScripts() 346 { 347 if ($this->coreScripts === null) { 348 return; 349 } 350 351 $cssFiles = array(); 352 $jsFiles = array(); 353 $jsFilesPositioned = array(); 354 355 foreach ($this->coreScripts as $name=>$package) { 356 $baseUrl = $this->getPackageBaseUrl($name); 357 if (!empty($package['js'])) { 358 foreach ($package['js'] as $js) { 359 if (isset($package['position'])) { 360 $jsFilesPositioned[$package['position']][$baseUrl.'/'.$js] = $baseUrl.'/'.$js; 361 } else { 362 $jsFiles[$baseUrl.'/'.$js] = $baseUrl.'/'.$js; 363 } 364 } 365 } 366 if (!empty($package['css'])) { 367 foreach ($package['css'] as $css) { 368 $cssFiles[$baseUrl.'/'.$css] = ''; 369 } 370 } 371 } 372 // merge in place 373 if ($cssFiles !== array()) { 374 foreach ($this->cssFiles as $cssFile=>$media) { 375 $cssFiles[$cssFile] = $media; 376 } 377 $this->cssFiles = $cssFiles; 378 } 379 if ($jsFiles !== array()) { 380 if (isset($this->scriptFiles[$this->coreScriptPosition])) { 381 foreach ($this->scriptFiles[$this->coreScriptPosition] as $url => $value) { 382 $jsFiles[$url] = $value; 383 } 384 } 385 $this->scriptFiles[$this->coreScriptPosition] = $jsFiles; 386 } 387 if ($jsFilesPositioned !== array()) { 388 foreach ($jsFilesPositioned as $position=>$fileArray) { 389 if (isset($this->scriptFiles[$position])) { 390 foreach ($this->scriptFiles[$position] as $url => $value) { 391 $fileArray[$url] = $value; 392 } 393 } 394 $this->scriptFiles[$position] = $fileArray; 395 } 396 } 397 } 398 399 /** 400 * Inserts the scripts in the head section. 401 * @param string $output the output to be inserted with scripts. 402 */ 403 public function renderHead(&$output) 404 { 405 $html = ''; 406 407 foreach ($this->metaTags as $meta) { 408 $html .= CHtml::metaTag($meta['content'], null, null, $meta)."\n"; 409 } 410 foreach ($this->linkTags as $link) { 411 $html .= CHtml::linkTag(null, null, null, null, $link)."\n"; 412 } 413 foreach ($this->cssFiles as $url=>$media) { 414 $html .= CHtml::cssFile($url, $media)."\n"; 415 } 416 417 //Propagate our debug settings into the javascript realm 418 if (function_exists('getGlobalSetting')) { 419 $debugFrontend = (int) getGlobalSetting('javascriptdebugfrntnd'); 420 $debugBackend = (int) getGlobalSetting('javascriptdebugbcknd'); 421 } else { 422 $debugFrontend = 0; 423 $debugBackend = 0; 424 } 425 426 $html .= "<script type='text/javascript'>window.debugState = {frontend : (".$debugFrontend." === 1), backend : (".$debugBackend." === 1)};</script>"; 427 428 if ($this->enableJavaScript) { 429 if (isset($this->scriptFiles[self::POS_HEAD])) { 430 foreach ($this->scriptFiles[self::POS_HEAD] as $scriptFileValueUrl=>$scriptFileValue) { 431 if (is_array($scriptFileValue)) { 432 $scriptFileValue['class'] = isset($scriptFileValue['class']) ? $scriptFileValue['class']." headScriptTag" : "headScriptTag"; 433 $html .= CHtml::scriptFile($scriptFileValueUrl, $scriptFileValue)."\n"; 434 } else { 435 $html .= CHtml::scriptFile($scriptFileValueUrl, array('class' => 'headScriptTag'))."\n"; 436 } 437 } 438 } 439 440 if (isset($this->scripts[self::POS_HEAD])) { 441 $html .= $this->renderScriptBatch($this->scripts[self::POS_HEAD]); 442 } 443 444 } 445 446 if ($html !== '') { 447 $count = 0; 448 $output = preg_replace('/(<title\b[^>]*>|<\\/head\s*>)/is', '<###head###>$1', $output, 1, $count); 449 if ($count) { 450 $output = str_replace('<###head###>', $html, $output); 451 } else { 452 $output = $html.$output; 453 } 454 } 455 } 456 457 /** 458 * Inserts the scripts at the beginning of the body section. 459 * This is overwriting the core method and is exactly the same except the marked parts 460 * @param string $output the output to be inserted with scripts. 461 */ 462 public function renderBodyBegin(&$output) 463 { 464 $html = ''; 465 466 if (isset($this->scriptFiles[self::POS_PREBEGIN])) { 467 foreach ($this->scriptFiles[self::POS_PREBEGIN] as $scriptFileUrl=>$scriptFileValue) { 468 if (is_array($scriptFileValue)) { 469 $html .= CHtml::scriptFile($scriptFileUrl, $scriptFileValue)."\n"; 470 } else { 471 $html .= CHtml::scriptFile($scriptFileUrl)."\n"; 472 } 473 } 474 } 475 if (isset($this->scripts[self::POS_PREBEGIN])) { 476 $html .= $this->renderScriptBatch($this->scripts[self::POS_PREBEGIN]); 477 } 478 if (isset($this->scriptFiles[self::POS_BEGIN])) { 479 foreach ($this->scriptFiles[self::POS_BEGIN] as $scriptFileUrl=>$scriptFileValue) { 480 if (is_array($scriptFileValue)) { 481 $html .= CHtml::scriptFile($scriptFileUrl, $scriptFileValue)."\n"; 482 } else { 483 $html .= CHtml::scriptFile($scriptFileUrl)."\n"; 484 } 485 } 486 } 487 if (isset($this->scripts[self::POS_BEGIN])) { 488 $html .= $this->renderScriptBatch($this->scripts[self::POS_BEGIN]); 489 } 490 491 if ($html !== '') { 492 $count = 0; 493 if (preg_match('/<###begin###>/', $output)) { 494 $count = 1; 495 } else { 496 $output = preg_replace('/(<body\b[^>]*>)/is', '$1<###begin###>', $output, 1, $count); 497 } 498 if ($count) { 499 $output = str_replace('<###begin###>', $html, $output); 500 } else { 501 $output = $html.$output; 502 } 503 } else { 504 $output = preg_replace('/<###begin###>/', '', $output, 1); 505 } 506 } 507 508 /** 509 * Inserts the scripts at the end of the body section. 510 * This is overwriting the core method and is exactly the same except the marked parts 511 * @param string $output the output to be inserted with scripts. 512 */ 513 public function renderBodyEnd(&$output) 514 { 515 if (!isset($this->scriptFiles[self::POS_END]) && !isset($this->scripts[self::POS_END]) && !isset($this->scripts[self::POS_READY]) 516 && !isset($this->scripts[self::POS_LOAD]) && !isset($this->scripts[self::POS_POSTSCRIPT])) { 517 str_replace('<###end###>', '', $output); 518 return; 519 } 520 521 $fullPage = 0; 522 if (preg_match('/<###end###>/', $output)) { 523 $fullPage = 1; 524 } else { 525 $output = preg_replace('/(<\\/body\s*>)/is', '<###end###>$1', $output, 1, $fullPage); 526 } 527 528 $html = ''; 529 if (isset($this->scriptFiles[self::POS_END])) { 530 foreach ($this->scriptFiles[self::POS_END] as $scriptFileUrl=>$scriptFileValue) { 531 if (is_array($scriptFileValue)) { 532 $html .= CHtml::scriptFile($scriptFileUrl, $scriptFileValue)."\n"; 533 } else { 534 $html .= CHtml::scriptFile($scriptFileUrl)."\n"; 535 } 536 } 537 } 538 $scripts = isset($this->scripts[self::POS_END]) ? $this->scripts[self::POS_END] : array(); 539 540 if (isset($this->scripts[self::POS_READY])) { 541 if ($fullPage) { 542 $scripts[] = "jQuery(function($) {\n".implode("\n", $this->scripts[self::POS_READY])."\n});"; 543 } else { 544 $scripts[] = implode("\n", $this->scripts[self::POS_READY]); 545 } 546 } 547 if (isset($this->scripts[self::POS_LOAD])) { 548 if ($fullPage) { 549 //This part is different to reflect the changes needed in the backend by the pjax loading of pages 550 551 552 $scripts[] = "jQuery(document).on('ready pjax:complete',function() {\n".implode("\n", $this->scripts[self::POS_LOAD])."\n});"; 553 } else { 554 $scripts[] = implode("\n", $this->scripts[self::POS_LOAD]); 555 } 556 } 557 558 if (isset($this->scripts[self::POS_POSTSCRIPT])) { 559 if ($fullPage) { 560 //This part is different to reflect the changes needed in the backend by the pjax loading of pages 561 $scripts[] = "jQuery(document).off('pjax:scriptcomplete.mainBottom').on('ready pjax:scriptcomplete.mainBottom', function() {\n".implode("\n", $this->scripts[self::POS_POSTSCRIPT])."\n});"; 562 } else { 563 $scripts[] = implode("\n", $this->scripts[self::POS_POSTSCRIPT]); 564 } 565 } 566 if (App()->getConfig('debug') > 0) { 567 $scripts[] = "jQuery(document).off('pjax:scriptsuccess.debugger').on('pjax:scriptsuccess.debugger',function(e) { console.ls.log('PJAX scriptsuccess', e); });"; 568 $scripts[] = "jQuery(document).off('pjax:scripterror.debugger').on('pjax:scripterror.debugger',function(e) { console.ls.log('PJAX scripterror', e); });"; 569 $scripts[] = "jQuery(document).off('pjax:scripttimeout.debugger').on('pjax:scripttimeout.debugger',function(e) { console.ls.log('PJAX scripttimeout', e); });"; 570 $scripts[] = "jQuery(document).off('pjax:success.debugger').on('pjax:success.debugger',function(e) { console.ls.log('PJAX success', e);});"; 571 $scripts[] = "jQuery(document).off('pjax:error.debugger').on('pjax:error.debugger',function(e) { console.ls.log('PJAX error', e);});"; 572 } 573 574 //All scripts are wrapped into a section to be able to reload them accordingly 575 if (!empty($scripts)) { 576 $html .= $this->renderScriptBatch($scripts); 577 } 578 579 if ($fullPage) { 580 $output = preg_replace('/<###end###>/', $html, $output, 1); 581 } else { 582 $output = $output.$html; 583 } 584 } 585 586 /** 587 * Renders the registered scripts. 588 * This method is called in {@link CController::render} when it finishes 589 * rendering content. CClientScript thus gets a chance to insert script tags 590 * at <code>head</code> and <code>body</code> sections in the HTML output. 591 * @param string $output the existing output that needs to be inserted with script tags 592 */ 593 public function render(&$output) 594 { 595 /** 596 * beforeCloseHtml event @see https://manual.limesurvey.org/BeforeCloseHtml 597 * Set it before all other action allow registerScript by plugin 598 * Whitelisting available controller (public plugin not happen for PluginsController using actionDirect, actionUnsecure event) 599 */ 600 $publicControllers = array('option','optout','printanswers','register','statistics_user','survey','surveys','uploader'); 601 if(Yii::app()->getController() && in_array(Yii::app()->getController()->getId(),$publicControllers) && strpos($output, '</body>')) { 602 $event = new PluginEvent('beforeCloseHtml'); 603 $surveyId = Yii::app()->getRequest()->getParam('surveyid',Yii::app()->getRequest()->getParam('sid',Yii::app()->getConfig('surveyid'))); 604 $event->set('surveyId', $surveyId); // Set to null if not set by param 605 App()->getPluginManager()->dispatchEvent($event); 606 $pluginHtml = $event->get('html'); 607 if (!empty($pluginHtml) && is_string($pluginHtml)) { 608 $output = preg_replace('/(<\\/body\s*>)/is', "{$pluginHtml}$1", $output, 1); 609 } 610 } 611 if (!$this->hasScripts) { 612 return; 613 } 614 615 $this->renderCoreScripts(); 616 617 if (!empty($this->scriptMap)) { 618 $this->remapScripts(); 619 } 620 621 $this->unifyScripts(); 622 623 $this->renderHead($output); 624 if ($this->enableJavaScript) { 625 $this->renderBodyBegin($output); 626 $this->renderBodyEnd($output); 627 } 628 } 629} 630