<?php
|
namespace JVBase\managers;
|
|
use JVBase\registrar\Registrar;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
class FavouritesManager
|
{
|
private Cache $cache;
|
|
protected CustomTable $favourites;
|
protected CustomTable $lists;
|
protected CustomTable $listItems;
|
protected CustomTable $listShares;
|
|
public function __construct()
|
{
|
$this->defineTables();
|
}
|
|
protected function defineTables():void
|
{
|
$this->defineFavouriteTable();
|
$this->defineListTable();
|
$this->defineListItemsTable();
|
$this->defineListSharesTable();
|
}
|
private function defineFavouriteTable():void
|
{
|
$table = CustomTable::for('favourites');
|
|
$table->setColumns([
|
'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
|
'user_id' => "{$table->getUserIDType()} NOT NULL",
|
'type' => 'varchar(50) NOT NULL', //As in, post type/user/taxonomy
|
'target_id' => 'bigint(20) unsigned NOT NULL',
|
'notes' => 'text DEFAULT NULL',
|
'created_at' => 'datetime DEFAULT CURRENT_TIMESTAMP',
|
'updated_at' => 'datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
|
]);
|
|
$table->setKeys([
|
['key' => 'PRIMARY', 'value' => '(`id`)'],
|
['key' => 'UNIQUE', 'value' => '`unique_favourite` (`user_id`, `type`, `target_id`)'],
|
'`user_type` (`user_id`, `type`)',
|
'`target_type` (`target_id`, `type`)'
|
]);
|
$base = BASE;
|
$table->setConstraints([
|
"CONSTRAINT `{$base}favourites_user` FOREIGN KEY (`user_id`)
|
REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
|
]);
|
|
$table->defineTable();
|
$this->favourites = $table;
|
}
|
private function defineListTable():void
|
{
|
$table = CustomTable::for('favourites_lists');
|
|
$table->setColumns([
|
'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
|
'user_id' => "{$table->getUserIDType()} NOT NULL",
|
'name' => 'varchar(255) NOT NULL',
|
'description'=> 'text',
|
'notes' => 'JSON DEFAULT (\'{}\')',
|
'created_at'=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP',
|
'updated_at'=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
|
]);
|
|
$table->setKeys([
|
['key' => 'PRIMARY', 'value' => '(`id`)'],
|
'`user_lists` (`user_id`)'
|
]);
|
$base = BASE;
|
$table->setConstraints([
|
"CONSTRAINT `{$base}favourites_list_user` FOREIGN KEY (`user_id`)
|
REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
|
]);
|
|
$table->defineTable();
|
$this->lists = $table;
|
}
|
private function defineListItemsTable():void
|
{
|
$table = CustomTable::for('favourites_list_items');
|
|
$table->setColumns([
|
'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
|
'user_id' => "{$table->getUserIDType()} NOT NULL",
|
'list_id' => 'bigint(20) unsigned NOT NULL',
|
'item_type' => 'varchar(50) NOT NULL',
|
'target_id' => 'bigint(20) unsigned NOT NULL',
|
'favourite_id'=> 'bigint(20) unsigned NOT NULL',
|
'notes' => 'JSON DEFAULT (\'{}\')',
|
'added_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP',
|
]);
|
|
$table->setKeys([
|
['key' => 'PRIMARY', 'value' => '(`id`)'],
|
['key' => 'UNIQUE', 'value' => '`unique_list_item` (`list_id`, `item_type`, `target_id`)'],
|
'`list_items` (`list_id`)',
|
'`favourite_id` (`favourite_id`)'
|
]);
|
$base = BASE;
|
$table->setConstraints([
|
"CONSTRAINT `{$base}favourites_list_items_user` FOREIGN KEY (`user_id`)
|
REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE",
|
"CONSTRAINT `{$base}favourite_list_items` FOREIGN KEY (`list_id`)
|
REFERENCES `{$this->lists->getFullTableName()}` (`id`) ON DELETE CASCADE",
|
"CONSTRAINT `{$base}list_favourite` FOREIGN KEY (`favourite_id`)
|
REFERENCES `{$this->favourites->getFullTableName()}` (`id`) ON DELETE CASCADE"
|
]);
|
|
$table->defineTable();
|
$this->listItems = $table;
|
}
|
private function defineListSharesTable():void
|
{
|
$table = CustomTable::for('favourites_list_shares');
|
|
$table->setColumns([
|
'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
|
'list_id' => 'bigint(20) unsigned NOT NULL',
|
'user_id' => "{$table->getUserIDType()} NOT NULL",
|
'email' => 'varchar(255) NOT NULL',
|
'permission' => "ENUM('view', 'edit') NOT NULL DEFAULT 'view'",
|
'status' => "ENUM('pending', 'accepted', 'rejected', 'revoked') NOT NULL DEFAULT 'pending'",
|
'created_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP',
|
'updated_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
|
]);
|
|
$table->setKeys([
|
['key' => 'PRIMARY', 'value' => '(`id`)'],
|
['key' => 'UNIQUE', 'value' => '`unique_share_user` (`list_id`, `user_id`)'],
|
['key'=> 'UNIQUE', 'value' => '`unique_share_email` (`list_id`, `email`)'],
|
'`list_shares` (`list_id`)',
|
'`list_user` (`list_id`, `user_id`)',
|
'`status_index` (`status`)'
|
]);
|
$base = BASE;
|
$table->setConstraints([
|
"CONSTRAINT `{$base}favourites_share_list` FOREIGN KEY (`list_id`)
|
REFERENCES `{$this->lists->getFullTableName()}` (`id`) ON DELETE CASCADE",
|
"CONSTRAINT `{$base}favourites_share_user` FOREIGN KEY (`user_id`)
|
REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE"
|
]);
|
|
$table->defineTable();
|
$this->listShares = $table;
|
}
|
public function toggleFavourite(bool $favourite, int $user_id, int $target_id, string $type):bool
|
{
|
if (!get_userdata($user_id)) {
|
error_log('[FAVOURITES]::toggleFavourite Invalid user '.$user_id);
|
return false;
|
}
|
$registrar = Registrar::getInstance($type);
|
if (!$registrar) {
|
error_log('[FAVOURITES]::toggleFavourite Invalid type: '.print_r($type, true));
|
return false;
|
}
|
$type = jvbCheckBase($type);
|
$registerType = $registrar->getType();
|
$test = false;
|
switch ($registerType) {
|
case 'post':
|
$test = get_post($target_id);
|
break;
|
case 'term':
|
$test = get_term($target_id, $type);
|
break;
|
case 'user':
|
$test = get_userdata($target_id);
|
break;
|
}
|
if (!$test || is_wp_error($test)) {
|
error_log('[FAVOURITES]::toggleFavourite Could not find target: '.$target_id);
|
return false;
|
}
|
|
$owner = null;
|
if ($registrar->getType() === 'post') {
|
$post = get_post($target_id);
|
$owner = $post->post_author;
|
} elseif ($registrar->hasFeature('is_ownable')) {
|
$term = get_term($target_id, jvbCheckBase($type));
|
$owner = jvbGetTermOwners($target_id);
|
}
|
|
if ($favourite) {
|
$result = $this->favourites->findOrCreate(
|
[
|
'user_id' => $user_id,
|
'target_id' => $target_id,
|
'type' => $type
|
],
|
);
|
|
if ($owner) {
|
JVB()->notification()->addNotification(
|
$owner,
|
'new_favourite',
|
$user_id,
|
'',
|
$target_id,
|
$type
|
);
|
}
|
|
|
} else {
|
$result = $this->favourites->delete([
|
'user_id' => $user_id,
|
'target_id' => $target_id,
|
'type' => $type
|
]);
|
|
if (in_array($registerType, ['term', 'user'])) {
|
JVB()->notification()->deleteUserPreference($user_id, $target_id, $type);
|
}
|
if ($owner) {
|
JVB()->notification()->removeNotification(
|
$owner,
|
[
|
'from_user' => $user_id,
|
'target_id' => $target_id,
|
'target_type' => $type
|
]
|
);
|
}
|
}
|
return (bool)$result;
|
}
|
|
public function favourite(int $user_id, int $target_id, string $type):bool
|
{
|
return $this->toggleFavourite(true, $user_id, $target_id, $type);
|
}
|
public function unfavourite(int $user_id, int $target_id, string $type):bool
|
{
|
return $this->toggleFavourite(false, $user_id, $target_id, $type);
|
}
|
|
public function addNoteFromTarget(int $user_id, int $target_id, string $type, string $note):bool
|
{
|
$favourite = $this->favourites->get([
|
'user_id' => $user_id,
|
'target_id' => $target_id,
|
'type' => $type
|
]);
|
if (!$favourite) {
|
return false;
|
}
|
return $this->addNote($user_id, $favourite->id, $note);
|
}
|
public function addNote(int $user_id, int $favourite_id, string $note):bool
|
{
|
$favourite = $this->favourites->get(['id' => $favourite_id]);
|
if (!$favourite) {
|
return false;
|
}
|
if ($favourite->user_id !== $user_id) {
|
return false;
|
}
|
return (bool) $this->favourites->update([
|
'note' => sanitize_textarea_field($note)
|
], [
|
'favourite_id' => $favourite_id
|
]);
|
}
|
public function deleteNote(int $user_id, int $favourite_id):bool
|
{
|
if (!$this->userCanEditFavourite($user_id, ['id' => $favourite_id])) {
|
return false;
|
}
|
return (bool) $this->favourites->update([
|
'note' => '',
|
], [
|
'favourite_id' => $favourite_id
|
]);
|
}
|
|
|
public function addListItemNote(int $user_id, int $listItemId, string|array $note):bool
|
{
|
if (!$this->userCanEditListItem($user_id, $listItemId)) {
|
return false;
|
}
|
$listItem = $this->listItems->get(['id' => $listItemId]);
|
$notes = json_decode($listItem->notes, true);
|
|
$notes = $this->addNoteToArray($notes, $user_id, $note);
|
|
return $this->listItems->update(['id' => $listItemId], ['notes' => json_encode($notes)]);
|
}
|
public function removeListItemNote(int $user_id, int $listItemId, string $noteID):bool
|
{
|
if (!$this->userCanEditListItem($user_id, $listItemId)) {
|
return false;
|
}
|
$listItem = $this->listItems->get(['id' => $listItemId]);
|
$notes = json_decode($listItem->notes, true);
|
$index = array_search($noteID, array_column($notes, 'id'));
|
if ($index === false) {
|
return false;
|
}
|
$note = $notes[$index];
|
if ($user_id !== $note['user_id']) {
|
return false;
|
}
|
|
$notes = $this->removeNoteFromArray($notes, $user_id, $noteID);
|
|
return $this->listItems->update(['id' => $listItemId], ['notes' => json_encode($notes)]);
|
}
|
|
public function addListNote(int $user_id, int $listId, string $note):bool
|
{
|
if (!$this->userCanEditList($user_id, $listId)) {
|
return false;
|
}
|
$list = $this->listItems->get(['id' => $listId]);
|
$notes = json_decode($list->notes, true);
|
|
$notes = $this->addNoteToArray($notes, $user_id, $note);
|
|
return $this->listItems->update(['id' => $listId], ['notes' => json_encode($notes)]);
|
}
|
|
public function removeListNote(int $user_id, int $listId, string $noteID):bool
|
{
|
if (!$this->userCanEditList($user_id, $listId)) {
|
return false;
|
}
|
$list = $this->lists->get(['id' => $listId]);
|
$notes = json_decode($list->notes, true);
|
$index = array_search($noteID, array_column($notes, 'id'));
|
if ($index === false) {
|
return false;
|
}
|
$note = $notes[$index];
|
if ($user_id !== $note['user_id']) {
|
return false;
|
}
|
|
$notes = $this->removeNoteFromArray($notes, $user_id, $noteID);
|
|
return $this->lists->update(['id' => $listId], ['notes' => json_encode($notes)]);
|
}
|
|
public function shareList(int $user_id, int $listId, bool $viewOnly = true, ?int $shareWithUser = null, ?array $newUser = null):bool
|
{
|
$list = $this->lists->get(['id' => $listId]);
|
if (!$list) {
|
return false;
|
}
|
if ($list->user_id !== $user_id) {
|
return false;
|
}
|
|
if (!$shareWithUser && (empty($newUser) || !array_key_exists('email', $newUser) || !array_key_exists('name', $newUser))) {
|
return false;
|
}
|
|
$args = [];
|
if ($shareWithUser) {
|
$args['user_id'] = $shareWithUser;
|
JVB()->notification()->addNotification($shareWithUser, 'list_shared', $user_id, '', $listId);
|
} elseif ($newUser) {
|
$email = sanitize_email($newUser['email']);
|
$name = sanitize_text_field($newUser['name']);
|
$args['email'] = $email;
|
//TODO: Magic Link setup
|
}
|
if (empty($args)) {
|
return false;
|
}
|
|
return (bool) $this->listShares->findOrCreate(array_merge($args, ['list_id' => $listId, 'permission' => $viewOnly ? 'view' : 'edit']));
|
}
|
public function unshareList(int $user_id, int $listId, int|string $userIDorEmail):bool
|
{
|
$list = $this->lists->get(['id' => $listId]);
|
if (!$list) {
|
return false;
|
}
|
if ($list->user_id !== $user_id) {
|
return false;
|
}
|
|
$where = [
|
'list_id' => $listId
|
];
|
if (is_int($userIDorEmail)) {
|
$where['user_id'] = $userIDorEmail;
|
|
$notification = JVB()->notification()->table();
|
$n = $notification->get([
|
'for_user' => $userIDorEmail,
|
'from_user' => $user_id,
|
'type' => 'list_shared',
|
'target_id' => $listId,
|
]);
|
if ($n) {
|
$notification->delete([
|
'for_user' => $userIDorEmail,
|
'from_user' => $user_id,
|
'type' => 'list_shared',
|
'target_id' => $listId,
|
]);
|
}
|
} else if (is_email($userIDorEmail)) {
|
$where['email'] = sanitize_email($userIDorEmail);
|
} else {
|
return false;
|
}
|
|
return $this->listShares->update($where, ['status' => 'revoked']);
|
}
|
|
public function acceptListShare(int $listId, int|string $userIDorEmail):bool
|
{
|
return $this->respondToShare(true, $listId, $userIDorEmail);
|
}
|
|
public function rejectListShare(int $listId, int|string $userIDorEmail):bool
|
{
|
return $this->respondToShare(false, $listId, $userIDorEmail);
|
}
|
public function respondToShare(bool $accept, int $listId, int|string $userIDorEmail):bool
|
{
|
$args = [
|
'listId' => $listId
|
];
|
if (is_int($userIDorEmail)) {
|
$args['user_id'] = $userIDorEmail;
|
|
$list = $this->lists->get(['id' => $listId]);
|
if ($list) {
|
$user = get_userdata($userIDorEmail);
|
|
$message = $accept ? 'accepted' : 'rejected';
|
$message = $user->first_name . ' ' . $message . ' your list share invitation.';
|
JVB()->notification()->addNotification($list->user_id, 'list_share_status', $userIDorEmail, $message);
|
}
|
}else if (is_email($userIDorEmail)) {
|
$args['email'] = sanitize_email($userIDorEmail);
|
} else {
|
return false;
|
}
|
$share = $this->listShares->get($args);
|
if (!$share) {
|
return false;
|
}
|
|
|
return $this->listShares->update([
|
'status' => $accept ? 'accepted' : 'rejected',
|
], $args);
|
}
|
/***************************************************************
|
* UTILITY METHODS
|
**************************************************************/
|
/**
|
* @param int $user_id
|
* @param array $args
|
* @return bool
|
*/
|
public function userCanEditFavourite(int $user_id, array $args):bool
|
{
|
if (array_key_exists('id', $args)) {
|
$where = [
|
'id' => $args['id']
|
];
|
} else {
|
if (!array_key_exists('target_id', $args)
|
|| !array_key_exists('type', $args)) {
|
error_log('[Favourites]::userCanEditFavourite missing required target_id or type');
|
return false;
|
}
|
$where = [
|
'target_id' => absint($args['target_id']),
|
'type' => $args['type']
|
];
|
}
|
|
$get = $this->favourites->get($where);
|
return !is_null($get) && $get->user_id === $user_id;
|
}
|
|
public function userCanViewList(int $user_id, int $list_id):bool
|
{
|
$list = $this->lists->get(['id' => $list_id]);
|
if (!$list) {
|
return false;
|
}
|
if ($list->user_id === $user_id) {
|
return true;
|
}
|
$share = $this->listShares->get(['user_id' => $user_id, 'list_id' => $list_id]);
|
return !is_null($share);
|
}
|
public function userCanEditList(int $user_id, int $list_id):bool
|
{
|
$list = $this->lists->get(['id' => $list_id]);
|
if (!$list) {
|
return false;
|
}
|
if ($list->user_id === $user_id) {
|
return true;
|
}
|
$share = $this->listShares->get(['user_id' => $user_id, 'list_id' => $list_id]);
|
if (!$share) return false;
|
return $share->permission === 'edit';
|
}
|
|
public function userCanEditListItem(int $user_id, int $list_item_id):bool
|
{
|
$item = $this->listItems->get(['id' => $list_item_id]);
|
if (!$item) {
|
return false;
|
}
|
if ($item->user_id === $user_id) {
|
return true;
|
}
|
return $this->userCanEditList($user_id, $item->list_id);
|
}
|
|
protected function addNoteToArray(array $notes, int $user_id, string|array $note):array
|
{
|
$updated = date('Y-m-d H:i:s');
|
if (is_string($note)) {
|
//It is a new note
|
$notes[] = [
|
'id' => uniqid(),
|
'note' => sanitize_textarea_field($note),
|
'user_id' => $user_id,
|
'updated' => $updated
|
];
|
} else {
|
$index = array_search($note['id'], array_column($notes, 'id'));
|
if ($index === false) {
|
//Shouldn't happen, but just in case: add it to the array
|
$notes[] = [
|
'id' => sanitize_text_field($note['id']),
|
'note' => sanitize_textarea_field($note['note']),
|
'user_id' => $user_id,
|
'updated' => $updated
|
];
|
} else {
|
$notes[$index]['note'] = sanitize_textarea_field($note['note']);
|
$notes[$index]['updated'] = $updated;
|
}
|
}
|
|
return $notes;
|
}
|
|
protected function removeNoteFromArray(array $notes, int $user_id, string $noteID):array
|
{
|
$index = array_search($noteID, array_column($notes, 'id'));
|
if ($index === false) {
|
return $notes;
|
}
|
$note = $notes[$index];
|
if ($user_id !== $note['user_id']) {
|
return $notes;
|
}
|
|
unset($notes[$index]);
|
return array_values($notes); //reindexed array
|
}
|
}
|