mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 13:12:41 +00:00
Started integration of PicoFeed
• Integrated PicoFeed into Database->subscriptionAdd • Added exception handling for feeds • Added static method for formatting SQL dates into Db/Common
This commit is contained in:
parent
e6feb8de8d
commit
e5d825d360
5 changed files with 110 additions and 20 deletions
|
@ -39,6 +39,17 @@ abstract class AbstractException extends \Exception {
|
|||
"User/Exception.authMissing" => 10411,
|
||||
"User/Exception.authFailed" => 10412,
|
||||
"User/Exception.notAuthorized" => 10421,
|
||||
"Feed/Exception.invalidCertificate" => 10501,
|
||||
"Feed/Exception.invalidUrl" => 10502,
|
||||
"Feed/Exception.maxRedirect" => 10503,
|
||||
"Feed/Exception.maxSize" => 10504,
|
||||
"Feed/Exception.timeout" => 10505,
|
||||
"Feed/Exception.forbidden" => 10506,
|
||||
"Feed/Exception.unauthorized" => 10507,
|
||||
"Feed/Exception.malformed" => 10511,
|
||||
"Feed/Exception.xmlEntity" => 10512,
|
||||
"Feed/Exception.subscriptionNotFound" => 10521,
|
||||
"Feed/Exception.unsupportedFeedFormat" => 10522
|
||||
];
|
||||
|
||||
public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {
|
||||
|
|
|
@ -3,13 +3,17 @@ declare(strict_types=1);
|
|||
namespace JKingWeb\NewsSync;
|
||||
|
||||
class Database {
|
||||
use PicoFeed\Reader\Reader;
|
||||
use PicoFeed\PicoFeedException;
|
||||
|
||||
const SCHEMA_VERSION = 1;
|
||||
const FORMAT_TS = "Y-m-d h:i:s";
|
||||
const FORMAT_DATE = "Y-m-d";
|
||||
const FORMAT_TIME = "h:i:s";
|
||||
|
||||
|
||||
protected $data;
|
||||
public $db;
|
||||
private $driver;
|
||||
|
||||
protected function cleanName(string $name): string {
|
||||
return (string) preg_filter("[^0-9a-zA-Z_\.]", "", $name);
|
||||
|
@ -17,8 +21,8 @@ class Database {
|
|||
|
||||
public function __construct(RuntimeData $data) {
|
||||
$this->data = $data;
|
||||
$driver = $data->conf->dbDriver;
|
||||
$this->db = $driver::create($data, INSTALL);
|
||||
$this->driver = $data->conf->dbDriver;
|
||||
$this->db = $this->driver::create($data, INSTALL);
|
||||
$ver = $this->db->schemaVersion();
|
||||
if(!INSTALL && $ver < self::SCHEMA_VERSION) {
|
||||
$this->db->update(self::SCHEMA_VERSION);
|
||||
|
@ -36,7 +40,7 @@ class Database {
|
|||
if(class_exists($name)) {
|
||||
$classes[$name] = $name::driverName();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $classes;
|
||||
}
|
||||
|
@ -72,12 +76,12 @@ class Database {
|
|||
switch(gettype($in)) {
|
||||
case "boolean": $type = "bool"; break;
|
||||
case "integer": $type = "int"; break;
|
||||
case "double": $type = "numeric"; break;
|
||||
case "double": $type = "numeric"; break;
|
||||
case "string":
|
||||
case "array": $type = "json"; break;
|
||||
case "array": $type = "json"; break;
|
||||
case "resource":
|
||||
case "unknown type":
|
||||
case "NULL": $type = "null"; break;
|
||||
case "NULL": $type = "null"; break;
|
||||
case "object":
|
||||
if($in instanceof DateTimeInterface) {
|
||||
$type = "timestamp";
|
||||
|
@ -85,7 +89,7 @@ class Database {
|
|||
$type = "text";
|
||||
}
|
||||
break;
|
||||
default: $type = 'null'; break;
|
||||
default: $type = 'null'; break;
|
||||
}
|
||||
}
|
||||
$type = strtolower($type);
|
||||
|
@ -113,7 +117,7 @@ class Database {
|
|||
$value = json_encode($in);
|
||||
} else {
|
||||
$value =& $in;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "datetime":
|
||||
$type = "timestamp";
|
||||
|
@ -201,13 +205,13 @@ class Database {
|
|||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
|
||||
public function userPasswordGet(string $user): string {
|
||||
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||
if(!$this->userExists($user)) return "";
|
||||
return (string) $this->db->prepare("SELECT password from newssync_users where id is ?", "str")->run($user)->getSingle();
|
||||
}
|
||||
|
||||
|
||||
public function userPasswordSet(string $user, string $password = null): bool {
|
||||
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||
if(!$this->userExists($user)) return false;
|
||||
|
@ -226,13 +230,13 @@ class Database {
|
|||
public function userPropertiesSet(string $user, array &$properties): array {
|
||||
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||
$valid = [ // FIXME: add future properties
|
||||
"name" => "str",
|
||||
"name" => "str",
|
||||
];
|
||||
if(!$this->userExists($user)) return [];
|
||||
$this->db->begin();
|
||||
foreach($valid as $prop => $type) {
|
||||
if(!array_key_exists($prop, $properties)) continue;
|
||||
$this->db->prepare("UPDATE newssync_users set $prop = ? where id is ?", $type, "str")->run($properties[$prop], $user);
|
||||
$this->db->prepare("UPDATE newssync_users set $prop = ? where id is ?", $type, "str")->run($properties[$prop], $user);
|
||||
}
|
||||
$this->db->commit();
|
||||
return $this->userPropertiesGet($user);
|
||||
|
@ -251,15 +255,55 @@ class Database {
|
|||
}
|
||||
|
||||
public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = ""): int {
|
||||
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||
if(!$this->userExists($user)) throw new User\Exception("doesNotExist", ["user" => $user, "action" => __FUNCTION__]);
|
||||
// If the user isn't authorized to perform this action then throw an exception.
|
||||
if (!$this->data->user->authorize($user, __FUNCTION__)) {
|
||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||
}
|
||||
// If the user doesn't exist throw an exception.
|
||||
if (!$this->userExists($user)) {
|
||||
throw new User\Exception("doesNotExist", ["user" => $user, "action" => __FUNCTION__]);
|
||||
}
|
||||
|
||||
$this->db->begin();
|
||||
|
||||
// If the feed doesn't already exist in the database then add it to the database after determining its validity with picoFeed
|
||||
$qFeed = $this->db->prepare("SELECT id from newssync_feeds where url is ? and username is ? and password is ?", "str", "str", "str");
|
||||
$feed = $qFeed->run($url, $fetchUser, $fetchPassword)->getSingle();
|
||||
if($feed===null) {
|
||||
$this->db->prepare("INSERT INTO newssync_feeds(url,username,password) values(?,?,?)", "str", "str", "str")->run($url, $fetchUser, $fetchPassword);
|
||||
if ($feed === null) {
|
||||
try {
|
||||
$reader = new Reader;
|
||||
$resource = $reader->download($url);
|
||||
|
||||
$parser = $reader->getParser(
|
||||
$resource->getUrl(),
|
||||
$resource->getContent(),
|
||||
$resource->getEncoding()
|
||||
);
|
||||
|
||||
$feed = $parser->execute();
|
||||
} catch (PicoFeedException $e) {
|
||||
// If there's any error while trying to download or parse the feed then return an exception
|
||||
throw new Feed\Exception($url, $e);
|
||||
}
|
||||
|
||||
$this->db->prepare("INSERT INTO newssync_feeds(url,title,favicon,source,updated,modified,etag,username,password) values(?,?,?)", "str", "str", "str", "str", "str", "str", "str", "str", "str")->run(
|
||||
$url,
|
||||
$feed->title,
|
||||
// Grab the favicon for the feed. Returns an empty string if it cannot find one
|
||||
(new PicoFeed\Reader\Favicon)->find($url),
|
||||
$feed->siteUrl,
|
||||
// Convert the date formats to ISO 8601 before inserting
|
||||
$driver::formatDate($feed->date),
|
||||
$driver::formatDate($resource->getLastModified()),
|
||||
$resource->getEtag(),
|
||||
$fetchUser,
|
||||
$fetchPassword
|
||||
);
|
||||
|
||||
$feed = $qFeed->run($url, $fetchUser, $fetchPassword)->getSingle();
|
||||
}
|
||||
|
||||
// Add the feed to a user's subscriptions.
|
||||
$this->db->prepare("INSERT INTO newssync_subscriptions(owner,feed) values(?,?)", "str", "int")->run($user,$feed);
|
||||
$sub = $this->db->prepare("SELECT id from newssync_subscriptions where owner is ? and feed is ?", "str", "int")->run($user, $feed)->getSingle();
|
||||
$this->db->commit();
|
||||
|
|
|
@ -5,7 +5,7 @@ use JKingWeb\DrUUID\UUID as UUID;
|
|||
|
||||
Trait Common {
|
||||
protected $transDepth = 0;
|
||||
|
||||
|
||||
public function schemaVersion(): int {
|
||||
try {
|
||||
return $this->data->db->settingGet("schema_version");
|
||||
|
@ -13,7 +13,7 @@ Trait Common {
|
|||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function begin(): bool {
|
||||
$this->exec("SAVEPOINT newssync_".($this->transDepth));
|
||||
$this->transDepth += 1;
|
||||
|
@ -27,7 +27,7 @@ Trait Common {
|
|||
$this->transDepth -= 1;
|
||||
} else {
|
||||
$this->exec("COMMIT TRANSACTION");
|
||||
$this->transDepth = 0;
|
||||
$this->transDepth = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -70,4 +70,13 @@ Trait Common {
|
|||
return $this->prepareArray($query, $paramType);
|
||||
}
|
||||
|
||||
public static function formatDate(string $date): string {
|
||||
// Force UTC.
|
||||
$timezone = date_default_timezone_get();
|
||||
date_default_timezone_set('UTC');
|
||||
// ISO 8601 with space in the middle instead of T.
|
||||
$date = date('Y-m-d h:i:sP', strtotime($date));
|
||||
date_default_timezone_set($timezone);
|
||||
return $date;
|
||||
}
|
||||
}
|
14
lib/Feed/Exception.php
Normal file
14
lib/Feed/Exception.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\NewsSync\Feed;
|
||||
|
||||
class Exception extends \JKingWeb\NewsSync\AbstractException {
|
||||
public function __construct($url, \Throwable $e) {
|
||||
$className = get_class($e);
|
||||
// Convert the exception thrown by PicoFeed to the one to be thrown here.
|
||||
$msgID = preg_replace('/^PicoFeed\\\(?:Client|Parser|Reader)\\\([A-Za-z]+)Exception$/', '$1', $className);
|
||||
// If the message ID doesn't change then it's unknown.
|
||||
$msgID = ($msgID !== $className) ? lcfirst($msgID) : '';
|
||||
parent::__construct($msgID, ['url' => $url], $e);
|
||||
}
|
||||
}
|
|
@ -51,4 +51,16 @@ return [
|
|||
'Exception.JKingWeb/NewsSync/User/Exception.authMissing' => 'Please log in to proceed',
|
||||
'Exception.JKingWeb/NewsSync/User/Exception.authFailed' => 'Authentication failed',
|
||||
'Exception.JKingWeb/NewsSync/User/ExceptionAuthz.notAuthorized' => 'Authenticated user is not authorized to perform the action "{action}" on behalf of {user}',
|
||||
|
||||
'Exception.JKingWeb/NewsSync/Feed/Exception.invalidCertificate' => 'Could not download feed "{url}" because its server is serving an invalid SSL certificate',
|
||||
'Exception.JKingWeb/NewsSync/Feed/Exception.invalidURL' => 'Feed URL "{url}" is invalid',
|
||||
'Exception.JKingWeb/NewsSync/Feed/Exception.maxRedirect' => 'Could not download feed "{url}" because its server reached its maximum number of HTTP redirections',
|
||||
'Exception.JKingWeb/NewsSync/Feed/Exception.maxSize' => 'Could not download feed "{url}" because its size exceeds the maximum allowed on its server',
|
||||
'Exception.JKingWeb/NewsSync/Feed/Exception.timeout' => 'Could not download feed "{url}" because its server timed out',
|
||||
'Exception.JKingWeb/NewsSync/Feed/Exception.forbidden' => 'Could not download feed "{url}" because you do not have permission to access it',
|
||||
'Exception.JKingWeb/NewsSync/Feed/Exception.unauthorized' => 'Could not download feed "{url}" because you provided insufficient or invalid credentials',
|
||||
'Exception.JKingWeb/NewsSync/Feed/Exception.malformed' => 'Could not parse feed "{url}" because it is malformed',
|
||||
'Exception.JKingWeb/NewsSync/Feed/Exception.xxe' => 'Refused to parse feed "{url}" because it contains an XXE attack',
|
||||
'Exception.JKingWeb/NewsSync/Feed/Exception.subscriptionNotFound' => 'Unable to find a feed at location "{url}"',
|
||||
'Exception.JKingWeb/NewsSync/Feed/Exception.unsupportedFormat' => 'Feed "{url}" is of an unsupported format'
|
||||
];
|
Loading…
Reference in a new issue