/
AdminTheme.php
474 lines (417 loc) · 23.5 KB
/
AdminTheme.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
<?php
if ( ! defined('BASEPATH')) exit('No direct script access allowed');
/*
* LimeSurvey
* Copyright (C) 2007-2015 The LimeSurvey Project Team / Carsten Schmitz
* All rights reserved.
* License: GNU/GPL License v2 or later, see LICENSE.php
* LimeSurvey is free software. This version may have been modified pursuant
* to the GNU General Public License, and as distributed it includes or
* is derivative of works licensed under the GNU General Public License or
* other free or open source software licenses.
* See COPYRIGHT.php for copyright notices and details.
*/
/**
* Admin Theme Model
*
*
* @package LimeSurvey
* @subpackage Backend
*/
class AdminTheme extends CFormModel
{
public $name; // Admin Theme's name
public $path; // Admin Theme's path
public $sTemplateUrl; // URL to reach Admin Theme (used to get CSS/JS/Files when asset manager is off)
public $config; // Contains the Admin Theme's configuration file
public static $use_asset_manager; // If true, force the use of asset manager even if debug mode is on (useful to debug asset manager's problems)
private static $instance; // The instance of theme object
/**
* Get the list of admin theme, as an array containing each configuration object for each template
* @return array the array of configuration object
*/
static public function getAdminThemeList()
{
$sStandardTemplateRootDir = Yii::app()->getConfig("styledir"); // The directory containing the default admin themes
$sUserTemplateDir = Yii::app()->getConfig('uploaddir').DIRECTORY_SEPARATOR.'admintheme'; // The directory containing the user themes
$aStandardThemeObjects = self::getThemeList( $sStandardTemplateRootDir ); // array containing the configuration files of standard admin themes (styles/...)
$aUserThemeObjects = self::getThemeList( $sUserTemplateDir ); // array containing the configuration files of user admin themes (upload/admintheme/...)
$aListOfThemeObjects = array_merge($aStandardThemeObjects, $aUserThemeObjects);
ksort($aListOfThemeObjects);
return $aListOfThemeObjects;
}
/**
* Set the Admin Theme :
* - checks if the required template exists
* - set the admin theme variables
* - set the admin theme constants
* - Register all the needed CSS/JS files
*/
public function setAdminTheme()
{
$sAdminThemeName = getGlobalSetting('admintheme'); // We retrieve the admin theme in config ( {{settings_global}} or config-defaults.php )
$sStandardTemplateRootDir = Yii::app()->getConfig("styledir"); // Path for the standard Admin Themes
$sUserTemplateDir = Yii::app()->getConfig('uploaddir').DIRECTORY_SEPARATOR.'admintheme'; // Path for the user Admin Themes
// Check if the required theme is a standard one
if($this->isStandardAdminTheme($sAdminThemeName))
{
$sTemplateDir = $sStandardTemplateRootDir; // It's standard, so it will be in standard path
$sTemplateUrl = Yii::app()->getConfig('styleurl').$sAdminThemeName ; // Available via a standard URL
}
else
{
// If it's not a standard theme, we bet it's a user one.
// In fact, it could also be a old 2.06 admin theme just aftet an update (it will then be caught as "non existent" in the next if statement")
$sTemplateDir = $sUserTemplateDir;
$sTemplateUrl = Yii::app()->getConfig('uploadurl').DIRECTORY_SEPARATOR.'admintheme'.DIRECTORY_SEPARATOR.$sAdminThemeName;
}
// If the theme directory doesn't exist, it can be that:
// - user updated from 2.06 and still have old theme configurated in database
// - user deleted a custom theme
// In any case, we just set Sea Green as the template to use
if(!is_dir($sTemplateDir.DIRECTORY_SEPARATOR.$sAdminThemeName))
{
$sAdminThemeName = 'Sea_Green';
$sTemplateDir = $sStandardTemplateRootDir;
$sTemplateUrl = Yii::app()->getConfig('styleurl').DIRECTORY_SEPARATOR.$sAdminThemeName ;
setGlobalSetting('admintheme', 'Sea_Green');
}
// Now that we are sure we have an existing template, we can set the variables of the AdminTheme
$this->sTemplateUrl = $sTemplateUrl;
$this->name = $sAdminThemeName;
$this->path = $sTemplateDir . DIRECTORY_SEPARATOR . $this->name;
// This is necessary because a lot of files still use "adminstyleurl".
// TODO: replace everywhere the call to Yii::app()->getConfig('adminstyleurl) by $oAdminTheme->sTemplateUrl;
Yii::app()->setConfig('adminstyleurl', $this->sTemplateUrl );
//////////////////////
// Config file loading
$bOldEntityLoaderState = libxml_disable_entity_loader(true); // @see: http://phpsecurity.readthedocs.io/en/latest/Injection-Attacks.html#xml-external-entity-injection
$sXMLConfigFile = file_get_contents( realpath ($this->path.'/config.xml')); // Now that entity loader is disabled, we can't use simplexml_load_file; so we must read the file with file_get_contents and convert it as a string
// Simple Xml is buggy on PHP < 5.4. The [ array -> json_encode -> json_decode ] workaround seems to be the most used one.
// @see: http://php.net/manual/de/book.simplexml.php#105330 (top comment on PHP doc for simplexml)
$this->config = json_decode( json_encode ( ( array ) simplexml_load_string($sXMLConfigFile), 1));
// If developers want to test asset manager with debug mode on
self::$use_asset_manager = isset($this->config->engine->use_asset_manager_in_debug_mode)?( $this->config->engine->use_asset_manager_in_debug_mode == 'true'):'false';
$this->defineConstants(); // Define the (still) necessary constants
$this->registerStylesAndScripts(); // Register all CSS and JS
libxml_disable_entity_loader($bOldEntityLoaderState); // Put back entity loader to its original state, to avoid contagion to other applications on the server
return $this;
}
/**
* Register all the styles and scripts of the current template.
* Check if RTL is needed, use asset manager if needed.
* This function is public because it appears that sometime, the package need to be register again in header (probably a cache problem)
*/
public function registerStylesAndScripts()
{
// First we register the different needed packages
// Bootstrap Registration
// We don't want to use bootstrap extension's register functionality, to be able to set dependencies between packages
// ie: to control load order setting 'depends' in our package
// So, we take the usual Bootstrap extensions TbApi::register (called normally with App()->bootstrap->register()) see: https://github.com/LimeSurvey/LimeSurvey/blob/master/application/extensions/bootstrap/components/TbApi.php#l162-l169
// keep here the necessary (registerMetaTag and registerAllScripts),
// and move the rest to the bootstrap package.
// NB: registerAllScripts could be replaced by js definition in package. If needed: not a problem to do it
if (!Yii::app()->request->getQuery('isAjax', false))
{
Yii::app()->getClientScript()->registerMetaTag('width=device-width, initial-scale=1.0', 'viewport'); // See: https://github.com/LimeSurvey/LimeSurvey/blob/master/application/extensions/bootstrap/components/TbApi.php#l108-l115
App()->bootstrap->registerAllScripts(); // See : https://github.com/LimeSurvey/LimeSurvey/blob/master/application/extensions/bootstrap/components/TbApi.php#l153-l160
App()->getClientScript()->registerPackage('jqueryui'); // jqueryui
App()->getClientScript()->registerPackage('jquery-cookie'); // jquery-cookie
App()->getClientScript()->registerPackage('fontawesome'); // fontawesome ??? TODO: check if neede
}
$aCssFiles = array();
$aJsFiles= array();
// Then we add the different CSS/JS files to load in arrays
// It will check if it needs or not the RTL files
// and it will add the directory prefix to the file name (css/ or js/ )
// This last step is needed for the package (yii package use a single baseUrl / basePath for css and js files )
// We check if RTL is needed
if (getLanguageRTL(Yii::app()->language))
{
if (!isset($this->config->files->rtl)
|| !isset($this->config->files->rtl->css))
{
throw new CException("Invalid template configuration: No CSS files found for right-to-left languages");
}
foreach ($this->config->files->rtl->css->filename as $cssfile)
{
$aCssFiles[] = 'css/'.$cssfile; // add the 'css/' prefix to the RTL css files
}
$aCssFiles[] = 'css/adminstyle-rtl.css'; // This file is needed for rtl
}
else
{
// Non-RTL style
foreach($this->config->files->css->filename as $cssfile)
{
$aCssFiles[] = 'css/'.$cssfile; // add the 'css/' prefix to the css files
}
}
foreach($this->config->files->js->filename as $jsfile)
{
$aJsFiles[] = 'scripts/'.$jsfile; // add the 'js/' prefix to the RTL css files
}
$package = array();
// We check if the asset manager should be use.
// When defining the package with a base path (a directory on the file system), the asset manager is used
// When defining the package with a base url, the file is directly registerd without the asset manager
// See : http://www.yiiframework.com/doc/api/1.1/CClientScript#packages-detail
if( !YII_DEBUG || self::$use_asset_manager || Yii::app()->getConfig('use_asset_manager'))
{
Yii::setPathOfAlias('admin.theme.path', $this->path);
$package['basePath'] = 'admin.theme.path'; // add the base path to the package, so it will use the asset manager
}
else
{
$package['baseUrl'] = $this->sTemplateUrl; // add the base url to the package, so it will not use the asset manager
}
$package['css'] = $aCssFiles; // add the css files to the package
$package['js'] = $aJsFiles; // add the js files to the package
$package['depends'] = array('bootstrap');
Yii::app()->clientScript->addPackage( 'admin-theme', $package); // add the package
Yii::app()->clientScript->registerPackage('admin-theme'); // register the package
Yii::app()->clientScript->registerPackage('moment'); // register moment for correct dateTime calculation
}
/**
* Register a JS File from the correct directory (publict style, style, upload, etc) using the correct method (with / whithout asset manager)
* This function is called from the different controllers when they want to register a specific css file.
*
* @var string $sPath 'PUBLIC' for /styles-public/, else templates/styles ('PUBLIC' is an heritage from 2.06, which was using constants to that goal.)
* @var string $sFile the name of the css file
*/
public function registerCssFile( $sPath='template', $sFile='' )
{
// We check if we should use the asset manager or not
if (!YII_DEBUG || self::$use_asset_manager || Yii::app()->getConfig('use_asset_manager'))
{
$path = ($sPath == 'PUBLIC')?dirname(Yii::app()->request->scriptFile).'/styles-public/':$this->path . '/css/'; // We get the wanted path
App()->getClientScript()->registerCssFile( App()->getAssetManager()->publish($path.$sFile) ); // We publish the asset
}
else
{
$url = ($sPath == 'PUBLIC')?Yii::app()->getConfig('publicstyleurl'):$this->sTemplateUrl.'/css/'; // We get the wanted url
App()->getClientScript()->registerCssFile( $url.$sFile ); // We publish the css file
}
}
/**
* @param string $cPATH
* @param string $sFile
*/
public function registerScriptFile( $cPATH, $sFile )
{
self::staticRegisterScriptFile( $cPATH, $sFile );
}
/**
* Register a Css File from the correct directory (publict style, style, upload, etc) using the correct method (with / whithout asset manager)
* This function is called from the different controllers when they want to register a specific css file
* Static method is necessary to avoid to init the admin theme to register admin_core script... (see: AdminController::_init())
*
* @var string $sPath 'SCRIPT_PATH' for root/scripts/ ; 'ADMIN_SCRIPT_PATH' for root/scripts/admin/; else templates/scripts (uppercase is an heritage from 2.06, which was using constants )
* @var string $sFile the name of the js file
* @param string $cPATH
* @param string $sFile
*/
static public function staticRegisterScriptFile( $cPATH, $sFile )
{
$bIsInAdminTheme = !($cPATH == 'ADMIN_SCRIPT_PATH' || $cPATH == 'SCRIPT_PATH'); // we check if the path required is in Admin Theme itself.
if (!$bIsInAdminTheme) // If not, it's or a normal script (like ranking.js) or an admin script
{
$sAdminScriptPath = realpath ( Yii::app()->basePath .'/../scripts/admin/') . '/';
$sScriptPath = realpath ( Yii::app()->basePath .'/../scripts/') . '/';
$path = ($cPATH == 'ADMIN_SCRIPT_PATH')?$sAdminScriptPath:$sScriptPath; // We get the wanted path
$url = ($cPATH == 'ADMIN_SCRIPT_PATH')?Yii::app()->getConfig('adminscripts'):Yii::app()->getConfig('generalscripts'); // We get the wanted url defined in config
}
else
{
$path = $this->path.'/scripts/';
$url = $this->sTemplateUrl.'/scripts/';
}
// We check if we should use the asset manager or not
if (!YII_DEBUG || self::$use_asset_manager || Yii::app()->getConfig('use_asset_manager'))
{
App()->getClientScript()->registerScriptFile( App()->getAssetManager()->publish( $path . $sFile )); // We publish the asset
}
else
{
App()->getClientScript()->registerScriptFile( $url . $sFile ); // We publish the script
}
}
/**
* Get instance of theme object.
* Will instantiate the Admin Theme object first time it is called.
* Please use this instead of global variable.
* @return AdminTheme
*/
public static function getInstance()
{
if (empty(self::$instance))
{
self::$instance = new self();
self::$instance->setAdminTheme();
}
return self::$instance;
}
/**
* Touch each directory in standard admin theme directory to force assset manager to republish them
* NB: This function still makes problem, because it's touching direcories inside application, which could be unwritable.
* But: if people used comfortUpdate, the probably made it writable
* TODO: check if 'force' parameter could be used to publish new assets here
* see: http://www.yiiframework.com/doc/api/1.1/CAssetManager#forceCopy-detail
*/
public static function forceAssets()
{
// Don't touch symlinked assets because it won't work
if (App()->getAssetManager()->linkAssets) return;
// Touch all the admin themes
$standardTemplatesPath = Yii::app()->getConfig("styledir");
self::touchSubDirectories($standardTemplatesPath);
//Touch all the assets folders of extensions and third party
$otherAssets = self::getOtherAssets();
$sRootDir = App()->getConfig("rootdir");
foreach($otherAssets as $otherAsset )
{
$sDirToTouch = $sRootDir . DIRECTORY_SEPARATOR . $otherAsset;
if ( is_dir($sDirToTouch))
{
if (is_writable($sDirToTouch))
touch($sDirToTouch);
}
}
// Touch all the root folders of third party
$sPath = $sRootDir . DIRECTORY_SEPARATOR . 'third_party';
self::touchSubDirectories($sPath);
//Touch all the root folders of extensions
$sPath = $sRootDir . DIRECTORY_SEPARATOR . 'application'. DIRECTORY_SEPARATOR .'extensions';
self::touchSubDirectories($sPath);
}
public static function touchSubDirectories( $sPath )
{
$Resource = opendir($sPath);
while ($Item = readdir($Resource))
{
if (is_dir($sPath . DIRECTORY_SEPARATOR . $Item) && $Item != "." && $Item != "..")
{
if (is_writable($sPath . DIRECTORY_SEPARATOR . $Item))
touch($sPath . DIRECTORY_SEPARATOR . $Item);
}
}
}
public static function getOtherAssets()
{
return array(
// Extension assets
'application/extensions/yiiwheels/assets',
'application/extensions/yiiwheels/widgets/box/assets',
'application/extensions/yiiwheels/widgets/grid/assets',
'application/extensions/yiiwheels/widgets/formhelpers/assets',
'application/extensions/yiiwheels/widgets/highcharts/assets',
'application/extensions/yiiwheels/widgets/maskinput/assets',
'application/extensions/yiiwheels/widgets/redactor/assets',
'application/extensions/yiiwheels/widgets/switch/assets',
'application/extensions/yiiwheels/widgets/fineuploader/assets',
'application/extensions/yiiwheels/widgets/datetimepicker/assets',
'application/extensions/yiiwheels/widgets/timeago/assets',
'application/extensions/yiiwheels/widgets/sparklines/assets',
'application/extensions/yiiwheels/widgets/datepicker/assets',
'application/extensions/yiiwheels/widgets/multiselect/assets',
'application/extensions/yiiwheels/widgets/gallery/assets',
'application/extensions/yiiwheels/widgets/select2/assets',
'application/extensions/yiiwheels/widgets/ace/assets',
'application/extensions/yiiwheels/widgets/modal/assets',
'application/extensions/yiiwheels/widgets/maskmoney/assets',
'application/extensions/yiiwheels/widgets/rangeslider/assets',
'application/extensions/yiiwheels/widgets/fileupload/assets',
'application/extensions/yiiwheels/widgets/typeahead/assets',
'application/extensions/yiiwheels/widgets/timepicker/assets',
'application/extensions/yiiwheels/widgets/html5editor/assets',
'application/extensions/yiiwheels/widgets/daterangepicker/assets',
'application/extensions/bootstrap/assets',
'application/extensions/LimeScript/assets',
'application/extensions/SettingsWidget/assets',
'application/extensions/FlashMessage/assets',
'application/extensions/admin/survey/question/PositionWidget/assets',
'application/extensions/admin/grid/MassiveActionsWidget/assets',
'application/extensions/admin/survey/question/PositionWidget/assets',
//'application/extensions/bootstrap/', we'll touch all the subdirectories of extensions
// Third party assets
'third_party/jquery-tablesorter/tests/assets',
'third_party/jquery-tablesorter/docs/assets',
'third_party/fancytree'
);
}
/**
* Return an array containing the configuration object of all templates in a given directory
*
* @param string $sDir the directory to scan
* @return array the array of object
*/
static private function getThemeList($sDir)
{
$bOldEntityLoaderState = libxml_disable_entity_loader(true); // @see: http://phpsecurity.readthedocs.io/en/latest/Injection-Attacks.html#xml-external-entity-injection
$aListOfFiles = array();
if ($sDir && $pHandle = opendir($sDir))
{
while (false !== ($file = readdir($pHandle)))
{
if (is_dir($sDir.DIRECTORY_SEPARATOR.$file) && is_file($sDir.DIRECTORY_SEPARATOR.$file.DIRECTORY_SEPARATOR.'config.xml'))
{
$sXMLConfigFile = file_get_contents( realpath ($sDir.DIRECTORY_SEPARATOR.$file.'/config.xml')); // Now that entity loader is disabled, we can't use simplexml_load_file; so we must read the file with file_get_contents and convert it as a string
// Simple Xml is buggy on PHP < 5.4. The [ array -> json_encode -> json_decode ] workaround seems to be the most used one.
// @see: http://php.net/manual/de/book.simplexml.php#105330 (top comment on PHP doc for simplexml)
$oTemplateConfig = json_decode( json_encode ( ( array ) simplexml_load_string($sXMLConfigFile), 1));
$aListOfFiles[$file] = $oTemplateConfig;
}
}
closedir($pHandle);
}
libxml_disable_entity_loader($bOldEntityLoaderState);
return $aListOfFiles;
}
/**
* Few constants depending on Template
*/
private function defineConstants()
{
// Define images url
if (!YII_DEBUG || self::$use_asset_manager || Yii::app()->getConfig('use_asset_manager'))
{
define('LOGO_URL', App()->getAssetManager()->publish( $this->path . '/images/logo.png'));
}
else
{
define('LOGO_URL', $this->sTemplateUrl.'/images/logo.png');
}
// Define presentation text on welcome page
if (isset($this->config->metadatas->presentation) && $this->config->metadatas->presentation)
{
define('PRESENTATION', $this->config->metadatas->presentation);
}
else
{
define('PRESENTATION', gT('This is the LimeSurvey admin interface. Start to build your survey from here.'));
}
}
/**
* Use to check if admin theme is standard
*
* @var string $sAdminThemeName the name of the template
* @return boolean return true if it's a standard template, else false
*/
private function isStandardAdminTheme($sAdminThemeName)
{
return in_array($sAdminThemeName,
array(
'Apple_Blossom',
'Bay_of_Many',
'Black_Pearl',
'Dark_Sky',
'Free_Magenta',
'Noto_All_Languages',
'Purple_Tentacle',
'Sea_Green',
'Sunset_Orange',
)
);
}
}