Merge branch '3.0'
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / data / style / StyleAction.class.php
CommitLineData
158bd3ca
TD
1<?php
2namespace wcf\data\style;
87fc5501 3use wcf\data\user\cover\photo\UserCoverPhoto;
dabfc48b 4use wcf\data\user\UserAction;
158bd3ca 5use wcf\data\AbstractDatabaseObjectAction;
6dcc3615 6use wcf\data\IToggleAction;
59ab4d0f 7use wcf\data\IUploadAction;
787263fc 8use wcf\system\cache\builder\StyleCacheBuilder;
8eacc867
AE
9use wcf\system\exception\IllegalLinkException;
10use wcf\system\exception\PermissionDeniedException;
11use wcf\system\exception\SystemException;
12use wcf\system\exception\UserInputException;
13use wcf\system\image\ImageHandler;
7375b309 14use wcf\system\request\LinkHandler;
8eacc867
AE
15use wcf\system\style\StyleHandler;
16use wcf\system\upload\DefaultUploadFileValidationStrategy;
e4499881
MS
17use wcf\system\upload\UploadFile;
18use wcf\system\upload\UploadHandler;
7375b309 19use wcf\system\Regex;
ee013cde 20use wcf\system\WCF;
8eacc867 21use wcf\util\FileUtil;
158bd3ca
TD
22
23/**
24 * Executes style-related actions.
25 *
26 * @author Alexander Ebert
c839bd49 27 * @copyright 2001-2018 WoltLab GmbH
158bd3ca 28 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
e71525e4 29 * @package WoltLabSuite\Core\Data\Style
0e8867ac
MS
30 *
31 * @method StyleEditor[] getObjects()
32 * @method StyleEditor getSingleObject()
158bd3ca 33 */
59ab4d0f 34class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction, IUploadAction {
83736ee3 35 /**
00cefc6d 36 * @inheritDoc
83736ee3 37 */
59ab4d0f 38 protected $allowGuestAccess = ['changeStyle', 'getStyleChooser'];
83736ee3 39
158bd3ca 40 /**
00cefc6d 41 * @inheritDoc
158bd3ca 42 */
157054c9 43 protected $className = StyleEditor::class;
ee013cde 44
9fba6c76 45 /**
00cefc6d 46 * @inheritDoc
9fba6c76 47 */
59ab4d0f 48 protected $permissionsDelete = ['admin.style.canManageStyle'];
9fba6c76 49
b3cec8b6 50 /**
00cefc6d 51 * @inheritDoc
b3cec8b6 52 */
59ab4d0f 53 protected $permissionsUpdate = ['admin.style.canManageStyle'];
b3cec8b6 54
bae8dd1e 55 /**
00cefc6d 56 * @inheritDoc
bae8dd1e 57 */
65a5d80c 58 protected $requireACP = ['copy', 'delete', 'deleteCoverPhoto', 'markAsTainted', 'setAsDefault', 'toggle', 'update', 'upload', 'uploadCoverPhoto', 'uploadLogo', 'uploadLogoMobile'];
bae8dd1e 59
83736ee3
AE
60 /**
61 * style object
59ab4d0f 62 * @var Style
83736ee3 63 */
65a5d80c 64 public $style;
83736ee3 65
429e91b8
AE
66 /**
67 * style editor object
59ab4d0f 68 * @var StyleEditor
429e91b8 69 */
65a5d80c 70 public $styleEditor;
429e91b8 71
ee013cde 72 /**
00cefc6d 73 * @inheritDoc
0e8867ac 74 * @return Style
ee013cde
AE
75 */
76 public function create() {
0e8867ac 77 /** @var Style $style */
ee013cde
AE
78 $style = parent::create();
79
80 // add variables
8eacc867
AE
81 $this->updateVariables($style);
82
83 // handle style preview image
84 $this->updateStylePreviewImage($style);
85
86 return $style;
87 }
88
89 /**
00cefc6d 90 * @inheritDoc
8eacc867
AE
91 */
92 public function update() {
93 parent::update();
94
4a130a51 95 foreach ($this->getObjects() as $style) {
8eacc867
AE
96 // update variables
97 $this->updateVariables($style->getDecoratedObject(), true);
98
99 // handle style preview image
100 $this->updateStylePreviewImage($style->getDecoratedObject());
101
91fa523c
AE
102 // create favicon data
103 $this->updateFavicons($style->getDecoratedObject());
104
87fc5501
AE
105 // handle the cover photo
106 $this->updateCoverPhoto($style->getDecoratedObject());
107
8eacc867
AE
108 // reset stylesheet
109 StyleHandler::getInstance()->resetStylesheet($style->getDecoratedObject());
110 }
111 }
112
113 /**
00cefc6d 114 * @inheritDoc
8eacc867
AE
115 */
116 public function delete() {
117 $count = parent::delete();
118
4a130a51 119 foreach ($this->getObjects() as $style) {
8eacc867
AE
120 // remove custom images
121 if ($style->imagePath && $style->imagePath != 'images/') {
122 $this->removeDirectory($style->imagePath);
123 }
124
125 // remove preview image
126 $previewImage = WCF_DIR.'images/'.$style->image;
127 if (file_exists($previewImage)) {
128 @unlink($previewImage);
129 }
130
131 // remove stylesheet
132 StyleHandler::getInstance()->resetStylesheet($style->getDecoratedObject());
133 }
134
135 return $count;
136 }
137
138 /**
139 * Recursively removes a directory and all it's contents.
140 *
141 * @param string $pathComponent
142 */
143 protected function removeDirectory($pathComponent) {
144 $dir = WCF_DIR.$pathComponent;
145 if (is_dir($dir)) {
146 $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir), \RecursiveIteratorIterator::CHILD_FIRST);
147 foreach ($iterator as $path) {
148 if ($path->isDir()) {
d8670bae 149 @rmdir($path);
8eacc867
AE
150 }
151 else {
d8670bae 152 @unlink($path);
8eacc867
AE
153 }
154 }
155
156 @rmdir($dir);
157 }
158 }
159
160 /**
161 * Updates style variables for given style.
162 *
59ab4d0f
MS
163 * @param Style $style
164 * @param boolean $removePreviousVariables
8eacc867
AE
165 */
166 protected function updateVariables(Style $style, $removePreviousVariables = false) {
167 if (!isset($this->parameters['variables']) || !is_array($this->parameters['variables'])) {
168 return;
169 }
170
171 $sql = "SELECT variableID, variableName, defaultValue
172 FROM wcf".WCF_N."_style_variable";
173 $statement = WCF::getDB()->prepareStatement($sql);
174 $statement->execute();
59ab4d0f 175 $variables = [];
8eacc867
AE
176 while ($row = $statement->fetchArray()) {
177 $variableName = $row['variableName'];
393e18c5 178
8eacc867
AE
179 // ignore variables with identical value
180 if (isset($this->parameters['variables'][$variableName])) {
181 if ($this->parameters['variables'][$variableName] == $row['defaultValue']) {
182 continue;
183 }
184 else {
185 $variables[$row['variableID']] = $this->parameters['variables'][$variableName];
186 }
187 }
188 }
a17de04e 189
8eacc867
AE
190 // remove previously set variables
191 if ($removePreviousVariables) {
192 $sql = "DELETE FROM wcf".WCF_N."_style_variable_value
193 WHERE styleID = ?";
194 $statement = WCF::getDB()->prepareStatement($sql);
59ab4d0f 195 $statement->execute([$style->styleID]);
8eacc867 196 }
a17de04e 197
8eacc867
AE
198 // insert variables that differ from default values
199 if (!empty($variables)) {
200 $sql = "INSERT INTO wcf".WCF_N."_style_variable_value
201 (styleID, variableID, variableValue)
202 VALUES (?, ?, ?)";
ee013cde 203 $statement = WCF::getDB()->prepareStatement($sql);
393e18c5 204
8eacc867
AE
205 WCF::getDB()->beginTransaction();
206 foreach ($variables as $variableID => $variableValue) {
59ab4d0f 207 $statement->execute([
8eacc867
AE
208 $style->styleID,
209 $variableID,
210 $variableValue
59ab4d0f 211 ]);
8eacc867
AE
212 }
213 WCF::getDB()->commitTransaction();
214 }
215 }
216
217 /**
218 * Updates style preview image.
219 *
59ab4d0f 220 * @param Style $style
8eacc867
AE
221 */
222 protected function updateStylePreviewImage(Style $style) {
223 if (!isset($this->parameters['tmpHash'])) {
224 return;
225 }
226
2e572b29
AE
227 foreach (['', '@2x'] as $type) {
228 $fileExtension = WCF::getSession()->getVar('stylePreview-' . $this->parameters['tmpHash'] . $type);
229 if ($fileExtension !== null) {
230 $oldFilename = WCF_DIR . 'images/stylePreview-' . $this->parameters['tmpHash'] . $type . '.' . $fileExtension;
231 if (file_exists($oldFilename)) {
232 $filename = 'stylePreview-' . $style->styleID . $type . '.' . $fileExtension;
233 if (@rename($oldFilename, WCF_DIR . 'images/' . $filename)) {
234 // delete old file if it has a different file extension
235 if ($type === '') {
236 if ($style->image != $filename) {
237 @unlink(WCF_DIR . 'images/' . $style->image);
238
239 // update filename in database
240 $sql = "UPDATE wcf" . WCF_N . "_style
241 SET image = ?
242 WHERE styleID = ?";
243 $statement = WCF::getDB()->prepareStatement($sql);
244 $statement->execute([
245 $filename, $style->styleID
246 ]);
247 }
248 }
249 else {
250 if ($style->image2x != $filename) {
251 @unlink(WCF_DIR . 'images/' . $style->image2x);
252
253 // update filename in database
254 $sql = "UPDATE wcf" . WCF_N . "_style
255 SET image2x = ?
256 WHERE styleID = ?";
257 $statement = WCF::getDB()->prepareStatement($sql);
258 $statement->execute([
259 $filename, $style->styleID
260 ]);
261 }
262 }
263 }
264 else {
265 // remove temp file
266 @unlink($oldFilename);
ee013cde 267 }
8eacc867 268 }
ee013cde 269 }
8eacc867
AE
270 }
271 }
272
91fa523c
AE
273 /**
274 * Updates style favicon files.
275 *
276 * @param Style $style
87fc5501 277 * @since 3.1
91fa523c
AE
278 */
279 protected function updateFavicons(Style $style) {
280 $styleID = $style->styleID;
281 $fileExtension = WCF::getSession()->getVar('styleFavicon-template-'.$styleID);
282 $hasFavicon = (bool)$style->hasFavicon;
283 if ($fileExtension) {
284 $template = WCF_DIR . "images/favicon/{$styleID}.favicon-template.{$fileExtension}";
285 $images = [
286 'android-chrome-192x192.png' => 192,
287 'android-chrome-256x256.png' => 256,
288 'apple-touch-icon.png' => 180,
289 'mstile-150x150.png' => 150
290 ];
291
292 $adapter = ImageHandler::getInstance()->getAdapter();
293 $adapter->loadFile($template);
294 foreach ($images as $filename => $length) {
295 $thumbnail = $adapter->createThumbnail($length, $length);
296 $adapter->writeImage($thumbnail, WCF_DIR."images/favicon/{$styleID}.{$filename}");
297 }
298
299 // create ico
300 require(WCF_DIR . 'lib/system/api/chrisjean/php-ico/class-php-ico.php');
301 $phpIco = new \PHP_ICO($template, [
302 [16, 16],
303 [32, 32]
304 ]);
305 $phpIco->save_ico(WCF_DIR . "images/favicon/{$styleID}.favicon.ico");
306
307 $hasFavicon = true;
308
309 (new StyleEditor($style))->update(['hasFavicon' => 1]);
310 WCF::getSession()->unregister('styleFavicon-template-'.$style->styleID);
311 }
312
313 if ($hasFavicon) {
314 // update manifest.json
315 $manifest = <<<MANIFEST
316{
317 "name": "",
318 "icons": [
319 {
320 "src": "{$styleID}.android-chrome-192x192.png",
321 "sizes": "192x192",
322 "type": "image/png"
323 },
324 {
325 "src": "{$styleID}.android-chrome-256x256.png",
326 "sizes": "256x256",
327 "type": "image/png"
328 }
329 ],
330 "theme_color": "#ffffff",
331 "background_color": "#ffffff",
332 "display": "standalone"
333}
334MANIFEST;
335 file_put_contents(WCF_DIR . "images/favicon/{$styleID}.manifest.json", $manifest);
336
337 $style->loadVariables();
338 $tileColor = $style->getVariable('wcfHeaderBackground', true);
339
340 // update browserconfig.xml
341 $browserconfig = <<<BROWSERCONFIG
342<?xml version="1.0" encoding="utf-8"?>
343<browserconfig>
344 <msapplication>
345 <tile>
346 <square150x150logo src="{$styleID}.mstile-150x150.png"/>
347 <TileColor>{$tileColor}</TileColor>
348 </tile>
349 </msapplication>
350</browserconfig>
351BROWSERCONFIG;
352 file_put_contents(WCF_DIR . "images/favicon/{$styleID}.browserconfig.xml", $browserconfig);
353 }
354 }
355
87fc5501
AE
356 /**
357 * Updates the style cover photo.
358 *
359 * @param Style $style
360 * @since 3.1
361 */
362 protected function updateCoverPhoto(Style $style) {
363 $styleID = $style->styleID;
364 $fileExtension = WCF::getSession()->getVar('styleCoverPhoto-'.$styleID);
365 if ($fileExtension) {
366 // remove old image
367 if ($style->coverPhotoExtension) {
368 @unlink(WCF_DIR . 'images/coverPhotos/' . $style->getCoverPhoto());
369 }
370
371 rename(
372 WCF_DIR . 'images/coverPhotos/' . $styleID . '.tmp.' . $fileExtension,
373 WCF_DIR . 'images/coverPhotos/' . $styleID . '.' . $fileExtension
374 );
375
376 (new StyleEditor($style))->update(['coverPhotoExtension' => $fileExtension]);
377 WCF::getSession()->unregister('styleCoverPhoto-'.$style->styleID);
378 }
379 }
380
8eacc867 381 /**
00cefc6d 382 * @inheritDoc
8eacc867
AE
383 */
384 public function validateUpload() {
385 // check upload permissions
315c6dc0 386 if (!WCF::getSession()->getPermission('admin.style.canManageStyle')) {
8eacc867
AE
387 throw new PermissionDeniedException();
388 }
389
2e572b29 390 $this->readBoolean('is2x', true);
2f06a43e 391 $this->readString('tmpHash');
860c0046 392 $this->readInteger('styleID', true);
2f06a43e
MS
393
394 if ($this->parameters['styleID']) {
395 $styles = StyleHandler::getInstance()->getStyles();
396 if (!isset($styles[$this->parameters['styleID']])) {
397 throw new UserInputException('styleID');
398 }
399
400 $this->style = $styles[$this->parameters['styleID']];
8eacc867
AE
401 }
402
e4499881
MS
403 /** @var UploadHandler $uploadHandler */
404 $uploadHandler = $this->parameters['__files'];
405
406 if (count($uploadHandler->getFiles()) != 1) {
8eacc867
AE
407 throw new IllegalLinkException();
408 }
409
410 // check max filesize, allowed file extensions etc.
d38156d3 411 $uploadHandler->validateFiles(new DefaultUploadFileValidationStrategy(PHP_INT_MAX, ['jpg', 'jpeg', 'png', 'gif', 'svg']));
8eacc867
AE
412 }
413
414 /**
00cefc6d 415 * @inheritDoc
8eacc867
AE
416 */
417 public function upload() {
418 // save files
1cf9e18f 419 /** @noinspection PhpUndefinedMethodInspection */
e4499881 420 /** @var UploadFile[] $files */
8eacc867
AE
421 $files = $this->parameters['__files']->getFiles();
422 $file = $files[0];
423
2e572b29
AE
424 $multiplier = ($this->parameters['is2x']) ? 2 : 1;
425
8eacc867
AE
426 try {
427 if (!$file->getValidationErrorType()) {
83d1d0e7 428 // shrink preview image if necessary
8eacc867 429 $fileLocation = $file->getLocation();
68c47814
TD
430 try {
431 if (($imageData = getimagesize($fileLocation)) === false) {
432 throw new UserInputException('image');
433 }
434 switch ($imageData[2]) {
b1e0a55d
TD
435 case IMAGETYPE_PNG:
436 case IMAGETYPE_JPEG:
b1e0a55d 437 case IMAGETYPE_GIF:
68c47814
TD
438 // fine
439 break;
440 default:
441 throw new UserInputException('image');
442 }
ee29e65e 443
2e572b29 444 if ($imageData[0] > (Style::PREVIEW_IMAGE_MAX_WIDTH * $multiplier) || $imageData[1] > (Style::PREVIEW_IMAGE_MAX_HEIGHT * $multiplier)) {
8eacc867
AE
445 $adapter = ImageHandler::getInstance()->getAdapter();
446 $adapter->loadFile($fileLocation);
447 $fileLocation = FileUtil::getTemporaryFilename();
2e572b29 448 $thumbnail = $adapter->createThumbnail(Style::PREVIEW_IMAGE_MAX_WIDTH * $multiplier, Style::PREVIEW_IMAGE_MAX_HEIGHT * $multiplier, false);
8eacc867 449 $adapter->writeImage($thumbnail, $fileLocation);
8eacc867 450 }
68c47814
TD
451 }
452 catch (SystemException $e) {
453 throw new UserInputException('image');
8eacc867 454 }
ee013cde 455
8eacc867 456 // move uploaded file
2e572b29 457 if (@copy($fileLocation, WCF_DIR.'images/stylePreview-'.$this->parameters['tmpHash'].($this->parameters['is2x'] ? '@2x' : '').'.'.$file->getFileExtension())) {
8eacc867
AE
458 @unlink($fileLocation);
459
460 // store extension within session variables
2e572b29 461 WCF::getSession()->register('stylePreview-'.$this->parameters['tmpHash'].($this->parameters['is2x'] ? '@2x' : ''), $file->getFileExtension());
8eacc867 462
2f06a43e
MS
463 if ($this->parameters['styleID']) {
464 $this->updateStylePreviewImage($this->style);
465
59ab4d0f 466 return [
2e572b29 467 'url' => WCF::getPath().'images/stylePreview-'.$this->parameters['styleID'].($this->parameters['is2x'] ? '@2x' : '').'.'.$file->getFileExtension()
59ab4d0f 468 ];
2f06a43e
MS
469 }
470
8eacc867 471 // return result
59ab4d0f 472 return [
2e572b29 473 'url' => WCF::getPath().'images/stylePreview-'.$this->parameters['tmpHash'].($this->parameters['is2x'] ? '@2x' : '').'.'.$file->getFileExtension()
59ab4d0f 474 ];
8eacc867
AE
475 }
476 else {
477 throw new UserInputException('image', 'uploadFailed');
ee013cde 478 }
ee013cde
AE
479 }
480 }
8eacc867
AE
481 catch (UserInputException $e) {
482 $file->setValidationErrorType($e->getType());
483 }
ee013cde 484
59ab4d0f 485 return ['errorType' => $file->getValidationErrorType()];
ee013cde 486 }
9fba6c76 487
83d1d0e7
AE
488 /**
489 * Validates parameters to update a logo.
490 */
491 public function validateUploadLogo() {
492 $this->validateUpload();
493 }
494
495 /**
496 * Handles logo upload.
1a6e8c52 497 *
59ab4d0f 498 * @return string[]
83d1d0e7
AE
499 */
500 public function uploadLogo() {
501 // save files
1cf9e18f 502 /** @noinspection PhpUndefinedMethodInspection */
e4499881 503 /** @var UploadFile[] $files */
83d1d0e7
AE
504 $files = $this->parameters['__files']->getFiles();
505 $file = $files[0];
506
507 try {
508 if (!$file->getValidationErrorType()) {
509 // shrink avatar if necessary
510 $fileLocation = $file->getLocation();
511
512 // move uploaded file
513 if (@copy($fileLocation, WCF_DIR.'images/styleLogo-'.$this->parameters['tmpHash'].'.'.$file->getFileExtension())) {
514 @unlink($fileLocation);
515
516 // store extension within session variables
517 WCF::getSession()->register('styleLogo-'.$this->parameters['tmpHash'], $file->getFileExtension());
518
4a292263
MW
519 // get logo size
520 list($width, $height) = getimagesize(WCF_DIR.'images/styleLogo-'.$this->parameters['tmpHash'].'.'.$file->getFileExtension());
521
522 // return result
523 return [
524 'url' => WCF::getPath().'images/styleLogo-'.$this->parameters['tmpHash'].'.'.$file->getFileExtension(),
525 'width' => $width,
526 'height' => $height
527 ];
528 }
529 else {
530 throw new UserInputException('image', 'uploadFailed');
531 }
532 }
533 }
534 catch (UserInputException $e) {
535 $file->setValidationErrorType($e->getType());
536 }
537
538 return ['errorType' => $file->getValidationErrorType()];
539 }
540
541 /**
542 * Validates parameters to update a mobile logo.
543 */
544 public function validateUploadLogoMobile() {
545 $this->validateUpload();
546 }
547
548 /**
549 * Handles mobile logo upload.
550 *
551 * @return string[]
552 */
553 public function uploadLogoMobile() {
554 // save files
555 /** @noinspection PhpUndefinedMethodInspection */
556 /** @var UploadFile[] $files */
557 $files = $this->parameters['__files']->getFiles();
558 $file = $files[0];
559
560 try {
561 if (!$file->getValidationErrorType()) {
562 // shrink avatar if necessary
563 $fileLocation = $file->getLocation();
564
565 // move uploaded file
566 if (@copy($fileLocation, WCF_DIR.'images/styleLogo-mobile-'.$this->parameters['tmpHash'].'.'.$file->getFileExtension())) {
567 @unlink($fileLocation);
568
569 // store extension within session variables
570 WCF::getSession()->register('styleLogo-mobile-'.$this->parameters['tmpHash'], $file->getFileExtension());
571
83d1d0e7 572 // return result
59ab4d0f 573 return [
4a292263 574 'url' => WCF::getPath().'images/styleLogo-mobile-'.$this->parameters['tmpHash'].'.'.$file->getFileExtension()
59ab4d0f 575 ];
83d1d0e7
AE
576 }
577 else {
578 throw new UserInputException('image', 'uploadFailed');
579 }
580 }
581 }
582 catch (UserInputException $e) {
583 $file->setValidationErrorType($e->getType());
584 }
585
59ab4d0f 586 return ['errorType' => $file->getValidationErrorType()];
83d1d0e7
AE
587 }
588
91fa523c
AE
589 /**
590 * Validates parameters to upload a favicon.
87fc5501
AE
591 *
592 * @since 3.1
91fa523c
AE
593 */
594 public function validateUploadFavicon() {
595 // ignore tmp hash, uploading is supported for existing styles only
596 // and files will be finally processed on form submit
597 $this->parameters['tmpHash'] = '@@@WCF_INVALID_TMP_HASH@@@';
598
599 $this->validateUpload();
600 }
601
602 /**
603 * Handles favicon upload.
604 *
605 * @return string[]
87fc5501 606 * @since 3.1
91fa523c
AE
607 */
608 public function uploadFavicon() {
609 // save files
610 /** @noinspection PhpUndefinedMethodInspection */
611 /** @var UploadFile[] $files */
612 $files = $this->parameters['__files']->getFiles();
613 $file = $files[0];
614
615 try {
616 if (!$file->getValidationErrorType()) {
617 $fileLocation = $file->getLocation();
618 try {
619 if (($imageData = getimagesize($fileLocation)) === false) {
620 throw new UserInputException('favicon');
621 }
622 switch ($imageData[2]) {
623 case IMAGETYPE_PNG:
624 case IMAGETYPE_JPEG:
625 case IMAGETYPE_GIF:
626 // fine
627 break;
628 default:
629 throw new UserInputException('favicon');
630 }
631
632 if ($imageData[0] != Style::FAVICON_IMAGE_WIDTH || $imageData[1] != Style::FAVICON_IMAGE_HEIGHT) {
633 throw new UserInputException('favicon', 'dimensions');
634 }
635 }
636 catch (SystemException $e) {
637 throw new UserInputException('favicon');
638 }
639
640 // move uploaded file
641 if (@copy($fileLocation, WCF_DIR.'images/favicon/'.$this->style->styleID.'.favicon-template.'.$file->getFileExtension())) {
642 @unlink($fileLocation);
643
644 // store extension within session variables
645 WCF::getSession()->register('styleFavicon-template-'.$this->style->styleID, $file->getFileExtension());
646
647 // return result
648 return [
649 'url' => WCF::getPath().'images/favicon/'.$this->style->styleID.'.favicon-template.'.$file->getFileExtension()
650 ];
651 }
652 else {
653 throw new UserInputException('favicon', 'uploadFailed');
654 }
655 }
656 }
657 catch (UserInputException $e) {
658 $file->setValidationErrorType($e->getType());
659 }
660
661 return ['errorType' => $file->getValidationErrorType()];
662 }
663
87fc5501
AE
664 /**
665 * Validates parameters to upload a cover photo.
666 *
667 * @since 3.1
668 */
669 public function validateUploadCoverPhoto() {
65a5d80c
AE
670 if (!MODULE_USER_COVER_PHOTO) {
671 throw new PermissionDeniedException();
672 }
673
87fc5501
AE
674 // ignore tmp hash, uploading is supported for existing styles only
675 // and files will be finally processed on form submit
676 $this->parameters['tmpHash'] = '@@@WCF_INVALID_TMP_HASH@@@';
677
678 $this->validateUpload();
679 }
680
681 /**
682 * Handles the cover photo upload.
683 *
684 * @return string[]
685 * @since 3.1
686 */
687 public function uploadCoverPhoto() {
688 // save files
689 /** @noinspection PhpUndefinedMethodInspection */
690 /** @var UploadFile[] $files */
691 $files = $this->parameters['__files']->getFiles();
692 $file = $files[0];
693
694 try {
695 if (!$file->getValidationErrorType()) {
696 $fileLocation = $file->getLocation();
697 try {
698 if (($imageData = getimagesize($fileLocation)) === false) {
699 throw new UserInputException('coverPhoto');
700 }
701 switch ($imageData[2]) {
702 case IMAGETYPE_PNG:
703 case IMAGETYPE_JPEG:
704 case IMAGETYPE_GIF:
705 // fine
706 break;
707 default:
708 throw new UserInputException('coverPhoto');
709 }
710
711 if ($imageData[0] < UserCoverPhoto::MIN_WIDTH) {
712 throw new UserInputException('coverPhoto', 'minWidth');
713 }
714 else if ($imageData[1] < UserCoverPhoto::MIN_HEIGHT) {
715 throw new UserInputException('coverPhoto', 'minHeight');
716 }
717 }
718 catch (SystemException $e) {
719 throw new UserInputException('coverPhoto');
720 }
721
722 // move uploaded file
723 if (@copy($fileLocation, WCF_DIR.'images/coverPhotos/'.$this->style->styleID.'.tmp.'.$file->getFileExtension())) {
724 @unlink($fileLocation);
725
726 // store extension within session variables
727 WCF::getSession()->register('styleCoverPhoto-'.$this->style->styleID, $file->getFileExtension());
728
729 // return result
730 return [
731 'url' => WCF::getPath().'images/coverPhotos/'.$this->style->styleID.'.tmp.'.$file->getFileExtension()
732 ];
733 }
734 else {
735 throw new UserInputException('coverPhoto', 'uploadFailed');
736 }
737 }
738 }
739 catch (UserInputException $e) {
740 $file->setValidationErrorType($e->getType());
741 }
742
743 return ['errorType' => $file->getValidationErrorType()];
744 }
745
65a5d80c
AE
746 /**
747 * Validates the parameters to delete a style's default cover photo.
748 *
749 * @throws PermissionDeniedException
750 * @throws UserInputException
751 * @since 3.1
752 */
753 public function validateDeleteCoverPhoto() {
754 if (!MODULE_USER_COVER_PHOTO) {
755 throw new PermissionDeniedException();
756 }
757
758 $this->styleEditor = $this->getSingleObject();
759 if (!$this->styleEditor->coverPhotoExtension) {
760 throw new UserInputException('objectIDs');
761 }
762 }
763
764 /**
765 * Deletes a style's default cover photo.
766 *
767 * @return string[]
768 * @since 3.1
769 */
770 public function deleteCoverPhoto() {
771 $this->styleEditor->deleteCoverPhoto();
772
773 return [
774 'url' => WCF::getPath().'images/coverPhotos/'.(new Style($this->styleEditor->styleID))->getCoverPhoto()
775 ];
776 }
777
9fba6c76
AE
778 /**
779 * Validates parameters to assign a new default style.
780 */
781 public function validateSetAsDefault() {
315c6dc0 782 if (!WCF::getSession()->getPermission('admin.style.canManageStyle')) {
9fba6c76
AE
783 throw new PermissionDeniedException();
784 }
785
786 if (empty($this->objects)) {
787 $this->readObjects();
788 if (empty($this->objects)) {
789 throw new UserInputException('objectIDs');
790 }
791 }
792
793 if (count($this->objects) > 1) {
794 throw new UserInputException('objectIDs');
795 }
796 }
797
798 /**
799 * Sets a style as new default style.
800 */
801 public function setAsDefault() {
802 $styleEditor = current($this->objects);
803 $styleEditor->setAsDefault();
804 }
7375b309
AE
805
806 /**
807 * Validates parameters to copy a style.
808 */
809 public function validateCopy() {
315c6dc0 810 if (!WCF::getSession()->getPermission('admin.style.canManageStyle')) {
7375b309
AE
811 throw new PermissionDeniedException();
812 }
813
429e91b8 814 $this->styleEditor = $this->getSingleObject();
7375b309
AE
815 }
816
817 /**
818 * Copies a style.
819 *
59ab4d0f 820 * @return string[]
7375b309
AE
821 */
822 public function copy() {
7375b309
AE
823 // get unique style name
824 $sql = "SELECT styleName
825 FROM wcf".WCF_N."_style
826 WHERE styleName LIKE ?
827 AND styleID <> ?";
828 $statement = WCF::getDB()->prepareStatement($sql);
59ab4d0f 829 $statement->execute([
429e91b8
AE
830 $this->styleEditor->styleName.'%',
831 $this->styleEditor->styleID
59ab4d0f
MS
832 ]);
833 $numbers = [];
7375b309
AE
834 $regEx = new Regex('\((\d+)\)$');
835 while ($row = $statement->fetchArray()) {
836 $styleName = $row['styleName'];
837
838 if ($regEx->match($styleName)) {
839 $matches = $regEx->getMatches();
840
841 // check if name matches the pattern 'styleName (x)'
429e91b8 842 if ($styleName == $this->styleEditor->styleName . ' ('.$matches[1].')') {
7375b309
AE
843 $numbers[] = $matches[1];
844 }
845 }
846 }
847
63b9817b 848 $number = count($numbers) ? max($numbers) + 1 : 2;
429e91b8 849 $styleName = $this->styleEditor->styleName . ' ('.$number.')';
7375b309
AE
850
851 // create the new style
59ab4d0f 852 $newStyle = StyleEditor::create([
7375b309 853 'styleName' => $styleName,
429e91b8 854 'templateGroupID' => $this->styleEditor->templateGroupID,
8f08c7d0 855 'isDisabled' => 1, // newly created styles are disabled by default
429e91b8
AE
856 'styleDescription' => $this->styleEditor->styleDescription,
857 'styleVersion' => $this->styleEditor->styleVersion,
858 'styleDate' => $this->styleEditor->styleDate,
859 'copyright' => $this->styleEditor->copyright,
860 'license' => $this->styleEditor->license,
861 'authorName' => $this->styleEditor->authorName,
862 'authorURL' => $this->styleEditor->authorURL,
7477a2f2
AE
863 'imagePath' => $this->styleEditor->imagePath,
864 'apiVersion' => $this->styleEditor->apiVersion
59ab4d0f 865 ]);
7375b309 866
dc65abe0
AE
867 // check if style description uses i18n
868 if (preg_match('~^wcf.style.styleDescription\d+$~', $newStyle->styleDescription)) {
869 $styleDescription = 'wcf.style.styleDescription'.$newStyle->styleID;
870
a9f00a19
AE
871 // delete any phrases that were the result of an import
872 $sql = "DELETE FROM wcf".WCF_N."_language_item
873 WHERE languageItem = ?";
874 $statement = WCF::getDB()->prepareStatement($sql);
875 $statement->execute([$styleDescription]);
876
dc65abe0
AE
877 // copy language items
878 $sql = "INSERT INTO wcf".WCF_N."_language_item
879 (languageID, languageItem, languageItemValue, languageItemOriginIsSystem, languageCategoryID, packageID)
880 SELECT languageID, '".$styleDescription."', languageItemValue, 0, languageCategoryID, packageID
881 FROM wcf".WCF_N."_language_item
882 WHERE languageItem = ?";
883 $statement = WCF::getDB()->prepareStatement($sql);
59ab4d0f 884 $statement->execute([$newStyle->styleDescription]);
dc65abe0
AE
885
886 // update style description
887 $styleEditor = new StyleEditor($newStyle);
59ab4d0f 888 $styleEditor->update([
dc65abe0 889 'styleDescription' => $styleDescription
59ab4d0f 890 ]);
dc65abe0
AE
891 }
892
7375b309
AE
893 // copy style variables
894 $sql = "INSERT INTO wcf".WCF_N."_style_variable_value
895 (styleID, variableID, variableValue)
896 SELECT ".$newStyle->styleID." AS styleID, value.variableID, value.variableValue
897 FROM wcf".WCF_N."_style_variable_value value
898 WHERE value.styleID = ?";
899 $statement = WCF::getDB()->prepareStatement($sql);
59ab4d0f 900 $statement->execute([$this->styleEditor->styleID]);
7375b309
AE
901
902 // copy preview image
7477a2f2
AE
903 foreach (['image', 'image2x'] as $imageType) {
904 $image = $this->styleEditor->{$imageType};
905 if ($image) {
906 // get extension
907 $fileExtension = mb_substr($image, mb_strrpos($image, '.'));
908
909 // copy existing preview image
910 if (@copy(WCF_DIR . 'images/' . $image, WCF_DIR . 'images/stylePreview-' . $newStyle->styleID . $fileExtension)) {
911 // bypass StyleEditor::update() to avoid scaling of already fitting image
912 $sql = "UPDATE wcf" . WCF_N . "_style
913 SET ".$imageType." = ?
914 WHERE styleID = ?";
915 $statement = WCF::getDB()->prepareStatement($sql);
916 $statement->execute([
917 'stylePreview-' . $newStyle->styleID . $fileExtension,
918 $newStyle->styleID
919 ]);
920 }
921 }
922 }
923
924 // copy cover photo
925 if ($this->styleEditor->coverPhotoExtension) {
926 if (@copy(WCF_DIR . "images/coverPhotos/{$this->styleEditor->styleID}.{$this->styleEditor->coverPhotoExtension}", WCF_DIR . "images/coverPhotos/{$newStyle->styleID}.{$this->styleEditor->coverPhotoExtension}")) {
927 $sql = "UPDATE wcf" . WCF_N . "_style
928 SET coverPhotoExtension = ?
7375b309
AE
929 WHERE styleID = ?";
930 $statement = WCF::getDB()->prepareStatement($sql);
59ab4d0f 931 $statement->execute([
7477a2f2 932 $this->styleEditor->coverPhotoExtension,
7375b309 933 $newStyle->styleID
59ab4d0f 934 ]);
7375b309
AE
935 }
936 }
937
7477a2f2
AE
938 // copy favicon
939 if ($this->styleEditor->hasFavicon) {
940 $path = WCF_DIR . 'images/favicon/';
941 foreach (glob($path . "{$this->styleEditor->styleID}.*") as $filepath) {
942 @copy($filepath, $path . preg_replace('~^\d+\.~', "{$newStyle->styleID}.", basename($filepath)));
943 }
944
945 $sql = "UPDATE wcf" . WCF_N . "_style
946 SET hasFavicon = ?
947 WHERE styleID = ?";
948 $statement = WCF::getDB()->prepareStatement($sql);
949 $statement->execute([
950 1,
951 $newStyle->styleID
952 ]);
953 }
954
3ddc6107
AE
955 // copy images
956 if ($this->styleEditor->imagePath && is_dir(WCF_DIR . $this->styleEditor->imagePath)) {
957 $path = FileUtil::removeTrailingSlash($this->styleEditor->imagePath);
958 $newPath = '';
959 $i = 2;
960 while (true) {
961 $newPath = "{$path}-{$i}/";
962 if (!file_exists(WCF_DIR . $newPath)) {
963 break;
964 }
965
966 $i++;
967 }
968
969 if (!FileUtil::makePath(WCF_DIR . $newPath)) {
970 $newPath = '';
971 }
972
973 if ($newPath) {
974 $src = FileUtil::addTrailingSlash(WCF_DIR . $this->styleEditor->imagePath);
975 $dst = WCF_DIR . $newPath;
976
977 $dir = opendir($src);
978 while (($file = readdir($dir)) !== false) {
979 if ($file != '.' && $file != '..' && !is_dir($file)) {
980 @copy($src . $file, $dst . $file);
981 }
982 }
983 closedir($dir);
984 }
985
986 $sql = "UPDATE wcf".WCF_N."_style
987 SET imagePath = ?
988 WHERE styleID = ?";
989 $statement = WCF::getDB()->prepareStatement($sql);
59ab4d0f 990 $statement->execute([
3ddc6107
AE
991 $newPath,
992 $newStyle->styleID
59ab4d0f 993 ]);
3ddc6107
AE
994 }
995
787263fc
AE
996 StyleCacheBuilder::getInstance()->reset();
997
59ab4d0f
MS
998 return [
999 'redirectURL' => LinkHandler::getInstance()->getLink('StyleEdit', ['id' => $newStyle->styleID])
1000 ];
7375b309 1001 }
b3cec8b6
AE
1002
1003 /**
00cefc6d 1004 * @inheritDoc
b3cec8b6
AE
1005 */
1006 public function validateToggle() {
1007 parent::validateUpdate();
1008
4a130a51 1009 foreach ($this->getObjects() as $style) {
b3cec8b6
AE
1010 if ($style->isDefault) {
1011 throw new UserInputException('objectIDs');
0f0590c2 1012 }
b3cec8b6
AE
1013 }
1014 }
1015
1016 /**
00cefc6d 1017 * @inheritDoc
b3cec8b6
AE
1018 */
1019 public function toggle() {
4a130a51 1020 foreach ($this->getObjects() as $style) {
63b9817b 1021 $isDisabled = $style->isDisabled ? 0 : 1;
59ab4d0f 1022 $style->update(['isDisabled' => $isDisabled]);
b3cec8b6
AE
1023 }
1024 }
83736ee3
AE
1025
1026 /**
1027 * Validates parameters to change user style.
83736ee3
AE
1028 */
1029 public function validateChangeStyle() {
1030 $this->style = $this->getSingleObject();
8f08c7d0 1031 if ($this->style->isDisabled && !WCF::getSession()->getPermission('admin.style.canUseDisabledStyle')) {
83736ee3
AE
1032 throw new PermissionDeniedException();
1033 }
1034 }
1035
1036 /**
1037 * Changes user style.
1038 *
7a23a706 1039 * @return string[]
83736ee3
AE
1040 */
1041 public function changeStyle() {
1042 StyleHandler::getInstance()->changeStyle($this->style->styleID);
1043 if (StyleHandler::getInstance()->getStyle()->styleID == $this->style->styleID) {
1044 WCF::getSession()->setStyleID($this->style->styleID);
9ba60a8e
AE
1045
1046 if (WCF::getUser()->userID) {
1047 // set this as the permanent style
1048 $userAction = new UserAction([WCF::getUser()], 'update', ['data' => [
63b9817b 1049 'styleID' => $this->style->isDefault ? 0 : $this->style->styleID
9ba60a8e
AE
1050 ]]);
1051 $userAction->executeAction();
1052 }
83736ee3 1053 }
83736ee3
AE
1054 }
1055
1056 /**
a17de04e 1057 * Validates the 'getStyleChooser' action.
83736ee3 1058 */
a17de04e
MS
1059 public function validateGetStyleChooser() {
1060 // does nothing
1061 }
83736ee3
AE
1062
1063 /**
1064 * Returns the style chooser dialog.
1065 *
59ab4d0f 1066 * @return string[]
83736ee3
AE
1067 */
1068 public function getStyleChooser() {
1069 $styleList = new StyleList();
1070 if (!WCF::getSession()->getPermission('admin.style.canUseDisabledStyle')) {
59ab4d0f 1071 $styleList->getConditionBuilder()->add("style.isDisabled = ?", [0]);
83736ee3
AE
1072 }
1073 $styleList->sqlOrderBy = "style.styleName ASC";
1074 $styleList->readObjects();
1075
59ab4d0f 1076 WCF::getTPL()->assign([
83736ee3 1077 'styleList' => $styleList
59ab4d0f 1078 ]);
83736ee3 1079
59ab4d0f 1080 return [
83736ee3
AE
1081 'actionName' => 'getStyleChooser',
1082 'template' => WCF::getTPL()->fetch('styleChooser')
59ab4d0f 1083 ];
83736ee3 1084 }
90b4b964 1085
63b02e3f 1086 /**
08b681a4
MW
1087 * Validates the mark as tainted action.
1088 *
e71525e4 1089 * @since 3.0
63b02e3f 1090 */
90b4b964
AE
1091 public function validateMarkAsTainted() {
1092 if (!WCF::getSession()->getPermission('admin.style.canManageStyle')) {
1093 throw new PermissionDeniedException();
1094 }
1095
1096 $this->styleEditor = $this->getSingleObject();
1097 }
1098
63b02e3f 1099 /**
08b681a4
MW
1100 * Marks a style as tainted.
1101 *
e71525e4 1102 * @since 3.0
63b02e3f 1103 */
90b4b964
AE
1104 public function markAsTainted() {
1105 // merge definitions
1106 $variables = $this->styleEditor->getVariables();
97ec0367
MW
1107 $variables['individualScss'] = str_replace("/* WCF_STYLE_CUSTOM_USER_MODIFICATIONS */\n", '', $variables['individualScss']);
1108 $variables['overrideScss'] = str_replace("/* WCF_STYLE_CUSTOM_USER_MODIFICATIONS */\n", '', $variables['overrideScss']);
90b4b964
AE
1109 $this->styleEditor->setVariables($variables);
1110
1111 $this->styleEditor->update([
cfd3e134
AE
1112 'isTainted' => 1,
1113 'packageName' => ''
90b4b964
AE
1114 ]);
1115 }
dcb3a44c 1116}