* @license GPLv3 * @copyright 2020 by Joachim Jensen */ defined('ABSPATH') || exit; final class RUA_Level_Manager { /** * Metadata * * @var WPCAObjectManager */ private $metadata; public function __construct() { $this->add_actions(); $this->add_filters(); add_shortcode('restrict', array($this,'shortcode_restrict')); add_shortcode('restrict-inner', array($this,'shortcode_restrict')); } /** * Add callbacks to actions queue * * @since 0.5 */ protected function add_actions() { add_action( 'template_redirect', array($this,'authorize_access') ); add_action( 'init', array($this,'create_restrict_type'), 99 ); add_action( 'user_register', array($this,'registered_add_level') ); } /** * Add callbacks to filters queue * * @since 0.5 */ protected function add_filters() { if (!is_admin()) { add_filter( 'show_admin_bar', array($this,'show_admin_toolbar'), 99 ); } //hook early, other plugins might add dynamic caps later //fixes problem with WooCommerce Orders add_filter( 'user_has_cap', array($this,'user_level_has_cap'), 9, 4 ); } /** * Maybe hide admin toolbar for Users * * @since 1.1 * @return bool */ public function show_admin_toolbar($show) { $user = rua_get_user(); if ($user->has_global_access()) { return $show; } $levels = $user->get_level_ids(); if (empty($levels)) { return $show; } $metadata = $this->metadata()->get('hide_admin_bar'); //if user has at least 1 level without this option //don't hide the toolbar foreach ($levels as $level_id) { if ($metadata->get_data($level_id) != '1') { return $show; } } return false; } /** * Get level by name * * @since 0.6 * @param string $name * @return WP_Post|bool */ public function get_level_by_name($name) { $all_levels = RUA_App::instance()->get_levels(); foreach ($all_levels as $id => $level) { if ($level->post_name == $name && $level->post_status == RUA_App::STATUS_ACTIVE) { return $level; } } return false; } /** * Restrict content in shortcode * * @version 0.1 * @param array $atts * @param string $content * @return string */ public function shortcode_restrict($atts, $content = null) { $user = rua_get_user(); if ($user->has_global_access()) { return do_shortcode($content); } $a = shortcode_atts(array( 'role' => '', 'level' => '', 'page' => 0, 'drip_days' => 0 ), $atts, 'restrict'); $has_access = false; if ($a['level'] !== '') { $user_levels = array_flip($user->get_level_ids()); if (!empty($user_levels)) { $level_names = explode(',', str_replace(' ', '', $a['level'])); $not_found = 0; foreach ($level_names as $level_name) { $level = $this->get_level_by_name(ltrim($level_name, '!')); if (!$level) { $not_found++; continue; } //if level param is negated, give access only if user does not have it if ($level->post_name != $level_name) { $has_access = !isset($user_levels[$level->ID]); } elseif (isset($user_levels[$level->ID])) { $drip = (int)$a['drip_days']; if ($drip > 0 && $this->metadata()->get('role')->get_data($level->ID) === '') { $start = $user->get_level_start($level->ID); $drip_time = strtotime('+'.$drip.' days 00:00', $start); $should_drip = apply_filters( 'rua/auth/content-drip', time() <= $drip_time, $user, $level->ID ); if ($should_drip) { continue; } } $has_access = true; } if ($has_access) { break; } } //if levels do not exist, make content visible if (!$has_access && $not_found && $not_found === count($level_names)) { $has_access = true; } } } elseif ($a['role'] !== '') { $user_roles = array_flip(wp_get_current_user()->roles); if (!empty($user_roles)) { $roles = explode(',', str_replace(' ', '', $a['role'])); foreach ($roles as $role_name) { $role = ltrim($role_name, '!'); $not = $role != $role_name; //when role is negated, give access if user does not have it //otherwise give access only if user has it if ($not xor isset($user_roles[$role])) { $has_access = true; break; } } } } /** * @var bool $has_access * @var RUA_User_Interface $user * @var array $a */ $has_access = apply_filters('rua/shortcode/restrict', $has_access, $user, $a); if (!$has_access) { $content = ''; // Only apply the page content if it exists $page = $a['page'] ? get_post($a['page']) : null; if ($page) { setup_postdata($page); $content = get_the_content(); wp_reset_postdata(); } } return do_shortcode($content); } /** * Get instance of metadata manager * * @since 1.0 * @return WPCAObjectManager */ public function metadata() { if (!$this->metadata) { $this->_init_metadata(); } return $this->metadata; } /** * Create and populate metadata fields * * @since 0.1 * @return void */ private function _init_metadata() { $this->metadata = new WPCAObjectManager(); $this->metadata ->add(new WPCAMeta( 'role', __('Synchronized Role'), '', 'select', array() ), 'role') ->add(new WPCAMeta( 'handle', _x('Non-Member Action', 'option', 'restrict-user-access'), 0, 'select', array( 0 => __('Redirect', 'restrict-user-access'), 1 => __('Tease & Include', 'restrict-user-access') ), __('Redirect to another page or show teaser.', 'restrict-user-access') ), 'handle') ->add(new WPCAMeta( 'page', __('Page'), 0, 'select', array(), __('Page to redirect to or display content from under teaser.', 'restrict-user-access') ), 'page') ->add(new WPCAMeta( 'duration', __('Duration'), 'day', 'select', array( 'day' => __('Day(s)', 'restrict-user-access'), 'week' => __('Week(s)', 'restrict-user-access'), 'month' => __('Month(s)', 'restrict-user-access'), 'year' => __('Year(s)', 'restrict-user-access') ), __('Set to 0 for unlimited.', 'restrict-user-access') ), 'duration') ->add(new WPCAMeta( 'caps', __('Capabilities'), array(), '', array(), '', array($this,'sanitize_capabilities') ), 'caps') ->add(new WPCAMeta( 'hide_admin_bar', __('Hide Admin Toolbar'), '', 'checkbox', array(), '' ), 'hide_admin_bar') ->add(new WPCAMeta( 'default_access', __('Default Access'), 1, 'select', array( 1 => 'All unrestricted content', 0 => 'Restricted content only' ), '' ), 'default_access'); apply_filters('rua/metadata', $this->metadata); } /** * @param array|mixed $value * * @return array */ public function sanitize_capabilities($value) { if (is_array($value)) { $inherited_caps = isset($_POST['inherited_caps']) ? $_POST['inherited_caps'] : array(); foreach ($value as $name => $cap) { /** * do not save if: * - value is equal to inherited * - no inherited value and unsetting */ if (isset($inherited_caps[$name]) ? $inherited_caps[$name] == $cap : $cap == -1) { unset($value[$name]); } } } return $value; } /** * Populate input fields for metadata * * @since 0.8 * @return void */ public function populate_metadata() { $role_list = array( '' => __('-- None --', 'restrict-user-access'), -1 => __('Logged-in', 'restrict-user-access'), 0 => __('Not logged-in', 'restrict-user-access') ); foreach (get_editable_roles() as $id => $role) { $role_list[$id] = $role['name']; } $this->metadata()->get('role')->set_input_list($role_list); } /** * Create restrict post type and add it to WPCACore * * @since 0.1 * @return void */ public function create_restrict_type() { // Register the sidebar type register_post_type(RUA_App::TYPE_RESTRICT, array( 'labels' => array( 'name' => __('Access Levels', 'restrict-user-access'), 'singular_name' => __('Access Level', 'restrict-user-access'), 'add_new' => _x('Add New', 'level', 'restrict-user-access'), 'add_new_item' => __('Add New Access Level', 'restrict-user-access'), 'edit_item' => __('Edit Access Level', 'restrict-user-access'), 'new_item' => __('New Access Level', 'restrict-user-access'), 'all_items' => __('Access Levels', 'restrict-user-access'), 'view_item' => __('View Access Level', 'restrict-user-access'), 'search_items' => __('Search Access Levels', 'restrict-user-access'), 'not_found' => __('No Access Levels found', 'restrict-user-access'), 'not_found_in_trash' => __('No Access Levels found in Trash', 'restrict-user-access'), 'parent_item_colon' => __('Extend Level', 'restrict-user-access'), //wp-content-aware-engine specific 'ca_title' => __('Members-Only Access', 'content-aware-sidebars') ), 'capabilities' => array( 'edit_post' => RUA_App::CAPABILITY, 'read_post' => RUA_App::CAPABILITY, 'delete_post' => RUA_App::CAPABILITY, 'edit_posts' => RUA_App::CAPABILITY, 'delete_posts' => RUA_App::CAPABILITY, 'edit_others_posts' => RUA_App::CAPABILITY, 'publish_posts' => RUA_App::CAPABILITY, 'read_private_posts' => RUA_App::CAPABILITY ), 'public' => false, 'hierarchical' => true, 'exclude_from_search' => true, 'publicly_queryable' => false, 'show_ui' => false, 'show_in_menu' => false, 'show_in_nav_menus' => false, 'show_in_admin_bar' => false, 'menu_icon' => RUA_App::ICON_SVG, 'has_archive' => false, 'rewrite' => false, 'query_var' => false, 'supports' => array('title','page-attributes'), 'can_export' => false, 'delete_with_user' => false )); WPCACore::types()->add(RUA_App::TYPE_RESTRICT); } /** * Get conditional restrictions * and authorize access for user * * @since 0.1 * @return void */ public function authorize_access() { $rua_user = rua_get_user(); if ($rua_user->has_global_access()) { return; } $user_levels = array_flip(array_reverse($rua_user->get_level_ids())); $authorized_levels = WPCACore::get_posts(RUA_App::TYPE_RESTRICT); $kick = false; //does user have level to view unrestricted content by default? foreach ($user_levels as $level => $val) { if ($this->metadata()->get('default_access')->get_data($level, true)) { $kick = false; break; } $kick = $level; } //does user have authorized level? foreach ($authorized_levels as $level) { if (isset($user_levels[$level->ID])) { $kick = false; break; } $kick = $level->ID; } if (!empty($authorized_levels) && $kick === false && is_user_logged_in()) { $conditions = WPCACore::get_conditions(RUA_App::TYPE_RESTRICT); foreach ($conditions as $condition => $level) { //Check post type if (!isset($authorized_levels[$level])) { continue; } $drip = get_post_meta($condition, RUA_App::META_PREFIX.'opt_drip', true); //Restrict access to dripped content if ($drip && $this->metadata()->get('role')->get_data($level) === '') { $start = $rua_user->get_level_start($level); $drip_time = strtotime('+'.$drip.' days 00:00', $start); $should_drip = apply_filters( 'rua/auth/content-drip', time() <= $drip_time, $rua_user, $level ); if ($should_drip) { $kick = $level; continue; } } $kick = false; break; } } if ($kick === false) { return; } $action = $this->metadata()->get('handle')->get_data($kick); self::$page = $this->metadata()->get('page')->get_data($kick); switch ($action) { case 0: $redirect = ''; $current_path = remove_query_arg('redirect_to', add_query_arg(null, null)); $parts = parse_url(get_site_url()); $pos = isset($parts['path']) ? stripos($current_path, $parts['path']) : false; if ($pos !== false) { $relative_path = substr($current_path, $pos + strlen($parts['path'])); } else { $relative_path = $current_path; } if (is_numeric(self::$page)) { if (self::$page != get_the_ID()) { $redirect = get_permalink(self::$page); } } else { /** * WP always appends / * also check case where non-member action does not have it, * which can cause infinite loop */ if ($relative_path != self::$page && $relative_path != self::$page.'/') { $redirect = get_site_url().self::$page; } } //only redirect if current page != redirect page if ($redirect) { wp_safe_redirect(add_query_arg( 'redirect_to', urlencode($current_path), $redirect )); exit; } break; case 1: add_filter('the_content', array($this,'content_tease'), 8); break; default: break; } } /** * Carry over page from restriction metadata * @var integer */ public static $page = false; /** * Limit content to only show teaser and * page content from restriction metadata * * @since 0.1 * @param string $content * @return string */ public function content_tease($content) { if (preg_match('/(<\/span>)/', $content, $matches)) { $teaser = explode($matches[0], $content, 2); $content = $teaser[0]; } else { $content = ''; } if (is_numeric(self::$page)) { setup_postdata(get_post(self::$page)); $content .= get_the_content(); wp_reset_postdata(); } return $content; } /** * Override user caps with level caps. * * @param array $allcaps * @param string $cap * @param array $args { * @type string [0] Requested capability * @type int [1] User ID * @type WP_User [2] Associated object ID (User object) * } * @param WP_User $user * * @return array */ public function user_level_has_cap($allcaps, $cap, $args, $user) { $rua_user = rua_get_user($user); $global_access = $rua_user->has_global_access(); // if ($cap && $cap[0] == RUA_App::CAPABILITY && $global_access ) { // $allcaps[ $cap[0] ] = true; // } if (!$global_access && defined('WPCA_VERSION')) { $allcaps = $rua_user->get_caps($allcaps); } return $allcaps; } /** * Get all capabilities of one or multiple levels * * If you pass an array the order of these levels should be set correctly! * The first level caps will be overwritten by the second etc. * * @since 0.13 * @param array|int $levels * @return array */ public function get_levels_caps($levels) { $levels = (array) $levels; $caps = array(); foreach ($levels as $level) { $level_caps = $this->metadata()->get('caps')->get_data($level, true); foreach ($level_caps as $key => $level_cap) { if ($level_cap > -1) { $caps[$key] = (bool)$level_cap; } else { unset($caps[$key]); } } } return $caps; } /** * Maybe add level on user register * * @since 0.10 * @param int $user_id * @return void */ public function registered_add_level($user_id) { try { $level_id = get_option('rua-registration-level', 0); if ($level_id) { rua_get_user($user_id)->add_level($level_id); } } catch (Exception $e) { } } }