JoryHogeveen/view-admin-as

View on GitHub
includes/class-view.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php
/**
 * View Admin As - Class View
 *
 * @author  Jory Hogeveen <info@keraweb.nl>
 * @package View_Admin_As
 */

if ( ! defined( 'VIEW_ADMIN_AS_DIR' ) ) {
    die();
}

/**
 * View handler class.
 *
 * @author  Jory Hogeveen <info@keraweb.nl>
 * @package View_Admin_As
 * @since   1.6.0
 * @since   1.7.0  Class got split up: data handling/updating is now in VAA_View_Admin_As_Controller.
 * @version 1.8.7
 * @uses    \VAA_View_Admin_As_Base Extends class
 */
final class VAA_View_Admin_As_View extends VAA_View_Admin_As_Base
{
    /**
     * The single instance of the class.
     *
     * @since  1.6.0
     * @static
     * @var    \VAA_View_Admin_As_View
     */
    private static $_instance = null;

    /**
     * Is the current user modified?
     *
     * @since  1.7.2
     * @var    bool
     */
    private $is_user_modified = false;

    /**
     * VAA_View_Admin_As_View constructor.
     *
     * @since   1.6.0
     * @since   1.6.1  `$vaa` param.
     * @access  protected
     * @param   \VAA_View_Admin_As  $vaa  The main VAA object.
     */
    protected function __construct( $vaa ) {
        self::$_instance = $this;
        parent::__construct( $vaa );
    }

    /**
     * Initializes after VAA is enabled.
     *
     * @since   1.6.0
     * @access  public
     * @return  void
     */
    public function init() {
        if ( VAA_API::is_view_active() ) {
            $this->do_view();
        }
    }

    /**
     * Apply view data.
     *
     * @since   1.6.3    Put logic in it's own function.
     * @access  private
     * @return  void
     */
    private function do_view() {

        // @since  1.6.4  Set the current user as the selected user by default.
        $this->store->set_selectedUser( $this->store->get_curUser() );
        $this->store->set_selectedCaps( $this->store->get_curUser()->allcaps );

        /**
         * VISITOR.
         * Current user object views (switches current user).
         *
         * @since  1.6.2  Visitor view.
         */
        if ( $this->store->get_view( 'visitor' ) ) {

            /**
             * Change current user object so changes can be made on various screen settings.
             * wp_set_current_user() returns the new user object.
             *
             * If it is a visitor view it will convert the false return from 'user' to 0.
             */
            $this->store->set_selectedUser( wp_set_current_user( 0 ) );

            // @since  1.6.2  Set the caps for this view (user view).
            if ( isset( $this->store->get_selectedUser()->allcaps ) ) {
                $this->store->set_selectedCaps( $this->store->get_selectedUser()->allcaps );
            }
        }

        /**
         * View data is set, apply the view.
         * This hook can be used by other modules to enable a view.
         *
         * Temporary modifications to the current user are set on priority 99.
         * This functionality has a separate action: `vaa_view_admin_as_modify_current_user`.
         *
         * @hooked
         * 2:   user
         * 5:   role
         * 8:   caps
         * 10:  locale (Languages)
         * 10:  group (Groups)
         * 10:  rua_level (Restrict User Access)
         *
         * @since  1.6.3
         * @param  array
         */
        $this->do_action( 'vaa_view_admin_as_do_view', $this->store->get_view() );

        /**
         * Force own locale on view.
         *
         * @since  1.6.1
         * @since  1.7.5  Add filter `view_admin_as_freeze_locale`.
         *
         * @param  bool  $freeze_locale  The user setting.
         * @return bool
         */
        $freeze_locale = apply_filters( 'view_admin_as_freeze_locale', $this->store->get_userSettings( 'freeze_locale' ) );
        if ( $freeze_locale && (int) $this->store->get_curUser()->ID !== (int) $this->store->get_selectedUser()->ID ) {
            $this->add_action( 'after_setup_theme', array( $this, 'freeze_locale' ), 0 );
        }
    }

    /**
     * Adds the actions and filters to modify the current user object.
     * Can only be run once.
     *
     * @since   1.6.3
     * @access  public
     * @return  void
     */
    public function init_user_modifications() {
        static $done;
        if ( $done ) return;

        $this->is_user_modified = true;

        $this->add_action( 'vaa_view_admin_as_do_view', array( $this, 'modify_user' ), 99 );

        /**
         * Make sure the $current_user view data isn't overwritten again by switch_blog functions.
         * @see    This filter is documented in wp-includes/ms-blogs.php
         * @since  1.6.3
         */
        $this->add_action( 'switch_blog', array( $this, 'modify_user' ) );

        /**
         * Prevent some meta updates for the current user while in modification to the current user are active.
         * @since  1.6.3
         */
        $this->add_filter( 'update_user_metadata', array( $this, 'filter_prevent_update_user_metadata' ), 999999999, 3 );

        /**
         * Get capabilities and user level from current user view object instead of database.
         * @since  1.6.4
         */
        $this->add_filter( 'get_user_metadata', array( $this, 'filter_overrule_get_user_metadata' ), 999999999, 3 );

        /**
         * The priority value of the VAA `user_has_cap` filter.
         * Runs as first by default.
         *
         * @since   1.7.2
         * @param   int  $priority  Default: -999999999.
         * @return  int
         */
        $priority = (int) apply_filters( 'view_admin_as_user_has_cap_priority', -999999999 );

        /**
         * Change the capabilities.
         *
         * @since  1.7.1
         * @since  1.7.2  Changed priority to set is at the beginning instead of as last
         *                to allow other plugins to filter based on the modified user.
         */
        $this->add_filter( 'user_has_cap', array( $this, 'filter_user_has_cap' ), $priority, 4 );

        /**
         * Map the capabilities (map_meta_cap is used for compatibility with network admins).
         * Filter as last to check other plugin changes as well.
         *
         * @since  0.1.0
         */
        $this->add_filter( 'map_meta_cap', array( $this, 'filter_map_meta_cap' ), 999999999, 4 );

        /**
         * Disable super admin status for the current user.
         * @since  1.7.3
         * @since  1.8.0  Check for multisite.
         */
        if (
            is_multisite()
            && ! is_network_admin()
            && is_super_admin( $this->store->get_selectedUser()->ID )
            && $this->store->get_userSettings( 'disable_super_admin' )
        ) {
            $this->disable_super_admin();
        }

        $done = true;
    }

    /**
     * Update the current user's WP_User instance with the current view capabilities.
     *
     * @since   1.6.3
     * @access  public
     * @return  void
     */
    public function modify_user() {

        // Can be the current or selected WP_User object (depending on the user view).
        $user = $this->store->get_selectedUser();

        /**
         * Allow other modules to hook after the initial changes to the current user.
         *
         * @since  1.6.3
         * @since  1.6.4  Renamed from `vaa_view_admin_as_modify_current_user`.
         * @param  \WP_User  $user        The modified user object.
         */
        $this->do_action( 'vaa_view_admin_as_modify_user', $user );
    }

    /**
     * Prevent some updates to the current user like roles and capabilities.
     * to prevent problems when making changes within a view.
     *
     * IMPORTANT! This filter should ONLY be used when a view is selected!
     *
     * @since   1.6.3
     * @access  public
     * @see     init_user_modifications()
     *
     * @see     'update_user_metadata' filter
     * @link    https://codex.wordpress.org/Plugin_API/Filter_Reference/update_(meta_type)_metadata
     * @link    http://hookr.io/filters/update_user_metadata/
     *
     * @global  \wpdb   $wpdb
     * @param   null    $null       Whether to allow updating metadata for the given type.
     * @param   int     $object_id  Object ID.
     * @param   string  $meta_key   Meta key.
     * @return  mixed
     */
    public function filter_prevent_update_user_metadata( $null, $object_id, $meta_key ) {
        global $wpdb;
        $user = $this->store->get_selectedUser();

        // Check if the object being updated is the current user.
        if ( (int) $user->ID === (int) $object_id ) {

            // Capabilities meta key check.
            if ( empty( $user->cap_key ) ) {
                $user->cap_key = $wpdb->get_blog_prefix() . 'capabilities';
            }

            // Do not update the current user capabilities or user level while in a view.
            if ( in_array( $meta_key, array(
                $user->cap_key,
                $wpdb->get_blog_prefix() . 'capabilities',
                $wpdb->get_blog_prefix() . 'user_level',
            ), true ) ) {
                return false;
            }
        }
        return $null;
    }

    /**
     * Return view roles when getting the current user data to prevent reloading current user data within a view.
     *
     * IMPORTANT! This filter should ONLY be used when a view is selected!
     *
     * @since   1.6.4
     * @access  public
     * @see     init_user_modifications()
     *
     * @see     'get_user_metadata' filter
     * @link    https://codex.wordpress.org/Plugin_API/Filter_Reference/get_(meta_type)_metadata
     *
     * @global  \wpdb   $wpdb
     * @param   null    $null       The value update_metadata() should return.
     * @param   int     $object_id  Object ID.
     * @param   string  $meta_key   Meta key.
     * @return  mixed
     */
    public function filter_overrule_get_user_metadata( $null, $object_id, $meta_key ) {
        global $wpdb;
        $user = $this->store->get_selectedUser();

        // Check if the object being updated is the current user.
        if ( (int) $user->ID === (int) $object_id ) {

            // Return the current user capabilities or user level while in a view.
            // Always return an array to fix $single usage.

            // Current user cap key should be equal to the meta_key for capabilities.
            if ( ! empty( $user->cap_key ) && $meta_key === $user->cap_key ) {
                return array( $user->caps );
            }
            // Fallback if cap_key doesn't exists.
            if ( $meta_key === $wpdb->get_blog_prefix() . 'capabilities' ) {
                return array( $user->caps );
            }
            if ( $meta_key === $wpdb->get_blog_prefix() . 'user_level' ) {
                if ( ! isset( $user->user_level ) ) {
                    // Make sure the key exists. Result will be filtered in `filter_prevent_update_user_metadata()`.
                    $user->update_user_level_from_caps();
                }
                return array( $user->user_level );
            }
        }
        return $null;
    }

    /**
     * Change capabilities when the user has selected a view.
     * If the capability isn't in the chosen view, then make the value for this capability empty and add "do_not_allow".
     *
     * @since   0.1.0
     * @since   1.5.0   Renamed from `change_caps()`.
     * @since   1.6.0   Moved from `VAA_View_Admin_As`.
     * @since   1.6.2   Use logic from `self::current_view_can()`.
     * @since   1.6.3   Prefix function name with `filter_`.
     * @since   1.7.2   Use the `user_has_cap` filter for compatibility enhancements.
     * @access  public
     *
     * @param   array   $caps     The actual (mapped) cap names, if the caps are not mapped this returns the requested cap.
     * @param   string  $cap      The capability that was requested.
     * @param   int     $user_id  The ID of the user.
     * @param   array   $args     Adds the context to the cap. Typically the object ID (not used).
     * @return  array   $caps
     */
    public function filter_map_meta_cap( $caps, $cap, $user_id, $args = array() ) {

        if ( (int) $this->store->get_selectedUser()->ID !== (int) $user_id ) {
            return $caps;
        }

        $filter_caps = (array) $this->store->get_selectedCaps();

        if ( ! $this->store->get_view( 'caps' ) ) {
            /**
             * Apply user_has_cap filters to make sure we are compatible with modifications from other plugins.
             *
             * Issues found:
             * - Restrict User Access - Overwrites our filtered capabilities. (fixed since RUA 0.15.x).
             * - Groups - Overwrites our filtered capabilities. (fixed in Groups module).
             *
             * @since  1.7.2
             * @see    \WP_User::has_cap()
             */
            $filter_caps = apply_filters(
                'user_has_cap',
                $filter_caps,
                $caps,
                // Replicate arguments for `user_has_cap`.
                array_merge( array( $cap, $user_id ), (array) $args ),
                $this->store->get_selectedUser()
            );
        }

        /**
         * Everyone is allowed to exist.
         * @since  1.8.3
         * @see    \WP_User::has_cap()
         * @link   https://wordpress.org/support/topic/compatibility-with-view-admin-as-2/
         */
        $filter_caps['exist'] = true;

        foreach ( (array) $caps as $actual_cap ) {
            if ( ! $this->current_view_can( $actual_cap, $filter_caps ) ) {
                // Regular users. Assuming this capability never exists..
                $caps['vaa_do_not_allow'] = 'vaa_do_not_allow';
                // Network admins.
                $caps['do_not_allow'] = 'do_not_allow';
            }
        }

        return $caps;
    }

    /**
     * Overwrite the user's capabilities.
     *
     * @since   1.6.3
     * @access  public
     *
     * @param   array     $allcaps  All the capabilities of the user.
     * @param   array     $caps     Actual capabilities for meta capability.
     * @param   array     $args     [0] Requested capability.
     *                              [1] User ID.
     *                              [2] Associated object ID.
     * @param   \WP_User  $user     (WP 3.7+) The user object.
     * @return  array
     */
    public function filter_user_has_cap( $allcaps, $caps, $args, $user = null ) {
        $user_id = ( $user ) ? $user->ID : $args[1];
        if ( is_numeric( $user_id ) && (int) $user_id === (int) $this->store->get_selectedUser()->ID ) {
            return (array) $this->store->get_selectedCaps();
        }
        return $allcaps;
    }

    /**
     * Remove the current user from the list of super admins.
     * This sets/changes the global $super_admins variable which overwrites the site option.
     *
     * @since   1.7.3
     * @access  public
     * @see     grant_super_admin()  >> wp-includes/capabilities.php
     * @see     revoke_super_admin() >> wp-includes/capabilities.php
     * @see     get_super_admins()   >> wp-includes/capabilities.php
     * @see     is_super_admin()     >> wp-includes/capabilities.php
     * @link    https://developer.wordpress.org/reference/functions/is_super_admin/
     *
     * @global  array  $super_admins
     * @param   \WP_User|int|string  $user   (optional) A user to remove. Both a user object or a user field is accepted.
     * @param   string               $field  (optional) A user field key to get the user data by.
     */
    public function disable_super_admin( $user = null, $field = 'id' ) {
        global $super_admins;

        if ( ! isset( $super_admins ) ) {
            $super_admins = get_super_admins();
        }

        $user = ( null !== $user ) ? $user : $this->store->get_selectedUser();
        if ( ! $user instanceof WP_User ) {
            $user = get_user_by( $field, $user );
        }

        // Remove current user from the super admins array.
        // Effectively disables functions grant_super_admin() and revoke_super_admin().
        if ( ! empty( $user->user_login ) && is_array( $super_admins ) ) {
            $key = array_search( $user->user_login, $super_admins, true );
            if ( false !== $key ) {
                unset( $super_admins[ $key ] );
                $GLOBALS['super_admins'] = $super_admins;
            }
        }
    }

    /**
     * Similar function to current_user_can().
     *
     * @since   1.6.2
     * @since   1.8.0  Check for non-scalar types being passed as first parameter.
     * @access  public
     *
     * @param   string  $cap   The capability.
     * @param   array   $caps  (optional) Capabilities to compare to.
     *                         Defaults to the selected caps for the current view.
     * @return  bool
     */
    public function current_view_can( $cap, $caps = array() ) {

        if ( empty( $caps ) ) {
            $caps = $this->store->get_selectedCaps();
        }

        if ( ! is_scalar( $cap ) ) {
            $cap = (array) $cap;
            foreach ( $cap as $capability ) {
                if ( ! $this->current_view_can( $capability, $caps ) ) {
                    return false;
                }
            }
            return true;
        }

        $cap = (string) $cap;

        if (
            is_array( $caps )
            && ! empty( $caps[ $cap ] )
            && 'do_not_allow' !== $cap
            && 'do_not_allow' !== $caps[ $cap ]
        ) {
            return true;
        }
        return false;
    }

    /**
     * Is the current user modified?
     *
     * @since   1.7.2
     * @access  public
     * @return  bool
     */
    public function is_user_modified() {
        return (bool) $this->is_user_modified;
    }

    /**
     * Set the locale for the current view.
     *
     * @since   1.6.1
     * @access  public
     * @return  bool  Will return false when used with older WP versions.
     */
    public function freeze_locale() {
        if ( function_exists( 'get_user_locale' ) && function_exists( 'switch_to_locale' ) ) {
            $locale = get_user_locale( $this->store->get_curUser()->ID );
            if ( get_locale() !== $locale ) {
                switch_to_locale( $locale );
            }
            return true;
        }
        return false;
    }

    /**
     * Main Instance.
     *
     * Ensures only one instance of this class is loaded or can be loaded.
     *
     * @since   1.6.0
     * @access  public
     * @static
     * @param   \VAA_View_Admin_As  $caller  The referrer class.
     * @return  \VAA_View_Admin_As_View  $this
     */
    public static function get_instance( $caller = null ) {
        if ( is_null( self::$_instance ) ) {
            self::$_instance = new self( $caller );
        }
        return self::$_instance;
    }

} // End class VAA_View_Admin_As_View.