Add media management (WIP)
authorMatthias Schmidt <gravatronics@live.com>
Tue, 22 Dec 2015 16:07:05 +0000 (17:07 +0100)
committerMatthias Schmidt <gravatronics@live.com>
Tue, 22 Dec 2015 16:07:05 +0000 (17:07 +0100)
57 files changed:
com.woltlab.wcf/acpMenu.xml
com.woltlab.wcf/clipboardAction.xml
com.woltlab.wcf/objectType.xml
com.woltlab.wcf/option.xml
com.woltlab.wcf/templates/dashboard.tpl
com.woltlab.wcf/templates/headIncludeJavaScript.tpl
com.woltlab.wcf/templates/languageChooser.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/mediaEditor.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/mediaListItems.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/mediaManager.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/multipleLanguageInputJavascript.tpl
com.woltlab.wcf/templates/sitemap.tpl
com.woltlab.wcf/userGroupOption.xml
wcfsetup/install/files/acp/js/WCF.ACP.Style.js
wcfsetup/install/files/acp/templates/languageChooser.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/mediaAdd.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/mediaList.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/multipleLanguageInputJavascript.tpl
wcfsetup/install/files/acp/templates/styleAdd.tpl
wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Style/Image/Upload.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Attachment/Upload.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Controller/Clipboard.js
wcfsetup/install/files/js/WoltLab/WCF/Dictionary.js
wcfsetup/install/files/js/WoltLab/WCF/File/Util.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Language/Chooser.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Media/Editor.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Media/Manager.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Media/Search.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Media/Upload.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Dialog.js
wcfsetup/install/files/js/WoltLab/WCF/Upload.js
wcfsetup/install/files/js/require.config.js
wcfsetup/install/files/lib/acp/form/MediaEditForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/page/MediaAddPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/page/MediaListPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/IFile.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/IThumbnailFile.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/IUploadAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/attachment/Attachment.class.php
wcfsetup/install/files/lib/data/attachment/AttachmentAction.class.php
wcfsetup/install/files/lib/data/media/Media.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/media/MediaAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/media/MediaEditor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/media/MediaList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/media/ViewableMedia.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/media/ViewableMediaList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/style/StyleAction.class.php
wcfsetup/install/files/lib/page/MediaPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/clipboard/action/MediaClipboardAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/language/I18nHandler.class.php
wcfsetup/install/files/lib/system/upload/DefaultUploadFileSaveStrategy.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/util/FileUtil.class.php
wcfsetup/install/files/media/.htaccess [new file with mode: 0644]
wcfsetup/install/files/style/ui/alert.scss
wcfsetup/install/files/style/ui/media.scss [new file with mode: 0644]
wcfsetup/install/lang/en.xml
wcfsetup/setup/db/install.sql

index 65f790266e517f075272624eb5914f647e579102..e0f5acc1464517746d5f4f493924900c1ff1ebad 100644 (file)
                        <parent>wcf.acp.menu.link.cms</parent>
                        <permissions>admin.content.cms.canManagePage</permissions>
                </acpmenuitem>
-               
                <acpmenuitem name="wcf.acp.menu.link.cms.page.add">
                        <controller><![CDATA[wcf\acp\form\PageAddForm]]></controller>
                        <parent>wcf.acp.menu.link.cms.page.list</parent>
                        <parent>wcf.acp.menu.link.cms</parent>
                        <permissions>admin.content.cms.canManageMenu</permissions>
                </acpmenuitem>
-               
                <acpmenuitem name="wcf.acp.menu.link.cms.menu.add">
                        <controller><![CDATA[wcf\acp\form\MenuAddForm]]></controller>
                        <parent>wcf.acp.menu.link.cms.menu.list</parent>
                        <parent>wcf.acp.menu.link.cms</parent>
                        <permissions>admin.content.cms.canManageBox</permissions>
                </acpmenuitem>
-               
                <acpmenuitem name="wcf.acp.menu.link.cms.box.add">
                        <controller><![CDATA[wcf\acp\form\BoxAddForm]]></controller>
                        <parent>wcf.acp.menu.link.cms.box.list</parent>
                        <permissions>admin.content.cms.canManageBox</permissions>
                        <icon>fa-plus</icon>
                </acpmenuitem>
+               
+               <acpmenuitem name="wcf.acp.menu.link.cms.media.list">
+                       <controller><![CDATA[wcf\acp\page\MediaListPage]]></controller>
+                       <parent>wcf.acp.menu.link.cms</parent>
+                       <permissions>admin.content.cms.canManageMedia</permissions>
+               </acpmenuitem>
+               <acpmenuitem name="wcf.acp.menu.link.cms.media.add">
+                       <controller><![CDATA[wcf\acp\form\MediaAddForm]]></controller>
+                       <parent>wcf.acp.menu.link.cms.media.list</parent>
+                       <permissions>admin.content.cms.canManageMedia</permissions>
+                       <icon>fa-plus</icon>
+               </acpmenuitem>
        </import>
 </data>
index c91192b202f058f031b93c30130472db96dd4adf..d633a98d87c1255f727c40690abaa73de0fe8a3e 100644 (file)
                        </pages>
                </action>
                <!-- com.woltlab.wcf.tag -->
+               
+               <!-- com.woltlab.wcf.media -->
+               <action name="insert">
+                       <actionclassname><![CDATA[wcf\system\clipboard\action\MediaClipboardAction]]></actionclassname>
+                       <showorder>1</showorder>
+                       <pages>
+                               <!--
+                               the clipboard API requires a page but since the media clipboard can be used
+                               in dialogs, we use this wildcard instead of a real page class
+                               -->
+                               <page><![CDATA[*]]></page>
+                       </pages>
+               </action>
+               <action name="delete">
+                       <actionclassname><![CDATA[wcf\system\clipboard\action\MediaClipboardAction]]></actionclassname>
+                       <showorder>2</showorder>
+                       <pages>
+                               <page><![CDATA[*]]></page>
+                       </pages>
+               </action>
+               <!-- /com.woltlab.wcf.media -->
        </import>
 </data>
index dc9c8000aa3bda5f59edce1ffd088ee4af2ce748..e9b0d6f37a3d3fc53f0cbdf9008f31c14cf18bc0 100644 (file)
                        <definitionname>com.woltlab.wcf.clipboardItem</definitionname>
                        <listclassname><![CDATA[wcf\data\tag\TagList]]></listclassname>
                </type>
+               <type>
+                       <name>com.woltlab.wcf.media</name>
+                       <definitionname>com.woltlab.wcf.clipboardItem</definitionname>
+                       <listclassname><![CDATA[wcf\data\media\ViewableMediaList]]></listclassname>
+               </type>
                <!-- /clipboard items -->
                
                <type>
index be74404b4e16ddd466d2e50996e4c593fbe56344..f09b7fd1da64170f9f2691871033b7cd7957bd9f 100644 (file)
                                        <category name="dashboard.sidebar.recentActivities">
                                                <parent>dashboard.sidebar</parent>
                                        </category>
+                       
+                       <!-- cms -->
+                       <category name="cms"></category>
+                       <category name="cms.media">
+                               <parent>cms</parent>
+                       </category>
+                       <category name="cms.media.thumbnail">
+                               <parent>cms.media</parent>
+                       </category>
+                       <!-- /cms -->
                </categories>
                
                <options>
@@ -1471,6 +1481,64 @@ DESC:wcf.global.sortOrder.descending]]></selectoptions>
                                <maxvalue>100</maxvalue>
                        </option>
                        <!-- /message.general.poll -->
+                       
+                       <!-- cms.media.thumnail -->
+                       <option name="media_small_thumbnail_width">
+                               <categoryname>cms.media.thumbnail</categoryname>
+                               <optiontype>integer</optiontype>
+                               <defaultvalue>280</defaultvalue> <!-- TODO: temporary value -->
+                               <minvalue>145</minvalue> <!-- TODO: temporary value -->
+                               <maxvalue>400</maxvalue> <!-- TODO: temporary value -->
+                       </option>
+                       <option name="media_small_thumbnail_height">
+                               <categoryname>cms.media.thumbnail</categoryname>
+                               <optiontype>integer</optiontype>
+                               <defaultvalue>210</defaultvalue> <!-- TODO: temporary value -->
+                               <minvalue>145</minvalue> <!-- TODO: temporary value -->
+                               <maxvalue>300</maxvalue> <!-- TODO: temporary value -->
+                       </option>
+                       <option name="media_small_thumbnail_retain_dimensions">
+                               <categoryname>cms.media.thumbnail</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                       </option>
+                       <option name="media_medium_thumbnail_width">
+                               <categoryname>cms.media.thumbnail</categoryname>
+                               <optiontype>integer</optiontype>
+                               <defaultvalue>560</defaultvalue> <!-- TODO: temporary value -->
+                               <minvalue>400</minvalue> <!-- TODO: temporary value -->
+                               <maxvalue>900</maxvalue> <!-- TODO: temporary value -->
+                       </option>
+                       <option name="media_medium_thumbnail_height">
+                               <categoryname>cms.media.thumbnail</categoryname>
+                               <optiontype>integer</optiontype>
+                               <defaultvalue>420</defaultvalue> <!-- TODO: temporary value -->
+                               <minvalue>300</minvalue> <!-- TODO: temporary value -->
+                               <maxvalue>700</maxvalue> <!-- TODO: temporary value -->
+                       </option>
+                       <option name="media_medium_thumbnail_retain_dimensions">
+                               <categoryname>cms.media.thumbnail</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                       </option>
+                       <option name="media_large_thumbnail_width">
+                               <categoryname>cms.media.thumbnail</categoryname>
+                               <optiontype>integer</optiontype>
+                               <defaultvalue>1200</defaultvalue> <!-- TODO: temporary value -->
+                               <minvalue>900</minvalue> <!-- TODO: temporary value -->
+                       </option>
+                       <option name="media_large_thumbnail_height">
+                               <categoryname>cms.media.thumbnail</categoryname>
+                               <optiontype>integer</optiontype>
+                               <defaultvalue>900</defaultvalue> <!-- TODO: temporary value -->
+                               <minvalue>700</minvalue> <!-- TODO: temporary value -->
+                       </option>
+                       <option name="media_large_thumbnail_retain_dimensions">
+                               <categoryname>cms.media.thumbnail</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                       </option>
+                       <!-- cms.media.thumnail -->
                </options>
        </import>
        
index a3b3daf148c7cee0c49027869a4c0404528b4825..eda866905ae424c2bbabf0705fe4206378b53548 100644 (file)
        </header>
 {/if}
 
+{*TODO: remove dummy media manager dialog demonstration code later on*}
+<p class="button" id="mediaManagerButton">click</p>
+
+<script data-relocate="true">
+       $(function() {
+               require(['WoltLab/WCF/Media/Manager', 'Language', 'Permission'], function(MediaManager, Language, Permission) {
+                       Language.addObject({
+                               'wcf.global.button.insert': '{lang}wcf.global.button.insert{/lang}',
+                               
+                               'wcf.media.insert': '{lang}wcf.media.insert{/lang}',
+                               'wcf.media.insert.imageSize': '{lang}wcf.media.insert.imageSize{/lang}',
+                               'wcf.media.insert.imageSize.small': '{lang __literal=true}wcf.media.insert.imageSize.small{/lang}',
+                               'wcf.media.insert.imageSize.medium': '{lang __literal=true}wcf.media.insert.imageSize.medium{/lang}',
+                               'wcf.media.insert.imageSize.large': '{lang __literal=true}wcf.media.insert.imageSize.large{/lang}',
+                               'wcf.media.insert.imageSize.original': '{lang __literal=true}wcf.media.insert.imageSize.original{/lang}',
+                               'wcf.media.manager': '{lang}wcf.media.manager{/lang}',
+                               'wcf.media.edit': '{lang}wcf.media.edit{/lang}',
+                               'wcf.media.imageDimensions.value': '{lang __literal=true}wcf.media.imageDimensions.value{/lang}',
+                               'wcf.media.button.insert': '{lang}wcf.media.button.insert{/lang}',
+                               'wcf.media.search.filetype': '{lang}wcf.media.search.filetype{/lang}',
+                               'wcf.media.search.noResults': '{lang}wcf.media.search.noResults{/lang}'
+                       });
+                       
+                       Permission.add('admin.content.cms.canManageMedia', {if $__wcf->session->getPermission('admin.content.cms.canManageMedia')}true{else}false{/if});
+                       
+                       new MediaManager();
+               });
+       });
+</script>
+{* /end *}
+
 {include file='userNotice'}
 
 <div class="contentNavigation">
index 47caae7d03f24e7555c86f77f61251fe08f9e8f7..f938b8d9950ab9f82bafa474d9bc640a9dabe244 100644 (file)
@@ -65,6 +65,7 @@ requirejs.config({
                        'wcf.global.form.error.greaterThan': '{lang __literal=true}wcf.global.form.error.greaterThan{/lang}',
                        'wcf.global.form.error.lessThan': '{lang __literal=true}wcf.global.form.error.lessThan{/lang}',
                        'wcf.global.form.input.maxItems': '{lang}wcf.global.form.input.maxItems{/lang}',
+                       'wcf.global.form.error.multilingual': '{lang}wcf.global.form.error.multilingual{/lang}',
                        'wcf.global.language.noSelection': '{lang}wcf.global.language.noSelection{/lang}',
                        'wcf.global.loading': '{lang}wcf.global.loading{/lang}',
                        'wcf.global.page.jumpTo': '{lang}wcf.global.page.jumpTo{/lang}',
diff --git a/com.woltlab.wcf/templates/languageChooser.tpl b/com.woltlab.wcf/templates/languageChooser.tpl
new file mode 100644 (file)
index 0000000..c75dfaa
--- /dev/null
@@ -0,0 +1,31 @@
+{if !$label|isset}{assign var='label' value='wcf.user.language'}{/if}
+
+{if $languages|count}
+       <dl{if $errorField|isset && $errorField == 'languageID'} class="formError"{/if}>
+               <dt>{lang}{$label}{/lang}</dt>
+               <dd id="languageIDContainer">
+                       <noscript>
+                               <select name="languageID" id="languageID">
+                                       {foreach from=$languages item=__language}
+                                               <option value="{@$__language->languageID}">{$__language}</option>
+                                       {/foreach}
+                               </select>
+                       </noscript>
+               </dd>
+       </dl>
+       
+       <script data-relocate="true">
+               require(['WoltLab/WCF/Language/Chooser'], function(LanguageChooser) {
+                       var languages = {
+                               {implode from=$languages item=__language}
+                                       '{@$__language->languageID}': {
+                                               iconPath: '{@$__language->getIconPath()}',
+                                               languageName: '{$__language}'
+                                       }
+                               {/implode}
+                       };
+                       
+                       LanguageChooser.init('languageIDContainer', 'languageID', {$languageID}, languages)
+               });
+       </script>
+{/if}
diff --git a/com.woltlab.wcf/templates/mediaEditor.tpl b/com.woltlab.wcf/templates/mediaEditor.tpl
new file mode 100644 (file)
index 0000000..65172e5
--- /dev/null
@@ -0,0 +1,65 @@
+<div id="mediaThumbnail" class="framed"></div>
+
+<div class="box48">
+       <span id="mediaFileIcon" class="icon icon48 fa-file-o"></span>
+       
+       <dl class="plain dataList">
+               <dt>{lang}wcf.media.filename{/lang}</dt>
+               <dd id="mediaFilename"></dd>
+               
+               <dt>{lang}wcf.media.filesize{/lang}</dt>
+               <dd id="mediaFilesize"></dd>
+               
+               <dt>{lang}wcf.media.imageDimensions{/lang}</dt>
+               <dd id="mediaImageDimensions"></dd>
+               
+               <dt>{lang}wcf.media.uploader{/lang}</dt>
+               <dd id="mediaUploader"></dd>
+       </dl>
+</div>
+
+<fieldset class="marginTop">
+       <legend>{lang}wcf.global.form.data{/lang}</legend>
+       
+       <dl>
+               <dt></dt>
+               <dd>
+                       <label>
+                               <input type="checkbox" id="isMultilingual" name="isMultilingual" value="1" />
+                               <span>{lang}wcf.media.isMultilingual{/lang}</span>
+                       </label>
+               </dd>
+       </dl>
+       
+       {include file='languageChooser' label='wcf.media.languageID'}
+       
+       <dl>
+               <dt>{lang}wcf.global.title{/lang}</dt>
+               <dd>
+                       <input type="text" id="title" name="title" class="long" />
+               </dd>
+       </dl>
+       {include file='multipleLanguageInputJavascript' elementIdentifier='title' forceSelection=true}
+       
+       <dl>
+               <dt>{lang}wcf.media.caption{/lang}</dt>
+               <dd>
+                       <textarea id="caption" name="caption" cols="40" rows="3"></textarea>
+               </dd>
+       </dl>
+       {include file='multipleLanguageInputJavascript' elementIdentifier='caption' forceSelection=true}
+       
+       <dl>
+               <dt>{lang}wcf.media.altText{/lang}</dt>
+               <dd>
+                       <input type="text" id="altText" name="altText" class="long" />
+               </dd>
+       </dl>
+       {include file='multipleLanguageInputJavascript' elementIdentifier='altText' forceSelection=true}
+       
+       {event name='dataFields'}
+</fieldset>
+
+<div class="formSubmit">
+       <button data-type="submit" class="buttonPrimary">{lang}wcf.global.button.submit{/lang}</button>
+</div>
diff --git a/com.woltlab.wcf/templates/mediaListItems.tpl b/com.woltlab.wcf/templates/mediaListItems.tpl
new file mode 100644 (file)
index 0000000..1621bd5
--- /dev/null
@@ -0,0 +1,30 @@
+{foreach from=$mediaList item=media}
+       <li class="jsClipboardObject" data-object-id="{@$media->mediaID}">
+               <div class="mediaThumbnail">
+                       {@$media->getElementTag(96)}
+               </div>
+               
+               <div class="mediaInformation">
+                       <p class="mediaTitle">{if $media->title}{$media->title}{else}{$media->filename}{/if}</p>
+               </div>
+               
+               <nav class="buttonGroupNavigation">
+                       <ul class="smallButtons buttonGroup">
+                               <li>
+                                       <input type="checkbox" class="jsClipboardItem jsMediaCheckbox" data-object-id="{@$media->mediaID}" />
+                               </li>
+                               {if $__wcf->session->getPermission('admin.content.cms.canManageMedia')}
+                                       <li>
+                                               <a><span class="icon icon16 fa-pencil jsTooltip jsMediaEditIcon" data-object-id="{@$media->mediaID}" title="{lang}wcf.global.button.edit{/lang}"></span></a>
+                                       </li>
+                                       <li>
+                                               <a><span class="icon icon16 fa-times jsTooltip jsMediaDeleteIcon" data-object-id="{@$media->mediaID}" title="{lang}wcf.global.button.delete{/lang}"></span></a>
+                                       </li>
+                               {/if}
+                               <li>
+                                       <a><span class="icon icon16 fa-plus jsTooltip jsMediaInsertIcon" data-object-id="{@$media->mediaID}" title="{lang}wcf.media.button.insert{/lang}"></span></a>
+                               </li>
+                       </ul>
+               </nav>
+       </li>
+{/foreach}
diff --git a/com.woltlab.wcf/templates/mediaManager.tpl b/com.woltlab.wcf/templates/mediaManager.tpl
new file mode 100644 (file)
index 0000000..6f53d90
--- /dev/null
@@ -0,0 +1,33 @@
+<div class="inputAddon dropdown" id="mediaManagerSearch">
+       <span class="button dropdownToggle inputPrefix">
+               <span class="active">{lang}wcf.media.search.filetype{/lang}</span>
+       </span>
+       <ul class="dropdownMenu">
+               <li data-file-type="image"><span>{lang}wcf.media.search.filetype.image{/lang}</span></li>
+               <li data-file-type="text"><span>{lang}wcf.media.search.filetype.text{/lang}</span></li>
+               <li data-file-type="pdf"><span>{lang}wcf.media.search.filetype.pdf{/lang}</span></li>
+               <li data-file-type="other"><span>{lang}wcf.media.search.filetype.other{/lang}</span></li>
+               {event name='filetype'}
+               <li class="dropdownDivider"></li>
+               <li data-file-type="all"><span>{lang}wcf.media.search.filetype.all{/lang}</span></li>
+       </ul>
+       <input type="text" id="mediaManagerSearchField" placeholder="{lang}wcf.media.search.placeholder{/lang}" />
+       <span class="inputSuffix">
+               <span id="mediaManagerSearchCancelButton" class="icon icon16 fa-times pointer jsTooltip" title="{lang}wcf.media.search.cancel{/lang}"></span>
+       </span>
+</div>
+
+{if $__wcf->session->getPermission('admin.content.cms.canManageMedia')}
+       <div id="mediaManagerMediaUploadButton" class="marginTop"></div>
+{/if}
+
+<div class="jsClipboardContainer marginTop" data-type="com.woltlab.wcf.media">
+       <input type="checkbox" class="jsClipboardMarkAll" style="display: none;" />
+       <ul id="mediaManagerMediaList">
+               {include file='mediaListItems'}
+       </ul>
+       
+       <div class="contentNavigation">
+               <nav class="jsClipboardEditor" data-types="[ 'com.woltlab.wcf.media' ]"></nav>
+       </div>
+</div>
index f59328760d2fd65b0c6a705853bf6c20c818052f..cdacde670434be4b8043eda63035190d7fa773f1 100644 (file)
@@ -1,11 +1,14 @@
 {if $availableLanguages|count > 1}
        <script data-relocate="true">
-               //<![CDATA[
-               $(function() {
-                       var $availableLanguages = { {implode from=$availableLanguages key=languageID item=languageName}{@$languageID}: '{$languageName}'{/implode} };
-                       var $values = { {implode from=$i18nValues[$elementIdentifier] key=languageID item=value}'{@$languageID}': '{$value}'{/implode} };
-                       new WCF.MultipleLanguageInput('{@$elementIdentifier}', {if $forceSelection}true{else}false{/if}, $values, $availableLanguages);
+               require(['Language', 'WoltLab/WCF/Language/Input'], function(Language, LanguageInput) {
+                       Language.addObject({
+                               'wcf.global.button.disabledI18n': '{lang}wcf.global.button.disabledI18n{/lang}'
+                       });
+                       
+                       var availableLanguages = { {implode from=$availableLanguages key=languageID item=languageName}{@$languageID}: '{$languageName}'{/implode} };
+                       var values = { {implode from=$i18nValues[$elementIdentifier] key=languageID item=value}'{@$languageID}': '{$value}'{/implode} };
+                       
+                       LanguageInput.init('{@$elementIdentifier}', values, availableLanguages, {if $forceSelection}true{else}false{/if});
                });
-               //]]>
        </script>
-{/if}
\ No newline at end of file
+{/if}
index 3518a738208a640bcdcdffcaac4eac10a473d563..cffdc031220ab4cebfd16686e5a01c6bd58f8c92 100644 (file)
                                </div>
                        {/foreach}
                </div>
-               
-               <script data-relocate="true">
-                       //<![CDATA[
-                       $(function() {
-                               // fix anchor
-                               var $location = location.toString().replace(location.hash, '');
-                               $('.sitemap .tabMenu a').each(function(index, link) {
-                                       var $link = $(link);
-                                       $link.attr('href', $location + $link.attr('href'));
-                               });
-                               
-                               WCF.TabMenu.init();
-                       });
-                       //]]>
-               </script>
        {hascontentelse}
                {@$sitemap}
        {/hascontent}
index 7f2b18aea303317291094d08c89021da3c94e945..0209520d2523be5ff227cbec014029e155d7c846 100644 (file)
@@ -413,7 +413,7 @@ pdf]]></defaultvalue>
                                <admindefaultvalue>1</admindefaultvalue>
                                <usersonly>1</usersonly>
                        </option>
-                                               
+                       
                        <option name="admin.content.cms.canManagePage">
                                <categoryname>admin.content</categoryname>
                                <optiontype>boolean</optiontype>
@@ -438,6 +438,22 @@ pdf]]></defaultvalue>
                                <usersonly>1</usersonly>
                        </option>
                        
+                       <option name="admin.content.cms.canManageMedia">
+                               <categoryname>admin.content</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>0</defaultvalue>
+                               <admindefaultvalue>1</admindefaultvalue>
+                               <usersonly>1</usersonly>
+                       </option>
+                       
+                       <option name="admin.content.cms.canUseMedia">
+                               <categoryname>admin.content</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>0</defaultvalue>
+                               <admindefaultvalue>1</admindefaultvalue>
+                               <usersonly>1</usersonly>
+                       </option>
+                       
                        <!-- user.message -->
                        <option name="user.message.canUseSmilies">
                                <categoryname>user.message</categoryname>
index 1285d703677fb845fbf2c92d5c0f86cb9b779aeb..3f257b52000bdb414521be144c3094dd54cf3212 100644 (file)
@@ -69,6 +69,7 @@ WCF.ACP.Style.CopyStyle = Class.extend({
  * 
  * @param      integer         styleID
  * @param      string          tmpHash
+ * @deprecated use WoltLab/WCF/Acp/Ui/Style/Image/Upload
  */
 WCF.ACP.Style.ImageUpload = WCF.Upload.extend({
        /**
diff --git a/wcfsetup/install/files/acp/templates/languageChooser.tpl b/wcfsetup/install/files/acp/templates/languageChooser.tpl
new file mode 100644 (file)
index 0000000..ddbeb07
--- /dev/null
@@ -0,0 +1,31 @@
+{if !$label|isset}{assign var='label' value='wcf.user.language'}{/if}
+
+{if $languages|count}
+       <dl{if $errorField == 'languageID'} class="formError"{/if}>
+               <dt>{lang}{$label}{/lang}</dt>
+               <dd id="languageIDContainer">
+                       <noscript>
+                               <select name="languageID" id="languageID">
+                                       {foreach from=$languages item=__language}
+                                               <option value="{@$__language->languageID}">{$__language}</option>
+                                       {/foreach}
+                               </select>
+                       </noscript>
+               </dd>
+       </dl>
+       
+       <script data-relocate="true">
+               require(['WoltLab/WCF/Language/Chooser'], function(LanguageChooser) {
+                       var languages = {
+                               {implode from=$languages item=__language}
+                                       '{@$__language->languageID}': {
+                                               iconPath: '{@$__language->getIconPath()}',
+                                               languageName: '{$__language}'
+                                       }
+                               {/implode}
+                       };
+                       
+                       LanguageChooser.init('languageIDContainer', 'languageID', {$languageID}, languages)
+               });
+       </script>
+{/if}
diff --git a/wcfsetup/install/files/acp/templates/mediaAdd.tpl b/wcfsetup/install/files/acp/templates/mediaAdd.tpl
new file mode 100644 (file)
index 0000000..99571ec
--- /dev/null
@@ -0,0 +1,173 @@
+{include file='header' pageTitle='wcf.acp.media.'|concat:$action}
+
+{if $action == 'add'}
+       <script data-relocate="true">
+               require(['EventHandler', 'WoltLab/WCF/Media/Upload'], function(EventHandler, MediaUpload) {
+                       new MediaUpload('uploadButton', 'mediaFile');
+                       
+                       // redirect the user to the edit form after uploading the file
+                       EventHandler.add('com.woltlab.wcf.media.upload', 'success', function(data) {
+                               for (var index in data.media) {
+                                       window.location = '{link controller='MediaEdit' id=2147483648 encode=false}{/link}'.replace(2147483648, data.media[index].mediaID);
+                               }
+                       });
+               });
+       </script>
+{/if}
+
+<header class="boxHeadline">
+       <h1 id="mediaActionTitle">{lang}wcf.acp.media.{$action}{/lang}</h1>
+</header>
+
+{if $action == 'edit'}
+       {include file='formError'}
+{/if}
+
+{if $success|isset}
+       <p class="success">{lang}wcf.global.success.{$action}{/lang}</p>
+{/if}
+
+<div class="contentNavigation">
+       <nav>
+               <ul>
+                       <li><a href="{link controller='MediaList'}{/link}" class="button"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.menu.link.media.list{/lang}</span></a></li>
+                       
+                       {event name='contentNavigationButtons'}
+               </ul>
+       </nav>
+</div>
+
+{if $action == 'add'}
+       <section class="marginTop">
+               <h1>{lang}wcf.media.file{/lang}</h1>
+               
+               <dl>
+                       <dt></dt>
+                       <dd>
+                               <div id="mediaFile"></div>
+                               <div id="uploadButton"></div>
+                       </dd>
+               </dl>
+       </section>
+{else}
+       <form method="post" action="{link controller='MediaEdit' object=$media}{/link}">
+               <section class="marginTop">
+                       <h1>{lang}wcf.global.form.data{/lang}</h1>
+                       
+                       <dl>
+                               <dt>{lang}wcf.media.file{/lang}</dt>
+                               <dd>{$media->filename} {*TODO: better output *}</dd>
+                       </dl>
+                       
+                       <dl>
+                               <dt></dt>
+                               <dd>
+                                       <label>
+                                               <input type="checkbox" id="isMultilingual" name="isMultilingual" value="1"{if $isMultilingual} checked="checked"{/if} />
+                                               <span>{lang}wcf.media.isMultilingual{/lang}</span>
+                                       </label>
+                               </dd>
+                       </dl>
+                       
+                       {include file='languageChooser' label='wcf.media.languageID'}
+                       
+                       <dl{if $errorField == 'title'} class="formError"{/if}>
+                               <dt>{lang}wcf.global.title{/lang}</dt>
+                               <dd>
+                                       <input type="text" id="title" name="title" value="{$i18nPlainValues['title']}" class="long" />
+                                       {if $errorField == 'title'}
+                                               <small class="innerError">
+                                                       {if $errorType == 'title' || $errorType == 'multilingual'}
+                                                               {lang}wcf.global.form.error.{@$errorType}{/lang}
+                                                       {else}
+                                                               {lang}wcf.media.title.error.{@$errorType}{/lang}
+                                                       {/if}
+                                               </small>
+                                       {/if}
+                               </dd>
+                       </dl>
+                       {include file='multipleLanguageInputJavascript' elementIdentifier='title' forceSelection=true}
+                       
+                       <dl{if $errorField == 'caption'} class="formError"{/if}>
+                               <dt>{lang}wcf.media.caption{/lang}</dt>
+                               <dd>
+                                       <textarea id="caption" name="caption" cols="40" rows="3">{$i18nPlainValues['caption']}</textarea>
+                                       {if $errorField == 'caption'}
+                                               <small class="innerError">
+                                                       {if $errorType == 'title' || $errorType == 'multilingual'}
+                                                               {lang}wcf.global.form.error.{@$errorType}{/lang}
+                                                       {else}
+                                                               {lang}wcf.media.caption.error.{@$errorType}{/lang}
+                                                       {/if}
+                                               </small>
+                                       {/if}
+                               </dd>
+                       </dl>
+                       {include file='multipleLanguageInputJavascript' elementIdentifier='caption' forceSelection=true}
+                       
+                       <dl{if $errorField == 'altText'} class="formError"{/if}>
+                               <dt>{lang}wcf.media.altText{/lang}</dt>
+                               <dd>
+                                       <input type="text" id="altText" name="altText" value="{$i18nPlainValues['altText']}" class="long" />
+                                       {if $errorField == 'altText'}
+                                               <small class="innerError">
+                                                       {if $errorType == 'title' || $errorType == 'multilingual'}
+                                                               {lang}wcf.global.form.error.{@$errorType}{/lang}
+                                                       {else}
+                                                               {lang}wcf.media.altText.error.{@$errorType}{/lang}
+                                                       {/if}
+                                               </small>
+                                       {/if}
+                               </dd>
+                       </dl>
+                       {include file='multipleLanguageInputJavascript' elementIdentifier='altText' forceSelection=true}
+                       
+                       {event name='dataFields'}
+               </section>
+               
+               {event name='sections'}
+               
+               <div class="formSubmit">
+                       <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s" />
+                       {@SECURITY_TOKEN_INPUT_TAG}
+               </div>
+       </form>
+{/if}
+
+{if $action == 'edit'}
+       {* this code needs to be put after all multipleLanguageInputJavascript template have been included *}
+       <script data-relocate="true">
+               require(['WoltLab/WCF/Language/Input'], function(LanguageInput) {
+                       function updateLanguageFields() {
+                               var languageIdContainer = elById('languageIDContainer').parentNode;
+                               
+                               if (elById('isMultilingual').checked) {
+                                       LanguageInput.enable('title');
+                                       LanguageInput.enable('caption');
+                                       LanguageInput.enable('altText');
+                                       
+                                       elHide(languageIdContainer);
+                               }
+                               else {
+                                       LanguageInput.disable('title');
+                                       LanguageInput.disable('caption');
+                                       LanguageInput.disable('altText');
+                                       
+                                       elShow(languageIdContainer);
+                               }
+                       };
+                       
+                       elById('isMultilingual').addEventListener('change', updateLanguageFields);
+                       
+                       updateLanguageFields();
+                       
+                       {if !$isMultilingual}
+                               elById('title').value = '{$i18nPlainValues['title']|encodeJs}';
+                               elById('title').caption = '{$i18nPlainValues['caption']|encodeJs}';
+                               elById('title').altText = '{$i18nPlainValues['altText']|encodeJs}';
+                       {/if}
+               });
+       </script>
+{/if}
+
+{include file='footer'}
diff --git a/wcfsetup/install/files/acp/templates/mediaList.tpl b/wcfsetup/install/files/acp/templates/mediaList.tpl
new file mode 100644 (file)
index 0000000..f1ef8a9
--- /dev/null
@@ -0,0 +1,131 @@
+{include file='header' pageTitle='wcf.acp.media.list'}
+
+<header class="boxHeadline">
+       <h1>{lang}wcf.acp.media.list{/lang}</h1>
+       <p>{lang}wcf.acp.media.stats{/lang}</p>
+</header>
+
+{include file='formError'}
+
+{*
+TODO: add file search
+<form method="post" action="{link controller='MediaList'}{/link}">
+       <div class="container containerPadding marginTop">
+               <fieldset>
+                       <legend>{lang}wcf.global.filter{/lang}</legend>
+                       
+                       <dl>
+                               <dt><label for="username">{lang}wcf.user.username{/lang}</label></dt>
+                               <dd>
+                                       <input type="text" id="username" name="username" value="{$username}" class="long" />
+                               </dd>
+                       </dl>
+                       
+                       <dl>
+                               <dt><label for="filename">{lang}wcf.media.filename{/lang}</label></dt>
+                               <dd>
+                                       <input type="text" id="filename" name="filename" value="{$filename}" class="long" />
+                               </dd>
+                       </dl>
+                       
+                       <dl>
+                               <dt><label for="fileType">{lang}wcf.media.fileType{/lang}</label></dt>
+                               <dd>
+                                       <select name="fileType" id="fileType">
+                                               <option value="">{lang}wcf.global.noSelection{/lang}</option>
+                                               {htmlOptions options=$availableFileTypes selected=$fileType}
+                                       </select>
+                               </dd>
+                       </dl>
+               </fieldset>
+       </div>
+       
+       <div class="formSubmit">
+               <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s" />
+               {@SECURITY_TOKEN_INPUT_TAG}
+       </div>
+</form>
+*}
+
+<div class="contentNavigation">
+       {assign var='linkParameters' value=''}
+       {*
+       {if $username}{capture append=linkParameters}&username={@$username|rawurlencode}{/capture}{/if}
+       {if $filename}{capture append=linkParameters}&filename={@$filename|rawurlencode}{/capture}{/if}
+       {if $fileType}{capture append=linkParameters}&fileType={@$fileType|rawurlencode}{/capture}{/if}
+       *}
+       
+       {pages print=true assign=pagesLinks controller="MediaList" link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder$linkParameters"}
+       
+       <nav>
+               <ul>
+                       <li><a href="{link controller='MediaAdd'}{/link}" class="button"><span class="icon icon16 fa-plus"></span> <span>{lang}wcf.acp.media.add{/lang}</span></a></li>
+                       
+                       {event name='contentNavigationButtonsTop'}
+               </ul>
+       </nav>
+</div>
+
+{if $objects|count}
+       <div class="tabularBox tabularBoxTitle marginTop">
+               <header>
+                       <h2>{lang}wcf.acp.media.list{/lang} <span class="badge badgeInverse">{#$items}</span></h2>
+               </header>
+               
+               <table class="table">
+                       <thead>
+                               <tr>
+                                       <th class="columnID columnMediaID{if $sortField == 'mediaID'} active {@$sortOrder}{/if}" colspan="2"><a href="{link controller='MediaList'}pageNo={@$pageNo}&sortField=mediaID&sortOrder={if $sortField == 'mediaID' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{@$linkParameters}{/link}">{lang}wcf.global.objectID{/lang}</a></th>
+                                       <th class="columnTitle columnFilename{if $sortField == 'filename'} active {@$sortOrder}{/if}"><a href="{link controller='MediaList'}pageNo={@$pageNo}&sortField=filename&sortOrder={if $sortField == 'filename' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{@$linkParameters}{/link}">{lang}wcf.media.filename{/lang}</a></th>
+                                       <th class="columnDate columnUploadTime{if $sortField == 'uploadTime'} active {@$sortOrder}{/if}"><a href="{link controller='MediaList'}pageNo={@$pageNo}&sortField=uploadTime&sortOrder={if $sortField == 'uploadTime' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{@$linkParameters}{/link}">{lang}wcf.media.uploadTime{/lang}</a></th>
+                                       <th class="columnDigits columnFilesize{if $sortField == 'filesize'} active {@$sortOrder}{/if}"><a href="{link controller='MediaList'}pageNo={@$pageNo}&sortField=filesize&sortOrder={if $sortField == 'filesize' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{@$linkParameters}{/link}">{lang}wcf.media.filesize{/lang}</a></th>
+                                       
+                                       {event name='columnHeads'}
+                               </tr>
+                       </thead>
+                       
+                       <tbody>
+                               {foreach from=$objects item=media}
+                                       <tr class="jsMediaRow">
+                                               <td class="columnIcon">
+                                                       <span class="icon icon16 fa-times jsDeleteButton jsTooltip pointer" title="{lang}wcf.global.button.delete{/lang}" data-object-id="{@$media->media}" data-confirm-message="{lang}wcf.media.delete.confirmMessage{/lang}"></span>
+                                                       
+                                                       {event name='rowButtons'}
+                                               </td>
+                                               <td class="columnID columnMediaID">{@$media->mediaID}</td>
+                                               <td class="columnTitle columnFilename">
+                                                       <div class="box48">
+                                                               {@$media->getElementTag(48)}
+                                                               
+                                                               <div>
+                                                                       <p><a href="{link controller='MediaEdit' object=$media}{/link}">{$media->filename|tableWordwrap}</a></p>
+                                                                       <p><small>{if $media->userID}{if $__wcf->session->getPermission('admin.user.canEditUser')}<a href="{link controller='UserEdit' id=$media->userID}{/link}">{$media->username}</a>{else}{$media->username}{/if}{else}{lang}wcf.user.guest{/lang}{/if}</small></p>
+                                                               </div>
+                                                       </div>
+                                               </td>
+                                               <td class="columnDate columnUploadTime">{@$media->uploadTime|time}</td>
+                                               <td class="columnDigits columnFilesize">{@$media->filesize|filesize}</td>
+                                               
+                                               {event name='columns'}
+                                       </tr>
+                               {/foreach}
+                       </tbody>
+               </table>
+       </div>
+       
+       <div class="contentNavigation">
+               {@$pagesLinks}
+               
+               <nav>
+                       <ul>
+                               <li><a href="{link controller='MediaAdd'}{/link}" class="button"><span class="icon icon16 fa-plus"></span> <span>{lang}wcf.acp.media.add{/lang}</span></a></li>
+                               
+                               {event name='contentNavigationButtonsBottom'}
+                       </ul>
+               </nav>
+       </div>
+{else}
+       <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+{include file='footer'}
index b78c765a37eceb92012f1825ba9ce5a8ea749819..cdacde670434be4b8043eda63035190d7fa773f1 100644 (file)
@@ -11,4 +11,4 @@
                        LanguageInput.init('{@$elementIdentifier}', values, availableLanguages, {if $forceSelection}true{else}false{/if});
                });
        </script>
-{/if}
\ No newline at end of file
+{/if}
index 831a65ef4a346b6c987a73c20fb5373f58a94d68..dc65484925265a28ab4f1666844aa1af9754be35 100644 (file)
@@ -5,12 +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(['WoltLab/WCF/Acp/Ui/Style/Editor'], function(AcpUiStyleEditor) {
+       require(['WoltLab/WCF/Acp/Ui/Style/Image/Upload', 'WoltLab/WCF/Acp/Ui/Style/Editor'], function(AcpUiStyleImageUpload, AcpUiStyleEditor) {
                AcpUiStyleEditor.setup({
                        isTainted: {if $isTainted}true{else}false{/if},
                        styleId: {if $action === 'edit'}{@$style->styleID}{else}0{/if},
                        styleRuleMap: styleRuleMap
                });
+               
+               new AcpUiStyleImageUpload({if $action == 'add'}0{else}{@$style->styleID}{/if}, '{$tmpHash}');
        });
        
        $(function() {
@@ -23,7 +25,6 @@
                        'wcf.style.colorPicker.button.apply': '{lang}wcf.style.colorPicker.button.apply{/lang}',
                        'wcf.acp.style.image.error.invalidExtension': '{lang}wcf.acp.style.image.error.invalidExtension{/lang}'
                });
-               new WCF.ACP.Style.ImageUpload({if $action == 'add'}0{else}{@$style->styleID}{/if}, '{$tmpHash}');
                new WCF.ACP.Style.LogoUpload('{$tmpHash}', '{@$__wcf->getPath()}images/');
                
                {if $action == 'edit'}
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Style/Image/Upload.js b/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Style/Image/Upload.js
new file mode 100644 (file)
index 0000000..71a85e9
--- /dev/null
@@ -0,0 +1,69 @@
+/**
+ * Handles uploading style preview images.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/Acp/Ui/Style/Image/Upload
+ */
+define(['Core', 'Dom/Traverse', 'Language', 'Ui/Notification', 'Upload'], function(Core, DomTraverse, Language, UiNotification, Upload) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function AcpUiStyleImageUpload(styleId, tmpHash) {
+               this._styleId = ~~styleId;
+               this._tmpHash = tmpHash;
+               
+               Upload.call(this, 'uploadImage', 'styleImage', {
+                       className: 'wcf\\data\\style\\StyleAction'
+               });
+       }
+       Core.inherit(AcpUiStyleImageUpload, Upload, {
+               /**
+                * @see WoltLab/WCF/Upload#_createFileElement
+                */
+               _createFileElement: function(file) {
+                       return this._target;
+               },
+               
+               /**
+                * @see WoltLab/WCF/Upload#_getParameters
+                */
+               _getParameters: function() {
+                       return {
+                               styleId: this._styleId,
+                               tmpHash: this._tmpHash
+                       };
+               },
+               
+               /**
+                * @see WoltLab/WCF/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) {
+                                       error.parentNode.removeChild(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.image.error.' + data.returnValues.errorType)
+                       }
+               }
+       });
+       
+       return AcpUiStyleImageUpload;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Attachment/Upload.js b/wcfsetup/install/files/js/WoltLab/WCF/Attachment/Upload.js
new file mode 100644 (file)
index 0000000..fae9ea7
--- /dev/null
@@ -0,0 +1,308 @@
+
+define(['Core', 'Dom/ChangeListener', 'Dom/Traverse', 'Language', 'List', 'Upload'], function(Core, DomChangeListener, DomTraverse, Language, List, Upload) {
+       "use strict";
+       
+       function AttachmentUpload(buttonContainerId, targetId, tmpHash, objectType, objectId, parentObjectId, maxUploads, maxSize, wysiwygContainerId) {
+               this._tmpHash = tmpHash;
+               this._objectType = objectType;
+               this._objectId = ~~objectId;
+               this._parentObjectId = ~~parentObjectID;
+               this._wysiwygContainerId = wysiwygContainerId;
+               
+               this._autoInsert = new List();
+               
+               Upload.call(this, 'uploadImage', 'styleImage', {
+                       className: 'wcf\\data\\attachment\\AttachmentAction',
+                       maxSize: ~~maxSize,
+                       maxUploads: ~~maxUploads,
+                       multiple: true
+               });
+               
+               // add event listeners
+               DomTraverse.childByClass(this._button, '.button').addEventListener('click', this._validateLimit.bind(this));
+               elByClass(this._target, 'jsButtonInsertAttachment').addEventListener('click', this._insert.bind(this));
+               elByClass(this._target, 'jsButtonAttachmentInsertThumbnail').addEventListener('click', this._insert.bind(this));
+               elByClass(this._target, 'jsButtonAttachmentInsertFull').addEventListener('click', this._insert.bind(this));
+               
+               // TODO: WCF.System.Event.addListener('com.woltlab.wcf.action.delete', 'attachment_' + this._wysiwygContainerId, $.proxy(this._removeLimitError, this));
+               
+               // TODO: this._makeSortable();
+               
+               this._insertAllButton = elCreate('p');
+               this._insertAllButton.className = 'button jsButtonAttachmentInsertAll';
+               this._insertAllButton.textContent = Language.get('wcf.attachment.insertAll');
+               if (DomTraverse.childBySel(this._target, 'li:not(.uploadFailed)')) {
+                       this._insertAllButton.style.setProperty('display', 'none');
+               }
+               this._insertAllButton.addEventListener('click', this._insertAll.bind(this));
+               this._button.appendChild(this._insertAllButton);
+               
+               if (this._wysiwygContainerId) {
+                       // TODO: WCF.System.Event.addListener('com.woltlab.wcf.messageOptionsInline', 'submit_' + this._wysiwygContainerId, $.proxy(this._submitInline, this));
+                       // TODO: WCF.System.Event.addListener('com.woltlab.wcf.messageOptionsInline', 'prepareExtended_' + this._wysiwygContainerId, $.proxy(this._prepareExtended, this));
+                       // TODO: WCF.System.Event.addListener('com.woltlab.wcf.redactor', 'reset', $.proxy(this._reset, this));
+                       // TODO: WCF.System.Event.addListener('com.woltlab.wcf.redactor', 'upload_' + this._wysiwygContainerId, $.proxy(this._editorUpload, this));
+                       // TODO: WCF.System.Event.addListener('com.woltlab.wcf.redactor', 'getImageAttachments_' + this._wysiwygContainerId, $.proxy(this._getImageAttachments, this));
+               }
+       };
+       Core.inherit(AttachmentUpload, Upload,{
+               /**
+                * @see WoltLab/WCF/Upload#_createFileElement
+                */
+               _createFileElement: function(file) {
+                       var listItem = elCreate('li');
+                       listItem.className = 'box64';
+                       elAttr(listItem, 'data-filename', filename);
+                       this._target.appendChild(listItem);
+                       this._target.style.removeProperty('display');
+                       
+                       var span = elCreate('span');
+                       if (this._options.maxSize >= file.size) {
+                               span.className = 'icon icon48 fa-spinnner';
+                       }
+                       else {
+                               span.className = 'icon icon48 fa-ban-circle';
+                       }
+                       listItem.appendChild(span);
+                       
+                       var div = elCreate('div');
+                       listItem.appendChild(div);
+                       
+                       var div2 = elCreate('div');
+                       div.appendChild(div2);
+                       
+                       var p = elCreate('p');
+                       p.textContent = file.name;
+                       div2.appendChild(p);
+                       
+                       var small = elCreate('small');
+                       div2.appendChild(small);
+                       
+                       if (this._options.maxSize >= file.size) {
+                               var progress = elCreate('progress');
+                               elAttr(progress, 'max', 100);
+                       }
+                       
+                       div.appendChild(elCreate('ul'));
+                       
+                       if (this._options.maxSize < file.size) {
+                               small = elCreate('small');
+                               small.className = 'innerError';
+                               small.textContent = Language.get('wcf.attachment.upload.error.tooLarge');
+                               div2.appendChild(small);
+                               
+                               listItem.classList.add('uploadFailed');
+                       }
+               },
+               
+               /**
+                * @see WoltLab/WCF/Upload#_createFileElements
+                */
+               _createFileElements: function(files) {
+                       var failedUploads = DomTraverse.childrenBySel(this._target, 'li.uploadFailed');
+                       for (var i = 0, length = failedUploads.length; i < length; i++) {
+                               this._target.removeChild(failedUploads[i]);
+                       }
+                       
+                       return Upload.prototype._createFileElements.call(this, files);
+               },
+               
+               /**
+                * @see WoltLab/WCF/Upload#_getParameters
+                */
+               _getParameters: function() {
+                       return {
+                               objectID: this._objectIdh,
+                               objectType: this._objectType,
+                               parentObjectID: this._parentObjectId,
+                               tmpHash: this._tmpHas
+                       };
+               },
+               
+               /**
+                * @see WoltLab/WCF/Upload#_success
+                */
+               _success: function(uploadId, data) {
+                       for (var i = 0, length = this._fileElements[uploadId].length; i < length; i++) {
+                               var listItem = this._fileElements[uploadId][i];
+                               
+                               var progress = elByTag(listItem, 'PROGRESS');
+                               progress.parentNode.removeChild(progress);
+                               
+                               var filename = elAttr(listItem, 'data-filename');
+                               var internalFileId = elAttr(listItem, 'data-internal-file-id');
+                               
+                               var icon = DomTraverse.childByClass(listItem, 'fa-spinner');
+                               
+                               if (data.returnValues && data.returnValues.attachments[internalFileId]) {
+                                       var attachment = data.returnValues.attachments[internalFileId];
+                                       if (attachment.tinyURL) {
+                                               var img = elCreate('img');
+                                               img.className = 'attachmentTinyThumbnail';
+                                               elAttr(img, 'src', attachment.tinyURL);
+                                               elAttr(img, 'alt', '');
+                                               icon.parentNode.replaceChild(icon, img);
+                                               
+                                               elAttr(listItem, 'data-height', attachment.height);
+                                               elAttr(listItem, 'data-width', attachment.width);
+                                       }
+                                       else {
+                                               // TODO: Use FileUtil.getIconClassByMimeType()?
+                                               icon.classList.remove('fa-spinner');
+                                               icon.classList.add('fa-paper-clip');
+                                       }
+                                       
+                                       var p = elByTag(listItem, 'P');
+                                       p.innerHtml = '';
+                                       
+                                       var a = elCreate('a');
+                                       a.textContent = filename;
+                                       elAttr(a, 'href', attachment.url);
+                                       
+                                       if (attachment.isImage) {
+                                               a.className = 'jsImageViewer';
+                                               elAttr(a, 'title', filename);
+                                       }
+                                       
+                                       p.appendChild(a);
+                                       
+                                       elByTag(listItem, 'SMALL').textContent = attachment.formattedFilesize;
+                                       
+                                       var ul = elByTag(listItem, 'UL');
+                                       ul.classList.add('buttonGroup');
+                                       
+                                       var deleteButton = elCreate('li');
+                                       ul.appendChild(deleteButton);
+                                       
+                                       var span = elCreate('span');
+                                       span.className = 'button small jsDeleteButton';
+                                       span.textContent = Language.get('wcf.global.button.delete');
+                                       elAttr(span, 'data-object-id', attachment.attachmentID);
+                                       elAttr(span, 'data-confirm-message', Language.get('wcf.attachment.delete.sure'));
+                                       if (this._wysiwygContainerId) {
+                                               elAttr(span, 'data-event-name', 'attachment_' + this._wysiwygContainerId);
+                                       }
+                                       deleteButton.appendChild(span);
+                                       
+                                       elAttr(span, 'data-object-id', attachment.attachmentID);
+                                       
+                                       if (this._wysiwygContainerId) {
+                                               if (attachment.tinyURL) {
+                                                       var insertThumbnailButton = elCreate('li');
+                                                       ul.appendChild(insertThumbnailButton);
+                                                       
+                                                       span = elCreate('span');
+                                                       span.className = 'button small jsButtonAttachmentInsertThumbnail';
+                                                       span.textContent = Language.get('wcf.global.button.insertThumbnail');
+                                                       elAttr(span, 'data-object-id', attachment.attachmentID);
+                                                       span.addEventListener('click', this._insert.bind(this));
+                                                       insertThumbnailButton.appendChild(span);
+                                                       
+                                                       var insertOriginalButton = elCreate('li');
+                                                       ul.appendChild(insertOriginalButton);
+                                                       
+                                                       span = elCreate('span');
+                                                       span.className = 'button small jsButtonAttachmentInsertFull';
+                                                       span.textContent = Language.get('wcf.global.button.insertFull');
+                                                       elAttr(span, 'data-object-id', attachment.attachmentID);
+                                                       span.addEventListener('click', this._insert.bind(this));
+                                                       insertOriginalButton.appendChild(span);
+                                               }
+                                               else {
+                                                       var insertPlainButton elCreate('li');
+                                                       ul.appendChild(insertPlainButton);
+                                                       
+                                                       span = elCreate('span');
+                                                       span.className = 'button small jsButtonAttachmentInsertPlain';
+                                                       span.textContent = Language.get('wcf.global.button.insert');
+                                                       elAttr(span, 'data-object-id', attachment.attachmentID);
+                                                       span.addEventListener('click', this._insert.bind(this));
+                                                       insertPlainButton.appendChild(span);
+                                               }
+                                       }
+                               }
+                               else {
+                                       icon.classList.removeClass('fa-spinner');
+                                       icon.classList.addClass('fa-ban-circle');
+                                       
+                                       var errorType = 'uploadFailed';
+                                       if (data.returnValues && data.returnValues.errors[internalFileId]) {
+                                               errorType = data.returnValues.errors[internalFileId].errorType;
+                                       }
+                                       
+                                       var small = elCreate('small');
+                                       small.className = 'innerError';
+                                       small.textContent = Language.get('wcf.attachment.upload.error.' + errorType);
+                                       elBySel(listItem, 'div > div').appendChild(small);
+                                       
+                                       listItem.classList.add('uploadFailed');
+                               }
+                               
+                               // fix WebKit rendinering bug
+                               // TODO: still necessary?
+                               listItem.style.setProperty('display', 'block');
+                               
+                               if (this._autoInsert.has(uploadId)) {
+                                       this._autoInsert['delete'](uploadId);
+                                       
+                                       if (listItem.classList.contains('uploadFailed')) {
+                                               // TODO: WCF.System.Event.fireEvent('com.woltlab.wcf.attachment', 'autoInsert_' + this._wysiwygContainerId, {
+                                               //      attachment: '[attach=' + attachment.attachmentID + '][/attach]',
+                                               //      uploadID: uploadId
+                                               //});
+                                       }
+                               }
+                       }
+                       
+                       // TODO: this._makeSortable();
+                       
+                       if (DomTraverse.childrenBySel(this._target, 'li:not(.uploadFailed)').length) {
+                               this._insertAllButton.style.removeProperty('display');
+                       }
+                       else {
+                               this._insertAllButton.style.setProperty('display', 'none');
+                       }
+                       
+                       DomChangeListener.trigger();
+               },
+               
+               /**
+                * @see WoltLab/WCF/Upload#_upload
+                */
+               _upload: function(event, file, blob) {
+                       if (this._validateLimit()) {
+                               Upload.prototype._upload.call(this, event, file, blob);
+                       }
+               },
+               
+               _validateLimit: function() {
+                       var innerError = DomTraverse.nextBySel(this._button, 'small.innerError');
+                       
+                       var remainingUploads = this._options.maxUploads - DomTraverse.childrenBySel(this._target, 'li:not(.uploadFailed)').length;
+                       if (remainingUploads <= 0 || remainingUploads < this._fileUpload.files.length) {
+                               if (!innerError) {
+                                       innerError = elCreate('small');
+                                       innerError.className = 'innerError';
+                                       DomUtil.insertAfter(innerError, this._button);
+                               }
+                               
+                               if (remainingUploads <= 0) {
+                                       innerError.textContent = Language.get('wcf.attachment.upload.error.reachedLimit');
+                               }
+                               else {
+                                       innerError.textContent = Language.get('wcf.attachment.upload.error.reachedRemainingLimit', {
+                                               remaining: remainingUploads
+                                       });
+                               }
+                               
+                               return false;
+                       }
+                       
+                       if (innerError) {
+                               innerError.parentNode.removeChild(innerError);
+                       }
+                       
+                       return true;
+               }
+       });
+});
index 24117fef65d5141db474b39496011c9a24a172e2..300932e56fa4ecf1b006a689e71d5b2894daa5c2 100644 (file)
@@ -64,6 +64,7 @@ define(
                        }
                        
                        DomChangeListener.add('WoltLab/WCF/Controller/Clipboard', this._initContainers.bind(this));
+                       DomChangeListener.add('WoltLab/WCF/Controller/Clipboard', this._initEditors.bind(this));
                },
                
                /**
index 94f818985feca18c3feaaef973969c0d634ea8d3..e9b5d9c7b85ad5469177ac3e38833c4a64333f41 100644 (file)
@@ -8,7 +8,7 @@
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @module     WoltLab/WCF/Dictionary
  */
-define([], function() {
+define(['Core'], function(Core) {
        "use strict";
        
        var _hasMap = objOwns(window, 'Map') && typeof window.Map === 'function';
@@ -119,6 +119,22 @@ define([], function() {
                                        this.set(key, value);
                                }).bind(this));
                        }
+               },
+               
+               /**
+                * Returns the object representation of the dictionary.
+                * 
+                * @return      {object}        dictionary's object representation
+                */
+               toObject: function() {
+                       if (!_hasMap) return Core.clone(this._dictionary);
+                       
+                       var object = { };
+                       this._dictionary.forEach(function(value, key) {
+                               object[key] = value;
+                       });
+                       
+                       return object;
                }
        };
        
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/File/Util.js b/wcfsetup/install/files/js/WoltLab/WCF/File/Util.js
new file mode 100644 (file)
index 0000000..213693a
--- /dev/null
@@ -0,0 +1,71 @@
+/**
+ * Provides helper functions to work with files.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/File/Util
+ */
+define([], function() {
+       /**
+        * @exports     WoltLab/WCF/File/Util
+        */
+       var FileUtil = {
+               /**
+                * Returns the FontAwesome icon CSS class name for a mime type.
+                * 
+                * @param       {string}        mimeType        mime type of the relevant file
+                * @return      {string}        FontAwesome icon CSS class name for the mime type
+                */
+               getIconClassByMimeType: function(mimeType) {
+                       if (mimeType.substr(0, 6) == 'image/') {
+                               return 'fa-file-image-o';
+                       }
+                       else if (mimeType.substr(0, 6) == 'video/') {
+                               return 'fa-file-video-o';
+                       }
+                       else if (mimeType.substr(0, 6) == 'audio/') {
+                               return 'fa-file-sound-o';
+                       }
+                       else if (mimeType.substr(0, 5) == 'text/') {
+                               return 'fa-file-text-o';
+                       }
+                       else {
+                               switch (mimeType) {
+                                       case 'application/msword':
+                                       case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
+                                               return 'fa-file-word-o';
+                                       break;
+                                       
+                                       case 'application/pdf':
+                                               return 'fa-file-pdf-o';
+                                       break;
+                                       
+                                       case 'application/vnd.ms-powerpoint':
+                                       case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
+                                               return 'fa-file-powerpoint-o';
+                                       break;
+                                       
+                                       case 'application/vnd.ms-excel':
+                                       case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
+                                               return 'fa-file-excel-o';
+                                       break;
+                                       
+                                       case 'application/zip':
+                                       case 'application/x-tar':
+                                       case 'application/x-gzip':
+                                               return 'fa-file-archive-o';
+                                       break;
+                                       
+                                       case 'application/xml':
+                                               return 'fa-file-text-o';
+                                       break;
+                               }
+                       }
+                       
+                       return 'fa-file-o';
+               }
+       };
+       
+       return FileUtil;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Language/Chooser.js b/wcfsetup/install/files/js/WoltLab/WCF/Language/Chooser.js
new file mode 100644 (file)
index 0000000..caa8b06
--- /dev/null
@@ -0,0 +1,274 @@
+/**
+ * Dropdown language chooser.
+ * 
+ * @author     Alexander Ebert, Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/Language/Chooser
+ */
+define(['Dictionary', 'Language', 'Dom/Traverse', 'Dom/Util', 'Ui/SimpleDropdown'], function(Dictionary, Language, DomTraverse, DomUtil, UiSimpleDropdown) {
+       "use strict";
+       
+       var _choosers = new Dictionary();
+       var _didInit = false;
+       
+       var _callbackDropdownToggle = null;
+       var _callbackSubmit = null;
+       
+       /**
+        * @exports     WoltLab/WCF/Language/Chooser
+        */
+       var LanguageChooser = {
+               /**
+                * Initializes a language chooser.
+                * 
+                * @param       {string}                                        containerId             input element conainer id
+                * @param       {string}                                        chooserId               input element id
+                * @param       {integer}                                       languageId              selected language id
+                * @param       {object<integer, object<string, string>>}       languages               data of available languages
+                * @param                                                       callback                
+                * @param                                                       allowEmptyValue         
+                */
+               init: function(containerId, chooserId, languageId, languages, callback, allowEmptyValue) {
+                       if (_choosers.has(chooserId)) {
+                               return;
+                       }
+                       
+                       var container = elById(containerId);
+                       if (container === null) {
+                               throw new Error("Expected a valid container id, cannot find '" + chooserId + "'.");
+                       }
+                       
+                       var element = elById(chooserId);
+                       if (element === null) {
+                               element = elCreate('input');
+                               elAttr(element, 'type', 'hidden');
+                               elAttr(element, 'id', chooserId);
+                               elAttr(element, 'name', chooserId);
+                               elAttr(element, 'value', languageId);
+                               
+                               container.appendChild(element);
+                       }
+                       
+                       // todo: callback
+                       
+                       this._initElement(chooserId, element, languageId, languages, allowEmptyValue);
+               },
+               
+               /**
+                * Caches common event listener callbacks.
+                */
+               _setup: function() {
+                       if (_didInit) return;
+                       _didInit = true;
+                       
+                       _callbackSubmit = this._submit.bind(this);
+               },
+               
+               _initElement: function(chooserId, element, languageId, languages, allowEmptyValue) {
+                       var container = element.parentNode;
+                       if (!container.classList.contains('inputAddon')) {
+                               container = elCreate('div');
+                               container.className = 'inputAddon';
+                               elAttr(container, 'data-input-id', chooserId);
+                               
+                               element.parentNode.insertBefore(container, element);
+                               container.appendChild(element);
+                       }
+                       
+                       container.classList.add('dropdown');
+                       var dropdownToggle = elCreate('div');
+                       dropdownToggle.className = 'dropdownToggle boxFlag box24 inputPrefix';
+                       container.insertBefore(dropdownToggle, element);
+                       
+                       var dropdownMenu = elCreate('ul');
+                       dropdownMenu.className = 'dropdownMenu';
+                       DomUtil.insertAfter(dropdownMenu, dropdownToggle);
+                       
+                       var callbackClick = (function(event) {
+                               var languageId = ~~event.currentTarget.getAttribute('data-language-id');
+                               
+                               var activeItem = DomTraverse.childByClass(dropdownMenu, 'active');
+                               if (activeItem !== null) activeItem.classList.remove('active');
+                               
+                               if (languageId) event.currentTarget.classList.add('active');
+                               
+                               this._select(chooserId, languageId, event.currentTarget);
+                       }).bind(this);
+                       
+                       // add language dropdown items
+                       for (var availableLanguageId in languages) {
+                               if (languages.hasOwnProperty(availableLanguageId)) {
+                                       var language = languages[availableLanguageId];
+                                       
+                                       var listItem = elCreate('li');
+                                       listItem.className = 'boxFlag';
+                                       listItem.addEventListener('click', callbackClick)
+                                       elAttr(listItem, 'data-language-id', availableLanguageId);
+                                       dropdownMenu.appendChild(listItem);
+                                       
+                                       var a = elCreate('a');
+                                       a.className = 'box24';
+                                       listItem.appendChild(a);
+                                       
+                                       var div = elCreate('div');
+                                       div.className = 'framed';
+                                       a.appendChild(div);
+                                       
+                                       var img = elCreate('img');
+                                       elAttr(img, 'src', language.iconPath);
+                                       elAttr(img, 'alt', '');
+                                       img.className = 'iconFlag';
+                                       div.appendChild(img);
+                                       
+                                       div = elCreate('div');
+                                       a.appendChild(div);
+                                       
+                                       var h3 = elCreate('h3');
+                                       h3.textContent = language.languageName;
+                                       div.appendChild(h3);
+                                       
+                                       if (availableLanguageId == languageId) {
+                                               dropdownToggle.innerHTML = listItem.firstChild.innerHTML;
+                                       }
+                               }
+                       }
+                       
+                       // add dropdown item for "no selection"
+                       if (allowEmptyValue) {
+                               var listItem = elCreate('li');
+                               listItem.className = 'dropdownDivider';
+                               dropdownMenu.appendChild(listItem);
+                               
+                               listItem = elCreate('li');
+                               elAttr(listItem, 'data-language-id', availableLanguageId);
+                               listItem.addEventListener('click', callbackClick);
+                               dropdownMenu.appendChild(listItem);
+                               
+                               var a = elCreate('a');
+                               a.textContent = Language.get('wcf.global.language.noSelection');
+                               listItem.appendChild(a);
+                               
+                               if (languageId === 0) {
+                                       dropdownToggle.innerHTML = listItem.firstChild.innerHTML;
+                               }
+                               
+                               listItem.addEventListener('click', callbackClick)
+                       }
+                       else if (languageId === 0) {
+                               dropdownToggle.innerHTML = null;
+                               
+                               var div = elCreate('div');
+                               dropdownToggle.appendChild(div);
+                               
+                               var span = elCreate('span');
+                               span.className = 'icon icon24 fa-question';
+                               div.appendChild(span);
+                               
+                               div = elCreate('div');
+                               dropdownToggle.appendChild(div);
+                               
+                               var h3 = elCreate('h3');
+                               h3.textContent = Language.get('wcf.global.language.noSelection');
+                               div.appendChild(h3);
+                       }
+                       
+                       UiSimpleDropdown.init(dropdownToggle);
+                       
+                       _choosers.set(chooserId, {
+                               dropdownMenu: dropdownMenu,
+                               dropdownToggle: dropdownToggle,
+                               element: element
+                       });
+                       
+                       // bind to submit event
+                       var submit = DomTraverse.parentByTag(element, 'FORM');
+                       if (submit !== null) {
+                               submit.addEventListener('submit', _callbackSubmit);
+                               
+                               // TODO: WHAT?
+                               return;
+                               var chooserIds = _forms.get(submit);
+                               if (chooserIds === undefined) {
+                                       chooserIds = [];
+                                       _forms.set(submit, chooserIds);
+                               }
+                               
+                               chooserIds.push(chooserId);
+                       }
+               },
+               
+               /**
+                * Selects a language from the dropdown list.
+                * 
+                * @param       {string}        chooserId       input element id
+                * @param       {integer}       languageId      language id or `0` to disable i18n
+                * @param       {Element}       listItem        selected list item
+                */
+               _select: function(chooserId, languageId, listItem) {
+                       var chooser = _choosers.get(chooserId);
+                       
+                       if (listItem === undefined) {
+                               var listItems = chooser.dropdownMenu.childNodes;
+                               for (var i = 0, length = listItems.length; i < length; i++) {
+                                       var _listItem = listItems[i];
+                                       if (elAttr(_listItem, 'data-language-id') == languageId) {
+                                               listItem = _listItem;
+                                               break;
+                                       }
+                               }
+                               
+                               if (listItem === undefined) {
+                                       throw new Error("Cannot select unknown language id '" + languageId + "'");
+                               }
+                       }
+                       
+                       chooser.element.value = languageId;
+                       
+                       chooser.dropdownToggle.innerHTML = listItem.firstChild.innerHTML;
+                       
+                       _choosers.set(chooserId, chooser);
+               },
+               
+               /**
+                * Returns the chooser for an input field.
+                * 
+                * @param       {string}        chooserId       input element id
+                * @return      {Dictionary}    data of the chooser
+                */
+               getChooser: function(chooserId) {
+                       var chooser = _choosers.get(chooserId);
+                       if (chooser === undefined) {
+                               throw new Error("Expected a valid language chooser input element, '" + chooserId + "' is not i18n input field.");
+                       }
+                       
+                       return chooser;
+               },
+               
+               /**
+                * Returns the selected language for a certain chooser.
+                * 
+                * @param       {string}        chooserId       input element id
+                * @return      {integer}       choosen language id
+                */
+               getLanguageId: function(chooserId) {
+                       return ~~this.getChooser(chooserId).element.value;
+               },
+               
+               /**
+                * Sets the language for a certain chooser.
+                * 
+                * @param       {string}        chooserId       input element id
+                * @param       {integer}       languageId      language id to be set
+                */
+               setLanguageId: function(chooserId, languageId) {
+                       if (_choosers.get(chooserId) === undefined) {
+                               throw new Error("Expected a valid  input element, '" + chooserId + "' is not i18n input field.");
+                       }
+                       
+                       this._select(chooserId, languageId);
+               }
+       };
+       
+       return LanguageChooser;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Media/Editor.js b/wcfsetup/install/files/js/WoltLab/WCF/Media/Editor.js
new file mode 100644 (file)
index 0000000..a018918
--- /dev/null
@@ -0,0 +1,325 @@
+/**
+ * Handles editing media files via dialog.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/Media/Editor
+ */
+define(
+       [
+               'Ajax',                       'Core',     'Dictionary', 'Dom/ChangeListener',
+               'Dom/Traverse',               'Language', 'Ui/Dialog',  'WoltLab/WCF/Language/Chooser',
+               'WoltLab/WCF/Language/Input', 'WoltLab/WCF/File/Util'
+       ],
+       function(
+               Ajax,                          Core,       Dictionary,   DomChangeListener,
+               DomTraverse,                   Language,   UiDialog,     LanguageChooser,
+               LanguageInput,                 FileUtil
+       )
+{
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function MediaEditor(callbackObject) {
+               // todo: validate callbackObject
+               
+               this._callbackObject = callbackObject;
+               this._media = null;
+               
+               this._elements = {};
+       };
+       MediaEditor.prototype = {
+               /**
+                * Returns the data for Ajax to setup the Ajax/Request object.
+                * 
+                * @return      {object}        setup data for Ajax/Request object
+                */
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       actionName: 'update',
+                                       className: 'wcf\\data\\media\\MediaAction'
+                               }
+                       };
+               },
+               
+               /**
+                * Handles successful AJAX requests.
+                * 
+                * @param       {object}        data    response data
+                */
+               _ajaxSuccess: function(data) {
+                       // TODO: check if there are validation errors?
+                       
+                       // TODO: success message
+                       
+                       this._callbackObject._editorSuccess(this._media);
+                       
+                       UiDialog.close('mediaEditor');
+                       
+                       this._media = null;
+               },
+               
+               /**
+                * Is called if the editor is manually closed by the user.
+                */
+               _close: function() {
+                       this._media = null;
+                       
+                       this._callbackObject._editorClose();
+               },
+               
+               /**
+                * Returns the data for Ui/Dialog to setup the editor dialog.
+                * 
+                * @return      {object}        data to setup the editor dialog
+                */
+               _dialogSetup: function() {
+                       return {
+                               id: 'mediaEditor',
+                               options: {
+                                       backdropCloseOnClick: false,
+                                       onClose: this._close.bind(this),
+                                       title: Language.get('wcf.media.edit')
+                               },
+                               source: {
+                                       after: (function(content, data) {
+                                               var editor = UiDialog.getDialog('mediaEditor').content;
+                                               
+                                               // data elements
+                                               this._elements.thumbnail = elById('mediaThumbnail');
+                                               this._elements.filename = elById('mediaFilename');
+                                               this._elements.filesize = elById('mediaFilesize');
+                                               this._elements.imageDimensions = elById('mediaImageDimensions');
+                                               this._elements.fileIcon = elById('mediaFileIcon');
+                                               this._elements.uploader = elById('mediaUploader');
+                                               
+                                               // input elements
+                                               this._elements.altText = elById('altText');
+                                               this._elements.caption = elById('caption');
+                                               this._elements.isMultilingual = elById('isMultilingual');
+                                               this._elements.isMultilingual.addEventListener('change', this._updateLanguageFields.bind(this));
+                                               this._elements.title = elById('title');
+                                               this._elements.languageIdContainer = elById('languageIDContainer');
+                                               
+                                               var keyPress = this._keyPress.bind(this);
+                                               this._elements.altText.addEventListener('keypress', keyPress);
+                                               this._elements.title.addEventListener('keypress', keyPress);
+                                               
+                                               setTimeout(this._setData.bind(this), 100);
+                                               
+                                               elBySel('button[data-type="submit"]', editor).addEventListener('click', this._saveData.bind(this));
+                                       }).bind(this),
+                                       data: {
+                                               actionName: 'getEditorDialog',
+                                               className: 'wcf\\data\\media\\MediaAction'
+                                       }
+                               }
+                       };
+               },
+               
+               /**
+                * Handles the `[ENTER]` key to submit the form.
+                * 
+                * @param       {object}        event           event object
+                */
+               _keyPress: function(event) {
+                       // 13 = [ENTER]
+                       if (event.charCode === 13) {
+                               event.preventDefault();
+                               
+                               this._saveData();
+                       }
+               },
+               
+               /**
+                * Saves the data of the currently edited media.
+                */
+               _saveData: function() {
+                       var hasError = false;
+                       var altTextError = DomTraverse.childByClass(this._elements.altText.parentNode.parentNode, 'innerError');
+                       var captionError = DomTraverse.childByClass(this._elements.caption.parentNode.parentNode, 'innerError');
+                       var titleError = DomTraverse.childByClass(this._elements.title.parentNode.parentNode, 'innerError');
+                       
+                       this._media.isMultilingual = ~~this._elements.isMultilingual.checked;
+                       this._media.languageID = this._media.isMultilingual ? null : LanguageChooser.getLanguageId('languageID');
+                       
+                       this._media.altText = {};
+                       this._media.caption = {};
+                       this._media.title = {};
+                       if (this._media.isMultilingual) {
+                               if (!LanguageInput.validate('altText', true)) {
+                                       hasError = true;
+                                       if (!altTextError) {
+                                               var error = elCreate('small');
+                                               error.className = 'innerError';
+                                               error.textContent = Language.get('wcf.global.form.error.multilingual');
+                                               this._elements.altText.parentNode.parentNode.appendChild(error);
+                                       }
+                               }
+                               if (!LanguageInput.validate('caption', true)) {
+                                       hasError = true;
+                                       if (!captionError) {
+                                               var error = elCreate('small');
+                                               error.className = 'innerError';
+                                               error.textContent = Language.get('wcf.global.form.error.multilingual');
+                                               this._elements.caption.parentNode.parentNode.appendChild(error);
+                                       }
+                               }
+                               if (!LanguageInput.validate('title', true)) {
+                                       hasError = true;
+                                       if (!titleError) {
+                                               var error = elCreate('small');
+                                               error.className = 'innerError';
+                                               error.textContent = Language.get('wcf.global.form.error.multilingual');
+                                               this._elements.title.parentNode.parentNode.appendChild(error);
+                                       }
+                               }
+                               
+                               this._media.altText = LanguageInput.getValues('altText').toObject();
+                               this._media.caption = LanguageInput.getValues('caption').toObject();
+                               this._media.title = LanguageInput.getValues('title').toObject();
+                       }
+                       else {
+                               this._media.altText[this._media.languageID] = this._elements.altText.value;
+                               this._media.caption[this._media.languageID] = this._elements.caption.value;
+                               this._media.title[this._media.languageID] = this._elements.title.value;
+                       }
+                       
+                       if (!hasError) {
+                               if (altTextError) elRemove(altTextError);
+                               if (captionError) elRemove(captionError);
+                               if (titleError) elRemove(titleError);
+                               
+                               Ajax.api(this, {
+                                       actionName: 'update',
+                                       objectIDs: [ this._media.mediaID ],
+                                       parameters: {
+                                               altText: this._media.altText,
+                                               caption: this._media.caption,
+                                               data: {
+                                                       isMultilingual: this._media.isMultilingual,
+                                                       languageID: this._media.languageID
+                                               },
+                                               title: this._media.title
+                                       }
+                               });
+                       }
+               },
+               
+               /**
+                * Inserts the data of the currently edited media into the dialog.
+                */
+               _setData: function() {
+                       this._elements.thumbnail.innerHTML = '';
+                       this._elements.fileIcon.parentNode.classList.remove('marginTop');
+                       
+                       this._elements.filename.textContent = this._media.filename;
+                       this._elements.filesize.textContent = this._media.formattedFilesize;
+                       
+                       this._elements.uploader.innerHTML = '';
+                       if (this._media.userLink) {
+                               var a = elCreate('a');
+                               a.className = 'userLink';
+                               elAttr(a, 'href', this._media.userLink);
+                               elData(a, 'user-id', this._media.userID);
+                               a.textContent = this._media.username;
+                               
+                               this._elements.uploader.appendChild(a);
+                       }
+                       else {
+                               this._elements.uploader.textContent = this._media.username;
+                       }
+                       
+                       if (this._media.isImage) {
+                               if (this._media.smallThumbnailLink) {
+                                       var img = elCreate('img');
+                                       elAttr(img, 'src', this._media.smallThumbnailLink);
+                                       elAttr(img, 'alt', '');
+                                       
+                                       this._elements.thumbnail.appendChild(img);
+                                       
+                                       this._elements.fileIcon.parentNode.classList.add('marginTop');
+                               }
+                               
+                               this._elements.imageDimensions.textContent = Language.get('wcf.media.imageDimensions.value', {
+                                       height: this._media.height,
+                                       width: this._media.width
+                               });
+                               elShow(this._elements.imageDimensions);
+                               elShow(this._elements.imageDimensions.previousElementSibling);
+                               
+                               this._elements.fileIcon.className = 'icon icon48 fa-file-image-o';
+                       }
+                       else {
+                               elHide(this._elements.imageDimensions);
+                               elHide(this._elements.imageDimensions.previousElementSibling);
+                               
+                               this._elements.fileIcon.className = 'icon icon48 ' + FileUtil.getIconClassByMimeType(this._media.fileType);
+                       }
+                       
+                       this._elements.isMultilingual.checked = this._media.isMultilingual;
+                       
+                       LanguageChooser.setLanguageId('languageID', this._media.languageID || LANGUAGE_ID);
+                       
+                       if (this._media.isMultilingual) {
+                               LanguageInput.setValues('altText', Dictionary.fromObject(this._media.altText || { }));
+                               LanguageInput.setValues('caption', Dictionary.fromObject(this._media.caption || { }));
+                               LanguageInput.setValues('title', Dictionary.fromObject(this._media.title || { }));
+                       }
+                       else {
+                               this._elements.altText.value = this._media.altText ? this._media.altText[languageId] : '';
+                               this._elements.caption.value = this._media.caption ? this._media.caption[this._media.languageID] : '';
+                               this._elements.title.value = this._media.title ? this._media.title[this._media.languageID] : '';
+                       }
+                       
+                       this._updateLanguageFields();
+                       
+                       DomChangeListener.trigger();
+               },
+               
+               /**
+                * Updates language-related input fields depending on whether multilingualism
+                * is enabled.
+                */
+               _updateLanguageFields: function() {
+                       if (this._elements.isMultilingual.checked) {
+                               LanguageInput.enable('title');
+                               LanguageInput.enable('caption');
+                               LanguageInput.enable('altText');
+                               
+                               elHide(this._elements.languageIdContainer.parentNode);
+                       }
+                       else {
+                               LanguageInput.disable('title');
+                               LanguageInput.disable('caption');
+                               LanguageInput.disable('altText');
+                               
+                               elShow(this._elements.languageIdContainer.parentNode);
+                       }
+               },
+               
+               /**
+                * Edits the media with the given data.
+                * 
+                * @param       {object}        media           data of the edited media
+                */
+               edit: function(media) {
+                       if (this._media !== null) {
+                               throw new Error("Cannot edit media with id '" + media.mediaID + "' while editing media with id '" + this._media.mediaID + "'")
+                       }
+                       
+                       this._media = media;
+                       
+                       if (UiDialog.getDialog('mediaEditor') !== undefined) {
+                               this._setData();
+                       }
+                       UiDialog.open(this);
+               }
+       };
+       
+       return MediaEditor;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Media/Manager.js b/wcfsetup/install/files/js/WoltLab/WCF/Media/Manager.js
new file mode 100644 (file)
index 0000000..8df686f
--- /dev/null
@@ -0,0 +1,448 @@
+/**
+ * Provides the media manager dialog.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/Controller/Media/Manager
+ */
+define(
+       [
+               'Core',                     'Dictionary',               'Dom/ChangeListener',      'Dom/Traverse',
+               'Dom/Util',                 'EventHandler',             'Language',                'List',
+               'Permission',               'Ui/Dialog',                'Ui/Notification',         'WoltLab/WCF/Controller/Clipboard',
+               'WoltLab/WCF/Media/Editor', 'WoltLab/WCF/Media/Upload', 'WoltLab/WCF/Media/Search'
+               
+
+       ],
+       function(
+               Core,                        Dictionary,                 DomChangeListener,         DomTraverse,
+               DomUtil,                     EventHandler,               Language,                  List,
+               Permission,                  UiDialog,                   UiNotification,            Clipboard,
+               MediaEditor,                 MediaUpload,                MediaSearch
+       )
+{
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function MediaManager() {
+               this._media = new Dictionary();
+               this._mediaData = new Dictionary();
+               this._mediaCache = null;
+               this._mediaManagerMediaList = null;
+               this._search = null;
+               
+               if (Permission.get('admin.content.cms.canManageMedia')) {
+                       this._mediaEditor = new MediaEditor(this);
+               }
+               
+               elById('mediaManagerButton').addEventListener('click', this._click.bind(this));
+               
+               DomChangeListener.add('WoltLab/WCF/Controller/Media/Manager', this._addButtonEventListeners.bind(this));
+       };
+       MediaManager.prototype = {
+               /**
+                * Adds click event listeners to media buttons.
+                */
+               _addButtonEventListeners: function() {
+                       if (!this._mediaManagerMediaList) return;
+                       
+                       var listItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI');
+                       for (var i = 0, length = listItems.length; i < length; i++) {
+                               var listItem = listItems[i];
+                               
+                               if (Permission.get('admin.content.cms.canManageMedia')) {
+                                       var editIcon = elByClass('jsMediaEditIcon', listItem)[0];
+                                       if (editIcon) {
+                                               editIcon.classList.remove('jsMediaEditIcon');
+                                               editIcon.addEventListener('click', this._editMedia.bind(this));
+                                       }
+                               }
+                               
+                               var insertIcon = elByClass('jsMediaInsertIcon', listItem)[0];
+                               if (insertIcon) {
+                                       insertIcon.classList.remove('jsMediaInsertIcon');
+                                       insertIcon.addEventListener('click', this._openInsertDialog.bind(this));
+                               }
+                       }
+               },
+               
+               /**
+                * Handles clicks on the media manager button.
+                * 
+                * @param       {object}        event   event object
+                */
+               _click: function(event) {
+                       event.preventDefault();
+                       
+                       UiDialog.open(this);
+               },
+               
+               /**
+                * Reacts to executed clipboard actions.
+                * 
+                * @param       {object<string, *>}     actionData      data of the executed clipboard action
+                */
+               _clipboardAction: function(actionData) {
+                       // only consider events if the action has been executed
+                       if (actionData.responseData === null) {
+                               return;
+                       }
+                       
+                       switch (actionData.data.actionName) {
+                               case 'com.woltlab.wcf.media.delete':
+                                       var mediaIds = actionData.responseData.objectIDs;
+                                       for (var i = 0, length = mediaIds.length; i < length; i++) {
+                                               this.removeMedia(~~mediaIds[i], true);
+                                       }
+                                       
+                                       UiNotification.show();
+                                       
+                                       break;
+                               case 'com.woltlab.wcf.media.insert':
+                                       // TODO
+                                       break;
+                       }
+               },
+               
+               /**
+                * Returns all data to setup the media manager dialog.
+                * 
+                * @return      {object}        dialog setup data
+                */
+               _dialogSetup: function() {
+                       return {
+                               id: 'mediaManager',
+                               options: {
+                                       title: Language.get('wcf.media.manager')
+                               },
+                               source: {
+                                       after: this._initDialog.bind(this),
+                                       data: {
+                                               actionName: 'getManagementDialog',
+                                               className: 'wcf\\data\\media\\MediaAction'
+                                       }
+                               }
+                       };
+               },
+               
+               /**
+                * Opens the media editor for a media file.
+                * 
+                * @param       {Event}         event           event object for clicks on edit icons
+                */
+               _editMedia: function(event) {
+                       if (!Permission.get('admin.content.cms.canManageMedia')) {
+                               throw new Error("You are not allowed to edit media files.");
+                       }
+                       
+                       UiDialog.close('mediaManager');
+                       
+                       this._mediaEditor.edit(this._mediaData.get(~~elData(event.currentTarget, 'object-id')));
+               },
+               
+               /**
+                * Re-opens the manager dialog after closing the editor dialog.
+                */
+               _editorClose: function() {
+                       UiDialog.open(this);
+               },
+               
+               /**
+                * Re-opens the manager dialog and updates the media data after
+                * successfully editing a media file.
+                * 
+                * @param       {object}        media           updated media file data
+                */
+               _editorSuccess: function(media) {
+                       UiDialog.open(this);
+                       
+                       this._mediaData.set(~~media.mediaID, media);
+                       
+                       var listItem = this._media.get(~~media.mediaID);
+                       var p = elByClass('mediaTitle', listItem)[0];
+                       if (media.isMultilingual) {
+                               p.textContent = media.title[LANGUAGE_ID] || media.filename;
+                       }
+                       else {
+                               p.textContent = media.title[media.languageID] || media.filename;
+                       }
+               },
+               
+               /**
+                * Initializes the dialog when first loaded.
+                * 
+                * @param       {string}        content         dialog content
+                * @param       {object}        data            AJAX request's response data
+                */
+               _initDialog: function(content, data) {
+                       // store media data locally
+                       var media = data.returnValues.media || { };
+                       for (var mediaId in media) {
+                               if (media.hasOwnProperty(mediaId)) {
+                                       this._mediaData.set(~~mediaId, media[mediaId]);
+                               }
+                       }
+                       
+                       this._mediaManagerMediaList = elById('mediaManagerMediaList');
+                       
+                       // store list items locally
+                       var listItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI');
+                       for (var i = 0, length = listItems.length; i < length; i++) {
+                               var listItem = listItems[i];
+                               
+                               this._media.set(~~elData(listItem, 'object-id'), listItem);
+                       }
+                       
+                       if (Permission.get('admin.content.cms.canManageMedia')) {
+                               new MediaUpload('mediaManagerMediaUploadButton', 'mediaManagerMediaList', {
+                                       mediaManager: this
+                               });
+                               
+                               Clipboard.setup({
+                                       hasMarkedItems: data.returnValues.hasMarkedItems ? true : false,
+                                       pageClassName: '*'
+                               });
+                               
+                               EventHandler.add('com.woltlab.wcf.clipboard', 'com.woltlab.wcf.media', this._clipboardAction.bind(this));
+                       }
+                       
+                       this._search = new MediaSearch(this);
+                       
+                       if (!listItems.length) {
+                               this._search.hideSearch();
+                               
+                               if (true) {
+                                       elById('mediaManagerMediaUploadButton').classList.remove('marginTop');
+                               }
+                       }
+               },
+               
+               _insertMedia: function() {
+                       // TODO
+               },
+               
+               _openInsertDialog: function(event) {
+                       var media = this._mediaData.get(~~elData(event.currentTarget, 'object-id'));
+                       
+                       // check if media file is image and has at least small thumbnail
+                       // to show insertion options
+                       if (media.isImage && media.smallThumbnailType) {
+                               UiDialog.close(this);
+                               var dialogId = 'mediaInsert' + media.mediaID;
+                               if (UiDialog.getDialog(dialogId)) {
+                                       UiDialog.openStatic(dialogId);
+                               }
+                               else {
+                                       var dialog = elCreate('div');
+                                       
+                                       var fieldset = elCreate('fieldset');
+                                       dialog.appendChild(fieldset);
+                                       
+                                       var dl = elCreate('dl');
+                                       fieldset.appendChild(dl);
+                                       
+                                       var dt = elCreate('dt');
+                                       dt.textContent = Language.get('wcf.media.insert.imageSize');
+                                       dl.appendChild(dt);
+                                       
+                                       var dd = elCreate('dd');
+                                       dl.appendChild(dd);
+                                       
+                                       var select = elCreate('select');
+                                       dd.appendChild(select);
+                                       
+                                       var sizes = ['small', 'medium', 'large'];
+                                       var size, option;
+                                       for (var i = 0, length = sizes.length; i < length; i++) {
+                                               size = sizes[i];
+                                               
+                                               if (media[size + 'ThumbnailType']) {
+                                                       option = elCreate('option');
+                                                       elAttr(option, 'value', size);
+                                                       option.textContent = Language.get('wcf.media.insert.imageSize.' + size, {
+                                                               height: media[size + 'ThumbnailHeight'],
+                                                               width: media[size + 'ThumbnailWidth']
+                                                       });
+                                                       select.appendChild(option);
+                                               }
+                                       }
+                                       
+                                       option = elCreate('option');
+                                       elAttr(option, 'value', 'original');
+                                       option.textContent = Language.get('wcf.media.insert.imageSize.original', {
+                                               height: media.height,
+                                               width: media.width
+                                       });
+                                       select.appendChild(option);
+                                       
+                                       var formSubmit = elCreate('div');
+                                       formSubmit.className = 'formSubmit';
+                                       dialog.appendChild(formSubmit);
+                                       
+                                       var submitButton = elCreate('button');
+                                       submitButton.className = 'buttonPrimary';
+                                       submitButton.textContent = Language.get('wcf.global.button.insert');
+                                       elData(submitButton, 'object-id', media.mediaID);
+                                       submitButton.addEventListener('click', this._insertMedia.bind(this));
+                                       formSubmit.appendChild(submitButton);
+                                       
+                                       UiDialog.open({
+                                               _dialogSetup: (function() {
+                                                       return {
+                                                               id: dialogId,
+                                                               options: {
+                                                                       onClose: this._editorClose.bind(this),
+                                                                       title: Language.get('wcf.media.insert')
+                                                               },
+                                                               source: dialog.outerHTML
+                                                       }
+                                               }).bind(this)
+                                       });
+                               }
+                       }
+                       else {
+                               // insert media
+                               // TODO
+                       }
+               },
+               
+               _setMedia: function(media, listItems) {
+                       if (Core.isPlainObject(media)) {
+                               this._media = Dictionary.fromObject(media);
+                       }
+                       else {
+                               this._media = media;
+                       }
+                       
+                       var info = DomTraverse.nextByClass(this._mediaManagerMediaList, 'info');
+                       
+                       if (this._media.size) {
+                               if (info) {
+                                       elHide(info);
+                               }
+                       }
+                       else {
+                               if (info === null) {
+                                       info = elCreate('p');
+                                       info.className = 'info';
+                                       info.textContent = Language.get('wcf.media.search.noResults');
+                               }
+                               
+                               elShow(info);
+                               DomUtil.insertAfter(info, this._mediaManagerMediaList);
+                       }
+                       
+                       var mediaListItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI');
+                       for (var i = 0, length = mediaListItems.length; i < length; i++) {
+                               var listItem = mediaListItems[i];
+                               
+                               if (!this._media.has(elData(listItem, 'object-id'))) {
+                                       elHide(listItem);
+                               }
+                               else {
+                                       elShow(listItem);
+                               }
+                       }
+                       
+                       DomChangeListener.trigger();
+                       
+                       Clipboard.reload();
+               },
+               
+               /**
+                * Adds a media file to the manager.
+                * 
+                * @param       {object}        media           data of the media file
+                * @param       {Element}       listItem        list item representing the file
+                */
+               addMedia: function(media, listItem) {
+                       if (!media.languageID) media.isMultilingual = 1;
+                       
+                       this._mediaData.set(~~media.mediaID, media);
+                       this._media.set(~~media.mediaID, listItem);
+                       
+                       if (this._media.size === 1) {
+                               this._search.showSearch();
+                               
+                               if (true) {
+                                       elById('mediaManagerMediaUploadButton').classList.add('marginTop');
+                               }
+                       }
+               },
+
+               /**
+                * Removes a media file.
+                *
+                * @param       {int}                   mediaId         id of the removed media file
+                * @param       {boolean|undefined}     checkCache      media file will also be removed from the local cache if true
+                */
+               removeMedia: function(mediaId, checkCache) {
+                       if (this._media.has(mediaId)) {
+                               // remove list item
+                               elRemove(this._media.get(mediaId));
+
+                               this._media.delete(mediaId);
+                               this._mediaData.delete(mediaId);
+                       }
+
+                       if (checkCache && this._mediaCache && this._mediaCache.has(mediaId)) {
+                               this._mediaCache.delete(mediaId);
+                       }
+               },
+               
+               /**
+                * Changes the displayed media to the previously displayed media.
+                */
+               resetMedia: function() {
+                       if (this._mediaCache !== null) {
+                               this._setMedia(this._mediaCache);
+                               
+                               this._mediaCache = null;
+                               
+                               this._search.resetSearch();
+                       }
+               },
+               
+               /**
+                * Sets the media files currently displayed.
+                * 
+                * @param       {object}        media           media data
+                * @param       {string}        template        
+                */
+               setMedia: function(media, template) {
+                       if (!this._mediaCache) {
+                               this._mediaCache = this._media;
+                       }
+                       
+                       var hasMedia = false;
+                       for (var mediaId in media) {
+                               if (media.hasOwnProperty(mediaId)) {
+                                       hasMedia = true;
+                               }
+                       }
+                       
+                       var newListItems = [];
+                       if (hasMedia) {
+                               var ul = elCreate('ul');
+                               ul.innerHTML = template;
+                               
+                               var listItems = DomTraverse.childrenByTag(ul, 'LI');
+                               for (var i = 0, length = listItems.length; i < length; i++) {
+                                       var listItem = listItems[i];
+                                       if (!this._mediaData.has(~~elData(listItem, 'object-id'))) {
+                                               this._mediaData.set(elData(listItem, 'object-id'), listItem);
+                                               
+                                               this._mediaManagerMediaList.appendChild(listItem);
+                                       }
+                               }
+                       }
+                       
+                       this._setMedia(media);
+               }
+       };
+       
+       return MediaManager;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Media/Search.js b/wcfsetup/install/files/js/WoltLab/WCF/Media/Search.js
new file mode 100644 (file)
index 0000000..c01c4c0
--- /dev/null
@@ -0,0 +1,188 @@
+/**
+ * Provides the media search for the media manager.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/Controller/Media/Search
+ */
+define(['Ajax', 'Dom/Traverse', 'Dom/Util', 'Language', 'Ui/SimpleDropdown'], function(Ajax, DomTraverse, DomUtil, Language, UiSimpleDropdown) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function MediaSearch(mediaManager) {
+               this._mediaManager = mediaManager;
+               this._searchMode = false;
+               this._fileType = 'all';
+               
+               this._input = elById('mediaManagerSearchField');
+               this._input.addEventListener('keypress', this._keyPress.bind(this));
+               
+               this._cancelButton = elById('mediaManagerSearchCancelButton');
+               this._cancelButton.addEventListener('click', this._cancelSearch.bind(this));
+               
+               this._fileTypes = DomTraverse.childrenBySel(UiSimpleDropdown.getDropdownMenu('mediaManagerSearch'), 'li:not(.dropdownDivider)');
+               var selectFileType = this._selectFileType.bind(this);
+               for (var i = 0, length = this._fileTypes.length; i < length; i++) {
+                       this._fileTypes[i].addEventListener('click', selectFileType);
+               }
+               
+               UiSimpleDropdown.registerCallback('mediaManagerSearch', this._updateFileTypeDropdown.bind(this));
+       };
+       MediaSearch.prototype = {
+               /**
+                * Returns the data for Ajax to setup the Ajax/Request object.
+                * 
+                * @return      {object}        setup data for Ajax/Request object
+                */
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       actionName: 'getSearchResultList',
+                                       className: 'wcf\\data\\media\\MediaAction',
+                                       interfaceName: 'wcf\\data\\ISearchAction'
+                               }
+                       };
+               },
+               
+               /**
+                * Handles successful AJAX requests.
+                * 
+                * @param       {object}        data    response data
+                */
+               _ajaxSuccess: function(data) {
+                       this._mediaManager.setMedia(data.returnValues.media || { }, data.returnValues.template || '');
+               },
+               
+               /**
+                * Cancels the search after clicking on the cancel search button.
+                */
+               _cancelSearch: function() {
+                       if (this._searchMode) {
+                               this._searchMode = false;
+                               
+                               this._mediaManager.resetMedia();
+                               this.resetSearch();
+                       }
+               },
+               
+               /**
+                * Handles the `[ENTER]` key to submit the form.
+                * 
+                * @param       {Event} event           event object
+                */
+               _keyPress: function(event) {
+                       // 13 = [ENTER]
+                       if (event.charCode === 13) {
+                               event.preventDefault();
+
+                               var innerInfo = DomTraverse.childByClass(this._input.parentNode, '.innerInfo');
+                               
+                               // TODO: treshold option?
+                               if (this._input.value.length >= 3) {
+                                       if (innerInfo) {
+                                               elHide(innerInfo);
+                                       }
+
+                                       this._search();
+                               }
+                               else {
+                                       if (innerInfo) {
+                                               elShow(innerInfo);
+                                       }
+                                       else {
+                                               innerInfo = elCreate('p');
+                                               innerInfo.className = 'innerInfo';
+                                               innerInfo.textContent = Language.get('wcf.media.search.info.searchStringTreshold');
+
+                                               this._input.parentNode.appendChild(innerInfo);
+                                       }
+                               }
+                       }
+               },
+               
+               /**
+                * Sends an AJAX request to fetch seach results.
+                */
+               _search: function() {
+                       this._searchMode = true;
+                       
+                       Ajax.api(this, {
+                               parameters: {
+                                       data: {
+                                               fileType: this._fileType,
+                                               // TODO: treshold option?
+                                               searchString: this._input.value.length > 3 ? this._input.value : ''
+                                       }
+                               }
+                       });
+               },
+
+               /**
+                * Selects a certain file type after clicking on it in the dropdown menu.
+                *
+                * @param       {Event} event
+         */
+               _selectFileType: function(event) {
+                       this._fileType = elData(event.currentTarget, 'file-type');
+                       
+                       this._updateDropdownButtonLabel();
+                       
+                       this._search();
+               },
+
+               /**
+                * Updates the label of the dropdown button based on the currently selected file type.
+         */
+               _updateDropdownButtonLabel: function() {
+                       var dropdown = UiSimpleDropdown.getDropdown('mediaManagerSearch');
+                       var buttonLabel = DomTraverse.childBySel(DomTraverse.childByClass(dropdown, 'dropdownToggle'), 'SPAN');
+                       
+                       if (this._fileType !== 'all') {
+                               buttonLabel.textContent = DomTraverse.childBySel(event.currentTarget, 'SPAN').textContent;
+                       }
+                       else {
+                               buttonLabel.textContent = Language.get('wcf.media.search.filetype');
+                       }
+               },
+
+               /**
+                * Updates the file type dropdown by correctly marking the currently selected file type.
+         */
+               _updateFileTypeDropdown: function() {
+                       for (var i = 0, length = this._fileTypes.length; i < length; i++) {
+                               var listItem = this._fileTypes[i];
+
+                               listItem.classList[elData(listItem, 'file-type') === this._fileType ? 'add' : 'remove']('active');
+                       }
+               },
+               
+               /**
+                * Hides the media search.
+                */
+               hideSearch: function() {
+                       elHide(elById('mediaManagerSearch'));
+               },
+
+               /**
+                * Resets the media search.
+                */
+               resetSearch: function() {
+                       this._input.value = '';
+                       this._fileType = 'all';
+                       
+                       this._updateDropdownButtonLabel();
+               },
+               
+               /**
+                * Shows the media search.
+                */
+               showSearch: function() {
+                       elShow(elById('mediaManagerSearch'));
+               }
+       };
+       
+       return MediaSearch;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Media/Upload.js b/wcfsetup/install/files/js/WoltLab/WCF/Media/Upload.js
new file mode 100644 (file)
index 0000000..14a8226
--- /dev/null
@@ -0,0 +1,194 @@
+/**
+ * Uploads media files.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/Controller/Media/Upload
+ */
+define(
+       [
+               'Core',                'Dom/ChangeListener', 'Dom/Traverse', 'Dom/Util',
+               'EventHandler',        'Language',           'Permission',   'Upload',
+               'WoltLab/WCF/File/Util'
+       ],
+       function(
+               Core,                   DomChangeListener,    DomTraverse,    DomUtil,
+               EventHandler,           Language,             Permission,     Upload,
+               FileUtil
+       )
+{
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function MediaUpload(buttonContainerId, targetId, options) {
+               options = options || {};
+               
+               this._mediaManager = null;
+               if (options.mediaManager) {
+                       this._mediaManager = options.mediaManager;
+                       delete options.mediaManager;
+               }
+               
+               Upload.call(this, buttonContainerId, targetId, Core.extend({
+                       className: 'wcf\\data\\media\\MediaAction',
+                       multiple: this._mediaManager ? true : false,
+                       singleFileRequests: true
+               }, options));
+       };
+       Core.inherit(MediaUpload, Upload, {
+               /**
+                * @see WoltLab/WCF/Upload#_createFileElement
+                */
+               _createFileElement: function(file) {
+                       var fileElement;
+                       if (this._target.nodeName === 'OL' || this._target.nodeName === 'UL') {
+                               fileElement = elCreate('li');
+                       }
+                       else {
+                               fileElement = elCreate('p');
+                       }
+                       
+                       var thumbnail = elCreate('div');
+                       thumbnail.className = 'mediaThumbnail';
+                       fileElement.appendChild(thumbnail);
+                       
+                       var fileIcon = elCreate('span');
+                       fileIcon.className = 'icon icon96 fa-spinner';
+                       thumbnail.appendChild(fileIcon);
+                       
+                       var mediaInformation = elCreate('div');
+                       mediaInformation.className = 'mediaInformation';
+                       fileElement.appendChild(mediaInformation);
+                       
+                       var p = elCreate('p');
+                       p.className = 'mediaTitle';
+                       p.textContent = file.name;
+                       mediaInformation.appendChild(p);
+                       
+                       var progress = elCreate('progress');
+                       elAttr(progress, 'max', 100);
+                       mediaInformation.appendChild(progress);
+                       
+                       DomUtil.prepend(fileElement, this._target);
+                       
+                       DomChangeListener.trigger();
+                       
+                       return fileElement;
+               },
+               
+               /**
+                * @see WoltLab/WCF/Upload#_success
+                */
+               _success: function(uploadId, data) {
+                       var files = this._fileElements[uploadId];
+                       
+                       for (var i = 0, length = files.length; i < length; i++) {
+                               var file = files[i];
+                               var internalFileId = elData(file, 'internal-file-id');
+                               var media = data.returnValues.media[internalFileId];
+                               
+                               if (media) {
+                                       var fileIcon = DomTraverse.childByTag(DomTraverse.childByClass(file, 'mediaThumbnail'), 'SPAN');
+                                       if (media.tinyThumbnailType) {
+                                               var parentNode = fileIcon.parentNode;
+                                               elRemove(fileIcon);
+                                               
+                                               var img = elCreate('img');
+                                               elAttr(img, 'src', media.tinyThumbnailLink);
+                                               elAttr(img, 'alt', '');
+                                               img.style.setProperty('width', '96px');
+                                               img.style.setProperty('height', '96px');
+                                               parentNode.appendChild(img);
+                                       }
+                                       else {
+                                               fileIcon.classList.remove('fa-spinner');
+                                               fileIcon.classList.add(FileUtil.getIconClassByMimeType(media.fileType));
+                                       }
+                                       
+                                       file.className = 'jsClipboardObject';
+                                       elData(file, 'object-id', media.mediaID);
+                                       
+                                       var mediaInformation = DomTraverse.childByClass(file, 'mediaInformation');
+                                       
+                                       elRemove(DomTraverse.childByTag(mediaInformation, 'PROGRESS'));
+                                       
+                                       if (this._mediaManager) {
+                                               var buttonGroupNavigation = elCreate('nav');
+                                               buttonGroupNavigation.className = 'buttonGroupNavigation';
+                                               mediaInformation.parentNode.appendChild(buttonGroupNavigation);
+                                               
+                                               var smallButtons = elCreate('ul');
+                                               smallButtons.className = 'smallButtons buttonGroup';
+                                               buttonGroupNavigation.appendChild(smallButtons);
+                                               
+                                               var listItem = elCreate('li');
+                                               smallButtons.appendChild(listItem);
+                                               
+                                               var checkbox = elCreate('input');
+                                               checkbox.className = 'jsClipboardItem jsMediaCheckbox';
+                                               elAttr(checkbox, 'type', 'checkbox');
+                                               elData(checkbox, 'object-id', media.mediaID);
+                                               listItem.appendChild(checkbox);
+                                               
+                                               if (Permission.get('admin.content.cms.canManageMedia')) {
+                                                       listItem = elCreate('li');
+                                                       smallButtons.appendChild(listItem);
+                                                       
+                                                       var a = elCreate('a');
+                                                       listItem.appendChild(a);
+                                                       
+                                                       var icon = elCreate('span');
+                                                       icon.className = 'icon icon16 fa-pencil jsTooltip jsMediaEditIcon';
+                                                       elData(icon, 'object-id', media.mediaID);
+                                                       elAttr(icon, 'title', Language.get('wcf.global.button.edit'));
+                                                       a.appendChild(icon);
+                                                       
+                                                       listItem = elCreate('li');
+                                                       smallButtons.appendChild(listItem);
+                                                       
+                                                       a = elCreate('a');
+                                                       listItem.appendChild(a);
+                                                       
+                                                       icon = elCreate('span');
+                                                       icon.className = 'icon icon16 fa-times jsTooltip jsMediaDeleteIcon';
+                                                       elData(icon, 'object-id', media.mediaID);
+                                                       elAttr(icon, 'title', Language.get('wcf.global.button.delete'));
+                                                       a.appendChild(icon);
+                                               }
+                                               
+                                               listItem = elCreate('li');
+                                               smallButtons.appendChild(listItem);
+                                               
+                                               var a = elCreate('a');
+                                               listItem.appendChild(a);
+                                               
+                                               var icon = elCreate('span');
+                                               icon.className = 'icon icon16 fa-plus jsTooltip jsMediaInsertIcon';
+                                               elData(icon, 'object-id', media.mediaID);
+                                               elAttr(icon, 'title', Language.get('wcf.media.button.insert'));
+                                               a.appendChild(icon);
+                                               
+                                               this._mediaManager.resetMedia();
+                                               this._mediaManager.addMedia(media, file);
+                                       }
+                                       
+                                       DomChangeListener.trigger();
+                               }
+                               else {
+                                       // error: TODO
+                               }
+                       }
+                       
+                       EventHandler.fire('com.woltlab.wcf.media.upload', 'success', {
+                               files: files,
+                               media: data.returnValues.media,
+                               upload: this
+                       });
+               }
+       });
+       
+       return MediaUpload;
+});
index 2c0382ff0b41ddc5653241d9f5ccb2c265f47dd6..6916667bcfe8af324584cc19b0cf4ded129d7b71 100644 (file)
@@ -361,6 +361,16 @@ define(
                                var content = elCreate('div');
                                content.innerHTML = html;
                                
+                               var scripts = elBySelAll('script', content);
+                               for (var i = 0, length = scripts.length; i < length; i++) {
+                                       var script = scripts[i];
+                                       var newScript = elCreate('script');
+                                       newScript.innerHTML = script.innerHTML;
+                                       content.appendChild(newScript);
+                                       
+                                       script.parentNode.removeChild(script);
+                               }
+                               
                                data.content.appendChild(content);
                        }
                        
index 07f0ba0fe4fe6c9e795eb54c70ac896916614042..d2387557bc8c9785df7fed44f336ba5d5ac34d6b 100644 (file)
@@ -102,9 +102,9 @@ define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util'], fu
                        else {
                                var p = elCreate('p');
                                p.appendChild(progress);
-                               
+
                                this._target.appendChild(p);
-                               
+
                                return p;
                        }
                },
@@ -298,6 +298,7 @@ define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util'], fu
                        
                        formData.append('actionName', this._options.action);
                        formData.append('className', this._options.className);
+                       formData.append('interfaceName', 'wcf\\data\\IUploadAction');
                        var additionalParameters = this._getParameters();
                        for (var name in additionalParameters) {
                                formData.append('parameters[' + name + ']', additionalParameters[name]);
@@ -307,6 +308,7 @@ define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util'], fu
                                data: formData,
                                contentType: false,
                                failure: this._failure.bind(this, uploadId),
+                               silent: true,
                                success: this._success.bind(this, uploadId),
                                uploadProgress: this._progress.bind(this, uploadId),
                                url: this._options.url
index e8fb712ee49397398cfb1f360342cf9540895fa4..d57e30a0e2fe150bc6137e571e8155a8ff547bf9 100644 (file)
@@ -26,6 +26,7 @@ requirejs.config({
                        'Language': 'WoltLab/WCF/Language',
                        'List': 'WoltLab/WCF/List',
                        'ObjectMap': 'WoltLab/WCF/ObjectMap',
+                       'Permission': 'WoltLab/WCF/Permission',
                        'StringUtil': 'WoltLab/WCF/StringUtil',
                        'Ui/Alignment': 'WoltLab/WCF/Ui/Alignment',
                        'Ui/CloseOverlay': 'WoltLab/WCF/Ui/CloseOverlay',
@@ -34,7 +35,8 @@ requirejs.config({
                        'Ui/Notification': 'WoltLab/WCF/Ui/Notification',
                        'Ui/ReusableDropdown': 'WoltLab/WCF/Ui/Dropdown/Reusable',
                        'Ui/SimpleDropdown': 'WoltLab/WCF/Ui/Dropdown/Simple',
-                       'Ui/TabMenu': 'WoltLab/WCF/Ui/TabMenu'
+                       'Ui/TabMenu': 'WoltLab/WCF/Ui/TabMenu',
+                       'Upload': 'WoltLab/WCF/Upload'
                }
        }
 });
diff --git a/wcfsetup/install/files/lib/acp/form/MediaEditForm.class.php b/wcfsetup/install/files/lib/acp/form/MediaEditForm.class.php
new file mode 100644 (file)
index 0000000..d8a19dc
--- /dev/null
@@ -0,0 +1,188 @@
+<?php
+namespace wcf\acp\form;
+use wcf\data\media\Media;
+use wcf\data\media\MediaAction;
+use wcf\form\AbstractForm;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\UserInputException;
+use wcf\system\language\I18nHandler;
+use wcf\system\language\LanguageFactory;
+use wcf\system\WCF;
+use wcf\util\ArrayUtil;
+
+/**
+ * Shows the form to edit a media file.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage acp.form
+ * @category   Community Framework
+ * @since      2.2
+ */
+class MediaEditForm extends AbstractForm {
+       /**
+        * @inheritdoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.cms.media.list';
+       
+       /**
+        * is 1 if media data is multilingual
+        * @var integer
+        */
+       public $isMultilingual = 0;
+       
+       /**
+        * id of the selected language
+        * @var integer
+        */
+       public $languageID = 0;
+       
+       /**
+        * edited media
+        * @var Media
+        */
+       public $media = null;
+       
+       /**
+        * id of the edited media
+        * @var integer
+        */
+       public $mediaID = 0;
+       
+       /**
+        * @inheritdoc
+        */
+       public $neededPermissions = ['admin.content.cms.canManageMedia'];
+       
+       /**
+        * @inheritdoc
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               I18nHandler::getInstance()->assignVariables();
+               
+               WCF::getTPL()->assign([
+                       'action' => 'edit',
+                       'isMultilingual' => $this->isMultilingual,
+                       'languages' => LanguageFactory::getInstance()->getLanguages(),
+                       'languageID' => $this->languageID,
+                       'media' => $this->media
+               ]);
+       }
+       
+       /**
+        * @inheritdoc
+        */
+       public function readData() {
+               parent::readData();
+               
+               if (empty($_POST)) {
+                       $this->isMultilingual = $this->media->isMultilingual;
+                       if (!$this->isMultilingual && !$this->media->languageID) {
+                               $this->isMultilingual = 1;
+                       }
+                       
+                       if ($this->media->languageID) {
+                               $this->languageID = $this->media->languageID;
+                       }
+                       else {
+                               $this->languageID = WCF::getUser()->languageID;
+                       }
+                       
+                       $contentData = $this->media->getI18nData();
+                       if (!empty($contentData)) {
+                               I18nHandler::getInstance()->setValues('altText', $contentData['altText']);
+                               I18nHandler::getInstance()->setValues('caption', $contentData['caption']);
+                               I18nHandler::getInstance()->setValues('title', $contentData['title']);
+                       }
+               }
+               
+               if (!$this->languageID) {
+                       $this->languageID = WCF::getUser()->languageID;
+               }
+       }
+       
+       /**
+        * @inheritdoc
+        */
+       public function readFormParameters() {
+               parent::readFormParameters();
+               
+               if (isset($_POST['isMultilingual'])) $this->isMultilingual = intval($_POST['isMultilingual']);
+               if (!$this->isMultilingual) {
+                       if (isset($_POST['languageID'])) $this->languageID = intval($_POST['languageID']);
+               }
+               I18nHandler::getInstance()->readValues();
+       }
+       
+       /**
+        * @inheritdoc
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               if (isset($_REQUEST['id'])) $this->mediaID = intval($_REQUEST['id']);
+               
+               $this->media = new Media($this->mediaID);
+               if (!$this->media->mediaID) {
+                       throw new IllegalLinkException();
+               }
+               
+               I18nHandler::getInstance()->register('title');
+               I18nHandler::getInstance()->register('caption');
+               I18nHandler::getInstance()->register('altText');
+       }
+       
+       /**
+        * @inheritdoc
+        */
+       public function save() {
+               parent::save();
+               
+               $this->objectAction = new MediaAction([$this->media], 'update', [
+                       'data' => [
+                               'isMultilingual' => $this->isMultilingual,
+                               'languageID' => $this->languageID ?: null
+                       ],
+                       'altText' => I18nHandler::getInstance()->getValues('altText'),
+                       'caption' => I18nHandler::getInstance()->getValues('caption'),
+                       'title' => I18nHandler::getInstance()->getValues('title')
+               ]);
+               $this->objectAction->executeAction();
+               
+               $this->saved();
+               
+               WCF::getTPL()->assign('success', true);
+       }
+       
+       /**
+        * @inheritdoc
+        * @throws      UserInputException
+        */
+       public function validate() {
+               parent::validate();
+               
+               if ($this->languageID && !LanguageFactory::getInstance()->getLanguage($this->languageID)) {
+                       throw new UserInputException('languageID');
+               }
+               
+               foreach (['title', 'caption', 'altText'] as $i18nData) {
+                       if (!I18nHandler::getInstance()->validateValue($i18nData, $this->isMultilingual? true : false, false)) {
+                               if ($this->isMultilingual) {
+                                       // in contrast to I18nHandler::validateValues(), we allow all fields to be empty
+                                       if (empty(ArrayUtil::trim(I18nHandler::getInstance()->getValues($i18nData)))) {
+                                               continue;
+                                       }
+                                       
+                                       throw new UserInputException($i18nData, 'multilingual');
+                               }
+                               else {
+                                       throw new UserInputException($i18nData);
+                               }
+                       }
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/acp/page/MediaAddPage.class.php b/wcfsetup/install/files/lib/acp/page/MediaAddPage.class.php
new file mode 100644 (file)
index 0000000..1e6627c
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+namespace wcf\acp\page;
+use wcf\page\AbstractPage;
+use wcf\system\WCF;
+
+/**
+ * Shows the page to upload a media file.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage acp.page
+ * @category   Community Framework
+ * @since      2.2
+ */
+class MediaAddPage extends AbstractPage {
+       /**
+        * @inheritdoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.cms.media.add';
+       
+       /**
+        * @inheritdoc
+        */
+       public $neededPermissions = ['admin.content.cms.canManageMedia'];
+       
+       /**
+        * @inheritdoc
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign('action', 'add');
+       }
+}
diff --git a/wcfsetup/install/files/lib/acp/page/MediaListPage.class.php b/wcfsetup/install/files/lib/acp/page/MediaListPage.class.php
new file mode 100644 (file)
index 0000000..3e62846
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+namespace wcf\acp\page;
+use wcf\data\media\ViewableMediaList;
+use wcf\page\SortablePage;
+
+/**
+ * Shows the list of media entries.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage acp.page
+ * @category   Community Framework
+ * @since      2.2
+ */
+class MediaListPage extends SortablePage {
+       /**
+        * @inheritdoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.media.list';
+       
+       /**
+        * @inheritdoc
+        */
+       public $defaultSortField = 'uploadTime';
+       
+       /**
+        * @inheritdoc
+        */
+       public $defaultSortOrder = 'DESC';
+       
+       /**
+        * @inheritdoc
+        */
+       public $neededPermissions = ['admin.content.cms.canManageMedia'];
+       
+       /**
+        * @inheritdoc
+        */
+       public $objectListClassName = ViewableMediaList::class;
+       
+       /**
+        * @inheritdoc
+        */
+       public $validSortFields = [
+               'filename',
+               'filesize',
+               'mediaID',
+               'title',
+               'uploadTime'
+       ];
+       
+       /**
+        * @inheritdoc
+        */
+       protected function readObjects() {
+               if ($this->sqlOrderBy && $this->sortField == 'mediaID') {
+                       $this->sqlOrderBy = 'media.'.$this->sortField.' '.$this->sortOrder;
+               }
+               
+               parent::readObjects();
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/IFile.class.php b/wcfsetup/install/files/lib/data/IFile.class.php
new file mode 100644 (file)
index 0000000..febb09e
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+namespace wcf\data;
+
+/**
+ * Every database object representing a file should implement this interface.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data
+ * @category   Community Framework
+ * @since      2.2
+ */
+interface IFile extends IStorableObject {
+       /**
+        * Returns the physical location of the file.
+        * 
+        * @return      string
+        */
+       public function getLocation();
+}
diff --git a/wcfsetup/install/files/lib/data/IThumbnailFile.class.php b/wcfsetup/install/files/lib/data/IThumbnailFile.class.php
new file mode 100644 (file)
index 0000000..0b4f27b
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+namespace wcf\data;
+
+/**
+ * Every database object representing a file supporting thumbnails should implement
+ * this interface.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data
+ * @category   Community Framework
+ * @since      2.2
+ */
+interface IThumbnailFile extends IFile {
+       /**
+        * Returns the link to the thumbnail file with the given size.
+        * 
+        * @param       string          $size
+        * @return      sting
+        */
+       public function getThumbnailLink($size);
+       
+       /**
+        * Returns the physical location of the thumbnail file with the given size.
+        * 
+        * @param       string          $size
+        * @return      sting
+        */
+       public function getThumbnailLocation($size);
+       
+       /**
+        * Returns the available thumbnail sizes.
+        * 
+        * @return      array
+        */
+       public static function getThumbnailSizes();
+}
diff --git a/wcfsetup/install/files/lib/data/IUploadAction.class.php b/wcfsetup/install/files/lib/data/IUploadAction.class.php
new file mode 100644 (file)
index 0000000..d507c47
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+namespace wcf\data;
+
+/**
+ * Every database object action supporting file upload has to implement this interface.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data
+ * @category   Community Framework
+ * @since      2.2
+ */
+interface IUploadAction {
+       /**
+        * Validates the 'upload' action.
+        */
+       public function validateUpload();
+       
+       /**
+        * Saves uploaded files and returns the data of the uploaded files.
+        * 
+        * @return      array
+        */
+       public function upload();
+}
index 2eba56e2c30221b90a0fda090ea52583db15b391..1582704b7a55dfd78697dc4ea11d1b196eec03b6 100644 (file)
@@ -2,9 +2,11 @@
 namespace wcf\data\attachment;
 use wcf\data\object\type\ObjectTypeCache;
 use wcf\data\DatabaseObject;
+use wcf\data\IThumbnailFile;
 use wcf\system\request\IRouteController;
 use wcf\system\WCF;
 use wcf\util\FileUtil;
+use wcf\system\request\LinkHandler;
 
 /**
  * Represents an attachment.
@@ -16,14 +18,14 @@ use wcf\util\FileUtil;
  * @subpackage data.attachment
  * @category   Community Framework
  */
-class Attachment extends DatabaseObject implements IRouteController {
+class Attachment extends DatabaseObject implements IRouteController, IThumbnailFile {
        /**
-        * @see \wcf\data\DatabaseObject::$databaseTableName
+        * @inheritdoc
         */
        protected static $databaseTableName = 'attachment';
        
        /**
-        * @see \wcf\data\DatabaseObject::$databaseTableIndexName
+        * @inheritdoc
         */
        protected static $databaseTableIndexName = 'attachmentID';
        
@@ -35,9 +37,9 @@ class Attachment extends DatabaseObject implements IRouteController {
        
        /**
         * user permissions for attachment access
-        * @var array<boolean>
+        * @var boolean[]
         */
-       protected $permissions = array();
+       protected $permissions = [];
        
        /**
         * Returns true if a user has the permission to download this attachment.
@@ -87,7 +89,7 @@ class Attachment extends DatabaseObject implements IRouteController {
                                $objectType = ObjectTypeCache::getInstance()->getObjectType($this->objectTypeID);
                                $processor = $objectType->getProcessor();
                                if ($processor !== null) {
-                                       $this->permissions[$permission] = call_user_func(array($processor, $permission), $this->objectID);
+                                       $this->permissions[$permission] = call_user_func([$processor, $permission], $this->objectID);
                                }
                        }
                }
@@ -98,16 +100,14 @@ class Attachment extends DatabaseObject implements IRouteController {
        /**
         * Sets the permissions for attachment access.
         * 
-        * @param       array<boolean>          $permissions
+        * @param       boolean[]               $permissions
         */
        public function setPermissions(array $permissions) {
                $this->permissions = $permissions;
        }
        
        /**
-        * Returns the physical location of this attachment.
-        * 
-        * @return      string
+        * @inheritdoc
         */
        public function getLocation() {
                return self::getStorage() . substr($this->fileHash, 0, 2) . '/' . ($this->attachmentID) . '-' . $this->fileHash;
@@ -119,20 +119,37 @@ class Attachment extends DatabaseObject implements IRouteController {
         * @return      string
         */
        public function getTinyThumbnailLocation() {
-               return self::getStorage() . substr($this->fileHash, 0, 2) . '/' . ($this->attachmentID) . '-tiny-' . $this->fileHash;
+               return $this->getThumbnailLocation('tiny');
        }
        
        /**
-        * Returns the physical location of the standard thumbnail.
-        * 
-        * @return      string
+        * @inheritdoc
         */
-       public function getThumbnailLocation() {
+       public function getThumbnailLocation($size = '') {
+               if ($size == 'tiny') {
+                       return self::getStorage() . substr($this->fileHash, 0, 2) . '/' . ($this->attachmentID) . '-tiny-' . $this->fileHash;
+               }
+               
                return self::getStorage() . substr($this->fileHash, 0, 2) . '/' . ($this->attachmentID) . '-thumbnail-' . $this->fileHash;
        }
        
        /**
-        * @see \wcf\system\request\IRouteController::getTitle()
+        * @inheritdoc
+        */
+       public function getThumbnailLink($size = '') {
+               $parameters = [
+                       'id' => $this->attachmentID
+               ];
+               
+               if ($size == 'tiny') {
+                       $parameters['tiny'] = 1;
+               }
+               
+               return LinkHandler::getInstance()->getLink('Attachment', $parameters);
+       }
+       
+       /**
+        * @inheritdoc
         */
        public function getTitle() {
                return $this->filename;
@@ -203,4 +220,23 @@ class Attachment extends DatabaseObject implements IRouteController {
                
                return WCF_DIR . 'attachments/';
        }
+       
+       /**
+        * @inheritdoc
+        */
+       public static function getThumbnailSizes() {
+               return [
+                       'tiny' => [
+                               'height' => 144,
+                               'retainDimensions' => false,
+                               'width' => 144
+                       ],
+                       // standard thumbnail size
+                       '' => [
+                               'height' => ATTACHMENT_THUMBNAIL_HEIGHT,
+                               'retainDimensions' => ATTACHMENT_RETAIN_DIMENSIONS,
+                               'width' => ATTACHMENT_THUMBNAIL_WIDTH
+                       ]
+               ];
+       }
 }
index c0aed2f4a1db8307bad6bc1a22d98bdc729fb62f..4c9d7369cf0b6b5e576f68715c5d3df6fb9e134d 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 namespace wcf\data\attachment;
+use wcf\data\ISortableAction;
+use wcf\data\IUploadAction;
 use wcf\data\object\type\ObjectTypeCache;
 use wcf\data\AbstractDatabaseObjectAction;
 use wcf\system\attachment\AttachmentHandler;
@@ -9,6 +11,7 @@ use wcf\system\exception\PermissionDeniedException;
 use wcf\system\exception\UserInputException;
 use wcf\system\image\ImageHandler;
 use wcf\system\request\LinkHandler;
+use wcf\system\upload\DefaultUploadFileSaveStrategy;
 use wcf\system\upload\DefaultUploadFileValidationStrategy;
 use wcf\system\WCF;
 use wcf\util\ArrayUtil;
@@ -25,20 +28,20 @@ use wcf\util\FileUtil;
  * @subpackage data.attachment
  * @category   Community Framework
  */
-class AttachmentAction extends AbstractDatabaseObjectAction {
+class AttachmentAction extends AbstractDatabaseObjectAction implements ISortableAction, IUploadAction {
        /**
-        * @see \wcf\data\AbstractDatabaseObjectAction::$allowGuestAccess
+        * @inheritdoc
         */
-       protected $allowGuestAccess = array('delete', 'updatePosition', 'upload');
+       protected $allowGuestAccess = ['delete', 'updatePosition', 'upload'];
        
        /**
-        * @see \wcf\data\AbstractDatabaseObjectAction::$className
+        * @inheritdoc
         */
-       protected $className = 'wcf\data\attachment\AttachmentEditor';
+       protected $className = AttachmentEditor::class;
        
        /**
         * current attachment object, used to communicate with event listeners
-        * @var \wcf\data\attachment\Attachment
+        * @var Attachment
         */
        public $eventAttachment = null;
        
@@ -46,10 +49,10 @@ class AttachmentAction extends AbstractDatabaseObjectAction {
         * current data, used to communicate with event listeners.
         * @var array
         */
-       public $eventData = array();
+       public $eventData = [];
        
        /**
-        * Validates the delete action.
+        * @inheritdoc
         */
        public function validateDelete() {
                // read objects
@@ -74,7 +77,7 @@ class AttachmentAction extends AbstractDatabaseObjectAction {
        }
        
        /**
-        * Validates the upload action.
+        * @inheritdoc
         */
        public function validateUpload() {
                // IE<10 fallback
@@ -108,10 +111,10 @@ class AttachmentAction extends AbstractDatabaseObjectAction {
                // check max count of uploads
                $handler = new AttachmentHandler($this->parameters['objectType'], intval($this->parameters['objectID']), $this->parameters['tmpHash']);
                if ($handler->count() + count($this->parameters['__files']->getFiles()) > $processor->getMaxCount()) {
-                       throw new UserInputException('files', 'exceededQuota', array(
+                       throw new UserInputException('files', 'exceededQuota', [
                                'current' => $handler->count(),
                                'quota' => $processor->getMaxCount()
-                       ));
+                       ]);
                }
                
                // check max filesize, allowed file extensions etc.
@@ -119,141 +122,30 @@ class AttachmentAction extends AbstractDatabaseObjectAction {
        }
        
        /**
-        * Handles uploaded attachments.
+        * @inheritdoc
         */
        public function upload() {
                // get object type
                $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.attachment.objectType', $this->parameters['objectType']);
                
                // save files
-               $thumbnails = $attachments = $failedUploads = array();
-               $files = $this->parameters['__files']->getFiles();
-               foreach ($files as $file) {
-                       if ($file->getValidationErrorType()) {
-                               $failedUploads[] = $file;
-                               continue;
-                       }
-                       
-                       $data = array(
-                               'objectTypeID' => $objectType->objectTypeID,
-                               'objectID' => intval($this->parameters['objectID']),
-                               'userID' => (WCF::getUser()->userID ?: null),
-                               'tmpHash' => (!$this->parameters['objectID'] ? $this->parameters['tmpHash'] : ''),
-                               'filename' => $file->getFilename(),
-                               'filesize' => $file->getFilesize(),
-                               'fileType' => $file->getMimeType(),
-                               'fileHash' => sha1_file($file->getLocation()),
-                               'uploadTime' => TIME_NOW
-                       );
-                       
-                       // get image data
-                       if (($imageData = $file->getImageData()) !== null) {
-                               $data['width'] = $imageData['width'];
-                               $data['height'] = $imageData['height'];
-                               $data['fileType'] = $imageData['mimeType'];
-                               
-                               if (preg_match('~^image/(gif|jpe?g|png)$~i', $data['fileType'])) {
-                                       $data['isImage'] = 1;
-                               }
-                       }
-                       
-                       // create attachment
-                       $attachment = AttachmentEditor::create($data);
-                       
-                       // check attachment directory
-                       // and create subdirectory if necessary
-                       $dir = dirname($attachment->getLocation());
-                       if (!@file_exists($dir)) {
-                               FileUtil::makePath($dir);
-                       }
-                       
-                       // move uploaded file
-                       if (@move_uploaded_file($file->getLocation(), $attachment->getLocation())) {
-                               if ($attachment->isImage) {
-                                       $thumbnails[] = $attachment;
-                                       
-                                       // rotate image based on the exif data
-                                       $neededMemory = $attachment->width * $attachment->height * ($attachment->fileType == 'image/png' ? 4 : 3) * 2.1;
-                                       if (FileUtil::getMemoryLimit() == -1 || FileUtil::getMemoryLimit() > (memory_get_usage() + $neededMemory)) {
-                                               $exifData = ExifUtil::getExifData($attachment->getLocation());
-                                               if (!empty($exifData)) {
-                                                       $orientation = ExifUtil::getOrientation($exifData);
-                                                       if ($orientation != ExifUtil::ORIENTATION_ORIGINAL) {
-                                                               $adapter = ImageHandler::getInstance()->getAdapter();
-                                                               $adapter->loadFile($attachment->getLocation());
-                                                               
-                                                               $newImage = null;
-                                                               switch ($orientation) {
-                                                                       case ExifUtil::ORIENTATION_180_ROTATE:
-                                                                               $newImage = $adapter->rotate(180);
-                                                                       break;
-                                                                       
-                                                                       case ExifUtil::ORIENTATION_90_ROTATE:
-                                                                               $newImage = $adapter->rotate(90);
-                                                                       break;
-                                                                       
-                                                                       case ExifUtil::ORIENTATION_270_ROTATE:
-                                                                               $newImage = $adapter->rotate(270);
-                                                                       break;
-                                                                       
-                                                                       case ExifUtil::ORIENTATION_HORIZONTAL_FLIP:
-                                                                       case ExifUtil::ORIENTATION_VERTICAL_FLIP:
-                                                                       case ExifUtil::ORIENTATION_VERTICAL_FLIP_270_ROTATE:
-                                                                       case ExifUtil::ORIENTATION_HORIZONTAL_FLIP_270_ROTATE:
-                                                                               // unsupported
-                                                                       break;
-                                                               }
-                                                               
-                                                               if ($newImage !== null) {
-                                                                       $adapter->load($newImage, $adapter->getType());
-                                                               }
-                                                               
-                                                               $adapter->writeImage($attachment->getLocation());
-                                                               
-                                                               if ($newImage !== null) {
-                                                                       // update width, height and filesize of the attachment
-                                                                       if ($orientation == ExifUtil::ORIENTATION_90_ROTATE || $orientation == ExifUtil::ORIENTATION_270_ROTATE) {
-                                                                               $attachmentEditor = new AttachmentEditor($attachment);
-                                                                               $attachmentEditor->update(array(
-                                                                                       'height' => $attachment->width,
-                                                                                       'width' => $attachment->height,
-                                                                                       'filesize' => filesize($attachment->getLocation())
-                                                                               ));
-                                                                       }
-                                                               }
-                                                       }
-                                               }
-                                       }
-                               }
-                               else {
-                                       // check whether we can create thumbnails for this file
-                                       $this->eventAttachment = $attachment;
-                                       $this->eventData = array('hasThumbnail' => false);
-                                       EventHandler::getInstance()->fireAction($this, 'checkThumbnail');
-                                       if ($this->eventData['hasThumbnail']) $thumbnails[] = $attachment;
-                               }
-                               $attachments[$file->getInternalFileID()] = $attachment;
-                       }
-                       else {
-                               // moving failed; delete attachment
-                               $editor = new AttachmentEditor($attachment);
-                               $editor->delete();
-                       }
-               }
+               $saveStrategy = new DefaultUploadFileSaveStrategy(self::class, [
+                       'generateThumbnails' => ATTACHMENT_ENABLE_THUMBNAILS,
+                       'rotateImages' => true
+               ], [
+                       'objectID' => intval($this->parameters['objectID']),
+                       'objectTypeID' => $objectType->objectTypeID,
+                       'tmpHash' => (!$this->parameters['objectID'] ? $this->parameters['tmpHash'] : '')
+               ]);
                
-               // generate thumbnails
-               if (ATTACHMENT_ENABLE_THUMBNAILS) {
-                       if (!empty($thumbnails)) {
-                               $action = new AttachmentAction($thumbnails, 'generateThumbnails');
-                               $action->executeAction();
-                       }
-               }
+               $this->parameters['__files']->saveFiles($saveStrategy);
+               $attachments = $saveStrategy->getObjects();
                
                // return result
-               $result = array('attachments' => array(), 'errors' => array());
+               $result = ['attachments' => [], 'errors' => []];
                if (!empty($attachments)) {
                        // get attachment ids
-                       $attachmentIDs = $attachmentToFileID = array();
+                       $attachmentIDs = $attachmentToFileID = [];
                        foreach ($attachments as $internalFileID => $attachment) {
                                $attachmentIDs[] = $attachment->attachmentID;
                                $attachmentToFileID[$attachment->attachmentID] = $internalFileID;
@@ -265,27 +157,30 @@ class AttachmentAction extends AbstractDatabaseObjectAction {
                        $attachmentList->readObjects();
                        
                        foreach ($attachmentList as $attachment) {
-                               $result['attachments'][$attachmentToFileID[$attachment->attachmentID]] = array(
+                               $result['attachments'][$attachmentToFileID[$attachment->attachmentID]] = [
                                        'filename' => $attachment->filename,
                                        'filesize' => $attachment->filesize,
                                        'formattedFilesize' => FileUtil::formatFilesize($attachment->filesize),
                                        'isImage' => $attachment->isImage,
                                        'attachmentID' => $attachment->attachmentID,
-                                       'tinyURL' => ($attachment->tinyThumbnailType ? LinkHandler::getInstance()->getLink('Attachment', array('object' => $attachment), 'tiny=1') : ''),
-                                       'thumbnailURL' => ($attachment->thumbnailType ? LinkHandler::getInstance()->getLink('Attachment', array('object' => $attachment), 'thumbnail=1') : ''),
-                                       'url' => LinkHandler::getInstance()->getLink('Attachment', array('object' => $attachment)),
+                                       'tinyURL' => ($attachment->tinyThumbnailType ? LinkHandler::getInstance()->getLink('Attachment', ['object' => $attachment], 'tiny=1') : ''),
+                                       'thumbnailURL' => ($attachment->thumbnailType ? LinkHandler::getInstance()->getLink('Attachment', ['object' => $attachment], 'thumbnail=1') : ''),
+                                       'url' => LinkHandler::getInstance()->getLink('Attachment', ['object' => $attachment]),
                                        'height' => $attachment->height,
                                        'width' => $attachment->width
-                               );
+                               ];
                        }
                }
                
-               foreach ($failedUploads as $failedUpload) {
-                       $result['errors'][$failedUpload->getInternalFileID()] = array(
-                               'filename' => $failedUpload->getFilename(),
-                               'filesize' => $failedUpload->getFilesize(),
-                               'errorType' => $failedUpload->getValidationErrorType()
-                       );
+               $files = $this->parameters['__files']->getFiles();
+               foreach ($files as $file) {
+                       if ($file->getValidationErrorType()) {
+                               $result['errors'][$file->getInternalFileID()] = [
+                                       'filename' => $file->getFilename(),
+                                       'filesize' => $file->getFilesize(),
+                                       'errorType' => $file->getValidationErrorType()
+                               ];
+                       }
                }
                
                return $result;
@@ -299,11 +194,13 @@ class AttachmentAction extends AbstractDatabaseObjectAction {
                        $this->readObjects();
                }
                
+               $saveStrategy = new DefaultUploadFileSaveStrategy(self::class);
+               
                foreach ($this->objects as $attachment) {
                        if (!$attachment->isImage) {
                                // create thumbnails for every file that isn't an image
                                $this->eventAttachment = $attachment;
-                               $this->eventData = array();
+                               $this->eventData = [];
                                
                                EventHandler::getInstance()->fireAction($this, 'generateThumbnail');
                                
@@ -314,68 +211,12 @@ class AttachmentAction extends AbstractDatabaseObjectAction {
                                continue;
                        }
                        
-                       if ($attachment->width <= 144 && $attachment->height < 144) {
-                               continue; // image smaller than thumbnail size; skip
-                       }
-                       
-                       $adapter = ImageHandler::getInstance()->getAdapter();
-                       
-                       // check memory limit
-                       $neededMemory = $attachment->width * $attachment->height * ($attachment->fileType == 'image/png' ? 4 : 3) * 2.1;
-                       if (FileUtil::getMemoryLimit() != -1 && FileUtil::getMemoryLimit() < (memory_get_usage() + $neededMemory)) {
-                               continue;
-                       }
-                       
-                       $adapter->loadFile($attachment->getLocation());
-                       $updateData = array();
-                       // remove / reset old thumbnails
-                       if ($attachment->tinyThumbnailType) {
-                               @unlink($attachment->getTinyThumbnailLocation());
-                               $updateData['tinyThumbnailType'] = '';
-                               $updateData['tinyThumbnailSize'] = 0;
-                               $updateData['tinyThumbnailWidth'] = 0;
-                               $updateData['tinyThumbnailHeight'] = 0;
-                       }
-                       if ($attachment->thumbnailType) {
-                               @unlink($attachment->getThumbnailLocation());
-                               $updateData['thumbnailType'] = '';
-                               $updateData['thumbnailSize'] = 0;
-                               $updateData['thumbnailWidth'] = 0;
-                               $updateData['thumbnailHeight'] = 0;
-                       }
-                       
-                       // create tiny thumbnail
-                       $tinyThumbnailLocation = $attachment->getTinyThumbnailLocation();
-                       $thumbnail = $adapter->createThumbnail(144, 144, false);
-                       $adapter->writeImage($thumbnail, $tinyThumbnailLocation);
-                       if (file_exists($tinyThumbnailLocation) && ($imageData = @getImageSize($tinyThumbnailLocation)) !== false) {
-                               $updateData['tinyThumbnailType'] = $imageData['mime'];
-                               $updateData['tinyThumbnailSize'] = @filesize($tinyThumbnailLocation);
-                               $updateData['tinyThumbnailWidth'] = $imageData[0];
-                               $updateData['tinyThumbnailHeight'] = $imageData[1];
-                       }
-                       
-                       // create standard thumbnail
-                       if ($attachment->width > ATTACHMENT_THUMBNAIL_WIDTH || $attachment->height > ATTACHMENT_THUMBNAIL_HEIGHT) {
-                               $thumbnailLocation = $attachment->getThumbnailLocation();
-                               $thumbnail = $adapter->createThumbnail(ATTACHMENT_THUMBNAIL_WIDTH, ATTACHMENT_THUMBNAIL_HEIGHT, ATTACHMENT_RETAIN_DIMENSIONS);
-                               $adapter->writeImage($thumbnail, $thumbnailLocation);
-                               if (file_exists($thumbnailLocation) && ($imageData = @getImageSize($thumbnailLocation)) !== false) {
-                                       $updateData['thumbnailType'] = $imageData['mime'];
-                                       $updateData['thumbnailSize'] = @filesize($thumbnailLocation);
-                                       $updateData['thumbnailWidth'] = $imageData[0];
-                                       $updateData['thumbnailHeight'] = $imageData[1];
-                               }
-                       }
-                       
-                       if (!empty($updateData)) {
-                               $attachment->update($updateData);
-                       }
+                       $saveStrategy->generateThumbnails($attachment->getDecoratedObject());
                }
        }
        
        /**
-        * Validates parameters to update the attachments show order.
+        * @inheritdoc
         */
        public function validateUpdatePosition() {
                $this->readInteger('objectID', true);
@@ -406,16 +247,16 @@ class AttachmentAction extends AbstractDatabaseObjectAction {
                $this->parameters['attachmentIDs'] = ArrayUtil::toIntegerArray($this->parameters['attachmentIDs']);
                
                // check attachment ids
-               $attachmentIDs = array();
+               $attachmentIDs = [];
                $conditions = new PreparedStatementConditionBuilder();
-               $conditions->add("attachmentID IN (?)", array($this->parameters['attachmentIDs']));
-               $conditions->add("objectTypeID = ?", array($objectType->objectTypeID));
+               $conditions->add("attachmentID IN (?)", [$this->parameters['attachmentIDs']]);
+               $conditions->add("objectTypeID = ?", [$objectType->objectTypeID]);
                
                if (!empty($this->parameters['objectID'])) {
-                       $conditions->add("objectID = ?", array($this->parameters['objectID']));
+                       $conditions->add("objectID = ?", [$this->parameters['objectID']]);
                }
                else {
-                       $conditions->add("tmpHash = ?", array($this->parameters['tmpHash']));
+                       $conditions->add("tmpHash = ?", [$this->parameters['tmpHash']]);
                }
                
                $sql = "SELECT  attachmentID
@@ -435,7 +276,7 @@ class AttachmentAction extends AbstractDatabaseObjectAction {
        }
        
        /**
-        * Updates the attachments show order.
+        * @inheritdoc
         */
        public function updatePosition() {
                $sql = "UPDATE  wcf".WCF_N."_attachment
@@ -446,10 +287,10 @@ class AttachmentAction extends AbstractDatabaseObjectAction {
                WCF::getDB()->beginTransaction();
                $showOrder = 1;
                foreach ($this->parameters['attachmentIDs'] as $attachmentID) {
-                       $statement->execute(array(
+                       $statement->execute([
                                $showOrder++,
                                $attachmentID
-                       ));
+                       ]);
                }
                WCF::getDB()->commitTransaction();
        }
@@ -462,13 +303,13 @@ class AttachmentAction extends AbstractDatabaseObjectAction {
                $targetObjectType = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.attachment.objectType', $this->parameters['targetObjectType']);
                
                $attachmentList = new AttachmentList();
-               $attachmentList->getConditionBuilder()->add("attachment.objectTypeID = ?", array($sourceObjectType->objectTypeID));
-               $attachmentList->getConditionBuilder()->add("attachment.objectID = ?", array($this->parameters['sourceObjectID']));
+               $attachmentList->getConditionBuilder()->add("attachment.objectTypeID = ?", [$sourceObjectType->objectTypeID]);
+               $attachmentList->getConditionBuilder()->add("attachment.objectID = ?", [$this->parameters['sourceObjectID']]);
                $attachmentList->readObjects();
                
-               $newAttachmentIDs = array();
+               $newAttachmentIDs = [];
                foreach ($attachmentList as $attachment) {
-                       $newAttachment = AttachmentEditor::create(array(
+                       $newAttachment = AttachmentEditor::create([
                                'objectTypeID' => $targetObjectType->objectTypeID,
                                'objectID' => $this->parameters['targetObjectID'],
                                'userID' => $attachment->userID,
@@ -491,7 +332,7 @@ class AttachmentAction extends AbstractDatabaseObjectAction {
                                'lastDownloadTime' => $attachment->lastDownloadTime,
                                'uploadTime' => $attachment->uploadTime,
                                'showOrder' => $attachment->showOrder
-                       ));
+                       ]);
                        
                        // copy attachment
                        @copy($attachment->getLocation(), $newAttachment->getLocation());
@@ -506,8 +347,8 @@ class AttachmentAction extends AbstractDatabaseObjectAction {
                        $newAttachmentIDs[$attachment->attachmentID] = $newAttachment->attachmentID;
                }
                
-               return array(
+               return [
                        'attachmentIDs' => $newAttachmentIDs
-               );
+               ];
        }
 }
diff --git a/wcfsetup/install/files/lib/data/media/Media.class.php b/wcfsetup/install/files/lib/data/media/Media.class.php
new file mode 100644 (file)
index 0000000..cb6f2d3
--- /dev/null
@@ -0,0 +1,148 @@
+<?php
+namespace wcf\data\media;
+use wcf\data\DatabaseObject;
+use wcf\data\IThumbnailFile;
+use wcf\system\request\IRouteController;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+
+/**
+ * Represents a madia file.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.media
+ * @category   Community Framework
+ * @since      2.2
+ */
+class Media extends DatabaseObject implements IRouteController, IThumbnailFile {
+       /**
+        * i18n media data grouped by language id for all language
+        * @var string[][]
+        */
+       protected $i18nData = null;
+       
+       /**
+        * @inheritdoc
+        */
+       protected static $databaseTableName = 'media';
+       
+       /**
+        * @inheritdoc
+        */
+       protected static $databaseTableIndexName = 'mediaID';
+
+       /**
+        * data of the different thumbnail sizes
+        * @var array
+        */
+       protected static $thumbnailSizes = [
+               'tiny' => [
+                       'height' => 144,
+                       'retainDimensions' => false,
+                       'width' => 144
+               ],
+               'small' => [
+                       'height' => MEDIA_SMALL_THUMBNAIL_HEIGHT,
+                       'retainDimensions' => MEDIA_SMALL_THUMBNAIL_RETAIN_DIMENSIONS,
+                       'width' => MEDIA_SMALL_THUMBNAIL_WIDTH
+               ],
+               'medium' => [
+                       'height' => MEDIA_MEDIUM_THUMBNAIL_HEIGHT,
+                       'retainDimensions' => MEDIA_MEDIUM_THUMBNAIL_RETAIN_DIMENSIONS,
+                       'width' => MEDIA_MEDIUM_THUMBNAIL_WIDTH
+               ],
+               'large' => [
+                       'height' => MEDIA_LARGE_THUMBNAIL_HEIGHT,
+                       'retainDimensions' => MEDIA_LARGE_THUMBNAIL_RETAIN_DIMENSIONS,
+                       'width' => MEDIA_LARGE_THUMBNAIL_WIDTH
+               ]
+       ];
+       
+       /**
+        * @inheritcoc
+        */
+       public function getLocation() {
+               return self::getStorage().substr($this->fileHash, 0, 2).'/'.$this->mediaID.'-'.$this->fileHash;
+       }
+       
+       /**
+        * @inheritcoc
+        */
+       public function getThumbnailLink($size) {
+               if (!isset(self::$thumbnailSizes[$size])) {
+                       throw new SystemException("Unknown thumbnail size '".$size."'");
+               }
+               
+               return LinkHandler::getInstance()->getLink('Media', [
+                       'forceFrontend' => true,
+                       'object' => $this,
+                       'thumbnail' => $size
+               ]);
+       }
+       
+       /**
+        * @inheritcoc
+        */
+       public function getThumbnailLocation($size) {
+               if (!isset(self::$thumbnailSizes[$size])) {
+                       throw new SystemException("Unknown thumbnail size '".$size."'");
+               }
+               
+               return self::getStorage().substr($this->fileHash, 0, 2).'/'.$this->mediaID.'-'.$size.'-'.$this->fileHash;
+       }
+       
+       /**
+        * @inheritcoc
+        */
+       public function getTitle() {
+               return $this->filename;
+       }
+       
+       /**
+        * Returns the i18n media data grouped by language id for all language.
+        * 
+        * @return      string[][]
+        */
+       public function getI18nData() {
+               if ($this->i18nData === null) {
+                       $this->i18nData = [
+                               'altText' => [],
+                               'caption' => [],
+                               'title' => []
+                       ];
+                       
+                       $sql = "SELECT  *
+                               FROM    wcf".WCF_N."_media_content
+                               WHERE   mediaID = ?";
+                       $statement = WCF::getDB()->prepareStatement($sql);
+                       $statement->execute([$this->mediaID]);
+                       
+                       while ($row = $statement->fetchArray()) {
+                               $this->i18nData['altText'][$row['languageID']] = $row['altText'];
+                               $this->i18nData['caption'][$row['languageID']] = $row['caption'];
+                               $this->i18nData['title'][$row['languageID']] = $row['title'];
+                       }
+               }
+               
+               return $this->i18nData;
+       }
+       
+       /**
+        * Returns the storage path of the media files.
+        * 
+        * @return      string
+        */
+       public static function getStorage() {
+               return WCF_DIR.'media/';
+       }
+       
+       /**
+        * @inheritcoc
+        */
+       public static function getThumbnailSizes() {
+               return static::$thumbnailSizes;
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/media/MediaAction.class.php b/wcfsetup/install/files/lib/data/media/MediaAction.class.php
new file mode 100644 (file)
index 0000000..c70430c
--- /dev/null
@@ -0,0 +1,461 @@
+<?php
+namespace wcf\data\media;
+use wcf\data\AbstractDatabaseObjectAction;
+use wcf\data\ISearchAction;
+use wcf\data\IUploadAction;
+use wcf\system\clipboard\ClipboardHandler;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\language\I18nHandler;
+use wcf\system\language\LanguageFactory;
+use wcf\system\request\Linkhandler;
+use wcf\system\upload\DefaultUploadFileSaveStrategy;
+use wcf\system\WCF;
+use wcf\util\FileUtil;
+
+/**
+ * Executes madia file-related actions.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.media
+ * @category   Community Framework
+ * @since      2.2
+ */
+class MediaAction extends AbstractDatabaseObjectAction implements ISearchAction, IUploadAction {
+       /**
+        * condition builder for searched media file type
+        * @var PreparedStatementConditionBuilder
+        */
+       public $fileTypeConditionBuilder = null;
+       
+       /**
+        * @inheritdoc
+        */
+       public function validateUpload() {
+               WCF::getSession()->checkPermissions(['admin.content.cms.canManageMedia']);
+               
+               // TODO
+       }
+       
+       /**
+        * @inheritdoc
+        */
+       public function upload() {
+               // save files
+               $saveStrategy = new DefaultUploadFileSaveStrategy(self::class, [
+                       'generateThumbnails' => true,
+                       'rotateImages' => true
+               ], [
+                       'username' => WCF::getUser()->username
+               ]);
+               
+               $this->parameters['__files']->saveFiles($saveStrategy);
+               $mediaFiles = $saveStrategy->getObjects();
+               
+               $result = [
+                       'errors' => [],
+                       'media' => []
+               ];
+               
+               if (!empty($mediaFiles)) {
+                       // get attachment ids
+                       $mediaIDs = $mediaToFileID = array();
+                       foreach ($mediaFiles as $internalFileID => $media) {
+                               $mediaIDs[] = $media->mediaID;
+                               $mediaToFileID[$media->mediaID] = $internalFileID;
+                       }
+                       
+                       // get attachments from database (check thumbnail status)
+                       $mediaList = new MediaList();
+                       $mediaList->setObjectIDs($mediaIDs);
+                       $mediaList->readObjects();
+                       
+                       foreach ($mediaList as $media) {
+                               $result['media'][$mediaToFileID[$media->mediaID]] = $this->getMediaData($media);
+                       }
+               }
+               
+               $files = $this->parameters['__files']->getFiles();
+               foreach ($files as $file) {
+                       if ($file->getValidationErrorType()) {
+                               $result['errors'][$file->getInternalFileID()] = array(
+                                       'filename' => $file->getFilename(),
+                                       'filesize' => $file->getFilesize(),
+                                       'errorType' => $file->getValidationErrorType()
+                               );
+                       }
+               }
+               
+               return $result;
+       }
+       
+       /**
+        * Returns the data of the media file to be returned by AJAX requests.
+        * 
+        * @param       object          $media          media files whose data will be returned
+        * @return      string[]
+        */
+       protected function getMediaData($media) {
+               return [
+                       'altText' => $media instanceof ViewableMedia ? $media->altText : [],
+                       'caption' => $media instanceof ViewableMedia ? $media->caption : [],
+                       'fileHash' => $media->fileHash,
+                       'filename' => $media->filename,
+                       'filesize' => $media->filesize,
+                       'formattedFilesize' => FileUtil::formatFilesize($media->filesize),
+                       'fileType' => $media->fileType,
+                       'height' => $media->height,
+                       'languageID' => $media->languageID,
+                       'isImage' => $media->isImage,
+                       'isMultilingual' => $media->isMultilingual,
+                       'largeThumbnailHeight' => $media->largeThumbnailHeight,
+                       'largeThumbnailLink' => $media->largeThumbnailType ? $media->getThumbnailLink('large') : '',
+                       'largeThumbnailType' => $media->largeThumbnailType,
+                       'largeThumbnailWidth' => $media->largeThumbnailWidth,
+                       'mediaID' => $media->mediaID,
+                       'mediumThumbnailHeight' => $media->mediumThumbnailHeight,
+                       'mediumThumbnailLink' => $media->mediumThumbnailType ? $media->getThumbnailLink('medium') : '',
+                       'mediumThumbnailType' => $media->mediumThumbnailType,
+                       'mediumThumbnailWidth' => $media->mediumThumbnailWidth,
+                       'smallThumbnailHeight' => $media->smallThumbnailHeight,
+                       'smallThumbnailLink' => $media->smallThumbnailType ? $media->getThumbnailLink('small') : '',
+                       'smallThumbnailType' => $media->smallThumbnailType,
+                       'smallThumbnailWidth' => $media->smallThumbnailWidth,
+                       'tinyThumbnailHeight' => $media->tinyThumbnailHeight,
+                       'tinyThumbnailLink' => $media->tinyThumbnailType ? $media->getThumbnailLink('tiny') : '',
+                       'tinyThumbnailType' => $media->tinyThumbnailType,
+                       'tinyThumbnailWidth' => $media->tinyThumbnailWidth,
+                       'title' => $media instanceof ViewableMedia ? $media->title : [],
+                       'uploadTime' => $media->uploadTime,
+                       'userID' => $media->userID,
+                       'userLink' => $media->userID ? LinkHandler::getInstance()->getLink('User', [
+                               'id' => $media->userID,
+                               'title' => $media->username
+                       ]) : '',
+                       'username' => $media->username,
+                       'width' => $media->width
+               ];
+       }
+       
+       /**
+        * Validates the 'getManagementDialog' action.
+        */
+       public function validateGetManagementDialog() {
+               if (!WCF::getSession()->getPermission('admin.content.cms.canManageMedia') && !WCF::getSession()->getPermission('admin.content.cms.canUseMedia')) {
+                       throw new PermissionDeniedException();
+               }
+       }
+       
+       /**
+        * Returns the dialog to manage media.
+        * 
+        * @return      string[]
+        */
+       public function getManagementDialog() {
+               $mediaList = new ViewableMediaList();
+               $mediaList->readObjects();
+               
+               return [
+                       'hasMarkedItems' => ClipboardHandler::getInstance()->hasMarkedItems(ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.media')),
+                       'media' => $this->getI18nMediaData($mediaList),
+                       'template' => WCF::getTPL()->fetch('mediaManager', 'wcf', [
+                               'mediaList' => $mediaList
+                       ])
+               ];
+       }
+       
+       /**
+        * Returns the complete i18n data of the media files in the given list.
+        * 
+        * @param       MediaList       $mediaList
+        * @return      array
+        */
+       protected function getI18nMediaData(MediaList $mediaList) {
+               $conditionBuilder = new PreparedStatementConditionBuilder();
+               $conditionBuilder->add('mediaID IN (?)', [$mediaList->getObjectIDs()]);
+               
+               $sql = "SELECT  *
+                       FROM    wcf".WCF_N."_media_content
+                       ".$conditionBuilder;
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute($conditionBuilder->getParameters());
+               
+               $mediaData = [];
+               while ($row = $statement->fetchArray()) {
+                       if (!isset($mediaData[$row['mediaID']])) {
+                               $mediaData[$row['mediaID']] = [
+                                       'altText' => [],
+                                       'caption' => [],
+                                       'title' => [],
+                               ];
+                       }
+                       
+                       $mediaData[$row['mediaID']]['altText'][intval($row['languageID'])] = $row['altText'];
+                       $mediaData[$row['mediaID']]['caption'][intval($row['languageID'])] = $row['caption'];
+                       $mediaData[$row['mediaID']]['title'][intval($row['languageID'])] = $row['title'];
+               }
+               
+               $i18nMediaData = [];
+               foreach ($mediaList as $media) {
+                       if (!isset($mediaData[$media->mediaID])) {
+                               $mediaData[$media->mediaID] = [];
+                       }
+                       
+                       $i18nMediaData[$media->mediaID] = array_merge($this->getMediaData($media), $mediaData[$media->mediaID]);
+               }
+               
+               return $i18nMediaData;
+       }
+       
+       /**
+        * Validates the 'getEditorDialog' action.
+        */
+       public function validateGetEditorDialog() {
+               WCF::getSession()->checkPermissions(['admin.content.cms.canManageMedia']);
+       }
+       
+       /**
+        * Returns the template for the media editor.
+        * 
+        * @return      string[]
+        */
+       public function getEditorDialog() {
+               I18nHandler::getInstance()->register('title');
+               I18nHandler::getInstance()->register('caption');
+               I18nHandler::getInstance()->register('altText');
+               I18nHandler::getInstance()->assignVariables();
+               
+               return [
+                       'template' => WCF::getTPL()->fetch('mediaEditor', 'wcf', [
+                               'languageID' => WCF::getUser()->languageID,
+                               'languages' => LanguageFactory::getInstance()->getLanguages()
+                       ])
+               ];
+       }
+       
+       /**
+        * @inheritdoc
+        */
+       public function validateUpdate() {
+               WCF::getSession()->checkPermissions(['admin.content.cms.canManageMedia']);
+               
+               if (empty($this->objects)) {
+                       $this->readObjects();
+                       
+                       if (empty($this->objects)) {
+                               throw new UserInputException('objectIDs');
+                       }
+               }
+               
+               // TODO: check data
+       }
+       
+       /**
+        * @inheritdoc
+        */
+       public function update() {
+               if (empty($this->objects)) {
+                       $this->readObjects();
+               }
+               
+               parent::update();
+               
+               if (count($this->objects) == 1 && (isset($this->parameters['title']) || isset($this->parameters['caption']) || isset($this->parameters['altText']))) {
+                       $media = reset($this->objects);
+                       
+                       $isMultilingual = $media->isMultilingual;
+                       if (isset($this->parameters['data']['isMultilingual'])) {
+                               $isMultilingual = $this->parameters['data']['isMultilingual'];
+                       }
+                       
+                       $sql = "DELETE FROM     wcf".WCF_N."_media_content
+                               WHERE           mediaID = ?";
+                       $statement = WCF::getDB()->prepareStatement($sql);
+                       $statement->execute([$media->mediaID]);
+                       
+                       $sql = "INSERT INTO     wcf".WCF_N."_media_content
+                                               (mediaID, languageID, title, caption, altText)
+                               VALUES          (?, ?, ?, ?, ?)";
+                       $statement = WCF::getDB()->prepareStatement($sql);
+                       
+                       if (!$isMultilingual) {
+                               $languageID = $media->languageID;
+                               if (isset($this->parameters['data']['languageID'])) {
+                                       $languageID = $this->parameters['data']['languageID'];
+                               }
+                               $statement->execute([
+                                       $media->mediaID,
+                                       $languageID,
+                                       isset($this->parameters['title'][$languageID]) ? $this->parameters['title'][$languageID] : '',
+                                       isset($this->parameters['caption'][$languageID]) ? $this->parameters['caption'][$languageID] : '',
+                                       isset($this->parameters['altText'][$languageID]) ? $this->parameters['altText'][$languageID] : ''
+                               ]);
+                       }
+                       else {
+                               $languages = LanguageFactory::getInstance()->getLanguages();
+                               foreach ($languages as $language) {
+                                       $title = $caption = $altText = '';
+                                       foreach (['title', 'caption', 'altText'] as $type) {
+                                               if (isset($this->parameters[$type])) {
+                                                       if (is_array($this->parameters[$type])) {
+                                                               if (isset($this->parameters[$type][$language->languageID])) {
+                                                                       $$type = $this->parameters[$type][$language->languageID];
+                                                               }
+                                                       }
+                                                       else {
+                                                               $$type = $this->parameters[$type];
+                                                       }
+                                               }
+                                       }
+                                       
+                                       $statement->execute([
+                                               $media->mediaID,
+                                               $language->languageID,
+                                               $title,
+                                               $caption,
+                                               $altText
+                                       ]);
+                               }
+                       }
+               }
+       }
+       
+       /**
+        * @inheritdoc
+        */
+       public function validateGetSearchResultList() {
+               if (!WCF::getSession()->getPermission('admin.content.cms.canManageMedia') && !WCF::getSession()->getPermission('admin.content.cms.canUseMedia')) {
+                       throw new PermissionDeniedException();
+               }
+               
+               $this->readString('searchString', true, 'data');
+               $this->readString('fileType', true, 'data');
+               
+               if (!$this->parameters['data']['searchString'] && !$this->parameters['data']['fileType']) {
+                       throw new UserInputException('searchString');
+               }
+               
+               $this->fileTypeConditionBuilder = new PreparedStatementConditionBuilder(false);
+               switch ($this->parameters['data']['fileType']) {
+                       case 'other':
+                               $this->fileTypeConditionBuilder->add('media.fileType NOT LIKE ?', ['image/%']);
+                               $this->fileTypeConditionBuilder->add('media.fileType <> ?', ['application/pdf']);
+                               $this->fileTypeConditionBuilder->add('media.fileType NOT LIKE ?', ['text/%']);
+                       break;
+                       
+                       case 'image':
+                               $this->fileTypeConditionBuilder->add('media.fileType LIKE ?', ['image/%']);
+                       break;
+                       
+                       case 'pdf':
+                               $this->fileTypeConditionBuilder->add('media.fileType = ?', ['application/pdf']);
+                       break;
+                       
+                       case 'text':
+                               $this->fileTypeConditionBuilder->add('media.fileType LIKE ?', ['text/%']);
+                       break;
+               }
+       }
+       
+       /**
+        * @inheritdoc
+        */
+       public function getSearchResultList() {
+               $searchString = '%'.addcslashes($this->parameters['data']['searchString'], '_%').'%';
+               
+               $sql = "SELECT          media.mediaID
+                       FROM            wcf".WCF_N."_media media
+                       LEFT JOIN       wcf".WCF_N."_media_content media_content
+                       ON              (media_content.mediaID = media.mediaID)
+                       WHERE           (media_content.title LIKE ?
+                                       OR media_content.caption LIKE ?
+                                       OR media_content.altText LIKE ?
+                                       OR media.filename LIKE ?)";
+               if (!empty($this->fileTypeConditionBuilder->__toString())) {
+                       $sql .= " AND ".$this->fileTypeConditionBuilder;
+               }
+               $statement = WCF::getDB()->prepareStatement($sql, 0, 10);
+               $statement->execute(array_merge([
+                       $searchString,
+                       $searchString,
+                       $searchString,
+                       $searchString
+               ], $this->fileTypeConditionBuilder->getParameters()));
+               
+               $mediaIDs = [];
+               while ($mediaID = $statement->fetchColumn()) {
+                       $mediaIDs[] = $mediaID;
+               }
+               
+               if (empty($mediaIDs)) {
+                       return [
+                               'template' => WCF::getLanguage()->getDynamicVariable('wcf.media.search.noResults')
+                       ];
+               }
+               
+               $mediaList = new ViewableMediaList();
+               $mediaList->setObjectIDs($mediaIDs);
+               $mediaList->readObjects();
+               
+               return [
+                       'media' => $this->getI18nMediaData($mediaList),
+                       'template' => WCF::getTPL()->fetch('mediaListItems', 'wcf', [
+                               'mediaList' => $mediaList
+                       ])
+               ];
+       }
+       
+       /**
+        * @inheritdoc
+        */
+       public function validateDelete() {
+               WCF::getSession()->checkPermissions(['admin.content.cms.canManageMedia']);
+               
+               if (empty($this->objects)) {
+                       $this->readObjects();
+                       
+                       if (empty($this->objects)) {
+                               throw new UserInputException('objectIDs');
+                       }
+               }
+       }
+       
+       /**
+        * @inheritdoc
+        */
+       public function delete() {
+               if (empty($this->objects)) {
+                       $this->readObjects();
+               }
+               
+               /** @var MediaEditor $mediaEditor */
+               foreach ($this->objects as $mediaEditor) {
+                       $mediaEditor->deleteFiles();
+               }
+               
+               parent::delete();
+               
+               $this->unmarkItems();
+       }
+       
+       /**
+        * Unmarks the media files with the given ids. If no media ids are given,
+        * all media files currently loaded are unmarked.
+        * 
+        * @param       integer[]       $mediaIDs       ids of the media files to be unmarked
+        */
+       protected function unmarkItems(array $mediaIDs = []) {
+               if (empty($mediaIDs)) {
+                       foreach ($this->objects as $media) {
+                               $mediaIDs[] = $media->mediaID;
+                       }
+               }
+               
+               if (!empty($mediaIDs)) {
+                       ClipboardHandler::getInstance()->unmark($mediaIDs, ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.media'));
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/media/MediaEditor.class.php b/wcfsetup/install/files/lib/data/media/MediaEditor.class.php
new file mode 100644 (file)
index 0000000..e707364
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+namespace wcf\data\media;
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Procides functions to edit media files.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.media
+ * @category   Community Framework
+ * @since      2.2
+ */
+class MediaEditor extends DatabaseObjectEditor {
+       /**
+        * @inheritdoc
+        */
+       protected static $baseClass = Media::class;
+
+       /**
+        * Deletes the physical files of the media file.
+        */
+       public function deleteFiles() {
+               @unlink($this->getLocation());
+
+               // delete thumbnails
+               if ($this->isImage) {
+                       foreach (Media::getThumbnailSizes() as $size => $data) {
+                               @unlink($this->getThumbnailLocation($size));
+                       }
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/media/MediaList.class.php b/wcfsetup/install/files/lib/data/media/MediaList.class.php
new file mode 100644 (file)
index 0000000..ab8ca3f
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+namespace wcf\data\media;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of madia files.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.media
+ * @category   Community Framework
+ * @since      2.2
+ */
+class MediaList extends DatabaseObjectList {
+       /**
+        * @inheritdoc
+        */
+       public $className = Media::class;
+}
diff --git a/wcfsetup/install/files/lib/data/media/ViewableMedia.class.php b/wcfsetup/install/files/lib/data/media/ViewableMedia.class.php
new file mode 100644 (file)
index 0000000..b015593
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+namespace wcf\data\media;
+use wcf\data\DatabaseObjectDecorator;
+use wcf\util\StringUtil;
+use wcf\util\FileUtil;
+
+/**
+ * Represents a viewable madia file.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.media
+ * @category   Community Framework
+ * @since      2.2
+ */
+class ViewableMedia extends DatabaseObjectDecorator {
+       /**
+        * @inheritdoc
+        */
+       protected static $baseClass = Media::class;
+       
+       /**
+        * Returns a tag to display the media element.
+        * 
+        * @param       string          $size
+        * @return      string
+        */
+       public function getElementTag($size) {
+               // todo: validate $size
+               if ($this->isImage && $this->tinyThumbnailType) {
+                       return '<img src="'.$this->getThumbnailLink('tiny').'" alt="" style="width: '.$size.'px; height: '.$size.'px;" />';
+               }
+               
+               return '<span class="icon icon'.$size.' '.FileUtil::getIconClassByMimeType($this->fileType).'"></span>';
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/media/ViewableMediaList.class.php b/wcfsetup/install/files/lib/data/media/ViewableMediaList.class.php
new file mode 100644 (file)
index 0000000..d444be8
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+namespace wcf\data\media;
+use wcf\system\WCF;
+
+/**
+ * Represents a list of viewable madia files.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.media
+ * @category   Community Framework
+ * @since      2.2
+ */
+class ViewableMediaList extends MediaList {
+       /**
+        * @inheritdoc
+        */
+       public $decoratorClassName = ViewableMedia::class;
+       
+       /**
+        * @inheritdoc
+        */
+       public function __construct() {
+               parent::__construct();
+               
+               // fetch content data
+               $this->sqlSelects .= "media_content.*";
+               $this->sqlJoins .= " LEFT JOIN wcf".WCF_N."_media_content media_content ON (media_content.mediaID = media.mediaID AND media_content.languageID = COALESCE(media.languageID, ".WCF::getUser()->languageID."))";
+       }
+}
index 5a6fc67fdfce680afa221b575fd35159b4fc7f8f..568b84ccf454d30765d40b13fc05a69f9ca1f971 100644 (file)
@@ -2,6 +2,7 @@
 namespace wcf\data\style;
 use wcf\data\AbstractDatabaseObjectAction;
 use wcf\data\IToggleAction;
+use wcf\data\IUploadAction;
 use wcf\system\cache\builder\StyleCacheBuilder;
 use wcf\system\exception\IllegalLinkException;
 use wcf\system\exception\PermissionDeniedException;
@@ -26,46 +27,46 @@ use wcf\util\StringUtil;
  * @subpackage data.style
  * @category   Community Framework
  */
-class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction {
+class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction, IUploadAction {
        /**
-        * @see \wcf\data\AbstractDatabaseObjectAction::$allowGuestAccess
+        * @inheritdoc
         */
-       protected $allowGuestAccess = array('changeStyle', 'getStyleChooser');
+       protected $allowGuestAccess = ['changeStyle', 'getStyleChooser'];
        
        /**
-        * @see \wcf\data\AbstractDatabaseObjectAction::$className
+        * @inheritdoc
         */
        protected $className = 'wcf\data\style\StyleEditor';
        
        /**
-        * @see \wcf\data\AbstractDatabaseObjectAction::$permissionsDelete
+        * @inheritdoc
         */
-       protected $permissionsDelete = array('admin.style.canManageStyle');
+       protected $permissionsDelete = ['admin.style.canManageStyle'];
        
        /**
-        * @see \wcf\data\AbstractDatabaseObjectAction::$permissionsUpdate
+        * @inheritdoc
         */
-       protected $permissionsUpdate = array('admin.style.canManageStyle');
+       protected $permissionsUpdate = ['admin.style.canManageStyle'];
        
        /**
-        * @see \wcf\data\AbstractDatabaseObjectAction::$requireACP
+        * @inheritdoc
         */
-       protected $requireACP = array('copy', 'delete', 'markAsTainted', 'setAsDefault', 'toggle', 'update', 'upload', 'uploadLogo');
+       protected $requireACP = ['copy', 'delete', 'markAsTainted', 'setAsDefault', 'toggle', 'update', 'upload', 'uploadLogo'];
        
        /**
         * style object
-        * @var \wcf\data\style\Style
+        * @var Style
         */
        public $style = null;
        
        /**
         * style editor object
-        * @var \wcf\data\style\StyleEditor
+        * @var StyleEditor
         */
        public $styleEditor = null;
        
        /**
-        * @see \wcf\data\AbstractDatabaseObjectAction::create()
+        * @inheritdoc
         */
        public function create() {
                $style = parent::create();
@@ -80,7 +81,7 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
        }
        
        /**
-        * @see \wcf\data\AbstractDatabaseObjectAction::update()
+        * @inheritdoc
         */
        public function update() {
                parent::update();
@@ -98,7 +99,7 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
        }
        
        /**
-        * @see \wcf\data\AbstractDatabaseObjectAction::delete()
+        * @inheritdoc
         */
        public function delete() {
                $count = parent::delete();
@@ -147,8 +148,8 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
        /**
         * Updates style variables for given style.
         * 
-        * @param       \wcf\data\style\Style   $style
-        * @param       boolean                 $removePreviousVariables
+        * @param       Style           $style
+        * @param       boolean         $removePreviousVariables
         */
        protected function updateVariables(Style $style, $removePreviousVariables = false) {
                if (!isset($this->parameters['variables']) || !is_array($this->parameters['variables'])) {
@@ -159,7 +160,7 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
                        FROM    wcf".WCF_N."_style_variable";
                $statement = WCF::getDB()->prepareStatement($sql);
                $statement->execute();
-               $variables = array();
+               $variables = [];
                while ($row = $statement->fetchArray()) {
                        $variableName = $row['variableName'];
                        
@@ -179,7 +180,7 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
                        $sql = "DELETE FROM     wcf".WCF_N."_style_variable_value
                                WHERE           styleID = ?";
                        $statement = WCF::getDB()->prepareStatement($sql);
-                       $statement->execute(array($style->styleID));
+                       $statement->execute([$style->styleID]);
                }
                
                // insert variables that differ from default values
@@ -191,11 +192,11 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
                        
                        WCF::getDB()->beginTransaction();
                        foreach ($variables as $variableID => $variableValue) {
-                               $statement->execute(array(
+                               $statement->execute([
                                        $style->styleID,
                                        $variableID,
                                        $variableValue
-                               ));
+                               ]);
                        }
                        WCF::getDB()->commitTransaction();
                }
@@ -204,7 +205,7 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
        /**
         * Updates style preview image.
         * 
-        * @param       \wcf\data\style\Style   $style
+        * @param       Style           $style
         */
        protected function updateStylePreviewImage(Style $style) {
                if (!isset($this->parameters['tmpHash'])) {
@@ -226,10 +227,10 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
                                                        SET     image = ?
                                                        WHERE   styleID = ?";
                                                $statement = WCF::getDB()->prepareStatement($sql);
-                                               $statement->execute(array(
+                                               $statement->execute([
                                                        $filename,
                                                        $style->styleID
-                                               ));
+                                               ]);
                                        }
                                }
                                else {
@@ -241,7 +242,7 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
        }
        
        /**
-        * Validates the upload action.
+        * @inheritdoc
         */
        public function validateUpload() {
                // check upload permissions
@@ -266,13 +267,11 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
                }
                
                // check max filesize, allowed file extensions etc.
-               $this->parameters['__files']->validateFiles(new DefaultUploadFileValidationStrategy(PHP_INT_MAX, array('jpg', 'jpeg', 'png', 'gif')));
+               $this->parameters['__files']->validateFiles(new DefaultUploadFileValidationStrategy(PHP_INT_MAX, ['jpg', 'jpeg', 'png', 'gif']));
        }
        
        /**
-        * Handles uploaded preview images.
-        * 
-        * @return      array<string>
+        * @inheritdoc
         */
        public function upload() {
                // save files
@@ -308,15 +307,15 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
                                        if ($this->parameters['styleID']) {
                                                $this->updateStylePreviewImage($this->style);
                                                
-                                               return array(
+                                               return [
                                                        'url' => WCF::getPath().'images/stylePreview-'.$this->parameters['styleID'].'.'.$file->getFileExtension()
-                                               );
+                                               ];
                                        }
                                        
                                        // return result
-                                       return array(
+                                       return [
                                                'url' => WCF::getPath().'images/stylePreview-'.$this->parameters['tmpHash'].'.'.$file->getFileExtension()
-                                       );
+                                       ];
                                }
                                else {
                                        throw new UserInputException('image', 'uploadFailed');
@@ -327,7 +326,7 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
                        $file->setValidationErrorType($e->getType());
                }
                
-               return array('errorType' => $file->getValidationErrorType());
+               return ['errorType' => $file->getValidationErrorType()];
        }
        
        /**
@@ -340,7 +339,7 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
        /**
         * Handles logo upload.
         * 
-        * @return      array<string>
+        * @return      string[]
         */
        public function uploadLogo() {
                // save files
@@ -360,9 +359,9 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
                                        WCF::getSession()->register('styleLogo-'.$this->parameters['tmpHash'], $file->getFileExtension());
                                        
                                        // return result
-                                       return array(
+                                       return [
                                                'url' => WCF::getPath().'images/styleLogo-'.$this->parameters['tmpHash'].'.'.$file->getFileExtension()
-                                       );
+                                       ];
                                }
                                else {
                                        throw new UserInputException('image', 'uploadFailed');
@@ -373,7 +372,7 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
                        $file->setValidationErrorType($e->getType());
                }
                
-               return array('errorType' => $file->getValidationErrorType());
+               return ['errorType' => $file->getValidationErrorType()];
        }
        
        /**
@@ -418,7 +417,7 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
        /**
         * Copies a style.
         * 
-        * @return      array<string>
+        * @return      string[]
         */
        public function copy() {
                // get unique style name
@@ -427,11 +426,11 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
                        WHERE   styleName LIKE ?
                                AND styleID <> ?";
                $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute(array(
+               $statement->execute([
                        $this->styleEditor->styleName.'%',
                        $this->styleEditor->styleID
-               ));
-               $numbers = array();
+               ]);
+               $numbers = [];
                $regEx = new Regex('\((\d+)\)$');
                while ($row = $statement->fetchArray()) {
                        $styleName = $row['styleName'];
@@ -450,7 +449,7 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
                $styleName = $this->styleEditor->styleName . ' ('.$number.')';
                
                // create the new style
-               $newStyle = StyleEditor::create(array(
+               $newStyle = StyleEditor::create([
                        'styleName' => $styleName,
                        'templateGroupID' => $this->styleEditor->templateGroupID,
                        'isDisabled' => 1, // newly created styles are disabled by default
@@ -462,7 +461,7 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
                        'authorName' => $this->styleEditor->authorName,
                        'authorURL' => $this->styleEditor->authorURL,
                        'imagePath' => $this->styleEditor->imagePath
-               ));
+               ]);
                
                // check if style description uses i18n
                if (preg_match('~^wcf.style.styleDescription\d+$~', $newStyle->styleDescription)) {
@@ -475,13 +474,13 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
                                FROM            wcf".WCF_N."_language_item
                                WHERE           languageItem = ?";
                        $statement = WCF::getDB()->prepareStatement($sql);
-                       $statement->execute(array($newStyle->styleDescription));
+                       $statement->execute([$newStyle->styleDescription]);
                        
                        // update style description
                        $styleEditor = new StyleEditor($newStyle);
-                       $styleEditor->update(array(
+                       $styleEditor->update([
                                'styleDescription' => $styleDescription
-                       ));
+                       ]);
                }
                
                // copy style variables
@@ -491,7 +490,7 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
                        FROM            wcf".WCF_N."_style_variable_value value
                        WHERE           value.styleID = ?";
                $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute(array($this->styleEditor->styleID));
+               $statement->execute([$this->styleEditor->styleID]);
                
                // copy preview image
                if ($this->styleEditor->image) {
@@ -505,10 +504,10 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
                                        SET     image = ?
                                        WHERE   styleID = ?";
                                $statement = WCF::getDB()->prepareStatement($sql);
-                               $statement->execute(array(
+                               $statement->execute([
                                        'stylePreview-'.$newStyle->styleID.$fileExtension,
                                        $newStyle->styleID
-                               ));
+                               ]);
                        }
                }
                
@@ -547,21 +546,21 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
                                SET     imagePath = ?
                                WHERE   styleID = ?";
                        $statement = WCF::getDB()->prepareStatement($sql);
-                       $statement->execute(array(
+                       $statement->execute([
                                $newPath,
                                $newStyle->styleID
-                       ));
+                       ]);
                }
                
                StyleCacheBuilder::getInstance()->reset();
                
-               return array(
-                       'redirectURL' => LinkHandler::getInstance()->getLink('StyleEdit', array('id' => $newStyle->styleID))
-               );
+               return [
+                       'redirectURL' => LinkHandler::getInstance()->getLink('StyleEdit', ['id' => $newStyle->styleID])
+               ];
        }
        
        /**
-        * @see \wcf\data\IToggleAction::validateToggle()
+        * @inheritdoc
         */
        public function validateToggle() {
                parent::validateUpdate();
@@ -574,12 +573,12 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
        }
        
        /**
-        * @see \wcf\data\IToggleAction::toggle()
+        * @inheritdoc
         */
        public function toggle() {
                foreach ($this->objects as $style) {
                        $isDisabled = ($style->isDisabled) ? 0 : 1;
-                       $style->update(array('isDisabled' => $isDisabled));
+                       $style->update(['isDisabled' => $isDisabled]);
                }
        }
        
@@ -615,24 +614,24 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction
        /**
         * Returns the style chooser dialog.
         * 
-        * @return      array<string>
+        * @return      string[]
         */
        public function getStyleChooser() {
                $styleList = new StyleList();
                if (!WCF::getSession()->getPermission('admin.style.canUseDisabledStyle')) {
-                       $styleList->getConditionBuilder()->add("style.isDisabled = ?", array(0));
+                       $styleList->getConditionBuilder()->add("style.isDisabled = ?", [0]);
                }
                $styleList->sqlOrderBy = "style.styleName ASC";
                $styleList->readObjects();
                
-               WCF::getTPL()->assign(array(
+               WCF::getTPL()->assign([
                        'styleList' => $styleList
-               ));
+               ]);
                
-               return array(
+               return [
                        'actionName' => 'getStyleChooser',
                        'template' => WCF::getTPL()->fetch('styleChooser')
-               );
+               ];
        }
        
        /**
diff --git a/wcfsetup/install/files/lib/page/MediaPage.class.php b/wcfsetup/install/files/lib/page/MediaPage.class.php
new file mode 100644 (file)
index 0000000..5808fe3
--- /dev/null
@@ -0,0 +1,161 @@
+<?php
+namespace wcf\page;
+use wcf\data\media\Media;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\request\LinkHandler;
+use wcf\util\FileReader;
+use wcf\util\StringUtil;
+
+/**
+ * Shows a media file.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage page
+ * @category   Community Framework
+ * @since      2.2
+ */
+class MediaPage extends AbstractPage {
+       /**
+        * etag for the media file
+        * @var string
+        */
+       public $eTag = null;
+       
+       /**
+        * file reader object
+        * @var FileReader
+        */
+       public $fileReader = null;
+       
+       /**
+        * requested media file
+        * @var Media
+        */
+       public $media = null;
+       
+       /**
+        * id of the requested media file
+        * @var integer
+        */
+       public $mediaID = 0;
+       
+       /**
+        * size of the requested thumbnail
+        * @var string
+        */
+       public $thumbnail = '';
+       
+       /**
+        * @inheritdoc
+        */
+       public $useTemplate = false;
+       
+       /**
+        * list of mime types which belong to files that are displayed inline
+        * @var string[]
+        */
+       public static $inlineMimeTypes = [
+               'image/gif',
+               'image/jpeg',
+               'image/png',
+               'image/x-png',
+               'application/pdf',
+               'image/pjpeg'
+       ];
+       
+       /**
+        * @inheritdoc
+        */
+       public function checkPermissions() {
+               parent::checkPermissions();
+               
+               // TODO
+       }
+       
+       /**
+        * @inheritdoc
+        */
+       public function readData() {
+               parent::readData();
+               
+               // get file data
+               if ($this->thumbnail) {
+                       $mimeType = $this->media->{$this->thumbnail.'ThumbnailType'};
+                       $filesize = $this->media->{$this->thumbnail.'ThumbnailSize'};
+                       $location = $this->media->getThumbnailLocation($this->thumbnail);
+                       $this->eTag = strtoupper($this->thumbnail).'_'.$this->mediaID;
+               }
+               else {
+                       $mimeType = $this->media->fileType;
+                       $filesize = $this->media->filesize;
+                       $location = $this->media->getLocation();
+                       $this->eTag = $this->mediaID;
+               }
+               
+               // init file reader
+               $this->fileReader = new FileReader($location, [
+                       'filename' => $this->media->filename,
+                       'mimeType' => $mimeType,
+                       'filesize' => $filesize,
+                       'showInline' => (in_array($mimeType, self::$inlineMimeTypes)),
+                       'enableRangeSupport' => ($this->thumbnail ? true : false),
+                       'lastModificationTime' => $this->media->uploadTime,
+                       'expirationDate' => TIME_NOW + 31536000,
+                       'maxAge' => 31536000
+               ]);
+               
+               if ($this->eTag !== null) {
+                       $this->fileReader->addHeader('ETag', '"'.$this->eTag.'"');
+               }
+       }
+       
+       /**
+        * @inheritdoc
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               if (isset($_REQUEST['id'])) $this->mediaID = intval($_REQUEST['id']);
+               $this->media = new Media($this->mediaID);
+               if (!$this->media->mediaID) {
+                       throw new IllegalLinkException();
+               }
+               
+               if (isset($_REQUEST['thumbnail'])) $this->thumbnail = StringUtil::trim($_REQUEST['thumbnail']);
+               if ($this->thumbnail && !isset(Media::getThumbnailSizes()[$this->thumbnail])) {
+                       throw new IllegalLinkException();
+               }
+               
+               $parameters = [
+                       'object' => $this->media
+               ];
+               if ($this->thumbnail && $this->media->{$this->thumbnail.'ThumbnailType'}) {
+                       $parameters['thumbnail'] = $this->thumbnail;
+               }
+               else {
+                       $this->thumbnail = '';
+               }
+               
+               $this->canonicalURL = LinkHandler::getInstance()->getLink('Media', $parameters);
+       }
+       
+       /**
+        * @inheritdoc
+        */
+       public function show() {
+               parent::show();
+               
+               // etag caching
+               if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == '"'.$this->eTag.'"') {
+                       @header('HTTP/1.1 304 Not Modified');
+                       exit;
+               }
+               
+               // send file to client
+               $this->fileReader->send();
+               exit;
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/clipboard/action/MediaClipboardAction.class.php b/wcfsetup/install/files/lib/system/clipboard/action/MediaClipboardAction.class.php
new file mode 100644 (file)
index 0000000..79a7898
--- /dev/null
@@ -0,0 +1,89 @@
+<?php
+namespace wcf\system\clipboard\action;
+use wcf\data\clipboard\action\ClipboardAction;
+use wcf\data\media\MediaAction;
+use wcf\system\WCF;
+
+/**
+ * Clipboard action implementation for media files.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage system.clipboard.action
+ * @category   Community Framework
+ * @since      2.2
+ */
+class MediaClipboardAction extends AbstractClipboardAction {
+       /**
+        * @inheritdoc
+        */
+       protected $actionClassActions = [ 'delete' ];
+       
+       /**
+        * @inheritdoc
+        */
+       protected $supportedActions = [
+               'delete',
+               'insert'
+       ];
+       
+       /**
+        * @inheritdoc
+        */
+       public function execute(array $objects, ClipboardAction $action) {
+               $item = parent::execute($objects, $action);
+               
+               if ($item === null) {
+                       return null;
+               }
+               
+               // handle actions
+               switch ($action->actionName) {
+                       case 'delete':
+                               $item->addInternalData('confirmMessage', WCF::getLanguage()->getDynamicVariable('wcf.clipboard.item.com.woltlab.wcf.media.delete.confirmMessage', [
+                                       'count' => $item->getCount()
+                               ]));
+                       break;
+               }
+               
+               return $item;
+       }
+       
+       /**
+        * @inheritdoc
+        */
+       public function getClassName() {
+               return MediaAction::class;
+       }
+       
+       /**
+        * @inheritdoc
+        */
+       public function getTypeName() {
+               return 'com.woltlab.wcf.media';
+       }
+       
+       /**
+        * Returns the ids of the media files which can be deleted.
+        * 
+        * @return      integer[]
+        */
+       public function validateDelete() {
+               if (!WCF::getSession()->getPermission('admin.content.cms.canManageMedia')) {
+                       return [];
+               }
+               
+               return array_keys($this->objects);
+       }
+       
+       /**
+        * Returns the ids of the media files which can be inserted.
+        * 
+        * @return      integer[]
+        */
+       public function validateInsert() {
+               return array_keys($this->objects);
+       }
+}
index 026d0be51c95600a99eb8ddfa7fc5b1864d9a6ae..89260483f548c0eee4b7216e2a7fb68c9ea52a82 100644 (file)
@@ -166,7 +166,7 @@ class I18nHandler extends SingletonFactory {
         * Sets the value for the given element. If the element is multilingual,
         * the given value is set for every available language.
         * 
-        * @param       integer         $elementID
+        * @param       string          $elementID
         * @param       string          $plainValue
         */
        public function setValue($elementID, $plainValue) {
@@ -189,8 +189,8 @@ class I18nHandler extends SingletonFactory {
         * Sets the values for the given element. If the element is not multilingual,
         * use I18nHandler::setValue() instead.
         * 
-        * @param       integer         $elementID
-        * @param       array<array>    $i18nValues
+        * @param       string          $elementID
+        * @param       string[]        $i18nValues
         */
        public function setValues($elementID, array $i18nValues) {
                if (empty($i18nValues)) {
diff --git a/wcfsetup/install/files/lib/system/upload/DefaultUploadFileSaveStrategy.class.php b/wcfsetup/install/files/lib/system/upload/DefaultUploadFileSaveStrategy.class.php
new file mode 100644 (file)
index 0000000..af36be4
--- /dev/null
@@ -0,0 +1,248 @@
+<?php
+namespace wcf\system\upload;
+use wcf\system\WCF;
+use wcf\data\AbstractDatabaseObjectAction;
+use wcf\data\IFile;
+use wcf\data\IThumbnailFile;
+use wcf\util\FileUtil;
+use wcf\system\image\ImageHandler;
+use wcf\util\ExifUtil;
+use wcf\system\event\EventHandler;
+
+/**
+ * Default implementation for saving uploaded files.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage system.upload
+ * @category   Community Framework
+ * @since      2.2
+ */
+class DefaultUploadFileSaveStrategy implements IUploadFileSaveStrategy {
+       /**
+        * name of the database object action class name
+        * @var string
+        */
+       public $actionClassName = '';
+       
+       /**
+        * name of the database object editor class name
+        * @var string
+        */
+       public $editorClassName = '';
+       
+       /**
+        * additional data stored with the default file data
+        * @var array
+        */
+       public $data = [];
+       
+       /**
+        * created objects
+        * @var IFile[]
+        */
+       public $objects = [];
+       
+       /**
+        * options handing saving details
+        * 
+        * - bool rotateImages: if true, images are automatically rotated
+        * - bool generateThumbnails: if true, thumbnails are automatically generated after saving file
+        * 
+        * @var array
+        */
+       public $options = [];
+       
+       /**
+        * Creates a new instance of DefaultUploadFileSaveStrategy.
+        * 
+        * @param       string          $actionClassName
+        * @param       array           $options
+        * @param       array           $data
+        */
+       public function __construct($actionClassName, array $options = [ ], array $data = [ ]) {
+               $this->actionClassName = $actionClassName;
+               $this->options = $options;
+               $this->data = $data;
+               
+               if (!is_subclass_of($this->actionClassName, AbstractDatabaseObjectAction::class)) {
+                       throw new SystemException("'".$this->actionClassName."' does not extend '".AbstractDatabaseObjectAction::class."'");
+               }
+               
+               $this->editorClassName = (new $this->actionClassName([ ], ''))->getClassName();
+               $baseClass = call_user_func([ $this->editorClassName, 'getBaseClass' ]);
+               if (!is_subclass_of($baseClass, IFile::class)) {
+                       throw new SystemException("'".$this->editorClassName."' does not implement '".IFile::class."'");
+               }
+               if (is_subclass_of($baseClass, IThumbnailFile::class)) {
+                       $this->options['thumbnailSizes'] = call_user_func([ $baseClass, 'getThumbnailSizes' ]);
+               }
+       }
+       
+       /**
+        * Returns the successfully created file objects.
+        * 
+        * @return      IFile[]
+        */
+       public function getObjects() {
+               return $this->objects;
+       }
+       
+       /**
+        * @inheritdoc
+        */
+       public function save(UploadFile $uploadFile) {
+               $data = array_merge([
+                       'filename' => $uploadFile->getFilename(),
+                       'filesize' => $uploadFile->getFilesize(),
+                       'fileType' => $uploadFile->getMimeType(),
+                       'fileHash' => sha1_file($uploadFile->getLocation()),
+                       'uploadTime' => TIME_NOW,
+                       'userID' => (WCF::getUser()->userID ?: null)
+               ], $this->data);
+               
+               // get image data
+               if (($imageData = $uploadFile->getImageData()) !== null) {
+                       $data['width'] = $imageData['width'];
+                       $data['height'] = $imageData['height'];
+                       $data['fileType'] = $imageData['mimeType'];
+                       
+                       if (preg_match('~^image/(gif|jpe?g|png)$~i', $data['fileType'])) {
+                               $data['isImage'] = 1;
+                       }
+               }
+               
+               $action = new $this->actionClassName([ ], 'create', [
+                       'data' => $data
+               ]);
+               $object = $action->executeAction()['returnValues'];
+               
+               $dir = dirname($object->getLocation());
+               if (!@file_exists($dir)) {
+                       FileUtil::makePath($dir, 0777);
+               }
+               
+               // move uploaded filex
+               if (@move_uploaded_file($uploadFile->getLocation(), $object->getLocation())) {
+                       // rotate image based on the exif data
+                       if (!empty($this->options['rotateImages'])) {
+                               if ($object->isImage) {
+                                       if (FileUtil::checkMemoryLimit($object->width * $object->height * ($object->fileType == 'image/png' ? 4 : 3) * 2.1)) {
+                                               $exifData = ExifUtil::getExifData($object->getLocation());
+                                               if (!empty($exifData)) {
+                                                       $orientation = ExifUtil::getOrientation($exifData);
+                                                       if ($orientation != ExifUtil::ORIENTATION_ORIGINAL) {
+                                                               $adapter = ImageHandler::getInstance()->getAdapter();
+                                                               $adapter->loadFile($object->getLocation());
+                                                               
+                                                               $newImage = null;
+                                                               switch ($orientation) {
+                                                                       case ExifUtil::ORIENTATION_180_ROTATE:
+                                                                               $newImage = $adapter->rotate(180);
+                                                                       break;
+                                                                       
+                                                                       case ExifUtil::ORIENTATION_90_ROTATE:
+                                                                               $newImage = $adapter->rotate(90);
+                                                                       break;
+                                                                       
+                                                                       case ExifUtil::ORIENTATION_270_ROTATE:
+                                                                               $newImage = $adapter->rotate(270);
+                                                                       break;
+                                                                       
+                                                                       case ExifUtil::ORIENTATION_HORIZONTAL_FLIP:
+                                                                       case ExifUtil::ORIENTATION_VERTICAL_FLIP:
+                                                                       case ExifUtil::ORIENTATION_VERTICAL_FLIP_270_ROTATE:
+                                                                       case ExifUtil::ORIENTATION_HORIZONTAL_FLIP_270_ROTATE:
+                                                                               // unsupported
+                                                                       break;
+                                                               }
+                                                               
+                                                               if ($newImage !== null) {
+                                                                       $adapter->load($newImage, $adapter->getType());
+                                                               }
+                                                               
+                                                               $adapter->writeImage($object->getLocation());
+                                                               
+                                                               // update width, height and filesize of the attachment
+                                                               if ($newImage !== null && ($orientation == ExifUtil::ORIENTATION_90_ROTATE || $orientation == ExifUtil::ORIENTATION_270_ROTATE)) {
+                                                                       (new $this->editorClassName($object))->update([
+                                                                               'height' => $object->width,
+                                                                               'width' => $object->height,
+                                                                               'filesize' => filesize($object->getLocation())
+                                                                       ]);
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+                       
+                       $this->objects[$uploadFile->getInternalFileID()] = $object;
+               }
+               else {
+                       (new $this->editorClassName($object))->delete();
+               }
+               
+               if ($object->isImage && !empty($this->options['generateThumbnails']) && $object instanceof IThumbnailFile) {
+                       $this->generateThumbnails($object);
+               }
+       }
+       
+       /**
+        * Generates thumbnails for the given file.
+        * 
+        * @param       IThumbnailFile  $file
+        */
+       public function generateThumbnails(IThumbnailFile $file) {
+               $smallestThumbnailSize = reset($this->options['thumbnailSizes']);
+               
+               // image is smaller than smallest thumbnail size
+               if ($file->width <= $smallestThumbnailSize['width'] && $file->height <= $smallestThumbnailSize['height']) {
+                       return;
+               }
+               
+               // check memory limit
+               if (!FileUtil::checkMemoryLimit($file->width * $file->height * ($file->fileType == 'image/png' ? 4 : 3) * 2.1)) {
+                       return;
+               }
+               
+               $adapter = ImageHandler::getInstance()->getAdapter();
+               $adapter->loadFile($file->getLocation());
+               
+               $updateData = [ ];
+               foreach ($this->options['thumbnailSizes'] as $type => $sizeData) {
+                       $prefix = 'thumbnail';
+                       if (!empty($type)) {
+                               $prefix = $type.'Thumbnail';
+                       }
+                       
+                       $thumbnailLocation = $file->getThumbnailLocation($type);
+                       
+                       // delete old thumbnails
+                       if ($file->{$prefix.'Type'}) {
+                               @unlink($thumbnailLocation);
+                               $updateData[$prefix.'Type'] = '';
+                               $updateData[$prefix.'Size'] = 0;
+                               $updateData[$prefix.'Width'] = 0;
+                               $updateData[$prefix.'Height'] = 0;
+                       }
+                       
+                       if ($file->width > $sizeData['width'] || $file->height > $sizeData['height']) {
+                               $thumbnail = $adapter->createThumbnail($sizeData['width'], $sizeData['height'], isset($sizeData['retainDimensions']) ? $sizeData['retainDimensions'] : true);
+                               $adapter->writeImage($thumbnail, $thumbnailLocation);
+                               if (file_exists($thumbnailLocation) && ($imageData = @getimagesize($thumbnailLocation)) !== false) {
+                                       $updateData[$prefix.'Type'] = $imageData['mime'];
+                                       $updateData[$prefix.'Size'] = @filesize($thumbnailLocation);
+                                       $updateData[$prefix.'Width'] = $imageData[0];
+                                       $updateData[$prefix.'Height'] = $imageData[1];
+                               }
+                       }
+               }
+               
+               if (!empty($updateData)) {
+                       (new $this->editorClassName($file))->update($updateData);
+               }
+       }
+}
index 720a6ed18e8e0e05821cf53f3cc7e82e995720ac..8483717fbbe6c49ba03422b9910ffa40b379b30b 100644 (file)
@@ -321,7 +321,7 @@ final class FileUtil {
        public static function getRealPath($path) {
                $path = self::unifyDirSeparator($path);
                
-               $result = array();
+               $result = [];
                $pathA = explode('/', $path);
                if ($pathA[0] === '') {
                        $result[] = '';
@@ -425,7 +425,7 @@ final class FileUtil {
         * @deprecated  This method currently only is a wrapper around \wcf\util\HTTPRequest. Please use
         *              HTTPRequest from now on, as this method may be removed in the future.
         */
-       public static function downloadFileFromHttp($httpUrl, $prefix = 'package', array $options = array(), array $postParameters = array(), &$headers = array()) {
+       public static function downloadFileFromHttp($httpUrl, $prefix = 'package', array $options = [], array $postParameters = [], &$headers = []) {
                $request = new HTTPRequest($httpUrl, $options, $postParameters);
                $request->execute();
                $reply = $request->getReply();
@@ -638,5 +638,61 @@ final class FileUtil {
                return self::getMemoryLimit() == -1 || self::getMemoryLimit() > (memory_get_usage() + $neededMemory);
        }
        
+       /**
+        * Returns the FontAwesome icon CSS class name for a file with the given
+        * mime type.
+        * 
+        * @param       string          $mimeType
+        * @return      string
+        */
+       public static function getIconClassByMimeType($mimeType) {
+               if (StringUtil::startsWith($mimeType, 'image/')) {
+                       return 'fa-file-image-o';
+               }
+               else if (StringUtil::startsWith($mimeType, 'video/')) {
+                       return 'fa-file-video-o';
+               }
+               else if (StringUtil::startsWith($mimeType, 'audio/')) {
+                       return 'fa-file-sound-o';
+               }
+               else if (StringUtil::startsWith($mimeType, 'text/')) {
+                       return 'fa-file-text-o';
+               }
+               else {
+                       switch ($mimeType) {
+                               case 'application/msword':
+                               case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
+                                       return 'fa-file-word-o';
+                               break;
+                               
+                               case 'application/pdf':
+                                       return 'fa-file-pdf-o';
+                               break;
+                               
+                               case 'application/vnd.ms-powerpoint':
+                               case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
+                                       return 'fa-file-powerpoint-o';
+                               break;
+                               
+                               case 'application/vnd.ms-excel':
+                               case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
+                                       return 'fa-file-excel-o';
+                               break;
+                               
+                               case 'application/zip':
+                               case 'application/x-tar':
+                               case 'application/x-gzip':
+                                       return 'fa-file-archive-o';
+                               break;
+                               
+                               case 'application/xml':
+                                       return 'fa-file-text-o';
+                               break;
+                       }
+               }
+               
+               return 'fa-file-o';
+       }
+       
        private function __construct() { }
 }
diff --git a/wcfsetup/install/files/media/.htaccess b/wcfsetup/install/files/media/.htaccess
new file mode 100644 (file)
index 0000000..3418e55
--- /dev/null
@@ -0,0 +1 @@
+deny from all
\ No newline at end of file
index e7e36eafb4ce35e1a0489939130747c5ba8cd245..70c8ca795194d767be49ed25eb0a8b52935b3b76 100644 (file)
        color: $wcfStatusWarningText;
 }
 
-/* inline errors */
-.innerError {
-       background-color: rgb(242, 222, 222);
-       color: rgb(169, 68, 66);
+.innerError,
+.innerInfo {
        display: table;
        line-height: 1.5;
        margin-top: 8px;
        padding: 5px 10px;
        position: relative;
-       
+
        /* pointer */
        &::before {
                border: 6px solid transparent;
-               border-bottom-color: rgb(242, 222, 222);
                border-top-width: 0;
                content: "";
                display: inline-block;
                z-index: 101;
        }
 }
+
+/* inline errors */
+.innerError {
+       background-color: rgb(242, 222, 222);
+       color: rgb(169, 68, 66);
+
+       &::before {
+               border-bottom-color: rgb(242, 222, 222);
+       }
+}
+
+/* inline infos */
+/* TODO: use other colors */
+.innerInfo {
+       background-color: $wcfStatusInfoBackground;
+       color: $wcfStatusInfoText;
+
+       &::before {
+               border-bottom-color: $wcfStatusInfoBorder;
+       }
+}
diff --git a/wcfsetup/install/files/style/ui/media.scss b/wcfsetup/install/files/style/ui/media.scss
new file mode 100644 (file)
index 0000000..68a4dd8
--- /dev/null
@@ -0,0 +1,95 @@
+#mediaManagerMediaUploadButton > .button {
+       box-sizing: border-box;
+       margin: 0;
+       text-align: center;
+       width: 100%;
+       
+       > input {
+               width: 100%;
+       }
+}
+
+#mediaManagerMediaList {
+       font-size: 0;
+       
+       @extend .clearfix;
+       
+       > li {
+               float: left;
+               height: 120px;
+               width: 120px;
+               position: relative;
+               border: 1px solid #aaa;
+               overflow: hidden;
+               font-size: 1rem;
+               margin: 0 $wcfGapSmall $wcfGapSmall 0;
+               
+               &:hover {
+                       > .buttonGroupNavigation {
+                               height: auto;
+                               padding: $wcfGapSmall;
+                       }
+               }
+               
+               &.jsMarked {
+                       > .mediaInformation,
+                       > .buttonGroupNavigation {
+                               // TODO background-color: fade($wcfSelectedBackgroundColor, 90%);
+                               // TODO color: $wcfSelectedColor;
+                               
+                               a {
+                                       //TODO: color: $wcfSelectedColor;
+                               }
+                               
+                               .icon {
+                                       //TODO: color: $wcfSelectedColor;
+                                       text-shadow: none;
+                               }
+                       }
+               }
+               
+               > .mediaThumbnail {
+                       height: 96px;
+                       width: 96px;
+                       padding: 12px;
+               }
+               
+               > .mediaInformation {
+                       position: absolute;
+                       bottom: 0;
+                       background: rgba(0,0,0,0.6);
+                       color: #fff;
+                       width: 100%;
+                       padding: $wcfGapSmall;
+                       box-sizing: border-box;
+                       
+                       @extend .wcfFontSmall;
+               }
+               
+               > .buttonGroupNavigation {
+                       position: absolute;
+                       top: 0;
+                       right: 0;
+                       background: rgba(0,0,0,0.6);
+                       height: 0;
+                       overflow: hidden;
+                       
+                       /* TODO: transition */
+                       
+                       .icon {
+                               color: #fff;
+                               @include textShadow(#000);
+                       }
+               }
+       }
+}
+
+#mediaEditor {
+       #mediaThumbnail {
+               text-align: center;
+               
+               + .box48 > dl {
+                       font-size: $wcfFontSizeSmall;
+               }
+       }
+}
index 733a1493cbbd3fe0073d7c2a625b63b47a93d483..c043f7cdc297b63aa4eddc5a7556abc755cf7d01 100644 (file)
@@ -1909,6 +1909,9 @@ Errors are:
        <category name="wcf.clipboard">
                <item name="wcf.clipboard.item.unmarkAll"><![CDATA[Unmark All]]></item>
                
+               <item name="wcf.clipboard.item.com.woltlab.wcf.media.delete"><![CDATA[Delete ({#$count})]]></item>
+               <item name="wcf.clipboard.item.com.woltlab.wcf.media.insert"><![CDATA[Insert ({#$count})]]></item>
+               
                <item name="wcf.clipboard.item.com.woltlab.wcf.tag.delete"><![CDATA[Delete ({#$count})]]></item>
                <item name="wcf.clipboard.item.com.woltlab.wcf.tag.delete.confirmMessage"><![CDATA[Do you really want to delete {#$count} tag{if $count != 1}s{/if}?]]></item>
                <item name="wcf.clipboard.item.com.woltlab.wcf.tag.setAsSynonyms"><![CDATA[Set as Synonyms ({#$count})]]></item>
@@ -1925,6 +1928,7 @@ Errors are:
                <item name="wcf.clipboard.item.com.woltlab.wcf.user.sendNewPassword"><![CDATA[Send New Password ({#$count})]]></item>
                <item name="wcf.clipboard.item.com.woltlab.wcf.user.sendNewPassword.confirmMessage"><![CDATA[Do you really want to send a new password to {#$count} user{if $count != 1}s{/if}?]]></item>
                
+               <item name="wcf.clipboard.label.com.woltlab.wcf.media.marked"><![CDATA[{#$count} File{if $count != 1}s{/if} marked]]></item>
                <item name="wcf.clipboard.label.com.woltlab.wcf.tag.marked"><![CDATA[{#$count} Tag{if $count != 1}s{/if} marked]]></item>
                <item name="wcf.clipboard.label.com.woltlab.wcf.user.marked"><![CDATA[{#$count} User{if $count != 1}s{/if} marked]]></item>
        </category>
@@ -2194,6 +2198,7 @@ Errors are:
                <item name="wcf.global.button.enable"><![CDATA[Enable]]></item>
                <item name="wcf.global.button.fullscreen"><![CDATA[Full Screen Mode]]></item>
                <item name="wcf.global.button.hide"><![CDATA[Hide]]></item>
+               <item name="wcf.global.button.insert"><![CDATA[Insert]]></item>
                <item name="wcf.global.button.next"><![CDATA[Next »]]></item>
                <item name="wcf.global.button.preview"><![CDATA[Preview]]></item>
                <item name="wcf.global.button.refresh"><![CDATA[Refresh]]></item>
@@ -2335,6 +2340,34 @@ Errors are:
                <item name="wcf.map.useLocationSuggestion"><![CDATA[Use Location]]></item>
        </category>
        
+       <category name="wcf.media">
+               <item name="wcf.media.altText"><![CDATA[Alternate Text]]></item>
+               <item name="wcf.media.button.insert"><![CDATA[Insert]]></item>
+               <item name="wcf.media.caption"><![CDATA[Caption]]></item>
+               <item name="wcf.media.edit"><![CDATA[Edit Media File]]></item>
+               <item name="wcf.media.filename"><![CDATA[Filename]]></item>
+               <item name="wcf.media.filesize"><![CDATA[Filesize]]></item>
+               <item name="wcf.media.imageDimensions"><![CDATA[Dimensions]]></item>
+               <item name="wcf.media.imageDimensions.value"><![CDATA[{#$width}×{#$height}]]></item>
+               <item name="wcf.media.insert"><![CDATA[Insert File]]></item>
+               <item name="wcf.media.insert.imageSize"><![CDATA[Image Size]]></item>
+               <item name="wcf.media.insert.imageSize.large"><![CDATA[Large Thumbnail ({#$width}×{#$height})]]></item>
+               <item name="wcf.media.insert.imageSize.medium"><![CDATA[Medium Thumbnail ({#$width}×{#$height})]]></item>
+               <item name="wcf.media.insert.imageSize.original"><![CDATA[Original Image ({#$width}×{#$height})]]></item>
+               <item name="wcf.media.insert.imageSize.small"><![CDATA[Small Thumbnail ({#$width}×{#$height})]]></item>
+               <item name="wcf.media.isMultilingual"><![CDATA[Enable Multilingualism]]></item>
+               <item name="wcf.media.languageID"><![CDATA[Language]]></item>
+               <item name="wcf.media.manager"><![CDATA[Manage Media]]></item>
+               <item name="wcf.media.search.cancel"><![CDATA[Cancel Search]]></item>
+               <item name="wcf.media.search.filetype"><![CDATA[File Types]]></item>
+               <item name="wcf.media.search.filetype.all"><![CDATA[All File Types]]></item>
+               <item name="wcf.media.search.filetype.image"><![CDATA[Images]]></item>
+               <item name="wcf.media.search.filetype.other"><![CDATA[Other]]></item>
+               <item name="wcf.media.search.filetype.text"><![CDATA[Texts]]></item>
+               <item name="wcf.media.search.placeholder"><![CDATA[Search Files]]></item>
+               <item name="wcf.media.uploader"><![CDATA[Uploaded By]]></item>
+       </category>
+       
        <category name="wcf.message">
                <item name="wcf.message.autosave.prompt"><![CDATA[Restore saved draft?]]></item>
                <item name="wcf.message.autosave.prompt.confirm"><![CDATA[Restore draft]]></item>
index 39e462c4552aafa086194be88045a9a6cfa6ac34..9744f22dd59857d35d4f75bf8e1b91e2598c17cb 100644 (file)
@@ -146,7 +146,7 @@ CREATE TABLE wcf1_attachment (
        
        isImage TINYINT(1) NOT NULL DEFAULT 0,
        width SMALLINT(5) NOT NULL DEFAULT 0,
-       height SMALLINT(5) NOT NULL DEFAULT 0, 
+       height SMALLINT(5) NOT NULL DEFAULT 0,
        
        tinyThumbnailType VARCHAR(255) NOT NULL DEFAULT '',
        tinyThumbnailSize INT(10) NOT NULL DEFAULT 0,
@@ -533,6 +533,55 @@ CREATE TABLE wcf1_like_object (
        UNIQUE KEY (objectTypeID, objectID)
 );
 
+DROP TABLE IF EXISTS wcf1_media;
+CREATE TABLE wcf1_media (
+       mediaID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+       
+       filename VARCHAR(255) NOT NULL DEFAULT '',
+       filesize INT(10) NOT NULL DEFAULT 0,
+       fileType VARCHAR(255) NOT NULL DEFAULT '',
+       fileHash VARCHAR(255) NOT NULL DEFAULT '',
+       uploadTime INT(10) NOT NULL DEFAULT 0,
+       userID INT(10),
+       username VARCHAR(255) NOT NULL,
+       languageID INT(10),
+       isMultilingual TINYINT(1) NOT NULL DEFAULT 0,
+       
+       isImage TINYINT(1) NOT NULL DEFAULT 0,
+       width SMALLINT(5) NOT NULL DEFAULT 0,
+       height SMALLINT(5) NOT NULL DEFAULT 0,
+       
+       tinyThumbnailType VARCHAR(255) NOT NULL DEFAULT '',
+       tinyThumbnailSize INT(10) NOT NULL DEFAULT 0,
+       tinyThumbnailWidth SMALLINT(5) NOT NULL DEFAULT 0,
+       tinyThumbnailHeight SMALLINT(5) NOT NULL DEFAULT 0,
+       
+       smallThumbnailType VARCHAR(255) NOT NULL DEFAULT '',
+       smallThumbnailSize INT(10) NOT NULL DEFAULT 0,
+       smallThumbnailWidth SMALLINT(5) NOT NULL DEFAULT 0,
+       smallThumbnailHeight SMALLINT(5) NOT NULL DEFAULT 0,
+       
+       mediumThumbnailType VARCHAR(255) NOT NULL DEFAULT '',
+       mediumThumbnailSize INT(10) NOT NULL DEFAULT 0,
+       mediumThumbnailWidth SMALLINT(5) NOT NULL DEFAULT 0,
+       mediumThumbnailHeight SMALLINT(5) NOT NULL DEFAULT 0,
+       
+       largeThumbnailType VARCHAR(255) NOT NULL DEFAULT '',
+       largeThumbnailSize INT(10) NOT NULL DEFAULT 0,
+       largeThumbnailWidth SMALLINT(5) NOT NULL DEFAULT 0,
+       largeThumbnailHeight SMALLINT(5) NOT NULL DEFAULT 0
+);
+
+DROP TABLE IF EXISTS wcf1_media_content;
+CREATE TABLE wcf1_media_content (
+       mediaID INT(10) NOT NULL,
+       languageID INT(10),
+       title VARCHAR(255) NOT NULL,
+       caption TEXT,
+       altText VARCHAR(255) NOT NULL DEFAULT '',
+       UNIQUE KEY (mediaID, languageID)
+);
+
 DROP TABLE IF EXISTS wcf1_menu;
 CREATE TABLE wcf1_menu (
        menuID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
@@ -1633,6 +1682,12 @@ ALTER TABLE wcf1_language_item ADD FOREIGN KEY (languageID) REFERENCES wcf1_lang
 ALTER TABLE wcf1_language_item ADD FOREIGN KEY (languageCategoryID) REFERENCES wcf1_language_category (languageCategoryID) ON DELETE CASCADE;
 ALTER TABLE wcf1_language_item ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;
 
+ALTER TABLE wcf1_media ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE SET NULL;
+ALTER TABLE wcf1_media ADD FOREIGN KEY (languageID) REFERENCES wcf1_language (languageID) ON DELETE SET NULL;
+
+ALTER TABLE wcf1_media_content ADD FOREIGN KEY (mediaID) REFERENCES wcf1_media (mediaID) ON DELETE CASCADE;
+ALTER TABLE wcf1_media_content ADD FOREIGN KEY (languageID) REFERENCES wcf1_language (languageID) ON DELETE CASCADE;
+
 ALTER TABLE wcf1_menu ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE; 
 
 ALTER TABLE wcf1_menu_item ADD FOREIGN KEY (menuID) REFERENCES wcf1_menu (menuID) ON DELETE CASCADE;