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