<commit_before><?php
namespace wcf\data\style;
use wcf\data\language\category\LanguageCategory;
use wcf\data\language\LanguageList;
use wcf\data\package\Package;
use wcf\data\package\PackageCache;
use wcf\data\template\group\TemplateGroup;
use wcf\data\template\group\TemplateGroupAction;
use wcf\data\template\Template;
use wcf\data\template\TemplateEditor;
use wcf\data\DatabaseObjectEditor;
use wcf\data\IEditableCachedObject;
use wcf\system\application\ApplicationHandler;
use wcf\system\cache\builder\StyleCacheBuilder;
use wcf\system\exception\SystemException;
use wcf\system\image\ImageHandler;
use wcf\system\io\Tar;
use wcf\system\io\TarWriter;
use wcf\system\language\LanguageFactory;
use wcf\system\package\PackageArchive;
use wcf\system\style\StyleCompiler;
use wcf\system\style\StyleHandler;
use wcf\system\Regex;
use wcf\system\WCF;
use wcf\util\DateUtil;
use wcf\util\FileUtil;
use wcf\util\StringUtil;
use wcf\util\XML;
use wcf\util\XMLWriter;

/**
 * Provides functions to edit, import, export and delete a style.
 * 
 * @author	<NAME>
 * @copyright	2001-2017 WoltLab GmbH
 * @license	GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 * @package	WoltLabSuite\Core\Data\Style
 * 
 * @method	Style	getDecoratedObject()
 * @mixin	Style
 */
class StyleEditor extends DatabaseObjectEditor implements IEditableCachedObject {
	const EXCLUDE_WCF_VERSION = '3.2.0 Alpha 1';
	const INFO_FILE = 'style.xml';
	
	/**
	 * @inheritDoc
	 */
	protected static $baseClass = Style::class;
	
	/**
	 * @inheritDoc
	 */
	public function update(array $parameters = []) {
		$variables = null;
		if (isset($parameters['variables'])) {
			$variables = $parameters['variables'];
			unset($parameters['variables']);
		}
		
		// update style data
		parent::update($parameters);
		
		// update variables
		if ($variables !== null) {
			$this->setVariables($variables);
		}
		
		// scale preview image
		if (!empty($parameters['image']) && $parameters['image'] != $this->image) {
			self::scalePreviewImage($parameters['image']);
		}
	}
	
	/**
	 * @inheritDoc
	 */
	public function delete() {
		parent::delete();
		
		// delete variables
		$sql = "DELETE FROM	wcf".WCF_N."_style_variable_value
			WHERE		styleID = ?";
		$statement = WCF::getDB()->prepareStatement($sql);
		$statement->execute([$this->styleID]);
		
		// delete style files
		$files = @glob(WCF_DIR.'style/style-'.$this->styleID.'*.css');
		if (is_array($files)) {
			foreach ($files as $file) {
				@unlink($file);
			}
		}
		
		// delete preview image
		if ($this->image) {
			@unlink(WCF_DIR.'images/'.$this->image);
		}
		
		// delete language items
		$sql = "DELETE FROM	wcf".WCF_N."_language_item
			WHERE		languageItem = ?";
		$statement = WCF::getDB()->prepareStatement($sql);
		$statement->execute(['wcf.style.styleDescription'.$this->styleID]);
	}
	
	/**
	 * Sets this style as default style.
	 */
	public function setAsDefault() {
		// remove old default
		$sql = "UPDATE	wcf".WCF_N."_style
			SET	isDefault = ?
			WHERE	isDefault = ?";
		$statement = WCF::getDB()->prepareStatement($sql);
		$statement->execute([0, 1]);
		
		// set new default
		$this->update([
			'isDefault' => 1,
			'isDisabled' => 0
		]);
		
		self::resetCache();
	}
	
	/**
	 * Deletes the style's default cover photo.
	 */
	public function deleteCoverPhoto() {
		if ($this->coverPhotoExtension) {
			@unlink(WCF_DIR.'images/coverPhotos/'.$this->styleID.'.'.$this->coverPhotoExtension);
			
			$this->update([
				'coverPhotoExtension' => ''
			]);
		}
	}
	
	/**
	 * Reads the data of a style exchange format file.
	 * 
	 * @param	Tar	$tar
	 * @return	array
	 * @throws	SystemException
	 */
	public static function readStyleData(Tar $tar) {
		// search style.xml
		$index = $tar->getIndexByFilename(self::INFO_FILE);
		if ($index === false) {
			throw new SystemException("unable to find required file '".self::INFO_FILE."' in style archive");
		}
		
		// open style.xml
		$xml = new XML();
		$xml->loadXML(self::INFO_FILE, $tar->extractToString($index));
		$xpath = $xml->xpath();
		
		$data = [
			'name' => '', 'description' => [], 'version' => '', 'image' => '', 'image2x' => '', 'copyright' => '', 'default' => false,
			'license' => '', 'authorName' => '', 'authorURL' => '', 'templates' => '', 'images' => '', 'coverPhoto' => '',
			'variables' => '', 'date' => '0000-00-00', 'imagesPath' => '', 'packageName' => '', 'apiVersion' => '3.0'
		];
		
		$categories = $xpath->query('/ns:style/*');
		foreach ($categories as $category) {
			switch ($category->tagName) {
				case 'author':
					$elements = $xpath->query('child::*', $category);
					foreach ($elements as $element) {
						switch ($element->tagName) {
							case 'authorname':
								$data['authorName'] = $element->nodeValue;
							break;
							
							case 'authorurl':
								$data['authorURL'] = $element->nodeValue;
							break;
						}
					}
				break;
				
				case 'files':
					$elements = $xpath->query('child::*', $category);
					
					/** @var \DOMElement $element */
					foreach ($elements as $element) {
						$data[$element->tagName] = $element->nodeValue;
						if ($element->hasAttribute('path')) {
							$data[$element->tagName.'Path'] = $element->getAttribute('path');
						}
					}
				break;
				
				case 'general':
					$elements = $xpath->query('child::*', $category);
					foreach ($elements as $element) {
						switch ($element->tagName) {
							case 'date':
								DateUtil::validateDate($element->nodeValue);
								
								$data['date'] = $element->nodeValue;
							break;
							
							case 'default':
								$data['default'] = true;
							break;
							
							case 'description':
								if ($element->hasAttribute('language')) {
									$data['description'][$element->getAttribute('language')] = $element->nodeValue;
								}
							break;
							
							case 'stylename':
								$data['name'] = $element->nodeValue;
							break;
							
							case 'packageName':
								$data['packageName'] = $element->nodeValue;
							break;
							
							case 'version':
								if (!Package::isValidVersion($element->nodeValue)) {
									throw new SystemException("style version '".$element->nodeValue."' is invalid");
								}
								
								$data['version'] = $element->nodeValue;
							break;
							
							case 'copyright':
							case 'image':
							case 'image2x':
							case 'license':
							case 'coverPhoto':
								$data[$element->tagName] = $element->nodeValue;
							break;
							
							case 'apiVersion':
								if (!in_array($element->nodeValue, Style::$supportedApiVersions)) {
									throw new SystemException("Unknown api version '".$element->nodeValue."'");
								}
								
								$data['apiVersion'] = $element->nodeValue;
								break;
						}
					}
				break;
			}
		}
		
		if (empty($data['name'])) {
			throw new SystemException("required tag 'stylename' is missing in '".self::INFO_FILE."'");
		}
		if (empty($data['variables'])) {
			throw new SystemException("required tag 'variables' is missing in '".self::INFO_FILE."'");
		}
		
		// search variables.xml
		$index = $tar->getIndexByFilename($data['variables']);
		if ($index === false) {
			throw new SystemException("unable to find required file '".$data['variables']."' in style archive");
		}
		
		// open variables.xml
		$data['variables'] = self::readVariablesData($data['variables'], $tar->extractToString($index));
		
		return $data;
	}
	
	/**
	 * Reads the data of a variables.xml file.
	 * 
	 * @param	string		$filename
	 * @param	string		$content
	 * @return	array
	 */
	public static function readVariablesData($filename, $content) {
		// open variables.xml
		$xml = new XML();
		$xml->loadXML($filename, $content);
		$variables = $xml->xpath()->query('/ns:variables/ns:variable');
		
		$data = [];
		
		/** @var \DOMElement $variable */
		foreach ($variables as $variable) {
			$data[$variable->getAttribute('name')] = $variable->nodeValue;
		}
		
		return $data;
	}
	
	/**
	 * Returns the data of a style exchange format file.
	 * 
	 * @param	string		$filename
	 * @return	array
	 */
	public static function getStyleData($filename) {
		// open file
		$tar = new Tar($filename);
		
		// get style data
		$data = self::readStyleData($tar);
		
		// export preview image to temporary location
		if (!empty($data['image'])) {
			$i = $tar->getIndexByFilename($data['image']);
			if ($i !== false) {
				$path = FileUtil::getTemporaryFilename('stylePreview_', $data['image'], WCF_DIR.'tmp/');
				$data['image'] = basename($path);
				$tar->extract($i, $path);
			}
		}
		
		$tar->close();
		
		return $data;
	}
	
	/**
	 * Imports a style.
	 * 
	 * @param	string		$filename
	 * @param	integer		$packageID
	 * @param	StyleEditor	$style
	 * @return	StyleEditor
	 */
	public static function import($filename, $packageID = 1, StyleEditor $style = null) {
		// open file
		$tar = new Tar($filename);
		
		// get style data
		$data = self::readStyleData($tar);
		
		$styleData = [
			'styleName' => $data['name'],
			'variables' => $data['variables'],
			'styleVersion' => $data['version'],
			'styleDate' => $data['date'],
			'copyright' => $data['copyright'],
			'license' => $data['license'],
			'authorName' => $data['authorName'],
			'authorURL' => $data['authorURL'],
			'packageName' => $data['packageName'],
			'apiVersion' => $data['apiVersion']
		];
		
		// check if there is an untainted style with the same package name
		if ($style === null && !empty($styleData['packageName'])) {
			$style = StyleHandler::getInstance()->getStyleByName($styleData['packageName'], true);
		}
		
		// import images
		if (!empty($data['images']) && $data['imagesPath'] != 'images/') {
			// create images folder if necessary
			$imagesLocation = self::getFileLocation($data['imagesPath']);
			$styleData['imagePath'] = FileUtil::getRelativePath(WCF_DIR, $imagesLocation);
			
			$index = $tar->getIndexByFilename($data['images']);
			if ($index !== false) {
				// extract images tar
				$destination = FileUtil::getTemporaryFilename('images_');
				$tar->extract($index, $destination);
				
				// open images tar
				$imagesTar = new Tar($destination);
				$contentList = $imagesTar->getContentList();
				foreach ($contentList as $key => $val) {
					if ($val['type'] == 'file') {
						$imagesTar->extract($key, $imagesLocation.basename($val['filename']));
						FileUtil::makeWritable($imagesLocation.basename($val['filename']));
					}
				}
				
				// delete tmp file
				$imagesTar->close();
				@unlink($destination);
			}
		}
		
		// handle templates
		if (!empty($data['templates'])) {
			$templateGroupFolderName = '';
			if ($style !== null && $style->templateGroupID) {
				$templateGroupFolderName = (new TemplateGroup($style->templateGroupID))->templateGroupFolderName;
				$styleData['templateGroupID'] = $style->templateGroupID;
			}
			
			if (empty($templateGroupFolderName)) {
				// create template group
				$templateGroupName = $originalTemplateGroupName = $data['name'];
				$templateGroupFolderName = preg_replace('/[^a-z0-9_-]/i', '', $templateGroupName);
				if (empty($templateGroupFolderName)) $templateGroupFolderName = 'generic'.mb_substr(StringUtil::getRandomID(), 0, 8);
				$originalTemplateGroupFolderName = $templateGroupFolderName;
				
				// get unique template group name
				$i = 1;
				while (true) {
					$sql = "SELECT	COUNT(*)
						FROM	wcf".WCF_N."_template_group
						WHERE	templateGroupName = ?";
					$statement = WCF::getDB()->prepareStatement($sql);
					$statement->execute([$templateGroupName]);
					if (!$statement->fetchSingleColumn()) break;
					$templateGroupName = $originalTemplateGroupName . '_' . $i;
					$i++;
				}
				
				// get unique folder name
				$i = 1;
				while (true) {
					$sql = "SELECT	COUNT(*)
						FROM	wcf".WCF_N."_template_group
						WHERE	templateGroupFolderName = ?";
					$statement = WCF::getDB()->prepareStatement($sql);
					$statement->execute([
						FileUtil::addTrailingSlash($templateGroupFolderName)
					]);
					if (!$statement->fetchSingleColumn()) break;
					$templateGroupFolderName = $originalTemplateGroupFolderName . '_' . $i;
					$i++;
				}
				
				$templateGroupAction = new TemplateGroupAction([], 'create', [
					'data' => [
						'templateGroupName' => $templateGroupName,
						'templateGroupFolderName' => FileUtil::addTrailingSlash($templateGroupFolderName)
					]
				]);
				$returnValues = $templateGroupAction->executeAction();
				$styleData['templateGroupID'] = $returnValues['returnValues']->templateGroupID;
			}
			
			// import templates
			$index = $tar->getIndexByFilename($data['templates']);
			if ($index !== false) {
				// extract templates tar
				$destination = FileUtil::getTemporaryFilename('templates_');
				$tar->extract($index, $destination);
				
				// open templates tar and group templates by package
				$templatesTar = new Tar($destination);
				$contentList = $templatesTar->getContentList();
				$packageToTemplates = [];
				foreach ($contentList as $val) {
					if ($val['type'] == 'file') {
						$folders = explode('/', $val['filename']);
						$packageName = array_shift($folders);
						if (!isset($packageToTemplates[$packageName])) {
							$packageToTemplates[$packageName] = [];
						}
						$packageToTemplates[$packageName][] = ['index' => $val['index'], 'filename' => implode('/', $folders)];
					}
				}
				
				$knownTemplates = [];
				if ($style !== null && $style->templateGroupID) {
					$sql = "SELECT	templateName
						FROM	wcf".WCF_N."_template
						WHERE	templateGroupID = ?";
					$statement = WCF::getDB()->prepareStatement($sql);
					$statement->execute([$style->templateGroupID]);
					$knownTemplates = $statement->fetchAll(\PDO::FETCH_COLUMN);
				}
				
				// copy templates
				foreach ($packageToTemplates as $package => $templates) {
					// try to find package
					$sql = "SELECT	*
						FROM	wcf".WCF_N."_package
						WHERE	package = ?
							AND isApplication = ?";
					$statement = WCF::getDB()->prepareStatement($sql);
					$statement->execute([
						$package,
						1
					]);
					while ($row = $statement->fetchArray()) {
						// get template path
						$templatesDir = FileUtil::addTrailingSlash(FileUtil::getRealPath(WCF_DIR.$row['packageDir']).'templates/'.$templateGroupFolderName);
						
						// create template path
						if (!file_exists($templatesDir)) {
							@mkdir($templatesDir, 0777);
							FileUtil::makeWritable($templatesDir);
						}
						
						// copy templates
						foreach ($templates as $template) {
							$templatesTar->extract($template['index'], $templatesDir.$template['filename']);
							
							$templateName = str_replace('.tpl', '', $template['filename']);
							if (!in_array($templateName, $knownTemplates)) {
								TemplateEditor::create([
									'application' => Package::getAbbreviation($package),
									'packageID' => $row['packageID'],
									'templateName' => $templateName,
									'templateGroupID' => $styleData['templateGroupID']
								]);
							}
						}
					}
				}
				
				// delete tmp file
				$templatesTar->close();
				@unlink($destination);
			}
		}
		
		// save style
		if ($style === null) {
			$styleData['packageID'] = $packageID;
			$style = new StyleEditor(self::create($styleData));
			
			// handle descriptions
			if (!empty($data['description'])) {
				self::saveLocalizedDescriptions($style, $data['description']);
				LanguageFactory::getInstance()->deleteLanguageCache();
			}
			
			if ($data['default']) {
				$style->setAsDefault();
			}
		}
		else {
			unset($styleData['styleName']);
			
			$variables = $style->getVariables();
			if (!isset($styleData['variables']['individualScss'])) $styleData['variables']['individualScss'] = '';
			if (!isset($styleData['variables']['overrideScss'])) $styleData['variables']['overrideScss'] = '';
			
			$individualScss = Style::splitLessVariables($variables['individualScss']);
			$variables['individualScss'] = Style::joinLessVariables($styleData['variables']['individualScss'], $individualScss['custom']);
			
			$overrideScss = Style::splitLessVariables($variables['overrideScss']);
			$variables['overrideScss'] = Style::joinLessVariables($styleData['variables']['overrideScss'], $overrideScss['custom']);
			
			$styleData['variables'] = $variables;
			
			$style->update($styleData);
		}
		
		// import preview image
		foreach (['image', 'image2x'] as $type) {
			if (!empty($data[$type])) {
				$fileExtension = mb_substr($data[$type], mb_strrpos($data[$type], '.'));
				$index = $tar->getIndexByFilename($data[$type]);
				if ($index !== false) {
					$filename = WCF_DIR . 'images/stylePreview-' . $style->styleID . ($type === 'image2x' ? '@2x' : '') . $fileExtension;
					$tar->extract($index, $filename);
					FileUtil::makeWritable($filename);
					
					if (file_exists($filename)) {
						try {
							if (($imageData = getimagesize($filename)) !== false) {
								switch ($imageData[2]) {
									case IMAGETYPE_PNG:
									case IMAGETYPE_JPEG:
									case IMAGETYPE_GIF:
										$style->update([$type => 'stylePreview-' . $style->styleID . ($type === 'image2x' ? '@2x' : '') . $fileExtension]);
								}
							}
						}
						catch (SystemException $e) {
							// broken image
						}
					}
				}
			}
		}
		
		// import cover photo
		if (!empty($data['coverPhoto'])) {
			$fileExtension = mb_substr($data['coverPhoto'], mb_strrpos($data['coverPhoto'], '.'));
			$index = $tar->getIndexByFilename($data['coverPhoto']);
			if ($index !== false) {
				$filename = WCF_DIR . 'images/coverPhotos/' . $style->styleID . $fileExtension;
				$tar->extract($index, $filename);
				FileUtil::makeWritable($filename);
				
				if (file_exists($filename)) {
					try {
						if (($imageData = getimagesize($filename)) !== false) {
							switch ($imageData[2]) {
								case IMAGETYPE_PNG:
								case IMAGETYPE_JPEG:
								case IMAGETYPE_GIF:
									$style->update(['coverPhotoExtension' => mb_substr($fileExtension, 1)]);
							}
						}
					}
					catch (SystemException $e) {
						// broken image
					}
				}
			}
		}
		
		$tar->close();
		
		return $style;
	}
	
	/**
	 * Saves localized style descriptions.
	 * 
	 * @param	StyleEditor	$styleEditor
	 * @param	string[]	$descriptions
	 */
	protected static function saveLocalizedDescriptions(StyleEditor $styleEditor, array $descriptions) {
		// localize package information
		$sql = "REPLACE INTO	wcf".WCF_N."_language_item
					(languageID, languageItem, languageItemValue, languageCategoryID, packageID)
			VALUES		(?, ?, ?, ?, ?)";
		$statement = WCF::getDB()->prepareStatement($sql);
		
		// get language list
		$languageList = new LanguageList();
		$languageList->readObjects();
		
		// workaround for WCFSetup
		if (!PACKAGE_ID) {
			$sql = "SELECT	*
				FROM	wcf".WCF_N."_language_category
				WHERE	languageCategory = ?";
			$statement2 = WCF::getDB()->prepareStatement($sql);
			$statement2->execute(['wcf.style']);
			$languageCategory = $statement2->fetchObject(LanguageCategory::class);
		}
		else {
			$languageCategory = LanguageFactory::getInstance()->getCategory('wcf.style');
		}
		
		foreach ($languageList as $language) {
			if (isset($descriptions[$language->languageCode])) {
				$statement->execute([
					$language->languageID,
					'wcf.style.styleDescription'.$styleEditor->styleID,
					$descriptions[$language->languageCode],
					$languageCategory->languageCategoryID,
					$styleEditor->packageID
				]);
			}
		}
		
		$styleEditor->update([
			'styleDescription' => 'wcf.style.styleDescription'.$styleEditor->styleID
		]);
	}
	
	/**
	 * Returns available location path.
	 * 
	 * @param	string		$location
	 * @return	string
	 */
	protected static function getFileLocation($location) {
		$location = FileUtil::removeLeadingSlash(FileUtil::removeTrailingSlash($location));
		$location = WCF_DIR.$location;
		
		$index = null;
		do {
			$directory = $location . ($index === null ? '' : $index);
			if (!is_dir($directory)) {
				@mkdir($directory, 0777, true);
				FileUtil::makeWritable($directory);
				
				return FileUtil::addTrailingSlash($directory);
			}
			
			$index = ($index === null ? 2 : ($index + 1));
		}
		while (true);
		
		// this should never happen
		throw new \LogicException();
	}
	
	/**
	 * Exports this style.
	 * 
	 * @param	boolean		$templates
	 * @param	boolean		$images
	 * @param	string		$packageName
	 */
	public function export($templates = false, $images = false, $packageName = '') {
		// create style tar
		$styleTarName = FileUtil::getTemporaryFilename('style_', '.tgz');
		$styleTar = new TarWriter($styleTarName, true);
		
		// append style preview image
		if ($this->image && @file_exists(WCF_DIR.'images/'.$this->image)) {
			$styleTar->add(WCF_DIR.'images/'.$this->image, '', FileUtil::addTrailingSlash(dirname(WCF_DIR.'images/'.$this->image)));
		}
		if ($this->image2x && @file_exists(WCF_DIR.'images/'.$this->image2x)) {
			$styleTar->add(WCF_DIR.'images/'.$this->image2x, '', FileUtil::addTrailingSlash(dirname(WCF_DIR.'images/'.$this->image2x)));
		}
		
		// append cover photo
		$coverPhoto = ($this->coverPhotoExtension) ? WCF_DIR.'images/coverPhotos/'.$this->styleID.'.'.$this->coverPhotoExtension : '';
		if ($coverPhoto && @file_exists($coverPhoto)) {
			$styleTar->add($coverPhoto, '', FileUtil::addTrailingSlash(dirname($coverPhoto)));
		}
		
		// fetch style description
		$sql = "SELECT		language.languageCode, language_item.languageItemValue
			FROM		wcf".WCF_N."_language_item language_item
			LEFT JOIN	wcf".WCF_N."_language language
			ON		(language.languageID = language_item.languageID)
			WHERE		language_item.languageItem = ?";
		$statement = WCF::getDB()->prepareStatement($sql);
		$statement->execute([$this->styleDescription]);
		$styleDescriptions = $statement->fetchMap('languageCode', 'languageItemValue');
		
		// create style info file
		$xml = new XMLWriter();
		$xml->beginDocument('style', 'http://www.woltlab.com', 'http://www.woltlab.com/XSD/vortex/style.xsd');
		
		// general block
		$xml->startElement('general');
		$xml->writeElement('stylename', $this->styleName);
		$xml->writeElement('packageName', $this->packageName);
		
		// style description
		foreach ($styleDescriptions as $languageCode => $value) {
			$xml->writeElement('description', $value, ['language' => $languageCode]);
		}
		
		$xml->writeElement('date', $this->styleDate);
		$xml->writeElement('version', $this->styleVersion);
		$xml->writeElement('apiVersion', $this->apiVersion);
		if ($this->image) $xml->writeElement('image', $this->image);
		if ($this->image2x) $xml->writeElement('image2x', $this->image2x);
		if ($coverPhoto) $xml->writeElement('coverPhoto', basename(FileUtil::unifyDirSeparator($coverPhoto)));
		if ($this->copyright) $xml->writeElement('copyright', $this->copyright);
		if ($this->license) $xml->writeElement('license', $this->license);
		$xml->endElement();
		
		// author block
		$xml->startElement('author');
		$xml->writeElement('authorname', $this->authorName);
		if ($this->authorURL) $xml->writeElement('authorurl', $this->authorURL);
		$xml->endElement();
		
		// files block
		$xml->startElement('files');
		$xml->writeElement('variables', 'variables.xml');
		if ($templates) $xml->writeElement('templates', 'templates.tar');
		if ($images) $xml->writeElement('images', 'images.tar', ['path' => $this->imagePath]);
		$xml->endElement();
		
		// append style info file to style tar
		$styleTar->addString(self::INFO_FILE, $xml->endDocument());
		unset($string);
		
		// create variable list
		$xml->beginDocument('variables', 'http://www.woltlab.com', 'http://www.woltlab.com/XSD/vortex/styleVariables.xsd');
		
		// get variables
		$sql = "SELECT		variable.variableName, value.variableValue
			FROM		wcf".WCF_N."_style_variable_value value
			LEFT JOIN	wcf".WCF_N."_style_variable variable
			ON		(variable.variableID = value.variableID)
			WHERE		value.styleID = ?";
		$statement = WCF::getDB()->prepareStatement($sql);
		$statement->execute([$this->styleID]);
		while ($row = $statement->fetchArray()) {
			$xml->writeElement('variable', $row['variableValue'], ['name' => $row['variableName']]);
		}
		
		// append variable list to style tar
		$styleTar->addString('variables.xml', $xml->endDocument());
		unset($string);
		
		if ($templates && $this->templateGroupID) {
			$templateGroup = new TemplateGroup($this->templateGroupID);
			
			// create templates tar
			$templatesTarName = FileUtil::getTemporaryFilename('templates', '.tar');
			$templatesTar = new TarWriter($templatesTarName);
			FileUtil::makeWritable($templatesTarName);
			
			// append templates to tar
			// get templates
			$sql = "SELECT		template.*, package.package
				FROM		wcf".WCF_N."_template template
				LEFT JOIN	wcf".WCF_N."_package package
				ON		(package.packageID = template.packageID)
				WHERE		template.templateGroupID = ?";
			$statement = WCF::getDB()->prepareStatement($sql);
			$statement->execute([$this->templateGroupID]);
			while ($row = $statement->fetchArray()) {
				$packageDir = 'com.woltlab.wcf';
				$package = null;
				
				if (Template::isSystemCritical($row['templateName'])) {
					continue;
				}
				
				if ($row['application'] != 'wcf') {
					$application = ApplicationHandler::getInstance()->getApplication($row['application']);
					$package = PackageCache::getInstance()->getPackage($application->packageID);
					$packageDir = $package->package;
				}
				else {
					$application = ApplicationHandler::getInstance()->getWCF();
					$package = PackageCache::getInstance()->getPackage($application->packageID);
				}
				
				$filename = FileUtil::addTrailingSlash(FileUtil::getRealPath(WCF_DIR . $package->packageDir . 'templates/' . $templateGroup->templateGroupFolderName)) . $row['templateName'] . '.tpl';
				$templatesTar->add($filename, $packageDir, dirname($filename));
			}
			
			// append templates tar to style tar
			$templatesTar->create();
			$styleTar->add($templatesTarName, 'templates.tar', $templatesTarName);
			@unlink($templatesTarName);
		}
		
		if ($images && ($this->imagePath && $this->imagePath != 'images/')) {
			// create images tar
			$imagesTarName = FileUtil::getTemporaryFilename('images_', '.tar');
			$imagesTar = new TarWriter($imagesTarName);
			FileUtil::makeWritable($imagesTarName);
			
			// append images to tar
			$path = FileUtil::addTrailingSlash(WCF_DIR.$this->imagePath);
			if (file_exists($path) && is_dir($path)) {
				$handle = opendir($path);
				
				$regEx = new Regex('\.(jpg|jpeg|gif|png|svg)$', Regex::CASE_INSENSITIVE);
				while (($file = readdir($handle)) !== false) {
					if (is_file($path.$file) && $regEx->match($file)) {
						$imagesTar->add($path.$file, '', $path);
					}
				}
			}
			
			// append images tar to style tar
			$imagesTar->create();
			$styleTar->add($imagesTarName, 'images.tar', $imagesTarName);
			@unlink($imagesTarName);
		}
		
		// output file content
		$styleTar->create();
		
		// export as style package
		if (empty($packageName)) {
			readfile($styleTarName);
		}
		else {
			// export as package
			
			// create package tar
			$packageTarName = FileUtil::getTemporaryFilename('package_', '.tar.gz');
			$packageTar = new TarWriter($packageTarName, true);
			
			// append style tar
			$styleTarName = FileUtil::unifyDirSeparator($styleTarName);
			$packageTar->add($styleTarName, '', FileUtil::addTrailingSlash(dirname($styleTarName)));
			
			// create package.xml
			$xml->beginDocument('package', 'http://www.woltlab.com', 'http://www.woltlab.com/XSD/vortex/package.xsd', ['name' => $packageName]);
			
			$xml->startElement('packageinformation');
			$xml->writeElement('packagename', $this->styleName);
			
			// description
			foreach ($styleDescriptions as $languageCode => $value) {
				$xml->writeElement('packagedescription', $value, ['language' => $languageCode]);
			}
			
			$xml->writeElement('version', $this->styleVersion);
			$xml->writeElement('date', $this->styleDate);
			$xml->endElement();
			
			$xml->startElement('authorinformation');
			$xml->writeElement('author', $this->authorName);
			if ($this->authorURL) $xml->writeElement('authorurl', $this->authorURL);
			$xml->endElement();
			
			$xml->startElement('requiredpackages');
			$xml->writeElement('requiredpackage', 'com.woltlab.wcf', ['minversion' => PackageCache::getInstance()->getPackageByIdentifier('com.woltlab.wcf')->packageVersion]);
			$xml->endElement();
			
			$xml->startElement('compatibility');
			$xml->writeElement('api', '', ['version' => 2018]);
			$xml->endElement();
			
			$xml->startElement('instructions', ['type' => 'install']);
			$xml->writeElement('instruction', basename($styleTarName), ['type' => 'style']);
			$xml->endElement();
			
			// append package info file to package tar
			$packageTar->addString(PackageArchive::INFO_FILE, $xml->endDocument());
			
			$packageTar->create();
			readfile($packageTarName);
			@unlink($packageTarName);
		}
		
		@unlink($styleTarName);
	}
	
	/**
	 * Sets the variables of a style.
	 * 
	 * @param	string[]		$variables<commit_msg>Use a static list to expose compatible API versions<commit_after><?php
namespace wcf\data\style;
use wcf\data\language\category\LanguageCategory;
use wcf\data\language\LanguageList;
use wcf\data\package\Package;
use wcf\data\package\PackageCache;
use wcf\data\template\group\TemplateGroup;
use wcf\data\template\group\TemplateGroupAction;
use wcf\data\template\Template;
use wcf\data\template\TemplateEditor;
use wcf\data\DatabaseObjectEditor;
use wcf\data\IEditableCachedObject;
use wcf\system\application\ApplicationHandler;
use wcf\system\cache\builder\StyleCacheBuilder;
use wcf\system\exception\SystemException;
use wcf\system\image\ImageHandler;
use wcf\system\io\Tar;
use wcf\system\io\TarWriter;
use wcf\system\language\LanguageFactory;
use wcf\system\package\PackageArchive;
use wcf\system\style\StyleCompiler;
use wcf\system\style\StyleHandler;
use wcf\system\Regex;
use wcf\system\WCF;
use wcf\util\DateUtil;
use wcf\util\FileUtil;
use wcf\util\StringUtil;
use wcf\util\XML;
use wcf\util\XMLWriter;

/**
 * Provides functions to edit, import, export and delete a style.
 * 
 * @author	<NAME>
 * @copyright	2001-2017 WoltLab GmbH
 * @license	GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 * @package	WoltLabSuite\Core\Data\Style
 * 
 * @method	Style	getDecoratedObject()
 * @mixin	Style
 */
class StyleEditor extends DatabaseObjectEditor implements IEditableCachedObject {
	/**
	 * @deprecated 3.1 use the compatibility api versions instead
	 */
	const EXCLUDE_WCF_VERSION = '3.2.0 Alpha 1';
	const INFO_FILE = 'style.xml';
	
	/**
	 * list of compatible API versions
	 * @var integer[]
	 */
	public static $compatibilityApiVersions = [2018];
	
	/**
	 * @inheritDoc
	 */
	protected static $baseClass = Style::class;
	
	/**
	 * @inheritDoc
	 */
	public function update(array $parameters = []) {
		$variables = null;
		if (isset($parameters['variables'])) {
			$variables = $parameters['variables'];
			unset($parameters['variables']);
		}
		
		// update style data
		parent::update($parameters);
		
		// update variables
		if ($variables !== null) {
			$this->setVariables($variables);
		}
		
		// scale preview image
		if (!empty($parameters['image']) && $parameters['image'] != $this->image) {
			self::scalePreviewImage($parameters['image']);
		}
	}
	
	/**
	 * @inheritDoc
	 */
	public function delete() {
		parent::delete();
		
		// delete variables
		$sql = "DELETE FROM	wcf".WCF_N."_style_variable_value
			WHERE		styleID = ?";
		$statement = WCF::getDB()->prepareStatement($sql);
		$statement->execute([$this->styleID]);
		
		// delete style files
		$files = @glob(WCF_DIR.'style/style-'.$this->styleID.'*.css');
		if (is_array($files)) {
			foreach ($files as $file) {
				@unlink($file);
			}
		}
		
		// delete preview image
		if ($this->image) {
			@unlink(WCF_DIR.'images/'.$this->image);
		}
		
		// delete language items
		$sql = "DELETE FROM	wcf".WCF_N."_language_item
			WHERE		languageItem = ?";
		$statement = WCF::getDB()->prepareStatement($sql);
		$statement->execute(['wcf.style.styleDescription'.$this->styleID]);
	}
	
	/**
	 * Sets this style as default style.
	 */
	public function setAsDefault() {
		// remove old default
		$sql = "UPDATE	wcf".WCF_N."_style
			SET	isDefault = ?
			WHERE	isDefault = ?";
		$statement = WCF::getDB()->prepareStatement($sql);
		$statement->execute([0, 1]);
		
		// set new default
		$this->update([
			'isDefault' => 1,
			'isDisabled' => 0
		]);
		
		self::resetCache();
	}
	
	/**
	 * Deletes the style's default cover photo.
	 */
	public function deleteCoverPhoto() {
		if ($this->coverPhotoExtension) {
			@unlink(WCF_DIR.'images/coverPhotos/'.$this->styleID.'.'.$this->coverPhotoExtension);
			
			$this->update([
				'coverPhotoExtension' => ''
			]);
		}
	}
	
	/**
	 * Reads the data of a style exchange format file.
	 * 
	 * @param	Tar	$tar
	 * @return	array
	 * @throws	SystemException
	 */
	public static function readStyleData(Tar $tar) {
		// search style.xml
		$index = $tar->getIndexByFilename(self::INFO_FILE);
		if ($index === false) {
			throw new SystemException("unable to find required file '".self::INFO_FILE."' in style archive");
		}
		
		// open style.xml
		$xml = new XML();
		$xml->loadXML(self::INFO_FILE, $tar->extractToString($index));
		$xpath = $xml->xpath();
		
		$data = [
			'name' => '', 'description' => [], 'version' => '', 'image' => '', 'image2x' => '', 'copyright' => '', 'default' => false,
			'license' => '', 'authorName' => '', 'authorURL' => '', 'templates' => '', 'images' => '', 'coverPhoto' => '',
			'variables' => '', 'date' => '0000-00-00', 'imagesPath' => '', 'packageName' => '', 'apiVersion' => '3.0'
		];
		
		$categories = $xpath->query('/ns:style/*');
		foreach ($categories as $category) {
			switch ($category->tagName) {
				case 'author':
					$elements = $xpath->query('child::*', $category);
					foreach ($elements as $element) {
						switch ($element->tagName) {
							case 'authorname':
								$data['authorName'] = $element->nodeValue;
							break;
							
							case 'authorurl':
								$data['authorURL'] = $element->nodeValue;
							break;
						}
					}
				break;
				
				case 'files':
					$elements = $xpath->query('child::*', $category);
					
					/** @var \DOMElement $element */
					foreach ($elements as $element) {
						$data[$element->tagName] = $element->nodeValue;
						if ($element->hasAttribute('path')) {
							$data[$element->tagName.'Path'] = $element->getAttribute('path');
						}
					}
				break;
				
				case 'general':
					$elements = $xpath->query('child::*', $category);
					foreach ($elements as $element) {
						switch ($element->tagName) {
							case 'date':
								DateUtil::validateDate($element->nodeValue);
								
								$data['date'] = $element->nodeValue;
							break;
							
							case 'default':
								$data['default'] = true;
							break;
							
							case 'description':
								if ($element->hasAttribute('language')) {
									$data['description'][$element->getAttribute('language')] = $element->nodeValue;
								}
							break;
							
							case 'stylename':
								$data['name'] = $element->nodeValue;
							break;
							
							case 'packageName':
								$data['packageName'] = $element->nodeValue;
							break;
							
							case 'version':
								if (!Package::isValidVersion($element->nodeValue)) {
									throw new SystemException("style version '".$element->nodeValue."' is invalid");
								}
								
								$data['version'] = $element->nodeValue;
							break;
							
							case 'copyright':
							case 'image':
							case 'image2x':
							case 'license':
							case 'coverPhoto':
								$data[$element->tagName] = $element->nodeValue;
							break;
							
							case 'apiVersion':
								if (!in_array($element->nodeValue, Style::$supportedApiVersions)) {
									throw new SystemException("Unknown api version '".$element->nodeValue."'");
								}
								
								$data['apiVersion'] = $element->nodeValue;
								break;
						}
					}
				break;
			}
		}
		
		if (empty($data['name'])) {
			throw new SystemException("required tag 'stylename' is missing in '".self::INFO_FILE."'");
		}
		if (empty($data['variables'])) {
			throw new SystemException("required tag 'variables' is missing in '".self::INFO_FILE."'");
		}
		
		// search variables.xml
		$index = $tar->getIndexByFilename($data['variables']);
		if ($index === false) {
			throw new SystemException("unable to find required file '".$data['variables']."' in style archive");
		}
		
		// open variables.xml
		$data['variables'] = self::readVariablesData($data['variables'], $tar->extractToString($index));
		
		return $data;
	}
	
	/**
	 * Reads the data of a variables.xml file.
	 * 
	 * @param	string		$filename
	 * @param	string		$content
	 * @return	array
	 */
	public static function readVariablesData($filename, $content) {
		// open variables.xml
		$xml = new XML();
		$xml->loadXML($filename, $content);
		$variables = $xml->xpath()->query('/ns:variables/ns:variable');
		
		$data = [];
		
		/** @var \DOMElement $variable */
		foreach ($variables as $variable) {
			$data[$variable->getAttribute('name')] = $variable->nodeValue;
		}
		
		return $data;
	}
	
	/**
	 * Returns the data of a style exchange format file.
	 * 
	 * @param	string		$filename
	 * @return	array
	 */
	public static function getStyleData($filename) {
		// open file
		$tar = new Tar($filename);
		
		// get style data
		$data = self::readStyleData($tar);
		
		// export preview image to temporary location
		if (!empty($data['image'])) {
			$i = $tar->getIndexByFilename($data['image']);
			if ($i !== false) {
				$path = FileUtil::getTemporaryFilename('stylePreview_', $data['image'], WCF_DIR.'tmp/');
				$data['image'] = basename($path);
				$tar->extract($i, $path);
			}
		}
		
		$tar->close();
		
		return $data;
	}
	
	/**
	 * Imports a style.
	 * 
	 * @param	string		$filename
	 * @param	integer		$packageID
	 * @param	StyleEditor	$style
	 * @return	StyleEditor
	 */
	public static function import($filename, $packageID = 1, StyleEditor $style = null) {
		// open file
		$tar = new Tar($filename);
		
		// get style data
		$data = self::readStyleData($tar);
		
		$styleData = [
			'styleName' => $data['name'],
			'variables' => $data['variables'],
			'styleVersion' => $data['version'],
			'styleDate' => $data['date'],
			'copyright' => $data['copyright'],
			'license' => $data['license'],
			'authorName' => $data['authorName'],
			'authorURL' => $data['authorURL'],
			'packageName' => $data['packageName'],
			'apiVersion' => $data['apiVersion']
		];
		
		// check if there is an untainted style with the same package name
		if ($style === null && !empty($styleData['packageName'])) {
			$style = StyleHandler::getInstance()->getStyleByName($styleData['packageName'], true);
		}
		
		// import images
		if (!empty($data['images']) && $data['imagesPath'] != 'images/') {
			// create images folder if necessary
			$imagesLocation = self::getFileLocation($data['imagesPath']);
			$styleData['imagePath'] = FileUtil::getRelativePath(WCF_DIR, $imagesLocation);
			
			$index = $tar->getIndexByFilename($data['images']);
			if ($index !== false) {
				// extract images tar
				$destination = FileUtil::getTemporaryFilename('images_');
				$tar->extract($index, $destination);
				
				// open images tar
				$imagesTar = new Tar($destination);
				$contentList = $imagesTar->getContentList();
				foreach ($contentList as $key => $val) {
					if ($val['type'] == 'file') {
						$imagesTar->extract($key, $imagesLocation.basename($val['filename']));
						FileUtil::makeWritable($imagesLocation.basename($val['filename']));
					}
				}
				
				// delete tmp file
				$imagesTar->close();
				@unlink($destination);
			}
		}
		
		// handle templates
		if (!empty($data['templates'])) {
			$templateGroupFolderName = '';
			if ($style !== null && $style->templateGroupID) {
				$templateGroupFolderName = (new TemplateGroup($style->templateGroupID))->templateGroupFolderName;
				$styleData['templateGroupID'] = $style->templateGroupID;
			}
			
			if (empty($templateGroupFolderName)) {
				// create template group
				$templateGroupName = $originalTemplateGroupName = $data['name'];
				$templateGroupFolderName = preg_replace('/[^a-z0-9_-]/i', '', $templateGroupName);
				if (empty($templateGroupFolderName)) $templateGroupFolderName = 'generic'.mb_substr(StringUtil::getRandomID(), 0, 8);
				$originalTemplateGroupFolderName = $templateGroupFolderName;
				
				// get unique template group name
				$i = 1;
				while (true) {
					$sql = "SELECT	COUNT(*)
						FROM	wcf".WCF_N."_template_group
						WHERE	templateGroupName = ?";
					$statement = WCF::getDB()->prepareStatement($sql);
					$statement->execute([$templateGroupName]);
					if (!$statement->fetchSingleColumn()) break;
					$templateGroupName = $originalTemplateGroupName . '_' . $i;
					$i++;
				}
				
				// get unique folder name
				$i = 1;
				while (true) {
					$sql = "SELECT	COUNT(*)
						FROM	wcf".WCF_N."_template_group
						WHERE	templateGroupFolderName = ?";
					$statement = WCF::getDB()->prepareStatement($sql);
					$statement->execute([
						FileUtil::addTrailingSlash($templateGroupFolderName)
					]);
					if (!$statement->fetchSingleColumn()) break;
					$templateGroupFolderName = $originalTemplateGroupFolderName . '_' . $i;
					$i++;
				}
				
				$templateGroupAction = new TemplateGroupAction([], 'create', [
					'data' => [
						'templateGroupName' => $templateGroupName,
						'templateGroupFolderName' => FileUtil::addTrailingSlash($templateGroupFolderName)
					]
				]);
				$returnValues = $templateGroupAction->executeAction();
				$styleData['templateGroupID'] = $returnValues['returnValues']->templateGroupID;
			}
			
			// import templates
			$index = $tar->getIndexByFilename($data['templates']);
			if ($index !== false) {
				// extract templates tar
				$destination = FileUtil::getTemporaryFilename('templates_');
				$tar->extract($index, $destination);
				
				// open templates tar and group templates by package
				$templatesTar = new Tar($destination);
				$contentList = $templatesTar->getContentList();
				$packageToTemplates = [];
				foreach ($contentList as $val) {
					if ($val['type'] == 'file') {
						$folders = explode('/', $val['filename']);
						$packageName = array_shift($folders);
						if (!isset($packageToTemplates[$packageName])) {
							$packageToTemplates[$packageName] = [];
						}
						$packageToTemplates[$packageName][] = ['index' => $val['index'], 'filename' => implode('/', $folders)];
					}
				}
				
				$knownTemplates = [];
				if ($style !== null && $style->templateGroupID) {
					$sql = "SELECT	templateName
						FROM	wcf".WCF_N."_template
						WHERE	templateGroupID = ?";
					$statement = WCF::getDB()->prepareStatement($sql);
					$statement->execute([$style->templateGroupID]);
					$knownTemplates = $statement->fetchAll(\PDO::FETCH_COLUMN);
				}
				
				// copy templates
				foreach ($packageToTemplates as $package => $templates) {
					// try to find package
					$sql = "SELECT	*
						FROM	wcf".WCF_N."_package
						WHERE	package = ?
							AND isApplication = ?";
					$statement = WCF::getDB()->prepareStatement($sql);
					$statement->execute([
						$package,
						1
					]);
					while ($row = $statement->fetchArray()) {
						// get template path
						$templatesDir = FileUtil::addTrailingSlash(FileUtil::getRealPath(WCF_DIR.$row['packageDir']).'templates/'.$templateGroupFolderName);
						
						// create template path
						if (!file_exists($templatesDir)) {
							@mkdir($templatesDir, 0777);
							FileUtil::makeWritable($templatesDir);
						}
						
						// copy templates
						foreach ($templates as $template) {
							$templatesTar->extract($template['index'], $templatesDir.$template['filename']);
							
							$templateName = str_replace('.tpl', '', $template['filename']);
							if (!in_array($templateName, $knownTemplates)) {
								TemplateEditor::create([
									'application' => Package::getAbbreviation($package),
									'packageID' => $row['packageID'],
									'templateName' => $templateName,
									'templateGroupID' => $styleData['templateGroupID']
								]);
							}
						}
					}
				}
				
				// delete tmp file
				$templatesTar->close();
				@unlink($destination);
			}
		}
		
		// save style
		if ($style === null) {
			$styleData['packageID'] = $packageID;
			$style = new StyleEditor(self::create($styleData));
			
			// handle descriptions
			if (!empty($data['description'])) {
				self::saveLocalizedDescriptions($style, $data['description']);
				LanguageFactory::getInstance()->deleteLanguageCache();
			}
			
			if ($data['default']) {
				$style->setAsDefault();
			}
		}
		else {
			unset($styleData['styleName']);
			
			$variables = $style->getVariables();
			if (!isset($styleData['variables']['individualScss'])) $styleData['variables']['individualScss'] = '';
			if (!isset($styleData['variables']['overrideScss'])) $styleData['variables']['overrideScss'] = '';
			
			$individualScss = Style::splitLessVariables($variables['individualScss']);
			$variables['individualScss'] = Style::joinLessVariables($styleData['variables']['individualScss'], $individualScss['custom']);
			
			$overrideScss = Style::splitLessVariables($variables['overrideScss']);
			$variables['overrideScss'] = Style::joinLessVariables($styleData['variables']['overrideScss'], $overrideScss['custom']);
			
			$styleData['variables'] = $variables;
			
			$style->update($styleData);
		}
		
		// import preview image
		foreach (['image', 'image2x'] as $type) {
			if (!empty($data[$type])) {
				$fileExtension = mb_substr($data[$type], mb_strrpos($data[$type], '.'));
				$index = $tar->getIndexByFilename($data[$type]);
				if ($index !== false) {
					$filename = WCF_DIR . 'images/stylePreview-' . $style->styleID . ($type === 'image2x' ? '@2x' : '') . $fileExtension;
					$tar->extract($index, $filename);
					FileUtil::makeWritable($filename);
					
					if (file_exists($filename)) {
						try {
							if (($imageData = getimagesize($filename)) !== false) {
								switch ($imageData[2]) {
									case IMAGETYPE_PNG:
									case IMAGETYPE_JPEG:
									case IMAGETYPE_GIF:
										$style->update([$type => 'stylePreview-' . $style->styleID . ($type === 'image2x' ? '@2x' : '') . $fileExtension]);
								}
							}
						}
						catch (SystemException $e) {
							// broken image
						}
					}
				}
			}
		}
		
		// import cover photo
		if (!empty($data['coverPhoto'])) {
			$fileExtension = mb_substr($data['coverPhoto'], mb_strrpos($data['coverPhoto'], '.'));
			$index = $tar->getIndexByFilename($data['coverPhoto']);
			if ($index !== false) {
				$filename = WCF_DIR . 'images/coverPhotos/' . $style->styleID . $fileExtension;
				$tar->extract($index, $filename);
				FileUtil::makeWritable($filename);
				
				if (file_exists($filename)) {
					try {
						if (($imageData = getimagesize($filename)) !== false) {
							switch ($imageData[2]) {
								case IMAGETYPE_PNG:
								case IMAGETYPE_JPEG:
								case IMAGETYPE_GIF:
									$style->update(['coverPhotoExtension' => mb_substr($fileExtension, 1)]);
							}
						}
					}
					catch (SystemException $e) {
						// broken image
					}
				}
			}
		}
		
		$tar->close();
		
		return $style;
	}
	
	/**
	 * Saves localized style descriptions.
	 * 
	 * @param	StyleEditor	$styleEditor
	 * @param	string[]	$descriptions
	 */
	protected static function saveLocalizedDescriptions(StyleEditor $styleEditor, array $descriptions) {
		// localize package information
		$sql = "REPLACE INTO	wcf".WCF_N."_language_item
					(languageID, languageItem, languageItemValue, languageCategoryID, packageID)
			VALUES		(?, ?, ?, ?, ?)";
		$statement = WCF::getDB()->prepareStatement($sql);
		
		// get language list
		$languageList = new LanguageList();
		$languageList->readObjects();
		
		// workaround for WCFSetup
		if (!PACKAGE_ID) {
			$sql = "SELECT	*
				FROM	wcf".WCF_N."_language_category
				WHERE	languageCategory = ?";
			$statement2 = WCF::getDB()->prepareStatement($sql);
			$statement2->execute(['wcf.style']);
			$languageCategory = $statement2->fetchObject(LanguageCategory::class);
		}
		else {
			$languageCategory = LanguageFactory::getInstance()->getCategory('wcf.style');
		}
		
		foreach ($languageList as $language) {
			if (isset($descriptions[$language->languageCode])) {
				$statement->execute([
					$language->languageID,
					'wcf.style.styleDescription'.$styleEditor->styleID,
					$descriptions[$language->languageCode],
					$languageCategory->languageCategoryID,
					$styleEditor->packageID
				]);
			}
		}
		
		$styleEditor->update([
			'styleDescription' => 'wcf.style.styleDescription'.$styleEditor->styleID
		]);
	}
	
	/**
	 * Returns available location path.
	 * 
	 * @param	string		$location
	 * @return	string
	 */
	protected static function getFileLocation($location) {
		$location = FileUtil::removeLeadingSlash(FileUtil::removeTrailingSlash($location));
		$location = WCF_DIR.$location;
		
		$index = null;
		do {
			$directory = $location . ($index === null ? '' : $index);
			if (!is_dir($directory)) {
				@mkdir($directory, 0777, true);
				FileUtil::makeWritable($directory);
				
				return FileUtil::addTrailingSlash($directory);
			}
			
			$index = ($index === null ? 2 : ($index + 1));
		}
		while (true);
		
		// this should never happen
		throw new \LogicException();
	}
	
	/**
	 * Exports this style.
	 * 
	 * @param	boolean		$templates
	 * @param	boolean		$images
	 * @param	string		$packageName
	 */
	public function export($templates = false, $images = false, $packageName = '') {
		// create style tar
		$styleTarName = FileUtil::getTemporaryFilename('style_', '.tgz');
		$styleTar = new TarWriter($styleTarName, true);
		
		// append style preview image
		if ($this->image && @file_exists(WCF_DIR.'images/'.$this->image)) {
			$styleTar->add(WCF_DIR.'images/'.$this->image, '', FileUtil::addTrailingSlash(dirname(WCF_DIR.'images/'.$this->image)));
		}
		if ($this->image2x && @file_exists(WCF_DIR.'images/'.$this->image2x)) {
			$styleTar->add(WCF_DIR.'images/'.$this->image2x, '', FileUtil::addTrailingSlash(dirname(WCF_DIR.'images/'.$this->image2x)));
		}
		
		// append cover photo
		$coverPhoto = ($this->coverPhotoExtension) ? WCF_DIR.'images/coverPhotos/'.$this->styleID.'.'.$this->coverPhotoExtension : '';
		if ($coverPhoto && @file_exists($coverPhoto)) {
			$styleTar->add($coverPhoto, '', FileUtil::addTrailingSlash(dirname($coverPhoto)));
		}
		
		// fetch style description
		$sql = "SELECT		language.languageCode, language_item.languageItemValue
			FROM		wcf".WCF_N."_language_item language_item
			LEFT JOIN	wcf".WCF_N."_language language
			ON		(language.languageID = language_item.languageID)
			WHERE		language_item.languageItem = ?";
		$statement = WCF::getDB()->prepareStatement($sql);
		$statement->execute([$this->styleDescription]);
		$styleDescriptions = $statement->fetchMap('languageCode', 'languageItemValue');
		
		// create style info file
		$xml = new XMLWriter();
		$xml->beginDocument('style', 'http://www.woltlab.com', 'http://www.woltlab.com/XSD/vortex/style.xsd');
		
		// general block
		$xml->startElement('general');
		$xml->writeElement('stylename', $this->styleName);
		$xml->writeElement('packageName', $this->packageName);
		
		// style description
		foreach ($styleDescriptions as $languageCode => $value) {
			$xml->writeElement('description', $value, ['language' => $languageCode]);
		}
		
		$xml->writeElement('date', $this->styleDate);
		$xml->writeElement('version', $this->styleVersion);
		$xml->writeElement('apiVersion', $this->apiVersion);
		if ($this->image) $xml->writeElement('image', $this->image);
		if ($this->image2x) $xml->writeElement('image2x', $this->image2x);
		if ($coverPhoto) $xml->writeElement('coverPhoto', basename(FileUtil::unifyDirSeparator($coverPhoto)));
		if ($this->copyright) $xml->writeElement('copyright', $this->copyright);
		if ($this->license) $xml->writeElement('license', $this->license);
		$xml->endElement();
		
		// author block
		$xml->startElement('author');
		$xml->writeElement('authorname', $this->authorName);
		if ($this->authorURL) $xml->writeElement('authorurl', $this->authorURL);
		$xml->endElement();
		
		// files block
		$xml->startElement('files');
		$xml->writeElement('variables', 'variables.xml');
		if ($templates) $xml->writeElement('templates', 'templates.tar');
		if ($images) $xml->writeElement('images', 'images.tar', ['path' => $this->imagePath]);
		$xml->endElement();
		
		// append style info file to style tar
		$styleTar->addString(self::INFO_FILE, $xml->endDocument());
		unset($string);
		
		// create variable list
		$xml->beginDocument('variables', 'http://www.woltlab.com', 'http://www.woltlab.com/XSD/vortex/styleVariables.xsd');
		
		// get variables
		$sql = "SELECT		variable.variableName, value.variableValue
			FROM		wcf".WCF_N."_style_variable_value value
			LEFT JOIN	wcf".WCF_N."_style_variable variable
			ON		(variable.variableID = value.variableID)
			WHERE		value.styleID = ?";
		$statement = WCF::getDB()->prepareStatement($sql);
		$statement->execute([$this->styleID]);
		while ($row = $statement->fetchArray()) {
			$xml->writeElement('variable', $row['variableValue'], ['name' => $row['variableName']]);
		}
		
		// append variable list to style tar
		$styleTar->addString('variables.xml', $xml->endDocument());
		unset($string);
		
		if ($templates && $this->templateGroupID) {
			$templateGroup = new TemplateGroup($this->templateGroupID);
			
			// create templates tar
			$templatesTarName = FileUtil::getTemporaryFilename('templates', '.tar');
			$templatesTar = new TarWriter($templatesTarName);
			FileUtil::makeWritable($templatesTarName);
			
			// append templates to tar
			// get templates
			$sql = "SELECT		template.*, package.package
				FROM		wcf".WCF_N."_template template
				LEFT JOIN	wcf".WCF_N."_package package
				ON		(package.packageID = template.packageID)
				WHERE		template.templateGroupID = ?";
			$statement = WCF::getDB()->prepareStatement($sql);
			$statement->execute([$this->templateGroupID]);
			while ($row = $statement->fetchArray()) {
				$packageDir = 'com.woltlab.wcf';
				$package = null;
				
				if (Template::isSystemCritical($row['templateName'])) {
					continue;
				}
				
				if ($row['application'] != 'wcf') {
					$application = ApplicationHandler::getInstance()->getApplication($row['application']);
					$package = PackageCache::getInstance()->getPackage($application->packageID);
					$packageDir = $package->package;
				}
				else {
					$application = ApplicationHandler::getInstance()->getWCF();
					$package = PackageCache::getInstance()->getPackage($application->packageID);
				}
				
				$filename = FileUtil::addTrailingSlash(FileUtil::getRealPath(WCF_DIR . $package->packageDir . 'templates/' . $templateGroup->templateGroupFolderName)) . $row['templateName'] . '.tpl';
				$templatesTar->add($filename, $packageDir, dirname($filename));
			}
			
			// append templates tar to style tar
			$templatesTar->create();
			$styleTar->add($templatesTarName, 'templates.tar', $templatesTarName);
			@unlink($templatesTarName);
		}
		
		if ($images && ($this->imagePath && $this->imagePath != 'images/')) {
			// create images tar
			$imagesTarName = FileUtil::getTemporaryFilename('images_', '.tar');
			$imagesTar = new TarWriter($imagesTarName);
			FileUtil::makeWritable($imagesTarName);
			
			// append images to tar
			$path = FileUtil::addTrailingSlash(WCF_DIR.$this->imagePath);
			if (file_exists($path) && is_dir($path)) {
				$handle = opendir($path);
				
				$regEx = new Regex('\.(jpg|jpeg|gif|png|svg)$', Regex::CASE_INSENSITIVE);
				while (($file = readdir($handle)) !== false) {
					if (is_file($path.$file) && $regEx->match($file)) {
						$imagesTar->add($path.$file, '', $path);
					}
				}
			}
			
			// append images tar to style tar
			$imagesTar->create();
			$styleTar->add($imagesTarName, 'images.tar', $imagesTarName);
			@unlink($imagesTarName);
		}
		
		// output file content
		$styleTar->create();
		
		// export as style package
		if (empty($packageName)) {
			readfile($styleTarName);
		}
		else {
			// export as package
			
			// create package tar
			$packageTarName = FileUtil::getTemporaryFilename('package_', '.tar.gz');
			$packageTar = new TarWriter($packageTarName, true);
			
			// append style tar
			$styleTarName = FileUtil::unifyDirSeparator($styleTarName);
			$packageTar->add($styleTarName, '', FileUtil::addTrailingSlash(dirname($styleTarName)));
			
			// create package.xml
			$xml->beginDocument('package', 'http://www.woltlab.com', 'http://www.woltlab.com/XSD/vortex/package.xsd', ['name' => $packageName]);
			
			$xml->startElement('packageinformation');
			$xml->writeElement('packagename', $this->styleName);
			
			// description
			foreach ($styleDescriptions as $languageCode => $value) {
				$xml->writeElement('packagedescription', $value, ['language' => $languageCode]);
			}
			
			$xml->writeElement('version', $this->styleVersion);
			$xml->writeElement('date', $this->styleDate);
			$xml->endElement();
			
			$xml->startElement('authorinformation');
			$xml->writeElement('author', $this->authorName);
			if ($this->authorURL) $xml->writeElement('authorurl', $this->authorURL);
			$xml->endElement();
			
			$xml->startElement('requiredpackages');
			$xml->writeElement('requiredpackage', 'com.woltlab.wcf', ['minversion' => PackageCache::getInstance()->getPackageByIdentifier('com.woltlab.wcf')->packageVersion]);
			$xml->endElement();
			
			$xml->startElement('compatibility');
			foreach (self::$compatibilityApiVersions as $apiVersion) {
				$xml->writeElement('api', '', ['version' => $apiVersion]);
			}
			$xml->endElement();
			
			$xml->startElement('instructions', ['type' => 'install']);
			$xml->writeElement('instruction', basename($styleTarName), ['type' => 'style']);
			$xml->endElement();
			
			// append package info file to package tar
			$packageTar->addString(PackageArchive::INFO_FILE, $xml->endDocument());
			
			$packageTar->create();
			readfile($packageTarName);
			@unlink($packageTarName);
		}
		
		@unlink($styleTarName);
	}
	
	/**
	 * Sets the variables of a style.
	 * 
	 * @param	string[]		$variables
{'fraction_non_alphanumeric': 0.17704940514965542, 'fraction_numerical': 0.0023656872492885794, 'mean_word_length': 3.612556337471337, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 613, 'https://': 0, 'lorem ipsum': 0, 'www.': 12, 'xml': 130}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7057543', 'n_tokens_mistral': 22611, 'n_tokens_neox': 17826, 'n_words': 3324}