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