Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
140 / 140
delete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
8 / 8
36
100.00% covered (success)
100.00%
140 / 140
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
7 / 7
 delete
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
35 / 35
 set_attachment_ids
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
7 / 7
 set_sql_constraints
100.00% covered (success)
100.00%
1 / 1
7
100.00% covered (success)
100.00%
16 / 16
 collect_attachment_info
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
16 / 16
 delete_attachments_from_db
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
19 / 19
 remove_from_filesystem
100.00% covered (success)
100.00%
1 / 1
7
100.00% covered (success)
100.00%
26 / 26
 unlink_attachment
100.00% covered (success)
100.00%
1 / 1
8
100.00% covered (success)
100.00%
14 / 14
<?php
/**
 *
 * This file is part of the phpBB Forum Software package.
 *
 * @copyright (c) phpBB Limited <https://www.phpbb.com>
 * @license GNU General Public License, version 2 (GPL-2.0)
 *
 * For full copyright and license information, please see
 * the docs/CREDITS.txt file.
 *
 */
namespace phpbb\attachment;
use phpbb\config\config;
use phpbb\db\driver\driver_interface;
use phpbb\event\dispatcher;
use phpbb\filesystem\filesystem;
/**
 * Attachment delete class
 */
class delete
{
    /** @var config */
    protected $config;
    /** @var driver_interface */
    protected $db;
    /** @var dispatcher */
    protected $dispatcher;
    /** @var filesystem  */
    protected $filesystem;
    /** @var resync */
    protected $resync;
    /** @var string phpBB root path */
    protected $phpbb_root_path;
    /** @var array Attachement IDs */
    protected $ids;
    /** @var string SQL ID string */
    private $sql_id;
    /** @var string SQL where string */
    private $sql_where = '';
    /** @var int Number of deleted items */
    private $num_deleted;
    /** @var array Post IDs */
    private $post_ids = array();
    /** @var array Message IDs */
    private $message_ids = array();
    /** @var array Topic IDs */
    private $topic_ids = array();
    /** @var array Info of physical file */
    private $physical = array();
    /**
     * Attachment delete class constructor
     *
     * @param config $config
     * @param driver_interface $db
     * @param dispatcher $dispatcher
     * @param filesystem $filesystem
     * @param resync $resync
     * @param string $phpbb_root_path
     */
    public function __construct(config $config, driver_interface $db, dispatcher $dispatcher, filesystem $filesystem, resync $resync, $phpbb_root_path)
    {
        $this->config = $config;
        $this->db = $db;
        $this->dispatcher = $dispatcher;
        $this->filesystem = $filesystem;
        $this->resync = $resync;
        $this->phpbb_root_path = $phpbb_root_path;
    }
    /**
     * Delete Attachments
     *
     * @param string $mode can be: post|message|topic|attach|user
     * @param mixed $ids can be: post_ids, message_ids, topic_ids, attach_ids, user_ids
     * @param bool $resync set this to false if you are deleting posts or topics
     *
     * @return int|bool Number of deleted attachments or false if something
     *            went wrong during attachment deletion
     */
    public function delete($mode, $ids, $resync = true)
    {
        if (!$this->set_attachment_ids($ids))
        {
            return false;
        }
        $this->set_sql_constraints($mode);
        $sql_id = $this->sql_id;
        /**
         * Perform additional actions before collecting data for attachment(s) deletion
         *
         * @event core.delete_attachments_collect_data_before
         * @var    string    mode            Variable containing attachments deletion mode, can be: post|message|topic|attach|user
         * @var    mixed    ids                Array or comma separated list of ids corresponding to the mode
         * @var    bool    resync            Flag indicating if posts/messages/topics should be synchronized
         * @var    string    sql_id            The field name to collect/delete data for depending on the mode
         * @since 3.1.7-RC1
         */
        $vars = array(
            'mode',
            'ids',
            'resync',
            'sql_id',
        );
        extract($this->dispatcher->trigger_event('core.delete_attachments_collect_data_before', compact($vars)));
        $this->sql_id = $sql_id;
        unset($sql_id);
        // Collect post and topic ids for later use if we need to touch remaining entries (if resync is enabled)
        $this->collect_attachment_info($resync);
        // Delete attachments from database
        $this->delete_attachments_from_db($mode, $ids, $resync);
        $sql_id = $this->sql_id;
        $post_ids = $this->post_ids;
        $topic_ids = $this->topic_ids;
        $message_ids = $this->message_ids;
        $physical = $this->physical;
        $num_deleted = $this->num_deleted;
        /**
         * Perform additional actions after attachment(s) deletion from the database
         *
         * @event core.delete_attachments_from_database_after
         * @var    string    mode            Variable containing attachments deletion mode, can be: post|message|topic|attach|user
         * @var    mixed    ids                Array or comma separated list of ids corresponding to the mode
         * @var    bool    resync            Flag indicating if posts/messages/topics should be synchronized
         * @var    string    sql_id            The field name to collect/delete data for depending on the mode
         * @var    array    post_ids        Array with post ids for deleted attachment(s)
         * @var    array    topic_ids        Array with topic ids for deleted attachment(s)
         * @var    array    message_ids        Array with private message ids for deleted attachment(s)
         * @var    array    physical        Array with deleted attachment(s) physical file(s) data
         * @var    int        num_deleted        The number of deleted attachment(s) from the database
         * @since 3.1.7-RC1
         */
        $vars = array(
            'mode',
            'ids',
            'resync',
            'sql_id',
            'post_ids',
            'topic_ids',
            'message_ids',
            'physical',
            'num_deleted',
        );
        extract($this->dispatcher->trigger_event('core.delete_attachments_from_database_after', compact($vars)));
        $this->sql_id = $sql_id;
        $this->post_ids = $post_ids;
        $this->topic_ids = $topic_ids;
        $this->message_ids = $message_ids;
        $this->physical = $physical;
        $this->num_deleted = $num_deleted;
        unset($sql_id, $post_ids, $topic_ids, $message_ids, $physical, $num_deleted);
        if (!$this->num_deleted)
        {
            return 0;
        }
        // Delete attachments from filesystem
        $this->remove_from_filesystem($mode, $ids, $resync);
        // If we do not resync, we do not need to adjust any message, post, topic or user entries
        if (!$resync)
        {
            return $this->num_deleted;
        }
        // No more use for the original ids
        unset($ids);
        // Update post indicators for posts now no longer having attachments
        $this->resync->resync('post', $this->post_ids);
        // Update message table if messages are affected
        $this->resync->resync('message', $this->message_ids);
        // Now update the topics. This is a bit trickier, because there could be posts still having attachments within the topic
        $this->resync->resync('topic', $this->topic_ids);
        return $this->num_deleted;
    }
    /**
     * Set attachment IDs
     *
     * @param mixed $ids ID or array of IDs
     *
     * @return bool True if attachment IDs were set, false if not
     */
    protected function set_attachment_ids($ids)
    {
        // 0 is as bad as an empty array
        if (empty($ids))
        {
            return false;
        }
        if (is_array($ids))
        {
            $ids = array_unique($ids);
            $this->ids = array_map('intval', $ids);
        }
        else
        {
            $this->ids = array((int) $ids);
        }
        return true;
    }
    /**
     * Set SQL constraints based on mode
     *
     * @param string $mode Delete mode; can be: post|message|topic|attach|user
     */
    private function set_sql_constraints($mode)
    {
        switch ($mode)
        {
            case 'post':
            case 'message':
                $this->sql_id = 'post_msg_id';
                $this->sql_where = ' AND in_message = ' . ($mode == 'message' ? 1 : 0);
            break;
            case 'topic':
                $this->sql_id = 'topic_id';
            break;
            case 'user':
                $this->sql_id = 'poster_id';
            break;
            case 'attach':
            default:
                $this->sql_id = 'attach_id';
            break;
        }
    }
    /**
     * Collect info about attachment IDs
     *
     * @param bool $resync Whether topics/posts should be resynced after delete
     */
    protected function collect_attachment_info($resync)
    {
        // Collect post and topic ids for later use if we need to touch remaining entries (if resync is enabled)
        $sql = 'SELECT post_msg_id, topic_id, in_message, physical_filename, thumbnail, filesize, is_orphan
            FROM ' . ATTACHMENTS_TABLE . '
            WHERE ' . $this->db->sql_in_set($this->sql_id, $this->ids);
        $sql .= $this->sql_where;
        $result = $this->db->sql_query($sql);
        while ($row = $this->db->sql_fetchrow($result))
        {
            // We only need to store post/message/topic ids if resync is enabled and the file is not orphaned
            if ($resync && !$row['is_orphan'])
            {
                if (!$row['in_message'])
                {
                    $this->post_ids[] = $row['post_msg_id'];
                    $this->topic_ids[] = $row['topic_id'];
                }
                else
                {
                    $this->message_ids[] = $row['post_msg_id'];
                }
            }
            $this->physical[] = array('filename' => $row['physical_filename'], 'thumbnail' => $row['thumbnail'], 'filesize' => $row['filesize'], 'is_orphan' => $row['is_orphan']);
        }
        $this->db->sql_freeresult($result);
        // IDs should be unique
        $this->post_ids = array_unique($this->post_ids);
        $this->message_ids = array_unique($this->message_ids);
        $this->topic_ids = array_unique($this->topic_ids);
    }
    /**
     * Delete attachments from database table
     */
    protected function delete_attachments_from_db($mode, $ids, $resync)
    {
        $sql_id = $this->sql_id;
        $post_ids = $this->post_ids;
        $topic_ids = $this->topic_ids;
        $message_ids = $this->message_ids;
        $physical = $this->physical;
        /**
         * Perform additional actions before attachment(s) deletion
         *
         * @event core.delete_attachments_before
         * @var    string    mode            Variable containing attachments deletion mode, can be: post|message|topic|attach|user
         * @var    mixed    ids                Array or comma separated list of ids corresponding to the mode
         * @var    bool    resync            Flag indicating if posts/messages/topics should be synchronized
         * @var    string    sql_id            The field name to collect/delete data for depending on the mode
         * @var    array    post_ids        Array with post ids for deleted attachment(s)
         * @var    array    topic_ids        Array with topic ids for deleted attachment(s)
         * @var    array    message_ids        Array with private message ids for deleted attachment(s)
         * @var    array    physical        Array with deleted attachment(s) physical file(s) data
         * @since 3.1.7-RC1
         */
        $vars = array(
            'mode',
            'ids',
            'resync',
            'sql_id',
            'post_ids',
            'topic_ids',
            'message_ids',
            'physical',
        );
        extract($this->dispatcher->trigger_event('core.delete_attachments_before', compact($vars)));
        $this->sql_id = $sql_id;
        $this->post_ids = $post_ids;
        $this->topic_ids = $topic_ids;
        $this->message_ids = $message_ids;
        $this->physical = $physical;
        unset($sql_id, $post_ids, $topic_ids, $message_ids, $physical);
        // Delete attachments
        $sql = 'DELETE FROM ' . ATTACHMENTS_TABLE . '
            WHERE ' . $this->db->sql_in_set($this->sql_id, $this->ids);
        $sql .= $this->sql_where;
        $this->db->sql_query($sql);
        $this->num_deleted = $this->db->sql_affectedrows();
    }
    /**
     * Delete attachments from filesystem
     */
    protected function remove_from_filesystem($mode, $ids, $resync)
    {
        $space_removed = $files_removed = 0;
        foreach ($this->physical as $file_ary)
        {
            if ($this->unlink_attachment($file_ary['filename'], 'file', true) && !$file_ary['is_orphan'])
            {
                // Only non-orphaned files count to the file size
                $space_removed += $file_ary['filesize'];
                $files_removed++;
            }
            if ($file_ary['thumbnail'])
            {
                $this->unlink_attachment($file_ary['filename'], 'thumbnail', true);
            }
        }
        $sql_id = $this->sql_id;
        $post_ids = $this->post_ids;
        $topic_ids = $this->topic_ids;
        $message_ids = $this->message_ids;
        $physical = $this->physical;
        $num_deleted = $this->num_deleted;
        /**
         * Perform additional actions after attachment(s) deletion from the filesystem
         *
         * @event core.delete_attachments_from_filesystem_after
         * @var    string    mode            Variable containing attachments deletion mode, can be: post|message|topic|attach|user
         * @var    mixed    ids                Array or comma separated list of ids corresponding to the mode
         * @var    bool    resync            Flag indicating if posts/messages/topics should be synchronized
         * @var    string    sql_id            The field name to collect/delete data for depending on the mode
         * @var    array    post_ids        Array with post ids for deleted attachment(s)
         * @var    array    topic_ids        Array with topic ids for deleted attachment(s)
         * @var    array    message_ids        Array with private message ids for deleted attachment(s)
         * @var    array    physical        Array with deleted attachment(s) physical file(s) data
         * @var    int        num_deleted        The number of deleted attachment(s) from the database
         * @var    int        space_removed    The size of deleted files(s) from the filesystem
         * @var    int        files_removed    The number of deleted file(s) from the filesystem
         * @since 3.1.7-RC1
         */
        $vars = array(
            'mode',
            'ids',
            'resync',
            'sql_id',
            'post_ids',
            'topic_ids',
            'message_ids',
            'physical',
            'num_deleted',
            'space_removed',
            'files_removed',
        );
        extract($this->dispatcher->trigger_event('core.delete_attachments_from_filesystem_after', compact($vars)));
        $this->sql_id = $sql_id;
        $this->post_ids = $post_ids;
        $this->topic_ids = $topic_ids;
        $this->message_ids = $message_ids;
        $this->physical = $physical;
        $this->num_deleted = $num_deleted;
        unset($sql_id, $post_ids, $topic_ids, $message_ids, $physical, $num_deleted);
        if ($space_removed || $files_removed)
        {
            $this->config->increment('upload_dir_size', $space_removed * (-1), false);
            $this->config->increment('num_files', $files_removed * (-1), false);
        }
    }
    /**
     * Delete attachment from filesystem
     *
     * @param string $filename Filename of attachment
     * @param string $mode Delete mode
     * @param bool $entry_removed Whether entry was removed. Defaults to false
     * @return bool True if file was removed, false if not
     */
    public function unlink_attachment($filename, $mode = 'file', $entry_removed = false)
    {
        // Because of copying topics or modifications a physical filename could be assigned more than once. If so, do not remove the file itself.
        $sql = 'SELECT COUNT(attach_id) AS num_entries
        FROM ' . ATTACHMENTS_TABLE . "
        WHERE physical_filename = '" . $this->db->sql_escape(utf8_basename($filename)) . "'";
        $result = $this->db->sql_query($sql);
        $num_entries = (int) $this->db->sql_fetchfield('num_entries');
        $this->db->sql_freeresult($result);
        // Do not remove file if at least one additional entry with the same name exist.
        if (($entry_removed && $num_entries > 0) || (!$entry_removed && $num_entries > 1))
        {
            return false;
        }
        $filename = ($mode == 'thumbnail') ? 'thumb_' . utf8_basename($filename) : utf8_basename($filename);
        $filepath = $this->phpbb_root_path . $this->config['upload_path'] . '/' . $filename;
        try
        {
            if ($this->filesystem->exists($filepath))
            {
                $this->filesystem->remove($this->phpbb_root_path . $this->config['upload_path'] . '/' . $filename);
                return true;
            }
        }
        catch (\phpbb\filesystem\exception\filesystem_exception $exception)
        {
            // Fail is covered by return statement below
        }
        return false;
    }
}