Merge pull request #4157 from WoltLab/attachment-lazy
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / data / style / Style.class.php
CommitLineData
158bd3ca 1<?php
a9229942 2
158bd3ca 3namespace wcf\data\style;
a9229942 4
158bd3ca 5use wcf\data\DatabaseObject;
e9dfcb4b 6use wcf\system\application\ApplicationHandler;
158bd3ca 7use wcf\system\WCF;
d261146e 8use wcf\util\FileUtil;
d4cf0997 9use wcf\util\ImageUtil;
158bd3ca
TD
10
11/**
12 * Represents a style.
e9335ed9 13 *
a9229942
TD
14 * @author Marcel Werk
15 * @copyright 2001-2019 WoltLab GmbH
16 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
17 * @package WoltLabSuite\Core\Data\Style
18 *
19 * @property-read int $styleID unique id of the style
20 * @property-read int $packageID id of the package which delivers the style
21 * @property-read string $styleName name of style
22 * @property-read int $templateGroupID id of the template group used for the style or `0` if the style uses no specific template group
23 * @property-read int $isDefault is `1` if the style is the default style for guests and users, otherwise `0`
24 * @property-read int $isDisabled is `1` if the style is disabled and thus cannot be used without having the specific permission to do so, otherwise `0`
25 * @property-read string $styleDescription description of the style or name of the language item which contains the description
26 * @property-read string $styleVersion version number of the style
27 * @property-read string $styleDate date when the used version of the style has been published
28 * @property-read string $image link or path (relative to `WCF_DIR`) to the preview image of the style
29 * @property-read string $image2x link or path (relative to `WCF_DIR`) to the preview image of the style (2x version)
30 * @property-read string $copyright copyright text of the style
31 * @property-read string $license name of the style's license
32 * @property-read string $authorName name(s) of the style's author(s)
33 * @property-read string $authorURL link to the author's website
34 * @property-read string $imagePath path (relative to `WCF_DIR`) to the images used by the style or empty if style has no special image path
35 * @property-read string $packageName package identifier used to export the style as a package or empty (thus style cannot be exported as package)
36 * @property-read int $isTainted is `0` if the original declarations of an imported or installed style are not and cannot be altered, otherwise `1`
37 * @property-read int $hasFavicon is `0` if the default favicon data should be used
38 * @property-read int $coverPhotoExtension extension of the style's cover photo file
39 * @property-read string $apiVersion the style's compatibility version, possible values: '3.0' or '3.1'
158bd3ca 40 */
a9229942
TD
41class Style extends DatabaseObject
42{
43 /**
44 * list of style variables
45 * @var string[]
46 */
47 protected $variables = [];
48
49 /**
50 * list of supported API versions
51 * @var string[]
52 */
53 public static $supportedApiVersions = ['3.0', '3.1', '5.2'];
54
55 const API_VERSION = '5.2';
56
57 const PREVIEW_IMAGE_MAX_HEIGHT = 64;
58
59 const PREVIEW_IMAGE_MAX_WIDTH = 102;
60
61 const FAVICON_IMAGE_HEIGHT = 256;
62
63 const FAVICON_IMAGE_WIDTH = 256;
64
65 const BASE_ASSET_PATH = WCF_DIR . 'images/';
66
67 /**
68 * Returns the name of this style.
69 *
70 * @return string
71 */
72 public function __toString()
73 {
74 return $this->styleName;
75 }
76
77 /**
78 * Returns the absolute path to the style's asset folder.
79 *
80 * @return string
81 * @since 5.3
82 */
83 public function getAssetPath()
84 {
85 return FileUtil::addTrailingSlash(static::BASE_ASSET_PATH . 'style-' . $this->styleID);
86 }
87
88 /**
89 * Returns the styles variables of this style.
90 *
91 * @return string[]
92 */
93 public function getVariables()
94 {
95 $this->loadVariables();
96
97 return $this->variables;
98 }
99
100 /**
101 * Returns a specific style variable or null if not found.
102 * If $toHex is set to true the color defined by the variable
103 * will be converted to the hexadecimal notation (e.g. for use
104 * in emails)
105 *
106 * @param string $variableName
107 * @param bool $toHex
108 * @return string
109 */
110 public function getVariable($variableName, $toHex = false)
111 {
112 if (isset($this->variables[$variableName])) {
113 // check if variable is empty
114 if ($this->variables[$variableName] == '~""') {
115 return '';
116 }
117
118 if (
119 $toHex && \preg_match(
120 '/^rgba\((\d+), (\d+), (\d+), (1|0?\.\d+)\)$/',
121 $this->variables[$variableName],
122 $matches
123 )
124 ) {
125 $r = $matches[1];
126 $g = $matches[2];
127 $b = $matches[3];
128 $a = \floatval($matches[4]);
129
130 // calculate alpha value assuming a white canvas, source rgb will be (255,255,255) or #fff
131 // see https://stackoverflow.com/a/2049362
132 if ($a < 1) {
133 $r = ((1 - $a) * 255) + ($a * $r);
134 $g = ((1 - $a) * 255) + ($a * $g);
135 $b = ((1 - $a) * 255) + ($a * $b);
136
137 $clamp = static function ($v) {
138 return \max(0, \min(255, \intval($v)));
139 };
140
141 $r = $clamp($r);
142 $g = $clamp($g);
143 $b = $clamp($b);
144 }
145
146 return \sprintf('#%02x%02x%02x', $r, $g, $b);
147 }
148
149 return $this->variables[$variableName];
150 }
151 }
152
153 /**
154 * Loads style-specific variables.
155 */
156 public function loadVariables()
157 {
158 if (!empty($this->variables)) {
159 return;
160 }
161
162 $sql = "SELECT variable.variableName, variable.defaultValue, value.variableValue
163 FROM wcf" . WCF_N . "_style_variable variable
164 LEFT JOIN wcf" . WCF_N . "_style_variable_value value
c240c98a
MS
165 ON value.variableID = variable.variableID
166 AND value.styleID = ?
a9229942
TD
167 ORDER BY variable.variableID ASC";
168 $statement = WCF::getDB()->prepareStatement($sql);
169 $statement->execute([$this->styleID]);
170 while ($row = $statement->fetchArray()) {
171 $variableName = $row['variableName'];
172 $variableValue = $row['variableValue'] ?? $row['defaultValue'];
173
174 $this->variables[$variableName] = $variableValue;
175 }
176
177 // see https://github.com/WoltLab/WCF/issues/2636
178 if (empty($this->variables['wcfPageThemeColor'])) {
179 $this->variables['wcfPageThemeColor'] = $this->variables['wcfHeaderBackground'];
180 }
181 }
182
183 /**
184 * Returns the style preview image path.
185 *
186 * @return string
187 */
188 public function getPreviewImage()
189 {
190 if ($this->image && \file_exists(WCF_DIR . 'images/' . $this->image)) {
191 return WCF::getPath() . 'images/' . $this->image;
192 }
193
194 return WCF::getPath() . 'images/stylePreview.png';
195 }
196
197 /**
198 * Returns the style preview image path (2x version).
199 *
200 * @return string
201 */
202 public function getPreviewImage2x()
203 {
204 if ($this->image2x && \file_exists(WCF_DIR . 'images/' . $this->image2x)) {
205 return WCF::getPath() . 'images/' . $this->image2x;
206 }
207
208 return WCF::getPath() . 'images/stylePreview@2x.png';
209 }
210
211 /**
212 * Returns the absolute path to the apple touch icon.
213 *
214 * @return string
215 */
216 public function getFaviconAppleTouchIcon()
217 {
218 return $this->getFaviconPath('apple-touch-icon.png');
219 }
220
221 /**
222 * Returns the absolute path to the `manifest.json` file.
223 *
224 * @return string
225 */
226 public function getFaviconManifest()
227 {
228 return $this->getFaviconPath('manifest.json');
229 }
230
231 /**
232 * Returns the absolute path to the `browserconfig.xml` file.
233 *
234 * @return string
235 */
236 public function getFaviconBrowserconfig()
237 {
238 return $this->getFaviconPath('browserconfig.xml');
239 }
240
241 /**
242 * Returns the relative path to the favicon.
243 *
244 * @return string
245 */
246 public function getRelativeFavicon()
247 {
248 return $this->getFaviconPath('favicon.ico', false);
249 }
250
251 /**
252 * Returns the cover photo filename.
a9229942
TD
253 * @since 3.1
254 */
d4cf0997 255 public function getCoverPhoto(?bool $forceWebP = null): string
a9229942 256 {
d4cf0997
AE
257 $useWebP = $this->useWebP($forceWebP);
258
a9229942 259 if ($this->coverPhotoExtension) {
d4cf0997 260 return 'coverPhoto.' . ($useWebP ? 'webp' : $this->coverPhotoExtension);
a9229942
TD
261 }
262
d4cf0997 263 return 'default.' . ($useWebP ? 'webp' : 'jpg');
a9229942
TD
264 }
265
266 /**
a9229942
TD
267 * @since 5.2
268 */
d4cf0997 269 public function getCoverPhotoLocation(?bool $forceWebP = null): string
a9229942 270 {
d4cf0997
AE
271 $useWebP = $this->useWebP($forceWebP);
272
a9229942 273 if ($this->coverPhotoExtension) {
d4cf0997 274 return $this->getAssetPath() . 'coverPhoto.' . ($useWebP ? 'webp' : $this->coverPhotoExtension);
a9229942
TD
275 }
276
d4cf0997 277 return WCF_DIR . 'images/coverPhotos/default.' . ($useWebP ? 'webp' : 'jpg');
a9229942
TD
278 }
279
280 /**
a9229942
TD
281 * @since 5.2
282 */
d4cf0997 283 public function getCoverPhotoUrl(?bool $forceWebP = null): string
a9229942 284 {
d4cf0997
AE
285 $useWebP = $this->useWebP($forceWebP);
286
a9229942
TD
287 if ($this->coverPhotoExtension) {
288 return WCF::getPath() . FileUtil::getRelativePath(
289 WCF_DIR,
290 $this->getAssetPath()
d4cf0997 291 ) . 'coverPhoto.' . ($useWebP ? 'webp' : $this->coverPhotoExtension);
a9229942
TD
292 }
293
294 return WCF::getPath() . 'images/coverPhotos/' . $this->getCoverPhoto();
295 }
296
d4cf0997
AE
297 /**
298 * Serve the WebP variant of the cover photo if the browser supports
299 * it and the original cover photo is not a GIF.
4cc0a9b2 300 *
d4cf0997
AE
301 * @since 5.4
302 */
303 protected function useWebP($forceWebP = null): bool
304 {
305 if ($this->coverPhotoExtension === "gif") {
306 return false;
307 }
308
309 return $forceWebP || ($forceWebP === null && ImageUtil::browserSupportsWebP());
310 }
311
a9229942
TD
312 /**
313 * Returns the path to a favicon-related file.
314 *
315 * @param string $filename name of the file
316 * @param bool $absolutePath if `true`, the absolute path is returned, otherwise the path relative to WCF is returned
317 * @return string
318 */
319 protected function getFaviconPath($filename, $absolutePath = true)
320 {
321 if ($filename === 'manifest.json') {
322 if (ApplicationHandler::getInstance()->getActiveApplication()->domainName !== ApplicationHandler::getInstance()->getApplicationByID(1)->domainName) {
323 return WCF::getPath() . 'images/favicon/corsProxy.php?type=manifest' . ($this->hasFavicon ? '&amp;styleID=' . $this->styleID : '');
324 }
325 }
326
327 if ($this->hasFavicon) {
328 $path = FileUtil::getRelativePath(WCF_DIR, $this->getAssetPath()) . $filename;
329 } else {
330 $path = 'images/favicon/default.' . $filename;
331 }
332
333 if ($absolutePath) {
334 return WCF::getPath() . $path;
335 }
336
337 return $path;
338 }
339
340 /**
341 * Splits the less variables string.
342 *
343 * @param string $variables
344 * @return array
345 * @since 3.0
346 */
347 public static function splitLessVariables($variables)
348 {
349 $tmp = \explode("/* WCF_STYLE_CUSTOM_USER_MODIFICATIONS */\n", $variables, 2);
350
351 return [
352 'preset' => $tmp[0],
353 'custom' => $tmp[1] ?? '',
354 ];
355 }
356
357 /**
358 * Joins the less variables.
359 *
360 * @param string $preset
361 * @param string $custom
362 * @return string
363 * @since 3.0
364 */
365 public static function joinLessVariables($preset, $custom)
366 {
367 if (empty($custom)) {
368 return $preset;
369 }
370
371 return $preset . "/* WCF_STYLE_CUSTOM_USER_MODIFICATIONS */\n" . $custom;
372 }
dcb3a44c 373}