Implemented support for custom favicons
authorAlexander Ebert <ebert@woltlab.com>
Mon, 12 Jun 2017 12:55:46 +0000 (14:55 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Mon, 12 Jun 2017 12:56:55 +0000 (14:56 +0200)
See #2257

com.woltlab.wcf/templates/headInclude.tpl
wcfsetup/install/files/acp/style/layout.scss
wcfsetup/install/files/acp/templates/styleAdd.tpl
wcfsetup/install/files/images/favicon/default.browserconfig.xml
wcfsetup/install/files/images/favicon/default.manifest.json
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Style/Favicon/Upload.js [new file with mode: 0644]
wcfsetup/install/files/lib/data/style/ActiveStyle.class.php
wcfsetup/install/files/lib/data/style/Style.class.php
wcfsetup/install/files/lib/data/style/StyleAction.class.php
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index f08fbf457f4d4455e87b61f8e3618a117433cc38..aaf222d9494f5f1ff381106bf825ab008efced72 100644 (file)
@@ -32,7 +32,7 @@
 <link rel="manifest" href="{@$__wcf->getStyleHandler()->getStyle()->getFaviconManifest()}">
 <link rel="shortcut icon" href="{@$__wcf->getFavicon()}">
 <meta name="msapplication-config" content="{@$__wcf->getStyleHandler()->getStyle()->getFaviconBrowserconfig()}">
-<meta name="theme-color" content="{$__wcf->getStyleHandler()->getStyle()->getVariable('wcfHeaderBackground')}">
+<meta name="theme-color" content="{$__wcf->getStyleHandler()->getStyle()->getVariable('wcfHeaderBackground', true)}">
 
 <script data-relocate="true">
        $(function() {
index 49bab44d2d25addc1e2c2509701ecc79489f4f4f..7c2e75a8060088601c45470dd7dfca64afd4e8fc 100644 (file)
@@ -365,7 +365,8 @@ $wcfAcpSubMenuWidth: 300px;
        min-width: 20px;
 }
 
-.selectedImagePreview {
+.selectedImagePreview,
+.selectedFaviconPreview {
        img {
                margin-bottom: 5px;
        }
index 72340b90e523c51eb84f5cb4abfadd872596f3a2..bad4b98dfe1f79ba426e2be4e8fc5de8d3b69418 100644 (file)
@@ -5,13 +5,14 @@
 {js application='wcf' acp='true' file='WCF.ACP.Style'}
 {js application='wcf' file='WCF.ColorPicker' bundle='WCF.Combined'}
 <script data-relocate="true">
-       require(['WoltLabSuite/Core/Acp/Ui/Style/Image/Upload', 'WoltLabSuite/Core/Acp/Ui/Style/Editor', 'WoltLabSuite/Core/Ui/Toggle/Input'], function(AcpUiStyleImageUpload, AcpUiStyleEditor, UiToggleInput) {
+       require(['WoltLabSuite/Core/Acp/Ui/Style/Favicon/Upload', 'WoltLabSuite/Core/Acp/Ui/Style/Image/Upload', 'WoltLabSuite/Core/Acp/Ui/Style/Editor', 'WoltLabSuite/Core/Ui/Toggle/Input'], function(AcpUiStyleFaviconUpload, AcpUiStyleImageUpload, AcpUiStyleEditor, UiToggleInput) {
                AcpUiStyleEditor.setup({
                        isTainted: {if $isTainted}true{else}false{/if},
                        styleId: {if $action === 'edit'}{@$style->styleID}{else}0{/if},
                        styleRuleMap: styleRuleMap
                });
                
+               {if $action == 'edit'}new AcpUiStyleFaviconUpload({@$style->styleID});{/if}
                new AcpUiStyleImageUpload({if $action == 'add'}0{else}{@$style->styleID}{/if}, '{$tmpHash}');
                
                new UiToggleInput('input[name="useGoogleFont"]', {
@@ -27,6 +28,8 @@
                        'wcf.style.colorPicker.new': '{lang}wcf.style.colorPicker.new{/lang}',
                        'wcf.style.colorPicker.current': '{lang}wcf.style.colorPicker.current{/lang}',
                        'wcf.style.colorPicker.button.apply': '{lang}wcf.style.colorPicker.button.apply{/lang}',
+                       'wcf.acp.style.favicon.error.dimensions': '{lang}wcf.acp.style.favicon.error.dimensions{/lang}',
+                       'wcf.acp.style.favicon.error.invalidExtension': '{lang}wcf.acp.style.favicon.error.invalidExtension{/lang}',
                        'wcf.acp.style.image.error.invalidExtension': '{lang}wcf.acp.style.image.error.invalidExtension{/lang}'
                });
                new WCF.ACP.Style.LogoUpload('{$tmpHash}', '{@$__wcf->getPath()}images/');
                                {event name='fileFields'}
                        </section>
                        
+                       {if $action == 'edit'}
+                               <section class="section">
+                                       <h2 class="sectionTitle">{lang}wcf.acp.style.general.favicon{/lang}</h2>
+                                       
+                                       <dl>
+                                               <dt><label for="favicon">{lang}wcf.acp.style.favicon{/lang}</label></dt>
+                                               <dd>
+                                                       <div class="selectedFaviconPreview">
+                                                               <img src="{if $action == 'add'}{@$__wcf->getPath()}images/favicon/default.apple-touch-icon.png{else}{@$style->getFaviconAppleTouchIcon()}{/if}" alt="" id="faviconImage" style="height: 32px; width: 32px;">
+                                                       </div>
+                                                       <div id="uploadFavicon"></div>
+                                                       <small>{lang}wcf.acp.style.favicon.description{/lang}</small>
+                                               </dd>
+                                       </dl>
+                                       
+                                       {event name='faviconFields'}
+                               </section>
+                       {/if}
+                       
                        {event name='generalFieldsets'}
                </div>
                
index 5c4d1e29d114144000b029b09b3d4ee81312c448..046422076e2b8866abe70bc92aa836b073256045 100644 (file)
@@ -2,7 +2,7 @@
 <browserconfig>
     <msapplication>
         <tile>
-            <square150x150logo src="/abc/test/mstile-150x150.png"/>
+            <square150x150logo src="default.mstile-150x150.png"/>
             <TileColor>#3a6d9c</TileColor>
         </tile>
     </msapplication>
index 8380019f915ca1dcf8aaf3faf9cbc7bf919b79a0..fb439082c0a7d5f8e4bfc56f9d039592671d4377 100644 (file)
@@ -2,12 +2,12 @@
     "name": "",
     "icons": [
         {
-            "src": "/abc/test/android-chrome-192x192.png",
+            "src": "default.android-chrome-192x192.png",
             "sizes": "192x192",
             "type": "image/png"
         },
         {
-            "src": "/abc/test/android-chrome-256x256.png",
+            "src": "default.android-chrome-256x256.png",
             "sizes": "256x256",
             "type": "image/png"
         }
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Style/Favicon/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Style/Favicon/Upload.js
new file mode 100644 (file)
index 0000000..ddf66eb
--- /dev/null
@@ -0,0 +1,68 @@
+/**
+ * Handles uploading the style favicon.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Acp/Ui/Style/Favicon/Upload
+ */
+define(['Core', 'Dom/Traverse', 'Language', 'Ui/Notification', 'Upload'], function(Core, DomTraverse, Language, UiNotification, Upload) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function AcpUiStyleImageUpload(styleId) {
+               this._styleId = ~~styleId;
+               
+               Upload.call(this, 'uploadFavicon', 'faviconImage', {
+                       action: 'uploadFavicon',
+                       className: 'wcf\\data\\style\\StyleAction'
+               });
+       }
+       Core.inherit(AcpUiStyleImageUpload, Upload, {
+               /**
+                * @see WoltLabSuite/Core/Upload#_createFileElement
+                */
+               _createFileElement: function(file) {
+                       return this._target;
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Upload#_getParameters
+                */
+               _getParameters: function() {
+                       return {
+                               styleID: this._styleId
+                       };
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Upload#_success
+                */
+               _success: function(uploadId, data) {
+                       var error = DomTraverse.childByClass(this._button.parentNode, 'innerError');
+                       if (data.returnValues.url) {
+                               elAttr(this._target, 'src', data.returnValues.url + '?timestamp=' + Date.now());
+                               
+                               if (error) {
+                                       elRemove(error);
+                               }
+                               
+                               UiNotification.show();
+                       }
+                       else if (data.returnValues.errorType) {
+                               if (!error) {
+                                       error = elCreate('small');
+                                       error.className = 'innerError';
+                                       
+                                       this._button.parentNode.appendChild(error);
+                               }
+                               
+                               error.textContent = Language.get('wcf.acp.style.favicon.error.' + data.returnValues.errorType);
+                       }
+               }
+       });
+       
+       return AcpUiStyleImageUpload;
+});
index 1fafba7b8f58bf37d5aa32dfe3ec7b446bd2f770..77f61f33f349626ea7f811b3ea75d01b48137bb5 100644 (file)
@@ -69,29 +69,4 @@ class ActiveStyle extends DatabaseObjectDecorator {
                
                return WCF::getPath() . 'images/default-logo-small.png';
        }
-       
-       public function getFaviconAppleTouchIcon() {
-               return $this->getFaviconPath('apple-touch-icon.png');
-       }
-       
-       public function getFaviconManifest() {
-               return $this->getFaviconPath('manifest.json');
-       }
-       
-       public function getFaviconBrowserconfig() {
-               return $this->getFaviconPath('browserconfig.xml');
-       }
-       
-       public function getRelativeFavicon() {
-               return $this->getFaviconPath('favicon.ico', false);
-       }
-       
-       protected function getFaviconPath($filename, $absolutePath = true) {
-               $path = 'images/favicon/'. ($this->getDecoratedObject()->hasFavicon ? $this->getDecoratedObject()->styleID : 'default') . ".{$filename}";
-               if ($absolutePath) {
-                       return WCF::getPath() . $path;
-               }
-               
-               return $path;
-       }
 }
index 6b35f624074d260de9dcd770a241416043932ee8..a2870f8cf2ba440d4a980c2a877d454dd5ad297e 100644 (file)
@@ -40,6 +40,9 @@ class Style extends DatabaseObject {
        const PREVIEW_IMAGE_MAX_HEIGHT = 64;
        const PREVIEW_IMAGE_MAX_WIDTH = 102;
        
+       const FAVICON_IMAGE_HEIGHT = 256;
+       const FAVICON_IMAGE_WIDTH = 256;
+       
        /**
         * Returns the name of this style.
         * 
@@ -123,6 +126,31 @@ class Style extends DatabaseObject {
                return WCF::getPath().'images/stylePreview.png';
        }
        
+       public function getFaviconAppleTouchIcon() {
+               return $this->getFaviconPath('apple-touch-icon.png');
+       }
+       
+       public function getFaviconManifest() {
+               return $this->getFaviconPath('manifest.json');
+       }
+       
+       public function getFaviconBrowserconfig() {
+               return $this->getFaviconPath('browserconfig.xml');
+       }
+       
+       public function getRelativeFavicon() {
+               return $this->getFaviconPath('favicon.ico', false);
+       }
+       
+       protected function getFaviconPath($filename, $absolutePath = true) {
+               $path = 'images/favicon/'. ($this->hasFavicon ? $this->styleID : 'default') . ".{$filename}";
+               if ($absolutePath) {
+                       return WCF::getPath() . $path;
+               }
+               
+               return $path;
+       }
+       
        /**
         * Splits the less variables string.
         * 
index 8716e8b46d7cc75f0e12fecf5c9bc31687b86758..b10b944aa2360b0d143d2bf532b03319fdb4bdd0 100644 (file)
@@ -98,6 +98,9 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction,
                        // handle style preview image
                        $this->updateStylePreviewImage($style->getDecoratedObject());
                        
+                       // create favicon data
+                       $this->updateFavicons($style->getDecoratedObject());
+                       
                        // reset stylesheet
                        StyleHandler::getInstance()->resetStylesheet($style->getDecoratedObject());
                }
@@ -246,6 +249,88 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction,
                }
        }
        
+       /**
+        * Updates style favicon files.
+        * 
+        * @param       Style           $style
+        */
+       protected function updateFavicons(Style $style) {
+               $styleID = $style->styleID;
+               $fileExtension = WCF::getSession()->getVar('styleFavicon-template-'.$styleID);
+               $hasFavicon = (bool)$style->hasFavicon;
+               if ($fileExtension) {
+                       $template = WCF_DIR . "images/favicon/{$styleID}.favicon-template.{$fileExtension}";
+                       $images = [
+                               'android-chrome-192x192.png' => 192,
+                               'android-chrome-256x256.png' => 256,
+                               'apple-touch-icon.png' => 180,
+                               'mstile-150x150.png' => 150
+                       ];
+                       
+                       $adapter = ImageHandler::getInstance()->getAdapter();
+                       $adapter->loadFile($template);
+                       foreach ($images as $filename => $length) {
+                               $thumbnail = $adapter->createThumbnail($length, $length);
+                               $adapter->writeImage($thumbnail, WCF_DIR."images/favicon/{$styleID}.{$filename}");
+                       }
+                       
+                       // create ico
+                       require(WCF_DIR . 'lib/system/api/chrisjean/php-ico/class-php-ico.php');
+                       $phpIco = new \PHP_ICO($template, [
+                               [16, 16],
+                               [32, 32]
+                       ]);
+                       $phpIco->save_ico(WCF_DIR . "images/favicon/{$styleID}.favicon.ico");
+                       
+                       $hasFavicon = true;
+                       
+                       (new StyleEditor($style))->update(['hasFavicon' => 1]);
+                       WCF::getSession()->unregister('styleFavicon-template-'.$style->styleID);
+               }
+               
+               if ($hasFavicon) {
+                       // update manifest.json
+                       $manifest = <<<MANIFEST
+{
+    "name": "",
+    "icons": [
+        {
+            "src": "{$styleID}.android-chrome-192x192.png",
+            "sizes": "192x192",
+            "type": "image/png"
+        },
+        {
+            "src": "{$styleID}.android-chrome-256x256.png",
+            "sizes": "256x256",
+            "type": "image/png"
+        }
+    ],
+    "theme_color": "#ffffff",
+    "background_color": "#ffffff",
+    "display": "standalone"
+}
+MANIFEST;
+                       file_put_contents(WCF_DIR . "images/favicon/{$styleID}.manifest.json", $manifest);
+                       
+                       $style->loadVariables();
+                       $tileColor = $style->getVariable('wcfHeaderBackground', true);
+                       
+                       // update browserconfig.xml
+                       $browserconfig = <<<BROWSERCONFIG
+<?xml version="1.0" encoding="utf-8"?>
+<browserconfig>
+    <msapplication>
+        <tile>
+            <square150x150logo src="{$styleID}.mstile-150x150.png"/>
+            <TileColor>{$tileColor}</TileColor>
+        </tile>
+    </msapplication>
+</browserconfig>
+BROWSERCONFIG;
+                       file_put_contents(WCF_DIR . "images/favicon/{$styleID}.browserconfig.xml", $browserconfig);
+               }
+       }
+       
        /**
         * @inheritDoc
         */
@@ -451,6 +536,78 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction,
                return ['errorType' => $file->getValidationErrorType()];
        }
        
+       /**
+        * Validates parameters to upload a favicon.
+        */
+       public function validateUploadFavicon() {
+               // ignore tmp hash, uploading is supported for existing styles only
+               // and files will be finally processed on form submit
+               $this->parameters['tmpHash'] = '@@@WCF_INVALID_TMP_HASH@@@';
+               
+               $this->validateUpload();
+       }
+       
+       /**
+        * Handles favicon upload.
+        *
+        * @return      string[]
+        */
+       public function uploadFavicon() {
+               // save files
+               /** @noinspection PhpUndefinedMethodInspection */
+               /** @var UploadFile[] $files */
+               $files = $this->parameters['__files']->getFiles();
+               $file = $files[0];
+               
+               try {
+                       if (!$file->getValidationErrorType()) {
+                               $fileLocation = $file->getLocation();
+                               try {
+                                       if (($imageData = getimagesize($fileLocation)) === false) {
+                                               throw new UserInputException('favicon');
+                                       }
+                                       switch ($imageData[2]) {
+                                               case IMAGETYPE_PNG:
+                                               case IMAGETYPE_JPEG:
+                                               case IMAGETYPE_GIF:
+                                                       // fine
+                                                       break;
+                                               default:
+                                                       throw new UserInputException('favicon');
+                                       }
+                                       
+                                       if ($imageData[0] != Style::FAVICON_IMAGE_WIDTH || $imageData[1] != Style::FAVICON_IMAGE_HEIGHT) {
+                                               throw new UserInputException('favicon', 'dimensions');
+                                       }
+                               }
+                               catch (SystemException $e) {
+                                       throw new UserInputException('favicon');
+                               }
+                               
+                               // move uploaded file
+                               if (@copy($fileLocation, WCF_DIR.'images/favicon/'.$this->style->styleID.'.favicon-template.'.$file->getFileExtension())) {
+                                       @unlink($fileLocation);
+                                       
+                                       // store extension within session variables
+                                       WCF::getSession()->register('styleFavicon-template-'.$this->style->styleID, $file->getFileExtension());
+                                       
+                                       // return result
+                                       return [
+                                               'url' => WCF::getPath().'images/favicon/'.$this->style->styleID.'.favicon-template.'.$file->getFileExtension()
+                                       ];
+                               }
+                               else {
+                                       throw new UserInputException('favicon', 'uploadFailed');
+                               }
+                       }
+               }
+               catch (UserInputException $e) {
+                       $file->setValidationErrorType($e->getType());
+               }
+               
+               return ['errorType' => $file->getValidationErrorType()];
+       }
+       
        /**
         * Validates parameters to assign a new default style.
         */
index e48f3525c223272739dfac13f2aa4ed307846983..b07e2fb684c2c15812a0ee41cd393b1ee0b456c9 100644 (file)
@@ -1798,6 +1798,11 @@ GmbH=Gesellschaft mit beschränkter Haftung]]></item>
                <item name="wcf.acp.style.globals.useGoogleFont"><![CDATA[Google-Schriftart aktivieren]]></item>
                <item name="wcf.acp.style.globals.fontFamilyGoogle"><![CDATA[Schriftart]]></item>
                <item name="wcf.acp.style.globals.fontFamilyFallback"><![CDATA[Schriftart (Fallback)]]></item>
+               <item name="wcf.acp.style.general.favicon"><![CDATA[Favicon]]></item>
+               <item name="wcf.acp.style.favicon"><![CDATA[Individuelles Favicon]]></item>
+               <item name="wcf.acp.style.favicon.description"><![CDATA[Laden Sie hier ein 256px × 256px großes Bild hoch, als Bildformate sind JPG und PNG zulässig. Das hochgeladene Bild wird für die Erzeugung aller notwendigen Grafiken verwendet.]]></item>
+               <item name="wcf.acp.style.favicon.error.dimensions"><![CDATA[Das Bild muss exakt 256px × 256px groß sein.]]></item>
+               <item name="wcf.acp.style.favicon.error.invalidExtension"><![CDATA[Die Datei hat eine ungültige Dateiendung.]]></item>
        </category>
        
        <category name="wcf.acp.tag">
index a99738a95e4435a222b193c2017c930780baf87d..dc85a0dd47444169628efdcbb8fd20c8bc188783 100644 (file)
                <item name="wcf.acp.style.exportStyle.components"><![CDATA[Options]]></item>
                <item name="wcf.acp.style.exportStyle.components.description"><![CDATA[Select components included in the export for the style “{$style->styleName}”.]]></item>
                <item name="wcf.acp.style.general"><![CDATA[Data]]></item>
+               <item name="wcf.acp.style.general.favicon"><![CDATA[Favicon]]></item>
                <item name="wcf.acp.style.general.files"><![CDATA[Files]]></item>
                <item name="wcf.acp.style.globals"><![CDATA[Global Settings]]></item>
                <item name="wcf.acp.style.globals.fixedLayoutWidth"><![CDATA[Width]]></item>
                <item name="wcf.acp.style.globals.useGoogleFont"><![CDATA[Use Google font face]]></item>
                <item name="wcf.acp.style.globals.fontFamilyGoogle"><![CDATA[Font Face]]></item>
                <item name="wcf.acp.style.globals.fontFamilyFallback"><![CDATA[Font Face (Fallback)]]></item>
+               <item name="wcf.acp.style.favicon"><![CDATA[Individual Favicon]]></item>
+               <item name="wcf.acp.style.favicon.description"><![CDATA[Upload an 256px × 256px image, acceptable image types are JPG and PNG. The uploaded image will be used to derive all required favicon sizes.]]></item>
+               <item name="wcf.acp.style.favicon.error.dimensions"><![CDATA[The image must be exactly 256px × 256px large.]]></item>
+               <item name="wcf.acp.style.favicon.error.invalidExtension"><![CDATA[The file extension is invalid.]]></item>
        </category>
        
        <category name="wcf.acp.tag">