Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
170 / 170 |
|
100.00% |
8 / 8 |
CRAP | |
100.00% |
1 / 1 |
delete | |
100.00% |
170 / 170 |
|
100.00% |
8 / 8 |
37 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
delete | |
100.00% |
50 / 50 |
|
100.00% |
1 / 1 |
4 | |||
set_attachment_ids | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
set_sql_constraints | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
8 | |||
collect_attachment_info | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
5 | |||
delete_attachments_from_db | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
1 | |||
remove_from_storage | |
100.00% |
37 / 37 |
|
100.00% |
1 / 1 |
7 | |||
unlink_attachment | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
8 |
1 | <?php |
2 | /** |
3 | * |
4 | * This file is part of the phpBB Forum Software package. |
5 | * |
6 | * @copyright (c) phpBB Limited <https://www.phpbb.com> |
7 | * @license GNU General Public License, version 2 (GPL-2.0) |
8 | * |
9 | * For full copyright and license information, please see |
10 | * the docs/CREDITS.txt file. |
11 | * |
12 | */ |
13 | |
14 | namespace phpbb\attachment; |
15 | |
16 | use phpbb\config\config; |
17 | use phpbb\db\driver\driver_interface; |
18 | use phpbb\event\dispatcher; |
19 | use phpbb\storage\storage; |
20 | |
21 | /** |
22 | * Attachment delete class |
23 | */ |
24 | class delete |
25 | { |
26 | /** @var config */ |
27 | protected $config; |
28 | |
29 | /** @var driver_interface */ |
30 | protected $db; |
31 | |
32 | /** @var dispatcher */ |
33 | protected $dispatcher; |
34 | |
35 | /** @var resync */ |
36 | protected $resync; |
37 | |
38 | /** @var storage */ |
39 | protected $storage; |
40 | |
41 | /** @var array Attachement IDs */ |
42 | protected $ids; |
43 | |
44 | /** @var string SQL ID string */ |
45 | private $sql_id; |
46 | |
47 | /** @var string SQL where string */ |
48 | private $sql_where = ''; |
49 | |
50 | /** @var int Number of deleted items */ |
51 | private $num_deleted; |
52 | |
53 | /** @var array Post IDs */ |
54 | private $post_ids = array(); |
55 | |
56 | /** @var array Message IDs */ |
57 | private $message_ids = array(); |
58 | |
59 | /** @var array Topic IDs */ |
60 | private $topic_ids = array(); |
61 | |
62 | /** @var array Info of physical file */ |
63 | private $physical = array(); |
64 | |
65 | /** |
66 | * Attachment delete class constructor |
67 | * |
68 | * @param config $config |
69 | * @param driver_interface $db |
70 | * @param dispatcher $dispatcher |
71 | * @param resync $resync |
72 | * @param storage $storage |
73 | */ |
74 | public function __construct(config $config, driver_interface $db, dispatcher $dispatcher, resync $resync, storage $storage) |
75 | { |
76 | $this->config = $config; |
77 | $this->db = $db; |
78 | $this->dispatcher = $dispatcher; |
79 | $this->resync = $resync; |
80 | $this->storage = $storage; |
81 | } |
82 | |
83 | /** |
84 | * Delete Attachments |
85 | * |
86 | * @param string $mode can be: post|message|topic|attach|user |
87 | * @param mixed $ids can be: post_ids, message_ids, topic_ids, attach_ids, user_ids |
88 | * @param bool $resync set this to false if you are deleting posts or topics |
89 | * |
90 | * @return int|bool Number of deleted attachments or false if something |
91 | * went wrong during attachment deletion |
92 | */ |
93 | public function delete($mode, $ids, $resync = true) |
94 | { |
95 | if (!$this->set_attachment_ids($ids)) |
96 | { |
97 | return false; |
98 | } |
99 | |
100 | $this->set_sql_constraints($mode); |
101 | |
102 | $sql_id = $this->sql_id; |
103 | |
104 | /** |
105 | * Perform additional actions before collecting data for attachment(s) deletion |
106 | * |
107 | * @event core.delete_attachments_collect_data_before |
108 | * @var string mode Variable containing attachments deletion mode, can be: post|message|topic|attach|user |
109 | * @var mixed ids Array or comma separated list of ids corresponding to the mode |
110 | * @var bool resync Flag indicating if posts/messages/topics should be synchronized |
111 | * @var string sql_id The field name to collect/delete data for depending on the mode |
112 | * @since 3.1.7-RC1 |
113 | */ |
114 | $vars = array( |
115 | 'mode', |
116 | 'ids', |
117 | 'resync', |
118 | 'sql_id', |
119 | ); |
120 | extract($this->dispatcher->trigger_event('core.delete_attachments_collect_data_before', compact($vars))); |
121 | |
122 | $this->sql_id = $sql_id; |
123 | unset($sql_id); |
124 | |
125 | // Collect post and topic ids for later use if we need to touch remaining entries (if resync is enabled) |
126 | $this->collect_attachment_info($resync); |
127 | |
128 | // Delete attachments from database |
129 | $this->delete_attachments_from_db($mode, $ids, $resync); |
130 | |
131 | $sql_id = $this->sql_id; |
132 | $post_ids = $this->post_ids; |
133 | $topic_ids = $this->topic_ids; |
134 | $message_ids = $this->message_ids; |
135 | $physical = $this->physical; |
136 | $num_deleted = $this->num_deleted; |
137 | |
138 | /** |
139 | * Perform additional actions after attachment(s) deletion from the database |
140 | * |
141 | * @event core.delete_attachments_from_database_after |
142 | * @var string mode Variable containing attachments deletion mode, can be: post|message|topic|attach|user |
143 | * @var mixed ids Array or comma separated list of ids corresponding to the mode |
144 | * @var bool resync Flag indicating if posts/messages/topics should be synchronized |
145 | * @var string sql_id The field name to collect/delete data for depending on the mode |
146 | * @var array post_ids Array with post ids for deleted attachment(s) |
147 | * @var array topic_ids Array with topic ids for deleted attachment(s) |
148 | * @var array message_ids Array with private message ids for deleted attachment(s) |
149 | * @var array physical Array with deleted attachment(s) physical file(s) data |
150 | * @var int num_deleted The number of deleted attachment(s) from the database |
151 | * @since 3.1.7-RC1 |
152 | */ |
153 | $vars = array( |
154 | 'mode', |
155 | 'ids', |
156 | 'resync', |
157 | 'sql_id', |
158 | 'post_ids', |
159 | 'topic_ids', |
160 | 'message_ids', |
161 | 'physical', |
162 | 'num_deleted', |
163 | ); |
164 | extract($this->dispatcher->trigger_event('core.delete_attachments_from_database_after', compact($vars))); |
165 | |
166 | $this->sql_id = $sql_id; |
167 | $this->post_ids = $post_ids; |
168 | $this->topic_ids = $topic_ids; |
169 | $this->message_ids = $message_ids; |
170 | $this->physical = $physical; |
171 | $this->num_deleted = $num_deleted; |
172 | unset($sql_id, $post_ids, $topic_ids, $message_ids, $physical, $num_deleted); |
173 | |
174 | if (!$this->num_deleted) |
175 | { |
176 | return 0; |
177 | } |
178 | |
179 | // Delete attachments from storage |
180 | $this->remove_from_storage($mode, $ids, $resync); |
181 | |
182 | // If we do not resync, we do not need to adjust any message, post, topic or user entries |
183 | if (!$resync) |
184 | { |
185 | return $this->num_deleted; |
186 | } |
187 | |
188 | // No more use for the original ids |
189 | unset($ids); |
190 | |
191 | // Update post indicators for posts now no longer having attachments |
192 | $this->resync->resync('post', $this->post_ids); |
193 | |
194 | // Update message table if messages are affected |
195 | $this->resync->resync('message', $this->message_ids); |
196 | |
197 | // Now update the topics. This is a bit trickier, because there could be posts still having attachments within the topic |
198 | $this->resync->resync('topic', $this->topic_ids); |
199 | |
200 | return $this->num_deleted; |
201 | } |
202 | |
203 | /** |
204 | * Set attachment IDs |
205 | * |
206 | * @param mixed $ids ID or array of IDs |
207 | * |
208 | * @return bool True if attachment IDs were set, false if not |
209 | */ |
210 | protected function set_attachment_ids($ids) |
211 | { |
212 | // 0 is as bad as an empty array |
213 | if (empty($ids)) |
214 | { |
215 | return false; |
216 | } |
217 | |
218 | if (is_array($ids)) |
219 | { |
220 | $ids = array_unique($ids); |
221 | $this->ids = array_map('intval', $ids); |
222 | } |
223 | else |
224 | { |
225 | $this->ids = array((int) $ids); |
226 | } |
227 | |
228 | return true; |
229 | } |
230 | |
231 | /** |
232 | * Set SQL constraints based on mode |
233 | * |
234 | * @param string $mode Delete mode; can be: post|message|topic|attach|user |
235 | */ |
236 | private function set_sql_constraints($mode) |
237 | { |
238 | switch ($mode) |
239 | { |
240 | case 'post': |
241 | case 'message': |
242 | $this->sql_id = 'post_msg_id'; |
243 | $this->sql_where = ' AND in_message = ' . ($mode == 'message' ? 1 : 0); |
244 | break; |
245 | |
246 | case 'topic': |
247 | $this->sql_id = 'topic_id'; |
248 | break; |
249 | |
250 | case 'user': |
251 | $this->sql_id = 'poster_id'; |
252 | break; |
253 | |
254 | case 'attach': |
255 | default: |
256 | $this->sql_id = 'attach_id'; |
257 | break; |
258 | } |
259 | } |
260 | |
261 | /** |
262 | * Collect info about attachment IDs |
263 | * |
264 | * @param bool $resync Whether topics/posts should be resynced after delete |
265 | */ |
266 | protected function collect_attachment_info($resync) |
267 | { |
268 | // Collect post and topic ids for later use if we need to touch remaining entries (if resync is enabled) |
269 | $sql = 'SELECT post_msg_id, topic_id, in_message, physical_filename, thumbnail, filesize, is_orphan |
270 | FROM ' . ATTACHMENTS_TABLE . ' |
271 | WHERE ' . $this->db->sql_in_set($this->sql_id, $this->ids); |
272 | |
273 | $sql .= $this->sql_where; |
274 | |
275 | $result = $this->db->sql_query($sql); |
276 | |
277 | while ($row = $this->db->sql_fetchrow($result)) |
278 | { |
279 | // We only need to store post/message/topic ids if resync is enabled and the file is not orphaned |
280 | if ($resync && !$row['is_orphan']) |
281 | { |
282 | if (!$row['in_message']) |
283 | { |
284 | $this->post_ids[] = $row['post_msg_id']; |
285 | $this->topic_ids[] = $row['topic_id']; |
286 | } |
287 | else |
288 | { |
289 | $this->message_ids[] = $row['post_msg_id']; |
290 | } |
291 | } |
292 | |
293 | $this->physical[] = array('filename' => $row['physical_filename'], 'thumbnail' => $row['thumbnail'], 'filesize' => $row['filesize'], 'is_orphan' => $row['is_orphan']); |
294 | } |
295 | $this->db->sql_freeresult($result); |
296 | |
297 | // IDs should be unique |
298 | $this->post_ids = array_unique($this->post_ids); |
299 | $this->message_ids = array_unique($this->message_ids); |
300 | $this->topic_ids = array_unique($this->topic_ids); |
301 | } |
302 | |
303 | /** |
304 | * Delete attachments from database table |
305 | */ |
306 | protected function delete_attachments_from_db($mode, $ids, $resync) |
307 | { |
308 | $sql_id = $this->sql_id; |
309 | $post_ids = $this->post_ids; |
310 | $topic_ids = $this->topic_ids; |
311 | $message_ids = $this->message_ids; |
312 | $physical = $this->physical; |
313 | |
314 | /** |
315 | * Perform additional actions before attachment(s) deletion |
316 | * |
317 | * @event core.delete_attachments_before |
318 | * @var string mode Variable containing attachments deletion mode, can be: post|message|topic|attach|user |
319 | * @var mixed ids Array or comma separated list of ids corresponding to the mode |
320 | * @var bool resync Flag indicating if posts/messages/topics should be synchronized |
321 | * @var string sql_id The field name to collect/delete data for depending on the mode |
322 | * @var array post_ids Array with post ids for deleted attachment(s) |
323 | * @var array topic_ids Array with topic ids for deleted attachment(s) |
324 | * @var array message_ids Array with private message ids for deleted attachment(s) |
325 | * @var array physical Array with deleted attachment(s) physical file(s) data |
326 | * @since 3.1.7-RC1 |
327 | */ |
328 | $vars = array( |
329 | 'mode', |
330 | 'ids', |
331 | 'resync', |
332 | 'sql_id', |
333 | 'post_ids', |
334 | 'topic_ids', |
335 | 'message_ids', |
336 | 'physical', |
337 | ); |
338 | extract($this->dispatcher->trigger_event('core.delete_attachments_before', compact($vars))); |
339 | |
340 | $this->sql_id = $sql_id; |
341 | $this->post_ids = $post_ids; |
342 | $this->topic_ids = $topic_ids; |
343 | $this->message_ids = $message_ids; |
344 | $this->physical = $physical; |
345 | unset($sql_id, $post_ids, $topic_ids, $message_ids, $physical); |
346 | |
347 | // Delete attachments |
348 | $sql = 'DELETE FROM ' . ATTACHMENTS_TABLE . ' |
349 | WHERE ' . $this->db->sql_in_set($this->sql_id, $this->ids); |
350 | |
351 | $sql .= $this->sql_where; |
352 | |
353 | $this->db->sql_query($sql); |
354 | $this->num_deleted = $this->db->sql_affectedrows(); |
355 | } |
356 | |
357 | /** |
358 | * Delete attachments from storage |
359 | */ |
360 | protected function remove_from_storage($mode, $ids, $resync) |
361 | { |
362 | $space_removed = $files_removed = 0; |
363 | |
364 | foreach ($this->physical as $file_ary) |
365 | { |
366 | if ($this->unlink_attachment($file_ary['filename'], 'file', true) && !$file_ary['is_orphan']) |
367 | { |
368 | // Only non-orphaned files count to the file size |
369 | $space_removed += $file_ary['filesize']; |
370 | $files_removed++; |
371 | } |
372 | |
373 | if ($file_ary['thumbnail']) |
374 | { |
375 | $this->unlink_attachment($file_ary['filename'], 'thumbnail', true); |
376 | } |
377 | } |
378 | |
379 | $sql_id = $this->sql_id; |
380 | $post_ids = $this->post_ids; |
381 | $topic_ids = $this->topic_ids; |
382 | $message_ids = $this->message_ids; |
383 | $physical = $this->physical; |
384 | $num_deleted = $this->num_deleted; |
385 | |
386 | /** |
387 | * Perform additional actions after attachment(s) deletion from the filesystem |
388 | * |
389 | * @event core.delete_attachments_from_filesystem_after |
390 | * @var string mode Variable containing attachments deletion mode, can be: post|message|topic|attach|user |
391 | * @var mixed ids Array or comma separated list of ids corresponding to the mode |
392 | * @var bool resync Flag indicating if posts/messages/topics should be synchronized |
393 | * @var string sql_id The field name to collect/delete data for depending on the mode |
394 | * @var array post_ids Array with post ids for deleted attachment(s) |
395 | * @var array topic_ids Array with topic ids for deleted attachment(s) |
396 | * @var array message_ids Array with private message ids for deleted attachment(s) |
397 | * @var array physical Array with deleted attachment(s) physical file(s) data |
398 | * @var int num_deleted The number of deleted attachment(s) from the database |
399 | * @var int space_removed The size of deleted files(s) from the filesystem |
400 | * @var int files_removed The number of deleted file(s) from the filesystem |
401 | * @since 3.1.7-RC1 |
402 | */ |
403 | $vars = array( |
404 | 'mode', |
405 | 'ids', |
406 | 'resync', |
407 | 'sql_id', |
408 | 'post_ids', |
409 | 'topic_ids', |
410 | 'message_ids', |
411 | 'physical', |
412 | 'num_deleted', |
413 | 'space_removed', |
414 | 'files_removed', |
415 | ); |
416 | extract($this->dispatcher->trigger_event('core.delete_attachments_from_filesystem_after', compact($vars))); |
417 | |
418 | $this->sql_id = $sql_id; |
419 | $this->post_ids = $post_ids; |
420 | $this->topic_ids = $topic_ids; |
421 | $this->message_ids = $message_ids; |
422 | $this->physical = $physical; |
423 | $this->num_deleted = $num_deleted; |
424 | unset($sql_id, $post_ids, $topic_ids, $message_ids, $physical, $num_deleted); |
425 | |
426 | if ($space_removed || $files_removed) |
427 | { |
428 | $this->config->increment('upload_dir_size', $space_removed * (-1), false); |
429 | $this->config->increment('num_files', $files_removed * (-1), false); |
430 | } |
431 | } |
432 | |
433 | /** |
434 | * Delete attachment from storage |
435 | * |
436 | * @param string $filename Filename of attachment |
437 | * @param string $mode Delete mode |
438 | * @param bool $entry_removed Whether entry was removed. Defaults to false |
439 | * @return bool True if file was removed, false if not |
440 | */ |
441 | public function unlink_attachment($filename, $mode = 'file', $entry_removed = false) |
442 | { |
443 | // Because of copying topics or modifications a physical filename could be assigned more than once. If so, do not remove the file itself. |
444 | $sql = 'SELECT COUNT(attach_id) AS num_entries |
445 | FROM ' . ATTACHMENTS_TABLE . " |
446 | WHERE physical_filename = '" . $this->db->sql_escape(utf8_basename($filename)) . "'"; |
447 | $result = $this->db->sql_query($sql); |
448 | $num_entries = (int) $this->db->sql_fetchfield('num_entries'); |
449 | $this->db->sql_freeresult($result); |
450 | |
451 | // Do not remove file if at least one additional entry with the same name exist. |
452 | if (($entry_removed && $num_entries > 0) || (!$entry_removed && $num_entries > 1)) |
453 | { |
454 | return false; |
455 | } |
456 | |
457 | $filename = ($mode == 'thumbnail') ? 'thumb_' . utf8_basename($filename) : utf8_basename($filename); |
458 | |
459 | try |
460 | { |
461 | if ($this->storage->exists($filename)) |
462 | { |
463 | $this->storage->delete($filename); |
464 | return true; |
465 | } |
466 | } |
467 | catch (\phpbb\storage\exception\storage_exception $exception) |
468 | { |
469 | // Fail is covered by return statement below |
470 | } |
471 | |
472 | return false; |
473 | } |
474 | } |