rugk/xenforo-threema-gateway

View on GitHub
src/library/ThreemaGateway/Handler/Action/Receiver.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php
/**
 * Allows one to query received Threema messages and to delete them.
 *
 * This class is basically a wrapper around the message model used for
 * querying the messages and tries to make it easier to access them.
 *
 * @package ThreemaGateway
 * @author rugk
 * @copyright Copyright (c) 2015-2016 rugk
 * @license MIT
 */

class ThreemaGateway_Handler_Action_Receiver extends ThreemaGateway_Handler_Action_Abstract
{
    /**
     * @var bool whether the model is already prepared
     */
    protected $isPrepared = false;

    /**
     * @var bool whether the permissions are already checked
     */
    protected $isPermissionChecked = false;

    /**
     * @var bool whether the result should be grouped by the message type
     */
    protected $groupByMessageType = false;

    /**
     * Startup.
     *
     * @param bool $alreadyPrepared     If the Message Model is already prepared you
     *                                  may set this to true.
     * @param bool $skipPermissionCheck Set to true to skip the permission check.
     *                                  (not recommend)
     */
    public function __construct($alreadyPrepared = false, $skipPermissionCheck = false)
    {
        parent::__construct();
        $this->isPrepared          = $alreadyPrepared;
        $this->isPermissionChecked = $skipPermissionCheck;
    }

    /**
     * Sets whether the result should be grouped by the message type.
     *
     * The option is ignored when you specify the message type in a function
     * as grouping in this case would not make any sense.
     *
     * @param  bool       $groupByMessageType
     * @return null|array
     */
    public function groupByMessageType($groupByMessageType)
    {
        $this->groupByMessageType = $groupByMessageType;
    }

    /**
     * Returns the single last message received.
     *
     * @param  string     $threemaId   filter by Threema ID (optional)
     * @param  string     $messageType filter by message type (optional, use Model constants)
     * @param  string     $keyword     filter by this string, which represents
     *                                 the text in a text message (Wildcards: * and ?)
     * @return null|array
     */
    public function getLastMessage($threemaId = null, $messageType = null, $keyword = null)
    {
        $this->initiate();
        /** @var ThreemaGateway_Model_Messages $model */
        $model = XenForo_Model::create('ThreemaGateway_Model_Messages');

        // set options
        if ($threemaId) {
            $model->setSenderId($threemaId);
        }
        if ($messageType) {
            $model->setTypeCode($messageType);
        }
        if ($keyword) {
            $keyword = $this->replaceWildcards($keyword);
            $model->setKeyword($keyword);
        }

        // only show last result
        $model->setResultLimit(1);
        // to make sure the message is really the last one, sort it by the send time
        $model->setOrder('date_send', 'desc');

        return $this->execute($model, $messageType);
    }

    /**
     * Returns all messages with the specified criterias.
     *
     * @param  string     $threemaId   filter by Threema ID (optional)
     * @param  string     $messageType filter by message type (optional, use Model constants)
     * @param  string     $timeSpan    a relative time (parsable by strtotime) to limit the query (e.g. '-7 days')
     * @param  string     $keyword     filter by this string, which represents
     *                                 the text in a text message (Wildcards: * and ?)
     * @return null|array
     */
    public function getMessages($threemaId = null, $messageType = null, $timeSpan = null, $keyword = null)
    {
        $this->initiate();
        /** @var ThreemaGateway_Model_Messages $model */
        $model = XenForo_Model::create('ThreemaGateway_Model_Messages');

        // set options
        if ($threemaId) {
            $model->setSenderId($threemaId);
        }
        if ($messageType) {
            $model->setTypeCode($messageType);
        }
        if ($timeSpan) {
            $model->setTimeLimit(strtotime($timeSpan, XenForo_Application::$time));
            // manual ordering is not necessary as only new messages are inserted
            // ("at the bottom") and the dates never change,
        }
        if ($keyword) {
            $keyword = $this->replaceWildcards($keyword);
            $model->setKeyword($keyword);
        }

        return $this->execute($model, $messageType);
    }

    /**
     * Returns the message data for a particular message ID.
     *
     * Note that when the database is corrupt and e.g. for a message some
     * datasets are missing, thsi will return null.
     *
     * @param  string     $messageId
     * @param  string     $messageType If you know the message type it is very much
     *                                 recommend to specify it here.
     * @return null|array
     */
    public function getMessageData($messageId, $messageType = null)
    {
        $this->initiate();
        /** @var ThreemaGateway_Model_Messages $model */
        $model = XenForo_Model::create('ThreemaGateway_Model_Messages');

        // set options
        if ($messageId) {
            $model->setMessageId($messageId, 'metamessage');
        }
        if ($messageType) {
            $model->setTypeCode($messageType);
        }

        return $this->execute($model, $messageType);
    }

    /**
     * Checks whether a message is saved in the database.
     *
     * Note that this does not guarantee that other methods return any data as
     * a message is also considered "received" when the actual data has
     * already been deleted.
     * All other methods return "null" in this case as they cannot return all of
     * the requested data. This function however would return true.
     *
     * @param  string $messageId
     * @return bool
     */
    public function messageIsReceived($messageId)
    {
        $this->initiate();
        /** @var ThreemaGateway_Model_Messages $model */
        $model = XenForo_Model::create('ThreemaGateway_Model_Messages');

        // validate parameter
        if (!$messageId) {
            throw new XenForo_Exception('Parameter $messageId missing.');
        }

        // set options
        $model->setMessageId($messageId, 'metamessage');

        // query meta data
        if ($model->getMessageMetaData(false, false)) {
            return true;
        }

        return false;
    }

    /**
     * Returns the list of all files.
     *
     * Grouping this result ({@see groupByMessageType()}) is not supported.
     * Note that the result is still the usual array of all other message
     * queries here.
     *
     * @param  string     $threemaId     filter by Threema ID (optional)
     * @param  string     $mimeType      Filter by mime type (optional).
     * @param  string     $fileType      The file type, e.g. thumbnail/file or image (optional).
     *                                   This is a Threema-internal type and may not
     *                                   be particular useful.
     * @param  bool       $queryMetaData Set to false, to prevent querying for meta
     *                                   data, which might speed up the query. (default: true)
     * @return null|array
     */
    public function getFileList($mimeType = null, $fileType = null, $threemaId = null, $queryMetaData = true)
    {
        $this->initiate();
        $model = XenForo_Model::create('ThreemaGateway_Model_Messages');

        // set options
        if ($threemaId) {
            $model->setSenderId($threemaId);
        }

        if ($fileType) {
            $model->injectFetchOption('where', 'filelist.file_type = ?', true);
            $model->injectFetchOption('params', $fileType, true);
        }

        // reset grouping as it cannot be processed
        $this->groupByMessageType = false;

        // determinate, which message types may be affected
        if ($mimeType !== null &&
            $mimeType !== 'image/jpeg') {
            // we can skip the image table as it is impossible that image files
            // would be returned in this query
            /** @var string|null $messageType */
            $messageType = ThreemaGateway_Model_Messages::TYPE_FILE_MESSAGE;

            // and we can already set the mime type as a condition
            $model->injectFetchOption('where', 'message.mime_type = ?', true);
            $model->injectFetchOption('params', $mimeType, true);

            // set message type as option
            $model->setTypeCode($messageType);

            // As the mime type is set to something different than image/jpeg,
            // now all images are already excluded, so we can return the data
            // with one query querying only the files table.

            /** @var array $result */
            $result = $model->getMessageDataByType($messageType, $queryMetaData);
        } else {
            // It's more complex if we want to query image & file messages
            // together, as the MIME type includes image files.
            //
            // Forunately this problem can be solved by just querying each
            // message type individually. This does also only do 2 queries,
            // which is even less than if we would use getAllMessageData(), as
            // there we need 3 queries: metadata + msg type 1 + msg type 2.
            // As we know the possible message types this is possible. In
            // all other ways one must query the metadata and filter or split
            // it accordingly, to later execute the queries.

            // first we query the image files
            // (without MIME type setting as images can only have one
            // MIME type - image/jpeg - anyway)

            /** @var array|null $images */
            $images = $model->getMessageDataByType(ThreemaGateway_Model_Messages::TYPE_IMAGE_MESSAGE, $queryMetaData);

            // now set the MIME type if there is one
            if ($mimeType) {
                $model->injectFetchOption('where', 'message.mime_type = ?', true);
                $model->injectFetchOption('params', $mimeType, true);
            }

            // and now query all other files
            /** @var array|null $files */
            $files = $model->getMessageDataByType(ThreemaGateway_Model_Messages::TYPE_FILE_MESSAGE, $queryMetaData);
            $model->resetFetchOptions();

            // handle empty results transparently
            if (!$images) {
                $images = [];
            }
            if (!$files) {
                $files = [];
            }

            // and combine results
            /** @var array $result */
            $result = array_merge($images, $files);
        }

        if (empty($result)) {
            return null;
        }

        return $result;
    }

    /**
     * Returns the current state of a particular message.
     *
     * Only the state of *send* messages can be queried.
     * Note: In contrast to most other methods here, this already returns the
     * message/delivery state as an integer.
     *
     * @param  string   $messageSentId The ID of message, which has been send to a user
     * @return null|int
     */
    public function getMessageState($messageSentId)
    {
        $this->initiate();

        // reset grouping as it cannot be processed
        $this->groupByMessageType = false;

        /** @var array $result */
        $result = $this->getMessageStateHistory($messageSentId, false, 1);
        if (!$result) {
            return null;
        }

        // dig into array
        $result = reset($result)['ackmsgs'];

        // as theoretically one delivery message could include multiple
        // delivery receipts we formally have to walk through the result
        /** @var int $deliveryReceipt */
        $deliveryReceipt = 0;
        foreach ($result as $content) {
            if ($content['receipt_type'] > $deliveryReceipt) {
                $deliveryReceipt = $content['receipt_type'];
            }
        }

        // finally return the state integer
        return $deliveryReceipt;
    }

    /**
     * Returns the history of all state changes of a particular message.
     *
     * Only the state of *send* messages can be queried.
     * The result is already ordered from the not so important state to the most
     * important one.
     *
     * @param  string     $messageSentId The ID of message, which has been send to a user
     * @param  bool       $getMetaData   Set to false, to speed up the query by not
     *                                   asking for meta data (when the state was received etc).
     *                                   (default: false)
     * @param  int        $limitQuery    When set, only the last x states are returned.
     * @return null|array
     */
    public function getMessageStateHistory($messageSentId, $getMetaData = true, $limitQuery = null)
    {
        $this->initiate();
        /** @var ThreemaGateway_Model_Messages $model */
        $model = XenForo_Model::create('ThreemaGateway_Model_Messages');

        $model->injectFetchOption('where', 'ack_messages.ack_message_id = ?', true);
        $model->injectFetchOption('params', $messageSentId, true);

        $model->setOrder('delivery_state', 'desc');

        if ($limitQuery) {
            $model->setResultLimit($limitQuery);
        }

        return $model->getMessageDataByType(ThreemaGateway_Model_Messages::TYPE_DELIVERY_MESSAGE, $getMetaData);
    }

    /**
     * Returns an array with all type codes currently supported.
     *
     * @return array
     */
    public function getTypesArray()
    {
        return [
            ThreemaGateway_Model_Messages::TYPE_DELIVERY_MESSAGE,
            ThreemaGateway_Model_Messages::TYPE_FILE_MESSAGE,
            ThreemaGateway_Model_Messages::TYPE_IMAGE_MESSAGE,
            ThreemaGateway_Model_Messages::TYPE_TEXT_MESSAGE
        ];
    }

    /**
     * Remove a message from the database.
     *
     * Note that the message is usually not completly removed and the message ID
     * will stay in the database. The exact behaviour depends on the ACP "Harden
     * against replay attacks" setting.
     * This prevents replay attacks as otherwise a message with the same message
     * ID could be inserted again into the database and would therefore be
     * considered a new message, which has just been received although it
     * actually had been received two times.
     * However the message ID alone does not reveal any data (as all data &
     * meta data, even the message type, is deleted).
     *
     * @param string $messageId
     */
    public function removeMessage($messageId)
    {
        $this->initiate();

        /** @var ThreemaGateway_DataWriter_Messages $dataWriter */
        $dataWriter = XenForo_DataWriter::create('ThreemaGateway_DataWriter_Messages');
        $dataWriter->setExistingData($messageId);
        $dataWriter->delete();
    }

    /**
     * Checks whether the user is allowed to receive messages and prepares Model
     * if necessary.
     *
     * @throws XenForo_Exception
     */
    protected function initiate()
    {
        // check permission
        if (!$this->isPermissionChecked) {
            if (!$this->permissions->hasPermission('receive')) {
                throw new XenForo_Exception(new XenForo_Phrase('threemagw_permission_error'));
            }

            $this->isPermissionChecked = true;
        }

        if (!$this->isPrepared) {
            /** @var ThreemaGateway_Model_Messages $model */
            $model = XenForo_Model::create('ThreemaGateway_Model_Messages');
            $model->preQuery();

            $this->isPrepared = true;
        }
    }

    /**
     * Queries the meta data and the main data of the messages itself.
     *
     * @param ThreemaGateway_Model_Messages $model
     * @param int                           $messageType The type of the message (optional)
     */
    protected function execute($model, $messageType = null)
    {
        if ($messageType) {
            return $model->getMessageDataByType($messageType, true);
        } else {
            // query meta data
            $metaData = $model->getMessageMetaData();
            if (!$metaData) {
                return null;
            }

            $model->resetFetchOptions();

            // query details
            return $model->getAllMessageData($metaData, $this->groupByMessageType);
        }
    }

    /**
     * Replace usual wildcards (?, *) with the ones used by MySQL (%, _).
     *
     * @param  string $string The string to replace
     * @return string
     */
    protected function replaceWildcards($string)
    {
        return str_replace([
            '%',
            '_',
            '*',
            '?',
        ], [
            '\%',
            '\_',
            '%',
            '?',
        ], $string);
    }
}