1 <?php
2 3 4 5 6 7
8
9 namespace Hawk;
10
11 12 13 14 15
16 class Theme{
17 18 19
20 const DEFAULT_THEME = 'hawk';
21
22 23 24
25 const LESS_BASENAME = 'theme.less';
26
27 28 29
30 const COMPILED_CSS_BASENAME = 'theme.css';
31
32 33 34
35 const CSS_CUSTOM_BASENAME = 'theme-custom.css';
36
37 38 39
40 const MANIFEST_BASENAME = 'manifest.json';
41
42 43 44
45 const PREVIEW_BASENAME = 'preview.png';
46
47 48 49
50 const NAME_PATTERN = '[a-zA-Z0-9\-_.]+';
51
52 53 54
55 const EDITABLE_VARS_PATTERN = '#^\s*@([\w\-]+)\s*\:\s*(.+?)\s*\;\s*//\s*editable\s*\:\s*"(.+?)"\s*\,?\s*(color|file)?\s*$#m';
56
57 58 59 60 61
62 private $name,
63
64 65 66 67 68
69 $data,
70
71 72 73 74 75
76 $parent = null;
77
78 79 80 81 82
83 public static $nativeThemes = array('hawk', 'dark');
84
85 86 87 88 89
90 private static $themes;
91
92
93 94 95 96 97 98 99
100 public static function get($name = self::DEFAULT_THEME){
101 try{
102 if(!isset($themes[$name])) {
103 self::$themes[$name] = new self($name);
104 }
105 return self::$themes[$name];
106 }
107 catch(\Exception $e){
108 return null;
109 }
110 }
111
112
113 114 115 116 117
118 public static function getSelected(){
119 return self::get(App::conf()->has('db') ? Option::get('main.selected-theme') : self::DEFAULT_THEME);
120 }
121
122 123 124 125 126
127 public static function setSelected($name){
128 Option::set('main.selected-theme', $name);
129 }
130
131 132 133 134 135
136 public static function getAll(){
137 foreach(glob(THEMES_DIR . '*', GLOB_ONLYDIR) as $theme){
138 self::get(basename($theme));
139 }
140
141 return self::$themes;
142 }
143
144 145 146 147 148
149 private function __construct($name){
150 $this->name = $name;
151
152 $this->getDefinition();
153
154 if(isset($this->data['extends'])) {
155 $this->parent = self::get($this->data['extends']);
156 }
157 elseif($this->name != self::DEFAULT_THEME) {
158 $this->parent = self::get(self::DEFAULT_THEME);
159 }
160 }
161
162
163 164 165 166 167 168 169 170
171 public function getDefinition($prop = ""){
172 if(!isset($this->data)) {
173 if(!is_file($this->getRootDir() . self::MANIFEST_BASENAME)) {
174 throw new \Exception('Impossible to get the manifest.json file for the theme ' . $this->name . ' : No such file or directory');
175 }
176 $this->data = json_decode(file_get_contents($this->getRootDir() . self::MANIFEST_BASENAME), true);
177 }
178
179 if($prop) {
180 return isset($this->data[$prop]) ? $this->data[$prop] : null;
181 }
182 else{
183 return $this->data;
184 }
185 }
186
187
188 189 190 191 192
193 public function getTitle(){
194 return $this->getDefinition('title');
195 }
196
197
198 199 200 201 202
203 public function getName(){
204 return $this->name;
205 }
206
207
208 209 210 211 212
213 public function isRemovable(){
214 return !in_array($this->name, self::$nativeThemes) && self::getSelected() != $this;
215 }
216
217
218 219 220 221 222
223 public function getStartFile(){
224 return $this->getRootDir() . 'start.php';
225 }
226
227
228 229 230 231 232
233 public function getRootDir(){
234 return THEMES_DIR . $this->name . '/';
235 }
236
237
238 239 240 241 242
243 public function getStaticDir(){
244 return STATIC_THEMES_DIR . $this->name . '/';
245 }
246
247
248 249 250 251 252
253 public function getRootUrl(){
254 return THEMES_ROOT_URL . $this->name . '/';
255 }
256
257
258 259 260 261 262 263 264
265 public function getFileUrl($file){
266 $privateFile = $this->getRootDir() . $file;
267 $publicFile = $this->getStaticDir() . $file;
268
269 if(!is_file($privateFile)) {
270 throw new \Exception('Impossible to get the URL for the file ' . $privateFile . ' : No such file or directory');
271 }
272
273 if(!is_file($publicFile) || filemtime($publicFile) < filemtime($privateFile)) {
274 if(!is_dir(dirname($publicFile))) {
275 mkdir(dirname($publicFile), 0755, true);
276 }
277 App::fs()->copy($privateFile, $publicFile);
278 }
279
280 return $this->getRootUrl() . $file . '?' . filemtime($publicFile);
281 }
282
283
284 285 286 287 288
289 public function getPreviewFilename(){
290 return $this->getRootDir() . self::PREVIEW_BASENAME;
291 }
292
293
294 295 296 297 298
299 public function getPreviewUrl(){
300 return $this->getFileUrl(self::PREVIEW_BASENAME);
301 }
302
303
304 305 306
307 public function getLessDirname(){
308 return $this->getRootDir() . 'less/';
309 }
310
311 312 313 314 315
316 public function getBaseLessFile(){
317 return $this->getLessDirname() . self::LESS_BASENAME;
318 }
319
320
321 322 323 324 325
326 public function getStaticLessFile(){
327 return $this->getStaticDir() . 'less/' . self::LESS_BASENAME;
328 }
329
330
331 332 333 334 335
336 public function getStaticCssFile(){
337 return $this->getStaticDir() . 'less/' . self::COMPILED_CSS_BASENAME;
338 }
339
340
341
342 343 344 345 346
347 public function getBaseLessUrl(){
348 $this->build();
349
350 return $this->getRootUrl() . 'less/' . self::LESS_BASENAME . '?' . filemtime($this->getStaticLessFile());
351 }
352
353
354 355 356 357 358
359 public function getBaseCssUrl(){
360 $this->build();
361
362 return $this->getRootUrl() . 'less/' . self::COMPILED_CSS_BASENAME . '?' . filemtime($this->getStaticCssFile());
363 }
364
365
366 367 368 369 370 371 372
373 public function build($force = false){
374 if($this->getDefinition('extends')) {
375 if(Theme::get($this->getDefinition('extends'))) {
376 Theme::get($this->getDefinition('extends'))->build($force);
377 }
378 }
379 $build = false;
380 if($force) {
381 $build = true;
382 }
383
384 if(!file_exists($this->getStaticDir())) {
385 mkdir($this->getStaticDir(), 0755, true);
386 $build = true;
387 }
388
389
390 if(!$build) {
391 $dest = $this->getStaticCssFile();
392
393 if(!is_file($dest)) {
394 $build = true;
395 }
396 else{
397
398 $files = App::fs()->find($this->getLessDirname(), '*.less');
399 $lastUpdate = filemtime($dest);
400 foreach($files as $file){
401 if(filemtime($file) > $lastUpdate) {
402 $build = true;
403 break;
404 }
405 }
406 }
407 }
408
409 if($build) {
410
411 foreach(glob($this->getRootDir() . '*') as $elt){
412 if(! in_array(basename($elt), array('views', 'start.php'))) {
413 App::fs()->copy($elt, $this->getStaticDir());
414 }
415 }
416
417
418 $values = $this->getVariablesCustomValues();
419 $precompiledLess = preg_replace_callback(
420 self::EDITABLE_VARS_PATTERN, function ($m) use ($values) {
421 return '@' . $m[1] . ' : ' . (isset($values[$m[1]]) ? $values[$m[1]] : $m[2]) . ';';
422 }, file_get_contents($this->getBaseLessFile())
423 );
424
425 file_put_contents($this->getStaticLessFile(), $precompiledLess);
426
427 Less::compile($this->getStaticLessFile(), $this->getStaticCssFile());
428 }
429
430 return $build;
431 }
432
433
434 435 436 437 438 439 440 441
442 public function getEditableVariables($less = null){
443 if(!$less) {
444 $less = file_get_contents($this->getBaseLessFile());
445 }
446 preg_match_all(self::EDITABLE_VARS_PATTERN, $less, $matches, PREG_SET_ORDER);
447 $variables = array();
448 foreach($matches as $match){
449
450 preg_match_all('#^{a-z}{/a-z}$#', $match[3], $matches_description, PREG_SET_ORDER);
451
452 App::logger()->error('match=');
453 $description = array();
454 foreach($matches_description as $match_description){
455 App::logger()->error('match=' . $match_description[1] . "=" . $match_description[2]);
456 $description[$match_description[1]] = $match_description[2];
457 }
458
459 $variables[] = array(
460 'name' => $match[1],
461 'default' => $match[2],
462 'description' => $match[3],
463 'type' => isset($match[4]) ? $match[4] : ''
464 );
465 }
466 return $variables;
467 }
468
469
470 471 472 473 474
475 public function getVariablesCustomValues(){
476 $options = Option::getPluginOptions('theme-' . $this->name);
477
478 $values = array();
479 foreach($options as $key => $value){
480 if(preg_match('/^custom\-value\-(.+?)$/', $key, $m)) {
481 $values[$m[1]] = $value;
482 }
483 }
484
485 return $values;
486 }
487
488
489 490 491 492 493
494 public function setVariablesCustomValues($values){
495 foreach($values as $key => $value){
496 $varname = 'custom-value-' . $key;
497 Option::set('theme-' . $this->name . '.' . $varname, $value);
498 }
499 }
500
501 502 503
504 public function getStaticUserfilesDir(){
505 return $this->getStaticDir() . 'userfiles/';
506 }
507
508
509 510 511 512 513 514 515
516 public function getStaticUserfilesUrl($filename = ''){
517 return $this->getRootUrl() . 'userfiles/' . $filename;
518 }
519
520
521 522 523 524 525
526 public function getMediasDir(){
527 return $this->getStaticUserfilesDir() . 'medias/';
528 }
529
530 531 532 533 534 535 536
537 public function getMediasUrl($filename = ''){
538 return $this->getStaticUserfilesUrl('medias/' . $filename);
539 }
540
541
542 543 544 545 546
547 public function getCustomCssFile(){
548 return $this->getStaticUserfilesDir() . self::CSS_CUSTOM_BASENAME;
549 }
550
551
552 553 554 555 556
557 public function getCustomCssUrl(){
558 $file = $this->getCustomCssFile();
559 if(!is_dir(dirname($file))) {
560 mkdir(dirname($file), 0775, true);
561 }
562
563 if(!is_file($file)) {
564 file_put_contents($file, '');
565 }
566
567 return $this->getStaticUserfilesUrl(self::CSS_CUSTOM_BASENAME);
568 }
569
570
571 572 573 574 575
576 public function getViewsDir(){
577 return $this->getRootDir() . 'views/';
578 }
579
580
581 582 583 584 585 586 587
588 public function getView($filename){
589 $file = $this->getViewsDir() . $filename;
590 if(!is_file($file) && $this->name != self::DEFAULT_THEME) {
591 if($this->parent) {
592
593 $file = $this->parent->getView($filename);
594 }
595 else{
596 $file = self::get(self::DEFAULT_THEME)->getView($filename);
597 }
598 }
599 return $file;
600 }
601 }