/**
* Executes style-related actions.
- *
- * @author Alexander Ebert
- * @copyright 2001-2019 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package WoltLabSuite\Core\Data\Style
- *
- * @method StyleEditor[] getObjects()
- * @method StyleEditor getSingleObject()
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Style
+ *
+ * @method StyleEditor[] getObjects()
+ * @method StyleEditor getSingleObject()
*/
-class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction {
- use TDatabaseObjectToggle;
-
- /**
- * @inheritDoc
- */
- protected $allowGuestAccess = ['changeStyle', 'getStyleChooser'];
-
- /**
- * @inheritDoc
- */
- protected $className = StyleEditor::class;
-
- /**
- * @inheritDoc
- */
- protected $permissionsDelete = ['admin.style.canManageStyle'];
-
- /**
- * @inheritDoc
- */
- protected $permissionsUpdate = ['admin.style.canManageStyle'];
-
- /**
- * @inheritDoc
- */
- protected $requireACP = ['copy', 'delete', 'markAsTainted', 'setAsDefault', 'toggle', 'update', 'upload',];
-
- /**
- * style object
- * @var Style
- */
- public $style;
-
- /**
- * style editor object
- * @var StyleEditor
- */
- public $styleEditor;
-
- /**
- * @inheritDoc
- * @return Style
- */
- public function create() {
- /** @var Style $style */
- $style = parent::create();
-
- // add variables
- $this->updateVariables($style);
-
- // handle style preview image
- $this->updateStylePreviewImage($style);
-
- // create favicon data
- $this->updateFavicons($style);
-
- // handle the cover photo
- $this->updateCoverPhoto($style);
-
- // handle custom assets
- $this->updateCustomAssets($style);
-
- return $style;
- }
-
- /**
- * @inheritDoc
- */
- public function update() {
- parent::update();
-
- foreach ($this->getObjects() as $style) {
- // update variables
- $this->updateVariables($style->getDecoratedObject(), true);
-
- // handle style preview image
- $this->updateStylePreviewImage($style->getDecoratedObject());
-
- // create favicon data
- $this->updateFavicons($style->getDecoratedObject());
-
- // handle the cover photo
- $this->updateCoverPhoto($style->getDecoratedObject());
-
- // handle custom assets
- $this->updateCustomAssets($style->getDecoratedObject());
-
- // reset stylesheet
- StyleHandler::getInstance()->resetStylesheet($style->getDecoratedObject());
- }
- }
-
- /**
- * @inheritDoc
- */
- public function delete() {
- $count = parent::delete();
-
- foreach ($this->getObjects() as $style) {
- // remove custom images
- if ($style->imagePath && $style->imagePath != 'images/') {
- $this->removeDirectory($style->imagePath);
- }
-
- // remove preview image
- $previewImage = WCF_DIR.'images/'.$style->image;
- if (file_exists($previewImage)) {
- @unlink($previewImage);
- }
-
- // remove stylesheet
- StyleHandler::getInstance()->resetStylesheet($style->getDecoratedObject());
- }
-
- return $count;
- }
-
- /**
- * Recursively removes a directory and all it's contents.
- *
- * @param string $pathComponent
- */
- protected function removeDirectory($pathComponent) {
- $dir = WCF_DIR.$pathComponent;
- if (is_dir($dir)) {
- $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir), \RecursiveIteratorIterator::CHILD_FIRST);
- foreach ($iterator as $path) {
- if ($path->isDir()) {
- @rmdir($path);
- }
- else {
- @unlink($path);
- }
- }
-
- @rmdir($dir);
- }
- }
-
- /**
- * Updates style variables for given style.
- *
- * @param Style $style
- * @param boolean $removePreviousVariables
- */
- protected function updateVariables(Style $style, $removePreviousVariables = false) {
- if (!isset($this->parameters['variables']) || !is_array($this->parameters['variables'])) {
- return;
- }
-
- $style->loadVariables();
- foreach (['pageLogo', 'pageLogoMobile'] as $type) {
- if (array_key_exists($type, $this->parameters['uploads'])) {
- /** @var \wcf\system\file\upload\UploadFile $file */
- $file = $this->parameters['uploads'][$type];
-
- if ($style->getVariable($type) && file_exists($style->getAssetPath().basename($style->getVariable($type)))) {
- if (!$file || $style->getAssetPath().basename($style->getVariable($type)) !== $file->getLocation()) {
- unlink($style->getAssetPath().basename($style->getVariable($type)));
- }
- }
-
- if ($file !== null) {
- if (!$file->isProcessed()) {
- $fileLocation = $file->getLocation();
- $extension = pathinfo($file->getFilename(), PATHINFO_EXTENSION);
- $newName = $type.'-'.\bin2hex(\random_bytes(4)).'.'.$extension;
- $newLocation = $style->getAssetPath().$newName;
- rename($fileLocation, $newLocation);
- $this->parameters['variables'][$type] = $newName;
- $file->setProcessed($newLocation);
- }
- else {
- $this->parameters['variables'][$type] = \basename($file->getLocation());
- }
- }
- else {
- $this->parameters['variables'][$type] = '';
- }
- }
- }
-
- $sql = "SELECT variableID, variableName, defaultValue
- FROM wcf".WCF_N."_style_variable";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute();
- $variables = [];
- while ($row = $statement->fetchArray()) {
- $variableName = $row['variableName'];
-
- // ignore variables with identical value
- if (isset($this->parameters['variables'][$variableName])) {
- if ($this->parameters['variables'][$variableName] == $row['defaultValue']) {
- continue;
- }
- else {
- $variables[$row['variableID']] = $this->parameters['variables'][$variableName];
- }
- }
- }
-
- // remove previously set variables
- if ($removePreviousVariables) {
- $sql = "DELETE FROM wcf".WCF_N."_style_variable_value
- WHERE styleID = ?";
- $statement = WCF::getDB()->prepareStatement($sql);
- $statement->execute([$style->styleID]);
- }
-
- // insert variables that differ from default values
- if (!empty($variables)) {
- $sql = "INSERT INTO wcf".WCF_N."_style_variable_value
- (styleID, variableID, variableValue)
- VALUES (?, ?, ?)";
- $statement = WCF::getDB()->prepareStatement($sql);
-
- WCF::getDB()->beginTransaction();
- foreach ($variables as $variableID => $variableValue) {
- $statement->execute([
- $style->styleID,
- $variableID,
- $variableValue
- ]);
- }
- WCF::getDB()->commitTransaction();
- }
- }
-
- /**
- * Updates style preview image.
- *
- * @param Style $style
- */
- protected function updateStylePreviewImage(Style $style) {
- foreach (['image', 'image2x'] as $type) {
- if (array_key_exists($type, $this->parameters['uploads'])) {
- /** @var \wcf\system\file\upload\UploadFile $file */
- $file = $this->parameters['uploads'][$type];
-
- if ($style->{$type} && file_exists($style->getAssetPath().basename($style->{$type}))) {
- if (!$file || $style->getAssetPath().basename($style->{$type}) !== $file->getLocation()) {
- unlink($style->getAssetPath().basename($style->{$type}));
- }
- }
- if ($file !== null) {
- $fileLocation = $file->getLocation();
- if (($imageData = getimagesize($fileLocation)) === false) {
- throw new \InvalidArgumentException('The given '.$type.' is not an image');
- }
- $extension = ImageUtil::getExtensionByMimeType($imageData['mime']);
- if ($type === 'image') {
- $newName = 'stylePreview.'.$extension;
- }
- else if ($type === 'image2x') {
- $newName = 'stylePreview@2x.'.$extension;
- }
- else {
- throw new \LogicException('Unreachable');
- }
- $newLocation = $style->getAssetPath().$newName;
- rename($fileLocation, $newLocation);
- (new StyleEditor($style))->update([
- $type => FileUtil::getRelativePath(WCF_DIR.'images/', $style->getAssetPath()).$newName,
- ]);
-
- $file->setProcessed($newLocation);
- }
- else {
- (new StyleEditor($style))->update([
- $type => '',
- ]);
- }
- }
- }
- }
-
- /**
- * Updates style favicon files.
- *
- * @param Style $style
- * @since 3.1
- */
- protected function updateFavicons(Style $style) {
- $images = [
- 'android-chrome-192x192.png' => 192,
- 'android-chrome-256x256.png' => 256,
- 'apple-touch-icon.png' => 180,
- 'mstile-150x150.png' => 150
- ];
-
- $hasFavicon = $style->hasFavicon;
- if (array_key_exists('favicon', $this->parameters['uploads'])) {
- /** @var \wcf\system\file\upload\UploadFile $file */
- $file = $this->parameters['uploads']['favicon'];
-
- if ($file !== null) {
- $fileLocation = $file->getLocation();
- if (($imageData = getimagesize($fileLocation)) === false) {
- throw new \InvalidArgumentException('The given favicon is not an image');
- }
- $extension = ImageUtil::getExtensionByMimeType($imageData['mime']);
- $newName = "favicon-template.".$extension;
- $newLocation = $style->getAssetPath().$newName;
- rename($fileLocation, $newLocation);
-
- // Create browser specific files.
- $adapter = ImageHandler::getInstance()->getAdapter();
- $adapter->loadFile($newLocation);
- foreach ($images as $filename => $length) {
- $thumbnail = $adapter->createThumbnail($length, $length);
- $adapter->writeImage($thumbnail, $style->getAssetPath().$filename);
- // Clear thumbnail as soon as possible to free up the memory.
- $thumbnail = null;
- }
-
- // Create ICO file.
- require(WCF_DIR . 'lib/system/api/chrisjean/php-ico/class-php-ico.php');
- (new \PHP_ICO($newLocation, [
- [16, 16],
- [32, 32]
- ]))->save_ico($style->getAssetPath()."favicon.ico");
-
- (new StyleEditor($style))->update([
- 'hasFavicon' => 1,
- ]);
-
- $file->setProcessed($newLocation);
- $hasFavicon = true;
- }
- else {
- foreach ($images as $filename => $length) {
- unlink($style->getAssetPath().$filename);
- }
- unlink($style->getAssetPath()."favicon.ico");
- foreach (['png', 'jpg', 'gif'] as $extension) {
- if (file_exists($style->getAssetPath()."favicon-template.".$extension)) {
- unlink($style->getAssetPath()."favicon-template.".$extension);
- }
- }
- (new StyleEditor($style))->update([
- 'hasFavicon' => 0,
- ]);
-
- $hasFavicon = false;
- }
- }
-
- if ($hasFavicon) {
- // update manifest.json
- $manifest = <<<MANIFEST
+class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
+{
+ use TDatabaseObjectToggle;
+
+ /**
+ * @inheritDoc
+ */
+ protected $allowGuestAccess = ['changeStyle', 'getStyleChooser'];
+
+ /**
+ * @inheritDoc
+ */
+ protected $className = StyleEditor::class;
+
+ /**
+ * @inheritDoc
+ */
+ protected $permissionsDelete = ['admin.style.canManageStyle'];
+
+ /**
+ * @inheritDoc
+ */
+ protected $permissionsUpdate = ['admin.style.canManageStyle'];
+
+ /**
+ * @inheritDoc
+ */
+ protected $requireACP = ['copy', 'delete', 'markAsTainted', 'setAsDefault', 'toggle', 'update', 'upload'];
+
+ /**
+ * style object
+ * @var Style
+ */
+ public $style;
+
+ /**
+ * style editor object
+ * @var StyleEditor
+ */
+ public $styleEditor;
+
+ /**
+ * @inheritDoc
+ * @return Style
+ */
+ public function create()
+ {
+ /** @var Style $style */
+ $style = parent::create();
+
+ // add variables
+ $this->updateVariables($style);
+
+ // handle style preview image
+ $this->updateStylePreviewImage($style);
+
+ // create favicon data
+ $this->updateFavicons($style);
+
+ // handle the cover photo
+ $this->updateCoverPhoto($style);
+
+ // handle custom assets
+ $this->updateCustomAssets($style);
+
+ return $style;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function update()
+ {
+ parent::update();
+
+ foreach ($this->getObjects() as $style) {
+ // update variables
+ $this->updateVariables($style->getDecoratedObject(), true);
+
+ // handle style preview image
+ $this->updateStylePreviewImage($style->getDecoratedObject());
+
+ // create favicon data
+ $this->updateFavicons($style->getDecoratedObject());
+
+ // handle the cover photo
+ $this->updateCoverPhoto($style->getDecoratedObject());
+
+ // handle custom assets
+ $this->updateCustomAssets($style->getDecoratedObject());
+
+ // reset stylesheet
+ StyleHandler::getInstance()->resetStylesheet($style->getDecoratedObject());
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function delete()
+ {
+ $count = parent::delete();
+
+ foreach ($this->getObjects() as $style) {
+ // remove custom images
+ if ($style->imagePath && $style->imagePath != 'images/') {
+ $this->removeDirectory($style->imagePath);
+ }
+
+ // remove preview image
+ $previewImage = WCF_DIR . 'images/' . $style->image;
+ if (\file_exists($previewImage)) {
+ @\unlink($previewImage);
+ }
+
+ // remove stylesheet
+ StyleHandler::getInstance()->resetStylesheet($style->getDecoratedObject());
+ }
+
+ return $count;
+ }
+
+ /**
+ * Recursively removes a directory and all it's contents.
+ *
+ * @param string $pathComponent
+ */
+ protected function removeDirectory($pathComponent)
+ {
+ $dir = WCF_DIR . $pathComponent;
+ if (\is_dir($dir)) {
+ $iterator = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($dir),
+ \RecursiveIteratorIterator::CHILD_FIRST
+ );
+ foreach ($iterator as $path) {
+ if ($path->isDir()) {
+ @\rmdir($path);
+ } else {
+ @\unlink($path);
+ }
+ }
+
+ @\rmdir($dir);
+ }
+ }
+
+ /**
+ * Updates style variables for given style.
+ *
+ * @param Style $style
+ * @param bool $removePreviousVariables
+ */
+ protected function updateVariables(Style $style, $removePreviousVariables = false)
+ {
+ if (!isset($this->parameters['variables']) || !\is_array($this->parameters['variables'])) {
+ return;
+ }
+
+ $style->loadVariables();
+ foreach (['pageLogo', 'pageLogoMobile'] as $type) {
+ if (\array_key_exists($type, $this->parameters['uploads'])) {
+ /** @var \wcf\system\file\upload\UploadFile $file */
+ $file = $this->parameters['uploads'][$type];
+
+ if ($style->getVariable($type) && \file_exists($style->getAssetPath() . \basename($style->getVariable($type)))) {
+ if (!$file || $style->getAssetPath() . \basename($style->getVariable($type)) !== $file->getLocation()) {
+ \unlink($style->getAssetPath() . \basename($style->getVariable($type)));
+ }
+ }
+
+ if ($file !== null) {
+ if (!$file->isProcessed()) {
+ $fileLocation = $file->getLocation();
+ $extension = \pathinfo($file->getFilename(), \PATHINFO_EXTENSION);
+ $newName = $type . '-' . Hex::encode(\random_bytes(4)) . '.' . $extension;
+ $newLocation = $style->getAssetPath() . $newName;
+ \rename($fileLocation, $newLocation);
+ $this->parameters['variables'][$type] = $newName;
+ $file->setProcessed($newLocation);
++ } else {
++ $this->parameters['variables'][$type] = \basename($file->getLocation());
+ }
+ } else {
+ $this->parameters['variables'][$type] = '';
+ }
+ }
+ }
+
+ $sql = "SELECT variableID, variableName, defaultValue
+ FROM wcf" . WCF_N . "_style_variable";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute();
+ $variables = [];
+ while ($row = $statement->fetchArray()) {
+ $variableName = $row['variableName'];
+
+ // ignore variables with identical value
+ if (isset($this->parameters['variables'][$variableName])) {
+ if ($this->parameters['variables'][$variableName] == $row['defaultValue']) {
+ continue;
+ } else {
+ $variables[$row['variableID']] = $this->parameters['variables'][$variableName];
+ }
+ }
+ }
+
+ // remove previously set variables
+ if ($removePreviousVariables) {
+ $sql = "DELETE FROM wcf" . WCF_N . "_style_variable_value
+ WHERE styleID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([$style->styleID]);
+ }
+
+ // insert variables that differ from default values
+ if (!empty($variables)) {
+ $sql = "INSERT INTO wcf" . WCF_N . "_style_variable_value
+ (styleID, variableID, variableValue)
+ VALUES (?, ?, ?)";
+ $statement = WCF::getDB()->prepareStatement($sql);
+
+ WCF::getDB()->beginTransaction();
+ foreach ($variables as $variableID => $variableValue) {
+ $statement->execute([
+ $style->styleID,
+ $variableID,
+ $variableValue,
+ ]);
+ }
+ WCF::getDB()->commitTransaction();
+ }
+ }
+
+ /**
+ * Updates style preview image.
+ *
+ * @param Style $style
+ */
+ protected function updateStylePreviewImage(Style $style)
+ {
+ foreach (['image', 'image2x'] as $type) {
+ if (\array_key_exists($type, $this->parameters['uploads'])) {
+ /** @var \wcf\system\file\upload\UploadFile $file */
+ $file = $this->parameters['uploads'][$type];
+
+ if ($style->{$type} && \file_exists($style->getAssetPath() . \basename($style->{$type}))) {
+ if (!$file || $style->getAssetPath() . \basename($style->{$type}) !== $file->getLocation()) {
+ \unlink($style->getAssetPath() . \basename($style->{$type}));
+ }
+ }
+ if ($file !== null) {
+ $fileLocation = $file->getLocation();
+ if (($imageData = \getimagesize($fileLocation)) === false) {
+ throw new \InvalidArgumentException('The given ' . $type . ' is not an image');
+ }
+ $extension = ImageUtil::getExtensionByMimeType($imageData['mime']);
+ if ($type === 'image') {
+ $newName = 'stylePreview.' . $extension;
+ } elseif ($type === 'image2x') {
+ $newName = 'stylePreview@2x.' . $extension;
+ } else {
+ throw new \LogicException('Unreachable');
+ }
+ $newLocation = $style->getAssetPath() . $newName;
+ \rename($fileLocation, $newLocation);
+ (new StyleEditor($style))->update([
+ $type => FileUtil::getRelativePath(WCF_DIR . 'images/', $style->getAssetPath()) . $newName,
+ ]);
+
+ $file->setProcessed($newLocation);
+ } else {
+ (new StyleEditor($style))->update([
+ $type => '',
+ ]);
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates style favicon files.
+ *
+ * @param Style $style
+ * @since 3.1
+ */
+ protected function updateFavicons(Style $style)
+ {
+ $images = [
+ 'android-chrome-192x192.png' => 192,
+ 'android-chrome-256x256.png' => 256,
+ 'apple-touch-icon.png' => 180,
+ 'mstile-150x150.png' => 150,
+ ];
+
+ $hasFavicon = $style->hasFavicon;
+ if (\array_key_exists('favicon', $this->parameters['uploads'])) {
+ /** @var \wcf\system\file\upload\UploadFile $file */
+ $file = $this->parameters['uploads']['favicon'];
+
+ if ($file !== null) {
+ $fileLocation = $file->getLocation();
+ if (($imageData = \getimagesize($fileLocation)) === false) {
+ throw new \InvalidArgumentException('The given favicon is not an image');
+ }
+ $extension = ImageUtil::getExtensionByMimeType($imageData['mime']);
+ $newName = "favicon-template." . $extension;
+ $newLocation = $style->getAssetPath() . $newName;
+ \rename($fileLocation, $newLocation);
+
+ // Create browser specific files.
+ $adapter = ImageHandler::getInstance()->getAdapter();
+ $adapter->loadFile($newLocation);
+ foreach ($images as $filename => $length) {
+ $thumbnail = $adapter->createThumbnail($length, $length);
+ $adapter->writeImage($thumbnail, $style->getAssetPath() . $filename);
+ // Clear thumbnail as soon as possible to free up the memory.
+ $thumbnail = null;
+ }
+
+ // Create ICO file.
+ require(WCF_DIR . 'lib/system/api/chrisjean/php-ico/class-php-ico.php');
+ (new \PHP_ICO($newLocation, [
+ [16, 16],
+ [32, 32],
+ ]))->save_ico($style->getAssetPath() . "favicon.ico");
+
+ (new StyleEditor($style))->update([
+ 'hasFavicon' => 1,
+ ]);
+
+ $file->setProcessed($newLocation);
+ $hasFavicon = true;
+ } else {
+ foreach ($images as $filename => $length) {
+ \unlink($style->getAssetPath() . $filename);
+ }
+ \unlink($style->getAssetPath() . "favicon.ico");
+ foreach (['png', 'jpg', 'gif'] as $extension) {
+ if (\file_exists($style->getAssetPath() . "favicon-template." . $extension)) {
+ \unlink($style->getAssetPath() . "favicon-template." . $extension);
+ }
+ }
+ (new StyleEditor($style))->update([
+ 'hasFavicon' => 0,
+ ]);
+
+ $hasFavicon = false;
+ }
+ }
+
+ if ($hasFavicon) {
+ // update manifest.json
+ $manifest = <<<'MANIFEST'
{
"name": "",
"icons": [