Arsse/lib/Database.php

597 lines
30 KiB
PHP
Raw Normal View History

2016-10-02 17:07:17 -04:00
<?php
declare(strict_types=1);
2017-03-28 00:12:12 -04:00
namespace JKingWeb\Arsse;
use PasswordGenerator\Generator as PassGen;
2016-10-02 17:07:17 -04:00
class Database {
2017-02-16 15:29:42 -05:00
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";
2017-02-16 15:29:42 -05:00
protected $data;
public $db;
private $driver;
protected function processUpdate(array $props, array $valid, array $where): array {
$out = [
'values' => [],
'types' => [],
'set' => [],
'where' => [],
];
foreach($valid as $prop => $type) {
if(!array_key_exists($prop, $props)) continue;
$out['values'][] = $props[$prop];
$out['types'][] = $type;
$out['set'][] = "$prop = ?";
}
foreach($where as $field => $value) {
$out['values'][] = $value[0];
$out['types'][] = $value[1];
$out['where'][] = "$field is ?";
}
$out['set'] = implode(", ", $out['set']);
$out['where'] = implode(" and ", $out['where']);
return $out;
2017-02-16 15:29:42 -05:00
}
2016-10-02 17:07:17 -04:00
public function __construct(Db\Driver $db = null) {
// if we're fed a pre-prepared driver, use it'
if($db) {
$this->db = $db;
} else {
$this->driver = $driver = Data::$conf->dbDriver;
$this->db = new $driver(INSTALL);
$ver = $this->db->schemaVersion();
if(!INSTALL && $ver < self::SCHEMA_VERSION) {
$this->db->schemaUpdate(self::SCHEMA_VERSION);
}
2017-02-16 15:29:42 -05:00
}
}
2016-10-02 17:07:17 -04:00
2017-02-16 15:29:42 -05:00
static public function listDrivers(): array {
$sep = \DIRECTORY_SEPARATOR;
$path = __DIR__.$sep."Db".$sep;
$classes = [];
foreach(glob($path."*".$sep."Driver.php") as $file) {
$name = basename(dirname($file));
$class = NS_BASE."Db\\$name\\Driver";
$classes[$class] = $class::driverName();
2017-02-16 15:29:42 -05:00
}
return $classes;
}
2017-02-16 15:29:42 -05:00
public function schemaVersion(): int {
return $this->db->schemaVersion();
}
public function schemaUpdate(): bool {
if($this->db->schemaVersion() < self::SCHEMA_VERSION) return $this->db->schemaUpdate(self::SCHEMA_VERSION);
2017-02-16 15:29:42 -05:00
return false;
}
2017-02-16 15:29:42 -05:00
public function settingGet(string $key) {
2017-03-28 00:12:12 -04:00
$row = $this->db->prepare("SELECT value, type from arsse_settings where key = ?", "str")->run($key)->getRow();
2017-02-16 15:29:42 -05:00
if(!$row) return null;
switch($row['type']) {
case "int": return (int) $row['value'];
case "numeric": return (float) $row['value'];
case "text": return $row['value'];
case "json": return json_decode($row['value']);
case "timestamp": return date_create_from_format("!".self::FORMAT_TS, $row['value'], new DateTimeZone("UTC"));
case "date": return date_create_from_format("!".self::FORMAT_DATE, $row['value'], new DateTimeZone("UTC"));
case "time": return date_create_from_format("!".self::FORMAT_TIME, $row['value'], new DateTimeZone("UTC"));
case "bool": return (bool) $row['value'];
case "null": return null;
default: return $row['value'];
}
}
2016-10-17 16:49:39 -04:00
2017-02-16 15:29:42 -05:00
public function settingSet(string $key, $in, string $type = null): bool {
if(!$type) {
switch(gettype($in)) {
case "boolean": $type = "bool"; break;
case "integer": $type = "int"; break;
case "double": $type = "numeric"; break;
2017-02-16 15:29:42 -05:00
case "string":
case "array": $type = "json"; break;
2017-02-16 15:29:42 -05:00
case "resource":
case "unknown type":
case "NULL": $type = "null"; break;
2017-02-16 15:29:42 -05:00
case "object":
if($in instanceof DateTimeInterface) {
$type = "timestamp";
} else {
$type = "text";
}
break;
default: $type = 'null'; break;
2017-02-16 15:29:42 -05:00
}
}
$type = strtolower($type);
switch($type) {
case "integer":
$type = "int";
case "int":
2017-03-09 17:14:26 -05:00
$value = $in;
2017-02-16 15:29:42 -05:00
break;
case "float":
case "double":
case "real":
$type = "numeric";
case "numeric":
2017-03-09 17:14:26 -05:00
$value = $in;
2017-02-16 15:29:42 -05:00
break;
case "str":
case "string":
$type = "text";
case "text":
2017-03-09 17:14:26 -05:00
$value = $in;
2017-02-16 15:29:42 -05:00
break;
case "json":
if(is_array($in) || is_object($in)) {
$value = json_encode($in);
} else {
2017-03-09 17:14:26 -05:00
$value = $in;
}
2017-02-16 15:29:42 -05:00
break;
case "datetime":
$type = "timestamp";
case "timestamp":
if($in instanceof DateTimeInterface) {
$value = gmdate(self::FORMAT_TS, $in->format("U"));
} else if(is_numeric($in)) {
$value = gmdate(self::FORMAT_TS, $in);
} else {
$value = gmdate(self::FORMAT_TS, gmstrftime($in));
}
break;
case "date":
if($in instanceof DateTimeInterface) {
$value = gmdate(self::FORMAT_DATE, $in->format("U"));
} else if(is_numeric($in)) {
$value = gmdate(self::FORMAT_DATE, $in);
} else {
$value = gmdate(self::FORMAT_DATE, gmstrftime($in));
}
break;
case "time":
if($in instanceof DateTimeInterface) {
$value = gmdate(self::FORMAT_TIME, $in->format("U"));
} else if(is_numeric($in)) {
$value = gmdate(self::FORMAT_TIME, $in);
} else {
$value = gmdate(self::FORMAT_TIME, gmstrftime($in));
}
break;
case "boolean":
case "bit":
$type = "bool";
case "bool":
$value = (int) $in;
break;
case "null":
$value = null;
break;
default:
$type = "text";
2017-03-09 17:14:26 -05:00
$value = $in;
2017-02-16 15:29:42 -05:00
break;
}
2017-03-28 00:12:12 -04:00
return (bool) $this->db->prepare("REPLACE INTO arsse_settings(key,value,type) values(?,?,?)", "str", "str", "str")->run($key, $value, $type)->changes();
2017-02-16 15:29:42 -05:00
}
2016-10-17 16:49:39 -04:00
2017-02-16 15:29:42 -05:00
public function settingRemove(string $key): bool {
2017-03-28 00:12:12 -04:00
$this->db->prepare("DELETE from arsse_settings where key is ?", "str")->run($key);
2017-02-16 15:29:42 -05:00
return true;
}
2016-10-17 16:49:39 -04:00
2017-02-16 15:29:42 -05:00
public function userExists(string $user): bool {
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
2017-03-28 00:12:12 -04:00
return (bool) $this->db->prepare("SELECT count(*) from arsse_users where id is ?", "str")->run($user)->getValue();
2017-02-16 15:29:42 -05:00
}
public function userAdd(string $user, string $password = null): string {
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if($this->userExists($user)) throw new User\Exception("alreadyExists", ["action" => __FUNCTION__, "user" => $user]);
if($password===null) $password = (new PassGen)->length(Data::$conf->userTempPasswordLength)->get();
$hash = "";
if(strlen($password) > 0) $hash = password_hash($password, \PASSWORD_DEFAULT);
2017-03-28 00:12:12 -04:00
$this->db->prepare("INSERT INTO arsse_users(id,password) values(?,?)", "str", "str")->runArray([$user,$hash]);
return $password;
2017-02-16 15:29:42 -05:00
}
2017-02-16 15:29:42 -05:00
public function userRemove(string $user): bool {
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
2017-03-28 00:12:12 -04:00
if($this->db->prepare("DELETE from arsse_users where id is ?", "str")->run($user)->changes() < 1) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
2017-02-16 15:29:42 -05:00
return true;
}
2017-02-16 15:29:42 -05:00
public function userList(string $domain = null): array {
$out = [];
2017-02-16 15:29:42 -05:00
if($domain !== null) {
if(!Data::$user->authorize("@".$domain, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]);
2017-02-16 15:29:42 -05:00
$domain = str_replace(["\\","%","_"],["\\\\", "\\%", "\\_"], $domain);
$domain = "%@".$domain;
foreach($this->db->prepare("SELECT id from arsse_users where id like ?", "str")->run($domain) as $user) {
$out[] = $user['id'];
}
2017-02-16 15:29:42 -05:00
} else {
if(!Data::$user->authorize("", __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "global"]);
foreach($this->db->prepare("SELECT id from arsse_users")->run() as $user) {
$out[] = $user['id'];
}
2017-02-16 15:29:42 -05:00
}
return $out;
2017-02-16 15:29:42 -05:00
}
2017-02-16 15:29:42 -05:00
public function userPasswordGet(string $user): string {
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
2017-03-28 00:12:12 -04:00
return (string) $this->db->prepare("SELECT password from arsse_users where id is ?", "str")->run($user)->getValue();
2017-02-16 15:29:42 -05:00
}
public function userPasswordSet(string $user, string $password = null): string {
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
if($password===null) $password = (new PassGen)->length(Data::$conf->userTempPasswordLength)->get();
$hash = "";
if(strlen($password) > 0) $hash = password_hash($password, \PASSWORD_DEFAULT);
2017-03-28 00:12:12 -04:00
$this->db->prepare("UPDATE arsse_users set password = ? where id is ?", "str", "str")->run($hash, $user);
return $password;
2017-02-16 15:29:42 -05:00
}
2017-02-16 15:29:42 -05:00
public function userPropertiesGet(string $user): array {
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
2017-03-28 00:12:12 -04:00
$prop = $this->db->prepare("SELECT name,rights from arsse_users where id is ?", "str")->run($user)->getRow();
if(!$prop) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
2017-02-16 15:29:42 -05:00
return $prop;
}
public function userPropertiesSet(string $user, array $properties): array {
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
2017-02-16 15:29:42 -05:00
$valid = [ // FIXME: add future properties
"name" => "str",
2017-02-16 15:29:42 -05:00
];
$data = $this->processUpdate($properties, $valid, ['id' => [$user, "str"]]);
extract($data);
$this->db->prepareArray("UPDATE arsse_users set $set where $where", $types)->runArray($values);
2017-02-16 15:29:42 -05:00
return $this->userPropertiesGet($user);
}
2016-11-03 22:54:27 -04:00
2017-02-16 15:29:42 -05:00
public function userRightsGet(string $user): int {
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
2017-03-28 00:12:12 -04:00
return (int) $this->db->prepare("SELECT rights from arsse_users where id is ?", "str")->run($user)->getValue();
2017-02-16 15:29:42 -05:00
}
2016-11-03 22:54:27 -04:00
2017-02-16 15:29:42 -05:00
public function userRightsSet(string $user, int $rights): bool {
if(!Data::$user->authorize($user, __FUNCTION__, $rights)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if(!$this->userExists($user)) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
2017-03-28 00:12:12 -04:00
$this->db->prepare("UPDATE arsse_users set rights = ? where id is ?", "int", "str")->run($rights, $user);
2017-02-16 15:29:42 -05:00
return true;
}
public function folderAdd(string $user, array $data): int {
// If the user isn't authorized to perform this action then throw an exception.
if(!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__]);
}
// if the desired folder name is missing or invalid, throw an exception
2017-03-31 15:27:59 -04:00
if(!array_key_exists("name", $data) || $data['name']=="") {
throw new Db\ExceptionInput("missing", ["action" => __FUNCTION__, "field" => "name"]);
} else if(!strlen(trim($data['name']))) {
throw new Db\ExceptionInput("whitespace", ["action" => __FUNCTION__, "field" => "name"]);
}
// normalize folder's parent, if there is one
$parent = array_key_exists("parent", $data) ? (int) $data['parent'] : 0;
if($parent===0) {
// if no parent is specified, do nothing
$parent = null;
} else {
// if a parent is specified, make sure it exists and belongs to the user; get its root (first-level) folder if it's a nested folder
$p = $this->db->prepare("SELECT id from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $parent)->getValue();
if(!$p) throw new Db\ExceptionInput("idMissing", ["action" => __FUNCTION__, "field" => "parent", 'id' => $parent]);
}
// check if a folder by the same name already exists, because nulls are wonky in SQL
// FIXME: How should folder name be compared? Should a Unicode normalization be applied before comparison and insertion?
2017-03-28 00:12:12 -04:00
if($this->db->prepare("SELECT count(*) from arsse_folders where owner is ? and parent is ? and name is ?", "str", "int", "str")->run($user, $parent, $data['name'])->getValue() > 0) {
throw new Db\ExceptionInput("constraintViolation"); // FIXME: There needs to be a practical message here
}
// actually perform the insert (!)
return $this->db->prepare("INSERT INTO arsse_folders(owner,parent,name) values(?,?,?)", "str", "int", "str")->run($user, $parent, $data['name'])->lastId();
}
public function folderList(string $user, int $parent = null, bool $recursive = true): Db\Result {
// if the user isn't authorized to perform this action then throw an exception.
if(!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__]);
}
// check to make sure the parent exists, if one is specified
if(!is_null($parent)) {
if(!$this->db->prepare("SELECT count(*) from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $parent)->getValue()) {
throw new Db\ExceptionInput("idMissing", ["action" => __FUNCTION__, "field" => "parent", 'id' => $parent]);
}
}
// if we're not returning a recursive list we can use a simpler query
if(!$recursive) {
return $this->db->prepare("SELECT id,name,parent from arsse_folders where owner is ? and parent is ?", "str", "int")->run($user, $parent);
} else {
return $this->db->prepare(
2017-03-28 00:12:12 -04:00
"WITH RECURSIVE folders(id) as (SELECT id from arsse_folders where owner is ? and parent is ? union select arsse_folders.id from arsse_folders join folders on arsse_folders.parent=folders.id) ".
"SELECT id,name,parent from arsse_folders where id in(SELECT id from folders) order by name",
2017-03-27 08:39:24 -04:00
"str", "int")->run($user, $parent);
}
}
2017-03-31 18:48:24 -04:00
public function folderRemove(string $user, int $id): bool {
if(!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__]);
$changes = $this->db->prepare("DELETE FROM arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $id)->changes();
if(!$changes) throw new Db\ExceptionInput("idMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]);
return true;
}
public function folderPropertiesGet(string $user, int $id): array {
if(!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__]);
$props = $this->db->prepare("SELECT id,name,parent from arsse_folders where owner is ? and id is ?", "str", "int")->run($user, $id)->getRow();
if(!$props) throw new Db\ExceptionInput("idMissing", ["action" => __FUNCTION__, "field" => "folder", 'id' => $id]);
return $props;
}
public function folderPropertiesSet(string $user, int $id, array $data): bool {
if(!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__]);
// layer the existing folder properties onto the new desired one
$data = array_merge($this->folderPropertiesGet($user, $id), $data);
// if the desired folder name is missing or invalid, throw an exception
if(!array_key_exists("name", $data) || $data['name']=="") {
throw new Db\ExceptionInput("missing", ["action" => __FUNCTION__, "field" => "name"]);
} else if(!strlen(trim($data['name']))) {
throw new Db\ExceptionInput("whitespace", ["action" => __FUNCTION__, "field" => "name"]);
2017-03-31 18:48:24 -04:00
}
// normalize folder's parent, if there is one
$parent = array_key_exists("parent", $data) ? (int) $data['parent'] : 0;
if($parent===0) {
// if no parent is specified, do nothing
$parent = null;
} else {
// if a parent is specified, make sure it exists and belongs to the user; get its root (first-level) folder if it's a nested folder
$p = $this->db->prepare(
"WITH RECURSIVE folders(id) as (SELECT id from arsse_folders where owner is ? and id is ? union select arsse_folders.id from arsse_folders join folders on arsse_folders.parent=folders.id) ".
"SELECT id,(id not in (select id from folders)) as valid from arsse_folders where owner is ? and id is ?",
"str", "int", "str", "int")->run($user, $id, $user, $parent)->getRow();
if(!$p) {
throw new Db\ExceptionInput("idMissing", ["action" => __FUNCTION__, "field" => "parent", 'id' => $parent]);
} else {
// if using the desired parent would create a circular dependence, throw an exception
if(!$p['valid']) throw new Db\ExceptionInput("circularDependence", ["action" => __FUNCTION__, "field" => "parent", 'id' => $parent]);
}
}
$data['parent'] = $parent;
// check to make sure the target folder name/location would not create a duplicate (we must di this check because null is not distinct in SQL)
$existing = $this->db->prepare("SELECT id from arsse_folders where owner is ? and parent is ? and name is ?", "str", "int", "str")->run($user, $data['parent'], $data['name'])->getValue();
if(!is_null($existing) && $existing != $id) {
throw new Db\ExceptionInput("constraintViolation"); // FIXME: There needs to be a practical message here
}
$valid = [
'name' => "str",
'parent' => "int",
];
$data = $this->processUpdate($data, $valid, ['owner' => [$user, "str"], 'id' => [$id, "int"]]);
extract($data);
$this->db->prepareArray("UPDATE arsse_folders set $set where $where", $types)->runArray($values);
return true;
2017-03-31 18:48:24 -04:00
}
public function subscriptionAdd(string $user, string $url, string $fetchUser = "", string $fetchPassword = ""): int {
// If the user isn't authorized to perform this action then throw an exception.
if(!Data::$user->authorize($user, __FUNCTION__)) {
2017-03-31 18:48:24 -04:00
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
}
// If the user doesn't exist throw an exception.
if(!$this->userExists($user)) {
2017-03-31 18:48:24 -04:00
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 arsse_feeds where url is ? and username is ? and password is ?", "str", "str", "str");
$feed = $qFeed->run($url, $fetchUser, $fetchPassword)->getValue();
if($feed === null) {
2017-03-31 18:48:24 -04:00
$feed = new Feed($url);
$feed->parse();
// Add the feed to the database and return its Id which will be used when adding
// its articles to the database.
$feedID = $this->db->prepare(
'INSERT INTO arsse_feeds(url,title,favicon,source,updated,modified,etag,username,password)
values(?,?,?,?,?,?,?,?,?)',
'str', 'str', 'str', 'str', 'datetime', 'datetime', 'str', 'str', 'str')->run(
$url,
$feed->data->title,
// Grab the favicon for the feed; returns an empty string if it cannot find one.
$feed->favicon,
$feed->data->siteUrl,
$feed->data->date,
$feed->resource->getLastModified(),
$feed->resource->getEtag(),
$fetchUser,
$fetchPassword
)->lastId();
// Add each of the articles to the database.
foreach($feed->data->items as $i) {
$this->articleAdd($feedID, $i);
2017-03-31 18:48:24 -04:00
}
}
// Add the feed to the user's subscriptions.
$sub = $this->db->prepare('INSERT INTO arsse_subscriptions(owner,feed) values(?,?)', 'str', 'int')->run($user, $feedID)->lastId();
$this->db->commit();
return $sub;
}
public function subscriptionRemove(string $user, int $id): bool {
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
return (bool) $this->db->prepare("DELETE from arsse_subscriptions where owner is ? and id is ?", "str", "int")->run($user, $id)->changes();
}
public function articleAdd(int $feedID, \PicoFeed\Parser\Item $article): int {
$this->db->begin();
$articleID = $this->db->prepare('INSERT INTO arsse_articles(feed,url,title,author,published,edited,guid,content,url_title_hash,url_content_hash,title_content_hash)
values(?,?,?,?,?,?,?,?,?,?,?)',
'int', 'str', 'str', 'str', 'datetime', 'datetime', 'str', 'str', 'str', 'str', 'str')->run(
$feedID,
$article->url,
$article->title,
$article->author,
$article->publishedDate,
$article->updatedDate,
$article->id,
$article->content,
$article->urlTitleHash,
$article->urlContentHash,
$article->titleContentHash
)->lastId();
// If the article has categories add them into the categories database.
$this->categoriesAdd($articleID, $article);
$this->db->commit();
return 1;
}
public function categoriesAdd(int $articleID, \PicoFeed\Parser\Item $article): int {
$this->db->begin();
$categories = $article->getTag('category');
if(count($categories) > 0) {
foreach($categories as $c) {
$this->db->prepare('INSERT INTO arsse_categories(article,name) values(?,?)', 'int', 'str')->run($articleID, $c);
}
}
$this->db->commit();
return count($categories);
}
public function updateFeeds(): int {
2017-03-28 00:12:12 -04:00
$feeds = $this->db->query('SELECT id, url, username, password, DATEFORMAT("http", modified) AS lastmodified, etag FROM arsse_feeds')->getAll();
foreach($feeds as $f) {
// Feed object throws an exception when there are problems, but that isn't ideal
// here. When an exception is occurred it should update the database with the
// error instead of failing.
try {
$feed = new Feed($f['url'], $f['lastmodified'], $f['etag'], $f['username'], $f['password']);
} catch (Feed\Exception $e) {
$this->db->prepare('UPDATE arsse_feeds SET err_count = err_count + 1, err_msg = "" WHERE id is ?', 'str', 'int')->run(
$e->getMessage(),
$f['id']
);
continue;
}
// If the feed has been updated then update the database.
if($feed->resource->isModified()) {
$feed->parse();
$this->db->begin();
2017-03-28 00:12:12 -04:00
$articles = $this->db->prepare('SELECT id, url, title, author, DATEFORMAT("http", edited) AS edited_date, guid, content, url_title_hash, url_content_hash, title_content_hash FROM arsse_articles WHERE feed is ? ORDER BY id', 'int')->run($f['id'])->getAll();
foreach($feed->data->items as $i) {
// Iterate through the articles in the database to determine a match for the one
// in the just-parsed feed.
$match = null;
foreach($articles as $a) {
// If the id exists and is equal to one in the database then this is the post.
if($i->id) {
if($i->id === $a['guid']) {
$match = $a;
}
}
// Otherwise if the id doesn't exist and any of the hashes match then this is
// the post.
elseif($i->urlTitleHash === $a['url_title_hash'] || $i->urlContentHash === $a['url_content_hash'] || $i->titleContentHash === $a['title_content_hash']) {
$match = $a;
}
}
// If there is no match then this is a new post and must be added to the
// database.
if(!$match) {
$this->articleAdd($i);
continue;
}
// With that out of the way determine if the post has been updated.
// If there is an updated date, and it doesn't match the database's then update
// the post.
$update = false;
if($i->updatedDate) {
if($i->updatedDate !== $match['edited_date']) {
$update = true;
}
}
// Otherwise if there isn't an updated date and any of the hashes don't match
// then update the post.
elseif($i->urlTitleHash !== $match['url_title_hash'] || $i->urlContentHash !== $match['url_content_hash'] || $i->titleContentHash !== $match['title_content_hash']) {
$update = true;
}
if($update) {
2017-03-28 00:12:12 -04:00
$this->db->prepare('UPDATE arsse_articles SET url = ?, title = ?, author = ?, published = ?, edited = ?, modified = ?, guid = ?, content = ?, url_title_hash = ?, url_content_hash = ?, title_content_hash = ? WHERE id is ?', 'str', 'str', 'str', 'datetime', 'datetime', 'datetime', 'str', 'str', 'str', 'str', 'str', 'int')->run(
$i->url,
$i->title,
$i->author,
$i->publishedDate,
$i->updatedDate,
time(),
$i->id,
$i->content,
$i->urlTitleHash,
$i->urlContentHash,
$i->titleContentHash,
$match['id']
);
// If the article has categories update them.
$this->db->prepare('DELETE FROM arsse_categories WHERE article is ?', 'int')->run($match['id']);
$this->categoriesAdd($i, $match['id']);
}
}
// Lastly update the feed database itself with updated information.
$this->db->prepare('UPDATE arsse_feeds SET url = ?, title = ?, favicon = ?, source = ?, updated = ?, modified = ?, etag = ?, err_count = 0, err_msg = "" WHERE id is ?', 'str', 'str', 'str', 'str', 'datetime', 'datetime', 'str', 'int')->run(
$feed->feedUrl,
$feed->title,
$feed->favicon,
$feed->siteUrl,
$feed->date,
$feed->resource->getLastModified(),
$feed->resource->getEtag(),
$f['id']
);
}
}
$this->db->commit();
return 1;
}
2016-10-02 17:07:17 -04:00
}