401] ); } $requestedUserId = $request->get_param('user'); // No user param specified - allow (controller will handle) if (empty($requestedUserId)) { return true; } $currentUserId = get_current_user_id(); if ((int) $requestedUserId !== $currentUserId) { return new WP_Error( 'forbidden', 'You can only access your own resources', ['status' => 403] ); } return true; } /** * Check if current user is an administrator */ public static function isAdmin(WP_REST_Request $request): bool|WP_Error { if (!is_user_logged_in()) { return new WP_Error( 'not_logged_in', 'You must be logged in', ['status' => 401] ); } if (!current_user_can('manage_options')) { return new WP_Error( 'forbidden', 'Administrator access required', ['status' => 403] ); } return true; } /** * Check if current user is verified (has skip_moderation capability) */ public static function isVerified(WP_REST_Request $request): bool|WP_Error { if (!is_user_logged_in()) { return new WP_Error( 'not_logged_in', 'You must be logged in', ['status' => 401] ); } if (!current_user_can('skip_moderation')) { return new WP_Error( 'not_verified', 'Account verification required', ['status' => 403] ); } return true; } /** * Check if current user has a specific role */ public static function hasRole(WP_REST_Request $request, string $role): bool|WP_Error { if (!is_user_logged_in()) { return new WP_Error( 'not_logged_in', 'You must be logged in', ['status' => 401] ); } $user = wp_get_current_user(); $role = self::normalizeRole($role); if (!in_array($role, $user->roles, true)) { return new WP_Error( 'forbidden', 'You do not have the required role', ['status' => 403] ); } return true; } /** * Check if current user has any of the specified roles */ public static function hasAnyRole(WP_REST_Request $request, array $roles): bool|WP_Error { if (!is_user_logged_in()) { return new WP_Error( 'not_logged_in', 'You must be logged in', ['status' => 401] ); } $user = wp_get_current_user(); $normalizedRoles = array_map([self::class, 'normalizeRole'], $roles); foreach ($user->roles as $userRole) { if (in_array($userRole, $normalizedRoles, true)) { return true; } } return new WP_Error( 'forbidden', 'You do not have any of the required roles', ['status' => 403] ); } /** * Check if current user has all specified roles */ public static function hasAllRoles(WP_REST_Request $request, array $roles): bool|WP_Error { if (!is_user_logged_in()) { return new WP_Error( 'not_logged_in', 'You must be logged in', ['status' => 401] ); } $user = wp_get_current_user(); $normalizedRoles = array_map([self::class, 'normalizeRole'], $roles); foreach ($normalizedRoles as $role) { if (!in_array($role, $user->roles, true)) { return new WP_Error( 'forbidden', 'You do not have all required roles', ['status' => 403] ); } } return true; } /** * Check if current user has a specific capability */ public static function hasCapability(WP_REST_Request $request, string $capability): bool|WP_Error { if (!is_user_logged_in()) { return new WP_Error( 'not_logged_in', 'You must be logged in', ['status' => 401] ); } if (!current_user_can($capability)) { return new WP_Error( 'forbidden', 'You do not have the required permission', ['status' => 403] ); } return true; } /** * Check if current user has any of the specified capabilities */ public static function hasAnyCapability(WP_REST_Request $request, array $capabilities): bool|WP_Error { if (!is_user_logged_in()) { return new WP_Error( 'not_logged_in', 'You must be logged in', ['status' => 401] ); } foreach ($capabilities as $cap) { if (current_user_can($cap)) { return true; } } return new WP_Error( 'forbidden', 'You do not have any of the required permissions', ['status' => 403] ); } /** * Check if user owns a specific post */ public static function ownsPost(WP_REST_Request $request, string $paramName = 'post_id'): bool|WP_Error { if (!is_user_logged_in()) { return new WP_Error( 'not_logged_in', 'You must be logged in', ['status' => 401] ); } $postId = $request->get_param($paramName); if (empty($postId)) { return new WP_Error( 'missing_param', 'Post ID is required', ['status' => 400] ); } $post = get_post($postId); if (!$post) { return new WP_Error( 'not_found', 'Post not found', ['status' => 404] ); } if ((int) $post->post_author !== get_current_user_id()) { // Allow admins to bypass ownership check if (!current_user_can('manage_options')) { return new WP_Error( 'forbidden', 'You can only modify your own content', ['status' => 403] ); } } return true; } /** * Check if user can edit a specific post type */ public static function canEditPostType(WP_REST_Request $request, string $postType): bool|WP_Error { if (!is_user_logged_in()) { return new WP_Error( 'not_logged_in', 'You must be logged in', ['status' => 401] ); } $postTypeObj = get_post_type_object($postType); if (!$postTypeObj) { return new WP_Error( 'invalid_post_type', 'Invalid post type', ['status' => 400] ); } if (!current_user_can($postTypeObj->cap->edit_posts)) { return new WP_Error( 'forbidden', 'You cannot edit this content type', ['status' => 403] ); } return true; } /** * Verify nonce from request header */ public static function verifyNonce(WP_REST_Request $request, string $action = 'wp_rest', string $header = 'X-WP-Nonce'): bool|WP_Error { $nonce = $request->get_header($header); if (empty($nonce)) { return new WP_Error( 'missing_nonce', 'Security token is missing', ['status' => 403] ); } if (!wp_verify_nonce($nonce, $action)) { return new WP_Error( 'invalid_nonce', 'Invalid or expired security token', ['status' => 403] ); } return true; } /** * Verify action-specific nonce (e.g., 'dash-{user_id}') */ public static function verifyActionNonce(WP_REST_Request $request, string $actionPrefix, string $header = 'X-Action-Nonce'): bool|WP_Error { $userId = absint($request->get_param('user')); if ($userId === 0) { return false; } $action = $actionPrefix . $userId; return self::verifyNonce($request, $action, $header); } /** * Create a custom permission callback combining multiple checks * * Usage: * PermissionHandler::combine(['logged_in', 'verified']) * PermissionHandler::combine([['role' => 'artist'], ['capability' => 'edit_posts']]) */ public static function combine(array $checks): callable { return function(WP_REST_Request $request) use ($checks) { foreach ($checks as $check) { $result = match (true) { $check === 'logged_in' => is_user_logged_in() ?: new WP_Error('not_logged_in', 'Login required', ['status' => 401]), $check === 'admin' => self::isAdmin($request), $check === 'verified' => self::isVerified($request), $check === 'user' => self::userMatch($request), $check === 'nonce' => self::verifyNonce($request), is_array($check) && isset($check['role']) => self::hasRole($request, $check['role']), is_array($check) && isset($check['roles']) => self::hasAnyRole($request, $check['roles']), is_array($check) && isset($check['capability']) => self::hasCapability($request, $check['capability']), is_array($check) && isset($check['actionNonce']) => self::verifyActionNonce($request, $check['actionNonce']), is_callable($check) => $check($request), default => true, }; if (is_wp_error($result)) { return $result; } if ($result === false) { return new WP_Error('forbidden', 'Access denied', ['status' => 403]); } } return true; }; } /** * Create an OR permission callback (passes if ANY check passes) */ public static function any(array $checks): callable { return function(WP_REST_Request $request) use ($checks) { $lastError = null; foreach ($checks as $check) { $result = match (true) { $check === 'logged_in' => is_user_logged_in(), $check === 'admin' => self::isAdmin($request), $check === 'verified' => self::isVerified($request), $check === 'user' => self::userMatch($request), $check === 'nonce' => self::verifyNonce($request), is_array($check) && isset($check['role']) => self::hasRole($request, $check['role']), is_array($check) && isset($check['roles']) => self::hasAnyRole($request, $check['roles']), is_array($check) && isset($check['capability']) => self::hasCapability($request, $check['capability']), is_array($check) && isset($check['actionNonce']) => self::verifyActionNonce($request, $check['actionNonce']), is_callable($check) => $check($request), default => false, }; if ($result === true) { return true; } if (is_wp_error($result)) { $lastError = $result; } } return $lastError ?: new WP_Error('forbidden', 'Access denied', ['status' => 403]); }; } /** * Normalize role name (add BASE prefix if needed) */ private static function normalizeRole(string $role): string { if (defined('BASE') && !str_starts_with($role, BASE)) { // Check if it's a WordPress core role $coreRoles = ['administrator', 'editor', 'author', 'contributor', 'subscriber']; if (!in_array($role, $coreRoles, true)) { return BASE . $role; } } return $role; } /** * Get current user for request (cached) */ public static function getCurrentUser(): ?WP_User { static $user = null; if ($user === null && is_user_logged_in()) { $user = wp_get_current_user(); } return $user instanceof WP_User ? $user : null; } /** * Check if request is from same origin (basic CSRF protection) */ public static function verifySameOrigin(WP_REST_Request $request): bool|WP_Error { $origin = $request->get_header('Origin'); $referer = $request->get_header('Referer'); $siteUrl = get_site_url(); $siteDomain = parse_url($siteUrl, PHP_URL_HOST); // Check origin header if ($origin) { $originDomain = parse_url($origin, PHP_URL_HOST); if ($originDomain !== $siteDomain) { return new WP_Error( 'cross_origin', 'Cross-origin requests not allowed', ['status' => 403] ); } } // Check referer as fallback if (!$origin && $referer) { $refererDomain = parse_url($referer, PHP_URL_HOST); if ($refererDomain !== $siteDomain) { return new WP_Error( 'cross_origin', 'Cross-origin requests not allowed', ['status' => 403] ); } } return true; } }