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

462 lines 16.0 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 || !is_file($sourcePath) || !is_readable($sourcePath)) {
274 return $result;
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) && is_file($attachment->file_path)) {
356 return $attachment->file_path;
357 }
358
359 if (!empty($attachment->full_url)) {
360 $uploadDir = wp_upload_dir();
361 $pathFromUrl = rawurldecode(str_replace($uploadDir['baseurl'], $uploadDir['basedir'], $attachment->full_url));
362 if (is_file($pathFromUrl)) {
363 return $pathFromUrl;
364 }
365
366 if ($boardId) {
367 $urlPath = wp_parse_url($attachment->full_url, PHP_URL_PATH);
368 $pathFromBoardUrl = $urlPath ? $this->getBoardDir($boardId) . DIRECTORY_SEPARATOR . basename(rawurldecode($urlPath)) : null;
369 if ($pathFromBoardUrl && is_file($pathFromBoardUrl)) {
370 return $pathFromBoardUrl;
371 }
372 }
373 }
374
375 if ($boardId && !empty($attachment->file_path)) {
376 $pathFromBoard = $this->getBoardDir($boardId) . DIRECTORY_SEPARATOR . basename(rawurldecode($attachment->file_path));
377 if (is_file($pathFromBoard)) {
378 return $pathFromBoard;
379 }
380 }
381
382 return null;
383 }
384
385 protected function getAttachmentFilename(Attachment $attachment)
386 {
387 if (!empty($attachment->file_path)) {
388 return basename($attachment->file_path);
389 }
390
391 $urlPath = wp_parse_url($attachment->full_url, PHP_URL_PATH);
392 if ($urlPath) {
393 return basename($urlPath);
394 }
395
396 return sanitize_file_name($attachment->title ?: wp_generate_uuid4());
397 }
398
399 protected function uniqueFilename($dir, $filename)
400 {
401 $filename = sanitize_file_name($filename);
402
403 if (function_exists('wp_unique_filename')) {
404 return wp_unique_filename($dir, $filename);
405 }
406
407 $candidate = $filename;
408 $index = 1;
409 $info = pathinfo($filename);
410 $name = $info['filename'] ?? $filename;
411 $extension = !empty($info['extension']) ? '.' . $info['extension'] : '';
412
413 while (file_exists($dir . DIRECTORY_SEPARATOR . $candidate)) {
414 $candidate = $name . '-' . $index . $extension;
415 $index++;
416 }
417
418 return $candidate;
419 }
420
421 protected function isLocalFileAttachment(Attachment $attachment)
422 {
423 return $attachment->attachment_type !== 'url'
424 && ($attachment->driver === 'local' || empty($attachment->driver));
425 }
426
427 protected function shouldStoreAbsolutePath(Attachment $attachment)
428 {
429 return !empty($attachment->file_path) && is_file($attachment->file_path);
430 }
431
432 protected function getCoverImageId(Task $task)
433 {
434 $settings = $task->settings;
435 return !empty($settings['cover']['imageId']) ? (int) $settings['cover']['imageId'] : null;
436 }
437
438 protected function createPublicUrl(Attachment $attachment, $boardId)
439 {
440 return (new CommentService())->createPublicUrl($attachment, $boardId);
441 }
442
443 protected function getBoardDir($boardId)
444 {
445 return FileSystem::setSubDir('board_' . absint($boardId))->getDir();
446 }
447
448 protected function getBoardUrl($boardId)
449 {
450 $uploadDir = wp_upload_dir();
451 $fbsUploadDir = apply_filters('fluent_boards/upload_folder_name', FLUENT_BOARDS_UPLOAD_DIR);
452
453 return $uploadDir['baseurl'] . '/' . trim($fbsUploadDir, '/') . '/board_' . absint($boardId);
454 }
455
456 protected function generateFileHash()
457 {
458 $uid = wp_generate_uuid4();
459 return md5($uid . wp_rand(0, 1000));
460 }
461 }
462