PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.95
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.95
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 1.95, at app/Services/AttachmentFileService.php

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