Apply PSR-12 code style (#3886)
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / data / attachment / AttachmentAction.class.php
1 <?php
2
3 namespace wcf\data\attachment;
4
5 use wcf\data\AbstractDatabaseObjectAction;
6 use wcf\data\ISortableAction;
7 use wcf\data\IUploadAction;
8 use wcf\data\object\type\ObjectTypeCache;
9 use wcf\system\attachment\AttachmentHandler;
10 use wcf\system\database\util\PreparedStatementConditionBuilder;
11 use wcf\system\event\EventHandler;
12 use wcf\system\exception\PermissionDeniedException;
13 use wcf\system\exception\UserInputException;
14 use wcf\system\upload\DefaultUploadFileSaveStrategy;
15 use wcf\system\upload\DefaultUploadFileValidationStrategy;
16 use wcf\system\upload\UploadFile;
17 use wcf\system\WCF;
18 use wcf\util\ArrayUtil;
19 use wcf\util\FileUtil;
20
21 /**
22 * Executes attachment-related actions.
23 *
24 * @author Marcel Werk
25 * @copyright 2001-2019 WoltLab GmbH
26 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
27 * @package WoltLabSuite\Core\Data\Attachment
28 *
29 * @method Attachment create()
30 * @method AttachmentEditor[] getObjects()
31 * @method AttachmentEditor getSingleObject()
32 */
33 class AttachmentAction extends AbstractDatabaseObjectAction implements ISortableAction, IUploadAction
34 {
35 /**
36 * @inheritDoc
37 */
38 protected $allowGuestAccess = ['delete', 'updatePosition', 'upload'];
39
40 /**
41 * @inheritDoc
42 */
43 protected $className = AttachmentEditor::class;
44
45 /**
46 * current attachment object, used to communicate with event listeners
47 * @var Attachment
48 */
49 public $eventAttachment;
50
51 /**
52 * current data, used to communicate with event listeners.
53 * @var array
54 */
55 public $eventData = [];
56
57 /**
58 * @inheritDoc
59 */
60 public function validateDelete()
61 {
62 // read objects
63 if (empty($this->objects)) {
64 $this->readObjects();
65
66 if (empty($this->objects)) {
67 throw new UserInputException('objectIDs');
68 }
69 }
70
71 foreach ($this->getObjects() as $attachment) {
72 if ($attachment->tmpHash) {
73 if ($attachment->userID != WCF::getUser()->userID) {
74 throw new PermissionDeniedException();
75 }
76 } elseif (!$attachment->canDelete()) {
77 // admin can always delete attachments (unless they are private)
78 if (!WCF::getSession()->getPermission('admin.attachment.canManageAttachment') || ObjectTypeCache::getInstance()->getObjectType($attachment->objectTypeID)->private) {
79 throw new PermissionDeniedException();
80 }
81 }
82 }
83 }
84
85 /**
86 * @inheritDoc
87 */
88 public function validateUpload()
89 {
90 // IE<10 fallback
91 if (isset($_POST['isFallback'])) {
92 $this->parameters['objectType'] = $_POST['objectType'] ?? '';
93 $this->parameters['objectID'] = $_POST['objectID'] ?? 0;
94 $this->parameters['parentObjectID'] = $_POST['parentObjectID'] ?? 0;
95 $this->parameters['tmpHash'] = $_POST['tmpHash'] ?? '';
96 }
97
98 // read variables
99 $this->readString('objectType');
100 $this->readInteger('objectID', true);
101 $this->readInteger('parentObjectID', true);
102 $this->readString('tmpHash');
103
104 // validate object type
105 $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName(
106 'com.woltlab.wcf.attachment.objectType',
107 $this->parameters['objectType']
108 );
109 if ($objectType === null) {
110 throw new UserInputException('objectType');
111 }
112
113 // get processor
114 $processor = $objectType->getProcessor();
115
116 // check upload permissions
117 if (
118 !$processor->canUpload(
119 (!empty($this->parameters['objectID']) ? \intval($this->parameters['objectID']) : 0),
120 (!empty($this->parameters['parentObjectID']) ? \intval($this->parameters['parentObjectID']) : 0)
121 )
122 ) {
123 throw new PermissionDeniedException();
124 }
125
126 // check max count of uploads
127 $handler = new AttachmentHandler(
128 $this->parameters['objectType'],
129 \intval($this->parameters['objectID']),
130 $this->parameters['tmpHash']
131 );
132 /** @noinspection PhpUndefinedMethodInspection */
133 if ($handler->count() + \count($this->parameters['__files']->getFiles()) > $processor->getMaxCount()) {
134 throw new UserInputException('files', 'exceededQuota', [
135 'current' => $handler->count(),
136 'quota' => $processor->getMaxCount(),
137 ]);
138 }
139
140 // check max filesize, allowed file extensions etc.
141 /** @noinspection PhpUndefinedMethodInspection */
142 $this->parameters['__files']->validateFiles(new DefaultUploadFileValidationStrategy(
143 $processor->getMaxSize(),
144 $processor->getAllowedExtensions()
145 ));
146 }
147
148 /**
149 * @inheritDoc
150 */
151 public function upload()
152 {
153 // get object type
154 $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName(
155 'com.woltlab.wcf.attachment.objectType',
156 $this->parameters['objectType']
157 );
158
159 // save files
160 $saveStrategy = new DefaultUploadFileSaveStrategy(self::class, [
161 'generateThumbnails' => true,
162 'rotateImages' => true,
163 ], [
164 'objectID' => \intval($this->parameters['objectID']),
165 'objectTypeID' => $objectType->objectTypeID,
166 'tmpHash' => !$this->parameters['objectID'] ? $this->parameters['tmpHash'] : '',
167 ]);
168
169 /** @noinspection PhpUndefinedMethodInspection */
170 $this->parameters['__files']->saveFiles($saveStrategy);
171
172 /** @var Attachment[] $attachments */
173 $attachments = $saveStrategy->getObjects();
174
175 // return result
176 $result = ['attachments' => [], 'errors' => []];
177 if (!empty($attachments)) {
178 // get attachment ids
179 $attachmentIDs = $attachmentToFileID = [];
180 foreach ($attachments as $internalFileID => $attachment) {
181 $attachmentIDs[] = $attachment->attachmentID;
182 $attachmentToFileID[$attachment->attachmentID] = $internalFileID;
183 }
184
185 // get attachments from database (check thumbnail status)
186 $attachmentList = new AttachmentList();
187 $attachmentList->setObjectIDs($attachmentIDs);
188 $attachmentList->readObjects();
189
190 foreach ($attachmentList as $attachment) {
191 $result['attachments'][$attachmentToFileID[$attachment->attachmentID]] = [
192 'filename' => $attachment->filename,
193 'filesize' => $attachment->filesize,
194 'formattedFilesize' => FileUtil::formatFilesize($attachment->filesize),
195 'isImage' => $attachment->isImage,
196 'attachmentID' => $attachment->attachmentID,
197 'tinyURL' => $attachment->tinyThumbnailType ? $attachment->getThumbnailLink('tiny') : '',
198 'thumbnailURL' => $attachment->thumbnailType ? $attachment->getThumbnailLink('thumbnail') : '',
199 'url' => $attachment->getLink(),
200 'height' => $attachment->height,
201 'width' => $attachment->width,
202 'iconName' => $attachment->getIconName(),
203 ];
204 }
205 }
206
207 /** @noinspection PhpUndefinedMethodInspection */
208 /** @var UploadFile[] $files */
209 $files = $this->parameters['__files']->getFiles();
210 foreach ($files as $file) {
211 if ($file->getValidationErrorType()) {
212 $result['errors'][$file->getInternalFileID()] = [
213 'filename' => $file->getFilename(),
214 'filesize' => $file->getFilesize(),
215 'errorType' => $file->getValidationErrorType(),
216 'additionalData' => $file->getValidationErrorAdditionalData(),
217 ];
218 }
219 }
220
221 return $result;
222 }
223
224 /**
225 * Generates thumbnails.
226 */
227 public function generateThumbnails()
228 {
229 if (empty($this->objects)) {
230 $this->readObjects();
231 }
232
233 $saveStrategy = new DefaultUploadFileSaveStrategy(self::class);
234
235 foreach ($this->getObjects() as $attachment) {
236 if (!$attachment->isImage) {
237 // create thumbnails for every file that isn't an image
238 $this->eventAttachment = $attachment;
239 $this->eventData = [];
240
241 EventHandler::getInstance()->fireAction($this, 'generateThumbnail');
242
243 if (!empty($this->eventData)) {
244 $attachment->update($this->eventData);
245 }
246
247 continue;
248 }
249
250 $saveStrategy->generateThumbnails($attachment->getDecoratedObject());
251 }
252 }
253
254 /**
255 * @inheritDoc
256 */
257 public function validateUpdatePosition()
258 {
259 $this->readInteger('objectID', true);
260 $this->readString('objectType');
261 $this->readString('tmpHash', true);
262
263 if (empty($this->parameters['objectID']) && empty($this->parameters['tmpHash'])) {
264 throw new UserInputException('objectID');
265 }
266
267 // validate object type
268 $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName(
269 'com.woltlab.wcf.attachment.objectType',
270 $this->parameters['objectType']
271 );
272 if ($objectType === null) {
273 throw new UserInputException('objectType');
274 }
275
276 if (!empty($this->parameters['objectID'])) {
277 // check upload permissions
278 if (!$objectType->getProcessor()->canUpload($this->parameters['objectID'])) {
279 throw new PermissionDeniedException();
280 }
281 }
282
283 if (!isset($this->parameters['attachmentIDs']) || !\is_array($this->parameters['attachmentIDs'])) {
284 throw new UserInputException('attachmentIDs');
285 }
286
287 $this->parameters['attachmentIDs'] = ArrayUtil::toIntegerArray($this->parameters['attachmentIDs']);
288
289 // check attachment ids
290 $conditions = new PreparedStatementConditionBuilder();
291 $conditions->add("attachmentID IN (?)", [$this->parameters['attachmentIDs']]);
292 $conditions->add("objectTypeID = ?", [$objectType->objectTypeID]);
293
294 if (!empty($this->parameters['objectID'])) {
295 $conditions->add("objectID = ?", [$this->parameters['objectID']]);
296 } else {
297 $conditions->add("tmpHash = ?", [$this->parameters['tmpHash']]);
298 }
299
300 $sql = "SELECT attachmentID
301 FROM wcf" . WCF_N . "_attachment
302 " . $conditions;
303 $statement = WCF::getDB()->prepareStatement($sql);
304 $statement->execute($conditions->getParameters());
305 $attachmentIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
306
307 foreach ($this->parameters['attachmentIDs'] as $attachmentID) {
308 if (!\in_array($attachmentID, $attachmentIDs)) {
309 throw new UserInputException('attachmentIDs');
310 }
311 }
312 }
313
314 /**
315 * @inheritDoc
316 */
317 public function updatePosition()
318 {
319 $sql = "UPDATE wcf" . WCF_N . "_attachment
320 SET showOrder = ?
321 WHERE attachmentID = ?";
322 $statement = WCF::getDB()->prepareStatement($sql);
323
324 WCF::getDB()->beginTransaction();
325 $showOrder = 1;
326 foreach ($this->parameters['attachmentIDs'] as $attachmentID) {
327 $statement->execute([
328 $showOrder++,
329 $attachmentID,
330 ]);
331 }
332 WCF::getDB()->commitTransaction();
333 }
334
335 /**
336 * Copies attachments from one object id to another.
337 */
338 public function copy()
339 {
340 $sourceObjectType = ObjectTypeCache::getInstance()->getObjectTypeByName(
341 'com.woltlab.wcf.attachment.objectType',
342 $this->parameters['sourceObjectType']
343 );
344 $targetObjectType = ObjectTypeCache::getInstance()->getObjectTypeByName(
345 'com.woltlab.wcf.attachment.objectType',
346 $this->parameters['targetObjectType']
347 );
348
349 $attachmentList = new AttachmentList();
350 $attachmentList->getConditionBuilder()->add("attachment.objectTypeID = ?", [$sourceObjectType->objectTypeID]);
351 $attachmentList->getConditionBuilder()->add("attachment.objectID = ?", [$this->parameters['sourceObjectID']]);
352 $attachmentList->readObjects();
353
354 $newAttachmentIDs = [];
355 foreach ($attachmentList as $attachment) {
356 $newAttachment = AttachmentEditor::create([
357 'objectTypeID' => $targetObjectType->objectTypeID,
358 'objectID' => $this->parameters['targetObjectID'],
359 'userID' => $attachment->userID,
360 'filename' => $attachment->filename,
361 'filesize' => $attachment->filesize,
362 'fileType' => $attachment->fileType,
363 'fileHash' => $attachment->fileHash,
364 'isImage' => $attachment->isImage,
365 'width' => $attachment->width,
366 'height' => $attachment->height,
367 'tinyThumbnailType' => $attachment->tinyThumbnailType,
368 'tinyThumbnailSize' => $attachment->tinyThumbnailSize,
369 'tinyThumbnailWidth' => $attachment->tinyThumbnailWidth,
370 'tinyThumbnailHeight' => $attachment->tinyThumbnailHeight,
371 'thumbnailType' => $attachment->thumbnailType,
372 'thumbnailSize' => $attachment->thumbnailSize,
373 'thumbnailWidth' => $attachment->thumbnailWidth,
374 'thumbnailHeight' => $attachment->thumbnailHeight,
375 'downloads' => $attachment->downloads,
376 'lastDownloadTime' => $attachment->lastDownloadTime,
377 'uploadTime' => $attachment->uploadTime,
378 'showOrder' => $attachment->showOrder,
379 ]);
380
381 // copy attachment
382 @\copy($attachment->getLocation(), $newAttachment->getLocation());
383
384 if ($attachment->tinyThumbnailSize) {
385 @\copy($attachment->getTinyThumbnailLocation(), $newAttachment->getTinyThumbnailLocation());
386 }
387 if ($attachment->thumbnailSize) {
388 @\copy($attachment->getThumbnailLocation(), $newAttachment->getThumbnailLocation());
389 }
390
391 $newAttachmentIDs[$attachment->attachmentID] = $newAttachment->attachmentID;
392 }
393
394 return [
395 'attachmentIDs' => $newAttachmentIDs,
396 ];
397 }
398 }