JoryHogeveen/view-admin-as

View on GitHub
includes/class-controller.php

Summary

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

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

/**
 * View controller class. Handles all view data.
 *
 * @author  Jory Hogeveen <info@keraweb.nl>
 * @package View_Admin_As
 * @since   1.7.0
 * @version 1.8.7
 * @uses    \VAA_View_Admin_As_Base Extends class
 */
final class VAA_View_Admin_As_Controller extends VAA_View_Admin_As_Base
{
    /**
     * The single instance of the class.
     *
     * @since  1.6.0
     * @static
     * @var    \VAA_View_Admin_As_Controller
     */
    private static $_instance = null;

    /**
     * Expiration time for view data.
     *
     * @since  1.3.4  (as $metaExpiration).
     * @since  1.6.2  Moved from `VAA_View_Admin_As`.
     * @var    int
     */
    private $viewExpiration = 86400; // one day: ( 24 * 60 * 60 ).

    /**
     * VAA_View_Admin_As_Controller 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 );

        // When a user logs in or out, reset the view to default.
        $this->add_action( 'wp_login',  array( $this, 'cleanup_views' ), 10, 2 );
        $this->add_action( 'wp_login',  array( $this, 'reset_view' ), 10, 2 );
        $this->add_action( 'wp_logout', array( $this, 'reset_view' ) );

        // Not needed, the delete_user actions already remove all metadata, keep code for possible future use.
        //$this->add_action( 'remove_user_from_blog', array( $this->store, 'delete_user_meta' ) );
        //$this->add_action( 'wpmu_delete_user', array( $this->store, 'delete_user_meta' ) );
        //$this->add_action( 'wp_delete_user', array( $this->store, 'delete_user_meta' ) );

        /**
         * Change expiration time for view meta.
         *
         * @example  You can set it to 1 to always clear everything after login.
         * @example  0 will be overwritten!
         *
         * @since  1.6.2
         * @param  int  $viewExpiration  86400 (1 day in seconds).
         * @return int
         */
        $this->viewExpiration = absint( apply_filters( 'view_admin_as_view_expiration', $this->viewExpiration ) );
    }

    /**
     * Initializes after VAA is enabled.
     *
     * @since   1.6.0
     * @access  public
     * @return  void
     */
    public function init() {

        // Reset hook.
        $this->add_filter( 'view_admin_as_handle_ajax_reset', array( $this, 'reset_view' ) );

        // Validation & update hooks for visitor view.
        $this->add_filter( 'view_admin_as_validate_view_data_visitor', '__return_true' );
        $this->add_filter( 'view_admin_as_update_view_visitor', array( $this, 'filter_update_view' ), 10, 3 );
        $this->add_filter( 'vaa_view_admin_as_view_titles', array( $this, 'filter_default_view_titles' ), 1, 2 );

        // Get the current view.
        $this->store->set_view( $this->get_view() );

        // Short circuit needed for visitor view (BEFORE the current user is set).
        if ( VAA_API::is_ajax_request( 'view_admin_as' ) ) {
            $this->ajax_view_admin_as();
        } else {
            // Admin selector ajax return (fallback).
            $this->add_action( 'wp_ajax_view_admin_as', array( $this, 'ajax_view_admin_as' ) );
            //$this->add_action( 'wp_ajax_nopriv_view_admin_as', array( $this, 'ajax_view_admin_as' ) );
        }

        /**
         * Reset view to default if something goes wrong.
         *
         * @since    0.1.0
         * @since    1.2.0  Only check for key
         * @example  http://www.your.domain/wp-admin/?reset-view
         */
        if ( VAA_API::is_request( 'reset-view', 'get' ) ) {
            $this->reset_view();
        }
        /**
         * Clear all user views.
         *
         * @since    1.3.4
         * @example  http://www.your.domain/wp-admin/?reset-all-views
         */
        if ( VAA_API::is_request( 'reset-all-views', 'get' ) ) {
            $this->reset_all_views();
        }
    }

    /**
     * AJAX listener.
     * Gets the AJAX input. If it is valid: pass it to the handler.
     *
     * @since   0.1.0
     * @since   1.3.0  Added caps handler.
     * @since   1.4.0  Added module handler.
     * @since   1.5.0  Validate a nonce.
     *                 Added global and user setting handler.
     * @since   1.6.0  Moved from `VAA_View_Admin_As`.
     * @since   1.6.2  Added visitor view handler + JSON view data.
     * @access  public
     * @return  void
     */
    public function ajax_view_admin_as() {

        $data = VAA_API::get_ajax_request( $this->store->get_nonce(), 'view_admin_as' );
        if ( ! $data ) {
            wp_send_json_error( __( 'Cheatin uh?', VIEW_ADMIN_AS_DOMAIN ) );
            die();
        }

        define( 'VAA_DOING_AJAX', true );

        $data = $this->validate_data_keys( $data );

        // Stop selecting the same view!
        if ( $this->is_current_view( $data ) ) {
            wp_send_json_error(
                array(
                    'type' => 'message',
                    'text' => esc_html__( 'This view is already selected!', VIEW_ADMIN_AS_DOMAIN ),
                )
            );
        }

        $success = false;
        if ( ! empty( $data ) ) {
            $success = $this->update( $data );
        }

        if ( true === $success ) {
            wp_send_json_success( $success ); // ahw yeah.
            die();
        } elseif ( $success ) {
            wp_send_json( $success );
            die();
        }

        wp_send_json_error(
            array(
                'type' => 'error',
                'text' => esc_html__( 'Something went wrong, please try again.', VIEW_ADMIN_AS_DOMAIN ),
            )
        );
        die();
    }

    /**
     * Applies the data input to update views and settings.
     *
     * @since   1.7.0
     * @since   1.8.3  Renamed from `ajax_handler` + made public.
     * @param   array  $data  Post data.
     * @return  array|bool
     */
    public function update( $data ) {
        $success    = false;
        $view_types = array();
        $data       = $this->validate_data_keys( $data );

        /**
         * Update data return filters.
         *
         * @see     `view_admin_as_update_view_{$key}`
         * @see     `view_admin_as_handle_ajax_{$key}`
         *
         * @since   1.7.0
         * @param   null    $null    Null.
         * @param   mixed   $value   View data value.
         * @param   string  $key     View data key.
         * @return  bool|array {
         *     In case of array. Uses wp_json_return() when AJAX is used.
         *     @type  bool   $success  Send JSON success or error?
         *     @type  array  $data {
         *         Optional extra data to send with the JSON return.
         *         In case of a view the page normally refreshes.
         *         @type  string  $redirect  (URL) Redirect the user? (Only works on success).
         *         @type  string  $display   Options: `notice`   A notice type in the admin bar.
         *                                            `popup`    A popup/overlay with content.
         *         @type  string  $type      Options: `success`  Ureka! (green)      - Default when $success is true.
         *                                            `error`    Send an error (red) - Default when $success is false.
         *                                            `message`  Just a message (blue).
         *                                            `warning`  Send a warning (orange).
         *         @type  string  $text      The text to show.
         *         @type  array   $list      Show multiple messages (Popup only).
         *         @type  string  $textarea  Textarea content (Popup only).
         *     }
         * }
         */
        foreach ( $data as $key => $value ) {
            if ( $this->is_view_type( $key ) ) {
                $view_types[] = $key;

                $success = apply_filters( 'view_admin_as_update_view_' . $key, null, $value, $key );
            } else {
                // @todo 1.9 Rename this to `view_admin_as_update_{$key}`
                $success = apply_filters( 'view_admin_as_handle_ajax_' . $key, null, $value, $key );
            }
            if ( true !== $success ) {
                break;
            }
        }

        if ( empty( $view_types ) ) {
            return $success;
        }

        // Update the view on success.
        if ( true === $success || ( isset( $success['success'] ) && true === $success['success'] ) ) {

            // Remove view type keys that are not in the new view anymore.
            $view = $this->store->get_view();
            $view = array_intersect_key( $view, array_flip( $view_types ) );
            $this->store->set_view( $view );

            $this->update_view();
        }

        return $success;
    }

    /**
     * Update regular view types.
     *
     * @since   1.7.0
     * @param   null    $null  Null.
     * @param   mixed   $data  The view data.
     * @param   string  $type  The view type.
     * @return  bool|array
     */
    public function filter_update_view( $null, $data, $type ) {
        $success = $null;
        if ( ! empty( $data ) && ! empty( $type ) ) {
            $this->store->set_view( $data, $type, true );
            $success = true;
        }
        if ( $success && 'visitor' === $type && VAA_API::is_admin() ) {
            $success = array(
                'success' => true,
                'data'    => array(
                    'redirect' => esc_url( home_url() ),
                ),
            );
        }
        return $success;
    }

    /**
     * Update the view titles for default views like `visitor` if selected.
     *
     * @since   1.8.7
     * @access  public
     * @param   array  $titles  The current title(s).
     * @param   array  $view    The view data.
     * @return  array
     */
    public function filter_default_view_titles( $titles, $view ) {
        if ( isset( $view['visitor'] ) ) {
            $titles[] = __( 'Site visitor', VIEW_ADMIN_AS_DOMAIN );
        }
        return $titles;
    }

    /**
     * Check if the provided data is the same as the current view.
     *
     * @since   1.7.0
     * @since   1.7.2  Data options: `null` for active view & `false` for only/single view.
     * @param   mixed  $data  The view data to compare with.
     * @param   bool   $type  Only compare a single view type instead of all view data?
     *                        If set, the data value should be the single view type data or `null`.
     *                        If data is `null` then it will return true if that view type is active.
     *                        If data is `false` then it will return true if this is the only active view type.
     * @return  bool
     */
    public function is_current_view( $data, $type = null ) {
        if ( ! empty( $type ) ) {
            $current = $this->store->get_view( $type );
            if ( ! $current ) {
                return false;
            }
            if ( is_array( $data ) ) {
                return VAA_API::array_equal( $data, $current );
            }
            if ( null === $data ) {
                return true;
            }
            if ( false === $data ) {
                return ( 1 === count( $this->store->get_view() ) );
            }
            return ( (string) $data === (string) $current );
        }
        return VAA_API::array_equal( $data, $this->store->get_view() );
    }

    /**
     * Is it a view type?
     *
     * @since   1.7.0
     * @param   string  $type  View type name to check
     * @return  bool
     */
    public function is_view_type( $type ) {
        return ( in_array( $type, $this->get_view_types(), true ) );
    }

    /**
     * Get the available view type keys.
     *
     * @since   1.7.0
     * @access  public
     * @return  string[]
     */
    public function get_view_types() {
        static $view_types;
        if ( ! is_null( $view_types ) ) return $view_types;

        $view_types   = array_keys( (array) view_admin_as()->get_view_types() );
        $view_types[] = 'visitor';

        /**
         * Add basic view types for automated use in JS and through VAA.
         *
         * - Menu items require the class vaa-{TYPE}-item (through the add_node() meta key).
         * - Menu items require the href attribute (the node needs to be an <a> element).
         * @see \VAA_View_Admin_As_Form::do_view_title()
         *
         * @deprecated  1.8.0
         *
         * @since  1.6.2
         * @param  array  $array  Empty array.
         * @return array  An array of strings (view types).
         */
        $dep_view_types = apply_filters( 'view_admin_as_view_types', array() );
        if ( $dep_view_types ) {

            /** @see https://developer.wordpress.org/reference/functions/apply_filters_deprecated/ */
            if ( function_exists( '_deprecated_hook' ) ) {
                _deprecated_hook( 'view_admin_as_view_types', 1.8, 'view_admin_as()->register_view_type()' );
            }

            $view_types = array_unique(
                array_merge(
                    array_filter( $dep_view_types, 'is_string' ),
                    $view_types
                )
            );
        }

        return $view_types;
    }

    /**
     * Get current view for the current session.
     *
     * @since   1.3.4
     * @since   1.5.0   Single mode.
     * @since   1.6.0   Moved from `VAA_View_Admin_As`.
     * @since   1.7.0   Private method. Use store.
     * @access  public
     * @return  array
     */
    public function get_view() {

        $view_mode = $this->store->get_userSettings( 'view_mode' );

        if ( 'browse' === $view_mode ) {

            // Static actions.
            $request = VAA_API::get_normal_request( $this->store->get_nonce(), 'view_admin_as', 'get' );
            if ( $request ) {
                $this->store->set_view( $this->validate_view_data( $request ) );
                $this->update_view();
                // Trigger page refresh.
                // @todo fix WP referrer/nonce checks and allow switching on any page without ajax. See VAA_API.
                if ( is_network_admin() ) {
                    wp_safe_redirect( network_admin_url(), 302, VIEW_ADMIN_AS_DOMAIN );
                } else {
                    wp_safe_redirect( admin_url(), 302, VIEW_ADMIN_AS_DOMAIN );
                }
                die();
            }

            // Browse mode.
            $meta    = $this->store->get_userMeta( 'views' );
            $session = $this->store->get_curUserSession();
            if ( isset( $meta[ $session ]['view'] ) ) {
                return $this->validate_view_data( $meta[ $session ]['view'] );
            }

        } elseif ( 'single' === $view_mode ) {

            // Single mode.
            $request = VAA_API::get_normal_request( $this->store->get_nonce(), 'view_admin_as' );
            if ( $request ) {
                return $this->validate_view_data( $request );
            }
        }

        return array();
    }

    /**
     * Update view for the current session.
     *
     * @since   1.3.4
     * @since   1.6.0   Moved from `VAA_View_Admin_As`.
     * @since   1.8.3   Made public.
     * @since   1.8.7   Add action.
     * @access  public
     *
     * @return  bool
     */
    public function update_view() {
        $return    = false;
        $view_data = $this->validate_view_data( $this->store->get_view() );

        if ( $view_data ) {
            $meta    = $this->store->get_userMeta( 'views' );
            $session = $this->store->get_curUserSession();
            // Make sure it is an array (no array means no valid data so we can safely clear it).
            if ( ! is_array( $meta ) ) {
                $meta = array();
            }
            // Add the new view metadata and expiration date.
            $meta[ $session ] = array(
                'view'   => $view_data,
                'expire' => ( time() + (int) $this->viewExpiration ),
            );
            // Update metadata (returns: true on success, false on failure).
            $return = $this->store->update_userMeta( $meta, 'views', true );

            /**
             * Fires after a view has been updated for a user.
             *
             * @since 1.8.7
             * @param \WP_User $user       User object.
             * @param array    $view_data  View data.
             * @param string   $session    User session.
             */
            $this->do_action( 'vaa_view_admin_as_update_view', $this->store->get_curUser(), $view_data, $session );
        }

        return $return;
    }

    /**
     * Reset view to default.
     * This function is also attached to the wp_login and wp_logout hook.
     *
     * @since   1.3.4
     * @since   1.6.0   Moved from `VAA_View_Admin_As`.
     * @since   1.8.7   Add action.
     * @access  public
     * @link    https://codex.wordpress.org/Plugin_API/Action_Reference/wp_login
     *
     * @param   string    $user_login  (not used) String provided by the wp_login hook.
     * @param   \WP_User  $user        User object provided by the wp_login hook.
     * @return  bool
     */
    public function reset_view( $user_login = null, $user = null ) {
        $return = true;

        if ( null === $user ) {
            // Function is not triggered by the wp_login action hook.
            $user = $this->store->get_curUser();
        }
        if ( ! empty( $user->ID ) ) {
            // Do not use the store as it currently doesn't support a different user ID.
            $meta    = get_user_meta( $user->ID, $this->store->get_userMetaKey(), true );
            $session = $this->store->get_curUserSession();
            // Check if this user session has metadata.
            if ( isset( $meta['views'][ $session ] ) ) {
                // Store old view data for hooks.
                $old_view_data = VAA_API::get_array_data( $meta['views'][ $session ], 'view' );
                // Remove metadata from this session.
                unset( $meta['views'][ $session ] );
                // Update current metadata if it is the current user.
                if ( $this->store->get_curUser() && (int) $this->store->get_curUser()->ID === (int) $user->ID ) {
                    $this->store->set_userMeta( $meta );
                }
                // Update db metadata (returns: true on success, false on failure).
                $return = update_user_meta( $user->ID, $this->store->get_userMetaKey(), $meta );

                /**
                 * Fires after a view has been reset for a user.
                 *
                 * @since 1.8.7
                 * @param \WP_User $user           User object.
                 * @param array    $old_view_data  Removed view data.
                 * @param string   $session        User session.
                 */
                $this->do_action( 'vaa_view_admin_as_reset_view', $user, $old_view_data, $session );
            }
        }

        return $return;
    }

    /**
     * Delete all expired View Admin As metadata for this user.
     * This function is also attached to the wp_login hook.
     *
     * @since   1.3.4
     * @since   1.6.0   Moved from `VAA_View_Admin_As`.
     * @since   1.8.7   Add action.
     * @access  public
     * @link    https://codex.wordpress.org/Plugin_API/Action_Reference/wp_login
     *
     * @param   string    $user_login  (not used) String provided by the wp_login hook.
     * @param   \WP_User  $user        User object provided by the wp_login hook.
     * @return  bool
     */
    public function cleanup_views( $user_login = null, $user = null ) {
        $return = true;

        if ( null === $user ) {
            // Function is not triggered by the wp_login action hook.
            $user = $this->store->get_curUser();
        }
        if ( ! empty( $user->ID ) ) {
            // Do not use the store as it currently doesn't support a different user ID.
            $meta = get_user_meta( $user->ID, $this->store->get_userMetaKey(), true );
            // If meta exists, loop it.
            if ( isset( $meta['views'] ) ) {
                // Store old views for hooks.
                $old_views = $meta['views'];

                foreach ( (array) $meta['views'] as $key => $value ) {
                    // Check expiration date: if it doesn't exist or is in the past, remove it.
                    if ( ! isset( $meta['views'][ $key ]['expire'] ) || time() > (int) $meta['views'][ $key ]['expire'] ) {
                        unset( $meta['views'][ $key ] );
                    }
                }
                $views = $meta['views'];

                // Update current metadata if it is the current user.
                if ( $this->store->get_curUser() && (int) $this->store->get_curUser()->ID === (int) $user->ID ) {
                    $this->store->set_userMeta( $meta );
                }
                // Update db metadata (returns: true on success, false on failure).
                $return = update_user_meta( $user->ID, $this->store->get_userMetaKey(), $meta );

                /**
                 * Fires after old views have been cleaned for a user.
                 *
                 * @since 1.8.7
                 * @param  \WP_User  $user       User object.
                 * @param  array     $views      Current views.
                 * @param  array     $old_views  Old views.
                 */
                $this->do_action( 'vaa_view_admin_as_cleanup_views', $user, $views, $old_views );
            }
        }

        return $return;
    }

    /**
     * Reset all View Admin As metadata for this user.
     *
     * @since   1.3.4
     * @since   1.6.0   Moved from `VAA_View_Admin_As`.
     * @since   1.8.7   Add action.
     * @access  public
     * @link    https://codex.wordpress.org/Plugin_API/Action_Reference/wp_login
     *
     * @param   string    $user_login  (not used) String provided by the wp_login hook.
     * @param   \WP_User  $user        User object provided by the wp_login hook.
     * @return  bool
     */
    public function reset_all_views( $user_login = null, $user = null ) {
        $return = true;

        if ( null === $user ) {
            // Function is not triggered by the wp_login action hook.
            $user = $this->store->get_curUser();
        }
        if ( ! empty( $user->ID ) ) {
            // Do not use the store as it currently doesn't support a different user ID.
            $meta = get_user_meta( $user->ID, $this->store->get_userMetaKey(), true );
            // If meta exists, reset it.
            if ( isset( $meta['views'] ) ) {
                // Store old views for hooks.
                $old_views     = $meta['views'];
                $meta['views'] = array();
                // Update current metadata if it is the current user.
                if ( $this->store->get_curUser() && (int) $this->store->get_curUser()->ID === (int) $user->ID ) {
                    $this->store->set_userMeta( $meta );
                }

                // Update db metadata (returns: true on success, false on failure).
                $return = update_user_meta( $user->ID, $this->store->get_userMetaKey(), $meta );

                /**
                 * Fires after all views have been reset for a user.
                 *
                 * @since  1.8.7
                 * @param  \WP_User  $user       User object.
                 * @param  array     $old_views  Old views.
                 */
                $this->do_action( 'vaa_view_admin_as_reset_all_views', $user, $old_views );
            }
        }

        return $return;
    }

    /**
     * Remove all unsupported keys.
     *
     * @since   1.7.0
     * @param   array  $data  Input data.
     * @return  array
     */
    public function validate_data_keys( $data ) {

        if ( ! is_array( $data ) || empty( $data ) ) {
            return array();
        }

        $allowed_keys = array_unique(
            array_merge(
                // View types.
                $this->get_view_types(),
                // Module keys.
                array_keys( $this->vaa->get_modules() ),
                // VAA core keys.
                array( 'setting', 'user_setting', 'reset' )
            )
        );

        $data = array_intersect_key( $data, array_flip( $allowed_keys ) );

        return $data;
    }

    /**
     * Validate data before changing the view.
     *
     * @since   1.5.0
     * @since   1.6.0  Moved from `VAA_View_Admin_As`.
     * @since   1.7.0  Changed name to `validate_view_data` from `validate_view_as_data`
     * @access  public
     *
     * @param   array  $data  Unvalidated data.
     * @return  array  $data  Validated data.
     */
    public function validate_view_data( $data ) {

        if ( ! is_array( $data ) || empty( $data ) ) {
            return array();
        }

        // Only leave keys that are view types.
        $data = array_intersect_key( $data, array_flip( $this->get_view_types() ) );

        // We only want allowed keys and data, otherwise it's not added through this plugin.
        foreach ( $data as $key => $value ) {

            /**
             * Validate the data.
             * Hook is required!
             *
             * @since  1.6.2
             * @since  1.7.0   Added third `$key` parameter
             * @param  null    $null   Ensures a validation filter is required.
             * @param  mixed   $value  Unvalidated view data.
             * @param  string  $key    The data key.
             * @return mixed   validated view data.
             */
            $data[ $key ] = apply_filters( 'view_admin_as_validate_view_data_' . $key, null, $value, $key );

            if ( null === $data[ $key ] ) {
                unset( $data[ $key ] );
            }
        }
        return $data;
    }

    /**
     * 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_Controller  $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_Controller.