| | |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\CustomTable; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\PermissionHandler; |
| | | use JVBase\rest\Response; |
| | | use JVBase\rest\Rest; |
| | |
| | | exit; |
| | | } |
| | | |
| | | /** |
| | | * TODO: Extract business logic into a Favourites.php manager class |
| | | */ |
| | | class FavouritesRoutes extends Rest |
| | | { |
| | | protected array $valid_types; |
| | |
| | | parent::__construct(); |
| | | |
| | | // Set up cache connections |
| | | $this->cache->connect('post')->connect('user')->connect('taxonomy'); |
| | | $this->listsCache = Cache::for('lists')->connect('favourites', true); |
| | | $this->sharedListsCache = Cache::for('sharedLists')->connect('favourites', true); |
| | | $this->favouritesCache = Cache::for('allFavourites')->connect('favourites', true); |
| | | $this->cache->connect('post')->connect('user')->connect('taxonomy')->user(); |
| | | $this->listsCache = Cache::for('lists')->connect('favourites', true)->user(); |
| | | $this->sharedListsCache = Cache::for('sharedLists')->connect('favourites', true)->user(); |
| | | $this->favouritesCache = Cache::for('allFavourites')->connect('favourites', true)->user(); |
| | | |
| | | $this->valid_types = array_keys(array_merge(JVB_CONTENT, JVB_TAXONOMY)); |
| | | $this->valid_types = array_merge(Registrar::getRegistered('post'), Registrar::getRegistered('term')); |
| | | |
| | | // Initialize CustomTable instances |
| | | $this->favourites = CustomTable::for('favourites'); |
| | |
| | | ->post([$this, 'handleFavourite']) |
| | | ->args([ |
| | | 'user' => 'integer|required', |
| | | 'id' => 'string|required', |
| | | 'action' => 'string|required|enum:add,remove,toggle,batch,note', |
| | | 'type' => 'string', |
| | | 'target_id' => 'integer', |
| | |
| | | if ($cache_check) { |
| | | return $cache_check; |
| | | } |
| | | |
| | | if (count($args) === 1 || ($request->get_param('include_all') === true)) { |
| | | $result = $this->getAllFavourites($user_id); |
| | | } else { |
| | | $result = $this->cache->remember($key, function() use ($args) { |
| | | return $this->getFilteredFavourites($args); |
| | | }); |
| | | } |
| | | $result = JVB()->favourites()->getFavourites($args); |
| | | $result['items'] = $this->formatItems($result['items']); |
| | | |
| | | return $this->addCacheHeaders(Response::success($result)); |
| | | } |
| | | |
| | | /** |
| | | * Get filtered favourites using CustomTable fluent interface |
| | | */ |
| | | protected function getFilteredFavourites(array $args): array |
| | | { |
| | | try { |
| | | // Build base query |
| | | $query = $this->favourites->where(['user_id' => $args['user']]); |
| | | |
| | | // Add type filter if specified |
| | | if (!empty($args['content']) && $args['content'] !== 'all') { |
| | | $query = $this->favourites->where([ |
| | | 'user_id' => $args['user'], |
| | | 'type' => BASE . $args['content'] |
| | | ]); |
| | | } |
| | | |
| | | // Apply ordering and pagination |
| | | $orderby = in_array($args['orderby'] ?? 'date_added', ['date_added', 'type']) |
| | | ? $args['orderby'] |
| | | : 'date_added'; |
| | | $order = in_array(strtoupper($args['order'] ?? 'DESC'), ['ASC', 'DESC']) |
| | | ? strtoupper($args['order']) |
| | | : 'DESC'; |
| | | |
| | | $favourites = $query |
| | | ->orderBy($orderby, $order) |
| | | ->limit(100, ($args['page'] - 1) * 100) |
| | | ->getResults(); |
| | | |
| | | // Get total count |
| | | $count_query = $this->favourites->where(['user_id' => $args['user']]); |
| | | if (!empty($args['content']) && $args['content'] !== 'all') { |
| | | $count_query->where(['type' => BASE . $args['content']]); |
| | | } |
| | | $total_items = $count_query->countResults(); |
| | | |
| | | return [ |
| | | 'items' => $this->formatItems($favourites), |
| | | 'has_more' => ($args['page'] * 100) < $total_items, |
| | | 'total' => $total_items, |
| | | 'success' => true, |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('getFilteredFavourites', [ |
| | | 'error' => $e->getMessage(), |
| | | 'args' => $args |
| | | ]); |
| | | |
| | | return [ |
| | | 'success' => false, |
| | | 'items' => [], |
| | | 'total' => 0, |
| | | 'has_more' => false |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get all user's favourites organized by content type |
| | | */ |
| | | protected function getAllFavourites(int $user_id): array |
| | | { |
| | | return $this->cache->remember($user_id, function() use ($user_id) { |
| | | try { |
| | | $favourites = $this->favourites |
| | | ->where(['user_id' => $user_id]) |
| | | ->getResults(); |
| | | |
| | | $by_type = []; |
| | | foreach ($favourites as $fav) { |
| | | $type = str_replace(BASE, '', $fav->type); |
| | | if (!isset($by_type[$type])) { |
| | | $by_type[$type] = []; |
| | | } |
| | | $by_type[$type][] = (int)$fav->target_id; |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'items' => $by_type, |
| | | 'has_more' => false, |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('getAllFavourites', [ |
| | | 'error' => $e->getMessage(), |
| | | 'user_id' => $user_id |
| | | ]); |
| | | |
| | | return [ |
| | | 'success' => false, |
| | | 'items' => [], |
| | | ]; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * Handle favourite operations |
| | | */ |
| | | public function handleFavourite(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $user_id = absint($request->get_param('user')); |
| | | $operation_id = sanitize_text_field($request->get_param('id')); |
| | | $action = sanitize_text_field($request->get_param('action')); |
| | | $params = $request->get_params(); |
| | | $user_id = absint($params['user']??0); |
| | | |
| | | if (!$this->userCheck($user_id)) { |
| | | return $this->unauthorized(); |
| | | } |
| | | $action = strtolower(sanitize_text_field($params['action'])); |
| | | $action = in_array($action, ['add', 'remove']) ? $action : false; |
| | | if (!$action) { |
| | | return $this->error('Invalid favourite action'); |
| | | } |
| | | $target_id = absint($params['target_id']??0); |
| | | if ($target_id === 0) { |
| | | return $this->error('Invalid target id'); |
| | | } |
| | | |
| | | $data = [ |
| | | 'action' => $action, |
| | | 'type' => sanitize_text_field($request->get_param('type') ?? ''), |
| | | 'target_id' => absint($request->get_param('target_id') ?? 0), |
| | | 'items' => $request->get_param('items') ?? [], |
| | | 'notes' => sanitize_textarea_field($request->get_param('notes') ?? ''), |
| | | ]; |
| | | $type = sanitize_text_field($params['type']??''); |
| | | if (empty($type)) { |
| | | return $this->error('No type provided'); |
| | | } |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | 'favourite_' . $action, |
| | | $result = JVB()->favourites()->toggleFavourite( |
| | | $action === 'add', |
| | | $user_id, |
| | | $data, |
| | | [ |
| | | 'operation_id' => $operation_id, |
| | | 'priority' => 'high', |
| | | ] |
| | | $target_id, |
| | | $type |
| | | ); |
| | | |
| | | return $this->queued($operation_id); |
| | | if ($result) { |
| | | return $this->success(); |
| | | } |
| | | return $this->error('Something went wrong'); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public function getLists(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $user_id = absint($request->get_param('user')); |
| | | $params = $request->get_params(); |
| | | $user_id = absint($params['user']); |
| | | |
| | | if (!$this->userCheck($user_id)) { |
| | | return $this->unauthorized(); |
| | | } |
| | | |
| | | $params = ['user' => $user_id]; |
| | | if ($request->get_param('id')) { |
| | | $params['list'] = sanitize_text_field($request->get_param('id')); |
| | | $args = $this->buildParams($request); |
| | | $args['per_page'] = 20; |
| | | $listId = $request->get_param('id'); |
| | | if (!empty($listId)) { |
| | | $args['where']['id'] = sanitize_text_field($listId); |
| | | } |
| | | |
| | | $key = $this->listsCache->generateKey($params); |
| | | $key = $this->listsCache->generateKey($args); |
| | | |
| | | // Check cache headers |
| | | $cache_check = $this->checkHeaders($request, $key); |
| | |
| | | return $cache_check; |
| | | } |
| | | |
| | | $list_id = $request->get_param('id'); |
| | | $response = $list_id |
| | | ? $this->getListDetails($list_id, $user_id) |
| | | : $this->getAvailableLists($user_id); |
| | | $includeShares = !empty($request->get_param('include_shares')); |
| | | |
| | | $response = !empty($listId) |
| | | ? JVB()->favourites()->getListDetails($listId, $user_id) |
| | | : JVB()->favourites()->getAvailableLists($args, $includeShares); |
| | | |
| | | return $this->addCacheHeaders(Response::success($response)); |
| | | } |
| | | |
| | | /** |
| | | * Get lists available to a user using CustomTable |
| | | */ |
| | | protected function getAvailableLists(int $user_id, bool $include_shared = true): array |
| | | { |
| | | if (!$this->checkUser($user_id)) { |
| | | return []; |
| | | } |
| | | |
| | | $cache = $include_shared ? $this->sharedListsCache : $this->listsCache; |
| | | |
| | | return $cache->remember($user_id, function() use ($user_id, $include_shared) { |
| | | try { |
| | | // Get owned lists |
| | | $owned = $this->lists |
| | | ->where(['user_id' => $user_id]) |
| | | ->orderBy('created_at', 'DESC') |
| | | ->getResults(ARRAY_A); |
| | | |
| | | // Add item counts |
| | | foreach ($owned as &$list) { |
| | | $list['item_count'] = $this->listItems |
| | | ->where(['list_id' => $list['id']]) |
| | | ->countResults(); |
| | | $list['is_owner'] = true; |
| | | $list['is_shared'] = false; |
| | | } |
| | | |
| | | if (!$include_shared) { |
| | | return [ |
| | | 'success' => true, |
| | | 'lists' => $owned |
| | | ]; |
| | | } |
| | | |
| | | // Get shared lists |
| | | $shares = $this->listShares |
| | | ->where(['user_id' => $user_id, 'status' => 'accepted']) |
| | | ->getResults(); |
| | | |
| | | $shared_lists = []; |
| | | foreach ($shares as $share) { |
| | | $list = $this->lists |
| | | ->where(['id' => $share->list_id]) |
| | | ->first(ARRAY_A); |
| | | |
| | | if ($list) { |
| | | $owner = get_userdata($list['user_id']); |
| | | $list['owner_name'] = $owner ? $owner->display_name : 'Unknown'; |
| | | $list['item_count'] = $this->listItems |
| | | ->where(['list_id' => $list['id']]) |
| | | ->countResults(); |
| | | $list['permission_type'] = $share->permission_type; |
| | | $list['is_owner'] = false; |
| | | $list['is_shared'] = true; |
| | | |
| | | $shared_lists[] = $list; |
| | | } |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'lists' => [ |
| | | 'owned' => $owned, |
| | | 'shared' => $shared_lists |
| | | ] |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('getAvailableLists', [ |
| | | 'error' => $e->getMessage(), |
| | | 'user_id' => $user_id |
| | | ]); |
| | | |
| | | return []; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * TODO: Done until here |
| | | * Get favourite counts by type |
| | | */ |
| | | public function getFavouriteCounts(WP_REST_Request $request): WP_REST_Response |
| | |
| | | 'target_id' => $target_id |
| | | ]); |
| | | |
| | | if ($result['created']) { |
| | | if ((bool)$result) { |
| | | $this->updateFavouriteCount($type, $target_id); |
| | | $this->maybeNotifyOwner($type, $target_id, $user_id); |
| | | } |
| | |
| | | 'type' => $type, |
| | | 'target_id' => $target_id |
| | | ]); |
| | | if ($result['created']) $results['added']++; |
| | | if ((bool) $result) $results['added']++; |
| | | } else { |
| | | $deleted = $table->where([ |
| | | $deleted = $table->delete([ |
| | | 'user_id' => $user_id, |
| | | 'type' => $type, |
| | | 'target_id' => $target_id |
| | | ])->deleteResults(); |
| | | ]); |
| | | if ($deleted) $results['removed']++; |
| | | } |
| | | |
| | |
| | | protected function buildParams(WP_REST_Request $request): array |
| | | { |
| | | $data = $request->get_params(); |
| | | $args = ['user' => absint($data['user'])]; |
| | | |
| | | if (!array_key_exists('page', $data)) { |
| | | return $args; |
| | | $where = ['user_id' => absint($data['user'])]; |
| | | if (!empty($data['content']) && $data['content'] !== 'all') { |
| | | $where['type'] = BASE . $data['content']; |
| | | } |
| | | |
| | | $args = array_merge($args, [ |
| | | 'page' => max(1, absint($data['page'] ?? 1)), |
| | | 'content' => $this->checkContent($data['content'] ?? 'all') |
| | | ]); |
| | | $page = max(1, absint($data['page'] ?? 1)); |
| | | $perPage = 250; |
| | | |
| | | return $this->applyOrderFilters($args, $data); |
| | | return [ |
| | | 'where' => $where, |
| | | 'orderby' => sanitize_text_field($data['orderby'] ?? 'created_at'), |
| | | 'order' => sanitize_text_field($data['order'] ?? 'DESC'), |
| | | 'per_page' => $perPage, |
| | | 'page' => $page |
| | | ]; |
| | | } |
| | | |
| | | |
| | |
| | | */ |
| | | protected function getListDetails(int $list_id, int $user_id): array |
| | | { |
| | | // Check access - either owner or has share |
| | | $is_owner = JVB()->favourites()->userOwnsList($list_id, $user_id); |
| | | $is_shared = JVB()->favourites()->userCanViewList($list_id, $user_id); |
| | | |
| | | |
| | | if (!$is_owner && !$is_shared) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'You do not have access to this list.' |
| | | ]; |
| | | } |
| | | |
| | | $list = JVB()->favourites()->getListDetails($list_id, $user_id); |
| | | |
| | | if (empty($list)) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'List not found' |
| | | ]; |
| | | } |
| | | |
| | | |
| | | |
| | | $key = "list_{$list_id}_user_{$user_id}"; |
| | | |
| | | return $this->listsCache->remember($key, function () use ($list_id, $user_id) { |
| | | try { |
| | | // Check access - either owner or has share |
| | | $is_owner = $this->lists->where([ |
| | | 'id' => $list_id, |
| | | 'user_id' => $user_id |
| | | ])->existsInQuery(); |
| | | |
| | | $share = null; |
| | | if (!$is_owner) { |
| | | $share = $this->listShares->where([ |
| | | 'list_id' => $list_id, |
| | | 'user_id' => $user_id, |
| | | 'status' => 'accepted' |
| | | ])->first(); |
| | | } |
| | | |
| | | if (!$is_owner && !$share) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'You do not have access to this list' |
| | | ]; |
| | | } |
| | | |
| | | // Get list details |
| | | $list = $this->lists->where(['id' => $list_id])->first(ARRAY_A); |
| | | if (!$list) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'List not found' |
| | | ]; |
| | | } |
| | | |
| | | // Get list items |
| | | $items = $this->listItems |