array( 'who' => 'everyone', 'roles' => array(), ), 'chat' => is_array( $old ) ? $old : array(), ); } $rule = isset( $rules[ $feature ] ) && is_array( $rules[ $feature ] ) ? $rules[ $feature ] : array(); $who = in_array( $rule['who'] ?? '', self::WHO, true ) ? $rule['who'] : 'everyone'; $roles = array_values( array_map( 'sanitize_key', (array) ( $rule['roles'] ?? array() ) ) ); return array( 'who' => 'roles' === $who && ! $roles ? 'logged_in' : $who, 'roles' => $roles, ); } /** * Whether the current visitor may use a feature. * * @param string $feature 'search' or 'chat'. * @return bool */ public static function allowed( $feature ) { $rule = self::rule( $feature ); if ( 'everyone' === $rule['who'] ) { return true; } if ( ! is_user_logged_in() ) { return false; } if ( 'roles' !== $rule['who'] ) { return true; } return (bool) array_intersect( (array) wp_get_current_user()->roles, $rule['roles'] ); } /** * The current visitor as the service sees them. * * @return array{loggedIn:bool,roles:string[]} */ public static function viewer() { if ( ! is_user_logged_in() ) { return array( 'loggedIn' => false, 'roles' => array(), ); } return array( 'loggedIn' => true, 'roles' => array_values( array_map( 'sanitize_key', (array) wp_get_current_user()->roles ) ), ); } /** * A short-lived token proving the current user's roles to the public * search and chat routes, signed with this site's private key. Empty * for visitors who are not logged in, or when members-only content is * off (nothing to prove). * * @return string */ public static function member_token() { if ( ! is_user_logged_in() || ! self::members_enabled() || ! Keys::is_connected() ) { return ''; } $user = wp_get_current_user(); $payload = wp_json_encode( array( 'k' => (string) get_option( Keys::OPT_KEY_ID ), 'r' => array_values( array_map( 'sanitize_key', (array) $user->roles ) ), 'e' => time() + self::TOKEN_SECONDS, 'u' => substr( hash( 'sha256', (string) $user->ID . wp_salt( 'auth' ) ), 0, 16 ), ) ); $body = self::b64url( $payload ); $sig = Keys::sign( self::TOKEN_PREFIX . $body ); if ( false === $sig ) { return ''; } return 'v1.' . $body . '.' . self::b64url( base64_decode( $sig ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- a signature. } /** * URL-safe base64 without padding. * * @param string $raw Bytes. * @return string */ private static function b64url( $raw ) { return rtrim( strtr( base64_encode( $raw ), '+/', '-_' ), '=' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- token encoding. } /** * Current access version (see OPT_VERSION). * * @return int */ public static function version() { return (int) get_option( self::OPT_VERSION, 0 ); } /** * Members-only settings. * * @return array{enabled:bool,private:bool,terms:int[],roles:string[]} */ public static function members() { $m = get_option( self::OPT_MEMBERS, array() ); $m = is_array( $m ) ? $m : array(); return array( 'enabled' => ! empty( $m['enabled'] ), 'private' => ! empty( $m['private'] ), 'terms' => array_values( array_filter( array_map( 'absint', (array) ( $m['terms'] ?? array() ) ) ) ), 'roles' => array_values( array_map( 'sanitize_key', (array) ( $m['roles'] ?? array() ) ) ), ); } /** * Whether members-only content is on and the plan allows it. * * @return bool */ public static function members_enabled() { return self::members()['enabled'] && Chat::feature( 'membersOnly' ); } /** * Whether private and password-protected items are indexed (as * members-only). * * @return bool */ public static function include_private() { return self::members_enabled() && self::members()['private']; } /** * The visibility an item is sent with: its own mark, else its parent's * (PDFs), else private/password-protected → members, else a members-only * category → the configured roles, else public. * * @param \WP_Post $post Post or attachment. * @return array{visibility:string,roles:string[]} */ public static function visibility_for( $post ) { $public = array( 'visibility' => 'public', 'roles' => array(), ); if ( ! $post instanceof \WP_Post || ! self::members_enabled() ) { return $public; } $own = self::own_mark( $post ); if ( $own ) { return $own; } if ( 'attachment' === $post->post_type && $post->post_parent ) { $parent = get_post( $post->post_parent ); if ( $parent ) { return self::visibility_for( $parent ); } return $public; } if ( 'private' === $post->post_status || '' !== $post->post_password ) { return array( 'visibility' => 'members', 'roles' => array(), ); } $m = self::members(); if ( $m['terms'] && is_object_in_taxonomy( $post->post_type, 'category' ) && has_category( $m['terms'], $post ) ) { return $m['roles'] ? array( 'visibility' => 'roles', 'roles' => $m['roles'], ) : array( 'visibility' => 'members', 'roles' => array(), ); } return $public; } /** * The item's own "Members only" mark from its edit screen, if any. * * @param \WP_Post $post Post. * @return array{visibility:string,roles:string[]}|null */ private static function own_mark( $post ) { $vis = (string) get_post_meta( $post->ID, self::META_VISIBILITY, true ); if ( 'members' !== $vis && 'roles' !== $vis ) { return null; } $roles = array_values( array_map( 'sanitize_key', (array) get_post_meta( $post->ID, self::META_ROLES, true ) ) ); return 'roles' === $vis && $roles ? array( 'visibility' => 'roles', 'roles' => $roles, ) : array( 'visibility' => 'members', 'roles' => array(), ); } /** * Saves the Access tab. */ public static function handle_save() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to do this.', 'askmypages' ), 403 ); } check_admin_referer( 'askmypages_access' ); // phpcs:disable WordPress.Security.NonceVerification.Missing -- checked above. $valid = array_keys( wp_roles()->get_names() ); $rules = array(); foreach ( array( 'search', 'chat' ) as $feature ) { $who = isset( $_POST[ $feature . '_who' ] ) ? sanitize_key( wp_unslash( $_POST[ $feature . '_who' ] ) ) : 'everyone'; $roles = isset( $_POST[ $feature . '_roles' ] ) ? array_map( 'sanitize_key', (array) wp_unslash( $_POST[ $feature . '_roles' ] ) ) : array(); $roles = array_values( array_intersect( $roles, $valid ) ); $rules[ $feature ] = array( 'who' => in_array( $who, self::WHO, true ) ? ( 'roles' === $who && ! $roles ? 'logged_in' : $who ) : 'everyone', 'roles' => $roles, ); } update_option( self::OPT_RULES, $rules, false ); $terms = isset( $_POST['members_terms'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['members_terms'] ) ) : array(); $mroles = isset( $_POST['members_roles'] ) ? array_map( 'sanitize_key', (array) wp_unslash( $_POST['members_roles'] ) ) : array(); update_option( self::OPT_MEMBERS, array( 'enabled' => isset( $_POST['members_enabled'] ), 'private' => isset( $_POST['members_private'] ), 'terms' => array_values( array_filter( $terms ) ), 'roles' => array_values( array_intersect( $mroles, $valid ) ), ), false ); // phpcs:enable WordPress.Security.NonceVerification.Missing // Visibility travels with every item: cached hashes are retired and // every item is checked again (unchanged ones cost one cheap request). update_option( self::OPT_VERSION, self::version() + 1, false ); if ( Keys::is_connected() ) { as_enqueue_async_action( Sync::HOOK_RECONCILE, array(), Sync::GROUP ); } wp_safe_redirect( add_query_arg( 'askmypages_notice', 'access_saved', Admin::tab_url( 'access' ) ) ); exit; } /** * The Access tab. */ public static function render_tab() { $paid = Chat::feature( 'membersOnly' ); $members = self::members(); $names = wp_roles()->get_names(); $rule = static function ( $feature, $label, $help ) use ( $names ) { $r = self::rule( $feature ); ?>