Use the OS' native font by default
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / acp / form / StyleAddForm.class.php
1 <?php
2
3 namespace wcf\acp\form;
4
5 use wcf\data\package\Package;
6 use wcf\data\style\Style;
7 use wcf\data\style\StyleAction;
8 use wcf\data\style\StyleEditor;
9 use wcf\data\template\group\TemplateGroup;
10 use wcf\data\user\cover\photo\UserCoverPhoto;
11 use wcf\form\AbstractForm;
12 use wcf\system\event\EventHandler;
13 use wcf\system\exception\SystemException;
14 use wcf\system\exception\UserInputException;
15 use wcf\system\file\upload\UploadField;
16 use wcf\system\file\upload\UploadHandler;
17 use wcf\system\image\ImageHandler;
18 use wcf\system\language\I18nHandler;
19 use wcf\system\Regex;
20 use wcf\system\request\LinkHandler;
21 use wcf\system\style\exception\FontDownloadFailed;
22 use wcf\system\style\FontManager;
23 use wcf\system\style\StyleCompiler;
24 use wcf\system\WCF;
25 use wcf\util\ArrayUtil;
26 use wcf\util\DateUtil;
27 use wcf\util\FileUtil;
28 use wcf\util\StringUtil;
29
30 /**
31 * Shows the style add form.
32 *
33 * @author Alexander Ebert
34 * @copyright 2001-2019 WoltLab GmbH
35 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
36 * @package WoltLabSuite\Core\Acp\Form
37 */
38 class StyleAddForm extends AbstractForm
39 {
40 /**
41 * @inheritDoc
42 */
43 public $activeMenuItem = 'wcf.acp.menu.link.style.add';
44
45 /**
46 * author's name
47 * @var string
48 */
49 public $authorName = '';
50
51 /**
52 * author's URL
53 * @var string
54 */
55 public $authorURL = '';
56
57 /**
58 * style api version
59 * @var string
60 */
61 public $apiVersion = Style::API_VERSION;
62
63 /**
64 * list of available font families
65 * @var string[]
66 */
67 public $availableFontFamilies = [
68 'Arial, Helvetica, sans-serif' => 'Arial',
69 'Chicago, Impact, Compacta, sans-serif' => 'Chicago',
70 '"Comic Sans MS", sans-serif' => 'Comic Sans',
71 '"Courier New", Courier, monospace' => 'Courier New',
72 'Geneva, Arial, Helvetica, sans-serif' => 'Geneva',
73 'Georgia, "Times New Roman", Times, serif' => 'Georgia',
74 'Helvetica, Verdana, sans-serif' => 'Helvetica',
75 'Impact, Compacta, Chicago, sans-serif' => 'Impact',
76 '"Lucida Sans", "Lucida Grande", Monaco, Geneva, sans-serif' => 'Lucida',
77 '"Segoe UI", "DejaVu Sans", "Lucida Grande", Helvetica, sans-serif' => 'Segoe UI',
78 'system' => 'System',
79 'Tahoma, Arial, Helvetica, sans-serif' => 'Tahoma',
80 '"Times New Roman", Times, Georgia, serif' => 'Times New Roman',
81 '"Trebuchet MS", Arial, sans-serif' => 'Trebuchet MS',
82 'Verdana, Helvetica, sans-serif' => 'Verdana',
83 ];
84
85 /**
86 * list of available template groups
87 * @var TemplateGroup[]
88 */
89 public $availableTemplateGroups = [];
90
91 /**
92 * list of available units
93 * @var string[]
94 */
95 public $availableUnits = ['px', 'pt', 'rem', 'em', '%'];
96
97 /**
98 * @var array
99 */
100 public $colorCategories = [];
101
102 /**
103 * list of color variables
104 * @var string[][]
105 */
106 public $colors = [];
107
108 /**
109 * copyright message
110 * @var string
111 */
112 public $copyright = '';
113
114 /**
115 * list of global variables
116 * @var array
117 */
118 public $globals = [];
119
120 /**
121 * tainted style
122 * @var bool
123 */
124 public $isTainted = true;
125
126 /**
127 * license name
128 * @var string
129 */
130 public $license = '';
131
132 /**
133 * @inheritDoc
134 */
135 public $neededPermissions = ['admin.style.canManageStyle'];
136
137 /**
138 * list of variables that were added after 3.0
139 * @var string[]
140 */
141 public $newVariables = [
142 // 3.1
143 'wcfContentContainerBackground' => '3.1',
144 'wcfContentContainerBorder' => '3.1',
145 'wcfEditorButtonBackground' => '3.1',
146 'wcfEditorButtonBackgroundActive' => '3.1',
147 'wcfEditorButtonText' => '3.1',
148 'wcfEditorButtonTextActive' => '3.1',
149 'wcfEditorButtonTextDisabled' => '3.1',
150
151 // 5.2
152 'wcfEditorTableBorder' => '5.2',
153 ];
154
155 /**
156 * style package name
157 * @var string
158 */
159 public $packageName = '';
160
161 /**
162 * last change date
163 * @var string
164 */
165 public $styleDate = '0000-00-00';
166
167 /**
168 * description
169 * @var string
170 */
171 public $styleDescription = '';
172
173 /**
174 * style name
175 * @var string
176 */
177 public $styleName = '';
178
179 /**
180 * version number
181 * @var string
182 */
183 public $styleVersion = '';
184
185 /**
186 * template group id
187 * @var int
188 */
189 public $templateGroupID = 0;
190
191 /**
192 * temporary image hash
193 * @var string
194 */
195 public $tmpHash = '';
196
197 /**
198 * @var string
199 */
200 public $styleTestFileDir;
201
202 /**
203 * list of variables and their value
204 * @var string[]
205 */
206 public $variables = [];
207
208 /**
209 * list of specialized variables
210 * @var string[]
211 */
212 public $specialVariables = [];
213
214 /**
215 * current scroll offsets before submitting the form
216 * @var int[]
217 */
218 public $scrollOffsets = [];
219
220 /**
221 * @var (null|UploadField)[]
222 * @since 5.3
223 */
224 public $uploads = [];
225
226 /**
227 * @var (null|UploadField)[]
228 * @since 5.3
229 */
230 public $removedUploads = [];
231
232 /**
233 * @var UploadField[]
234 * @since 5.3
235 */
236 public $customAssets = [];
237
238 public $supportedApiVersionsCompatibility = [
239 '3.0' => '3.0',
240 '3.1' => '3.1',
241 '5.2' => '5.2 / 5.3',
242 ];
243
244 /**
245 * @inheritDoc
246 */
247 public function readParameters()
248 {
249 parent::readParameters();
250
251 I18nHandler::getInstance()->register('styleDescription');
252
253 $this->setVariables();
254
255 $this->rebuildUploadFields();
256
257 if (empty($_POST)) {
258 $this->readStyleVariables();
259 }
260
261 $this->availableTemplateGroups = TemplateGroup::getSelectList([-1], 1);
262
263 if (isset($_REQUEST['tmpHash'])) {
264 $this->tmpHash = StringUtil::trim($_REQUEST['tmpHash']);
265 }
266 if (empty($this->tmpHash)) {
267 $this->tmpHash = StringUtil::getRandomID();
268 }
269 }
270
271 /**
272 * @since 5.3
273 */
274 protected function getUploadFields()
275 {
276 return [
277 'image' => [
278 'size' => [
279 'maxWidth' => Style::PREVIEW_IMAGE_MAX_WIDTH,
280 'maxHeight' => Style::PREVIEW_IMAGE_MAX_WIDTH,
281 'preserveAspectRatio' => false,
282 ],
283 ],
284 'image2x' => [
285 'size' => [
286 'maxWidth' => 2 * Style::PREVIEW_IMAGE_MAX_WIDTH,
287 'maxHeight' => 2 * Style::PREVIEW_IMAGE_MAX_WIDTH,
288 'preserveAspectRatio' => false,
289 ],
290 ],
291 'pageLogo' => [
292 'allowSvgImage' => true,
293 ],
294 'pageLogoMobile' => [
295 'allowSvgImage' => true,
296 ],
297 'coverPhoto' => [
298 'size' => [
299 'minWidth' => UserCoverPhoto::MIN_WIDTH,
300 'maxWidth' => UserCoverPhoto::MAX_WIDTH,
301 'minHeight' => UserCoverPhoto::MIN_HEIGHT,
302 'maxHeight' => UserCoverPhoto::MAX_HEIGHT,
303 ],
304 ],
305 'favicon' => [
306 'size' => [
307 'resize' => false,
308 'minWidth' => Style::FAVICON_IMAGE_WIDTH,
309 'maxWidth' => Style::FAVICON_IMAGE_WIDTH,
310 'minHeight' => Style::FAVICON_IMAGE_HEIGHT,
311 'maxHeight' => Style::FAVICON_IMAGE_HEIGHT,
312 ],
313 ],
314 ];
315 }
316
317 /**
318 * @since 5.3
319 */
320 protected function rebuildUploadFields()
321 {
322 $handler = UploadHandler::getInstance();
323 foreach ($this->getUploadFields() as $name => $options) {
324 if ($handler->isRegisteredFieldId($name)) {
325 $handler->unregisterUploadField($name);
326 }
327 $field = new UploadField($name);
328 $field->setImageOnly(true);
329 if (isset($options['allowSvgImage'])) {
330 $field->setAllowSvgImage($options['allowSvgImage']);
331 }
332 $field->maxFiles = 1;
333 $handler->registerUploadField($field);
334 }
335
336 // This field is special cased, because it may contain arbitrary data.
337 $name = 'customAssets';
338 if ($handler->isRegisteredFieldId($name)) {
339 $handler->unregisterUploadField($name);
340 }
341 $field = new UploadField($name);
342 $field->setImageOnly(true);
343 $field->setAllowSvgImage(true);
344 $field->maxFiles = null;
345 $handler->registerUploadField($field);
346 }
347
348 /**
349 * @inheritDoc
350 */
351 public function readFormParameters()
352 {
353 parent::readFormParameters();
354
355 I18nHandler::getInstance()->readValues();
356
357 $colors = [];
358 foreach ($this->colors as $categoryName => $variables) {
359 foreach ($variables as $variable) {
360 $colors[] = $categoryName . \ucfirst($variable);
361 }
362 }
363
364 // ignore everything except well-formed rgba()
365 $regEx = new Regex('rgba\(\d{1,3}, \d{1,3}, \d{1,3}, (1|1\.00?|0|0?\.[0-9]{1,2})\)');
366 foreach ($colors as $variableName) {
367 if (isset($_POST[$variableName]) && $regEx->match($_POST[$variableName])) {
368 $this->variables[$variableName] = $_POST[$variableName];
369 }
370 }
371
372 // read variables with units, e.g. 13px
373 foreach ($this->globals as $variableName) {
374 if (isset($_POST[$variableName]) && \is_numeric($_POST[$variableName])) {
375 if (
376 isset($_POST[$variableName . '_unit']) && \in_array(
377 $_POST[$variableName . '_unit'],
378 $this->availableUnits
379 )
380 ) {
381 $this->variables[$variableName] = \abs($_POST[$variableName]) . $_POST[$variableName . '_unit'];
382 }
383 } else {
384 // set default value
385 $this->variables[$variableName] = '0px';
386 }
387 }
388
389 // read specialized variables
390 $integerValues = ['pageLogoHeight', 'pageLogoWidth'];
391 foreach ($this->specialVariables as $variableName) {
392 if (isset($_POST[$variableName])) {
393 $this->variables[$variableName] = (\in_array(
394 $variableName,
395 $integerValues
396 )) ? \abs(\intval($_POST[$variableName])) : StringUtil::trim($_POST[$variableName]);
397 }
398 }
399 $this->variables['useFluidLayout'] = isset($_POST['useFluidLayout']) ? 1 : 0;
400
401 // style data
402 if (isset($_POST['authorName'])) {
403 $this->authorName = StringUtil::trim($_POST['authorName']);
404 }
405 if (isset($_POST['authorURL'])) {
406 $this->authorURL = StringUtil::trim($_POST['authorURL']);
407 }
408 if (isset($_POST['copyright'])) {
409 $this->copyright = StringUtil::trim($_POST['copyright']);
410 }
411 if (isset($_POST['license'])) {
412 $this->license = StringUtil::trim($_POST['license']);
413 }
414 if (isset($_POST['packageName'])) {
415 $this->packageName = StringUtil::trim($_POST['packageName']);
416 }
417 if (isset($_POST['styleDate'])) {
418 $this->styleDate = StringUtil::trim($_POST['styleDate']);
419 }
420 if (isset($_POST['styleDescription'])) {
421 $this->styleDescription = StringUtil::trim($_POST['styleDescription']);
422 }
423 if (isset($_POST['styleName'])) {
424 $this->styleName = StringUtil::trim($_POST['styleName']);
425 }
426 if (isset($_POST['styleVersion'])) {
427 $this->styleVersion = StringUtil::trim($_POST['styleVersion']);
428 }
429 if (isset($_POST['templateGroupID'])) {
430 $this->templateGroupID = \intval($_POST['templateGroupID']);
431 }
432 if (isset($_POST['apiVersion']) && \in_array($_POST['apiVersion'], Style::$supportedApiVersions)) {
433 $this->apiVersion = $_POST['apiVersion'];
434 }
435
436 // codemirror scroll offset
437 if (isset($_POST['scrollOffsets']) && \is_array($_POST['scrollOffsets'])) {
438 $this->scrollOffsets = ArrayUtil::toIntegerArray($_POST['scrollOffsets']);
439 }
440
441 $this->uploads = $this->removedUploads = [];
442 foreach (\array_keys($this->getUploadFields()) as $field) {
443 $removedFiles = UploadHandler::getInstance()->getRemovedFiledByFieldId($field);
444 if (!empty($removedFiles)) {
445 $this->removedUploads = \array_merge($this->removedUploads, $removedFiles);
446 }
447
448 $files = UploadHandler::getInstance()->getFilesByFieldId($field);
449 if (!empty($files)) {
450 $this->uploads[$field] = $files[0];
451 }
452 }
453
454 $this->customAssets = [
455 'removed' => UploadHandler::getInstance()->getRemovedFiledByFieldId('customAssets'),
456 'added' => UploadHandler::getInstance()->getFilesByFieldId('customAssets'),
457 ];
458 }
459
460 /**
461 * @since 5.3
462 */
463 protected function downloadGoogleFont()
464 {
465 $fontManager = FontManager::getInstance();
466 $family = $this->variables['wcfFontFamilyGoogle'];
467 if ($family) {
468 if (!$fontManager->isFamilyDownloaded($family)) {
469 try {
470 $fontManager->downloadFamily($family);
471 } catch (FontDownloadFailed $e) {
472 throw new UserInputException(
473 'wcfFontFamilyGoogle',
474 'downloadFailed' . ($e->getReason() ? '.' . $e->getReason() : '')
475 );
476 }
477 }
478 }
479 }
480
481 /**
482 * @inheritDoc
483 */
484 public function validate()
485 {
486 parent::validate();
487
488 if (empty($this->authorName)) {
489 throw new UserInputException('authorName');
490 }
491
492 // validate date
493 if (empty($this->styleDate)) {
494 throw new UserInputException('styleDate');
495 } else {
496 try {
497 DateUtil::validateDate($this->styleDate);
498 } catch (SystemException $e) {
499 throw new UserInputException('styleDate', 'invalid');
500 }
501 }
502
503 if (empty($this->styleName)) {
504 throw new UserInputException('styleName');
505 }
506
507 // validate version
508 if (empty($this->styleVersion)) {
509 throw new UserInputException('styleVersion');
510 } elseif (!Package::isValidVersion($this->styleVersion)) {
511 throw new UserInputException('styleVersion', 'invalid');
512 }
513
514 // validate style package name
515 if (!empty($this->packageName)) {
516 if (!Package::isValidPackageName($this->packageName)) {
517 throw new UserInputException('packageName', 'invalid');
518 }
519
520 $this->enforcePackageNameRestriction();
521 }
522
523 // validate template group id
524 if ($this->templateGroupID) {
525 if (!isset($this->availableTemplateGroups[$this->templateGroupID])) {
526 throw new UserInputException('templateGroupID');
527 }
528 }
529
530 if (!empty($this->variables['overrideScss'])) {
531 $this->parseOverrides();
532 }
533
534 $this->downloadGoogleFont();
535
536 $this->validateIndividualScss();
537
538 $this->validateApiVersion();
539
540 $this->validateUploads();
541 }
542
543 /**
544 * Validates the individual scss.
545 * @throws UserInputException
546 * @since 5.3
547 */
548 public function validateIndividualScss()
549 {
550 $variables = \array_merge(StyleCompiler::getDefaultVariables(), $this->variables);
551
552 $this->styleTestFileDir = FileUtil::getTemporaryFilename('style_');
553 FileUtil::makePath($this->styleTestFileDir);
554
555 $result = StyleCompiler::getInstance()->testStyle(
556 $this->styleTestFileDir,
557 $this->styleName,
558 $this->apiVersion,
559 false,
560 $variables
561 );
562
563 if ($result !== null) {
564 \rmdir($this->styleTestFileDir);
565
566 throw new UserInputException('individualScss', [
567 'message' => $result->getMessage(),
568 ]);
569 }
570 }
571
572 /**
573 * Disallow the use of `com.woltlab.*` for package names to avoid accidental collisions.
574 *
575 * @throws UserInputException
576 */
577 protected function enforcePackageNameRestriction()
578 {
579 // 3rd party styles may never have com.woltlab.* as name
580 if (\strpos($this->packageName, 'com.woltlab.') !== false) {
581 throw new UserInputException('packageName', 'reserved');
582 }
583 }
584
585 /**
586 * Validates the style API version.
587 *
588 * @throws UserInputException
589 * @since 3.1
590 */
591 protected function validateApiVersion()
592 {
593 if (!\in_array($this->apiVersion, Style::$supportedApiVersions)) {
594 throw new UserInputException('apiVersion', 'invalid');
595 }
596 }
597
598 /**
599 * @since 5.3
600 */
601 protected function validateUploads()
602 {
603 foreach ($this->getUploadFields() as $field => $options) {
604 $files = UploadHandler::getInstance()->getFilesByFieldId($field);
605 if (\count($files) > 1) {
606 throw new UserInputException($field, 'invalid');
607 }
608 if (empty($files)) {
609 continue;
610 }
611
612 if (isset($options['size'])) {
613 $fileLocation = $files[0]->getLocation();
614 if (($imageData = \getimagesize($fileLocation)) === false) {
615 throw new UserInputException($field, 'invalid');
616 }
617 switch ($imageData[2]) {
618 case \IMAGETYPE_PNG:
619 case \IMAGETYPE_JPEG:
620 case \IMAGETYPE_GIF:
621 // fine
622 break;
623 default:
624 throw new UserInputException($field, 'invalid');
625 }
626
627 $maxWidth = $options['size']['maxWidth'] ?? \PHP_INT_MAX;
628 $maxHeight = $options['size']['maxHeight'] ?? \PHP_INT_MAX;
629 $minWidth = $options['size']['minWidth'] ?? 0;
630 $minHeight = $options['size']['minHeight'] ?? 0;
631
632 if ($options['size']['resize'] ?? true) {
633 if ($imageData[0] > $maxWidth || $imageData[1] > $maxHeight) {
634 $adapter = ImageHandler::getInstance()->getAdapter();
635 $adapter->loadFile($fileLocation);
636 $thumbnail = $adapter->createThumbnail(
637 $maxWidth,
638 $maxHeight,
639 $options['size']['preserveAspectRatio'] ?? true
640 );
641 $adapter->writeImage($thumbnail, $fileLocation);
642 // Clear thumbnail as soon as possible to free up the memory.
643 $thumbnail = null;
644 }
645
646 // Check again after scaling
647 if (($imageData = \getimagesize($fileLocation)) === false) {
648 throw new UserInputException($field, 'invalid');
649 }
650 }
651
652 if ($imageData[0] > $maxWidth) {
653 throw new UserInputException($field, 'maxWidth');
654 }
655 if ($imageData[1] > $maxHeight) {
656 throw new UserInputException($field, 'maxHeight');
657 }
658 if ($imageData[0] < $minWidth) {
659 throw new UserInputException($field, 'minWidth');
660 }
661 if ($imageData[1] < $minHeight) {
662 throw new UserInputException($field, 'minHeight');
663 }
664 }
665 }
666 }
667
668 /**
669 * Validates LESS-variable overrides.
670 *
671 * If an override is invalid, unknown or matches a variable covered by
672 * the style editor itself, it will be silently discarded.
673 *
674 * @param string $variableName
675 * @throws UserInputException
676 */
677 protected function parseOverrides($variableName = 'overrideScss')
678 {
679 static $colorNames = null;
680 if ($colorNames === null) {
681 $colorNames = [];
682 foreach ($this->colors as $colorPrefix => $colors) {
683 foreach ($colors as $color) {
684 $colorNames[] = $colorPrefix . \ucfirst($color);
685 }
686 }
687 }
688
689 // get available variables
690 $sql = "SELECT variableName
691 FROM wcf" . WCF_N . "_style_variable";
692 $statement = WCF::getDB()->prepareStatement($sql);
693 $statement->execute();
694 $variables = $statement->fetchAll(\PDO::FETCH_COLUMN);
695
696 $lines = \explode("\n", StringUtil::unifyNewlines($this->variables[$variableName]));
697 $regEx = new Regex('^\$([a-zA-Z]+):\s*([@a-zA-Z0-9 ,\.\(\)\%\#-]+);$');
698 $errors = [];
699 foreach ($lines as $index => &$line) {
700 $line = StringUtil::trim($line);
701
702 // ignore empty lines
703 if (empty($line)) {
704 unset($lines[$index]);
705 continue;
706 }
707
708 if ($regEx->match($line)) {
709 $matches = $regEx->getMatches();
710
711 // cannot override variables covered by style editor
712 if (
713 \in_array($matches[1], $colorNames) || \in_array(
714 $matches[1],
715 $this->globals
716 ) || \in_array($matches[1], $this->specialVariables)
717 ) {
718 $errors[] = [
719 'error' => 'predefined',
720 'text' => $matches[1],
721 ];
722 } elseif (!\in_array($matches[1], $variables)) {
723 // unknown style variable
724 $errors[] = [
725 'error' => 'unknown',
726 'text' => $matches[1],
727 ];
728 } else {
729 $this->variables[$matches[1]] = $matches[2];
730 }
731 } else {
732 // not valid
733 $errors[] = [
734 'error' => 'invalid',
735 'text' => $line,
736 ];
737 }
738 }
739
740 $this->variables[$variableName] = \implode("\n", $lines);
741
742 if (!empty($errors)) {
743 throw new UserInputException($variableName, $errors);
744 }
745 }
746
747 /**
748 * @inheritDoc
749 */
750 public function readData()
751 {
752 parent::readData();
753
754 // parse global (unit) variables
755 foreach ($this->globals as $variableName) {
756 if (
757 \preg_match(
758 '/(.*?)(' . \implode('|', $this->availableUnits) . ')$/',
759 $this->variables[$variableName],
760 $match
761 )
762 ) {
763 $this->variables[$variableName] = $match[1];
764 $this->variables[$variableName . '_unit'] = $match[2];
765 }
766 }
767
768 if (empty($_POST)) {
769 $this->setDefaultValues();
770 }
771 }
772
773 /**
774 * Sets available variables
775 */
776 protected function setVariables()
777 {
778 $this->colorCategories = [
779 'wcfHeader' => ['wcfHeader', 'wcfHeaderSearchBox', 'wcfHeaderMenu', 'wcfHeaderMenuDropdown'],
780 'wcfNavigation' => 'wcfNavigation',
781 'wcfSidebar' => ['wcfSidebar', 'wcfSidebarDimmed', 'wcfSidebarHeadline'],
782 'wcfContent' => ['wcfContent', 'wcfContentContainer', 'wcfContentDimmed', 'wcfContentHeadline'],
783 'wcfTabularBox' => 'wcfTabularBox',
784 'wcfInput' => ['wcfInput', 'wcfInputDisabled'],
785 'wcfButton' => ['wcfButton', 'wcfButtonPrimary', 'wcfButtonDisabled'],
786 'wcfEditor' => ['wcfEditorButton', 'wcfEditorTable'],
787 'wcfDropdown' => 'wcfDropdown',
788 'wcfStatus' => ['wcfStatusInfo', 'wcfStatusSuccess', 'wcfStatusWarning', 'wcfStatusError'],
789 'wcfFooterBox' => ['wcfFooterBox', 'wcfFooterBoxHeadline'],
790 'wcfFooter' => ['wcfFooter', 'wcfFooterHeadline', 'wcfFooterCopyright'],
791 ];
792
793 $this->colors = [
794 'wcfHeader' => ['background', 'text', 'link', 'linkActive'],
795 'wcfHeaderSearchBox' => [
796 'background',
797 'text',
798 'placeholder',
799 'placeholderActive',
800 'backgroundActive',
801 'textActive',
802 ],
803 'wcfHeaderMenu' => ['background', 'linkBackground', 'linkBackgroundActive', 'link', 'linkActive'],
804 'wcfHeaderMenuDropdown' => ['background', 'link', 'backgroundActive', 'linkActive'],
805 'wcfNavigation' => ['background', 'text', 'link', 'linkActive'],
806 'wcfSidebar' => ['background', 'text', 'link', 'linkActive'],
807 'wcfSidebarDimmed' => ['text', 'link', 'linkActive'],
808 'wcfSidebarHeadline' => ['text', 'link', 'linkActive'],
809 'wcfContent' => ['background', 'border', 'borderInner', 'text', 'link', 'linkActive'],
810 'wcfContentContainer' => ['background', 'border'],
811 'wcfContentDimmed' => ['text', 'link', 'linkActive'],
812 'wcfContentHeadline' => ['border', 'text', 'link', 'linkActive'],
813 'wcfTabularBox' => ['borderInner', 'headline', 'backgroundActive', 'headlineActive'],
814 'wcfInput' => [
815 'label',
816 'background',
817 'border',
818 'text',
819 'placeholder',
820 'placeholderActive',
821 'backgroundActive',
822 'borderActive',
823 'textActive',
824 ],
825 'wcfInputDisabled' => ['background', 'border', 'text'],
826 'wcfButton' => ['background', 'text', 'backgroundActive', 'textActive'],
827 'wcfButtonPrimary' => ['background', 'text', 'backgroundActive', 'textActive'],
828 'wcfButtonDisabled' => ['background', 'text'],
829 'wcfEditorButton' => ['background', 'backgroundActive', 'text', 'textActive', 'textDisabled'],
830 'wcfEditorTable' => ['border'],
831 'wcfDropdown' => ['background', 'borderInner', 'text', 'link', 'backgroundActive', 'linkActive'],
832 'wcfStatusInfo' => ['background', 'border', 'text', 'link', 'linkActive'],
833 'wcfStatusSuccess' => ['background', 'border', 'text', 'link', 'linkActive'],
834 'wcfStatusWarning' => ['background', 'border', 'text', 'link', 'linkActive'],
835 'wcfStatusError' => ['background', 'border', 'text', 'link', 'linkActive'],
836 'wcfFooterBox' => ['background', 'text', 'link', 'linkActive'],
837 'wcfFooterBoxHeadline' => ['text', 'link', 'linkActive'],
838 'wcfFooter' => ['background', 'text', 'link', 'linkActive'],
839 'wcfFooterHeadline' => ['text', 'link', 'linkActive'],
840 'wcfFooterCopyright' => ['background', 'text', 'link', 'linkActive'],
841 ];
842
843 // set global variables
844 $this->globals = [
845 'wcfFontSizeSmall',
846 'wcfFontSizeDefault',
847 'wcfFontSizeHeadline',
848 'wcfFontSizeSection',
849 'wcfFontSizeTitle',
850
851 'wcfLayoutFixedWidth',
852 'wcfLayoutMinWidth',
853 'wcfLayoutMaxWidth',
854 ];
855
856 // set specialized variables
857 $this->specialVariables = [
858 'individualScss',
859 'overrideScss',
860 'pageLogoWidth',
861 'pageLogoHeight',
862 'useFluidLayout',
863 'wcfFontFamilyGoogle',
864 'wcfFontFamilyFallback',
865 ];
866
867 EventHandler::getInstance()->fireAction($this, 'setVariables');
868 }
869
870 /**
871 * Reads style variable values.
872 */
873 protected function readStyleVariables()
874 {
875 $sql = "SELECT variableName, defaultValue
876 FROM wcf" . WCF_N . "_style_variable";
877 $statement = WCF::getDB()->prepareStatement($sql);
878 $statement->execute();
879 $this->variables = $statement->fetchMap('variableName', 'defaultValue');
880 }
881
882 /**
883 * @inheritDoc
884 */
885 public function save()
886 {
887 parent::save();
888
889 // Remove control characters that break the SCSS parser, see https://stackoverflow.com/a/23066553
890 $this->variables['individualScss'] = \preg_replace('/[^\PC\s]/u', '', $this->variables['individualScss']);
891
892 $this->objectAction = new StyleAction([], 'create', [
893 'data' => \array_merge($this->additionalFields, [
894 'styleName' => $this->styleName,
895 'templateGroupID' => $this->templateGroupID,
896 'packageName' => $this->packageName,
897 'isDisabled' => 1, // styles are disabled by default
898 'isTainted' => 1,
899 'styleDescription' => '',
900 'styleVersion' => $this->styleVersion,
901 'styleDate' => $this->styleDate,
902 'copyright' => $this->copyright,
903 'license' => $this->license,
904 'authorName' => $this->authorName,
905 'authorURL' => $this->authorURL,
906 'apiVersion' => $this->apiVersion,
907 ]),
908 'uploads' => $this->uploads,
909 'removedUploads' => $this->removedUploads,
910 'customAssets' => $this->customAssets,
911 'tmpHash' => $this->tmpHash,
912 'variables' => $this->variables,
913 ]);
914 $returnValues = $this->objectAction->executeAction();
915 $style = $returnValues['returnValues'];
916
917 // save style description
918 I18nHandler::getInstance()->save(
919 'styleDescription',
920 'wcf.style.styleDescription' . $style->styleID,
921 'wcf.style'
922 );
923
924 $styleEditor = new StyleEditor($style);
925 $styleEditor->update([
926 'styleDescription' => 'wcf.style.styleDescription' . $style->styleID,
927 ]);
928
929 // Do not save the compiled style, because the image path was unknown during the style generation.
930 if ($this->styleTestFileDir) {
931 if (\file_exists($this->styleTestFileDir . '/style.css')) {
932 \unlink($this->styleTestFileDir . '/style.css');
933 }
934 if (\file_exists($this->styleTestFileDir . '/style-rtl.css')) {
935 \unlink($this->styleTestFileDir . '/style-rtl.css');
936 }
937 if (\file_exists($this->styleTestFileDir . '/style-preload.json')) {
938 \unlink($this->styleTestFileDir . '/style-preload.json');
939 }
940
941 \rmdir($this->styleTestFileDir);
942 }
943
944 // call saved event
945 $this->saved();
946
947 // reset variables
948 $this->authorName = $this->authorURL = $this->copyright = $this->packageName = '';
949 $this->license = $this->styleDate = $this->styleDescription = $this->styleName = $this->styleVersion = '';
950 $this->setDefaultValues();
951 $this->isTainted = true;
952 $this->templateGroupID = 0;
953 $this->styleTestFilename = null;
954 $this->rebuildUploadFields();
955
956 I18nHandler::getInstance()->reset();
957
958 // reload variables
959 $this->readStyleVariables();
960
961 WCF::getTPL()->assign([
962 'success' => true,
963 'objectEditLink' => LinkHandler::getInstance()->getControllerLink(
964 StyleEditForm::class,
965 ['id' => $style->styleID]
966 ),
967 ]);
968 }
969
970 /**
971 * @inheritDoc
972 */
973 public function assignVariables()
974 {
975 parent::assignVariables();
976
977 I18nHandler::getInstance()->assignVariables();
978
979 WCF::getTPL()->assign([
980 'action' => 'add',
981 'apiVersion' => $this->apiVersion,
982 'authorName' => $this->authorName,
983 'authorURL' => $this->authorURL,
984 'availableFontFamilies' => $this->availableFontFamilies,
985 'availableTemplateGroups' => $this->availableTemplateGroups,
986 'availableUnits' => $this->availableUnits,
987 'colorCategories' => $this->colorCategories,
988 'colors' => $this->colors,
989 'copyright' => $this->copyright,
990 'isTainted' => $this->isTainted,
991 'license' => $this->license,
992 'packageName' => $this->packageName,
993 'recommendedApiVersion' => Style::API_VERSION,
994 'styleDate' => $this->styleDate,
995 'styleDescription' => $this->styleDescription,
996 'styleName' => $this->styleName,
997 'styleVersion' => $this->styleVersion,
998 'templateGroupID' => $this->templateGroupID,
999 'tmpHash' => $this->tmpHash,
1000 'variables' => $this->variables,
1001 'supportedApiVersions' => Style::$supportedApiVersions,
1002 'supportedApiVersionsCompatibility' => $this->supportedApiVersionsCompatibility,
1003 'newVariables' => $this->newVariables,
1004 'scrollOffsets' => $this->scrollOffsets,
1005 'coverPhotoMinHeight' => UserCoverPhoto::MIN_HEIGHT,
1006 'coverPhotoMaxHeight' => UserCoverPhoto::MAX_HEIGHT,
1007 'coverPhotoMinWidth' => UserCoverPhoto::MIN_WIDTH,
1008 'coverPhotoMaxWidth' => UserCoverPhoto::MAX_WIDTH,
1009 ]);
1010 }
1011
1012 protected function setDefaultValues()
1013 {
1014 $this->authorName = WCF::getUser()->username;
1015 $this->styleDate = \gmdate('Y-m-d', TIME_NOW);
1016 $this->styleVersion = '1.0.0';
1017 }
1018 }