Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
170 / 170
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
delete
100.00% covered (success)
100.00%
170 / 170
100.00% covered (success)
100.00%
8 / 8
37
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
50 / 50
100.00% covered (success)
100.00%
1 / 1
4
 set_attachment_ids
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 set_sql_constraints
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
8
 collect_attachment_info
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
 delete_attachments_from_db
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
1
 remove_from_storage
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
7
 unlink_attachment
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
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
14namespace phpbb\attachment;
15
16use phpbb\config\config;
17use phpbb\db\driver\driver_interface;
18use phpbb\event\dispatcher;
19use phpbb\storage\storage;
20
21/**
22 * Attachment delete class
23 */
24class 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}