*/
class StyleAction extends AbstractDatabaseObjectAction {
/**
- * @see wcf\data\AbstractDatabaseObjectAction::$className
+ * @see wcf\data\AbstractDatabaseObjectAction::$className
*/
protected $className = 'wcf\data\style\StyleEditor';
+
+ /**
+ * @see wcf\data\AbstractDatabaseObjectAction::$permissionsDelete
+ */
+ protected $permissionsDelete = array('admin.style.canDeleteStyle');
+
+ /**
+ * @see wcf\data\AbstractDatabaseObjectAction::$permissionsUpdate
+ */
+ protected $permissionsUpdate = array('admin.style.canEditStyle');
+
+ /**
+ * @see wcf\data\AbstractDatabaseObjectAction::create()
+ */
+ public function create() {
+ $style = parent::create();
+
+ // add variables
+ $this->updateVariables($style);
+
+ // handle style preview image
+ $this->updateStylePreviewImage($style);
+
+ return $style;
+ }
+
+ /**
+ * @see wcf\data\AbstractDatabaseObjectAction::update()
+ */
+ public function update() {
+ parent::update();
+
+ foreach ($this->objects as $style) {
+ // update variables
+ $this->updateVariables($style->getDecoratedObject(), true);
+
+ // handle style preview image
+ $this->updateStylePreviewImage($style->getDecoratedObject());
+
+ // reset stylesheet
+ StyleHandler::getInstance()->resetStylesheet($style->getDecoratedObject());
+ }
+ }
+
+ /**
+ * @see wcf\data\AbstractDatabaseObjectAction::delete()
+ */
+ public function delete() {
+ $count = parent::delete();
+
+ foreach ($this->objects as $style) {
+ // remove custom icons
+ if ($style->iconPath && $style->iconPath != 'icon/') {
+ $this->removeDirectory($style->iconPath);
+ }
+
+ // 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->__toString());
+ }
+ else {
+ @unlink($path->__toString());
+ }
+ }
+
+ @rmdir($dir);
+ }
+ }
+
+ /**
+ * Updates style variables for given style.
+ *
+ * @param wcf\data\style\Style $style
+ * @param boolean $removePreviousVariables
+ */
+ protected function updateVariables(Style $style, $removePreviousVariables = false) {
+ if (!isset($this->parameters['variables']) || !is_array($this->parameters['variables'])) {
+ return;
+ }
+
+ $sql = "SELECT variableID, variableName, defaultValue
+ FROM wcf".WCF_N."_style_variable";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute();
+ $variables = array();
+ 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(array($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(array(
+ $style->styleID,
+ $variableID,
+ $variableValue
+ ));
+ }
+ WCF::getDB()->commitTransaction();
+ }
+ }
+
+ /**
+ * Updates style preview image.
+ *
+ * @param wcf\data\style\Style $style
+ */
+ protected function updateStylePreviewImage(Style $style) {
+ if (!isset($this->parameters['tmpHash'])) {
+ return;
+ }
+
+ $fileExtension = WCF::getSession()->getVar('stylePreview-'.$this->parameters['tmpHash']);
+ if ($fileExtension !== null) {
+ $oldFilename = WCF_DIR.'images/stylePreview-'.$this->parameters['tmpHash'].'.'.$fileExtension;
+ if (file_exists($oldFilename)) {
+ $filename = 'stylePreview-'.$style->styleID.'.'.$fileExtension;
+ if (@rename($oldFilename, WCF_DIR.'images/'.$filename)) {
+ // delete old file if it has a different file extension
+ if ($style->image != $filename) {
+ @unlink(WCF_DIR.'images/'.$style->image);
+
+ // update filename in database
+ $sql = "UPDATE wcf".WCF_N."_style
+ SET image = ?
+ WHERE styleID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array(
+ $filename,
+ $style->styleID
+ ));
+ }
+ }
+ else {
+ // remove temp file
+ @unlink($oldFilename);
+ }
+ }
+ }
+ }
+
+ /**
+ * Validates the upload action.
+ */
+ public function validateUpload() {
+ // check upload permissions
+ if (!WCF::getSession()->getPermission('admin.style.canAddStyle')) {
+ throw new PermissionDeniedException();
+ }
+
+ if (!isset($this->parameters['tmpHash']) || empty($this->parameters['tmpHash'])) {
+ throw new UserInputException('tmpHash');
+ }
+
+ if (count($this->parameters['__files']->getFiles()) != 1) {
+ throw new IllegalLinkException();
+ }
+
+ // check max filesize, allowed file extensions etc.
+ $this->parameters['__files']->validateFiles(new DefaultUploadFileValidationStrategy(PHP_INT_MAX, array('jpg', 'jpeg', 'png')));
+ }
+
+ /**
+ * Handles uploaded preview images.
+ *
+ * @return array<string>
+ */
+ public function upload() {
+ // save files
+ $files = $this->parameters['__files']->getFiles();
+ $file = $files[0];
+
+ try {
+ if (!$file->getValidationErrorType()) {
+ // shrink avatar if necessary
+ $fileLocation = $file->getLocation();
+ $imageData = getimagesize($fileLocation);
+ if ($imageData[0] > Style::PREVIEW_IMAGE_MAX_WIDTH || $imageData[1] > Style::PREVIEW_IMAGE_MAX_HEIGHT) {
+ try {
+ $adapter = ImageHandler::getInstance()->getAdapter();
+ $adapter->loadFile($fileLocation);
+ $fileLocation = FileUtil::getTemporaryFilename();
+ $thumbnail = $adapter->createThumbnail(Style::PREVIEW_IMAGE_MAX_WIDTH, Style::PREVIEW_IMAGE_MAX_HEIGHT, false);
+ $adapter->writeImage($thumbnail, $fileLocation);
+ $imageData = getimagesize($fileLocation);
+ }
+ catch (SystemException $e) {
+ throw new UserInputException('image');
+ }
+ }
+
+ // move uploaded file
+ if (@copy($fileLocation, WCF_DIR.'images/stylePreview-'.$this->parameters['tmpHash'].'.'.$file->getFileExtension())) {
+ @unlink($fileLocation);
+
+ // store extension within session variables
+ WCF::getSession()->register('stylePreview-'.$this->parameters['tmpHash'], $file->getFileExtension());
+
+ // return result
+ return array(
+ 'errorType' => '',
+ 'url' => WCF::getPath().'images/stylePreview-'.$this->parameters['tmpHash'].'.'.$file->getFileExtension()
+ );
+ }
+ else {
+ throw new UserInputException('image', 'uploadFailed');
+ }
+ }
+ }
+ catch (UserInputException $e) {
+ $file->setValidationErrorType($e->getType());
+ }
+
+ return array('errorType' => $file->getValidationErrorType());
+ }
+
+ /**
+ * Validates parameters to assign a new default style.
+ */
+ public function validateSetAsDefault() {
+ if (!WCF::getSession()->getPermission('admin.style.canEditStyle')) {
+ throw new PermissionDeniedException();
+ }
+
+ if (empty($this->objects)) {
+ $this->readObjects();
+ if (empty($this->objects)) {
+ throw new UserInputException('objectIDs');
+ }
+ }
+
+ if (count($this->objects) > 1) {
+ throw new UserInputException('objectIDs');
+ }
+ }
+
+ /**
+ * Sets a style as new default style.
+ */
+ public function setAsDefault() {
+ $styleEditor = current($this->objects);
+ $styleEditor->setAsDefault();
+ }
+
+ /**
+ * Validates parameters to copy a style.
+ */
+ public function validateCopy() {
+ if (!WCF::getSession()->getPermission('admin.style.canAddStyle')) {
+ throw new PermissionDeniedException();
+ }
+
+ if (empty($this->objects)) {
+ $this->readObjects();
+ if (empty($this->objects)) {
+ throw new UserInputException('objectIDs');
+ }
+ }
+
+ if (count($this->objects) > 1) {
+ throw new UserInputException('objectIDs');
+ }
+ }
+
+ /**
+ * Copies a style.
+ *
+ * @return array<string>
+ */
+ public function copy() {
+ $style = current($this->objects);
+
+ // get unique style name
+ $sql = "SELECT styleName
+ FROM wcf".WCF_N."_style
+ WHERE styleName LIKE ?
+ AND styleID <> ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array(
+ $style->styleName.'%',
+ $style->styleID
+ ));
+ $numbers = array();
+ $regEx = new Regex('\((\d+)\)$');
+ while ($row = $statement->fetchArray()) {
+ $styleName = $row['styleName'];
+
+ if ($regEx->match($styleName)) {
+ $matches = $regEx->getMatches();
+
+ // check if name matches the pattern 'styleName (x)'
+ if ($styleName == $style->styleName . ' ('.$matches[1].')') {
+ $numbers[] = $matches[1];
+ }
+ }
+ }
+
+ $number = (count($numbers)) ? max($numbers) + 1 : 2;
+ $styleName = $style->styleName . ' ('.$number.')';
+
+ // create the new style
+ $newStyle = StyleEditor::create(array(
+ 'packageID' => PACKAGE_ID,
+ 'styleName' => $styleName,
+ 'templateGroupID' => $style->templateGroupID,
+ 'disabled' => 1, // newly created styles are disabled by default
+ 'styleDescription' => $style->styleDescription,
+ 'styleVersion' => $style->styleVersion,
+ 'styleDate' => $style->styleDate,
+ 'copyright' => $style->copyright,
+ 'license' => $style->license,
+ 'authorName' => $style->authorName,
+ 'authorURL' => $style->authorURL,
+ 'iconPath' => $style->iconPath,
+ 'imagePath' => $style->imagePath
+ ));
+
+ // copy style variables
+ $sql = "INSERT INTO wcf".WCF_N."_style_variable_value
+ (styleID, variableID, variableValue)
+ SELECT ".$newStyle->styleID." AS styleID, value.variableID, value.variableValue
+ FROM wcf".WCF_N."_style_variable_value value
+ WHERE value.styleID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array($style->styleID));
+
+ // copy preview image
+ if ($style->image) {
+ // get extension
+ $fileExtension = StringUtil::substring($style->image, StringUtil::lastIndexOf($style->image, '.'));
+
+ // copy existing preview image
+ if (@copy(WCF_DIR.'images/'.$style->image, WCF_DIR.'images/stylePreview-'.$newStyle->styleID.$fileExtension)) {
+ // bypass StyleEditor::update() to avoid scaling of already fitting image
+ $sql = "UPDATE wcf".WCF_N."_style
+ SET image = ?
+ WHERE styleID = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute(array(
+ 'stylePreview-'.$newStyle->styleID.$fileExtension,
+ $newStyle->styleID
+ ));
+ }
+ }
+
+ return array(
+ 'redirectURL' => LinkHandler::getInstance()->getLink('StyleEdit', array('id' => $newStyle->styleID))
+ );
+ }
+
+ /**
+ * Validates parameters to enable/disable styles.
+ */
+ public function validateToggle() {
+ parent::validateUpdate();
+
+ foreach ($this->objects as $style) {
+ if ($style->isDefault) {
+ throw new UserInputException('objectIDs');
+ }
+ }
+ }
+
+ /**
+ * Enables/disables styles.
+ */
+ public function toggle() {
+ foreach ($this->objects as $style) {
+ $disabled = ($style->disabled) ? 0 : 1;
+ $style->update(array('disabled' => $disabled));
+ }
+ }
}