diff --git a/src/app-config.php b/src/app-config.php new file mode 100644 index 0000000..d5e34c9 --- /dev/null +++ b/src/app-config.php @@ -0,0 +1,17 @@ +` tag + * + * @param DOMNode $node The XML node from which a feed item should be constructed + * @return FeedItem A feed item constructed from the given node + */ + public static function fromAtom(DOMNode $node): FeedItem { + $guid = Feed::atomValue($node, 'id'); + $link = ''; + foreach ($node->getElementsByTagName('link') as $linkElt) { + if ($linkElt->hasAttributes()) { + $relAttr = $linkElt->attributes->getNamedItem('rel'); + if ($relAttr && $relAttr->value == 'alternate') { + $link = $linkElt->attributes->getNamedItem('href')->value; + break; + } + } + } + if ($link == '' && str_starts_with($guid, 'http')) $link = $guid; + + $item = new FeedItem(); + $item->guid = $guid; + $item->title = Feed::atomValue($node, 'title'); + $item->link = $link; + $item->publishedOn = Data::formatDate(Feed::atomValue($node, 'published')); + $item->updatedOn = Data::formatDate(Feed::atomValue($node, 'updated')); + $item->content = Feed::atomValue($node, 'content'); + + return $item; + } + + /** + * Construct a feed item from an RSS feed's `` tag + * + * @param DOMNode $node The XML node from which a feed item should be constructed + * @return FeedItem A feed item constructed from the given node + */ + public static function fromRSS(DOMNode $node): FeedItem { + $itemGuid = Feed::rssValue($node, 'guid'); + $updNodes = $node->getElementsByTagNameNS(Feed::ATOM_NS, 'updated'); + $encNodes = $node->getElementsByTagNameNS(Feed::CONTENT_NS, 'encoded'); + + $item = new FeedItem(); + $item->guid = $itemGuid == 'guid not found' ? Feed::rssValue($node, 'link') : $itemGuid; + $item->title = Feed::rssValue($node, 'title'); + $item->link = Feed::rssValue($node, 'link'); + $item->publishedOn = Data::formatDate(Feed::rssValue($node, 'pubDate')); + $item->updatedOn = Data::formatDate($updNodes->length > 0 ? $updNodes->item(0)->textContent : null); + $item->content = $encNodes->length > 0 + ? $encNodes->item(0)->textContent + : Feed::rssValue($node, 'description'); + + return $item; + } } /** @@ -87,11 +142,11 @@ class Feed { /** * Get the value of a child element by its tag name for an RSS feed * - * @param DOMElement $element The parent element + * @param DOMNode $element The parent element * @param string $tagName The name of the tag whose value should be obtained * @return string The value of the element (or "[element] not found" if that element does not exist) */ - private static function rssValue(DOMElement $element, string $tagName): string { + public static function rssValue(DOMNode $element, string $tagName): string { $tags = $element->getElementsByTagName($tagName); return $tags->length == 0 ? "$tagName not found" : $tags->item(0)->textContent; } @@ -109,34 +164,19 @@ class Feed { return ['error' => "Channel element not found ($channel->nodeType)"]; } - $feed = new Feed(); - $feed->title = self::rssValue($channel, 'title'); - $feed->url = $url; - // The Atom namespace provides a lastBuildDate, which contains the last time an item in the feed was updated; if // that is not present, use the pubDate element instead - $feed->updatedOn = self::rssValue($channel, 'lastBuildDate'); - if ($feed->updatedOn == 'lastBuildDate not found') { - $feed->updatedOn = self::rssValue($channel, 'pubDate'); - if ($feed->updatedOn == 'pubDate not found') $feed->updatedOn = null; + $updatedOn = self::rssValue($channel, 'lastBuildDate'); + if ($updatedOn == 'lastBuildDate not found') { + $updatedOn = self::rssValue($channel, 'pubDate'); + if ($updatedOn == 'pubDate not found') $updatedOn = null; } - $feed->updatedOn = Data::formatDate($feed->updatedOn); - foreach ($channel->getElementsByTagName('item') as $xmlItem) { - $itemGuid = self::rssValue($xmlItem, 'guid'); - $updNodes = $xmlItem->getElementsByTagNameNS(Feed::ATOM_NS, 'updated'); - $encNodes = $xmlItem->getElementsByTagNameNS(Feed::CONTENT_NS, 'encoded'); - $item = new FeedItem(); - $item->guid = $itemGuid == 'guid not found' ? self::rssValue($xmlItem, 'link') : $itemGuid; - $item->title = self::rssValue($xmlItem, 'title'); - $item->link = self::rssValue($xmlItem, 'link'); - $item->publishedOn = Data::formatDate(self::rssValue($xmlItem, 'pubDate')); - $item->updatedOn = Data::formatDate($updNodes->length > 0 ? $updNodes->item(0)->textContent : null); - $item->content = $encNodes->length > 0 - ? $encNodes->item(0)->textContent - : self::rssValue($xmlItem, 'description'); - $feed->items[] = $item; - } + $feed = new Feed(); + $feed->title = self::rssValue($channel, 'title'); + $feed->url = $url; + $feed->updatedOn = Data::formatDate($updatedOn); + foreach ($channel->getElementsByTagName('item') as $item) $feed->items[] = FeedItem::fromRSS($item); return ['ok' => $feed]; } @@ -147,11 +187,11 @@ class Feed { * (Atom feeds can have type attributes on nearly any value. For our purposes, types "text" and "html" will work as * regular string values; for "xhtml", though, we will need to get the `
` and extract its contents instead.) * - * @param DOMElement $element The parent element + * @param DOMNode $element The parent element * @param string $tagName The name of the tag whose value should be obtained * @return string The value of the element (or "[element] not found" if that element does not exist) */ - private static function atomValue(DOMElement $element, string $tagName): string { + public static function atomValue(DOMNode $element, string $tagName): string { $tags = $element->getElementsByTagName($tagName); if ($tags->length == 0) return "$tagName not found"; $tag = $tags->item(0); @@ -172,39 +212,15 @@ class Feed { * @return array|Feed[] ['ok' => feed] */ private static function fromAtom(DOMDocument $xml, string $url): array { - /** @var DOMElement $root */ - $root = $xml->getElementsByTagNameNS(self::ATOM_NS, 'feed')->item(0); - $feed = new Feed(); - $feed->title = self::atomValue($root, 'title'); - $feed->url = $url; + $root = $xml->getElementsByTagNameNS(self::ATOM_NS, 'feed')->item(0); + $updatedOn = self::atomValue($root, 'updated'); + if ($updatedOn == 'pubDate not found') $updatedOn = null; - $feed->updatedOn = self::atomValue($root, 'updated'); - if ($feed->updatedOn == 'pubDate not found') $feed->updatedOn = null; - $feed->updatedOn = Data::formatDate($feed->updatedOn); - - foreach ($root->getElementsByTagName('entry') as $xmlItem) { - $guid = self::atomValue($xmlItem, 'id'); - $link = ''; - foreach ($xmlItem->getElementsByTagName('link') as $linkElt) { - if ($linkElt->hasAttributes()) { - $relAttr = $linkElt->attributes->getNamedItem('rel'); - if ($relAttr && $relAttr->value == 'alternate') { - $link = $linkElt->attributes->getNamedItem('href')->value; - break; - } - } - } - if ($link == '' && str_starts_with($guid, 'http')) $link = $guid; - - $item = new FeedItem(); - $item->guid = $guid; - $item->title = self::atomValue($xmlItem, 'title'); - $item->link = $link; - $item->publishedOn = Data::formatDate(self::atomValue($xmlItem, 'published')); - $item->updatedOn = Data::formatDate(self::atomValue($xmlItem, 'updated')); - $item->content = self::atomValue($xmlItem, 'content'); - $feed->items[] = $item; - } + $feed = new Feed(); + $feed->title = self::atomValue($root, 'title'); + $feed->url = $url; + $feed->updatedOn = Data::formatDate($updatedOn); + foreach ($root->getElementsByTagName('entry') as $entry) $feed->items[] = FeedItem::fromAtom($entry); return ['ok' => $feed]; } diff --git a/src/lib/Security.php b/src/lib/Security.php index c79ebed..f7e1af3 100644 --- a/src/lib/Security.php +++ b/src/lib/Security.php @@ -84,6 +84,20 @@ class Security { add_error('Invalid credentials; log on unsuccessful'); } + /** + * Update the password for the given user + * + * @param string $email The e-mail address of the user whose password should be updated + * @param string $password The new password for this user + * @param SQLite3 $db The database connection to use in updating the password + */ + public static function updatePassword(string $email, string $password, SQLite3 $db): void { + $query = $db->prepare('UPDATE frc_user SET password = :password WHERE email = :email'); + $query->bindValue(':password', password_hash($password, PASSWORD_DEFAULT)); + $query->bindValue(':email', $email); + $query->execute(); + } + /** * Log on the single user * diff --git a/src/public/index.php b/src/public/index.php index 3775c26..7b8de61 100644 --- a/src/public/index.php +++ b/src/public/index.php @@ -19,14 +19,17 @@ if (array_key_exists('refresh', $_GET)) { } } -$result = $db->query(<<<'SQL' +$query = $db->prepare(<<<'SQL' SELECT item.id, item.title AS item_title, coalesce(item.updated_on, item.published_on) AS as_of, feed.title AS feed_title FROM item INNER JOIN feed ON feed.id = item.feed_id - WHERE item.is_read = 0 + WHERE feed.user_id = :userId + AND item.is_read = 0 ORDER BY coalesce(item.updated_on, item.published_on) DESC SQL); +$query->bindValue(':userId', $_SESSION[Key::USER_ID]); +$result = $query->execute(); $item = $result ? $result->fetchArray(SQLITE3_ASSOC) : false; page_head('Welcome'); ?> @@ -34,7 +37,7 @@ page_head('Welcome'); ?>
-

>
+

>

fetchArray(SQLITE3_ASSOC); } diff --git a/src/public/item.php b/src/public/item.php index 43fe030..b7a2cfb 100644 --- a/src/public/item.php +++ b/src/public/item.php @@ -53,7 +53,7 @@ $updated = isset($item['updated_on']) ? date_time($item['updated_on']) : null; page_head(htmlentities("{$item['item_title']} | {$item['feed_title']}")); ?>

-
+

From
diff --git a/src/start.php b/src/start.php index 249c21f..ba8a5a3 100644 --- a/src/start.php +++ b/src/start.php @@ -1,18 +1,7 @@ 'FRCSESSION', diff --git a/src/util/user.php b/src/util/user.php new file mode 100644 index 0000000..81b8a34 --- /dev/null +++ b/src/util/user.php @@ -0,0 +1,177 @@ +prepare('SELECT COUNT(*) FROM frc_user WHERE email = :email'); + $existsQuery->bindValue(':email', $argv[2]); + $existsResult = $existsQuery->execute(); + if (!$existsResult) { + printfn('SQLite error: %s', $db->lastErrorMsg()); + return; + } + if ($existsResult->fetchArray(SQLITE3_NUM)[0] != 0) { + printfn('A user with e-mail address "%s" already exists', $argv[2]); + return; + } + + Security::addUser($argv[2], $argv[3], $db); + + printfn('User "%s" with password "%s" added successfully', $argv[2], $argv[3]); + } finally { + $db->close(); + } +} + +/** + * Set a user's password + */ +function set_password(): void { + global $argv; + + $db = Data::getConnection(); + + try { + // Ensure this user exists + $existsQuery = $db->prepare('SELECT COUNT(*) FROM frc_user WHERE email = :email'); + $existsQuery->bindValue(':email', $argv[2]); + $existsResult = $existsQuery->execute(); + if (!$existsResult) { + printfn('SQLite error: %s', $db->lastErrorMsg()); + return; + } + if ($existsResult->fetchArray(SQLITE3_NUM)[0] == 0) { + printfn('No user exists with e-mail address "%s"', $argv[2]); + return; + } + + Security::updatePassword($argv[2], $argv[3], $db); + + printfn('User "%s" password set to "%s" successfully', $argv[2], $argv[3]); + } finally { + $db->close(); + } +} + +/** + * Delete a user + */ +function delete_user(): void { + global $argv; + + $db = Data::getConnection(); + + try { + // Get the ID for the provided e-mail address + $idQuery = $db->prepare('SELECT id FROM frc_user WHERE email = :email'); + $idQuery->bindValue(':email', $argv[2]); + $idResult = $idQuery->execute(); + if (!$idResult) { + printfn('SQLite error: %s', $db->lastErrorMsg()); + return; + } + $id = $idResult->fetchArray(SQLITE3_NUM); + if (!$id) { + printfn('No user exists with e-mail address "%s"', $argv[2]); + return; + } + + $feedCountQuery = $db->prepare('SELECT COUNT(*) FROM feed WHERE user_id = :user'); + $feedCountQuery->bindValue(':user', $id[0]); + $feedCountResult = $feedCountQuery->execute(); + if (!$feedCountResult) { + printfn('SQLite error: %s', $db->lastErrorMsg()); + return; + } + $feedCount = $feedCountResult->fetchArray(SQLITE3_NUM); + + $proceed = readline("Delete user \"$argv[2]\" and their $feedCount[0] feed(s)? (y/N)" . PHP_EOL); + if (!$proceed || !str_starts_with(strtolower($proceed), 'y')) { + printfn('Deletion canceled'); + return; + } + + $itemDelete = $db->prepare('DELETE FROM item WHERE feed_id IN (SELECT id FROM feed WHERE user_id = :user)'); + $itemDelete->bindValue(':user', $id[0]); + $itemDelete->execute(); + + $feedDelete = $db->prepare('DELETE FROM feed WHERE user_id = :user'); + $feedDelete->bindValue(':user', $id[0]); + $feedDelete->execute(); + + $userDelete = $db->prepare('DELETE FROM frc_user WHERE id = :user'); + $userDelete->bindValue(':user', $id[0]); + $userDelete->execute(); + + printfn('User "%s" deleted successfully', $argv[2]); + } finally { + $db->close(); + } +}