From f0bc263fadc39e5fcd9781c2ea0ce54b974fed6c Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Mon, 20 May 2013 22:26:47 +0200 Subject: [PATCH] Merged com.woltlab.wcf.attachment into WCF --- com.woltlab.wcf/acpMenu.xml | 10 + com.woltlab.wcf/cronjob.xml | 13 + com.woltlab.wcf/objectTypeDefinition.xml | 7 +- com.woltlab.wcf/option.xml | 32 ++ com.woltlab.wcf/template/attachments.tpl | 55 ++++ com.woltlab.wcf/userGroupOption.xml | 41 +++ .../files/acp/templates/attachmentList.tpl | 148 +++++++++ wcfsetup/install/files/attachments/.htaccess | 1 + wcfsetup/install/files/js/WCF.Attachment.js | 253 +++++++++++++++ .../install/files/js/WCF.Attachment.min.js | 1 + .../lib/acp/page/AttachmentListPage.class.php | 141 +++++++++ .../lib/acp/page/AttachmentPage.class.php | 31 ++ .../AdministrativeAttachment.class.php | 49 +++ .../AdministrativeAttachmentList.class.php | 89 ++++++ .../lib/data/attachment/Attachment.class.php | 205 +++++++++++++ .../attachment/AttachmentAction.class.php | 290 ++++++++++++++++++ .../attachment/AttachmentEditor.class.php | 68 ++++ .../data/attachment/AttachmentList.class.php | 20 ++ .../GroupedAttachmentList.class.php | 89 ++++++ .../files/lib/page/AttachmentPage.class.php | 205 +++++++++++++ .../AbstractAttachmentObjectType.class.php | 57 ++++ .../attachment/AttachmentHandler.class.php | 192 ++++++++++++ .../IAttachmentObjectType.class.php | 84 +++++ .../AttachmentCleanUpCronjob.class.php | 43 +++ wcfsetup/install/files/style/attachment.less | 107 +++++++ wcfsetup/install/lang/de.xml | 49 +++ wcfsetup/install/lang/en.xml | 48 +++ wcfsetup/setup/db/install.sql | 38 +++ 28 files changed, 2365 insertions(+), 1 deletion(-) create mode 100644 com.woltlab.wcf/template/attachments.tpl create mode 100644 wcfsetup/install/files/acp/templates/attachmentList.tpl create mode 100644 wcfsetup/install/files/attachments/.htaccess create mode 100644 wcfsetup/install/files/js/WCF.Attachment.js create mode 100644 wcfsetup/install/files/js/WCF.Attachment.min.js create mode 100644 wcfsetup/install/files/lib/acp/page/AttachmentListPage.class.php create mode 100644 wcfsetup/install/files/lib/acp/page/AttachmentPage.class.php create mode 100644 wcfsetup/install/files/lib/data/attachment/AdministrativeAttachment.class.php create mode 100644 wcfsetup/install/files/lib/data/attachment/AdministrativeAttachmentList.class.php create mode 100644 wcfsetup/install/files/lib/data/attachment/Attachment.class.php create mode 100644 wcfsetup/install/files/lib/data/attachment/AttachmentAction.class.php create mode 100644 wcfsetup/install/files/lib/data/attachment/AttachmentEditor.class.php create mode 100644 wcfsetup/install/files/lib/data/attachment/AttachmentList.class.php create mode 100644 wcfsetup/install/files/lib/data/attachment/GroupedAttachmentList.class.php create mode 100644 wcfsetup/install/files/lib/page/AttachmentPage.class.php create mode 100644 wcfsetup/install/files/lib/system/attachment/AbstractAttachmentObjectType.class.php create mode 100644 wcfsetup/install/files/lib/system/attachment/AttachmentHandler.class.php create mode 100644 wcfsetup/install/files/lib/system/attachment/IAttachmentObjectType.class.php create mode 100644 wcfsetup/install/files/lib/system/cronjob/AttachmentCleanUpCronjob.class.php create mode 100644 wcfsetup/install/files/style/attachment.less diff --git a/com.woltlab.wcf/acpMenu.xml b/com.woltlab.wcf/acpMenu.xml index bb9d8ac855..616cbf171b 100644 --- a/com.woltlab.wcf/acpMenu.xml +++ b/com.woltlab.wcf/acpMenu.xml @@ -383,6 +383,16 @@ 4 + + wcf.acp.menu.link.content + + + + + wcf.acp.menu.link.attachment + admin.attachment.canManageAttachment + + 5 diff --git a/com.woltlab.wcf/cronjob.xml b/com.woltlab.wcf/cronjob.xml index e02f3fe8d4..c1118c213b 100644 --- a/com.woltlab.wcf/cronjob.xml +++ b/com.woltlab.wcf/cronjob.xml @@ -65,5 +65,18 @@ 1 1 + + + wcf\system\cronjob\AttachmentCleanUpCronjob + + + 0 + 2 + * + * + * + 1 + 1 + \ No newline at end of file diff --git a/com.woltlab.wcf/objectTypeDefinition.xml b/com.woltlab.wcf/objectTypeDefinition.xml index ed67be9398..383800688c 100644 --- a/com.woltlab.wcf/objectTypeDefinition.xml +++ b/com.woltlab.wcf/objectTypeDefinition.xml @@ -24,6 +24,11 @@ com.woltlab.wcf.versionableObject - + + + + com.woltlab.wcf.attachment.objectType + wcf\system\attachment\IAttachmentObjectType + \ No newline at end of file diff --git a/com.woltlab.wcf/option.xml b/com.woltlab.wcf/option.xml index e006d58102..927f8e04c4 100644 --- a/com.woltlab.wcf/option.xml +++ b/com.woltlab.wcf/option.xml @@ -120,6 +120,11 @@ message + + + message + module_attachment + @@ -169,6 +174,12 @@ 1 + + + + + + + diff --git a/com.woltlab.wcf/template/attachments.tpl b/com.woltlab.wcf/template/attachments.tpl new file mode 100644 index 0000000000..7ab676e2ec --- /dev/null +++ b/com.woltlab.wcf/template/attachments.tpl @@ -0,0 +1,55 @@ +{if $attachmentList && $attachmentList->getGroupedObjects($objectID)|count} + {hascontent} +
+
+ {lang}wcf.attachment.images{/lang} + +
    + {content} + {foreach from=$attachmentList->getGroupedObjects($objectID) item=attachment} + {if $attachment->showAsImage() && !$attachment->isEmbedded()} +
  • + {if $attachment->hasThumbnail()} + canDownload()} class="jsImageViewer" title="{$attachment->filename}"{/if}> + {else} + + {/if} + +
    +

    {$attachment->filename}

    + {lang}wcf.attachment.image.info{/lang} +
    +
  • + {/if} + {/foreach} + {/content} +
+
+
+ {/hascontent} + + {hascontent} +
+
+ {lang}wcf.attachment.files{/lang} + +
    + {content} + {foreach from=$attachmentList->getGroupedObjects($objectID) item=attachment} + {if $attachment->showAsFile() && !$attachment->isEmbedded()} +
  • + + +
    +

    {$attachment->filename}

    + {lang}wcf.attachment.file.info{/lang} +
    +
  • + {/if} + {/foreach} + {/content} +
+
+
+ {/hascontent} +{/if} \ No newline at end of file diff --git a/com.woltlab.wcf/userGroupOption.xml b/com.woltlab.wcf/userGroupOption.xml index e9e8c7355c..93aeacc000 100644 --- a/com.woltlab.wcf/userGroupOption.xml +++ b/com.woltlab.wcf/userGroupOption.xml @@ -6,6 +6,9 @@ user + + user.message + @@ -54,6 +57,9 @@ admin + + admin.content + admin @@ -223,6 +229,41 @@ 0 1 + + + + + + + diff --git a/wcfsetup/install/files/acp/templates/attachmentList.tpl b/wcfsetup/install/files/acp/templates/attachmentList.tpl new file mode 100644 index 0000000000..5447c9794b --- /dev/null +++ b/wcfsetup/install/files/acp/templates/attachmentList.tpl @@ -0,0 +1,148 @@ +{include file='header' pageTitle='wcf.acp.attachment.list'} + +{include file='imageViewer'} + + +
+

{lang}wcf.acp.attachment.list{/lang}

+

{lang}wcf.acp.attachment.stats{/lang}

+
+ +
+
+
+ {lang}wcf.global.filter{/lang} + +
+
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+
+
+ +
+ +
+
+ +
+ {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="AttachmentList" link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder$linkParameters"} + + {hascontent} + + {/hascontent} +
+ +{if $objects|count} +
+
+

{lang}wcf.acp.attachment.list{/lang} {#$items}

+
+ + + + + + + + + + + + {event name='columnHeads'} + + + + + {foreach from=$objects item=attachment} + + + + + + + + + + {event name='columns'} + + {/foreach} + +
{lang}wcf.global.objectID{/lang}{lang}wcf.attachment.filename{/lang}{lang}wcf.attachment.uploadTime{/lang}{lang}wcf.attachment.filesize{/lang}{lang}wcf.attachment.downloads{/lang}{lang}wcf.attachment.lastDownloadTime{/lang}
+ + + {event name='rowButtons'} + {@$attachment->attachmentID} + + {@$attachment->uploadTime|time}{@$attachment->filesize|filesize}{#$attachment->downloads}{if $attachment->lastDownloadTime}{@$attachment->lastDownloadTime|time}{/if}
+
+ +
+ {@$pagesLinks} + + {hascontent} + + {/hascontent} +
+{else} +

{lang}wcf.acp.attachment.noItems{/lang}

+{/if} + +{include file='footer'} diff --git a/wcfsetup/install/files/attachments/.htaccess b/wcfsetup/install/files/attachments/.htaccess new file mode 100644 index 0000000000..3418e55a68 --- /dev/null +++ b/wcfsetup/install/files/attachments/.htaccess @@ -0,0 +1 @@ +deny from all \ No newline at end of file diff --git a/wcfsetup/install/files/js/WCF.Attachment.js b/wcfsetup/install/files/js/WCF.Attachment.js new file mode 100644 index 0000000000..657b813163 --- /dev/null +++ b/wcfsetup/install/files/js/WCF.Attachment.js @@ -0,0 +1,253 @@ +/** + * Namespace for attachments + */ +WCF.Attachment = {}; + +/** + * Attachment upload function + * + * @see WCF.Upload + */ +WCF.Attachment.Upload = WCF.Upload.extend({ + /** + * object type of the object the uploaded attachments belong to + * @var string + */ + _objectType: '', + + /** + * id of the object the uploaded attachments belong to + * @var string + */ + _objectID: 0, + + /** + * temporary hash to identify uploaded attachments + * @var string + */ + _tmpHash: '', + + /** + * id of the parent object of the object the uploaded attachments belongs to + * @var string + */ + _parentObjectID: 0, + + /** + * container if of WYSIWYG editor + * @var string + */ + _wysiwygContainerID: '', + + /** + * @see WCF.Upload.init() + */ + init: function(buttonSelector, fileListSelector, objectType, objectID, tmpHash, parentObjectID, maxUploads, wysiwygContainerID) { + this._super(buttonSelector, fileListSelector, 'wcf\\data\\attachment\\AttachmentAction', { multiple: true, maxUploads: maxUploads }); + + this._objectType = objectType; + this._objectID = objectID; + this._tmpHash = tmpHash; + this._parentObjectID = parentObjectID; + this._wysiwygContainerID = wysiwygContainerID; + + this._buttonSelector.children('p.button').click($.proxy(this._validateLimit, this)); + this._fileListSelector.find('.jsButtonInsertAttachment').click($.proxy(this._insert, this)); + }, + + /** + * Validates upload limits. + * + * @return boolean + */ + _validateLimit: function() { + var $innerError = this._buttonSelector.next('small.innerError'); + + // check maximum uploads + var $max = this._options.maxUploads - this._fileListSelector.children('li:not(.uploadFailed)').length; + var $filesLength = (this._fileUpload) ? this._fileUpload.prop('files').length : 0; + if ($max <= 0 || $max < $filesLength) { + // reached limit + var $errorMessage = ($max <= 0) ? WCF.Language.get('wcf.attachment.upload.error.reachedLimit') : WCF.Language.get('wcf.attachment.upload.error.reachedRemainingLimit').replace(/#remaining#/, $max); + if (!$innerError.length) { + $innerError = $('').insertAfter(this._buttonSelector); + } + + $innerError.html($errorMessage); + + // reset value of file input (the 'files' prop is actually readonly!) + if (this._fileUpload) { + this._fileUpload.attr('value', ''); + } + + return false; + } + + // remove previous errors + $innerError.remove(); + + return true; + }, + + + /** + * @see WCF.Upload._upload() + */ + _upload: function() { + if (!this._validateLimit()) { + return false; + } + + this._super(); + + // reset value of file input (the 'files' prop is actually readonly!) + if (this._fileUpload) { + this._fileUpload.attr('value', ''); + } + }, + + /** + * @see WCF.Upload._createUploadMatrix() + */ + _createUploadMatrix: function(files) { + // remove failed uploads + this._fileListSelector.children('li.uploadFailed').remove(); + + return this._super(files); + }, + + /** + * @see WCF.Upload._getParameters() + */ + _getParameters: function() { + return { + objectType: this._objectType, + objectID: this._objectID, + tmpHash: this._tmpHash, + parentObjectID: this._parentObjectID + }; + }, + + /** + * @see WCF.Upload._initFile() + */ + _initFile: function(file) { + var $li = $('
  • '+file.name+'

    • '); + this._fileListSelector.append($li); + this._fileListSelector.show(); + + return $li; + }, + + /** + * @see WCF.Upload._success() + */ + _success: function(uploadID, data) { + for (var $i = 0; $i < this._uploadMatrix[uploadID].length; $i++) { + // get li + var $li = this._uploadMatrix[uploadID][$i]; + + // remove progress bar + $li.find('progress').remove(); + + // get filename and check result + var $filename = $li.data('filename'); + if (data.returnValues && data.returnValues['attachments'][$filename]) { + // show thumbnail + if (data.returnValues['attachments'][$filename]['tinyURL']) { + $li.children('.icon-spinner').replaceWith($('')); + } + // show file icon + else { + $li.children('.icon-spinner').removeClass('icon-spinner').addClass('icon-paper-clip'); + } + + // update attachment link + var $link = $(''); + $link.text($filename).attr('href', data.returnValues['attachments'][$filename]['url']); + + if (data.returnValues['attachments'][$filename]['isImage'] != 0) { + console.debug(data.returnValues['attachments'][$filename]['isImage']); + $link.addClass('jsImageViewer').attr('title', $filename); + } + $li.find('p').empty().append($link); + + // update file size + $li.find('small').append(data.returnValues['attachments'][$filename]['formattedFilesize']); + + // init buttons + var $deleteButton = $('
    • '); + $li.find('ul').append($deleteButton); + + var $insertButton = $('
    • '); + $insertButton.children('.jsButtonInsertAttachment').click($.proxy(this._insert, this)); + $li.find('ul').append($insertButton); + } + else { + // upload icon + $li.children('.icon-spinner').removeClass('icon-spinner').addClass('icon-ban-circle'); + var $errorMessage = ''; + + // error handling + if (data.returnValues && data.returnValues['errors'][$filename]) { + $errorMessage = data.returnValues['errors'][$filename]['errorType']; + } + else { + // unknown error + $errorMessage = 'uploadFailed'; + } + + $li.find('div > div').append($(''+WCF.Language.get('wcf.attachment.upload.error.'+$errorMessage)+'')); + $li.addClass('uploadFailed'); + } + + // fix webkit rendering bug + $li.css('display', 'block'); + } + + WCF.DOMNodeInsertedHandler.forceExecution(); + }, + + /** + * Inserts an attachment into WYSIWYG editor contents. + * + * @param object event + */ + _insert: function(event) { + var $attachmentID = $(event.currentTarget).data('objectID'); + var $bbcode = '[attach=' + $attachmentID + '][/attach]'; + + var $ckEditor = $('#' + this._wysiwygContainerID).ckeditorGet(); + if ($ckEditor.mode === 'wysiwyg') { + // in design mode + $ckEditor.insertText($bbcode); + } + else { + // in source mode + var $textarea = $('#' + this._wysiwygContainerID).next('.cke_editor_text').find('textarea'); + var $value = $textarea.val(); + if ($value.length == 0) { + $textarea.val($bbcode); + } + else { + var $position = $textarea.getCaret(); + $textarea.val( $value.substr(0, $position) + $bbcode + $value.substr($position) ); + } + } + }, + + /** + * @see WCF.Upload._error() + */ + _error: function() { + // mark uploads as failed + this._fileListSelector.find('li').each(function(index, listItem) { + var $listItem = $(listItem); + if ($listItem.children('.icon-spinner').length) { + // upload icon + $listItem.addClass('uploadFailed').children('.icon-spinner').removeClass('icon-spinner').addClass('icon-ban-circle'); + $listItem.find('div > div').append($(''+WCF.Language.get('wcf.attachment.upload.error.uploadFailed')+'')); + } + }); + } +}); diff --git a/wcfsetup/install/files/js/WCF.Attachment.min.js b/wcfsetup/install/files/js/WCF.Attachment.min.js new file mode 100644 index 0000000000..baf0f27157 --- /dev/null +++ b/wcfsetup/install/files/js/WCF.Attachment.min.js @@ -0,0 +1 @@ +WCF.Attachment={};WCF.Attachment.Upload=WCF.Upload.extend({_objectType:"",_objectID:0,_tmpHash:"",_parentObjectID:0,_wysiwygContainerID:"",init:function(c,g,a,h,d,e,f,b){this._super(c,g,"wcf\\data\\attachment\\AttachmentAction",{multiple:true,maxUploads:f});this._objectType=a;this._objectID=h;this._tmpHash=d;this._parentObjectID=e;this._wysiwygContainerID=b;this._buttonSelector.children("p.button").click($.proxy(this._validateLimit,this));this._fileListSelector.find(".jsButtonInsertAttachment").click($.proxy(this._insert,this))},_validateLimit:function(){var c=this._buttonSelector.next("small.innerError");var a=this._options.maxUploads-this._fileListSelector.children("li:not(.uploadFailed)").length;var d=(this._fileUpload)?this._fileUpload.prop("files").length:0;if(a<=0||a').insertAfter(this._buttonSelector)}c.html(b);if(this._fileUpload){this._fileUpload.attr("value","")}return false}c.remove();return true},_upload:function(){if(!this._validateLimit()){return false}this._super();if(this._fileUpload){this._fileUpload.attr("value","")}},_createUploadMatrix:function(a){this._fileListSelector.children("li.uploadFailed").remove();return this._super(a)},_getParameters:function(){return{objectType:this._objectType,objectID:this._objectID,tmpHash:this._tmpHash,parentObjectID:this._parentObjectID}},_initFile:function(a){var b=$('
    • '+a.name+'

      • ');this._fileListSelector.append(b);this._fileListSelector.show();return b},_success:function(b,c){for(var i=0;i'))}else{g.children(".icon-spinner").removeClass("icon-spinner").addClass("icon-paper-clip")}var e=$('');e.text(h).attr("href",c.returnValues.attachments[h]["url"]);if(c.returnValues.attachments[h]["isImage"]!=0){console.debug(c.returnValues.attachments[h]["isImage"]);e.addClass("jsImageViewer").attr("title",h)}g.find("p").empty().append(e);g.find("small").append(c.returnValues.attachments[h]["formattedFilesize"]);var f=$('
      • ');g.find("ul").append(f);var a=$('
      • ');a.children(".jsButtonInsertAttachment").click($.proxy(this._insert,this));g.find("ul").append(a)}else{g.children(".icon-spinner").removeClass("icon-spinner").addClass("icon-ban-circle");var d="";if(c.returnValues&&c.returnValues.errors[h]){d=c.returnValues.errors[h]["errorType"]}else{d="uploadFailed"}g.find("div > div").append($(''+WCF.Language.get("wcf.attachment.upload.error."+d)+""));g.addClass("uploadFailed")}g.css("display","block")}WCF.DOMNodeInsertedHandler.forceExecution()},_insert:function(e){var d=$(e.currentTarget).data("objectID");var c="[attach="+d+"][/attach]";var a=$("#"+this._wysiwygContainerID).ckeditorGet();if(a.mode==="wysiwyg"){a.insertText(c)}else{var g=$("#"+this._wysiwygContainerID).next(".cke_editor_text").find("textarea");var b=g.val();if(b.length==0){g.val(c)}else{var f=g.getCaret();g.val(b.substr(0,f)+c+b.substr(f))}}},_error:function(){this._fileListSelector.find("li").each(function(a,c){var b=$(c);if(b.children(".icon-spinner").length){b.addClass("uploadFailed").children(".icon-spinner").removeClass("icon-spinner").addClass("icon-ban-circle");b.find("div > div").append($(''+WCF.Language.get("wcf.attachment.upload.error.uploadFailed")+""))}})}}); \ No newline at end of file diff --git a/wcfsetup/install/files/lib/acp/page/AttachmentListPage.class.php b/wcfsetup/install/files/lib/acp/page/AttachmentListPage.class.php new file mode 100644 index 0000000000..f7960e6f41 --- /dev/null +++ b/wcfsetup/install/files/lib/acp/page/AttachmentListPage.class.php @@ -0,0 +1,141 @@ + + * @package com.woltlab.wcf.attachment + * @subpackage acp.page + * @category Community Framework + */ +class AttachmentListPage extends SortablePage { + /** + * @see wcf\page\AbstractPage::$activeMenuItem + */ + public $activeMenuItem = 'wcf.acp.menu.link.attachment.list'; + + /** + * @see wcf\page\AbstractPage::$neededPermissions + */ + public $neededPermissions = array('admin.attachment.canManageAttachment'); + + /** + * @see wcf\page\SortablePage::$defaultSortField + */ + public $defaultSortField = 'uploadTime'; + + /** + * @see wcf\page\SortablePage::$defaultSortOrder + */ + public $defaultSortOrder = 'DESC'; + + /** + * @see wcf\page\SortablePage::$validSortFields + */ + public $validSortFields = array('attachmentID', 'filename', 'filesize', 'downloads', 'uploadTime', 'lastDownloadTime'); + + /** + * @see wcf\page\MultipleLinkPage::$objectListClassName + */ + public $objectListClassName = 'wcf\data\attachment\AdministrativeAttachmentList'; + + /** + * username + * @var string + */ + public $username = ''; + + /** + * filename + * @var string + */ + public $filename = ''; + + /** + * file type + * @var string + */ + public $fileType = ''; + + /** + * available file types + * @var array + */ + public $availableFileTypes = array(); + + /** + * attachment stats + * @var array + */ + public $stats = array(); + + /** + * @see wcf\page\IPage::readParameters() + */ + public function readParameters() { + parent::readParameters(); + + if (!empty($_REQUEST['username'])) $this->username = StringUtil::trim($_REQUEST['username']); + if (!empty($_REQUEST['filename'])) $this->filename = StringUtil::trim($_REQUEST['filename']); + if (!empty($_REQUEST['fileType'])) $this->fileType = $_REQUEST['fileType']; + } + + /** + * @see wcf\page\MultipleLinkPage::initObjectList + */ + protected function initObjectList() { + parent::initObjectList(); + + $objectTypeIDs = array(); + foreach (ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.attachment.objectType') as $objectType) { + if (!$objectType->private) { + $objectTypeIDs[] = $objectType->objectTypeID; + } + } + + if (!empty($objectTypeIDs)) $this->objectList->getConditionBuilder()->add('attachment.objectTypeID IN (?)', array($objectTypeIDs)); + else $this->objectList->getConditionBuilder()->add('1 = 0'); + $this->objectList->getConditionBuilder()->add("attachment.tmpHash = ''"); + + // get data + $this->stats = $this->objectList->getStats(); + $this->availableFileTypes = $this->objectList->getAvailableFileTypes(); + + // filter + if (!empty($this->username)) { + $user = User::getUserByUsername($this->username); + if ($user->userID) { + $this->objectList->getConditionBuilder()->add('attachment.userID = ?', array($user->userID)); + } + } + if (!empty($this->filename)) { + $this->objectList->getConditionBuilder()->add('attachment.filename LIKE ?', array($this->filename.'%')); + } + if (!empty($this->fileType)) { + $this->objectList->getConditionBuilder()->add('attachment.fileType LIKE ?', array($this->fileType)); + } + } + + /** + * @see wcf\page\IPage::assignVariables() + */ + public function assignVariables() { + parent::assignVariables(); + + WCF::getTPL()->assign(array( + 'stats' => $this->stats, + 'username' => $this->username, + 'filename' => $this->filename, + 'fileType' => $this->fileType, + 'availableFileTypes' => $this->availableFileTypes + )); + } +} diff --git a/wcfsetup/install/files/lib/acp/page/AttachmentPage.class.php b/wcfsetup/install/files/lib/acp/page/AttachmentPage.class.php new file mode 100644 index 0000000000..24d240c095 --- /dev/null +++ b/wcfsetup/install/files/lib/acp/page/AttachmentPage.class.php @@ -0,0 +1,31 @@ + + * @package com.woltlab.wcf.attachment + * @subpackage acp.page + * @category Community Framework + */ +class AttachmentPage extends \wcf\page\AttachmentPage { + /** + * @see wcf\page\IPage::checkPermissions() + */ + public function checkPermissions() { + if ($this->attachment->tmpHash) { + throw new PermissionDeniedException(); + } + + // check private status of attachment's object type + $objectType = ObjectTypeCache::getInstance()->getObjectType($this->attachment->objectTypeID); + if ($objectType->private) { + throw new PermissionDeniedException(); + } + } +} diff --git a/wcfsetup/install/files/lib/data/attachment/AdministrativeAttachment.class.php b/wcfsetup/install/files/lib/data/attachment/AdministrativeAttachment.class.php new file mode 100644 index 0000000000..092b543105 --- /dev/null +++ b/wcfsetup/install/files/lib/data/attachment/AdministrativeAttachment.class.php @@ -0,0 +1,49 @@ + + * @package com.woltlab.wcf.attachment + * @subpackage data.attachment + * @category Community Framework + */ +class AdministrativeAttachment extends DatabaseObjectDecorator { + /** + * @see wcf\data\DatabaseObjectDecorator::$baseClass + */ + protected static $baseClass = 'wcf\data\attachment\Attachment'; + + /** + * container object + * @var wcf\data\IUserContent + */ + protected $containerObject = null; + + /** + * true if container object has been loaded + * @var boolean + */ + protected $containerObjectLoaded = false; + + /** + * Gets the container object of this attachment. + * + * @return \wcf\data\IUserContent + */ + public function getContainerObject() { + if (!$this->containerObjectLoaded) { + $this->containerObjectLoaded = true; + + $objectType = ObjectTypeCache::getInstance()->getObjectType($this->objectTypeID); + $this->containerObject = $objectType->getProcessor()->getObject($this->objectID); + } + + return $this->containerObject; + } +} diff --git a/wcfsetup/install/files/lib/data/attachment/AdministrativeAttachmentList.class.php b/wcfsetup/install/files/lib/data/attachment/AdministrativeAttachmentList.class.php new file mode 100644 index 0000000000..3caaf5add8 --- /dev/null +++ b/wcfsetup/install/files/lib/data/attachment/AdministrativeAttachmentList.class.php @@ -0,0 +1,89 @@ + + * @package com.woltlab.wcf.attachment + * @subpackage data.attachment + * @category Community Framework + */ +class AdministrativeAttachmentList extends AttachmentList { + /** + * @see wcf\data\DatabaseObjectList::$decoratorClassName + */ + public $decoratorClassName = 'wcf\data\attachment\AdministrativeAttachment'; + + /** + * Creates a new AdministrativeAttachmentList object. + */ + public function __construct() { + parent::__construct(); + + $this->sqlSelects = 'user_table.username'; + $this->sqlJoins = " LEFT JOIN wcf".WCF_N."_user user_table ON (user_table.userID = attachment.userID)"; + } + + /** + * @see wcf\data\DatabaseObjectList::readObjects() + */ + public function readObjects() { + parent::readObjects(); + + // cache objects + $groupedObjectIDs = array(); + foreach ($this->objects as $attachment) { + if (!isset($groupedObjectIDs[$attachment->objectTypeID])) $groupedObjectIDs[$attachment->objectTypeID] = array(); + $groupedObjectIDs[$attachment->objectTypeID][] = $attachment->objectID; + } + + foreach ($groupedObjectIDs as $objectTypeID => $objectIDs) { + $objectType = ObjectTypeCache::getInstance()->getObjectType($objectTypeID); + $objectType->getProcessor()->cacheObjects($objectIDs); + } + } + + /** + * Returns a list of available mime types. + * + * @return array + */ + public function getAvailableFileTypes() { + $fileTypes = array(); + $sql = "SELECT DISTINCT attachment.fileType + FROM wcf".WCF_N."_attachment attachment + ".$this->getConditionBuilder(); + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute($this->getConditionBuilder()->getParameters()); + while ($row = $statement->fetchArray()) { + $fileTypes[$row['fileType']] = $row['fileType']; + } + + ksort($fileTypes); + + return $fileTypes; + } + + /** + * Returns attachment statistics. + * + * @return array + */ + public function getStats() { + $sql = "SELECT COUNT(*) AS count, + IFNULL(SUM(attachment.filesize), 0) AS size, + IFNULL(SUM(downloads), 0) AS downloads + FROM wcf".WCF_N."_attachment attachment + ".$this->getConditionBuilder(); + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute($this->getConditionBuilder()->getParameters()); + $row = $statement->fetchArray(); + + return $row; + } +} diff --git a/wcfsetup/install/files/lib/data/attachment/Attachment.class.php b/wcfsetup/install/files/lib/data/attachment/Attachment.class.php new file mode 100644 index 0000000000..508a470414 --- /dev/null +++ b/wcfsetup/install/files/lib/data/attachment/Attachment.class.php @@ -0,0 +1,205 @@ + + * @package com.woltlab.wcf.attachment + * @subpackage data.attachment + * @category Community Framework + */ +class Attachment extends DatabaseObject implements IRouteController { + /** + * @see wcf\data\DatabaseObject::$databaseTableName + */ + protected static $databaseTableName = 'attachment'; + + /** + * @see wcf\data\DatabaseObject::$databaseTableIndexName + */ + protected static $databaseTableIndexName = 'attachmentID'; + + /** + * indicates if the attachment is embedded + * @var boolean + */ + protected $embedded = false; + + /** + * user permissions for attachment access + * @var array + */ + protected $permissions = array(); + + /** + * Returns true if a user has the permission to download this attachment. + * + * @return boolean + */ + public function canDownload() { + return $this->getPermission('canDownload'); + } + + /** + * Returns true if a user has the permission to view the preview of this + * attachment. + * + * @return boolean + */ + public function canViewPreview() { + return $this->getPermission('canViewPreview'); + } + + /** + * Returns true if a user has the permission to delete the preview of this + * attachment. + * + * @return boolean + */ + public function canDelete() { + return $this->getPermission('canDelete'); + } + + /** + * Checks permissions. + * + * @param string $permission + * @return boolean + */ + protected function getPermission($permission) { + if (!isset($this->permissions[$permission])) { + $this->permissions[$permission] = true; + + if ($this->tmpHash) { + if ($this->userID && $this->userID != WCF::getUser()->userID) { + $this->permissions[$permission] = false; + } + } + else { + $objectType = ObjectTypeCache::getInstance()->getObjectType($this->objectTypeID); + $processor = $objectType->getProcessor(); + if ($processor !== null) { + $this->permissions[$permission] = call_user_func(array($processor, $permission), $this->objectID); + } + } + } + + return $this->permissions[$permission]; + } + + /** + * Sets the permissions for attachment access. + * + * @param array $permissions + */ + public function setPermissions(array $permissions) { + $this->permissions = $permissions; + } + + /** + * Returns the physical location of this attachment. + * + * @return string + */ + public function getLocation() { + return self::getStorage() . substr($this->fileHash, 0, 2) . '/' . ($this->attachmentID) . '-' . $this->fileHash; + } + + /** + * Returns the physical location of the tiny thumbnail. + * + * @return string + */ + public function getTinyThumbnailLocation() { + return self::getStorage() . substr($this->fileHash, 0, 2) . '/' . ($this->attachmentID) . '-tiny-' . $this->fileHash; + } + + /** + * Returns the physical location of the standard thumbnail. + * + * @return string + */ + public function getThumbnailLocation() { + return self::getStorage() . substr($this->fileHash, 0, 2) . '/' . ($this->attachmentID) . '-thumbnail-' . $this->fileHash; + } + + /** + * @see wcf\system\request\IRouteController::getTitle() + */ + public function getTitle() { + return $this->filename; + } + + /** + * Marks this attachment as embedded. + * + * @return boolean + */ + public function markAsEmbedded() { + $this->embedded = true; + } + + /** + * Returns true if this attachment is embedded. + * + * @return boolean + */ + public function isEmbedded() { + return $this->embedded; + } + + /** + * Returns true if this attachment should be shown as an image. + * + * @return boolean + */ + public function showAsImage() { + if ($this->isImage) { + if (!$this->hasThumbnail() && ($this->width > ATTACHMENT_THUMBNAIL_WIDTH || $this->height > ATTACHMENT_THUMBNAIL_HEIGHT)) return false; + + if ($this->canDownload()) return true; + + if ($this->canViewPreview() && $this->hasThumbnail()) return true; + } + + return false; + } + + /** + * Returns true if this attachment has a thumbnail. + * + * @return boolean + */ + public function hasThumbnail() { + return ($this->thumbnailType ? true : false); + } + + /** + * Returns true if this attachment should be shown as a file. + * + * @return boolean + */ + public function showAsFile() { + return !$this->showAsImage(); + } + + /** + * Returns the storage path. + * + * @return string + */ + public static function getStorage() { + if (ATTACHMENT_STORAGE) { + return ATTACHMENT_STORAGE; + } + + return WCF_DIR . 'attachments/'; + } +} diff --git a/wcfsetup/install/files/lib/data/attachment/AttachmentAction.class.php b/wcfsetup/install/files/lib/data/attachment/AttachmentAction.class.php new file mode 100644 index 0000000000..e1e81e5f25 --- /dev/null +++ b/wcfsetup/install/files/lib/data/attachment/AttachmentAction.class.php @@ -0,0 +1,290 @@ + + * @package com.woltlab.wcf.attachment + * @subpackage data.attachment + * @category Community Framework + */ +class AttachmentAction extends AbstractDatabaseObjectAction { + /** + * @see wcf\data\AbstractDatabaseObjectAction::$allowGuestAccess + */ + protected $allowGuestAccess = array('delete', 'upload'); + + /** + * @see wcf\data\AbstractDatabaseObjectAction::$className + */ + protected $className = 'wcf\data\attachment\AttachmentEditor'; + + /** + * current attachment object, used to communicate with event listeners + * @var wcf\data\attachment\Attachment + */ + public $eventAttachment = null; + + /** + * current data, used to communicate with event listeners. + * @var array + */ + public $eventData = array(); + + /** + * Validates the delete action. + */ + public function validateDelete() { + // read objects + if (empty($this->objects)) { + $this->readObjects(); + + if (empty($this->objects)) { + throw new UserInputException('objectIDs'); + } + } + + foreach ($this->objects as $attachment) { + if ($attachment->tmpHash) { + if ($attachment->userID != WCF::getUser()->userID) { + throw new PermissionDeniedException(); + } + } + else if (!$attachment->canDelete()) { + throw new PermissionDeniedException(); + } + } + } + + /** + * Validates the upload action. + */ + public function validateUpload() { + // IE<10 fallback + if (isset($_POST['isFallback'])) { + $this->parameters['objectType'] = (isset($_POST['objectType'])) ? $_POST['objectType'] : ''; + $this->parameters['objectID'] = (isset($_POST['objectID'])) ? $_POST['objectID'] : 0; + $this->parameters['parentObjectID'] = (isset($_POST['parentObjectID'])) ? $_POST['parentObjectID'] : 0; + $this->parameters['tmpHash'] = (isset($_POST['tmpHash'])) ? $_POST['tmpHash'] : ''; + } + + // read variables + $this->readString('objectType'); + $this->readInteger('objectID', true); + $this->readInteger('parentObjectID', true); + $this->readString('tmpHash'); + + // validate object type + $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.attachment.objectType', $this->parameters['objectType']); + if ($objectType === null) { + throw new UserInputException('objectType'); + } + + // get processor + $processor = $objectType->getProcessor(); + + // check upload permissions + if (!$processor->canUpload((!empty($this->parameters['objectID']) ? intval($this->parameters['objectID']) : 0), (!empty($this->parameters['parentObjectID']) ? intval($this->parameters['parentObjectID']) : 0))) { + throw new PermissionDeniedException(); + } + + // 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( + 'current' => $handler->count(), + 'quota' => $processor->getMaxCount() + )); + } + + // check max filesize, allowed file extensions etc. + $this->parameters['__files']->validateFiles(new DefaultUploadFileValidationStrategy($processor->getMaxSize(), $processor->getAllowedExtensions())); + } + + /** + * Handles uploaded attachments. + */ + 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['isImage'] = 1; + $data['width'] = $imageData['width']; + $data['height'] = $imageData['height']; + $data['fileType'] = $imageData['mimeType']; + } + + // create attachment + $attachment = AttachmentEditor::create($data); + + // check attachment directory + // and create subdirectory if necessary + $dir = dirname($attachment->getLocation()); + if (!@file_exists($dir)) { + @mkdir($dir, 0777); + } + + // move uploaded file + if (@move_uploaded_file($file->getLocation(), $attachment->getLocation())) { + if ($attachment->isImage) { + $thumbnails[] = $attachment; + } + 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[] = $attachment; + } + else { + // moving failed; delete attachment + $editor = new AttachmentEditor($attachment); + $editor->delete(); + } + } + + // generate thumbnails + if (ATTACHMENT_ENABLE_THUMBNAILS) { + if (!empty($thumbnails)) { + $action = new AttachmentAction($thumbnails, 'generateThumbnails'); + $action->executeAction(); + } + } + + // return result + $result = array('attachments' => array(), 'errors' => array()); + if (!empty($attachments)) { + // get attachment ids + $attachmentIDs = array(); + foreach ($attachments as $attachment) $attachmentIDs[] = $attachment->attachmentID; + + // get attachments from database (check thumbnail status) + $attachmentList = new AttachmentList(); + $attachmentList->getConditionBuilder()->add('attachment.attachmentID IN (?)', array($attachmentIDs)); + $attachmentList->readObjects(); + + foreach ($attachmentList as $attachment) { + $result['attachments'][$attachment->filename] = array( + '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)) + ); + } + } + + foreach ($failedUploads as $failedUpload) { + $result['errors'][$failedUpload->getFilename()] = array( + 'filename' => $failedUpload->getFilename(), + 'filesize' => $failedUpload->getFilesize(), + 'errorType' => $failedUpload->getValidationErrorType() + ); + } + + return $result; + } + + /** + * Generates thumbnails. + */ + public function generateThumbnails() { + if (!empty($this->objects)) { + $this->readObjects(); + } + + foreach ($this->objects as $attachment) { + if (!$attachment->isImage) { + // create thumbnails for every file that isn't an image + $this->eventAttachment = $attachment; + $this->eventData = array(); + + EventHandler::getInstance()->fireAction($this, 'generateThumbnail'); + + if (!empty($this->eventData)) { + $attachment->update($this->eventData); + } + + continue; + } + + if ($attachment->width <= 144 && $attachment->height < 144) { + continue; // image smaller than thumbnail size; skip + } + + $adapter = ImageHandler::getInstance()->getAdapter(); + $adapter->loadFile($attachment->getLocation()); + $updateData = array(); + + // 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, false); + $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); + } + } + } +} diff --git a/wcfsetup/install/files/lib/data/attachment/AttachmentEditor.class.php b/wcfsetup/install/files/lib/data/attachment/AttachmentEditor.class.php new file mode 100644 index 0000000000..0218f6d064 --- /dev/null +++ b/wcfsetup/install/files/lib/data/attachment/AttachmentEditor.class.php @@ -0,0 +1,68 @@ + + * @package com.woltlab.wcf.attachment + * @subpackage data.attachment + * @category Community Framework + */ +class AttachmentEditor extends DatabaseObjectEditor { + /** + * @see wcf\data\DatabaseObjectDecorator::$baseClass + */ + public static $baseClass = 'wcf\data\attachment\Attachment'; + + /** + * @see wcf\data\IEditableObject::delete() + */ + public function delete() { + $sql = "DELETE FROM wcf".WCF_N."_attachment + WHERE attachmentID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array($this->attachmentID)); + + $this->deleteFiles(); + } + + /** + * @see wcf\data\IEditableObject::deleteAll() + */ + public static function deleteAll(array $objectIDs = array()) { + // delete files first + $conditionBuilder = new PreparedStatementConditionBuilder(); + $conditionBuilder->add("attachmentID IN (?)", array($objectIDs)); + + $sql = "SELECT * + FROM wcf".WCF_N."_attachment + ".$conditionBuilder; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute($conditionBuilder->getParameters()); + while ($attachment = $statement->fetchObject(static::$baseClass)) { + $editor = new AttachmentEditor($attachment); + $editor->deleteFiles(); + } + + return parent::deleteAll($objectIDs); + } + + /** + * Deletes attachment files. + */ + public function deleteFiles() { + @unlink($this->getLocation()); + if ($this->tinyThumbnailType) { + @unlink($this->getTinyThumbnailLocation()); + } + if ($this->thumbnailType) { + @unlink($this->getThumbnailLocation()); + } + } +} diff --git a/wcfsetup/install/files/lib/data/attachment/AttachmentList.class.php b/wcfsetup/install/files/lib/data/attachment/AttachmentList.class.php new file mode 100644 index 0000000000..299191a133 --- /dev/null +++ b/wcfsetup/install/files/lib/data/attachment/AttachmentList.class.php @@ -0,0 +1,20 @@ + + * @package com.woltlab.wcf.attachment + * @subpackage data.attachment + * @category Community Framework + */ +class AttachmentList extends DatabaseObjectList { + /** + * @see wcf\data\DatabaseObjectList::$className + */ + public $className = 'wcf\data\attachment\Attachment'; +} diff --git a/wcfsetup/install/files/lib/data/attachment/GroupedAttachmentList.class.php b/wcfsetup/install/files/lib/data/attachment/GroupedAttachmentList.class.php new file mode 100644 index 0000000000..de0175e88c --- /dev/null +++ b/wcfsetup/install/files/lib/data/attachment/GroupedAttachmentList.class.php @@ -0,0 +1,89 @@ + + * @package com.woltlab.wcf.attachment + * @subpackage data.attachment + * @category Community Framework + */ +class GroupedAttachmentList extends AttachmentList { + /** + * grouped objects + * @var array + */ + public $groupedObjects = array(); + + /** + * object type + * @var wcf\data\object\type\ObjectType + */ + protected $objectType = null; + + /** + * wcf\data\DatabaseObjectList::$sqlLimit + */ + public $sqlLimit = 0; + + /** + * wcf\data\DatabaseObjectList::$sqlOrderBy + */ + public $sqlOrderBy = 'attachment.showOrder'; + + /** + * Creates a new GroupedAttachmentList object. + * + * @param string $objectType + */ + public function __construct($objectType) { + parent::__construct(); + + $this->objectType = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.attachment.objectType', $objectType); + $this->getConditionBuilder()->add('attachment.objectTypeID = ?', array($this->objectType->objectTypeID)); + } + + /** + * @see wcf\data\DatabaseObjectList::readObjects() + */ + public function readObjects() { + parent::readObjects(); + + // group by object id + foreach ($this->objects as $attachmentID => $attachment) { + if (!isset($this->groupedObjects[$attachment->objectID])) { + $this->groupedObjects[$attachment->objectID] = array(); + } + + $this->groupedObjects[$attachment->objectID][$attachmentID] = $attachment; + } + } + + /** + * Sets the permissions for attachment access. + * + * @param array $permissions + */ + public function setPermissions(array $permissions) { + foreach ($this->objects as $attachment) { + $attachment->setPermissions($permissions); + } + } + + /** + * Returns the objects of the list. + * + * @return array + */ + public function getGroupedObjects($objectID) { + if (isset($this->groupedObjects[$objectID])) { + return $this->groupedObjects[$objectID]; + } + + return array(); + } +} diff --git a/wcfsetup/install/files/lib/page/AttachmentPage.class.php b/wcfsetup/install/files/lib/page/AttachmentPage.class.php new file mode 100644 index 0000000000..1b80c81867 --- /dev/null +++ b/wcfsetup/install/files/lib/page/AttachmentPage.class.php @@ -0,0 +1,205 @@ + + * @package com.woltlab.wcf.attachment + * @subpackage page + * @category Community Framework + */ +class AttachmentPage extends AbstractPage { + /** + * @see wcf\page\IPage::$useTemplate + */ + public $useTemplate = false; + + /** + * attachment id + * @var integer + */ + public $attachmentID = 0; + + /** + * attachment object + * @var wcf\data\attachment\Attachment + */ + public $attachment = null; + + /** + * shows the tiny thumbnail + * @var boolean + */ + public $tiny = 0; + + /** + * shows the standard thumbnail + * @var boolean + */ + public $thumbnail = 0; + + /** + * list of mime types which belong to files that are displayed inline + * @var array + */ + public static $inlineMimeTypes = array('image/gif', 'image/jpeg', 'image/png', 'application/pdf', 'image/pjpeg'); + + /** + * @see wcf\page\IPage::readParameters() + */ + public function readParameters() { + parent::readParameters(); + + if (isset($_REQUEST['id'])) $this->attachmentID = intval($_REQUEST['id']); + $this->attachment = new Attachment($this->attachmentID); + if (!$this->attachment->attachmentID) { + throw new IllegalLinkException(); + } + if (isset($_REQUEST['tiny']) && $this->attachment->tinyThumbnailType) $this->tiny = intval($_REQUEST['tiny']); + if (isset($_REQUEST['thumbnail']) && $this->attachment->thumbnailType) $this->thumbnail = intval($_REQUEST['thumbnail']); + } + + /** + * @see wcf\page\IPage::checkPermissions() + */ + public function checkPermissions() { + parent::checkPermissions(); + + if ($this->attachment->tmpHash) { + if ($this->attachment->userID && $this->attachment->userID != WCF::getUser()->userID) { + throw new IllegalLinkException(); + } + } + else { + // check permissions + if ($this->tiny || $this->thumbnail) { + if (!$this->attachment->canViewPreview()) { + throw new PermissionDeniedException(); + } + } + else if (!$this->attachment->canDownload()) { + throw new PermissionDeniedException(); + } + } + } + + /** + * @see wcf\page\IPage::show() + */ + public function show() { + parent::show(); + + // update download count + if (!$this->tiny && !$this->thumbnail) { + $editor = new AttachmentEditor($this->attachment); + $editor->update(array( + 'downloads' => $this->attachment->downloads + 1, + 'lastDownloadTime' => TIME_NOW + )); + } + + // get file data + if ($this->tiny) { + $mimeType = $this->attachment->tinyThumbnailType; + $filesize = $this->attachment->tinyThumbnailSize; + $location = $this->attachment->getTinyThumbnailLocation(); + } + else if ($this->thumbnail) { + $mimeType = $this->attachment->thumbnailType; + $filesize = $this->attachment->thumbnailSize; + $location = $this->attachment->getThumbnailLocation(); + } + else { + $mimeType = $this->attachment->fileType; + $filesize = $this->attachment->filesize; + $location = $this->attachment->getLocation(); + } + + // range support + $startByte = 0; + $endByte = $filesize - 1; + if (!$this->tiny && !$this->thumbnail) { + if (!empty($_SERVER['HTTP_RANGE'])) { + $regex = new Regex('^bytes=(-?\d+)(?:-(\d+))?$'); + if ($regex->match($_SERVER['HTTP_RANGE'])) { + $matches = $regex->getMatches(); + $first = intval($matches[1]); + $last = (isset($matches[2]) ? intval($matches[2]) : 0); + + if ($first < 0) { + // negative value; subtract from filesize + $startByte = $filesize + $first; + } + else { + $startByte = $first; + if ($last > 0) { + $endByte = $last; + } + } + + // validate given range + if ($startByte < 0 || $startByte >= $filesize || $endByte >= $filesize) { + // invalid range given + @header('HTTP/1.1 416 Requested Range Not Satisfiable'); + @header('Accept-Ranges: bytes'); + @header('Content-Range: bytes */'.$filesize); + exit; + } + } + } + } + + // send headers + // file type + if ($mimeType == 'image/x-png') $mimeType = 'image/png'; + @header('Content-Type: '.$mimeType); + + // file name + @header('Content-disposition: '.(!in_array($mimeType, self::$inlineMimeTypes) ? 'attachment; ' : 'inline; ').'filename="'.$this->attachment->filename.'"'); + + // range + if ($startByte > 0 || $endByte < $filesize - 1) { + @header('HTTP/1.1 206 Partial Content'); + @header('Content-Range: bytes '.$startByte.'-'.$endByte.'/'.$filesize); + } + if (!$this->tiny && !$this->thumbnail) { + @header('ETag: "'.$this->attachmentID.'"'); + @header('Accept-Ranges: bytes'); + } + + // send file size + @header('Content-Length: '.($endByte + 1 - $startByte)); + + // cache headers + @header('Cache-control: max-age=31536000, private'); + @header('Expires: '.gmdate('D, d M Y H:i:s', TIME_NOW + 31536000).' GMT'); + @header('Last-Modified: '.gmdate('D, d M Y H:i:s', $this->attachment->uploadTime).' GMT'); + + // show attachment + if ($startByte > 0 || $endByte < $filesize - 1) { + $file = new File($location, 'rb'); + if ($startByte > 0) $file->seek($startByte); + while ($startByte <= $endByte) { + $remainingBytes = $endByte - $startByte; + $readBytes = ($remainingBytes > 1048576) ? 1048576 : $remainingBytes + 1; + echo $file->read($readBytes); + $startByte += $readBytes; + } + $file->close(); + } + else { + readfile($location); + } + exit; + } +} diff --git a/wcfsetup/install/files/lib/system/attachment/AbstractAttachmentObjectType.class.php b/wcfsetup/install/files/lib/system/attachment/AbstractAttachmentObjectType.class.php new file mode 100644 index 0000000000..5c3d75c19e --- /dev/null +++ b/wcfsetup/install/files/lib/system/attachment/AbstractAttachmentObjectType.class.php @@ -0,0 +1,57 @@ + + * @package com.woltlab.wcf.attachment + * @subpackage system.attachment + * @category Community Framework + */ +abstract class AbstractAttachmentObjectType implements IAttachmentObjectType { + /** + * @see wcf\system\attachment\IAttachmentObjectType::getMaxSize() + */ + public function getMaxSize() { + return WCF::getSession()->getPermission('user.attachment.maxSize'); + } + + /** + * @see wcf\system\attachment\IAttachmentObjectType::getAllowedExtensions() + */ + public function getAllowedExtensions() { + return ArrayUtil::trim(explode("\n", WCF::getSession()->getPermission('user.attachment.allowedExtensions'))); + } + + /** + * @see wcf\system\attachment\IAttachmentObjectType::getMaxCount() + */ + public function getMaxCount() { + return WCF::getSession()->getPermission('user.attachment.maxCount'); + } + + /** + * @see wcf\system\attachment\IAttachmentObjectType::canViewPreview() + */ + public function canViewPreview($objectID) { + return $this->canDownload($objectID); + } + + /** + * @see wcf\system\attachment\IAttachmentObjectType::getObject() + */ + public function getObject($objectID) { + return null; + } + + /** + * @see wcf\system\attachment\IAttachmentObjectType::getObject() + */ + public function cacheObjects(array $objectIDs) {} +} diff --git a/wcfsetup/install/files/lib/system/attachment/AttachmentHandler.class.php b/wcfsetup/install/files/lib/system/attachment/AttachmentHandler.class.php new file mode 100644 index 0000000000..c0b1dad0c1 --- /dev/null +++ b/wcfsetup/install/files/lib/system/attachment/AttachmentHandler.class.php @@ -0,0 +1,192 @@ + + * @package com.woltlab.wcf.attachment + * @subpackage system.attachment + * @category Community Framework + */ +class AttachmentHandler implements \Countable { + /** + * object type + * @var wcf\data\object\type\ObjectType + */ + protected $objectType = null; + + /** + * object type + * @var wcf\system\attachment\IAttachmentObjectType + */ + protected $processor = null; + + /** + * object id + * @var integer + */ + protected $objectID = 0; + + /** + * parent object id + * @var integer + */ + protected $parentObjectID = 0; + + /** + * temp hash + * @var string + */ + protected $tmpHash = ''; + + /** + * list of attachments + * @var wcf\data\attachment\AttachmentList + */ + protected $attachmentList = null; + + /** + * Creates a new AttachmentHandler object. + * + * @param string $objectType + * @param integer $objectID + * @param string $tmpHash + */ + public function __construct($objectType, $objectID, $tmpHash = '', $parentObjectID = 0) { + $this->objectType = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.attachment.objectType', $objectType); + $this->processor = $this->objectType->getProcessor(); + $this->objectID = $objectID; + $this->parentObjectID = $parentObjectID; + $this->tmpHash = $tmpHash; + } + + /** + * Returns a list of attachments. + * + * @return wcf\data\attachment\AttachmentList + */ + public function getAttachmentList() { + if ($this->attachmentList === null) { + $this->attachmentList = new AttachmentList(); + $this->attachmentList->sqlOrderBy = 'attachment.showOrder'; + $this->attachmentList->getConditionBuilder()->add('objectTypeID = ?', array($this->objectType->objectTypeID)); + if ($this->objectID) { + $this->attachmentList->getConditionBuilder()->add('objectID = ?', array($this->objectID)); + } + else { + $this->attachmentList->getConditionBuilder()->add('tmpHash = ?', array($this->tmpHash)); + } + $this->attachmentList->readObjects(); + } + + return $this->attachmentList; + } + + /** + * @see \Countable::count() + */ + public function count() { + return count($this->getAttachmentList()); + } + + /** + * Sets the object id of temporary saved attachments. + * + * @param integer $objectID + */ + public function updateObjectID($objectID) { + $sql = "UPDATE wcf".WCF_N."_attachment + SET objectID = ?, + tmpHash = '' + WHERE objectTypeID = ? + AND tmpHash = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array($objectID, $this->objectType->objectTypeID, $this->tmpHash)); + } + + /** + * Transfers attachments to a different object id of the same type (e.g. merging content) + * + * @param string $objectType + * @param integer $newObjectID + * @param array $oldObjectIDs + */ + public static function transferAttachments($objectType, $newObjectID, array $oldObjectIDs) { + $conditions = new PreparedStatementConditionBuilder(); + $conditions->add("objectTypeID = ?", array(ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.attachment.objectType', $objectType)->objectTypeID)); + $conditions->add("objectID IN (?)", array($oldObjectIDs)); + $parameters = $conditions->getParameters(); + array_unshift($parameters, $newObjectID); + + $sql = "UPDATE wcf".WCF_N."_attachment + SET objectID = ? + ".$conditions; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute($parameters); + } + + /** + * Removes all attachments for given object ids by type. + * + * @param string $objectType + * @param array $objectIDs + */ + public static function removeAttachments($objectType, array $objectIDs) { + $attachmentList = new AttachmentList(); + $attachmentList->getConditionBuilder()->add("objectTypeID = ?", array(ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.attachment.objectType', $objectType)->objectTypeID)); + $attachmentList->getConditionBuilder()->add("objectID IN (?)", array($objectIDs)); + $attachmentList->readObjects(); + + if (count($attachmentList)) { + $attachmentAction = new AttachmentAction($attachmentList->getObjects(), 'delete'); + $attachmentAction->executeAction(); + } + } + + /** + * @see wcf\system\attachment\IAttachmentObjectType::getMaxSize() + */ + public function getMaxSize() { + return $this->processor->getMaxSize(); + } + + /** + * @see wcf\system\attachment\IAttachmentObjectType::getAllowedExtensions() + */ + public function getAllowedExtensions() { + return $this->processor->getAllowedExtensions(); + } + + /** + * @see wcf\system\attachment\IAttachmentObjectType::getMaxCount() + */ + public function getMaxCount() { + return $this->processor->getMaxCount(); + } + + /** + * Returns true if the active user has the permission to upload attachments. + * + * @return boolean + */ + public function canUpload() { + return $this->processor->canUpload($this->objectID, $this->parentObjectID); + } + + /** + * Returns the object type processor. + * + * @return wcf\system\attachment\IAttachmentObjectType + */ + public function getProcessor() { + return $this->processor; + } +} diff --git a/wcfsetup/install/files/lib/system/attachment/IAttachmentObjectType.class.php b/wcfsetup/install/files/lib/system/attachment/IAttachmentObjectType.class.php new file mode 100644 index 0000000000..a744a35587 --- /dev/null +++ b/wcfsetup/install/files/lib/system/attachment/IAttachmentObjectType.class.php @@ -0,0 +1,84 @@ + + * @package com.woltlab.wcf.attachment + * @subpackage system.attachment + * @category Community Framework + */ +interface IAttachmentObjectType { + /** + * Returns true if the active user has the permission to download attachments. + * + * @param integer $objectID + * @return boolean + */ + public function canDownload($objectID); + + /** + * Returns true if the active user has the permission to view attachment + * previews (thumbnails). + * + * @param integer $objectID + * @return boolean + */ + public function canViewPreview($objectID); + + /** + * Returns true if the active user has the permission to upload attachments. + * + * @param integer $objectID + * @param integer $parentObjectID + * @return boolean + */ + public function canUpload($objectID, $parentObjectID = 0); + + /** + * Returns true if the active user has the permission to delete attachments. + * + * @param integer $objectID + * @return boolean + */ + public function canDelete($objectID); + + /** + * Returns the maximum filesize for an attachment. + * + * @return integer + */ + public function getMaxSize(); + + /** + * Returns the allowed file extensions. + * + * @return array + */ + public function getAllowedExtensions(); + + /** + * Returns the maximum number of attachments. + * + * @return integer + */ + public function getMaxCount(); + + /** + * Gets the container object of an attachment. + * + * @param integer $objectID + * @return wcf\data\IUserContent + */ + public function getObject($objectID); + + /** + * Caches the data of the given container objects. + * + * @param array $objectIDs + */ + public function cacheObjects(array $objectIDs); +} diff --git a/wcfsetup/install/files/lib/system/cronjob/AttachmentCleanUpCronjob.class.php b/wcfsetup/install/files/lib/system/cronjob/AttachmentCleanUpCronjob.class.php new file mode 100644 index 0000000000..434a5fd200 --- /dev/null +++ b/wcfsetup/install/files/lib/system/cronjob/AttachmentCleanUpCronjob.class.php @@ -0,0 +1,43 @@ + + * @package com.woltlab.wcf.attachment + * @subpackage system.cronjob + * @category Community Framework + */ +class AttachmentCleanUpCronjob extends AbstractCronjob { + /** + * @see wcf\system\cronjob\ICronjob::execute() + */ + public function execute(Cronjob $cronjob) { + parent::execute($cronjob); + + // delete orphaned attachments + $attachmentIDs = array(); + $sql = "SELECT attachmentID + FROM wcf".WCF_N."_attachment + WHERE objectID = ? + AND uploadTime < ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array( + 0, + (TIME_NOW - 86400) + )); + while ($row = $statement->fetchArray()) { + $attachmentIDs[] = $row['attachmentID']; + } + + if (!empty($attachmentIDs)) { + AttachmentEditor::deleteAll($attachmentIDs); + } + } +} diff --git a/wcfsetup/install/files/style/attachment.less b/wcfsetup/install/files/style/attachment.less new file mode 100644 index 0000000000..6e2a13ac19 --- /dev/null +++ b/wcfsetup/install/files/style/attachment.less @@ -0,0 +1,107 @@ +/* attachment thumbnail list */ +.attachmentThumbnailList, .attachmentFileList { + padding-top: 7px !important; + + > fieldset { + padding: 0; + + > legend { + border-bottom: 0; + font-size: @wcfTitleFontSize; + padding-bottom: 7px; + } + } +} + +.attachmentThumbnailList { + padding-bottom: 7px !important; + + > fieldset { + > ul { + padding: 0 @wcfGapSmall+@wcfGapTiny 0 @wcfGapTiny; + + > li { + margin: 0 0 @wcfGapMedium @wcfGapSmall; + } + } + } +} + +.attachmentFileList { + > fieldset > ul > li { + &:not(:first-child) { + margin-top: @wcfGapTiny; + } + } +} + +.attachmentThumbnail { + background-color: white; + display: inline-block; + min-height: 210px; + min-width: 280px; + position: relative; + vertical-align: top; + + > a { + display: inline-block; + min-height: 210px; + min-width: 280px; + } + + > div { + background-color: rgba(0, 0, 0, 0.6); + bottom: 0; + color: #fff; + position: absolute; + width: 100%; + padding: @wcfGapSmall 0; + + > p, + > small { + margin: 0 @wcfGapSmall; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + > small { + display: block; + height: 0; + + .transition(height, .25s, ease-out); + } + } + + &:hover { + > div > small { + height: 1.27em; + } + } +} + +/* attachment form */ +.formAttachmentList { + border-bottom: 1px solid @wcfContainerBorderColor; + padding-bottom: @wcfGapSmall; + + > li { + width: 33%; + float: left; + + > div { + padding-top: @wcfGapSmall; + + > div { + float: left; + margin-right: @wcfGapSmall; + } + } + } +} + +.box48 .attachmentTinyThumbnail { + border-radius: 6px; + max-height: 48px; + max-width: 48px; +} diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 66a26a4ba4..becf44e3c3 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -32,6 +32,12 @@ getPackage()->getName()}“ wirklich als primäre Anwendung festlegen?]]>
        + + + + + + @@ -178,6 +184,15 @@ + + + + + + + + + @@ -308,6 +323,8 @@ + + @@ -454,6 +471,13 @@ reCAPTCHA beantragen.]]> + + + + + + + @@ -837,6 +861,31 @@ Wenn Sie unter System -> Optionen -> Allgemein -> E-Mails alle + + filesize|filesize}, {#$attachment->downloads} mal heruntergeladen{if $attachment->downloads > 0}, zuletzt: {@$attachment->lastDownloadTime|time}{/if})]]> + filesize|filesize}, {#$attachment->width}×{#$attachment->height}, {#$attachment->downloads} mal angesehen]]> + + + + + + + + + + getMaxCount()}
        +Maximale Dateigröße: {@$attachmentHandler->getMaxSize()|filesize}
        +Erlaubte Dateiendungen: {', '|implode:$attachmentHandler->getAllowedExtensions()}]]>
        + + + + + + + + +
        + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index bf8f4d0c86..68f5119803 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -32,6 +32,12 @@ getPackage()->getName()}” as the primary application?]]> + + + + + + @@ -178,6 +184,15 @@ + + + + + + + + + @@ -308,6 +323,8 @@ + + @@ -454,6 +471,13 @@ reCAPTCHA website.]]> + + + + + + + @@ -837,6 +861,30 @@ You can define the default sender in System -> Options -> General -> + + filesize|filesize}, downloaded {#$attachment->downloads} times{if $attachment->downloads > 0}, last: {@$attachment->lastDownloadTime|time}{/if})]]> + filesize|filesize}, {#$attachment->width}×{#$attachment->height}, viewed {#$attachment->downloads} times]]> + + + + + + + + + + getMaxCount()}
        +Maximum file size: {@$attachmentHandler->getMaxSize()|filesize}
        +Allowed extensions: {', '|implode:$attachmentHandler->getAllowedExtensions()}]]>
        + + + + + + + +
        + diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index df1b8d6edf..6f6fa7dd9e 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -121,6 +121,41 @@ CREATE TABLE wcf1_application ( isPrimary TINYINT(1) NOT NULL DEFAULT 0 ); +DROP TABLE IF EXISTS wcf1_attachment; +CREATE TABLE wcf1_attachment ( + attachmentID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY, + objectTypeID INT(10) NOT NULL, + objectID INT(10), + userID INT(10), + tmpHash VARCHAR(40) NOT NULL DEFAULT '', + filename VARCHAR(255) NOT NULL DEFAULT '', + filesize INT(10) NOT NULL DEFAULT 0, + fileType VARCHAR(255) NOT NULL DEFAULT '', + fileHash VARCHAR(40) NOT NULL DEFAULT '', + + 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, + + thumbnailType VARCHAR(255) NOT NULL DEFAULT '', + thumbnailSize INT(10) NOT NULL DEFAULT 0, + thumbnailWidth SMALLINT(5) NOT NULL DEFAULT 0, + thumbnailHeight SMALLINT(5) NOT NULL DEFAULT 0, + + downloads INT(10) NOT NULL DEFAULT 0, + lastDownloadTime INT(10) NOT NULL DEFAULT 0, + uploadTime INT(10) NOT NULL DEFAULT 0, + showOrder SMALLINT(5) NOT NULL DEFAULT 0, + KEY (objectTypeID, objectID), + KEY (objectTypeID, tmpHash), + KEY (objectID, uploadTime) +); + DROP TABLE IF EXISTS wcf1_category; CREATE TABLE wcf1_category ( categoryID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY, @@ -776,6 +811,9 @@ ALTER TABLE wcf1_acp_template ADD FOREIGN KEY (packageID) REFERENCES wcf1_packag ALTER TABLE wcf1_application ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE; +ALTER TABLE wcf1_attachment ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE; +ALTER TABLE wcf1_attachment ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE SET NULL; + ALTER TABLE wcf1_category ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE; ALTER TABLE wcf1_clipboard_action ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE; -- 2.20.1