From 2127b1bdd73ecd2423e443992da4b442f5a3c1a3 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Wed, 04 Feb 2026 21:19:25 +0000
Subject: [PATCH] =Major overhaul of MetaManager.php -> Meta.php and RestRouteManager.php -> Rest.php. Seems to work for JakeVan
---
inc/rest/routes/FavouritesRoutes.php | 5284 ++++++++++++++++++++++-------------------------------------
1 files changed, 1,959 insertions(+), 3,325 deletions(-)
diff --git a/inc/rest/routes/FavouritesRoutes.php b/inc/rest/routes/FavouritesRoutes.php
index a5de911..f127aae 100644
--- a/inc/rest/routes/FavouritesRoutes.php
+++ b/inc/rest/routes/FavouritesRoutes.php
@@ -1,3113 +1,1022 @@
<?php
namespace JVBase\rest\routes;
-use JVBase\JVB;
-use JVBase\rest\RestRouteManager;
-use JVBase\managers\CacheManager;
+use JVBase\managers\Cache;
+use JVBase\managers\CustomTable;
+use JVBase\rest\PermissionHandler;
+use JVBase\rest\Response;
+use JVBase\rest\Rest;
+use JVBase\rest\Route;
use WP_REST_Request;
use WP_REST_Response;
-use WP_Error;
use Exception;
if (!defined('ABSPATH')) {
- exit; // Exit if accessed directly
+ exit;
}
-class FavouritesRoutes extends RestRouteManager
+/**
+ * TODO: Extract business logic into a Favourites.php manager class
+ */
+class FavouritesRoutes extends Rest
{
- protected array $valid_types;
- protected int $user_id;
+ protected array $valid_types;
+ protected Cache $listsCache;
+ protected Cache $sharedListsCache;
+ protected Cache $favouritesCache;
+ protected CustomTable $favourites;
+ protected CustomTable $lists;
+ protected CustomTable $listItems;
+ protected CustomTable $listShares;
- public function __construct()
- {
- $this->cache_name = 'favourites';
- parent::__construct();
-
- $this->valid_types = array_keys(array_merge(JVB_CONTENT, JVB_TAXONOMY));
-
- $this->user_id = get_current_user_id();
- $this->action = 'favourites-';
-
-
- add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
- add_action('before_delete_post', [$this, 'cleanupPostFavourites']);
- add_action('delete_term', [$this, 'cleanupTermFavourites'], 10, 3);
-
- add_action('jvbUserRegistered', [$this, 'maybeAcceptListInvite'], 10, 3);
-
- // Register cleanup scheduler
- add_action('jvb_cleanupOrphanedFavourites', [$this, 'cleanupOrphanedFavourites']);
- }
-
- /**
- * Registers favourites routes
- * @return void
- */
- public function registerRoutes():void
- {
- // Main favourites endpoint - GET for retrieval, POST for toggling/notes
- register_rest_route($this->namespace, '/favourites', [
- [
- 'methods' => 'GET',
- 'callback' => [$this, 'getFavourites'],
- 'permission_callback' => [$this, 'checkPermission']
- ],
- [
- 'methods' => 'POST',
- 'callback' => [$this, 'handleFavouriteOperation'],
- 'permission_callback' => [$this, 'checkPermission']
- ]
- ]);
-
- // Lists endpoint - GET for retrieval, POST for creation/modifications
- register_rest_route($this->namespace, '/favourites/lists', [
- [
- 'methods' => 'GET',
- 'callback' => [$this, 'getLists'],
- 'permission_callback' => [$this, 'checkPermission']
- ],
- //Adding and removing list items is handled by the body dta
- [
- 'methods' => 'POST',
- 'callback' => [$this, 'handleListOperation'],
- 'permission_callback' => [$this, 'checkPermission']
- ]
- ]);
-
- // List shares operations
- register_rest_route($this->namespace, '/favourites/lists/shares', [
- [
- 'methods' => 'GET',
- 'callback' => [$this, 'getShares'],
- 'permission_callback' => [$this, 'checkPermission']
- ],
- //Adds and removes are handled in the body data
- [
- 'methods' => 'POST',
- 'callback' => [$this, 'handleShare'],
- 'permission_callback' => [$this, 'checkPermission']
- ]
- ]);
- }
- protected function buildParams(WP_REST_Request $request):array
+ public function __construct()
{
- $data = $request->get_params();
- error_log('Favourites Request Data: '.print_r($data, true));
- $args = [];
- if (!array_key_exists('user', $data)) {
- return $args;
- }
- $args['user'] = absint($data['user']);
- if (!array_key_exists('page', $data)) {
- //No filters set, just get a list of favourites
- return $args;
- }
- $args = array_merge($args, [
- 'page' => max(1, absint($data['page'] ?? 1)),
- 'content' => $this->checkContent($data['content'])
- ]);
- return $this->applyOrderFilters($args, $data);
+ $this->cacheName = 'favourites';
+ $this->cacheTtl = HOUR_IN_SECONDS;
+ 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->valid_types = array_keys(array_merge(JVB_CONTENT, JVB_TAXONOMY));
+
+ // Initialize CustomTable instances
+ $this->favourites = CustomTable::for('favourites');
+ $this->lists = CustomTable::for('favourites_lists');
+ $this->listItems = CustomTable::for('favourites_list_items');
+ $this->listShares = CustomTable::for('favourites_list_shares');
+
+ // Register hooks
+ add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
+ add_action('before_delete_post', [$this, 'cleanupPostFavourites']);
+ add_action('delete_term', [$this, 'cleanupTermFavourites'], 10, 3);
+ add_action('jvbUserRegistered', [$this, 'maybeAcceptListInvite'], 10, 3);
+ add_action('jvb_cleanupOrphanedFavourites', [$this, 'cleanupOrphanedFavourites']);
}
- /**
- * Get user's favourites with optional filtering
- *
- * @param WP_REST_Request $request Request object
- * @return WP_REST_Response Response with favourites data
- */
- public function getFavourites(WP_REST_Request $request):WP_REST_Response
- {
- $args = $this->buildParams($request);
- if (!$args['user'] || $args['user'] === ''){
- $result = [
- 'success' => false,
- 'message' => 'No user set'
- ];
+
+ public function registerRoutes(): void
+ {
+ // Favourites endpoints
+ Route::for('favourites')
+ ->get([$this, 'getFavourites'])
+ ->args([
+ 'user' => 'integer|required',
+ 'type' => 'string',
+ 'include_all' => 'boolean',
+ ])
+ ->auth(PermissionHandler::combine(['user', ['actionNonce' => 'favourites-']]))
+ ->rateLimit(30)
+ ->post([$this, 'handleFavourite'])
+ ->args([
+ 'user' => 'integer|required',
+ 'id' => 'string|required',
+ 'action' => 'string|required|enum:add,remove,toggle,batch,note',
+ 'type' => 'string',
+ 'target_id' => 'integer',
+ 'items' => 'array',
+ 'notes' => 'string',
+ ])
+ ->auth(PermissionHandler::combine(['user', ['actionNonce' => 'favourites-']]))
+ ->rateLimit(30);
+
+ // Lists endpoints
+ Route::for('favourites/lists')
+ ->get([$this, 'getLists'])
+ ->args(['user' => 'integer|required'])
+ ->auth(PermissionHandler::combine(['user', ['actionNonce' => 'favourites-']]))
+ ->rateLimit(30)
+ ->post([$this, 'handleList'])
+ ->args([
+ 'user' => 'integer|required',
+ 'id' => 'string|required',
+ 'action' => 'string|required|enum:create,update,delete,share,unshare,add_items,remove_items',
+ 'list_id' => 'integer',
+ 'name' => 'string',
+ 'items' => 'array',
+ ])
+ ->auth(PermissionHandler::combine(['user', ['actionNonce' => 'favourites-']]))
+ ->rateLimit(20);
+
+ // Favourite counts
+ Route::for('favourites/counts')
+ ->get([$this, 'getFavouriteCounts'])
+ ->args(['user' => 'integer|required'])
+ ->auth(PermissionHandler::combine(['user', ['actionNonce' => 'favourites-']]));
+ }
+
+ /**
+ * Get user's favourites with optional filtering
+ */
+ public function getFavourites(WP_REST_Request $request): WP_REST_Response
+ {
+ $user_id = absint($request->get_param('user'));
+
+ if (!$this->userCheck($user_id)) {
+ return $this->unauthorized();
}
- // Check HTTP cache headers for user-specific data
- $cache_check = $this->checkUserHeaders($request, $args['user'], 'favourites');
+
+ $args = $this->buildParams($request);
+ $key = $this->cache->generateKey($args);
+
+ // Check cache headers
+ $cache_check = $this->checkHeaders($request, $key);
if ($cache_check) {
return $cache_check;
}
- if (count($args) === 1 || (array_key_exists('all', $args) && $args['all'] === true)) {
- $result = $this->getAllFavourites($args['user']);
+ if (count($args) === 1 || ($request->get_param('include_all') === true)) {
+ $result = $this->getAllFavourites($user_id);
} else {
- $result = $this->cache->remember(
- $args,
- function() use ($args) {
- $response = new WP_REST_Response($this->getFilteredFavourites($args));
- return $this->addCacheHeaders($response);
- }
- );
+ $result = $this->cache->remember($key, function() use ($args) {
+ return $this->getFilteredFavourites($args);
+ });
}
- $response = new WP_REST_Response($result);
- return $this->addCacheHeaders($response);
- }
- protected function getFilteredFavourites(array $args):array
+ return $this->addCacheHeaders(Response::success($result));
+ }
+
+ /**
+ * Get filtered favourites using CustomTable fluent interface
+ */
+ protected function getFilteredFavourites(array $args): array
{
try {
- global $wpdb;
- $table = $wpdb->prefix . BASE . 'favourites';
-
- // Build query with proper escaping
- $query_parts = ["SELECT f.* FROM {$table} f WHERE user_id = %d"];
- $params = [$args['user']];
+ // Build base query
+ $query = $this->favourites->where(['user_id' => $args['user']]);
// Add type filter if specified
- if ($args['content'] && $args['content']!=='all') {
- $query_parts[] = "AND type = %s";
- $params[] = BASE . $args['content'];
+ if (!empty($args['content']) && $args['content'] !== 'all') {
+ $query = $this->favourites->where([
+ 'user_id' => $args['user'],
+ 'type' => BASE . $args['content']
+ ]);
}
- // Add ordering - make sure to use whitelist for column names
- if ($args['orderby'] === 'date') {
- $args['orderby'] = 'date_added';
+ // 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']]);
}
- $valid_orderby_columns = ['date_added', 'type'];
- $valid_orders = ['ASC', 'DESC'];
- $orderby = in_array($args['orderby'], $valid_orderby_columns) ? $args['orderby'] : 'date_added';
- $order = in_array(strtoupper($args['order']), $valid_orders) ? strtoupper($args['order']) : 'DESC';
- $query_parts[] = "ORDER BY {$orderby} {$order}";
+ $total_items = $count_query->countResults();
- // Add pagination
- $query_parts[] = "LIMIT %d OFFSET %d";
- $params[] = 100;
- $params[] = ($args['page'] - 1) * 100;
-
- // Execute query
- $query = implode(' ', $query_parts);
- $favourites = $wpdb->get_results($wpdb->prepare($query, $params));
-
- // Get total count for pagination
- $count_query = "SELECT COUNT(*) FROM {$table} WHERE user_id = %d";
- $count_params = [$args['user']];
-
- if ($args['content'] && $args['content'] !== 'all') {
- $count_query .= " AND type = %s";
- $count_params[] = BASE . $args['content'];
- }
-
- $total_items = (int)$wpdb->get_var($wpdb->prepare($count_query, $count_params));
-
- // Format the favourites using batch processing to reduce queries
- $formatted = $this->formatItems($favourites);
-
- // Get counts by type for filters
- $counts = $this->getFavouriteCounts($args['user']);
-
- // Prepare response data
return [
- 'items' => $formatted,
- 'has_more' => ($args['page'] * 100) < $total_items,
- 'total' => $total_items,
- 'success' => true,
+ 'items' => $this->formatItems($favourites),
+ 'has_more' => ($args['page'] * 100) < $total_items,
+ 'total' => $total_items,
+ 'success' => true,
];
} catch (Exception $e) {
- $this->logError(
- $e->getMessage(),
- [
- 'method' => 'getFilteredFavourites',
- 'args' => $args
- ]
- );
+ $this->logError('getFilteredFavourites', [
+ 'error' => $e->getMessage(),
+ 'args' => $args
+ ]);
+
return [
- 'success' => false,
- 'favourites' => [],
- 'counts' => 0,
- 'pagination' => []
+ 'success' => false,
+ 'items' => [],
+ 'total' => 0,
+ 'has_more' => false
];
}
}
- /**
- * Get all user's favourites organized by content type
- *
- * @param int $user_id User ID
- * @return WP_REST_Response Response with favourites by content type
- */
- protected function getAllFavourites(int $user_id):WP_REST_Response
- {
- if (!$this->checkUser($user_id)) {
- return new WP_REST_Response([
- 'success' => false,
- 'message' => 'User ID doesn\'t match... are you a bot?'
- ]);
- }
-
- $result = $this->cache->remember(
- 'user_'.$user_id.'_all_favourites',
- function() use ($user_id) {
- return $this->fetchAllFavourites($user_id);
- }
- );
-
- return new WP_REST_Response($result);
- }
-
- protected function fetchAllFavourites(int $user_id):array
+ /**
+ * Get all user's favourites organized by content type
+ */
+ protected function getAllFavourites(int $user_id): array
{
- try {
- global $wpdb;
- $table = $wpdb->prefix . BASE . 'favourites';
+ return $this->cache->remember($user_id, function() use ($user_id) {
+ try {
+ $favourites = $this->favourites
+ ->where(['user_id' => $user_id])
+ ->getResults();
- // Get all favourites for this user
- $query = $wpdb->prepare(
- "SELECT type, target_id FROM {$table} WHERE user_id = %d",
- $user_id
- );
-
- $favourites = $wpdb->get_results($query);
-
- // Organize by content type
- $by_type = [];
-
- foreach ($favourites as $fav) {
- $type = str_replace(BASE, '', $fav->type);
-
- if (!isset($by_type[$type])) {
- $by_type[$type] = [];
+ $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;
}
- $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'));
+
+ if (!$this->userCheck($user_id)) {
+ return $this->unauthorized();
+ }
+
+ $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') ?? ''),
+ ];
+
+ JVB()->queue()->queueOperation(
+ 'favourite_' . $action,
+ $user_id,
+ $data,
+ [
+ 'operation_id' => $operation_id,
+ 'priority' => 'high',
+ ]
+ );
+
+ return $this->queued($operation_id);
+ }
+
+ /**
+ * Get user's favourite lists
+ */
+ public function getLists(WP_REST_Request $request): WP_REST_Response
+ {
+ $user_id = absint($request->get_param('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'));
+ }
+
+ $key = $this->listsCache->generateKey($params);
+
+ // Check cache headers
+ $cache_check = $this->checkHeaders($request, $key);
+ if ($cache_check) {
+ return $cache_check;
+ }
+
+ $list_id = $request->get_param('id');
+ $response = $list_id
+ ? $this->getListDetails($list_id, $user_id)
+ : $this->getAvailableLists($user_id);
+
+ 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 [];
+ }
+ });
+ }
+
+ /**
+ * Get favourite counts by type
+ */
+ public function getFavouriteCounts(WP_REST_Request $request): WP_REST_Response
+ {
+ $user_id = absint($request->get_param('user'));
+
+ if (!$this->userCheck($user_id)) {
+ return $this->unauthorized();
+ }
+
+ $key = "counts_{$user_id}";
+
+ $counts = $this->cache->remember($key, function() use ($user_id) {
+ try {
+ // Get counts grouped by type using raw query
+ $results = $this->favourites->queryResults(
+ "SELECT type, COUNT(*) as count FROM {table} WHERE user_id = %d GROUP BY type",
+ [$user_id],
+ OBJECT_K
+ );
+
+ $all_counts = array_fill_keys(
+ array_map(fn($type) => str_replace(BASE, '', $type), array_keys($this->valid_types)),
+ 0
+ );
+
+ foreach ($results as $type => $data) {
+ $type_key = str_replace(BASE, '', $type);
+ $all_counts[$type_key] = (int)$data->count;
+ }
+
+ return $all_counts;
+
+ } catch (Exception $e) {
+ $this->logError('getFavouriteCounts', [
+ 'error' => $e->getMessage(),
+ 'user_id' => $user_id
+ ]);
+
+ return array_fill_keys(array_keys($this->valid_types), 0);
+ }
+ });
+
+ return Response::success(['counts' => $counts]);
+ }
+
+ /**
+ * Process favourite operations using transactions
+ */
+ public function processOperation($result, object $operation, array $data)
+ {
+ $action_map = [
+ 'favourite_add' => 'addFavourite',
+ 'favourite_remove' => 'removeFavourite',
+ 'favourite_batch' => 'batchFavourites',
+ 'favourite_note' => 'processNote',
+ 'favourite_list_create' => 'createList',
+ 'favourite_list_update' => 'updateList',
+ 'favourite_list_delete' => 'deleteList',
+ 'favourite_list_add_items' => 'addToList',
+ 'favourite_list_remove_items' => 'removeFromList',
+ 'favourite_list_share' => 'shareList',
+ 'favourite_list_unshare' => 'unshareList',
+ ];
+
+ if (!isset($action_map[$operation->type])) {
+ return $result;
+ }
+
+ try {
+ $method = $action_map[$operation->type];
+ $response = $this->$method($operation->user_id, $data);
+
+ // Clear cache on success
+ if ($response['success'] ?? false) {
+ Cache::invalidateItem('favourites', $operation->user_id);
+ $this->listsCache->flush();
+ $this->sharedListsCache->flush();
+ }
+
+ return $response;
+ } catch (Exception $e) {
+ $this->logError('processOperation', [
+ 'error' => $e->getMessage(),
+ 'operation_id' => $operation->id,
+ 'type' => $operation->type
+ ]);
+
+ return [
+ 'success' => false,
+ 'result' => $e->getMessage()
+ ];
+ }
+ }
+
+ /**
+ * Add a favourite using findOrCreate pattern
+ */
+ protected function addFavourite(int $user_id, array $data): array
+ {
+ $type = $data['type'];
+ $target_id = $data['target_id'];
+
+ if (!str_starts_with($type, BASE)) {
+ $type = BASE . $type;
+ }
+
+ if (!isset($this->valid_types[$type])) {
+ return ['success' => false, 'result' => 'Invalid type'];
+ }
+
+ // Use findOrCreate pattern
+ $result = $this->favourites->findOrCreate([
+ 'user_id' => $user_id,
+ 'type' => $type,
+ 'target_id' => $target_id
+ ]);
+
+ if ($result['created']) {
+ $this->updateFavouriteCount($type, $target_id);
+ $this->maybeNotifyOwner($type, $target_id, $user_id);
+ }
+
+ return [
+ 'success' => true,
+ 'result' => [
+ 'action' => $result['created'] ? 'added' : 'already_exists',
+ 'favourite_id' => $result['id'],
+ 'count' => $this->getFavouriteCount($type, $target_id)
+ ]
+ ];
+ }
+
+ /**
+ * Remove a favourite
+ */
+ protected function removeFavourite(int $user_id, array $data): array
+ {
+ $type = $data['type'];
+ $target_id = $data['target_id'];
+
+ if (!str_starts_with($type, BASE)) {
+ $type = BASE . $type;
+ }
+
+ $deleted = $this->favourites->where([
+ 'user_id' => $user_id,
+ 'type' => $type,
+ 'target_id' => $target_id
+ ])->deleteResults();
+
+ if ($deleted) {
+ $this->updateFavouriteCount($type, $target_id);
+ $this->removeRelatedNotifications($type, $target_id, $user_id);
+ }
+
+ return [
+ 'success' => true,
+ 'result' => [
+ 'action' => $deleted ? 'removed' : 'not_found',
+ 'count' => $this->getFavouriteCount($type, $target_id)
+ ]
+ ];
+ }
+
+ /**
+ * Batch favourites using transaction
+ */
+ protected function batchFavourites(int $user_id, array $data): array
+ {
+ $items = $data['items'] ?? [];
+
+ if (empty($items)) {
+ return ['success' => false, 'result' => 'No items provided'];
+ }
+
+ return $this->favourites->transaction(function($table) use ($user_id, $items) {
+ $results = [
+ 'added' => 0,
+ 'removed' => 0,
+ 'errors' => []
+ ];
+
+ foreach ($items as $item) {
+ try {
+ $type = BASE . ($item['type'] ?? '');
+ $target_id = absint($item['target_id'] ?? 0);
+ $action = $item['action'] ?? 'add';
+
+ if ($action === 'add') {
+ $result = $table->findOrCreate([
+ 'user_id' => $user_id,
+ 'type' => $type,
+ 'target_id' => $target_id
+ ]);
+ if ($result['created']) $results['added']++;
+ } else {
+ $deleted = $table->where([
+ 'user_id' => $user_id,
+ 'type' => $type,
+ 'target_id' => $target_id
+ ])->deleteResults();
+ if ($deleted) $results['removed']++;
+ }
+
+ $this->updateFavouriteCount($type, $target_id);
+ } catch (Exception $e) {
+ $results['errors'][] = $item['target_id'] ?? 'unknown';
+ }
}
return [
'success' => true,
- 'items' => $by_type,
- 'has_more' => false,
+ 'result' => $results
];
+ });
+ }
+
+ /**
+ * Create a list using transaction
+ */
+ protected function createList(int $user_id, array $data): array
+ {
+ $name = sanitize_text_field($data['name'] ?? 'Untitled List');
+ $description = sanitize_textarea_field($data['description'] ?? '');
+ $items = $data['items'] ?? [];
+
+ return $this->lists->transaction(function($table) use ($user_id, $name, $description, $items) {
+ $list_id = $table->create([
+ 'user_id' => $user_id,
+ 'name' => $name,
+ 'description' => $description,
+ ]);
+
+ if (!$list_id) {
+ throw new Exception('Failed to create list');
+ }
+
+ $added_count = 0;
+ if (!empty($items)) {
+ $result = $this->addItemsToList($list_id, $items, $user_id);
+ $added_count = $result['added'];
+ }
+
+ return [
+ 'success' => true,
+ 'result' => [
+ 'list_id' => $list_id,
+ 'name' => $name,
+ 'added_items' => $added_count,
+ ]
+ ];
+ });
+ }
+
+ /**
+ * Add items to list with bulk insert
+ */
+ protected function addItemsToList(int $list_id, array $items, int $user_id): array
+ {
+ if (empty($items)) {
+ return ['added' => 0, 'errors' => []];
+ }
+
+ $added = 0;
+ $errors = [];
+
+ try {
+ // Group items by type
+ $items_by_type = [];
+ foreach ($items as $item) {
+ if (empty($item['type']) || !isset($item['target_id'])) {
+ $errors[] = ['message' => 'Item missing type or target_id', 'item' => $item];
+ continue;
+ }
+
+ $type = str_starts_with($item['type'], BASE)
+ ? $item['type']
+ : BASE . $item['type'];
+
+ if (!isset($items_by_type[$type])) {
+ $items_by_type[$type] = [];
+ }
+
+ $items_by_type[$type][] = absint($item['target_id']);
+ }
+
+ foreach ($items_by_type as $type => $target_ids) {
+ // Get existing items to avoid duplicates
+ $existing = $this->listItems
+ ->where(['list_id' => $list_id, 'item_type' => $type])
+ ->getResults();
+
+ $existing_map = [];
+ foreach ($existing as $item) {
+ $existing_map[$item->item_id] = true;
+ }
+
+ // Get favourite IDs in bulk
+ $favs = $this->favourites
+ ->where(['user_id' => $user_id, 'type' => $type])
+ ->getResults();
+
+ $fav_map = [];
+ foreach ($favs as $fav) {
+ $fav_map[$fav->target_id] = $fav->id;
+ }
+
+ // Prepare items for bulk insert
+ $to_insert = [];
+ foreach ($target_ids as $target_id) {
+ if (isset($existing_map[$target_id])) {
+ continue;
+ }
+
+ $to_insert[] = [
+ 'list_id' => $list_id,
+ 'item_type' => $type,
+ 'item_id' => $target_id,
+ 'favourite_id' => $fav_map[$target_id] ?? null
+ ];
+ }
+
+ // Bulk insert
+ if (!empty($to_insert)) {
+ $columns = ['list_id', 'item_type', 'item_id', 'favourite_id'];
+ $result = $this->listItems->bulkInsert($to_insert, $columns);
+ $added += $result;
+ }
+ }
+
+ // Update list timestamp
+ $this->lists->where(['id' => $list_id])->updateResults([]);
+
+ return ['added' => $added, 'errors' => $errors];
} catch (Exception $e) {
- $this->logError(
- $e->getMessage(),
- [
- 'context' => 'fetchAllFavourites',
- 'user' => $user_id
- ]
- );
- return [
- 'success' => false,
- 'favourites' => [],
- ];
+ $this->logError('addItemsToList', [
+ 'error' => $e->getMessage(),
+ 'user_id' => $user_id,
+ 'list_id' => $list_id,
+ ]);
+
+ $errors[] = ['message' => $e->getMessage()];
+ return ['added' => 0, 'errors' => $errors];
}
}
- /**
- * Handle favourite operations (toggle, notes update)
- *
- * @param WP_REST_Request $request Request object
- * @return WP_REST_Response Response with operation result
- */
- public function handleFavouriteOperation(WP_REST_Request $request): WP_REST_Response
- {
- $data = $request->get_json_params();
- $operation = $data['operation'] ?? 'toggle';
- $user_id = get_current_user_id();
+ /**
+ * Clean up favourites when a post is deleted
+ */
+ public function cleanupPostFavourites(int $post_id): void
+ {
+ try {
+ $type = get_post_type($post_id);
+ if (!$type) return;
- $queue = JVB()->queue();
- $operation_id = $data['id'] ?? uniqid('fav_');
+ $type = BASE . $type;
- error_log('Favourite Request: '.print_r($data, true));
+ // Delete using fluent interface
+ $this->favourites->where([
+ 'type' => $type,
+ 'target_id' => $post_id
+ ])->deleteResults();
- switch ($operation) {
- case 'toggle':
- $adds = $request->get_param('adds') ?? [];
- $removes = $request->get_param('removes') ?? [];
+ $this->listItems->where([
+ 'item_type' => $type,
+ 'item_id' => $post_id
+ ])->deleteResults();
- $queue->queueOperation(
- 'favourites_batch',
- $request->get_param('user'),
- [
- 'adds' => $adds,
- 'removes' => $removes
- ],
- [
- 'count' => count($adds) + count($removes),
- 'chunk_key' => ['adds', 'removes'],
- 'chunk_size' => 20,
- 'priority' => 'normal',
- 'operation_id' => $request->get_param('id')
- ]
- );
-
- break;
-
- case 'update_notes':
- // Handle notes update
- if (!array_key_exists('target_id', $data)) {
- return $this->createErrorResponse(
- self::ERROR_MISSING_PARAMS,
- 'Type and target ID are required',
- 400
- );
- }
-
- $ids = explode(',', $data['target_id']);
- foreach ($ids as $key => $id) {
- $ids[$key] = (int)$id;
- }
- $ids = implode(',', $ids);
-
- $queue->queueOperation(
- 'favourite_notes',
- $user_id,
- [
- 'target_id' => $ids,
- 'notes' => sanitize_textarea_field($data['notes'] ?? '')
- ],
- [
- 'count' => 1,
- 'operation_id' => $operation_id
- ]
- );
-
- break;
-
- default:
- return $this->createErrorResponse(
- self::ERROR_INVALID_OPERATION,
- 'Invalid operation',
- 400
- );
- }
-
- return new WP_REST_Response([
- 'success' => true,
- 'message' => __('Operation queued', 'jvb'),
- 'operation_id' => $operation_id,
- 'queue_status' => $queue->getQueueStatus()
- ]);
- }
-
- /**
- * Get user's favourite lists
- *
- * @param WP_REST_Request $request Request object
- * @return WP_REST_Response Response with lists data
- */
- public function getLists(WP_REST_Request $request):WP_REST_Response
- {
- $user_id = get_current_user_id();
-
- if (!$user_id || !$this->userCheck($user_id)) {
- return new WP_REST_Response([
- 'success' => false,
- 'message' => 'Invalid user'
+ } catch (Exception $e) {
+ $this->logError('cleanupPostFavourites', [
+ 'error' => $e->getMessage(),
+ 'post_id' => $post_id
]);
}
+ }
- // Check HTTP cache headers
- $cache_check = $this->checkUserHeaders($request, $user_id, 'favourites_lists');
- if ($cache_check) {
- return $cache_check;
- }
+ /**
+ * Clean up favourites when a term is deleted
+ */
+ public function cleanupTermFavourites(int $term_id, int $tt_id, string $taxonomy): void
+ {
+ try {
+ if (!isset($this->valid_types[$taxonomy])) {
+ return;
+ }
- $list_id = $request->get_param('id');
+ // Delete using fluent interface
+ $this->favourites->where([
+ 'type' => $taxonomy,
+ 'target_id' => $term_id
+ ])->deleteResults();
- if ($list_id) {
- $response = $this->getListDetails($list_id, $user_id);
- } else {
- $response = $this->getAvailableLists($user_id);
- }
+ $this->listItems->where([
+ 'item_type' => $taxonomy,
+ 'item_id' => $term_id
+ ])->deleteResults();
- $response = new WP_REST_Response($response);
- return $this->addCacheHeaders($response);
- }
- /**
- * Get lists available to a user (owned and shared)
- *
- * @param int $user_id User ID
- * @param bool $include_shared Include lists shared with user
- * @return array Lists data
- */
- public function getAvailableLists(int $user_id, bool $include_shared = true):array
- {
- if (!$this->checkUser($user_id)) {
- return [];
- }
- $key = sprintf(
- 'user_%d_lists',
- $user_id
- );
- if ($include_shared) {
- $key = $key.'_shared';
- }
- $cache = $this->cache->get($key, 'favourites_lists');
- if ($cache) {
- return $cache;
- }
-
- global $wpdb;
- error_log('Attempting to get available lists..');
- $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
- $items_table = $wpdb->prefix . BASE . 'favourites_list_items';
- $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
-
- try {
- // Get owned lists
- $owned_query = $wpdb->prepare(
- "SELECT l.*,
- COUNT(DISTINCT i.id) as item_count,
- TRUE as is_owner,
- FALSE as is_shared
- FROM {$lists_table} l
- LEFT JOIN {$items_table} i ON l.id = i.list_id
- WHERE l.user_id = %d
- GROUP BY l.id
- ORDER BY l.created_at DESC",
- $user_id
- );
-
- $lists = $wpdb->get_results($owned_query);
- error_log('Lists result: '.print_r($lists, true));
-
- // Add shared lists if requested
- if ($include_shared) {
- $shared_query = $wpdb->prepare(
- "SELECT l.*,
- u.display_name as owner_name,
- COUNT(DISTINCT i.id) as item_count,
- s.permission_type,
- FALSE as is_owner,
- TRUE as is_shared
- FROM {$lists_table} l
- JOIN {$shares_table} s ON l.id = s.list_id
- JOIN {$wpdb->users} u ON l.user_id = u.ID
- LEFT JOIN {$items_table} i ON l.id = i.list_id
- WHERE s.user_id = %d
- GROUP BY l.id
- ORDER BY l.created_at DESC",
- $user_id
- );
-
- $shared_lists = $wpdb->get_results($shared_query);
- error_log('Shared lists: '.print_r($shared_lists, true));
- $lists = [
- 'owned' => $lists,
- 'shared'=> $shared_lists,
- ];
- }
-
- // Cache result
- $this->cache->set($key, ['success' => true, 'lists'=>$lists], 'favourites_lists');
-
- error_log('Lists: '.print_r($lists, true));
- return [
- 'success' => true,
- 'lists' => $lists
- ];
- } catch (Exception $e) {
- JVB()->error()->log(
- 'favourites',
- 'Error getting available lists: ' . $e->getMessage(),
- ['user_id' => $user_id],
- 'error'
- );
-
- return [];
- }
- }
-
- /**
- * Get detailed information for a single list
- *
- * @param int $list_id List ID
- * @param int $user_id User ID
- * @return WP_REST_Response|WP_Error Response with list details or error
- */
- protected function getListDetails(int $list_id, int $user_id):WP_REST_Response
- {
- $key = sprintf(
- 'user_%d_list_%d',
- $user_id,
- $list_id
- );
- $cache = $this->cache->get($key, 'favourites_lists');
- if ($cache) {
- return new WP_REST_Response($cache);
- }
- //No cache, build it again
- global $wpdb;
- $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
- $items_table = $wpdb->prefix . BASE . 'favourites_list_items';
- $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
- $pending_shares_table = $wpdb->prefix . BASE . 'favourites_pending_shares';
-
- // Check if user has access to this list
- $list_access = $wpdb->get_row($wpdb->prepare("
- SELECT l.*,
- CASE WHEN l.user_id = %d THEN 1 ELSE 0 END as is_owner,
- CASE WHEN s.id IS NOT NULL THEN 1 ELSE 0 END as is_shared
- FROM {$lists_table} l
- LEFT JOIN {$shares_table} s ON l.id = s.list_id AND s.user_id = %d
- WHERE l.id = %d
- ", $user_id, $user_id, $list_id));
-
- if (!$list_access || (!$list_access->is_owner && !$list_access->is_shared)) {
- return $this->createErrorResponse(
- self::ERROR_ACCESS_DENIED,
- 'You do not have access to this list',
- 403
- );
- }
-
- // Get list items
- $items = $wpdb->get_results($wpdb->prepare("
- SELECT i.*, f.user_id, f.type, f.target_id, f.notes
- FROM {$items_table} i
- LEFT JOIN {$wpdb->prefix}" . BASE . "favourites f
- ON i.favourite_id = f.id
- WHERE i.list_id = %d
- ORDER BY i.added_at DESC
- ", $list_id));
-
- // Format items using batch processing to reduce queries
- $formatted_items = [];
- $dummy_items = [];
- $favourite_items = [];
-
- foreach ($items as $item) {
- if ($item->favourite_id === null) {
- // Create a dummy favourite object
- $dummy_items[] = (object)[
- 'type' => $item->item_type,
- 'target_id' => $item->item_id,
- 'date_added' => $item->added_at
- ];
- } else {
- // Use the joined favourite data
- $favourite_items[] = (object)[
- 'id' => $item->favourite_id,
- 'user_id' => $item->user_id,
- 'type' => $item->type,
- 'target_id' => $item->target_id,
- 'notes' => $item->notes,
- 'date_added' => $item->added_at
- ];
- }
- }
-
- // Process items in batches to reduce DB queries
- $formatted_dummy_items = $this->formatItems($dummy_items);
- $formatted_favourite_items = $this->formatItems($favourite_items);
-
- // Combine formatted items
- $formatted_items = array_merge($formatted_dummy_items, $formatted_favourite_items);
-
- // Get shared users if owner
- $shared_users = [];
- if ($list_access->is_owner) {
- // Get active shares
- $active_shares = $wpdb->get_results($wpdb->prepare("
- SELECT s.*, u.user_email as email, u.display_name
- FROM {$shares_table} s
- JOIN {$wpdb->users} u ON s.user_id = u.ID
- WHERE s.list_id = %d
- ", $list_id));
-
- // Get pending shares
- $pending_shares = $wpdb->get_results($wpdb->prepare("
- SELECT * FROM {$pending_shares_table}
- WHERE list_id = %d
- ", $list_id));
-
- // Format shared users
- foreach ($active_shares as $share) {
- $shared_users[] = [
- 'email' => $share->email,
- 'name' => $share->display_name,
- 'permission_type' => $share->permission_type,
- 'date_added' => $share->created_at,
- 'status' => 'active'
- ];
- }
-
- foreach ($pending_shares as $share) {
- $shared_users[] = [
- 'email' => $share->email,
- 'invitation_token' => $share->invitation_token,
- 'date_added' => $share->created_at,
- 'status' => 'pending'
- ];
- }
- }
-
- // Prepare response data
- $response_data = [
- 'success' => true,
- 'list' => [
- 'id' => (int)$list_access->id,
- 'name' => $list_access->name,
- 'description' => $list_access->description,
- 'created_at' => $list_access->created_at,
- 'is_owner' => (bool)$list_access->is_owner,
- 'is_shared' => (bool)$list_access->is_shared,
- 'items' => $formatted_items,
- 'shared_users' => $shared_users
- ]
- ];
-
- $this->cache->set($key, $response_data, 'favourites_lists');
- return new WP_REST_Response($response_data);
- }
-
- /**
- * Handle list operations (create, update, delete, add/remove items)
- *
- * @param WP_REST_Request $request Request object
- * @return WP_REST_Response Response with operation result
- */
- public function handleListOperation(WP_REST_Request $request):WP_REST_Response
- {
- $data = $request->get_json_params();
- $operation = $data['operation'] ?? '';
- $user_id = get_current_user_id();
-
- // Get queue system
- $queue = JVB()->queue();
- $operation_id = (array_key_exists('id', $data)) ? $data['id'] : uniqid('list_');
-
- // Process based on operation type
- switch ($operation) {
- case 'create':
- // Create new list
- if (!array_key_exists('name', $data)) {
- return $this->createErrorResponse(
- self::ERROR_MISSING_PARAMS,
- 'List name is required',
- 400
- );
- }
-
- $queue->queueOperation(
- 'favourite_list_create',
- $user_id,
- [
- 'name' => sanitize_text_field($data['name']),
- 'description' => sanitize_textarea_field($data['description'] ?? ''),
- 'items' => $data['items'] ?? []
- ],
- [
- 'count' => 1,
- 'operation_id' => $operation_id
- ]
- );
- break;
-
- case 'update':
- // Update list
- if (!array_key_exists('list_id', $data)) {
- return $this->createErrorResponse(
- self::ERROR_MISSING_PARAMS,
- 'List ID is required',
- 400
- );
- }
-
- $queue->queueOperation(
- 'favourite_list_update',
- $user_id,
- [
- 'list_id' => (int)$data['list_id'],
- 'name' => $data['name'] ?? null,
- 'description' => $data['description'] ?? null
- ],
- [
- 'count' => 1,
- 'operation_id' => $operation_id
- ]
- );
- break;
-
- case 'delete':
- // Delete list
- if (!array_key_exists('list_id', $data)) {
- return $this->createErrorResponse(
- self::ERROR_MISSING_PARAMS,
- 'List ID is required',
- 400
- );
- }
-
- $queue->queueOperation(
- 'favourite_list_delete',
- $user_id,
- [
- 'list_id' => (int)$data['list_id']
- ],
- [
- 'count' => 1,
- 'operation_id' => $operation_id
- ]
- );
- break;
-
- case 'add_items':
- // Add items to list
- if (!array_key_exists('list_id', $data) || !array_key_exists('items', $data)) {
- return $this->createErrorResponse(
- self::ERROR_MISSING_PARAMS,
- 'List ID and items are required',
- 400
- );
- }
-
- $queue->queueOperation(
- 'favourite_list_add',
- $user_id,
- [
- 'list_id' => (int)$data['list_id'],
- 'items' => $data['items']
- ],
- [
- 'count' => 1,
- 'chunk_key' => 'items',
- 'chunk_size' => 20,
- 'operation_id' => $operation_id
- ]
- );
- break;
-
- case 'remove_items':
- // Remove items from list
- if (!array_key_exists('list_id', $data) || !array_key_exists('items', $data)) {
- return $this->createErrorResponse(
- self::ERROR_MISSING_PARAMS,
- 'List ID and items are required',
- 400
- );
- }
-
- $queue->queueOperation(
- 'favourite_list_remove',
- $user_id,
- [
- 'list_id' => (int)$data['list_id'],
- 'items' => $data['items']
- ],
- [
- 'count' => 1,
- 'chunk_key' => 'items',
- 'chunk_size' => 20,
- 'operation_id' => $operation_id
- ]
- );
- break;
-
- default:
- return $this->createErrorResponse(
- self::ERROR_INVALID_OPERATION,
- 'Invalid list operation',
- 400
- );
- }
-
-
- return new WP_REST_Response([
- 'success' => true,
- 'message' => __('List operation queued', 'jvb'),
- 'operation_id' => $operation_id,
- 'queue_status' => $queue->getQueueStatus()
- ]);
- }
-
- /**
- * Get shares for a list
- *
- * @param WP_REST_Request $request Request object
- * @return WP_REST_Response Response with shares data
- */
- public function getShares(WP_REST_Request $request):WP_REST_Response
- {
- $user_id = $request->get_param('user');
-
- if (!$user_id || !$this->userCheck($user_id)) {
- return new WP_REST_Response([
- 'success' => false,
- 'message' => 'Invalid user'
+ } catch (Exception $e) {
+ $this->logError('cleanupTermFavourites', [
+ 'error' => $e->getMessage(),
+ 'term_id' => $term_id,
+ 'taxonomy' => $taxonomy
]);
}
+ }
- // Check HTTP cache headers
- $cache_check = $this->checkUserHeaders($request, $user_id, 'favourites_shares');
- if ($cache_check) {
- return $cache_check;
+ /**
+ * Cleanup orphaned favourites using CustomTable query method
+ */
+ public function cleanupOrphanedFavourites(): bool
+ {
+ try {
+ // Delete favourites for non-existent users
+ $this->favourites->query(
+ "DELETE f FROM {table} f
+ LEFT JOIN {$GLOBALS['wpdb']->users} u ON f.user_id = u.ID
+ WHERE u.ID IS NULL"
+ );
+
+ // Delete favourites for non-existent posts
+ $post_types = array_filter(
+ array_keys($this->valid_types),
+ fn($type) => $this->valid_types[$type]['table'] === 'post'
+ );
+
+ foreach ($post_types as $type) {
+ $this->favourites->query(
+ "DELETE f FROM {table} f
+ LEFT JOIN {$GLOBALS['wpdb']->posts} p ON f.target_id = p.ID
+ WHERE f.type = %s AND p.ID IS NULL",
+ [$type]
+ );
+ }
+
+ return true;
+
+ } catch (Exception $e) {
+ $this->logError('cleanupOrphanedFavourites', [
+ 'error' => $e->getMessage()
+ ]);
+
+ return false;
}
- $list_id = $request->get_param('list_id');
+ }
- if (!$list_id) {
- return $this->createErrorResponse(
- self::ERROR_MISSING_PARAMS,
- 'List ID is required',
- 400
- );
- }
+ /**
+ * Helper methods
+ */
+ protected function buildParams(WP_REST_Request $request): array
+ {
+ $data = $request->get_params();
+ $args = ['user' => absint($data['user'])];
- $key = sprintf(
- 'user_%d_shares_for_list_%d',
- $user_id,
- $list_id
- );
- $cache = $this->cache->get($key, 'favourites_list_shares');
- if ($cache) {
- return new WP_REST_Response($cache);
- }
+ if (!array_key_exists('page', $data)) {
+ return $args;
+ }
- try {
- global $wpdb;
- $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
- $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
+ $args = array_merge($args, [
+ 'page' => max(1, absint($data['page'] ?? 1)),
+ 'content' => $this->checkContent($data['content'] ?? 'all')
+ ]);
- // Verify ownership
- $is_owner = $wpdb->get_var($wpdb->prepare(
- "SELECT 1 FROM {$lists_table} WHERE id = %d AND user_id = %d",
- $list_id,
- $user_id
- ));
+ return $this->applyOrderFilters($args, $data);
+ }
- if (!$is_owner) {
- return $this->createErrorResponse(
- self::ERROR_ACCESS_DENIED,
- 'You do not own this list',
- 403
- );
- }
- // Get all shares (both active and pending) in one query
- $query = $wpdb->prepare(
- "SELECT s.*,
- CASE
- WHEN s.user_id IS NOT NULL THEN u.display_name
- ELSE NULL
- END as display_name,
- CASE
- WHEN s.user_id IS NOT NULL THEN u.user_email
- ELSE s.email
- END as email
- FROM {$shares_table} s
- LEFT JOIN {$wpdb->users} u ON s.user_id = u.ID
- WHERE s.list_id = %d
- ORDER BY s.status, s.created_at DESC",
- $list_id
- );
+ /**
+ * Batch format a collection of favourite items
+ *
+ * @param array $items Collection of favourite items
+ * @return array Formatted items
+ */
+ protected function formatItems(array $items):array
+ {
+ if (empty($items)) {
+ return [];
+ }
- $all_shares = $wpdb->get_results($query);
+ // Group items by type to reduce queries
+ $items_by_type = [];
+ foreach ($items as $item) {
+ if (!isset($item->type) || !isset($item->target_id)) {
+ continue;
+ }
- // Format shares for response
- $shares = [];
- foreach ($all_shares as $share) {
- $formatted_share = [
- 'id' => $share->id,
- 'email' => $share->email,
- 'status' => $share->status,
- 'date_added' => $share->created_at,
- ];
+ // Verify item type is valid
+ if (!isset($this->valid_types[$item->type])) {
+ continue;
+ }
- // Add attributes specific to the status
- if ($share->status === 'accepted') {
- $formatted_share['name'] = $share->display_name;
- $formatted_share['user_id'] = $share->user_id;
- $formatted_share['permission_type'] = $share->permission_type;
- } else if ($share->status === 'pending') {
- // Include invitation token if needed for managing invitations
- $formatted_share['invitation_token'] = $share->invitation_token;
- }
+ $type = $item->type;
+ if (!isset($items_by_type[$type])) {
+ $items_by_type[$type] = [];
+ }
+ $items_by_type[$type][] = $item;
+ }
- $shares[] = $formatted_share;
- }
+ $formatted = [];
- $response_data = [
- 'success' => true,
- 'list_id' => $list_id,
- 'shares' => $shares
- ];
+ // Process post-type items in batches
+ foreach ($items_by_type as $type => $type_items) {
+ $config = $this->valid_types[$type];
- // Cache the results
- $this->cache->set($key, $response_data, 'favourites_list_shares');
+ if ($config['table'] === 'post') {
+ $formatted = array_merge($formatted, $this->formatPostFavourites($type_items));
+ } else {
+ $formatted = array_merge($formatted, $this->formatTermFavourites($type_items));
+ }
+ }
- $response = new WP_REST_Response($response_data);
- return $this->addCacheHeaders($response);
+ return $formatted;
+ }
+ protected function getFavouriteCount(string $type, int $target_id): int
+ {
+ return $this->favourites->where([
+ 'type' => $type,
+ 'target_id' => $target_id
+ ])->countResults();
+ }
- } catch (Exception $e) {
- return $this->createErrorResponse(
- self::ERROR_PROCESSING,
- 'Error retrieving shares: ' . $e->getMessage(),
- 500,
- ['user_id' => $user_id, 'list_id' => $list_id]
- );
- }
- }
+ protected function updateFavouriteCount(string $type, int $target_id): void
+ {
+ $count = $this->getFavouriteCount($type, $target_id);
- /**
- * Handle share operations (add/remove)
- *
- * @param WP_REST_Request $request Request object
- * @return WP_REST_Response Response with operation result
- */
- public function handleShare(WP_REST_Request $request):WP_REST_Response
- {
- $data = $request->get_json_params();
- $operation = $data['operation'] ?? '';
- $user_id = get_current_user_id();
+ if (str_contains($type, 'post') || in_array($type, array_keys($this->valid_types))) {
+ update_post_meta($target_id, BASE.'favourite_count', $count);
+ } else {
+ update_term_meta($target_id, BASE.'favourite_count', $count);
+ }
+ }
- $queue = JVB()->queue();
- $operation_id = $data['id'] ?? uniqid('share_');
+ /**
+ * Notify content owner of new favourite if configured
+ *
+ * @param string $type Content type
+ * @param int $target_id Content ID
+ * @param int $user_id User who favourited
+ * @return void
+ */
+ protected function maybeNotifyOwner(string $type, int $target_id, int $user_id): void
+ {
+ try {
+ $owner_id = $this->getContentOwner($type, $target_id);
- if (empty($data['list_id']) || empty($data['email'])) {
- return $this->createErrorResponse(
- self::ERROR_MISSING_PARAMS,
- 'List ID and email are required',
- 400
- );
- }
+ if ($owner_id && $owner_id !== $user_id) {
+ JVB()->notification()->addNotification(
+ $owner_id,
+ 'new_favourite',
+ [
+ 'user_id' => $user_id,
+ 'type' => $type,
+ 'target_id' => $target_id
+ ]
+ );
+ }
+ } catch (Exception $e) {
+ // Silent fail - notifications are non-critical
+ }
+ }
- switch ($operation) {
- case 'add':
- // Share list with email
- if (!is_email($data['email'])) {
- return $this->createErrorResponse(
- self::ERROR_MISSING_PARAMS,
- 'Invalid email address',
- 400
- );
- }
+ /**
+ * Remove any existing notifications about a favorite action
+ *
+ * @param int $user_id User who removed the favorite
+ * @param string $type Content type
+ * @param int $target_id Content ID
+ * @return void
+ */
+ protected function removeRelatedNotifications(int $user_id, string $type, int $target_id):void
+ {
+ try {
+ // Get the content owner(s)
+ $owner_ids = $this->getContentOwner($type, $target_id);
+ if (!$owner_ids) {
+ return;
+ }
- $queue->queueOperation(
- 'favourite_list_share',
- $user_id,
- [
- 'list_id' => (int)$data['list_id'],
- 'email' => sanitize_email($data['email']),
- 'permission_type' => in_array($data['permission_type'] ?? '', ['view', 'edit'])
- ? $data['permission_type']
- : 'view'
- ],
- [
- 'count' => 1,
- 'operation_id' => $operation_id
- ]
- );
- break;
+ $owner_ids = (is_array($owner_ids)) ? $owner_ids : [$owner_ids];
- case 'remove':
- // Remove share
- $queue->queueOperation(
- 'favourite_list_unshare',
- $user_id,
- [
- 'list_id' => (int)$data['list_id'],
- 'email' => sanitize_email($data['email'])
- ],
- [
- 'count' => 1,
- 'operation_id' => $operation_id
- ]
- );
- break;
+ foreach ($owner_ids as $owner_id) {
+ // Skip if owner is the same as the user who unfavorited
+ if ($owner_id === $user_id) {
+ continue;
+ }
- case 'accept':
- $data = $request->get_json_params();
- $token = $data['token'] ?? '';
- $email = sanitize_email($data['email'] ?? '');
- $user_id = get_current_user_id(); // Get current user if logged in
+ global $wpdb;
+ $notifications_table = $wpdb->prefix . BASE . 'notifications';
- $result = $this->acceptListInvitation($token, $email, $user_id > 0 ? $user_id : null);
- break;
- default:
- return $this->createErrorResponse(
- self::ERROR_INVALID_OPERATION,
- 'Invalid share operation',
- 400
- );
- }
-
-
- return new WP_REST_Response([
- 'success' => true,
- 'message' => __('Share operation queued', 'jvb'),
- 'operation_id' => $operation_id,
- 'queue_status' => $queue->getQueueStatus()
- ]);
- }
-
- /**
- * Batch format a collection of favourite items
- *
- * @param array $items Collection of favourite items
- * @return array Formatted items
- */
- protected function formatItems(array $items):array
- {
- if (empty($items)) {
- return [];
- }
-
- // Group items by type to reduce queries
- $items_by_type = [];
- foreach ($items as $item) {
- if (!isset($item->type) || !isset($item->target_id)) {
- continue;
- }
-
- // Verify item type is valid
- if (!isset($this->valid_types[$item->type])) {
- continue;
- }
-
- $type = $item->type;
- if (!isset($items_by_type[$type])) {
- $items_by_type[$type] = [];
- }
- $items_by_type[$type][] = $item;
- }
-
- $formatted = [];
-
- // Process post-type items in batches
- foreach ($items_by_type as $type => $type_items) {
- $config = $this->valid_types[$type];
-
- if ($config['table'] === 'post') {
- $formatted = array_merge($formatted, $this->formatPostFavourites($type_items));
- } else {
- $formatted = array_merge($formatted, $this->formatTermFavourites($type_items));
- }
- }
-
- return $formatted;
- }
-
- /**
- * Batch format post-type favourites to reduce queries
- *
- * @param array $items Collection of favourite items of post type
- * @return array Formatted items
- */
- protected function formatPostFavourites(array $items):array
- {
- if (empty($items)) {
- return [];
- }
-
- $formatted = [];
- $post_ids = array_map(function ($item) {
- return (int)$item->target_id;
- }, $items);
-
- // Get all posts in one query
- $posts = get_posts([
- 'post__in' => $post_ids,
- 'post_type' => 'any',
- 'posts_per_page' => -1,
- 'post_status' => 'any',
- ]);
-
- // Create a lookup map
- $posts_by_id = [];
- foreach ($posts as $post) {
- $posts_by_id[$post->ID] = $post;
- }
-
- // Get all thumbnails for artists in one query if needed
- $artist_ids = [];
- foreach ($items as $item) {
- if ($item->type === BASE.'artist') {
- $artist_ids[] = (int)$item->target_id;
- }
- }
-
- $artist_images = [];
- if (!empty($artist_ids)) {
- global $wpdb;
- $meta_query = $wpdb->prepare(
- "SELECT post_id, meta_value FROM {$wpdb->postmeta}
- WHERE meta_key = %s AND post_id IN (" . implode(',', array_fill(0, count($artist_ids), '%d')) . ")",
- array_merge([BASE.'image'], $artist_ids)
- );
- $results = $wpdb->get_results($meta_query);
-
- foreach ($results as $result) {
- $artist_images[$result->post_id] = $result->meta_value;
- }
- }
-
- // Get all thumbnails in one query
- $thumbnail_ids = [];
- foreach ($items as $item) {
- if ($item->type !== BASE.'artist' && isset($posts_by_id[$item->target_id])) {
- $thumb_id = get_post_thumbnail_id($item->target_id);
- if ($thumb_id) {
- $thumbnail_ids[$item->target_id] = $thumb_id;
- }
- }
- }
-
- // Format each item
- foreach ($items as $item) {
- $post_id = (int)$item->target_id;
-
- // Skip if post doesn't exist
- if (!isset($posts_by_id[$post_id])) {
- continue;
- }
-
- $post = $posts_by_id[$post_id];
-
- $formatted_item = [
- 'id' => $item->id ?? null,
- 'type' => str_replace(BASE, '', $item->type),
- 'target_id' => $post_id,
- 'date_added' => $item->date_added ?? current_time('mysql'),
- 'notes' => $item->notes ?? '',
- 'url' => get_permalink($post),
- 'title' => $post->post_title,
- 'author' => [
- 'id' => $post->post_author,
- 'name' => get_the_author_meta('display_name', $post->post_author)
- ]
- ];
-
- // Add thumbnail
- if ($item->type === BASE.'artist') {
- $meta_value = $artist_images[$post_id] ?? null;
- $formatted_item['thumbnail'] = $meta_value ? jvbFormatImage($meta_value, 'medium', 'medium') : null;
- } else {
- $thumb_id = $thumbnail_ids[$post_id] ?? null;
- $formatted_item['thumbnail'] = $thumb_id ? jvbFormatImage($thumb_id, 'medium', 'medium') : null;
- }
-
- $formatted[] = $formatted_item;
- }
-
- return $formatted;
- }
-
- /**
- * Batch format term-type favourites to reduce queries
- *
- * @param array $items Collection of favourite items of term type
- * @return array Formatted items
- */
- protected function formatTermFavourites(array $items):array
- {
- if (empty($items)) {
- return [];
- }
-
- $formatted = [];
-
- // Group by taxonomy
- $terms_by_taxonomy = [];
- foreach ($items as $item) {
- $tax = $item->type;
- if (!isset($terms_by_taxonomy[$tax])) {
- $terms_by_taxonomy[$tax] = [];
- }
- $terms_by_taxonomy[$tax][] = (int)$item->target_id;
- }
-
- // Get all terms by taxonomy
- $terms_by_id = [];
- foreach ($terms_by_taxonomy as $taxonomy => $term_ids) {
- $terms = get_terms([
- 'taxonomy' => $taxonomy,
- 'include' => $term_ids,
- 'hide_empty' => false,
- ]);
-
- if (!is_wp_error($terms)) {
- foreach ($terms as $term) {
- $terms_by_id[$taxonomy . '_' . $term->term_id] = $term;
- }
- }
- }
-
- // Get all shop images in one query if needed
- $shop_ids = [];
- foreach ($items as $item) {
- if ($item->type === BASE.'shop') {
- $shop_ids[] = (int)$item->target_id;
- }
- }
-
- $shop_images = [];
- if (!empty($shop_ids)) {
- global $wpdb;
- $meta_query = $wpdb->prepare(
- "SELECT term_id, meta_value FROM {$wpdb->termmeta}
- WHERE meta_key = %s AND term_id IN (" . implode(',', array_fill(0, count($shop_ids), '%d')) . ")",
- array_merge([BASE.'image'], $shop_ids)
- );
- $results = $wpdb->get_results($meta_query);
-
- foreach ($results as $result) {
- $shop_images[$result->term_id] = $result->meta_value;
- }
- }
-
- // Format each item
- foreach ($items as $item) {
- $term_id = (int)$item->target_id;
- $key = $item->type . '_' . $term_id;
-
- // Skip if term doesn't exist
- if (!isset($terms_by_id[$key])) {
- continue;
- }
-
- $term = $terms_by_id[$key];
-
- $formatted_item = [
- 'id' => $item->id ?? null,
- 'type' => str_replace(BASE, '', $item->type),
- 'target_id' => $term_id,
- 'date_added' => $item->date_added ?? current_time('mysql'),
- 'notes' => $item->notes ?? '',
- 'title' => $term->name,
- 'url' => get_term_link($term)
- ];
-
- // Add thumbnail for shops
- if ($item->type === BASE.'shop') {
- $meta_value = $shop_images[$term_id] ?? null;
- $formatted_item['thumbnail'] = $meta_value ? jvbFormatImage($meta_value, 'medium', 'medium') : null;
- }
-
- $formatted[] = $formatted_item;
- }
-
- return $formatted;
- }
-
- /**
- * Get counts of favourites by type for a user
- *
- * @param int $user_id User ID
- * @return array Counts by type
- */
- public function getFavouriteCounts(int $user_id, bool $show_all = true):array
- {
- $key = 'favourite_counts_by_type_' . $user_id;
- $key .= ($show_all) ? '_all' : '_not_all';
- $cache = $this->cache->get($key);
- if ($cache) {
- return $cache;
- }
- try {
- global $wpdb;
- $table = $wpdb->prefix . BASE.'favourites';
-
- $counts = $wpdb->get_results($wpdb->prepare("
- SELECT type, COUNT(*) as count
- FROM {$table}
- WHERE user_id = %d
- GROUP BY type
- ", $user_id), OBJECT_K);
-
- $all_counts = [];
- if ($show_all) {
- // Fill in zeros for types with no favourites
- $all_counts = array_fill_keys(array_keys($this->valid_types), 0);
- $temp = [];
- foreach ($all_counts as $type => $count) {
- $type_key = str_replace(BASE, '', $type);
- $temp[$type_key] = $count;
- }
- $all_counts = $temp;
- }
-
-
- foreach ($counts as $type => $data) {
- $type_key = str_replace(BASE, '', $type);
- $all_counts[$type_key] = (int)$data->count;
- }
-
- $this->cache->set($key, $all_counts);
- return $all_counts;
- } catch (Exception $e) {
- JVB()->error()->log(
- 'favourites',
- 'Error getting counts by type: ' . $e->getMessage(),
- ['user_id' => $user_id],
- 'warning'
- );
- return array_fill_keys(array_keys($this->valid_types), 0);
- }
- }
-
- /**
- * Process batch favourites operation
- *
- * @param int $user_id User ID
- * @param array $data Operation data
- * @return array Operation result
- */
- protected function processBatches(int $user_id, array $data):array
- {
- error_log('Processing Batch Operation');
- if (empty($data['adds']) && empty($data['removes'])) {
- return [
- 'success' => true,
- 'result' => array()
- ];
- }
- error_log('Proceeding to TRANSACTION');
- global $wpdb;
-
- // Start transaction
- $wpdb->query('START TRANSACTION');
- try {
- // Collect notifications to send after transaction
- $notifications = [];
-
- // Process adds
- foreach ($data['adds'] as $item) {
- $result = $this->addFavourite($user_id, $item['type'], $item['target_id']);
- if (is_wp_error($result)) {
- $results[] = [
- 'success' => false,
- 'error' => $result->get_error_message(),
- 'type' => $item['type'],
- 'target_id' => $item['target_id']
- ];
- } else {
- $results[] = array_merge($item, $result);
-
- // If notification needed, add to collection instead of sending immediately
- if (isset($this->valid_types[$item['type']]) && $this->valid_types[$item['type']]['notify_owner']) {
- $owner_ids = $this->getContentOwner($item['type'], $item['target_id']);
- if ($owner_ids) {
- $owner_ids = (is_array($owner_ids)) ? $owner_ids : [$owner_ids];
- $type_label = str_replace(BASE, '', $item['type']);
-
- foreach ($owner_ids as $owner_id) {
- if ($owner_id !== $user_id) {
- $notifications[] = [
- 'owner_id' => $owner_id,
- 'type' => 'new_favourite',
- 'action_user_id' => $user_id,
- 'message' => sprintf(
- '%s favorited your %s',
- jvbShareName($user_id),
- $type_label
- ),
- 'target_id' => $item['target_id'],
- 'target_type' => $item['type'],
- 'context' => [
- 'favourite_user_id' => $user_id,
- 'content_type' => $item['type'],
- 'content_id' => $item['target_id'],
- ]
- ];
- }
- }
- }
- }
- }
- }
-
- // Process removes
- foreach ($data['removes'] as $item) {
- $result = $this->removeFavourite($user_id, $item['type'], $item['target_id']);
- if (is_wp_error($result)) {
- $results[] = [
- 'success' => false,
- 'error' => $result->get_error_message(),
- 'type' => $item['type'],
- 'target_id' => $item['target_id']
- ];
- } else {
- $results[] = array_merge($item, $result);
- }
- }
-
- $wpdb->query('COMMIT');
-
- // Send all notifications at once after successful commit
- $manager = JVB()->notification();
- if (!empty($notifications)) {
- foreach ($notifications as $notification) {
- $manager->addNotification(
- $notification['owner_id'],
- $notification['type'],
- $notification['action_user_id'],
- $notification['message'],
- $notification['target_id'],
- $notification['target_type'],
- $notification['context']
- );
- }
- }
- error_log('Results: '.print_r($results, true));
-
- $this->cache->invalidate('favourite_counts_by_type_' . $user_id.'_all');
- $this->cache->invalidate('favourite_counts_by_type_' . $user_id.'_not_all');
- return [
- 'success' => true,
- 'result' => $results
- ];
-
- } catch (Exception $e) {
- // Something went wrong, roll back changes
- $wpdb->query('ROLLBACK');
-
- JVB()->error()->log(
- 'favourites',
- 'Batch operation failed: ' . $e->getMessage(),
- ['user_id' => $user_id, 'item_count' => count($items ?? [])],
- 'error'
- );
- error_log('Error: '.print_r($e->getMessage(), true));
-
- return [
- 'success' => false,
- 'result' => $e->getMessage()
- ];
- }
- }
-
- /**
- * Add a favourite
- *
- * @param int $user_id User ID
- * @param string $type Content type
- * @param int $target_id Target content ID
- * @return array Operation result
- */
- protected function addFavourite(int $user_id, string $type, int $target_id):array
- {
- if (!str_starts_with($type, BASE)) {
- $type = BASE.$type;
- }
-
- // Validate type
- if (!isset($this->valid_types[$type])) {
- return [
- 'success' => false,
- 'message' => 'Invalid type',
- 'type' => $type,
- 'target_id' => $target_id
- ];
- }
-
- try {
- global $wpdb;
- $table = $wpdb->prefix . BASE . 'favourites';
-
- // Check if already favourited
- $exists = $wpdb->get_var($wpdb->prepare(
- "SELECT 1 FROM {$table}
- WHERE user_id = %d AND type = %s AND target_id = %d
- LIMIT 1",
- $user_id,
- $type,
- $target_id
- ));
-
- if ($exists) {
- $result = [
- 'success' => true,
- 'action' => 'already_exists',
- 'type' => str_replace(BASE, '', $type),
- 'target_id' => $target_id,
- 'count' => $this->getFavouriteCount($type, $target_id)
- ];
-
- // Fire after action even for already existing
- do_action('jvb_after_favourite_add', $user_id, $type, $target_id, $result);
-
- return $result;
- }
-
- // Insert new favourite
- $inserted = $wpdb->insert(
- $table,
- [
- 'user_id' => $user_id,
- 'type' => $type,
- 'target_id' => $target_id,
- 'date_added' => current_time('mysql')
- ],
- ['%d', '%s', '%d', '%s']
- );
-
- if ($inserted === false) {
- throw new Exception($wpdb->last_error);
- }
-
- // Get new favourite ID
- $favourite_id = $wpdb->insert_id;
-
- // Update favourite count
- $this->updateFavouriteCount($type, $target_id);
-
- // Notify owner if needed
- $this->maybeNotifyOwner($type, $target_id, $user_id);
-
- return [
- 'success' => true,
- 'action' => 'added',
- 'favourite_id' => $favourite_id,
- 'type' => str_replace(BASE, '', $type),
- 'target_id' => $target_id,
- 'count' => $this->getFavouriteCount($type, $target_id)
- ];
- } catch (Exception $e) {
- JVB()->error()->log(
- 'favourites',
- 'Error adding favourite: ' . $e->getMessage(),
- ['user_id' => $user_id, 'type' => $type, 'target_id' => $target_id],
- 'error'
- );
-
- $result = [
- 'success' => false,
- 'message' => $e->getMessage(),
- 'type' => str_replace(BASE, '', $type),
- 'target_id' => $target_id
- ];
-
- // Fire error action
- do_action('jvb_favourite_add_error', $user_id, $type, $target_id, $e->getMessage());
-
- return $result;
- }
- }
-
- protected function removeFavourite(int $user_id, string $type, int $target_id)
- {
- try {
- if (!str_starts_with($type, BASE)) {
- $type = BASE.$type;
- }
-
- // Validate type
- if (!isset($this->valid_types[$type])) {
- return [
- 'success' => false,
- 'message' => 'Invalid type',
- 'type' => $type,
- 'target_id' => $target_id
- ];
- }
-
- global $wpdb;
- $table = $wpdb->prefix . BASE . 'favourites';
-
- // Check if favorite exists before deleting
- $exists = $wpdb->get_var($wpdb->prepare(
- "SELECT 1 FROM {$table}
- WHERE user_id = %d AND type = %s AND target_id = %d
- LIMIT 1",
- $user_id,
- $type,
- $target_id
- ));
-
- if (!$exists) {
- return [
- 'success' => true,
- 'action' => 'already_removed',
- 'type' => str_replace(BASE, '', $type),
- 'target_id' => $target_id,
- 'count' => $this->getFavouriteCount($type, $target_id)
- ];
- }
-
- $deleted = $wpdb->delete(
- $table,
- [
- 'user_id' => $user_id,
- 'type' => $type,
- 'target_id' => $target_id
- ],
- ['%d', '%s', '%d']
- );
-
- if ($deleted === false) {
- throw new Exception($wpdb->last_error);
- }
-
- // Update favourite count
- $this->updateFavouriteCount($type, $target_id);
-
- // Remove any related notifications
- $this->removeRelatedNotifications($user_id, $type, $target_id);
-
- // Invalidate cache
- CacheManager::invalidateGroup($this->cache_name);
- CacheManager::invalidateGroup('favourites_lists');
-
- return [
- 'success' => true,
- 'action' => 'removed',
- 'type' => str_replace(BASE, '', $type),
- 'target_id' => $target_id,
- 'count' => $this->getFavouriteCount($type, $target_id)
- ];
- } catch (Exception $e) {
- JVB()->error()->log(
- 'favourites',
- 'Error removing favourite: ' . $e->getMessage(),
- ['user_id' => $user_id, 'type' => $type, 'target_id' => $target_id],
- 'error'
- );
-
- return [
- 'success' => false,
- 'message' => $e->getMessage(),
- 'type' => str_replace(BASE, '', $type),
- 'target_id' => $target_id
- ];
- }
- }
-
- /**
- * Remove any existing notifications about a favorite action
- *
- * @param int $user_id User who removed the favorite
- * @param string $type Content type
- * @param int $target_id Content ID
- * @return void
- */
- protected function removeRelatedNotifications(int $user_id, string $type, int $target_id):void
- {
- try {
- // Get the content owner(s)
- $owner_ids = $this->getContentOwner($type, $target_id);
- if (!$owner_ids) {
- return;
- }
-
- $owner_ids = (is_array($owner_ids)) ? $owner_ids : [$owner_ids];
-
- foreach ($owner_ids as $owner_id) {
- // Skip if owner is the same as the user who unfavorited
- if ($owner_id === $user_id) {
- continue;
- }
-
- global $wpdb;
- $notifications_table = $wpdb->prefix . BASE . 'notifications';
-
- // Find recent (within last 30 days) new_favourite notifications from this user for this content
- $notifications = $wpdb->get_results($wpdb->prepare(
- "SELECT id FROM {$notifications_table}
+ // Find recent (within last 30 days) new_favourite notifications from this user for this content
+ $notifications = $wpdb->get_results($wpdb->prepare(
+ "SELECT id FROM {$notifications_table}
WHERE owner_id = %d
AND action_user_id = %d
AND type = 'new_favourite'
AND target_id = %d
AND target_type = %s
AND created_at > DATE_SUB(%s, INTERVAL 30 DAY)",
- $owner_id,
- $user_id,
- $target_id,
- $type,
- current_time('mysql')
- ));
+ $owner_id,
+ $user_id,
+ $target_id,
+ $type,
+ current_time('mysql')
+ ));
- if (empty($notifications)) {
- continue;
- }
+ if (empty($notifications)) {
+ continue;
+ }
- // Delete the notifications
- foreach ($notifications as $notification) {
- $wpdb->delete(
- $notifications_table,
- ['id' => $notification->id],
- ['%d']
- );
- }
+ // Delete the notifications
+ foreach ($notifications as $notification) {
+ $wpdb->delete(
+ $notifications_table,
+ ['id' => $notification->id],
+ ['%d']
+ );
+ }
- // Invalidate notification cache for this user
- if (method_exists(JVB()->notification(), 'clearNotificationCache')) {
- JVB()->notification()->clearNotificationCache($owner_id);
- }
- }
- } catch (Exception $e) {
- // Log but continue
- JVB()->error()->log(
- 'favourites',
- 'Error removing related notifications: ' . $e->getMessage(),
- ['type' => $type, 'target_id' => $target_id, 'user_id' => $user_id],
- 'warning'
- );
- }
- }
-
- /**
- * Process favourite notes update with improved transaction handling
- *
- * @param int $user_id User ID
- * @param array $data Operation data
- * @return array Operation result
- */
- protected function processNote(int $user_id, array $data):array
- {
- global $wpdb;
- $result = [];
-
- // Start transaction
- $wpdb->query('START TRANSACTION');
-
- try {
- $IDs = explode(',', $data['target_id']);
- $results = [];
- foreach ($IDs as $ID) {
- $target_id = absint($ID);
- if ($target_id <= 0) {
- throw new Exception('Invalid target ID');
- }
- $notes = sanitize_textarea_field($data['notes'] ?? '');
-
- $table = $wpdb->prefix . BASE . 'favourites';
-
- // Check if favourite exists
- $favourite_id = $wpdb->get_var($wpdb->prepare(
- "SELECT id FROM {$table}
- WHERE user_id = %d AND target_id = %d",
- $user_id, $target_id
- ));
-
- if (!$favourite_id) {
- $result[] = [
- 'success' => false,
- 'target_id' => $target_id,
- 'message' => __('No favourite id found...', 'jvb')
- ];
- } else {
- // Update existing favourite
- $updated = $wpdb->update(
- $table,
- ['notes' => $notes],
- ['id' => $favourite_id],
- ['%s'],
- ['%d']
- );
-
- if ($updated === false) {
- throw new Exception($wpdb->last_error);
- }
-
- $result[] = [
- 'success' => true,
- 'action' => 'updated_notes',
- 'favourite_id' => $favourite_id,
- 'target_id' => $target_id
- ];
- }
- }
-
- // If we got here, everything worked - commit the transaction
- $wpdb->query('COMMIT');
-
- return [
- 'success' => true,
- 'results' => $result
- ];
- } catch (Exception $e) {
- // Something went wrong, roll back changes
- $wpdb->query('ROLLBACK');
-
- JVB()->error()->log(
- 'favourites',
- 'Notes update failed: ' . $e->getMessage(),
- [
- 'user_id' => $user_id,
- 'target_id' => $data['target_id'] ?? null
- ],
- 'error'
- );
-
- return [
- 'success' => false,
- 'result' => $e->getMessage(),
- ];
- }
- }
-
- /**
- * Process list creation
- *
- * @param int $user_id User ID
- * @param array $data Operation data
- * @return array Operation result
- */
- protected function processListCreate(int $user_id, array $data):array
- {
- global $wpdb;
-
- // Start transaction
- $wpdb->query('START TRANSACTION');
-
- try {
- $name = sanitize_text_field($data['name']);
- $description = sanitize_textarea_field($data['description'] ?? '');
- $items = $data['items'] ?? [];
-
- // Fire pre-create action
- do_action('jvb_before_list_create', $user_id, $name, $description);
-
- $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
-
- // Create the list
- $inserted = $wpdb->insert(
- $lists_table,
- [
- 'user_id' => $user_id,
- 'name' => $name,
- 'description' => $description,
- 'created_at' => current_time('mysql'),
- 'updated_at' => current_time('mysql')
- ],
- ['%d', '%s', '%s', '%s', '%s']
- );
-
- if (!$inserted) {
- throw new Exception($wpdb->last_error);
- }
-
- $list_id = $wpdb->insert_id;
-
- // Add items if any, but limit batch size
- $added_count = 0;
- $batch_size = 50; // Process in batches of 50
-
- if (!empty($items)) {
- $item_batches = array_chunk($items, $batch_size);
-
- foreach ($item_batches as $batch) {
- $result = $this->addItemsToList($list_id, $batch, $user_id);
- $batch_added = $result['added'];
- $added_count += $batch_added;
-
- // Give the server a small break between large batches
- if (count($items) > $batch_size) {
- usleep(50000); // 50ms pause
- }
- }
- }
-
- // Commit transaction
- $wpdb->query('COMMIT');
-
-
- // Fire post-create action
- do_action('jvb_after_list_create', $user_id, $list_id, $name, $added_count);
-
- return [
- 'success' => true,
- 'result' => [
- 'list_id' => $list_id,
- 'name' => $name,
- 'added_items' => $added_count,
- 'message' => sprintf('Created list "%s" with %d items', $name, $added_count)
- ]
- ];
- } catch (Exception $e) {
- // Rollback on error
- $wpdb->query('ROLLBACK');
-
- JVB()->error()->log(
- 'favourites',
- 'Error creating list: ' . $e->getMessage(),
- ['user_id' => $user_id, 'name' => $data['name'] ?? ''],
- 'error'
- );
-
- return [
- 'success' => false,
- 'result' => $e->getMessage()
- ];
- }
- }
-
-
-
- /**
- * Process adding items to a list
- *
- * @param int $user_id User ID
- * @param array $data Operation data
- * @return array Operation result
- */
- protected function processAddToList(int $user_id, array $data): array
- {
- global $wpdb;
-
- // Start transaction
- $wpdb->query('START TRANSACTION');
-
- try {
- $list_ids = explode(',', $data['list_id']);
- $items = $data['items'] ?? [];
-
- if (empty($items)) {
- throw new Exception('Items array is required');
- }
-
- $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
- $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
-
- $results = [];
- $total_added = 0;
- $all_errors = [];
-
- // Process each list
- foreach ($list_ids as $list_id) {
- $list_id = (int) $list_id;
-
- if (!$list_id) {
- $all_errors[] = ['message' => 'Invalid list ID', 'list_id' => $list_id];
- continue;
- }
-
- // Check permission - either owner or has edit permission
- $has_permission = $wpdb->get_var($wpdb->prepare(
- "SELECT 1 FROM {$lists_table} WHERE id = %d AND user_id = %d
- UNION
- SELECT 1 FROM {$shares_table}
- WHERE list_id = %d AND user_id = %d AND permission_type = 'edit'
- LIMIT 1",
- $list_id,
- $user_id,
- $list_id,
- $user_id
- ));
-
- if (!$has_permission) {
- $all_errors[] = [
- 'message' => 'You do not have permission to add items to this list',
- 'list_id' => $list_id
- ];
- continue;
- }
-
- // Call the optimized helper method to add items
- $add_result = $this->addItemsToList($list_id, $items, $user_id);
-
- // Track results
- $total_added += $add_result['added'];
-
- if (!empty($add_result['errors'])) {
- $all_errors = array_merge($all_errors, $add_result['errors']);
- }
-
- $results[] = [
- 'list_id' => $list_id,
- 'added_count' => $add_result['added']
- ];
-
- }
-
- // If we've had no successful additions and only errors, throw an exception
- if ($total_added == 0 && !empty($all_errors)) {
- throw new Exception('Failed to add any items: ' . json_encode($all_errors));
- }
-
- // Commit the transaction
- $wpdb->query('COMMIT');
-
- // Invalidate relevant caches
- CacheManager::invalidateGroup('favourites_lists');
-
- return [
- 'success' => true,
- 'results' => [
- 'success' => $results,
- 'added_count' => $total_added,
- 'errors' => $all_errors
- ]
- ];
-
- } catch (Exception $e) {
- // Something went wrong, roll back changes
- $wpdb->query('ROLLBACK');
-
- JVB()->error()->log(
- 'favourites',
- 'Error adding items to list: ' . $e->getMessage(),
- ['user_id' => $user_id, 'list_ids' => $data['list_id'] ?? 0],
- 'error'
- );
-
- return [
- 'success' => false,
- 'result' => $e->getMessage()
- ];
- }
- }
-
- /**
- * Helper method to add items to a list with improved performance and error handling
- *
- * @param int $list_id List ID
- * @param array $items Items to add
- * @param int $user_id User ID
- * @return array Success details with counts and errors
- */
- protected function addItemsToList(int $list_id, array $items, int $user_id)
- {
- if (empty($items)) {
- return ['added' => 0, 'errors' => []];
- }
-
- global $wpdb;
- $items_table = $wpdb->prefix . BASE . 'favourites_list_items';
- $added = 0;
- $errors = [];
-
- try {
- // Group items by type for more efficient processing
- $items_by_type = [];
- foreach ($items as $item) {
- if (empty($item['type']) || !isset($item['target_id'])) {
- $errors[] = ['message' => 'Item missing type or target_id', 'item' => $item];
- continue;
- }
-
- $type = isset($item['type']) && strpos($item['type'], BASE) !== 0
- ? BASE . $item['type']
- : $item['type'];
-
- if (!isset($items_by_type[$type])) {
- $items_by_type[$type] = [];
- }
-
- $items_by_type[$type][] = (int)$item['target_id'];
- }
-
- // Process each type in bulk
- foreach ($items_by_type as $type => $target_ids) {
- // Find existing items to avoid duplicates
- $placeholders = implode(',', array_fill(0, count($target_ids), '%d'));
- $query_params = array_merge([$list_id], $target_ids);
- array_unshift($query_params, $type);
-
- $existing_query = $wpdb->prepare(
- "SELECT item_id FROM {$items_table}
- WHERE list_id = %d AND item_type = %s AND item_id IN ({$placeholders})",
- $query_params
- );
-
- $existing_ids = $wpdb->get_col($existing_query);
- $existing_ids_map = array_flip($existing_ids);
-
- // Get favourite IDs in bulk
- $favourites_table = $wpdb->prefix . BASE . "favourites";
- $fav_query = $wpdb->prepare(
- "SELECT id, target_id FROM {$favourites_table}
- WHERE user_id = %d AND type = %s AND target_id IN ({$placeholders})",
- array_merge([$user_id, $type], $target_ids)
- );
-
- $fav_results = $wpdb->get_results($fav_query);
- $fav_id_map = [];
-
- foreach ($fav_results as $fav) {
- $fav_id_map[$fav->target_id] = $fav->id;
- }
-
- // Prepare bulk insert
- $now = current_time('mysql');
- $insert_values = [];
- $insert_placeholders = [];
-
- foreach ($target_ids as $target_id) {
- // Skip if already in list
- if (isset($existing_ids_map[$target_id])) {
- continue;
- }
-
- $fav_id = $fav_id_map[$target_id] ?? null;
- $insert_values[] = $list_id;
- $insert_values[] = $type;
- $insert_values[] = $target_id;
- $insert_values[] = $fav_id;
- $insert_values[] = $now;
-
- $insert_placeholders[] = "(%d, %s, %d, %d, %s)";
- }
-
- // Perform bulk insert if there are items to add
- if (!empty($insert_placeholders)) {
- $insert_query = "INSERT INTO {$items_table}
- (list_id, item_type, item_id, favourite_id, added_at)
- VALUES " . implode(',', $insert_placeholders);
-
- $result = $wpdb->query($wpdb->prepare($insert_query, $insert_values));
-
- if ($result === false) {
- throw new Exception($wpdb->last_error);
- }
-
- $added += $result;
- }
- }
-
- // Update list modified timestamp
- $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
- $wpdb->update(
- $lists_table,
- ['updated_at' => current_time('mysql')],
- ['id' => $list_id],
- ['%s'],
- ['%d']
- );
-
-
- return ['added' => $added, 'errors' => $errors];
-
- } catch (Exception $e) {
- // Rollback transaction on error
- $wpdb->query('ROLLBACK');
-
- JVB()->error()->log(
- 'favourites',
- 'Error adding items to list: ' . $e->getMessage(),
- ['user_id' => $user_id, 'list_id' => $list_id, 'items_count' => count($items)],
- 'error'
- );
-
- $errors[] = ['message' => $e->getMessage()];
- return ['added' => 0, 'errors' => $errors];
- }
- }
-
- /**
- * Process list update
- *
- * @param int $user_id User ID
- * @param array $data Operation data
- * @return array Operation result
- */
- protected function processUpdateList(int $user_id, array $data):array
- {
- $list_id = intval($data['list_id'] ?? 0);
-
- if (!$list_id) {
- return [
- 'success' => false,
- 'result' => 'List ID is required'
- ];
- }
-
- try {
- global $wpdb;
- $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
-
- // Verify ownership
- $is_owner = $wpdb->get_var($wpdb->prepare(
- "SELECT 1 FROM {$lists_table}
- WHERE id = %d AND user_id = %d",
- $list_id,
- $user_id
- ));
-
- if (!$is_owner) {
- return [
- 'success' => false,
- 'result' => 'You do not have permission to update this list'
- ];
- }
-
- // Build update data
- $update_data = [];
- $update_format = [];
-
- if (isset($data['name'])) {
- $update_data['name'] = sanitize_text_field($data['name']);
- $update_format[] = '%s';
- }
-
- if (isset($data['description'])) {
- $update_data['description'] = sanitize_textarea_field($data['description']);
- $update_format[] = '%s';
- }
-
- if (empty($update_data)) {
- return [
- 'success' => true,
- 'result' => 'No changes to update'
- ];
- }
-
- // Add updated timestamp
- $update_data['updated_at'] = current_time('mysql');
- $update_format[] = '%s';
-
- $updated = $wpdb->update(
- $lists_table,
- $update_data,
- ['id' => $list_id, 'user_id' => $user_id],
- $update_format,
- ['%d', '%d']
- );
-
- if ($updated === false) {
- throw new Exception($wpdb->last_error);
- }
-
- return [
- 'success' => true,
- 'result' => [
- 'message' => 'List updated successfully',
- 'list_id' => $list_id,
- 'updates' => array_keys($update_data)
- ]
- ];
- } catch (Exception $e) {
- JVB()->error()->log(
- 'favourites',
- 'Error updating list: ' . $e->getMessage(),
- ['user_id' => $user_id, 'list_id' => $list_id],
- 'error'
- );
-
- return [
- 'success' => false,
- 'result' => $e->getMessage()
- ];
- }
- }
-
- /**
- * Process list deletion
- *
- * @param int $user_id User ID
- * @param array $data Operation data
- * @return array Operation result
- */
- protected function processListDeletion(int $user_id, array $data):array
- {
- $list_id = intval($data['list_id'] ?? 0);
-
- if (!$list_id) {
- return [
- 'success' => false,
- 'result' => 'List ID is required'
- ];
- }
-
- try {
- global $wpdb;
- $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
-
- // Verify ownership
- $is_owner = $wpdb->get_var($wpdb->prepare(
- "SELECT 1 FROM {$lists_table}
- WHERE id = %d AND user_id = %d",
- $list_id,
- $user_id
- ));
-
- if (!$is_owner) {
- return [
- 'success' => false,
- 'result' => 'You do not have permission to delete this list'
- ];
- }
-
- // Start transaction for cascading deletes
- $wpdb->query('START TRANSACTION');
-
- // Delete from all related tables
- $items_table = $wpdb->prefix . BASE . 'favourites_list_items';
- $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
- $pending_shares_table = $wpdb->prefix . BASE . 'favourites_pending_shares';
-
- $wpdb->delete($items_table, ['list_id' => $list_id], ['%d']);
- $wpdb->delete($shares_table, ['list_id' => $list_id], ['%d']);
- $wpdb->delete($pending_shares_table, ['list_id' => $list_id], ['%d']);
-
- // Delete the list itself
- $deleted = $wpdb->delete(
- $lists_table,
- ['id' => $list_id, 'user_id' => $user_id],
- ['%d', '%d']
- );
-
- if ($deleted === false) {
- throw new Exception($wpdb->last_error);
- }
-
- $wpdb->query('COMMIT');
-
- return [
- 'success' => true,
- 'result' => [
- 'message' => 'List deleted successfully',
- 'list_id' => $list_id
- ]
- ];
- } catch (Exception $e) {
- $wpdb->query('ROLLBACK');
-
- JVB()->error()->log(
- 'favourites',
- 'Error deleting list: ' . $e->getMessage(),
- ['user_id' => $user_id, 'list_id' => $list_id],
- 'error'
- );
-
- return [
- 'success' => false,
- 'result' => $e->getMessage()
- ];
- }
- }
-
- /**
- * Process removing items from a list
- *
- * @param int $user_id User ID
- * @param array $data Operation data
- * @return array Operation result
- */
- protected function removeFromList(int $user_id, array $data):array
- {
- $list_id = intval($data['list_id'] ?? 0);
- $items = $data['items'] ?? [];
-
- if (!$list_id || empty($items)) {
- return [
- 'success' => false,
- 'result' => 'List ID and items are required'
- ];
- }
-
- try {
- global $wpdb;
- $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
- $items_table = $wpdb->prefix . BASE . 'favourites_list_items';
- $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
-
- // Check permission - either owner or has edit permission
- $has_permission = $wpdb->get_var($wpdb->prepare(
- "SELECT 1 FROM {$lists_table} WHERE id = %d AND user_id = %d
- UNION
- SELECT 1 FROM {$shares_table}
- WHERE list_id = %d AND user_id = %d AND permission_type = 'edit'
- LIMIT 1",
- $list_id,
- $user_id,
- $list_id,
- $user_id
- ));
-
- if (!$has_permission) {
- return [
- 'success' => false,
- 'result' => 'You do not have permission to remove items from this list'
- ];
- }
-
- // Remove items
- $removed = 0;
-
- foreach ($items as $item) {
- if (empty($item['type']) || !isset($item['target_id'])) {
- continue;
- }
-
- $type = isset($item['type']) && strpos($item['type'], BASE) !== 0
- ? BASE . $item['type']
- : $item['type'];
-
- $deleted = $wpdb->delete(
- $items_table,
- [
- 'list_id' => $list_id,
- 'item_type' => $type,
- 'item_id' => intval($item['target_id'])
- ],
- ['%d', '%s', '%d']
- );
-
- if ($deleted) {
- $removed += $deleted;
- }
- }
-
- return [
- 'success' => true,
- 'result' => [
- 'message' => "{$removed} items removed from list",
- 'list_id' => $list_id,
- 'removed_count' => $removed
- ]
- ];
- } catch (Exception $e) {
- JVB()->error()->log(
- 'favourites',
- 'Error removing items from list: ' . $e->getMessage(),
- ['user_id' => $user_id, 'list_id' => $list_id],
- 'error'
- );
-
- return [
- 'success' => false,
- 'result' => $e->getMessage()
- ];
- }
- }
-
- /**
- * Process sharing a list
- *
- * @param int $user_id User ID
- * @param array $data Operation data
- * @return array Operation result
- */
- protected function shareList(int $user_id, array $data):array
- {
- global $wpdb;
- $result = null;
-
- // Start transaction
- $wpdb->query('START TRANSACTION');
-
- try {
- $list_id = intval($data['list_id'] ?? 0);
- $email = sanitize_email($data['email'] ?? '');
- $permission_type = in_array($data['permission_type'] ?? '', ['view', 'edit'])
- ? $data['permission_type']
- : 'view';
-
- if (!$list_id || !$email) {
- throw new Exception('List ID and email are required');
- }
-
- $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
- $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
-
- // Verify ownership
- $list = $wpdb->get_row($wpdb->prepare(
- "SELECT l.*, u.display_name as owner_name
- FROM {$lists_table} l
- JOIN {$wpdb->users} u ON l.user_id = u.ID
- WHERE l.id = %d AND l.user_id = %d",
- $list_id,
- $user_id
- ));
-
- if (!$list) {
- throw new Exception('You do not have permission to share this list');
- }
-
- // Look up user by email
- $share_user = get_user_by('email', $email);
-
- if ($share_user) {
- // User exists - check if they already have access
- $existing_share = $wpdb->get_row($wpdb->prepare(
- "SELECT * FROM {$shares_table}
- WHERE list_id = %d AND (user_id = %d OR email = %s)",
- $list_id,
- $share_user->ID,
- $email
- ));
-
- if ($existing_share) {
- // Update permission if it's different
- if ($existing_share->permission_type !== $permission_type ||
- $existing_share->status !== 'accepted') {
- $wpdb->update(
- $shares_table,
- [
- 'permission_type' => $permission_type,
- 'status' => 'accepted',
- 'user_id' => $share_user->ID,
- 'updated_at' => current_time('mysql')
- ],
- ['id' => $existing_share->id]
- );
-
- if ($wpdb->last_error) {
- throw new Exception($wpdb->last_error);
- }
-
- $result = [
- 'success' => true,
- 'action' => 'updated',
- 'message' => "Updated sharing permissions for {$email}",
- 'user_id' => $share_user->ID,
- 'email' => $email,
- 'permission_type' => $permission_type
- ];
- } else {
- $result = [
- 'success' => true,
- 'action' => 'already_shared',
- 'message' => "List is already shared with {$email}",
- 'user_id' => $share_user->ID,
- 'email' => $email,
- 'permission_type' => $permission_type
- ];
- }
- } else {
- // Add new share for existing user
- $wpdb->insert(
- $shares_table,
- [
- 'list_id' => $list_id,
- 'user_id' => $share_user->ID,
- 'email' => $email,
- 'permission_type' => $permission_type,
- 'status' => 'accepted',
- 'created_at' => current_time('mysql'),
- 'updated_at' => current_time('mysql')
- ],
- ['%d', '%d', '%s', '%s', '%s', '%s', '%s']
- );
-
- if ($wpdb->last_error) {
- throw new Exception($wpdb->last_error);
- }
-
- // Send notification to user
- JVB()->notification()->addNotification(
- $share_user->ID, // Recipient
- 'list_shared',
- $user_id, // Action user ID
- sprintf('%s shared a favorites list with you: "%s"', jvbShareName($user_id), $list->name),
- $list_id,
- 'favourites_list',
- [
- 'list_id' => $list_id,
- 'list_name' => $list->name,
- 'permission_type' => $permission_type
- ]
- );
-
- $result = [
- 'success' => true,
- 'result' => [
- 'message' => "List shared with {$email}",
- 'user_id' => $share_user->ID,
- 'email' => $email,
- 'permission_type' => $permission_type
- ]
- ];
- }
- } else {
- // User doesn't exist - check for existing pending invitation
- $existing_pending = $wpdb->get_var($wpdb->prepare(
- "SELECT id FROM {$shares_table}
- WHERE list_id = %d AND email = %s AND status = 'pending'",
- $list_id,
- $email
- ));
-
- if ($existing_pending) {
- $result = [
- 'success' => true,
- 'result' => [
- 'action' => 'already_pending',
- 'message' => "Invitation already sent to {$email}",
- 'email' => $email
- ]
- ];
- } else {
- // Generate invitation token
- $token = wp_generate_password(32, false);
-
- // Create pending share
- $wpdb->insert(
- $shares_table,
- [
- 'list_id' => $list_id,
- 'email' => $email,
- 'permission_type' => $permission_type,
- 'status' => 'pending',
- 'invitation_token' => $token,
- 'created_at' => current_time('mysql'),
- 'updated_at' => current_time('mysql')
- ],
- ['%d', '%s', '%s', '%s', '%s', '%s', '%s']
- );
-
- if ($wpdb->last_error) {
- throw new Exception($wpdb->last_error);
- }
-
- // Send invitation email
- $this->sendListInviteEmail($email, [
- 'list_id' => $list_id,
- 'list_name' => $list->name,
- 'token' => $token,
- 'owner_name' => $list->owner_name,
- 'user_id' => $user_id
- ]);
-
- $result = [
- 'success' => true,
- 'result' => [
- 'action' => 'invitation_sent',
- 'message' => "Invitation sent to {$email}",
- 'email' => $email
- ]
- ];
- }
- }
-
- // All operations successful, commit the transaction
- $wpdb->query('COMMIT');
-
- // Return the result that was set earlier
- return $result;
-
- } catch (Exception $e) {
- // Something went wrong, roll back changes
- $wpdb->query('ROLLBACK');
-
- JVB()->error()->log(
- 'favourites',
- 'Error sharing list: ' . $e->getMessage(),
- ['user_id' => $user_id, 'list_id' => $data['list_id'] ?? 0, 'email' => $data['email'] ?? ''],
- 'error'
- );
-
- return [
- 'success' => false,
- 'result' => $e->getMessage()
- ];
- }
- }
-
- /**
- * Process unsharing a list
- *
- * @param int $user_id User ID
- * @param array $data Operation data
- * @return array Operation result
- */
- protected function unshareList(int $user_id, array $data):array
- {
- $list_id = intval($data['list_id'] ?? 0);
- $email = sanitize_email($data['email'] ?? '');
-
- if (!$list_id || !$email) {
- return [
- 'success' => false,
- 'result' => 'List ID and email are required'
- ];
- }
-
- try {
- global $wpdb;
- $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
- $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
-
- // Verify ownership
- $is_owner = $wpdb->get_var($wpdb->prepare(
- "SELECT 1 FROM {$lists_table}
- WHERE id = %d AND user_id = %d",
- $list_id,
- $user_id
- ));
-
- if (!$is_owner) {
- return [
- 'success' => false,
- 'result' => 'You do not have permission to manage shares for this list'
- ];
- }
-
- // Look for any share with this email, regardless of status
- $existing_share = $wpdb->get_row($wpdb->prepare(
- "SELECT * FROM {$shares_table}
- WHERE list_id = %d AND email = %s",
- $list_id,
- $email
- ));
-
- if (!$existing_share) {
- // Also check by user_id if it's a registered user's email
- $share_user = get_user_by('email', $email);
- if ($share_user) {
- $existing_share = $wpdb->get_row($wpdb->prepare(
- "SELECT * FROM {$shares_table}
- WHERE list_id = %d AND user_id = %d",
- $list_id,
- $share_user->ID
- ));
- }
- }
-
- if (!$existing_share) {
- return [
- 'success' => false,
- 'result' => "No active share or invitation found for {$email}"
- ];
- }
-
- // For active shares, update status to 'revoked'
- // For pending invitations, also update status to 'revoked'
- $updated = $wpdb->update(
- $shares_table,
- [
- 'status' => 'revoked',
- 'updated_at' => current_time('mysql')
- ],
- ['id' => $existing_share->id],
- ['%s', '%s'],
- ['%d']
- );
-
- if ($updated === false) {
- throw new Exception($wpdb->last_error);
- }
-
- // Determine the appropriate message based on previous status
- if ($existing_share->status === 'accepted') {
- $action = 'unshared';
- $message = "Removed {$email}'s access to list";
-
- // Send notification to user if they're registered
- if ($existing_share->user_id) {
- // Get list details
- $list = $wpdb->get_row($wpdb->prepare(
- "SELECT * FROM {$lists_table} WHERE id = %d",
- $list_id
- ));
-
- if ($list) {
- JVB()->notification()->addNotification(
- $existing_share->user_id,
- 'list_share_revoked',
- $user_id, // Action user ID
- sprintf('Your access to the list "%s" has been revoked', $list->name),
- $list_id,
- 'favourites_list',
- [
- 'list_id' => $list_id,
- 'list_name' => $list->name
- ]
- );
- }
- }
- } else {
- $action = 'invitation_cancelled';
- $message = "Cancelled invitation to {$email}";
- }
-
- return [
- 'success' => true,
- 'result' => [
- 'action' => $action,
- 'message' => $message,
- 'email' => $email
- ]
- ];
-
- } catch (Exception $e) {
- JVB()->error()->log(
- 'favourites',
- 'Error unsharing list: ' . $e->getMessage(),
- ['user_id' => $user_id, 'list_id' => $list_id, 'email' => $email],
- 'error'
- );
-
- return [
- 'success' => false,
- 'result' => $e->getMessage()
- ];
- }
- }
-
- /**
- * Send list invitation email
- *
- * @param string $email Recipient email
- * @param array $data Invitation data
- * @return bool Success status
- */
- protected function sendListInviteEmail(string $email, array $data):bool
- {
- $list_name = $data['list_name'];
- $token = $data['token'];
- $owner_name = $data['owner_name'];
-
- // Generate invitation URL
- $invite_url = add_query_arg([
- 'action' => 'accept_list_invite',
- 'token' => $token,
- 'list' => $data['list_id']
- ], home_url('/'));
-
- $inviteButton = sprintf(
- '<p style="text-align: center;"><a href="%s" class="button">Set Your Password</a></p>',
- $invite_url
- );
- $inviteUrl = sprintf(
- '<p style="user-select:all;">%s</p>',
- $invite_url
- );
-
- $subject = sprintf('%s shared a favourites list with you on edmonton.ink', $owner_name);
-
- $message = sprintf(
- '<p>Hi there,</p>
- <p><strong>%s</strong> has shared their list \"<strong>%s</strong>\" with you on edmonton.ink.</p>
- <p>To view this list, click the button below:</p>
- %s
- <p>Or copy and paste this link into your browser:</p>
- %s
- <p>If you don\'t already have an account, you\'ll be guided through creating one.</p>
- %s',
- $owner_name,
- $list_name,
- $inviteButton,
- $inviteUrl,
- JVB()->email()->signature()
- );
-
- return JVB()->email()->sendEmail($email, $subject, $message);
- }
-
- /**
- * Accept a list share invitation, handling both registered and non-registered users
- *
- * @param string $token Invitation token
- * @param string $email Email address of the invited user
- * @param int|null $user_id Optional user ID if already registered
- * @return array Result with success status and messages
- */
- public function acceptListInvitation(string $token, string $email, ?int $user_id = null):array
- {
- if (!$token || !$email) {
- return [
- 'success' => false,
- 'message' => 'Invalid invitation parameters'
- ];
- }
-
- try {
- global $wpdb;
- $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
-
- // Find the pending invitation
- $invitation = $wpdb->get_row($wpdb->prepare(
- "SELECT * FROM {$shares_table}
- WHERE invitation_token = %s
- AND email = %s
- AND status = 'pending'",
- $token,
- $email
- ));
-
- if (!$invitation) {
- return [
- 'success' => false,
- 'message' => 'Invalid or expired invitation'
- ];
- }
-
- // If no user_id provided, check if a user with this email exists
- if (!$user_id) {
- $existing_user = get_user_by('email', $email);
-
- if ($existing_user) {
- $user_id = $existing_user->ID;
- } else {
- // No user account exists - create a registration URL with the token embedded
- $registration_url = add_query_arg([
- 'action' => 'register',
- 'type' => 'favourites',
- 'list_token' => $token,
- 'email' => urlencode($email)
- ], wp_login_url());
-
- return [
- 'success' => false,
- 'message' => 'You need to create an account to access this shared list',
- 'needs_registration' => true,
- 'registration_url' => $registration_url
- ];
- }
- }
-
- // Get the list details for the response
- $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
- $list = $wpdb->get_row($wpdb->prepare(
- "SELECT l.*, u.display_name as owner_name
- FROM {$lists_table} l
- JOIN {$wpdb->users} u ON l.user_id = u.ID
- WHERE l.id = %d",
- $invitation->list_id
- ));
-
- if (!$list) {
- return [
- 'success' => false,
- 'message' => 'The shared list no longer exists'
- ];
- }
-
- // Update the invitation to accepted status
- $updated = $wpdb->update(
- $shares_table,
- [
- 'status' => 'accepted',
- 'user_id' => $user_id,
- 'updated_at' => current_time('mysql')
- ],
- ['id' => $invitation->id],
- ['%s', '%d', '%s'],
- ['%d']
- );
-
- if ($updated === false) {
- throw new Exception($wpdb->last_error);
- }
-
- // Notify the list owner
- $user = get_userdata($user_id);
- $display_name = $user ? $user->display_name : $email;
- JVB()->notification()->addNotification(
- $list->user_id, // Owner ID
- 'list_share_accepted',
- $user_id, // Action user ID
- sprintf('%s accepted your invitation to the list "%s"', $display_name, $list->name),
- $invitation->list_id,
- 'favourites_list',
- [
- 'list_id' => $invitation->list_id,
- 'list_name' => $list->name,
- 'user_id' => $user_id,
- 'email' => $email
- ]
- );
-
- return [
- 'success' => true,
- 'message' => 'List successfully shared with you',
- 'list_id' => $invitation->list_id,
- 'list_name' => $list->name,
- 'permission_type' => $invitation->permission_type
- ];
-
- } catch (Exception $e) {
- JVB()->error()->log(
- 'favourites',
- 'Error accepting list invitation: ' . $e->getMessage(),
- ['token' => $token, 'email' => $email],
- 'error'
- );
-
- return [
- 'success' => false,
- 'message' => $e->getMessage()
- ];
- }
- }
-
- /**
- * Get count of favourites for an item
- *
- * @param string $type Item type
- * @param int $target_id Item ID
- * @return int Favourite count
- */
- protected function getFavouriteCount(string $type, int $target_id):int
- {
- try {
- global $wpdb;
- $table = $wpdb->prefix . BASE . 'favourites';
-
- return (int)$wpdb->get_var($wpdb->prepare(
- "SELECT COUNT(*) FROM {$table}
- WHERE type = %s AND target_id = %d",
- $type,
- $target_id
- ));
- } catch (Exception $e) {
- return 0;
- }
- }
-
- /**
- * Update favourite count meta for an item
- *
- * @param string $type Item type
- * @param int $target_id Item ID
- */
- protected function updateFavouriteCount(string $type, int $target_id):void
- {
- if (!isset($this->valid_types[$type])) {
- return;
- }
- try {
- $config = $this->valid_types[$type];
- $count = $this->getFavouriteCount($type, $target_id);
-
- if ($config['table'] === 'post') {
- update_post_meta($target_id, $config['count_meta_key'], $count);
- } else {
- update_term_meta($target_id, $config['count_meta_key'], $count);
- }
- } catch (Exception $e) {
- // Log but continue
- JVB()->error()->log(
- 'favourites',
- 'Error updating favourite count: ' . $e->getMessage(),
- ['type' => $type, 'target_id' => $target_id],
- 'warning'
- );
- }
- }
-
- /**
- * Notify content owner of new favourite if configured
- *
- * @param string $type Content type
- * @param int $target_id Content ID
- * @param int $user_id User who favourited
- * @return void
- */
- protected function maybeNotifyOwner(string $type, int $target_id, int $user_id):void
- {
- // Skip if this type doesn't need owner notification
- if (!array_key_exists($type, $this->valid_types) || !$this->valid_types[$type]['notify_owner']) {
- return;
- }
-
- try {
- $owner_ids = $this->getContentOwner($type, $target_id);
- if ($owner_ids) {
- $owner_ids = (is_array($owner_ids)) ? $owner_ids : [$owner_ids];
- foreach ($owner_ids as $owner_id) {
- if ($owner_id !== $user_id) {
- $type_label = str_replace(BASE, '', $type);
- JVB()->notification()->addNotification(
- $owner_id,
- 'new_favourite',
- $user_id, // Action user ID
- sprintf('%s favourited your %s', jvbShareName($user_id), $type_label),
- $target_id,
- $type,
- [
- 'favourite_user_id' => $user_id,
- 'content_type' => $type,
- 'content_id' => $target_id
- ]
- );
- }
- }
- }
- } catch (Exception $e) {
- // Log but continue
- JVB()->error()->log(
- 'favourites',
- 'Error notifying owner: ' . $e->getMessage(),
- ['type' => $type, 'target_id' => $target_id, 'user_id' => $user_id],
- 'warning'
- );
- }
- }
+ // Invalidate notification cache for this user
+// if (method_exists(JVB()->notification(), 'clearNotificationCache')) {
+// JVB()->notification()->clearNotificationCache($owner_id);
+// }
+ }
+ } catch (Exception $e) {
+ // Log but continue
+ JVB()->error()->log(
+ 'favourites',
+ 'Error removing related notifications: ' . $e->getMessage(),
+ ['type' => $type, 'target_id' => $target_id, 'user_id' => $user_id],
+ 'warning'
+ );
+ }
+ }
public function maybeAcceptListInvite(int $user_id, string $email, array $data):void
{
@@ -3116,363 +1025,1088 @@
}
}
- /**
- * Get the owner ID for a content item
- *
- * @param string $type Content type
- * @param int $target_id Content ID
- * @return int|null Owner ID
- */
- protected function getContentOwner(string $type, int $target_id):int|null
- {
- if (!array_key_exists($type, $this->valid_types)) {
- return null;
- }
+ /**
+ * Get detailed information for a single list
+ */
+ protected function getListDetails(int $list_id, int $user_id): array
+ {
+ $key = "list_{$list_id}_user_{$user_id}";
- try {
- if ($this->valid_types[$type]['table'] === 'post') {
- $post = get_post($target_id);
- return $post ? $post->post_author : false;
- } elseif ($type === BASE.'shop') {
- return $this->getShopOwner($target_id);
- }
- } catch (Exception $e) {
- return null;
- }
+ 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();
- return null;
- }
+ $share = null;
+ if (!$is_owner) {
+ $share = $this->listShares->where([
+ 'list_id' => $list_id,
+ 'user_id' => $user_id,
+ 'status' => 'accepted'
+ ])->first();
+ }
- /**
- * Get the owner ID for a shop
- *
- * @param int $shop_id Shop term ID
- * @return int|null Owner ID
- */
- protected function getShopOwner(int $shop_id):array
- {
- // Get shop manager users
- $owners = get_term_meta($shop_id, BASE.'owner', true);
- $owners = explode(',', $owners);
- $managers = get_term_meta($shop_id, BASE.'managers', true);
- $managers = explode(',', $managers);
+ if (!$is_owner && !$share) {
+ return [
+ 'success' => false,
+ 'message' => 'You do not have access to this list'
+ ];
+ }
- return array_merge($owners, $managers);
- }
+ // Get list details
+ $list = $this->lists->where(['id' => $list_id])->first(ARRAY_A);
+ if (!$list) {
+ return [
+ 'success' => false,
+ 'message' => 'List not found'
+ ];
+ }
- /**
- * Maintenance method to clean up orphaned favourites
- * Called by scheduled task
- */
- /**
- * Maintenance method to clean up orphaned favourites
- * Scheduled action
- * @return bool
- */
- public function cleanupOrphanedFavourites():bool
- {
- try {
- global $wpdb;
- $table = $wpdb->prefix . BASE.'favourites';
+ // Get list items
+ $items = $this->listItems
+ ->where(['list_id' => $list_id])
+ ->orderBy('added_at', 'DESC')
+ ->getResults();
- // Delete favourites for non-existent users
- $wpdb->query("
- DELETE f FROM $table f
- LEFT JOIN {$wpdb->users} u ON f.user_id = u.ID
- WHERE u.ID IS NULL
- ");
+ // Format items - convert to favourite-like objects
+ $formatted_items = [];
+ foreach ($items as $item) {
+ // Try to get the actual favourite record if it exists
+ if ($item->favourite_id) {
+ $fav = $this->favourites->where(['id' => $item->favourite_id])->first();
+ if ($fav) {
+ $formatted_items[] = $fav;
+ continue;
+ }
+ }
- // Delete favourites for non-existent posts
- $post_types = array_map(function ($type) use ($wpdb) {
- return $wpdb->prepare('%s', $type);
- }, array_filter(array_keys($this->valid_types), function ($type) {
- return $type === 'content';
- }));
+ // Create dummy favourite object for formatting
+ $formatted_items[] = (object)[
+ 'type' => $item->item_type,
+ 'target_id' => $item->item_id,
+ 'date_added' => $item->added_at
+ ];
+ }
- if (!empty($post_types)) {
- $post_types_list = implode(',', $post_types);
+ // Get shared users if owner
+ $shared_users = [];
+ if ($is_owner) {
+ $shares = $this->listShares
+ ->where(['list_id' => $list_id])
+ ->orderBy('created_at', 'DESC')
+ ->getResults();
- $wpdb->query("
- DELETE f FROM $table f
- LEFT JOIN {$wpdb->posts} p ON f.target_id = p.ID
- WHERE f.type IN ($post_types_list)
- AND p.ID IS NULL
- ");
- }
+ foreach ($shares as $share_item) {
+ $shared_user = [
+ 'email' => $share_item->email,
+ 'status' => $share_item->status,
+ 'date_added' => $share_item->created_at,
+ ];
- // Delete favourites for non-existent terms
- $term_types = array_map(function ($type) use ($wpdb) {
- return $wpdb->prepare('%s', $type);
- }, array_filter(array_keys($this->valid_types), function ($type) {
- return $type === 'tax';
- }));
+ if ($share_item->status === 'accepted' && $share_item->user_id) {
+ $user = get_userdata($share_item->user_id);
+ $shared_user['name'] = $user ? $user->display_name : 'Unknown';
+ $shared_user['permission_type'] = $share_item->permission_type;
+ }
- if (!empty($term_types)) {
- $term_types_list = implode(',', $term_types);
+ $shared_users[] = $shared_user;
+ }
+ }
- $wpdb->query("
- DELETE f FROM $table f
- LEFT JOIN {$wpdb->terms} t ON f.target_id = t.term_id
- WHERE f.type IN ($term_types_list)
- AND t.term_id IS NULL
- ");
- }
+ return [
+ 'success' => true,
+ 'list' => [
+ 'id' => (int)$list['id'],
+ 'name' => $list['name'],
+ 'description' => $list['description'] ?? '',
+ 'created_at' => $list['created_at'],
+ 'is_owner' => $is_owner,
+ 'is_shared' => !$is_owner,
+ 'items' => $this->formatItems($formatted_items),
+ 'shared_users' => $shared_users
+ ]
+ ];
- return true;
- } catch (Exception $e) {
- JVB()->error()->log(
- 'favourites',
- 'Error during cleanup: ' . $e->getMessage(),
- [],
- 'error'
- );
+ } catch (Exception $e) {
+ $this->logError('getListDetails', [
+ 'error' => $e->getMessage(),
+ 'user_id' => $user_id,
+ 'list_id' => $list_id
+ ]);
- return false;
- }
- }
+ return [
+ 'success' => false,
+ 'message' => 'Error retrieving list details'
+ ];
+ }
+ });
+ }
+ /**
+ * Process favourite notes update
+ */
+ protected function processNote(int $user_id, array $data): array
+ {
+ $target_ids = isset($data['target_id'])
+ ? array_map('absint', explode(',', $data['target_id']))
+ : [];
+ $notes = sanitize_textarea_field($data['notes'] ?? '');
- /**
- * Create a standardized error response
- *
- * @param string $code Error code
- * @param string $message Error message
- * @param int $status HTTP status code
- * @param array $additional_data Additional context data
- * @return WP_REST_Response Formatted error
- */
- protected function createErrorResponse(string $code, string $message, int $status = 400, array $additional_data = []):WP_REST_Response
- {
- $error = new WP_Error(
- $code,
- __($message, 'jvb'),
- ['status' => $status]
- );
+ if (empty($target_ids)) {
+ return ['success' => false, 'result' => 'No target IDs provided'];
+ }
- if (!empty($additional_data)) {
- $error->add_data($additional_data, 'additional_data');
- }
+ return $this->favourites->transaction(function ($table) use ($user_id, $target_ids, $notes) {
+ $results = [];
- // Log error using central error handling system
- JVB()->error()->log(
- 'favourites',
- $message,
- $additional_data,
- 'error'
- );
- return new WP_REST_Response([
- 'success' => false,
- 'code' => $code,
- 'message' => __($message, 'jvb'),
- 'status' => $status
- ]);
- }
+ foreach ($target_ids as $target_id) {
+ if ($target_id <= 0) {
+ $results[] = [
+ 'success' => false,
+ 'target_id' => $target_id,
+ 'message' => 'Invalid target ID'
+ ];
+ continue;
+ }
+ $favourite = $table->where([
+ 'user_id' => $user_id,
+ 'target_id' => $target_id
+ ])->first();
- /**
- * Handle queued operations for favourites
- *
- * @param WP_Error|array $result Current result
- * @param object $operation Operation from queue
- * @param array $data Current Data
- * @return array|WP_Error Processing result
- */
- public function processOperation(WP_Error|array $result, object $operation, array $data):array|WP_Error
- {
- // Check if this is a favourites-related operation type
- $favourites_operations = [
- 'favourites_batch',
- 'favourite_notes',
- 'favourite_list_create',
- 'favourite_list_update',
- 'favourite_list_delete',
- 'favourite_list_add',
- 'favourite_list_remove',
- 'favourite_list_share',
- 'favourite_list_unshare'
- ];
+ if (!$favourite) {
+ $results[] = [
+ 'success' => false,
+ 'target_id' => $target_id,
+ 'message' => 'Favourite not found'
+ ];
+ continue;
+ }
- if (!in_array($operation->type, $favourites_operations)) {
- return $result; // Not our operation, pass through
- }
+ $table->where(['id' => $favourite->id])->updateResults([
+ 'notes' => $notes
+ ]);
- try {
- $user_id = $operation->user_id;
+ $results[] = [
+ 'success' => true,
+ 'action' => 'updated_notes',
+ 'favourite_id' => $favourite->id,
+ 'target_id' => $target_id
+ ];
+ }
- switch ($operation->type) {
- case 'favourites_batch':
- $response = $this->processBatches($user_id, $data);
- CacheManager::invalidateGroup($this->cache_name);
- return $response;
+ return [
+ 'success' => true,
+ 'result' => $results
+ ];
+ });
+ }
- case 'favourite_notes':
- $response = $this->processNote($user_id, $data);
- CacheManager::invalidateGroup($this->cache_name);
- return $response;
+ /**
+ * Update list
+ */
+ protected function updateList(int $user_id, array $data): array
+ {
+ $list_id = absint($data['list_id'] ?? 0);
- case 'favourite_list_create':
- $response = $this->processListCreate($user_id, $data);
- CacheManager::invalidateGroup('favourites_lists');
- return $response;
+ if (!$list_id) {
+ return ['success' => false, 'result' => 'List ID is required'];
+ }
- case 'favourite_list_update':
- $response = $this->processUpdateList($user_id, $data);
- CacheManager::invalidateGroup('favourites_lists');
- return $response;
+ try {
+ // Verify ownership
+ $is_owner = $this->lists->where([
+ 'id' => $list_id,
+ 'user_id' => $user_id
+ ])->existsInQuery();
- case 'favourite_list_delete':
- $response = $this->processListDeletion($user_id, $data);
- CacheManager::invalidateGroup('favourites_lists');
- return $response;
+ if (!$is_owner) {
+ return [
+ 'success' => false,
+ 'result' => 'You do not have permission to update this list'
+ ];
+ }
- case 'favourite_list_add':
- $response = $this->processAddToList($user_id, $data);
- CacheManager::invalidateGroup('favourites_lists');
- return $response;
+ // Build update data
+ $update_data = [];
- case 'favourite_list_remove':
- $response = $this->removeFromList($user_id, $data);
- CacheManager::invalidateGroup('favourites_lists');
- return $response;
+ if (isset($data['name'])) {
+ $update_data['name'] = sanitize_text_field($data['name']);
+ }
- case 'favourite_list_share':
- $response = $this->shareList($user_id, $data);
- CacheManager::invalidateGroup('favourites_lists_shares');
- return $response;
+ if (isset($data['description'])) {
+ $update_data['description'] = sanitize_textarea_field($data['description']);
+ }
- case 'favourite_list_unshare':
- $response = $this->unshareList($user_id, $data);
- CacheManager::invalidateGroup('favourites_lists_shares');
- return $response;
+ if (empty($update_data)) {
+ return [
+ 'success' => true,
+ 'result' => 'No changes to update'
+ ];
+ }
- default:
- return $result;
- }
- } catch (Exception $e) {
- JVB()->error()->log(
- '[FavouritesRoutes]:processOperation',
- 'Failed to process queued operation: ' . $e->getMessage(),
- [
- 'operation_id' => $operation->id,
- 'type' => $operation->type,
- 'user_id' => $operation->user_id
- ],
- 'error'
- );
+ // Update the list
+ $this->lists->where(['id' => $list_id])->updateResults($update_data);
- return $result;
- }
- }
+ return [
+ 'success' => true,
+ 'result' => [
+ 'message' => 'List updated successfully',
+ 'list_id' => $list_id,
+ 'updates' => array_keys($update_data)
+ ]
+ ];
- /**
- * Clean up favourites when a post is deleted
- *
- * @param int $post_id The ID of the post being deleted
- */
- public function cleanupPostFavourites(int $post_id)
- {
- try {
- global $wpdb;
- $table = $wpdb->prefix . BASE . 'favourites';
- $type = get_post_type($post_id);
+ } catch (Exception $e) {
+ $this->logError('updateList', [
+ 'error' => $e->getMessage(),
+ 'user_id' => $user_id,
+ 'list_id' => $list_id
+ ]);
- if (!$type) {
- return;
- }
+ return [
+ 'success' => false,
+ 'result' => $e->getMessage()
+ ];
+ }
+ }
- $type = BASE . $type;
+ /**
+ * Delete list
+ */
+ protected function deleteList(int $user_id, array $data): array
+ {
+ $list_id = absint($data['list_id'] ?? 0);
- if (!isset($this->valid_types[$type])) {
- return;
- }
+ if (!$list_id) {
+ return ['success' => false, 'result' => 'List ID is required'];
+ }
- // Delete favourites for this post
- $wpdb->delete(
- $table,
- [
- 'type' => $type,
- 'target_id' => $post_id
- ],
- ['%s', '%d']
- );
+ return $this->lists->transaction(function ($table) use ($user_id, $list_id) {
+ // Verify ownership
+ $is_owner = $table->where([
+ 'id' => $list_id,
+ 'user_id' => $user_id
+ ])->existsInQuery();
- // Also remove from list items
- $items_table = $wpdb->prefix . BASE . 'favourites_list_items';
- $wpdb->delete(
- $items_table,
- [
- 'item_type' => $type,
- 'item_id' => $post_id
- ],
- ['%s', '%d']
- );
+ if (!$is_owner) {
+ throw new Exception('You do not have permission to delete this list');
+ }
- } catch (Exception $e) {
- JVB()->error()->log(
- 'favourites',
- 'Error cleaning up favourites for deleted post: ' . $e->getMessage(),
- ['post_id' => $post_id],
- 'error'
- );
- }
- }
+ // Delete related data (foreign keys should handle this, but being explicit)
+ $this->listItems->where(['list_id' => $list_id])->deleteResults();
+ $this->listShares->where(['list_id' => $list_id])->deleteResults();
- /**
- * Clean up favourites when a term is deleted
- *
- * @param int $term_id The ID of the deleted term
- * @param int $tt_id Term taxonomy ID
- * @param string $taxonomy The taxonomy slug
- */
- public function cleanupTermFavourites(int $term_id, int $tt_id, string $taxonomy)
- {
- try {
- if (!isset($this->valid_types[$taxonomy])) {
- return;
- }
+ // Delete the list
+ $table->where(['id' => $list_id])->deleteResults();
- global $wpdb;
- $table = $wpdb->prefix . BASE . 'favourites';
+ return [
+ 'success' => true,
+ 'result' => [
+ 'message' => 'List deleted successfully',
+ 'list_id' => $list_id
+ ]
+ ];
+ });
+ }
- // Delete favourites for this term
- $wpdb->delete(
- $table,
- [
- 'type' => $taxonomy,
- 'target_id' => $term_id
- ],
- ['%s', '%d']
- );
+ /**
+ * Add items to list
+ */
+ protected function addToList(int $user_id, array $data): array
+ {
+ $list_ids = isset($data['list_id'])
+ ? array_map('absint', explode(',', $data['list_id']))
+ : [];
+ $items = $data['items'] ?? [];
- // Also remove from list items
- $items_table = $wpdb->prefix . BASE . 'favourites_list_items';
- $wpdb->delete(
- $items_table,
- [
- 'item_type' => $taxonomy,
- 'item_id' => $term_id
- ],
- ['%s', '%d']
- );
+ if (empty($list_ids) || empty($items)) {
+ return ['success' => false, 'result' => 'List ID and items are required'];
+ }
- // Clean up list stats
- $stats_table = $wpdb->prefix . BASE . 'favourites_list_stats';
- $wpdb->delete(
- $stats_table,
- [
- 'item_type' => $taxonomy,
- 'item_id' => $term_id
- ],
- ['%s', '%d']
- );
+ return $this->listItems->transaction(function ($table) use ($user_id, $list_ids, $items) {
+ $results = [];
+ $total_added = 0;
+ $all_errors = [];
- } catch (Exception $e) {
- JVB()->error()->log(
- 'favourites',
- 'Error cleaning up favourites for deleted term: ' . $e->getMessage(),
- ['term_id' => $term_id, 'taxonomy' => $taxonomy],
- 'error'
- );
- }
- }
+ foreach ($list_ids as $list_id) {
+ if (!$list_id) {
+ $all_errors[] = ['message' => 'Invalid list ID', 'list_id' => $list_id];
+ continue;
+ }
+
+ // Check permission - either owner or has edit permission
+ $is_owner = $this->lists->where([
+ 'id' => $list_id,
+ 'user_id' => $user_id
+ ])->existsInQuery();
+
+ $has_edit = $this->listShares->where([
+ 'list_id' => $list_id,
+ 'user_id' => $user_id,
+ 'permission_type' => 'edit',
+ 'status' => 'accepted'
+ ])->existsInQuery();
+
+ if (!$is_owner && !$has_edit) {
+ $all_errors[] = [
+ 'message' => 'You do not have permission to add items to this list',
+ 'list_id' => $list_id
+ ];
+ continue;
+ }
+
+ // Add items to this list
+ $add_result = $this->addItemsToList($list_id, $items, $user_id);
+ $total_added += $add_result['added'];
+
+ if (!empty($add_result['errors'])) {
+ $all_errors = array_merge($all_errors, $add_result['errors']);
+ }
+
+ $results[] = [
+ 'list_id' => $list_id,
+ 'added_count' => $add_result['added']
+ ];
+ }
+
+ if ($total_added == 0 && !empty($all_errors)) {
+ throw new Exception('Failed to add any items');
+ }
+
+ return [
+ 'success' => true,
+ 'result' => [
+ 'lists' => $results,
+ 'total_added' => $total_added,
+ 'errors' => $all_errors
+ ]
+ ];
+ });
+ }
+
+ /**
+ * Remove items from list
+ */
+ protected function removeFromList(int $user_id, array $data): array
+ {
+ $list_id = absint($data['list_id'] ?? 0);
+ $items = $data['items'] ?? [];
+
+ if (!$list_id || empty($items)) {
+ return ['success' => false, 'result' => 'List ID and items are required'];
+ }
+
+ try {
+ // Check permission
+ $is_owner = $this->lists->where([
+ 'id' => $list_id,
+ 'user_id' => $user_id
+ ])->existsInQuery();
+
+ $has_edit = $this->listShares->where([
+ 'list_id' => $list_id,
+ 'user_id' => $user_id,
+ 'permission_type' => 'edit',
+ 'status' => 'accepted'
+ ])->existsInQuery();
+
+ if (!$is_owner && !$has_edit) {
+ return [
+ 'success' => false,
+ 'result' => 'You do not have permission to remove items from this list'
+ ];
+ }
+
+ // Remove items
+ $removed = 0;
+ foreach ($items as $item) {
+ if (empty($item['type']) || !isset($item['target_id'])) {
+ continue;
+ }
+
+ $type = str_starts_with($item['type'], BASE)
+ ? $item['type']
+ : BASE . $item['type'];
+
+ $deleted = $this->listItems->where([
+ 'list_id' => $list_id,
+ 'item_type' => $type,
+ 'item_id' => absint($item['target_id'])
+ ])->deleteResults();
+
+ if ($deleted) {
+ $removed += $deleted;
+ }
+ }
+
+ return [
+ 'success' => true,
+ 'result' => [
+ 'message' => "{$removed} items removed from list",
+ 'list_id' => $list_id,
+ 'removed_count' => $removed
+ ]
+ ];
+
+ } catch (Exception $e) {
+ $this->logError('removeFromList', [
+ 'error' => $e->getMessage(),
+ 'user_id' => $user_id,
+ 'list_id' => $list_id
+ ]);
+
+ return [
+ 'success' => false,
+ 'result' => $e->getMessage()
+ ];
+ }
+ }
+
+ /**
+ * Share list with another user
+ */
+ protected function shareList(int $user_id, array $data): array
+ {
+ $list_id = absint($data['list_id'] ?? 0);
+ $email = sanitize_email($data['email'] ?? '');
+ $permission_type = in_array($data['permission_type'] ?? '', ['view', 'edit'])
+ ? $data['permission_type']
+ : 'view';
+
+ if (!$list_id || !$email) {
+ return ['success' => false, 'result' => 'List ID and email are required'];
+ }
+
+ return $this->listShares->transaction(function ($table) use ($user_id, $list_id, $email, $permission_type) {
+ // Verify ownership
+ $list = $this->lists->where([
+ 'id' => $list_id,
+ 'user_id' => $user_id
+ ])->first();
+
+ if (!$list) {
+ throw new Exception('You do not have permission to share this list');
+ }
+
+ // Get owner details
+ $owner = get_userdata($user_id);
+ $owner_name = $owner ? $owner->display_name : 'Someone';
+
+ // Look up user by email
+ $share_user = get_user_by('email', $email);
+
+ if ($share_user) {
+ // User exists - check for existing share
+ $existing = $table->where([
+ 'list_id' => $list_id,
+ 'user_id' => $share_user->ID
+ ])->first();
+
+ if ($existing) {
+ // Update if different
+ if ($existing->permission_type !== $permission_type || $existing->status !== 'accepted') {
+ $table->where(['id' => $existing->id])->updateResults([
+ 'permission_type' => $permission_type,
+ 'status' => 'accepted',
+ ]);
+
+ return [
+ 'success' => true,
+ 'result' => [
+ 'action' => 'updated',
+ 'message' => "Updated sharing permissions for {$email}",
+ ]
+ ];
+ }
+
+ return [
+ 'success' => true,
+ 'result' => [
+ 'action' => 'already_shared',
+ 'message' => "List is already shared with {$email}",
+ ]
+ ];
+ }
+
+ // Create new share
+ $table->create([
+ 'list_id' => $list_id,
+ 'user_id' => $share_user->ID,
+ 'email' => $email,
+ 'permission_type' => $permission_type,
+ 'status' => 'accepted'
+ ]);
+
+ // Send notification
+ JVB()->notification()->addNotification(
+ $share_user->ID,
+ 'list_shared',
+ $user_id,
+ sprintf('%s shared a favorites list with you: "%s"', $owner_name, $list->name),
+ $list_id,
+ 'favourites_list',
+ [
+ 'list_id' => $list_id,
+ 'list_name' => $list->name,
+ 'permission_type' => $permission_type
+ ]
+ );
+
+ return [
+ 'success' => true,
+ 'result' => [
+ 'action' => 'shared',
+ 'message' => "List shared with {$email}",
+ ]
+ ];
+ }
+
+ // User doesn't exist - create pending invitation
+ $existing_pending = $table->where([
+ 'list_id' => $list_id,
+ 'email' => $email,
+ 'status' => 'pending'
+ ])->first();
+
+ if ($existing_pending) {
+ return [
+ 'success' => true,
+ 'result' => [
+ 'action' => 'already_pending',
+ 'message' => "Invitation already sent to {$email}",
+ ]
+ ];
+ }
+
+ // Create pending share with invitation token
+ $token = wp_generate_password(32, false);
+
+ $table->create([
+ 'list_id' => $list_id,
+ 'email' => $email,
+ 'permission_type' => $permission_type,
+ 'status' => 'pending',
+ 'invitation_token' => $token,
+ 'user_id' => 0 // Will be set when they accept
+ ]);
+
+ // Send invitation email
+ $this->sendListInviteEmail($email, [
+ 'list_id' => $list_id,
+ 'list_name' => $list->name,
+ 'token' => $token,
+ 'owner_name' => $owner_name,
+ 'user_id' => $user_id
+ ]);
+
+ return [
+ 'success' => true,
+ 'result' => [
+ 'action' => 'invitation_sent',
+ 'message' => "Invitation sent to {$email}",
+ ]
+ ];
+ });
+ }
+
+ /**
+ * Unshare list
+ */
+ protected function unshareList(int $user_id, array $data): array
+ {
+ $list_id = absint($data['list_id'] ?? 0);
+ $email = sanitize_email($data['email'] ?? '');
+
+ if (!$list_id || !$email) {
+ return ['success' => false, 'result' => 'List ID and email are required'];
+ }
+
+ try {
+ // Verify ownership
+ $is_owner = $this->lists->where([
+ 'id' => $list_id,
+ 'user_id' => $user_id
+ ])->existsInQuery();
+
+ if (!$is_owner) {
+ return [
+ 'success' => false,
+ 'result' => 'You do not have permission to manage shares for this list'
+ ];
+ }
+
+ // Find share by email
+ $existing_share = $this->listShares->where([
+ 'list_id' => $list_id,
+ 'email' => $email
+ ])->first();
+
+ if (!$existing_share) {
+ return [
+ 'success' => false,
+ 'result' => "No active share or invitation found for {$email}"
+ ];
+ }
+
+ // Update status to revoked (or just delete)
+ $this->listShares->where(['id' => $existing_share->id])->updateResults([
+ 'status' => 'revoked'
+ ]);
+
+ // Notify user if registered and was accepted
+ if ($existing_share->status === 'accepted' && $existing_share->user_id) {
+ $list = $this->lists->where(['id' => $list_id])->first();
+
+ if ($list) {
+ JVB()->notification()->addNotification(
+ $existing_share->user_id,
+ 'list_share_revoked',
+ $user_id,
+ sprintf('Your access to the list "%s" has been revoked', $list->name),
+ $list_id,
+ 'favourites_list',
+ [
+ 'list_id' => $list_id,
+ 'list_name' => $list->name
+ ]
+ );
+ }
+ }
+
+ $action = $existing_share->status === 'accepted' ? 'unshared' : 'invitation_cancelled';
+ $message = $existing_share->status === 'accepted'
+ ? "Removed {$email}'s access to list"
+ : "Cancelled invitation to {$email}";
+
+ return [
+ 'success' => true,
+ 'result' => [
+ 'action' => $action,
+ 'message' => $message,
+ ]
+ ];
+
+ } catch (Exception $e) {
+ $this->logError('unshareList', [
+ 'error' => $e->getMessage(),
+ 'user_id' => $user_id,
+ 'list_id' => $list_id,
+ 'email' => $email
+ ]);
+
+ return [
+ 'success' => false,
+ 'result' => $e->getMessage()
+ ];
+ }
+ }
+
+ /**
+ * Send list invitation email
+ */
+ protected function sendListInviteEmail(string $email, array $data): bool
+ {
+ $list_name = $data['list_name'];
+ $token = $data['token'];
+ $owner_name = $data['owner_name'];
+
+ // Generate invitation URL
+ $invite_url = add_query_arg([
+ 'action' => 'accept_list_invite',
+ 'token' => $token,
+ 'list' => $data['list_id']
+ ], home_url('/'));
+
+ $subject = sprintf('%s shared a favourites list with you', $owner_name);
+
+ $message = sprintf(
+ '<p>Hi there,</p>
+ <p><strong>%s</strong> has shared their list "<strong>%s</strong>" with you.</p>
+ <p>To view this list, click the link below:</p>
+ <p><a href="%s">Accept List Invitation</a></p>
+ <p>Or copy and paste this link: %s</p>
+ <p>If you don\'t have an account, you\'ll be guided through creating one.</p>',
+ $owner_name,
+ $list_name,
+ $invite_url,
+ $invite_url
+ );
+
+ return JVB()->email()->sendEmail($email, $subject, $message);
+ }
+
+ /**
+ * Accept a list share invitation
+ */
+ protected function acceptListInvitation(string $token, string $email, ?int $user_id = null): array
+ {
+ if (!$token || !$email) {
+ return [
+ 'success' => false,
+ 'message' => 'Invalid invitation parameters'
+ ];
+ }
+
+ try {
+ // Find the pending invitation
+ $invitation = $this->listShares->where([
+ 'invitation_token' => $token,
+ 'email' => $email,
+ 'status' => 'pending'
+ ])->first();
+
+ if (!$invitation) {
+ return [
+ 'success' => false,
+ 'message' => 'Invalid or expired invitation'
+ ];
+ }
+
+ // If no user_id provided, check if user exists
+ if (!$user_id) {
+ $existing_user = get_user_by('email', $email);
+
+ if ($existing_user) {
+ $user_id = $existing_user->ID;
+ } else {
+ // No account - need to register
+ $registration_url = add_query_arg([
+ 'action' => 'register',
+ 'type' => 'favourites',
+ 'list_token' => $token,
+ 'email' => urlencode($email)
+ ], wp_login_url());
+
+ return [
+ 'success' => false,
+ 'message' => 'You need to create an account to access this shared list',
+ 'needs_registration' => true,
+ 'registration_url' => $registration_url
+ ];
+ }
+ }
+
+ // Get list details
+ $list = $this->lists->where(['id' => $invitation->list_id])->first();
+
+ if (!$list) {
+ return [
+ 'success' => false,
+ 'message' => 'The shared list no longer exists'
+ ];
+ }
+
+ // Update invitation to accepted
+ $this->listShares->where(['id' => $invitation->id])->updateResults([
+ 'status' => 'accepted',
+ 'user_id' => $user_id,
+ ]);
+
+ // Notify list owner
+ $user = get_userdata($user_id);
+ $display_name = $user ? $user->display_name : $email;
+
+ JVB()->notification()->addNotification(
+ $list->user_id,
+ 'list_share_accepted',
+ $user_id,
+ sprintf('%s accepted your invitation to the list "%s"', $display_name, $list->name),
+ $invitation->list_id,
+ 'favourites_list',
+ [
+ 'list_id' => $invitation->list_id,
+ 'list_name' => $list->name,
+ ]
+ );
+
+ return [
+ 'success' => true,
+ 'message' => 'List successfully shared with you',
+ 'list_id' => $invitation->list_id,
+ 'list_name' => $list->name,
+ 'permission_type' => $invitation->permission_type
+ ];
+
+ } catch (Exception $e) {
+ $this->logError('acceptListInvitation', [
+ 'error' => $e->getMessage(),
+ 'token' => $token,
+ 'email' => $email
+ ]);
+
+ return [
+ 'success' => false,
+ 'message' => $e->getMessage()
+ ];
+ }
+ }
+
+ /**
+ * Get the owner ID for a content item
+ */
+ protected function getContentOwner(string $type, int $target_id): int|array|null
+ {
+ // For posts
+ if (str_contains($type, 'post') || in_array($type, array_keys($this->valid_types))) {
+ $post = get_post($target_id);
+ return $post ? $post->post_author : null;
+ }
+
+ // For terms (shops, etc.)
+ $owners = get_term_meta($target_id, BASE . 'owner', true);
+ if ($owners) {
+ $owner_array = array_map('absint', explode(',', $owners));
+ return count($owner_array) === 1 ? $owner_array[0] : $owner_array;
+ }
+
+ return null;
+ }
+
+ /**
+ * Batch format post-type favourites to reduce queries
+ */
+ protected function formatPostFavourites(array $items): array
+ {
+ if (empty($items)) {
+ return [];
+ }
+
+ $formatted = [];
+ $post_ids = array_map(fn($item) => (int)$item->target_id, $items);
+
+ // Get all posts in one query
+ $posts = get_posts([
+ 'post__in' => $post_ids,
+ 'post_type' => 'any',
+ 'posts_per_page' => -1,
+ 'post_status' => 'any',
+ ]);
+
+ // Create lookup map
+ $posts_by_id = [];
+ foreach ($posts as $post) {
+ $posts_by_id[$post->ID] = $post;
+ }
+
+ // Get thumbnails for artists
+ $artist_ids = [];
+ foreach ($items as $item) {
+ if ($item->type === BASE . 'artist') {
+ $artist_ids[] = (int)$item->target_id;
+ }
+ }
+
+ $artist_images = [];
+ if (!empty($artist_ids)) {
+ global $wpdb;
+ $placeholders = implode(',', array_fill(0, count($artist_ids), '%d'));
+ $results = $wpdb->get_results($wpdb->prepare(
+ "SELECT post_id, meta_value FROM {$wpdb->postmeta}
+ WHERE meta_key = %s AND post_id IN ($placeholders)",
+ array_merge([BASE . 'image'], $artist_ids)
+ ));
+
+ foreach ($results as $result) {
+ $artist_images[$result->post_id] = $result->meta_value;
+ }
+ }
+
+ // Get regular thumbnails
+ $thumbnail_ids = [];
+ foreach ($items as $item) {
+ if ($item->type !== BASE . 'artist' && isset($posts_by_id[$item->target_id])) {
+ $thumb_id = get_post_thumbnail_id($item->target_id);
+ if ($thumb_id) {
+ $thumbnail_ids[$item->target_id] = $thumb_id;
+ }
+ }
+ }
+
+ // Format each item
+ foreach ($items as $item) {
+ $post_id = (int)$item->target_id;
+
+ if (!isset($posts_by_id[$post_id])) {
+ continue;
+ }
+
+ $post = $posts_by_id[$post_id];
+
+ $formatted_item = [
+ 'id' => $item->id ?? null,
+ 'type' => str_replace(BASE, '', $item->type),
+ 'target_id' => $post_id,
+ 'date_added' => $item->date_added ?? current_time('mysql'),
+ 'notes' => $item->notes ?? '',
+ 'url' => get_permalink($post),
+ 'title' => $post->post_title,
+ 'author' => [
+ 'id' => $post->post_author,
+ 'name' => get_the_author_meta('display_name', $post->post_author)
+ ]
+ ];
+
+ // Add thumbnail
+ if ($item->type === BASE . 'artist') {
+ $meta_value = $artist_images[$post_id] ?? null;
+ $formatted_item['thumbnail'] = $meta_value ? jvbFormatImage($meta_value, 'medium', 'medium') : null;
+ } else {
+ $thumb_id = $thumbnail_ids[$post_id] ?? null;
+ $formatted_item['thumbnail'] = $thumb_id ? jvbFormatImage($thumb_id, 'medium', 'medium') : null;
+ }
+
+ $formatted[] = $formatted_item;
+ }
+
+ return $formatted;
+ }
+
+ /**
+ * Batch format term-type favourites to reduce queries
+ */
+ protected function formatTermFavourites(array $items): array
+ {
+ if (empty($items)) {
+ return [];
+ }
+
+ $formatted = [];
+
+ // Group by taxonomy
+ $terms_by_taxonomy = [];
+ foreach ($items as $item) {
+ $tax = $item->type;
+ if (!isset($terms_by_taxonomy[$tax])) {
+ $terms_by_taxonomy[$tax] = [];
+ }
+ $terms_by_taxonomy[$tax][] = (int)$item->target_id;
+ }
+
+ // Get all terms by taxonomy
+ $terms_by_id = [];
+ foreach ($terms_by_taxonomy as $taxonomy => $term_ids) {
+ $terms = get_terms([
+ 'taxonomy' => $taxonomy,
+ 'include' => $term_ids,
+ 'hide_empty' => false,
+ ]);
+
+ if (!is_wp_error($terms)) {
+ foreach ($terms as $term) {
+ $terms_by_id[$taxonomy . '_' . $term->term_id] = $term;
+ }
+ }
+ }
+
+ // Get shop images
+ $shop_ids = [];
+ foreach ($items as $item) {
+ if ($item->type === BASE . 'shop') {
+ $shop_ids[] = (int)$item->target_id;
+ }
+ }
+
+ $shop_images = [];
+ if (!empty($shop_ids)) {
+ global $wpdb;
+ $placeholders = implode(',', array_fill(0, count($shop_ids), '%d'));
+ $results = $wpdb->get_results($wpdb->prepare(
+ "SELECT term_id, meta_value FROM {$wpdb->termmeta}
+ WHERE meta_key = %s AND term_id IN ($placeholders)",
+ array_merge([BASE . 'image'], $shop_ids)
+ ));
+
+ foreach ($results as $result) {
+ $shop_images[$result->term_id] = $result->meta_value;
+ }
+ }
+
+ // Format each item
+ foreach ($items as $item) {
+ $term_id = (int)$item->target_id;
+ $key = $item->type . '_' . $term_id;
+
+ if (!isset($terms_by_id[$key])) {
+ continue;
+ }
+
+ $term = $terms_by_id[$key];
+
+ $formatted_item = [
+ 'id' => $item->id ?? null,
+ 'type' => str_replace(BASE, '', $item->type),
+ 'target_id' => $term_id,
+ 'date_added' => $item->date_added ?? current_time('mysql'),
+ 'notes' => $item->notes ?? '',
+ 'title' => html_entity_decode($term->name),
+ 'url' => get_term_link($term)
+ ];
+
+ // Add thumbnail for shops
+ if ($item->type === BASE . 'shop') {
+ $meta_value = $shop_images[$term_id] ?? null;
+ $formatted_item['thumbnail'] = $meta_value ? jvbFormatImage($meta_value, 'medium', 'medium') : null;
+ }
+
+ $formatted[] = $formatted_item;
+ }
+
+ return $formatted;
+ }
+
+ /**
+ * Handle list operations - routes to appropriate method
+ */
+ public function handleList(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'));
+
+ if (!$this->userCheck($user_id)) {
+ return $this->unauthorized();
+ }
+
+ $data = [
+ 'action' => $action,
+ 'list_id' => absint($request->get_param('list_id') ?? 0),
+ 'name' => sanitize_text_field($request->get_param('name') ?? ''),
+ 'description' => sanitize_textarea_field($request->get_param('description') ?? ''),
+ 'items' => $request->get_param('items') ?? [],
+ 'email' => sanitize_email($request->get_param('email') ?? ''),
+ 'permission_type' => sanitize_text_field($request->get_param('permission_type') ?? 'view'),
+ ];
+
+ // Map action to operation type
+ $operation_type = match ($action) {
+ 'create' => 'favourite_list_create',
+ 'update' => 'favourite_list_update',
+ 'delete' => 'favourite_list_delete',
+ 'add_items' => 'favourite_list_add_items',
+ 'remove_items' => 'favourite_list_remove_items',
+ 'share' => 'favourite_list_share',
+ 'unshare' => 'favourite_list_unshare',
+ default => null
+ };
+
+ if (!$operation_type) {
+ return $this->error('Invalid action', 'invalid_action', 400);
+ }
+
+ JVB()->queue()->queueOperation(
+ $operation_type,
+ $user_id,
+ $data,
+ [
+ 'operation_id' => $operation_id,
+ 'priority' => 'normal',
+ ]
+ );
+
+ return $this->queued($operation_id);
+ }
}
--
Gitblit v1.10.0