| | |
| | | $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']); |
| | | } |
| | | |
| | | public function registerRoutes(): void |
| | |
| | | */ |
| | | 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); |
| | | } |
| | | }); |
| | | $counts = JVB()->favourites()->getFavouriteCounts($user_id); |
| | | |
| | | return Response::success(['counts' => $counts]); |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * 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; |
| | | |
| | | $type = BASE . $type; |
| | | |
| | | // Delete using fluent interface |
| | | $this->favourites->where([ |
| | | 'type' => $type, |
| | | 'target_id' => $post_id |
| | | ])->deleteResults(); |
| | | |
| | | $this->listItems->where([ |
| | | 'item_type' => $type, |
| | | 'item_id' => $post_id |
| | | ])->deleteResults(); |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('cleanupPostFavourites', [ |
| | | 'error' => $e->getMessage(), |
| | | 'post_id' => $post_id |
| | | ]); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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; |
| | | } |
| | | |
| | | // Delete using fluent interface |
| | | $this->favourites->where([ |
| | | 'type' => $taxonomy, |
| | | 'target_id' => $term_id |
| | | ])->deleteResults(); |
| | | |
| | | $this->listItems->where([ |
| | | 'item_type' => $taxonomy, |
| | | 'item_id' => $term_id |
| | | ])->deleteResults(); |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('cleanupTermFavourites', [ |
| | | 'error' => $e->getMessage(), |
| | | 'term_id' => $term_id, |
| | | 'taxonomy' => $taxonomy |
| | | ]); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Helper methods |
| | | */ |
| | | protected function buildParams(WP_REST_Request $request): array |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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 ($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 |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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} |
| | | 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') |
| | | )); |
| | | |
| | | if (empty($notifications)) { |
| | | continue; |
| | | } |
| | | |
| | | // 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' |
| | | ); |
| | | } |
| | | } |
| | | |
| | | public function maybeAcceptListInvite(int $user_id, string $email, array $data):void |
| | | { |
| | | if (array_key_exists('list_token', $data) && !empty($data['list_token'])) { |
| | | $this->acceptListInvitation($data['list_token'], $email); |
| | | JVB()->favourites()->acceptListShare($data['list_token'], $user_id); |
| | | } |
| | | } |
| | | |