PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.2.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.2.0
2.2.0 2.1.0 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 All 43 releases
← All changes | app/Hooks/Handlers/FileHandler.php +83 -6 2.1.0 → 2.2.0 View file →
@@ -32,25 +32,41 @@
32 32 );
33 33 }
34 34
35 35 /**
36 - * Delete a local attachment only when its stored path belongs to Fluent Boards.
36 + * Delete an attachment's stored file once no attachment row references it.
37 37 *
38 - * @param mixed $attachment
38 + * Local files are removed here, but only when the stored path belongs to
39 + * Fluent Boards. Files on a cloud driver are handed to the storage add-on
40 + * through the `fluent_boards/delete_remote_attachment_file` action, because
41 + * the core plugin holds no bucket credentials.
42 + *
43 + * @param mixed $attachment Attachment row or snapshot (driver, file_path, attachment_type).
39 44 * @param int|null $boardId
45 + * @param array|null $sharedReferences Preloaded local references from getSharedLocalReferences().
40 46 * @return bool
41 47 */
42 - public function deleteAttachmentFile($attachment, $boardId = null)
48 + public function deleteAttachmentFile($attachment, $boardId = null, $sharedReferences = null)
43 49 {
44 50 if (
45 51 !$attachment ||
46 52 $attachment->attachment_type === 'url' ||
47 - (!empty($attachment->driver) && $attachment->driver !== 'local') ||
48 53 empty($attachment->file_path)
49 54 ) {
50 55 return false;
51 56 }
52 57
58 + if (!empty($attachment->driver) && $attachment->driver !== 'local') {
59 + // Cloned tasks share the remote object, so keep it while another row points at it.
60 + if (Attachment::where('file_path', $attachment->file_path)->exists()) {
61 + return false;
62 + }
63 +
64 + do_action('fluent_boards/delete_remote_attachment_file', $attachment);
65 +
66 + return true;
67 + }
68 +
53 69 $storedFilename = rawurldecode((string) $attachment->file_path);
54 70 $isBareFilename = $storedFilename !== ''
55 71 && strpos($storedFilename, '/') === false
56 72 && strpos($storedFilename, '\\') === false;
@@ -63,13 +79,13 @@
63 79 // Legacy filenames need the board-qualified URL to distinguish same-named files across boards.
64 80 if ($isBareFilename) {
65 81 if (
66 82 empty($attachment->full_url) ||
67 - Attachment::where('full_url', $attachment->full_url)->exists()
83 + $this->hasSharedLocalReference('full_url', $attachment->full_url, $sharedReferences)
68 84 ) {
69 85 return false;
70 86 }
71 - } elseif (Attachment::where('file_path', $attachment->file_path)->exists()) {
87 + } elseif ($this->hasSharedLocalReference('file_path', $attachment->file_path, $sharedReferences)) {
72 88 return false;
73 89 }
74 90
75 91 return (bool) wp_delete_file($filePath);
@@ -74,8 +90,69 @@
74 90
75 91 return (bool) wp_delete_file($filePath);
76 92 }
77 93
94 + /**
95 + * Batch the shared-reference lookups deleteAttachmentFile() makes for local files.
96 + *
97 + * @param iterable $attachments Deleted attachment rows or snapshots.
98 + * @return array ['file_path' => [value => true], 'full_url' => [value => true]]
99 + */
100 + public function getSharedLocalReferences($attachments)
101 + {
102 + $values = ['file_path' => [], 'full_url' => []];
103 + foreach ($attachments as $attachment) {
104 + if (!empty($attachment->file_path)) {
105 + $values['file_path'][] = (string) $attachment->file_path;
106 + }
107 + if (!empty($attachment->full_url)) {
108 + $values['full_url'][] = (string) $attachment->full_url;
109 + }
110 + }
111 +
112 + $references = ['file_path' => [], 'full_url' => []];
113 + foreach ($values as $column => $columnValues) {
114 + foreach (array_chunk(array_values(array_unique($columnValues)), 200) as $chunk) {
115 + foreach (Attachment::whereIn($column, $chunk)->pluck($column)->toArray() as $value) {
116 + $references[$column][(string) $value] = true;
117 + }
118 + }
119 + }
120 +
121 + return $references;
122 + }
123 +
124 + private function hasSharedLocalReference($column, $value, $sharedReferences)
125 + {
126 + if (is_array($sharedReferences)) {
127 + return isset($sharedReferences[$column][(string) $value]);
128 + }
129 +
130 + return Attachment::where($column, $value)->exists();
131 + }
132 +
133 +
134 + /**
135 + * Action Scheduler callback: delete the remote files of attachment rows that
136 + * were removed in bulk (a deleted board), outside the request that deleted them.
137 + *
138 + * @param array $files List of ['file_path' => string, 'driver' => string].
139 + * @return void
140 + */
141 + public function deleteRemoteAttachmentFiles($files)
142 + {
143 + foreach ((array) $files as $file) {
144 + if (!is_array($file) || empty($file['file_path']) || empty($file['driver'])) {
145 + continue;
146 + }
147 +
148 + $this->deleteAttachmentFile((object) [
149 + 'attachment_type' => 'file',
150 + 'driver' => sanitize_key($file['driver']),
151 + 'file_path' => (string) $file['file_path'],
152 + ]);
153 + }
154 + }
78 155
79 156 /**
80 157 * Summary of isFileTypeSupported checking file type it will allow only file which is readable by browser
81 158 * @param mixed $file