1<?php
2/**
3 * This extension is needed to add complex functions to twig, needing specific process (like accessing config datas).
4 * Most of the calls to internal functions don't need to be set here, but can be directly added to the internal config file.
5 * For example, the calls to encode, gT and eT don't need any extra parameters or process, so they are added as filters in the congif/internal.php:
6 *
7 * 'filters' => array(
8 *     'jencode' => 'CJSON::encode',
9 *     't'     => 'eT',
10 *     'gT'    => 'gT',
11 * ),
12 *
13 * So you only add functions here when they need a specific process while called via Twig.
14 * To add an advanced function to twig:
15 *
16 * 1. Add it here as a static public function
17 *      eg:
18 *          static public function foo($bar)
19 *          {
20 *              return procces($bar);
21 *          }
22 *
23 * 2. Add it in config/internal.php as a function, and as an allowed function in the sandbox
24 *      eg:
25 *          twigRenderer' => array(
26 *              ...
27 *              'functions' => array(
28 *                  ...
29 *                  'foo' => 'LS_Twig_Extension::foo',
30 *                ...),
31 *              ...
32 *              'sandboxConfig' => array(
33 *              ...
34 *                  'functions' => array('include', ..., 'foo')
35 *                 ),
36 *
37 * Now you access this function in any twig file via: {{ foo($bar) }}, it will show the result of process($bar).
38 * If LS_Twig_Extension::foo() returns some HTML, by default the HTML will be escaped and shows as text.
39 * To get the pure HTML, just do: {{ foo($bar) | raw }}
40 */
41
42
43class LS_Twig_Extension extends Twig_Extension
44{
45    /**
46     * Publish a css file from public style directory, using or not the asset manager (depending on configuration)
47     * In any twig file, you can register a public css file doing: {{ registerPublicCssFile($sPublicCssFileName) }}
48     * @param string $sPublicCssFileName name of the CSS file to publish in public style directory
49     */
50    public static function registerPublicCssFile($sPublicCssFileName)
51    {
52        Yii::app()->clientScript->registerCssFile(
53            Yii::app()->getConfig('publicstyleurl').
54            $sPublicCssFileName
55        );
56    }
57
58
59    /**
60     * Publish a css file from template directory, using or not the asset manager (depending on configuration)
61     * In any twig file, you can register a template css file doing: {{ registerTemplateCssFile($sTemplateCssFileName) }}
62     * @param string $sTemplateCssFileName name of the CSS file to publish in template directory (it should contains the subdirectories)
63     */
64    public static function registerTemplateCssFile($sTemplateCssFileName)
65    {
66        /*
67            CSS added from template could require some files from the template folder file...  (eg: background.css)
68            So, if we want the statements like :
69              url("../files/myfile.jpg)
70             to point to an existing file, the css file must be published in the same tmp directory than the template files
71             in other words, the css file must be added to the template package.
72        */
73
74        $oTemplate = self::getTemplateForRessource($sTemplateCssFileName);
75        Yii::app()->clientScript->packages[$oTemplate->sPackageName]['css'][] = $sTemplateCssFileName;
76    }
77
78    /**
79     * Publish a script file from general script directory, using or not the asset manager (depending on configuration)
80     * In any twig file, you can register a general script file doing: {{ registerGeneralScript($sGeneralScriptFileName) }}
81     * @param string $sGeneralScriptFileName name of the script file to publish in general script directory (it should contains the subdirectories)
82     * @param string $position
83     * @param array $htmlOptions
84     */
85    public static function registerGeneralScript($sGeneralScriptFileName, $position = null, array $htmlOptions = array())
86    {
87        $position = self::getPosition($position);
88        Yii::app()->clientScript->registerScriptFile(
89            App()->getConfig('generalscripts').
90            $sGeneralScriptFileName,
91            $position,
92            $htmlOptions
93        );
94    }
95
96    /**
97     * Publish a script file from template directory, using or not the asset manager (depending on configuration)
98     * In any twig file, you can register a template script file doing: {{ registerTemplateScript($sTemplateScriptFileName) }}
99     * @param string $sTemplateScriptFileName name of the script file to publish in general script directory (it should contains the subdirectories)
100     * @param string $position
101     * @param array $htmlOptions
102     */
103    public static function registerTemplateScript($sTemplateScriptFileName, $position = null, array $htmlOptions = array())
104    {
105        $oTemplate = self::getTemplateForRessource($sTemplateScriptFileName);
106        Yii::app()->clientScript->packages[$oTemplate->sPackageName]['js'][] = $sTemplateScriptFileName;
107    }
108
109    /**
110     * Publish a script
111     * In any twig file, you can register a script doing: {{ registerScript($sId, $sScript) }}
112     *
113     * NOTE: this function is not recursive, so don't use it to register a script located inside a theme folder, or inherited themes will be broken.
114     * NOTE! to register a script located inside a theme folder, registerTemplateScript()
115     *
116     */
117    public static function registerScript($id, $script, $position = null, array $htmlOptions = array())
118    {
119        $position = self::getPosition($position);
120        Yii::app()->clientScript->registerScript(
121            $id,
122            $script,
123            $position,
124            $htmlOptions
125        );
126    }
127
128    /**
129     * Convert a json object to a PHP array (so no troubles with object method in sandbox)
130     * @param string $json
131     * @param boolean $assoc return sub object as array too
132     * @return array
133     */
134    public static function json_decode($json,$assoc = true)
135    {
136        return (array) json_decode($json,$assoc);
137    }
138
139    /**
140     * @param $position
141     * @return string
142     */
143    public static function getPosition($position)
144    {
145        switch ($position) {
146            case "POS_HEAD":
147                $position = LSYii_ClientScript::POS_HEAD;
148                break;
149
150            case "POS_BEGIN":
151                $position = LSYii_ClientScript::POS_BEGIN;
152                break;
153
154            case "POS_END":
155                $position = LSYii_ClientScript::POS_END;
156                break;
157
158            case "POS_POSTSCRIPT":
159                $position = LSYii_ClientScript::POS_POSTSCRIPT;
160                break;
161
162            default:
163                $position = '';
164                break;
165        }
166
167        return $position;
168    }
169
170    /**
171     * since count with a noncountable element is throwing a warning in latest php versions
172     * we have to be sure not to kill rendering by a wrong variable
173     *
174     * @param mixed $element
175     * @return void
176     */
177    public static function safecount($element)
178    {
179        $isCountable = is_array($element) || $element instanceof Countable;
180        if($isCountable) {
181            return count($element);
182        }
183        return 0;
184    }
185    /**
186     * Retreive the question classes for a given question id
187     * Use in survey template question.twig file.
188     * TODO: we'd rather provide a oQuestion object to the twig view with a method getAllQuestion(). But for now, this public static function respect the old way of doing
189     *
190     * @param  int      $iQid the question id
191     * @return string   the classes
192     * @deprecated must be removed when allow to broke template. Since it was in 3.0 , it was in API (and question.twig are surely be updated).
193     */
194    public static function getAllQuestionClasses($iQid)
195    {
196
197        $lemQuestionInfo = LimeExpressionManager::GetQuestionStatus($iQid);
198        $sType           = $lemQuestionInfo['info']['type'];
199        $aSGQA           = explode('X', $lemQuestionInfo['sgqa']);
200        $iSurveyId       = $aSGQA[0];
201
202        $aQuestionClass  = Question::getQuestionClass($sType);
203
204        /* Add the relevance class */
205        if (!$lemQuestionInfo['relevant']) {
206            $aQuestionClass .= ' ls-irrelevant';
207            $aQuestionClass .= ' ls-hidden';
208        }
209
210        /* Can use aQuestionAttributes too */
211        if ($lemQuestionInfo['hidden']) {
212            $aQuestionClass .= ' ls-hidden-attribute'; /* another string ? */
213            $aQuestionClass .= ' ls-hidden';
214        }
215
216        $aQuestionAttributes = QuestionAttribute::model()->getQuestionAttributes($iQid);
217
218        //add additional classes
219        if (isset($aQuestionAttributes['cssclass']) && $aQuestionAttributes['cssclass'] != "") {
220            /* Got to use static expression */
221            $emCssClass = trim(LimeExpressionManager::ProcessString($aQuestionAttributes['cssclass'], null, array(), 1, 1, false, false, true)); /* static var is the lmast one ...*/
222            if ($emCssClass != "") {
223                $aQuestionClass .= " ".CHtml::encode($emCssClass);
224            }
225        }
226
227        if ($lemQuestionInfo['info']['mandatory'] == 'Y') {
228            $aQuestionClass .= ' mandatory';
229        }
230
231        if ($lemQuestionInfo['anyUnanswered'] && $_SESSION['survey_'.$iSurveyId]['maxstep'] != $_SESSION['survey_'.$iSurveyId]['step']) {
232            $aQuestionClass .= ' missing';
233        }
234
235        return $aQuestionClass;
236    }
237
238    public static function renderCaptcha()
239    {
240        return App()->getController()->createWidget('LSCaptcha', array(
241            'captchaAction'=>'captcha',
242            'buttonOptions'=>array('class'=> 'btn btn-xs btn-info'),
243            'buttonType' => 'button',
244            'buttonLabel' => gt('Reload image', 'unescaped')
245        ));
246    }
247
248
249    public static function createUrl($url, $params = array())
250    {
251        return App()->getController()->createUrl($url, $params);
252    }
253
254    /**
255     * @param string $sRessource
256     */
257    public static function assetPublish($sRessource)
258    {
259        return App()->assetManager->publish($sRessource);
260    }
261
262    /**
263     * @var $sImagePath  string                 the image path relative to the template root
264     * @var $alt         string                 the alternative text display
265     * @var $htmlOptions array                  additional HTML attribute
266     * @return string
267     */
268    public static function image($sImagePath, $alt = '', $htmlOptions = array( ))
269    {
270        $sUrlImgAsset = self::imageSrc($sImagePath,'');
271        if(!$sUrlImgAsset) {
272            return '';
273        }
274        return CHtml::image($sUrlImgAsset, $alt, $htmlOptions);
275    }
276
277    /**
278     * @var $sImagePath  string                 the image path relative to the template root
279     * @var $default     string|false                 an alternative image if the provided one cant be found
280     * @return string|false
281     */
282    public static function imageSrc($sImagePath, $default = false)
283    {
284        // Reccurence on templates to find the file
285        $oTemplate = self::getTemplateForRessource($sImagePath);
286        $sUrlImgAsset =  $sImagePath;
287
288        if ($oTemplate) {
289            $sFullPath = $oTemplate->path.$sImagePath;
290        } else {
291            if(!is_file(Yii::app()->getConfig('rootdir').'/'.$sImagePath)) {
292                if($default) {
293                    return self::imageSrc($default);
294                }
295                return false;
296            }
297            $sFullPath = Yii::app()->getConfig('rootdir').'/'.$sImagePath;
298        }
299
300        // check if this is a true image
301        $checkImage = LSYii_ImageValidator::validateImage($sFullPath);
302
303        if (!$checkImage['check']) {
304            return false;
305        }
306
307        $sUrlImgAsset = self::assetPublish($sFullPath);
308        return $sUrlImgAsset;
309    }
310
311    /**
312     * @param string $sRessource
313     */
314    public static function getTemplateForRessource($sRessource)
315    {
316        $oRTemplate =  Template::getLastInstance();
317
318        while (!file_exists($oRTemplate->path.$sRessource)) {
319
320            $oMotherTemplate = $oRTemplate->oMotherTemplate;
321            if (!($oMotherTemplate instanceof TemplateConfiguration)) {
322                return false;
323                break;
324            }
325            $oRTemplate = $oMotherTemplate;
326        }
327
328        return $oRTemplate;
329    }
330
331    public static function getPost($sName, $sDefaultValue = null)
332    {
333        return Yii::app()->request->getPost($sName, $sDefaultValue);
334    }
335
336    public static function getParam($sName, $sDefaultValue = null)
337    {
338        return Yii::app()->request->getParam($sName, $sDefaultValue);
339    }
340
341    public static function getQuery($sName, $sDefaultValue = null)
342    {
343        return Yii::app()->request->getQuery($sName, $sDefaultValue);
344    }
345
346    /**
347     * @param string $name
348     */
349    public static function unregisterPackage($name)
350    {
351        return Yii::app()->clientScript->unregisterPackage($name);
352    }
353
354    /**
355     * @param string $name
356     */
357    public static function unregisterScriptFile($name)
358    {
359        return Yii::app()->clientScript->unregisterScriptFile($name);
360    }
361
362    public static function registerScriptFile($path, $position = null)
363    {
364
365        Yii::app()->clientScript->registerScriptFile($path, ($position === null ? LSYii_ClientScript::POS_BEGIN : self::getPosition($position)));
366    }
367
368    public static function registerCssFile($path)
369    {
370        Yii::app()->clientScript->registerCssFile($path);
371    }
372
373    public static function registerPackage($name)
374    {
375        Yii::app()->clientScript->registerPackage($name, LSYii_ClientScript::POS_BEGIN);
376    }
377
378    /**
379     * Unregister all packages/script files for AJAX rendering
380     */
381    public static function unregisterScriptForAjax()
382    {
383        $oTemplate            = Template::getLastInstance();
384        $sTemplatePackageName = 'limesurvey-'.$oTemplate->sTemplateName;
385        self::unregisterPackage($sTemplatePackageName);
386        self::unregisterPackage('template-core');
387        self::unregisterPackage('bootstrap');
388        self::unregisterPackage('jquery');
389        self::unregisterPackage('bootstrap-template');
390        self::unregisterPackage('fontawesome');
391        self::unregisterPackage('template-default-ltr');
392        self::unregisterPackage('decimal');
393        self::unregisterPackage('expressionscript');
394        self::unregisterScriptFile('/assets/scripts/survey_runtime.js');
395        self::unregisterScriptFile('/assets/scripts/nojs.js');
396        self::unregisterScriptFile('/assets/scripts/expressions/em_javascript.js');
397    }
398
399    public static function listCoreScripts()
400    {
401        foreach (Yii::app()->clientScript->coreScripts as $key => $package) {
402
403            echo "<hr>";
404            echo "$key: <br>";
405            var_dump($package);
406
407        }
408    }
409
410    public static function listScriptFiles()
411    {
412        foreach (Yii::app()->clientScript->getScriptFiles() as $key => $file) {
413
414            echo "<hr>";
415            echo "$key: <br>";
416            var_dump($file);
417
418        }
419    }
420
421    /**
422     * Process any string with current page
423     * @param string to be processed
424     * @param boolean $static return static string (or not)
425     * @param integer $numRecursionLevels recursion (max) level to do
426     * @param array $aReplacement replacement out of EM
427     * @return string
428     */
429    public static function processString($string,$static=false,$numRecursionLevels=3,$aReplacement = array())
430    {
431        if(!is_string($string)) {
432            /* Add some errors in template editor , see #13532 too */
433            if(Yii::app()->getController()->getId() == 'admin' && Yii::app()->getController()->getAction()->getId() == 'themes') {
434                Yii::app()->setFlashMessage(gT("Usage of processString without a string in your template"),'error');
435            }
436            return;
437        }
438        return LimeExpressionManager::ProcessStepString($string, $aReplacement,$numRecursionLevels, $static);
439    }
440
441    /**
442     * Get html text and remove whole not clean string
443     * @param string $string to flatten
444     * @param boolean $encode html entities
445     * @return string
446     */
447    public static function flatString($string,$encode=false)
448    {
449        // Remove script before removing tag, no tag : no other script (onload, on error etc …
450        $string = strip_tags(stripJavaScript($string));
451        // Remove new lines
452        if (version_compare(substr(PCRE_VERSION, 0, strpos(PCRE_VERSION, ' ')), '7.0') > -1) {
453            $string = preg_replace(array('~\R~u'), array(' '), $string);
454        } else {
455            $string = str_replace(array("\r\n", "\n", "\r"), array(' ', ' ', ' '), $string);
456        }
457        // White space to real space
458        $string = preg_replace('/\s+/', ' ', $string);
459
460        if($encode) {
461            return \CHtml::encode($string);
462        }
463        return $string;
464    }
465
466    /**
467     * get flat and ellipsize string
468     * @param string $string to ellipsize
469     * @param integer $maxlength of the final string
470     * @param float $position of the ellipsis in string (between 0 and 1)
471     * @param string $ellipsis string to shown in place of removed part
472     * @return string
473     */
474    public static function ellipsizeString($string, $maxlength, $position = 1, $ellipsis = '…')
475    {
476        $string = self::flatString($string,false);
477        $string = ellipsize($string, $maxlength, $position, $ellipsis);// Use common_helper function
478        return $string;
479    }
480
481    /**
482     * flat and ellipsize text, for template compatibility
483     * @deprecated (4.0)
484     * @param string $sString :the string
485     * @param boolean $bFlat : flattenText or not : completely flat (not like flattenText from common_helper)
486     * @param integer $iAbbreviated : max string text (if true : allways flat), 0 or false : don't abbreviated
487     * @param string $sEllipsis if abbreviated : the char to put at end (or middle)
488     * @param integer $fPosition if abbreviated position to split (in % : 0 to 1)
489     * @return string
490     */
491    public static function flatEllipsizeText($sString, $bFlat = true, $iAbbreviated = 0, $sEllipsis = '...', $fPosition = 1)
492    {
493        if (!$bFlat && !$iAbbreviated) {
494            return $sString;
495        }
496        $sString = self::flatString($sString);
497        if ($iAbbreviated > 0) {
498            $sString = ellipsize($sString, $iAbbreviated, $fPosition, $sEllipsis);
499        }
500        return $sString;
501    }
502
503    public static function darkencss($cssColor, $grade=10, $alpha=1){
504
505        $aColors = str_split(substr($cssColor,1), 2);
506        $return = [];
507        foreach ($aColors as $color) {
508            $decColor = hexdec($color);
509            $decColor = $decColor-$grade;
510            $decColor = $decColor<0 ? 0 : ($decColor>255 ? 255 : $decColor);
511            $return[] = $decColor;
512        }
513        if($alpha === 1) {
514            return '#'.join('', array_map(function($val){ return dechex($val);}, $return));
515        }
516
517        return 'rgba('.join(', ', $return).','.$alpha.')';
518    }
519
520    /**
521     * Check if a needle is in a multidimensional array
522     * @param mixed $needle The searched value.
523     * @param array $haystack The array.
524     * @param bool $strict If the third parameter strict is set to TRUE then the in_array() function will also check the types of the needle in the haystack.
525     */
526    function in_multiarray($needle, $haystack, $strict = false) {
527
528        foreach ($haystack as $item) {
529            if (($strict ? $item === $needle : $item == $needle) || (is_array($item) && in_array_r($needle, $item, $strict))) {
530                return true;
531            }
532        }
533
534        return false;
535    }
536
537
538    public static function lightencss($cssColor, $grade=10, $alpha=1)
539    {
540        $aColors = str_split(substr($cssColor,1), 2);
541        $return = [];
542        foreach ($aColors as $color) {
543            $decColor = hexdec($color);
544            $decColor = $decColor+$grade;
545            $decColor = $decColor<0 ? 0 : ($decColor>255 ? 255 : $decColor);
546            $return[] = $decColor;
547        }
548        if($alpha === 1) {
549            return '#'.join('', array_map(function($val){ return dechex($val);}, $return));
550        }
551
552        return 'rgba('.join(', ', $return).','.$alpha.')';
553    }
554
555    public static function getConfig($item)
556    {
557        return Yii::app()->getConfig($item);
558    }
559
560
561    /**
562     * Retreive all the previous answers from a given token
563     * To use it:
564     *  {% set aResponses = getAllTokenAnswers(aSurveyInfo.sid) %}
565     *  {{ dump(aResponses) }}
566     *
567     *  Of course, the survey must use token. If you want to show it after completion, the you must turn on public statistics
568     */
569    public static function getAllTokenAnswers( $iSurveyID )
570    {
571
572        $oResponses = SurveyDynamic::model($iSurveyID)->findAll(
573                            array(
574                                'condition' => 'token = :token',
575                                'params'    => array( ':token'=>$_SESSION['survey_'.$iSurveyID]['token']),
576                            )
577
578                        );
579
580        $aResponses = array();
581
582        if( count($oResponses) > 0 ){
583            foreach($oResponses as $oResponse)
584                array_push($aResponses,$oResponse->attributes);
585        }
586
587        return $aResponses;
588    }
589
590}
591