PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.0.12
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.0.12
2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 1.45 All 41 releases
fluent-boards / app / Services / AttachmentFileService.php

AttachmentFileService.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 2.0.12, at app/Services/AttachmentFileService.php

498 lines 17.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\App\Services;
4
5 use FluentBoards\App\Models\Attachment;
6 use FluentBoards\App\Models\Comment;
7 use FluentBoards\App\Models\CommentImage;
8 use FluentBoards\App\Models\Task;
9 use FluentBoards\App\Models\TaskImage;
10 use FluentBoards\App\Services\Libs\FileSystem;
11
12 class AttachmentFileService
13 {
14 protected $createdFiles = [];
15 protected $createdAttachments = [];
16 protected $movedOriginalFiles = [];
17
18 public function moveTaskFilesToBoard(Task $task, $sourceBoardId, $targetBoardId)
19 {
20 if ((int) $sourceBoardId === (int) $targetBoardId) {
21 return $task;
22 }
23
24 $descriptionUrlMap = [];
25 $movedImages = [];
26
27 $images = TaskImage::where('object_id', $task->id)
28 ->where('object_type', Constant::TASK_DESCRIPTION)
29 ->get();
30 $taskAttachments = $this->getTaskAttachmentsForMove($task);
31 $sharedFullUrls = $this->getSharedFullUrlLookup($this->combineAttachmentCollections($images, $taskAttachments));
32
33 foreach ($images as $image) {
34 $result = $this->moveAttachmentToBoard($image, $sourceBoardId, $targetBoardId, $sharedFullUrls);
35 $movedImages[$image->id] = $result;
36
37 if (!empty($result['old_public_url']) && !empty($result['new_public_url'])) {
38 $descriptionUrlMap[$result['old_public_url']] = $result['new_public_url'];
39 }
40 }
41
42 $this->moveTaskAttachmentsToBoard($taskAttachments, $sourceBoardId, $targetBoardId, $sharedFullUrls);
43 $this->rewriteTaskDescriptionUrls($task, $descriptionUrlMap);
44 $this->updateTaskCoverFromFileResults($task, $movedImages, $targetBoardId);
45
46 return $task;
47 }
48
49 /**
50 * Move every comment and reply image for a task to another board.
51 */
52 public function moveCommentImagesToBoard($taskId, $sourceBoardId, $targetBoardId)
53 {
54 $taskId = absint($taskId);
55 $sourceBoardId = absint($sourceBoardId);
56 $targetBoardId = absint($targetBoardId);
57
58 if (!$taskId || !$sourceBoardId || !$targetBoardId || $sourceBoardId === $targetBoardId) {
59 return;
60 }
61
62 $commentIds = Comment::where('task_id', $taskId)
63 ->pluck('id')
64 ->toArray();
65 $commentIds = array_filter(array_map('intval', $commentIds));
66
67 if (!$commentIds) {
68 return;
69 }
70
71 $images = CommentImage::whereIn('object_id', $commentIds)
72 ->where('object_type', Constant::COMMENT_IMAGE)
73 ->get();
74 $sharedFullUrls = $this->getSharedFullUrlLookup($this->combineAttachmentCollections($images));
75
76 CommentImage::withoutTimestamps(function () use ($images, $sourceBoardId, $targetBoardId, $sharedFullUrls) {
77 foreach ($images as $image) {
78 $this->moveAttachmentToBoard($image, $sourceBoardId, $targetBoardId, $sharedFullUrls);
79 }
80 });
81 }
82
83 public function cloneTaskFilesToBoard(Task $sourceTask, Task $targetTask, $targetBoardId, array $options = [])
84 {
85 $options = array_merge([
86 'description_images' => true,
87 'cover' => true,
88 'task_attachments' => true,
89 ], $options);
90
91 $descriptionUrlMap = [];
92 $clonedImages = [];
93 $coverImageId = $this->getCoverImageId($sourceTask);
94
95 if ($options['description_images'] || ($options['cover'] && $coverImageId)) {
96 $imageQuery = TaskImage::where('object_id', $sourceTask->id)
97 ->where('object_type', Constant::TASK_DESCRIPTION);
98
99 if (!$options['description_images'] && $coverImageId) {
100 $imageQuery->where('id', $coverImageId);
101 }
102
103 $images = $imageQuery->get();
104
105 foreach ($images as $image) {
106 $result = $this->cloneAttachmentToBoard($image, $targetTask->id, $targetBoardId, Constant::TASK_DESCRIPTION, $sourceTask->board_id);
107 $clonedImages[$image->id] = $result;
108
109 if (!empty($result['old_public_url']) && !empty($result['new_public_url'])) {
110 $descriptionUrlMap[$result['old_public_url']] = $result['new_public_url'];
111 }
112 }
113 }
114
115 if ($options['task_attachments']) {
116 $this->cloneTaskAttachmentsToBoard($sourceTask, $targetTask, $targetBoardId);
117 }
118
119 $this->rewriteTaskDescriptionUrls($targetTask, $descriptionUrlMap);
120 $this->updateTaskCoverFromFileResults($targetTask, $clonedImages, $targetBoardId, $coverImageId);
121 $this->refreshTaskAttachmentCount($targetTask);
122 $targetTask->save();
123
124 return $targetTask;
125 }
126
127 public function cloneAttachmentToBoard(Attachment $source, $targetObjectId, $targetBoardId, $objectType = null, $sourceBoardId = null)
128 {
129 $clonedAttachment = $source->replicate();
130 $clonedAttachment->object_id = (int) $targetObjectId;
131 $clonedAttachment->object_type = $objectType ?: $source->object_type;
132 $clonedAttachment->file_hash = $this->generateFileHash();
133
134 $result = $this->copyAttachmentFileData($source, $clonedAttachment, $sourceBoardId, $targetBoardId);
135 if ($this->isLocalFileAttachment($source) && empty($result['copied'])) {
136 return $result;
137 }
138
139 $clonedAttachment->save();
140 $this->createdAttachments[] = [
141 'class' => get_class($clonedAttachment),
142 'id' => $clonedAttachment->id,
143 ];
144
145 $result['attachment'] = $clonedAttachment;
146 $result['new_attachment_id'] = $clonedAttachment->id;
147 $result['new_public_url'] = $this->createPublicUrl($clonedAttachment, $targetBoardId);
148
149 return $result;
150 }
151
152 public function moveAttachmentToBoard(Attachment $attachment, $sourceBoardId, $targetBoardId, array $sharedFullUrls = [])
153 {
154 $oldFullUrl = $attachment->full_url;
155 $oldPublicUrl = $this->createPublicUrl($attachment, $sourceBoardId);
156
157 $result = $this->copyAttachmentFileData($attachment, $attachment, $sourceBoardId, $targetBoardId);
158 if ($this->isLocalFileAttachment($attachment) && empty($result['copied'])) {
159 $result['attachment'] = $attachment;
160 $result['old_public_url'] = $oldPublicUrl;
161 $result['new_public_url'] = $oldPublicUrl;
162
163 return $result;
164 }
165
166 $attachment->file_hash = $this->generateFileHash();
167 $attachment->save();
168
169 $result['attachment'] = $attachment;
170 $result['old_public_url'] = $oldPublicUrl;
171 $result['new_public_url'] = $this->createPublicUrl($attachment, $targetBoardId);
172
173 if ($this->isLocalFileAttachment($attachment) && !empty($result['old_path']) && file_exists($result['old_path'])) {
174 if (empty($sharedFullUrls[$oldFullUrl]) && $result['old_path'] !== $result['new_path']) {
175 $this->movedOriginalFiles[] = $result['old_path'];
176 }
177 }
178
179 return $result;
180 }
181
182 public function commitMovedOriginalFiles()
183 {
184 foreach (array_unique($this->movedOriginalFiles) as $file) {
185 if (file_exists($file)) {
186 wp_delete_file($file);
187 }
188 }
189
190 $this->createdFiles = [];
191 $this->createdAttachments = [];
192 $this->movedOriginalFiles = [];
193 }
194
195 public function rollbackCreatedFiles()
196 {
197 foreach (array_reverse($this->createdAttachments) as $createdAttachment) {
198 if (!class_exists($createdAttachment['class'])) {
199 continue;
200 }
201
202 $attachment = $createdAttachment['class']::find($createdAttachment['id']);
203 if ($attachment) {
204 $attachment->delete();
205 }
206 }
207
208 foreach (array_reverse($this->createdFiles) as $file) {
209 if (file_exists($file)) {
210 wp_delete_file($file);
211 }
212 }
213
214 $this->createdFiles = [];
215 $this->createdAttachments = [];
216 $this->movedOriginalFiles = [];
217 }
218
219 protected function cloneTaskAttachmentsToBoard(Task $sourceTask, Task $targetTask, $targetBoardId)
220 {
221 if (!defined('FLUENT_BOARDS_PRO_VERSION')) {
222 return;
223 }
224
225 $attachments = \FluentBoardsPro\App\Models\TaskAttachment::where('object_id', $sourceTask->id)
226 ->where('object_type', Constant::TASK_ATTACHMENT)
227 ->get();
228
229 foreach ($attachments as $attachment) {
230 $this->cloneAttachmentToBoard($attachment, $targetTask->id, $targetBoardId, Constant::TASK_ATTACHMENT, $sourceTask->board_id);
231 }
232 }
233
234 protected function moveTaskAttachmentsToBoard($attachments, $sourceBoardId, $targetBoardId, array $sharedFullUrls)
235 {
236 foreach ($attachments as $attachment) {
237 $this->moveAttachmentToBoard($attachment, $sourceBoardId, $targetBoardId, $sharedFullUrls);
238 }
239 }
240
241 protected function getTaskAttachmentsForMove(Task $task)
242 {
243 if (!defined('FLUENT_BOARDS_PRO_VERSION')) {
244 return [];
245 }
246
247 return \FluentBoardsPro\App\Models\TaskAttachment::where('object_id', $task->id)
248 ->where('object_type', Constant::TASK_ATTACHMENT)
249 ->get();
250 }
251
252 protected function combineAttachmentCollections(...$collections)
253 {
254 $attachments = [];
255
256 foreach ($collections as $collection) {
257 foreach ($collection as $attachment) {
258 $attachments[] = $attachment;
259 }
260 }
261
262 return $attachments;
263 }
264
265 protected function getSharedFullUrlLookup(array $attachments)
266 {
267 $fullUrls = [];
268 $attachmentIds = [];
269
270 foreach ($attachments as $attachment) {
271 if (!empty($attachment->full_url)) {
272 $fullUrls[] = $attachment->full_url;
273 $attachmentIds[] = (int) $attachment->id;
274 }
275 }
276
277 $fullUrls = array_values(array_unique($fullUrls));
278 $attachmentIds = array_filter(array_unique($attachmentIds));
279
280 if (!$fullUrls) {
281 return [];
282 }
283
284 $sharedUrls = Attachment::whereIn('full_url', $fullUrls)
285 ->whereNotIn('id', $attachmentIds)
286 ->pluck('full_url')
287 ->toArray();
288
289 return array_fill_keys($sharedUrls, true);
290 }
291
292 protected function copyAttachmentFileData(Attachment $source, Attachment $target, $sourceBoardId, $targetBoardId)
293 {
294 $result = [
295 'old_path' => null,
296 'new_path' => null,
297 'old_full_url' => $source->full_url,
298 'new_full_url' => $source->full_url,
299 'old_public_url' => $sourceBoardId ? $this->createPublicUrl($source, $sourceBoardId) : null,
300 'new_public_url' => null,
301 'copied' => false,
302 ];
303
304 if (!$this->isLocalFileAttachment($source)) {
305 return $result;
306 }
307
308 $sourcePath = $this->resolveLocalPath($source, $sourceBoardId);
309 if (!$sourcePath || !is_file($sourcePath) || !is_readable($sourcePath)) {
310 return $result;
311 }
312
313 $targetDir = $this->getBoardDir($targetBoardId);
314 if (!is_dir($targetDir)) {
315 wp_mkdir_p($targetDir);
316 }
317
318 $filename = $this->uniqueFilename($targetDir, $this->getAttachmentFilename($source));
319 $targetPath = $targetDir . DIRECTORY_SEPARATOR . $filename;
320
321 if (!copy($sourcePath, $targetPath)) {
322 throw new \Exception(esc_html__('Could not copy attachment file.', 'fluent-boards'));
323 }
324
325 $this->createdFiles[] = $targetPath;
326
327 $target->file_path = $this->shouldStoreAbsolutePath($source) ? $targetPath : $filename;
328 $target->full_url = $this->getBoardUrl($targetBoardId) . '/' . rawurlencode($filename);
329 $target->driver = 'local';
330
331 $result['old_path'] = $sourcePath;
332 $result['new_path'] = $targetPath;
333 $result['new_full_url'] = $target->full_url;
334 $result['copied'] = true;
335
336 return $result;
337 }
338
339 protected function rewriteTaskDescriptionUrls(Task $task, array $urlMap)
340 {
341 if (!$urlMap || empty($task->description)) {
342 return;
343 }
344
345 $description = $task->description;
346
347 foreach ($urlMap as $oldUrl => $newUrl) {
348 $description = str_replace($oldUrl, $newUrl, $description);
349 $description = str_replace(esc_url($oldUrl), esc_url($newUrl), $description);
350 $description = str_replace(esc_attr($oldUrl), esc_attr($newUrl), $description);
351 }
352
353 if ($description !== $task->description) {
354 $task->description = $description;
355 }
356 }
357
358 protected function updateTaskCoverFromFileResults(Task $task, array $fileResults, $targetBoardId, $sourceCoverImageId = null)
359 {
360 $settings = $task->settings;
361 if (empty($settings['cover']) || !is_array($settings['cover'])) {
362 return;
363 }
364
365 $coverImageId = $sourceCoverImageId ?: $this->getCoverImageId($task);
366 if (!$coverImageId || empty($fileResults[$coverImageId]['attachment'])) {
367 return;
368 }
369
370 $newCoverAttachment = $fileResults[$coverImageId]['attachment'];
371 $settings['cover']['imageId'] = $newCoverAttachment->id;
372 $settings['cover']['backgroundImage'] = $this->createPublicUrl($newCoverAttachment, $targetBoardId);
373 $task->settings = $settings;
374 }
375
376 protected function refreshTaskAttachmentCount(Task $task)
377 {
378 if (!defined('FLUENT_BOARDS_PRO_VERSION')) {
379 return;
380 }
381
382 $settings = $task->settings ?: [];
383 $settings['attachment_count'] = \FluentBoardsPro\App\Models\TaskAttachment::where('object_id', $task->id)
384 ->where('object_type', Constant::TASK_ATTACHMENT)
385 ->count();
386 $task->settings = $settings;
387 }
388
389 protected function resolveLocalPath(Attachment $attachment, $boardId)
390 {
391 if (!empty($attachment->file_path) && is_file($attachment->file_path)) {
392 return $attachment->file_path;
393 }
394
395 if (!empty($attachment->full_url)) {
396 $uploadDir = wp_upload_dir();
397 $pathFromUrl = rawurldecode(str_replace($uploadDir['baseurl'], $uploadDir['basedir'], $attachment->full_url));
398 if (is_file($pathFromUrl)) {
399 return $pathFromUrl;
400 }
401
402 if ($boardId) {
403 $urlPath = wp_parse_url($attachment->full_url, PHP_URL_PATH);
404 $pathFromBoardUrl = $urlPath ? $this->getBoardDir($boardId) . DIRECTORY_SEPARATOR . basename(rawurldecode($urlPath)) : null;
405 if ($pathFromBoardUrl && is_file($pathFromBoardUrl)) {
406 return $pathFromBoardUrl;
407 }
408 }
409 }
410
411 if ($boardId && !empty($attachment->file_path)) {
412 $pathFromBoard = $this->getBoardDir($boardId) . DIRECTORY_SEPARATOR . basename(rawurldecode($attachment->file_path));
413 if (is_file($pathFromBoard)) {
414 return $pathFromBoard;
415 }
416 }
417
418 return null;
419 }
420
421 protected function getAttachmentFilename(Attachment $attachment)
422 {
423 if (!empty($attachment->file_path)) {
424 return basename($attachment->file_path);
425 }
426
427 $urlPath = wp_parse_url($attachment->full_url, PHP_URL_PATH);
428 if ($urlPath) {
429 return basename($urlPath);
430 }
431
432 return sanitize_file_name($attachment->title ?: wp_generate_uuid4());
433 }
434
435 protected function uniqueFilename($dir, $filename)
436 {
437 $filename = sanitize_file_name($filename);
438
439 if (function_exists('wp_unique_filename')) {
440 return wp_unique_filename($dir, $filename);
441 }
442
443 $candidate = $filename;
444 $index = 1;
445 $info = pathinfo($filename);
446 $name = $info['filename'] ?? $filename;
447 $extension = !empty($info['extension']) ? '.' . $info['extension'] : '';
448
449 while (file_exists($dir . DIRECTORY_SEPARATOR . $candidate)) {
450 $candidate = $name . '-' . $index . $extension;
451 $index++;
452 }
453
454 return $candidate;
455 }
456
457 protected function isLocalFileAttachment(Attachment $attachment)
458 {
459 return $attachment->attachment_type !== 'url'
460 && ($attachment->driver === 'local' || empty($attachment->driver));
461 }
462
463 protected function shouldStoreAbsolutePath(Attachment $attachment)
464 {
465 return !empty($attachment->file_path) && is_file($attachment->file_path);
466 }
467
468 protected function getCoverImageId(Task $task)
469 {
470 $settings = $task->settings;
471 return !empty($settings['cover']['imageId']) ? (int) $settings['cover']['imageId'] : null;
472 }
473
474 protected function createPublicUrl(Attachment $attachment, $boardId)
475 {
476 return (new CommentService())->createPublicUrl($attachment, $boardId);
477 }
478
479 protected function getBoardDir($boardId)
480 {
481 return FileSystem::setSubDir('board_' . absint($boardId))->getDir();
482 }
483
484 protected function getBoardUrl($boardId)
485 {
486 $uploadDir = wp_upload_dir();
487 $fbsUploadDir = apply_filters('fluent_boards/upload_folder_name', FLUENT_BOARDS_UPLOAD_DIR);
488
489 return $uploadDir['baseurl'] . '/' . trim($fbsUploadDir, '/') . '/board_' . absint($boardId);
490 }
491
492 protected function generateFileHash()
493 {
494 $uid = wp_generate_uuid4();
495 return md5($uid . wp_rand(0, 1000));
496 }
497 }
498