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 } }