rugk/xenforo-threema-gateway

View on GitHub
src/library/ThreemaGateway/Handler/Permissions.php

Summary

Maintainability
B
4 hrs
Test Coverage
<?php
/**
 * Manages permissions of all actions.
 *
 * It is designed as a singleton, as there should only be one user whose
 * permissions are checked and this is the current user.
 * Thus it is not allowed to check the permissions of third-party users as
 * it does not make any sense and only introduces potential vulnerabilities as
 * it might allow to take over permissions from other users.
 *
 * @package ThreemaGateway
 * @author rugk
 * @copyright Copyright (c) 2015-2016 rugk
 * @license MIT
 */

/**
 * This manages the permissions of all actions.
 */
class ThreemaGateway_Handler_Permissions
{
    /**
     * @var Singleton
     */
    protected static $instance = null;

    /**
     * @var string identifier of the permission group of this addon
     */
    const PERMISSION_GROUP = 'threemagw';

    /**
     * @var array all possible permissions of this add-on
     */
    const PERMISSION_LIST = [
        ['id' => 'use'],
        ['id' => 'send'],
        ['id' => 'receive'],
        ['id' => 'fetch'],
        ['id' => 'lookup'],
        ['id' => 'tfa'],
        // 2FA fast mode
        ['id' => 'blockedNotification'],
        ['id' => 'blockTfaMode'],
        ['id' => 'blockUser'],
        ['id' => 'blockIp'],
        // admin
        [
            'id' => 'credits',
            'adminOnly' => true,
            'adminName' => 'showcredits'
        ]
    ];

    /**
     * @var array|null User who is using the Threema Gateway
     */
    protected $user = null;

    /**
     * @var array Permissions cache
     */
    protected $permissions;

    /**
     * Private constructor so nobody else can instance it.
     * Use {@link getInstance()} instead.
     *
     * @return true
     */
    protected function __construct()
    {
        // do nothing
    }

    /**
     * Prevent cloning for Singleton.
     */
    protected function __clone()
    {
        // I smash clones!
    }

    /**
     * SDK startup as a Singleton.
     *
     * @throws XenForo_Exception
     * @return ThreemaGateway_Handler_Permissions
     */
    public static function getInstance()
    {
        if (!isset(self::$instance)) {
            self::$instance = new self;
        }

        return self::$instance;
    }

    /**
     * Sets/Changes the user id of the user using the Threema Gateway. This
     * also forces a reload of the permission cache. (See {@link hasPermission()}).
     *
     * Returns true when value was changed. If false is returned the user is
     * already the visitor or the user id is already set or and the value was
     * not changed.
     *
     * @param  array|null $newUser (optional) User array
     * @return bool
     */
    public function setUserId($newUser = null)
    {
        // get user ids (or null)
        if ($this->user === null) {
            /** @var int|null $oldUserId User id of user (from class) */
            $oldUserId = null;
        } else {
            /** @var int|null $oldUserId User id of user (from class) */
            $oldUserId = $this->user['user_id'];
        }
        if ($newUser === null) {
            /** @var int|null $newUserId User id of new user (from param) */
            $newUserId = null;
        } else {
            /** @var int|null $newUserId User id of new user (from param) */
            $newUserId = $newUser['user_id'];
        }

        // check whether visitor is user
        /**
         * @var bool $userIsAlreadyVisitor Whether the new user is already the
         *           default user and the old user is the
         *           default user too.
         */
        $userIsAlreadyVisitor = $this->userIsDefault($newUserId) && $this->userIsDefault($oldUserId);
        // prevent unnecessary changes
        if ($oldUserId == $newUserId || $userIsAlreadyVisitor) {
            return false;
        }

        //change user Id
        $this->user = $newUser;
        $this->renewCache($newUserId);
        return true;
    }

    /**
     * Checks whether the user has the permission to do something.
     *
     * This uses the user e.g. set by {@link setUserId()}. If no user is set it
     * uses the current visitor/user.
     * The currently avaliable actions are: use, send, receive, fetch, lookup,
     * tfa and credits
     * If you do not specify an action an array of all possible actions is
     * returned.
     * Note that "credits" is an admin permission and is therefore only avaliable
     * to admins.
     *
     * @param  string     $action  (optional) The action you want to do
     * @param  bool       $noCache (optional) Forces the cache to reload
     * @return bool|array
     */
    public function hasPermission($action = null, $noCache = false)
    {
        /** @var int|null $userId User id of user */
        $userId = $this->userGetId(false);

        // check permission cache
        if ($noCache || !is_array($this->permissions)) {
            // (need to) renew cache
            $this->renewCache($userId);
        }

        // return permission
        if ($action) {
            if (!array_key_exists($action, $this->permissions)) {
                // invalid action
                return false;
            }
            return $this->permissions[$action];
        }

        return $this->permissions;
    }

    /**
     * Logs an action for checking it via {@link isLimited()} later.
     *
     * This uses the user e.g. set by {@link setUserId()}. If no user is set it
     * uses the current visitor/user.
     *
     * @param  string     $action  The action the user has done
     * @return bool|array
     */
    public function logAction($action)
    {
        /** @var int|null $userId User id of user */
        $userId = $this->userGetId();

        return (new ThreemaGateway_Model_ActionThrottle)->logAction($userId, $action);
    }

    /**
     * Checks whether the user has the permission to do something.
     *
     * This uses the user e.g. set by {@link setUserId()}. If no user is set it
     * uses the current visitor/user.
     *
     * @param  string     $action  The action you want to do
     * @return bool|array
     */
    public function isLimited($action)
    {
        /** @var int|null $userId User id of user */
        $userId = $this->userGetId();

        return (new ThreemaGateway_Model_ActionThrottle)->isLimited($userId, $action);
    }

    /**
     * Reload the permission cache.
     *
     * @param string $userId the ID of the user
     */
    protected function renewCache($userId)
    {
        /** @var array $permissions Temporary variable for permissions */
        $permissions = [];

        if ($this->userIsDefault($userId)) {
            /** @var XenForo_Visitor $visitor */
            $visitor = XenForo_Visitor::getInstance();

            //normal visitor, use simple methods
            foreach (self::PERMISSION_LIST as $testPerm) {
                if (!empty($testPerm['adminOnly'])) {
                    $permissions[$testPerm['id']] = $visitor->hasAdminPermission(self::PERMISSION_GROUP . '_' . $testPerm['adminName']);
                } else {
                    $permissions[$testPerm['id']] = $visitor->hasPermission(self::PERMISSION_GROUP, $testPerm['id']);
                }
            }
        } else {
            // fetch permissions (from DB) if needed
            if (!array_key_exists('permissions', $this->user)) {
                if (!array_key_exists('global_permission_cache', $this->user) || !$this->user['global_permission_cache']) {
                    // used code by XenForo_Visitor::setup()
                    // get permissions from cache
                    $perms = XenForo_Model::create('XenForo_Model_Permission')->rebuildPermissionCombinationById(
                        $this->user['permission_combination_id']
                    );
                    $this->user['permissions'] = $perms ? $perms : [];
                } else {
                    $this->user['permissions'] = XenForo_Permission::unserializePermissions($this->user['global_permission_cache']);
                }
            }

            //get permissions
            foreach (self::PERMISSION_LIST as $testPerm) {
                if (!empty($testPerm['adminOnly'])) {
                    // Getting admin permission would be extensive and admins should
                    // also only have special permissions if they are really logged in.
                    // Therefore all admin permissions are set to false
                    $permissions[$testPerm['id']] = false;
                } else {
                    $permissions[$testPerm['id']] = XenForo_Permission::hasPermission($this->user['permissions'], self::PERMISSION_GROUP, $testPerm['id']);
                }
            }
        }

        $this->permissions = $permissions;
    }

    /**
     * Returns the current user ID.
     *
     * @param  bool $visitorFallback Optional - If set to false, this does not
     *                                          fallback to the current user.
     *
     * @return int|null
     */
    protected function userGetId($visitorFallback = true)
    {
        if ($this->user !== null) {
            return $this->user['user_id'];
        }
        if ($visitorFallback) {
            return $this->getVisitorUserId();
        }

        return null;
    }

    /**
     * Checks whether a user is the default user/the current "visitor".
     *
     * @param  int|null $userId A user id or null
     * @return bool
     */
    protected function userIsDefault($userId)
    {
        return $userId === $this->getVisitorUserId() || $userId === null;
    }

    /**
     * Returns the user ID of the current visitor.
     *
     * @return int
     */
    protected function getVisitorUserId()
    {
        /** @var XenForo_Visitor $visitor */
        $visitor = XenForo_Visitor::getInstance();
        return $visitor->getUserId();
    }
}