1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-22 21:22:40 +00:00
Arsse/lib/Database.php

359 lines
17 KiB
PHP
Raw Normal View History

2016-10-02 21:07:17 +00:00
<?php
declare(strict_types=1);
2016-10-02 21:07:17 +00:00
namespace JKingWeb\NewsSync;
use PasswordGenerator\Generator as PassGen;
use PicoFeed\Reader\Reader;
use PicoFeed\PicoFeedException;
2016-10-02 21:07:17 +00:00
class Database {
2017-02-16 20:29:42 +00: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 20:29:42 +00:00
protected $data;
public $db;
private $driver;
2017-02-16 20:29:42 +00:00
protected function cleanName(string $name): string {
return (string) preg_filter("[^0-9a-zA-Z_\.]", "", $name);
}
2016-10-02 21:07:17 +00:00
2017-02-16 20:29:42 +00:00
public function __construct(RuntimeData $data) {
$this->data = $data;
$this->driver = $driver = $data->conf->dbDriver;
$this->db = new $driver($data, INSTALL);
2017-02-16 20:29:42 +00:00
$ver = $this->db->schemaVersion();
if(!INSTALL && $ver < self::SCHEMA_VERSION) {
$this->db->schemaUpdate(self::SCHEMA_VERSION);
2017-02-16 20:29:42 +00:00
}
}
2016-10-02 21:07:17 +00:00
2017-02-16 20:29:42 +00: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 20:29:42 +00:00
}
return $classes;
}
2017-02-16 20:29:42 +00: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 20:29:42 +00:00
return false;
}
2017-02-16 20:29:42 +00:00
public function settingGet(string $key) {
$row = $this->db->prepare("SELECT value, type from newssync_settings where key = ?", "str")->run($key)->getRow();
2017-02-16 20:29:42 +00: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 20:49:39 +00:00
2017-02-16 20:29:42 +00: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 20:29:42 +00:00
case "string":
case "array": $type = "json"; break;
2017-02-16 20:29:42 +00:00
case "resource":
case "unknown type":
case "NULL": $type = "null"; break;
2017-02-16 20:29:42 +00:00
case "object":
if($in instanceof DateTimeInterface) {
$type = "timestamp";
} else {
$type = "text";
}
break;
default: $type = 'null'; break;
2017-02-16 20:29:42 +00:00
}
}
$type = strtolower($type);
switch($type) {
case "integer":
$type = "int";
case "int":
2017-03-09 22:14:26 +00:00
$value = $in;
2017-02-16 20:29:42 +00:00
break;
case "float":
case "double":
case "real":
$type = "numeric";
case "numeric":
2017-03-09 22:14:26 +00:00
$value = $in;
2017-02-16 20:29:42 +00:00
break;
case "str":
case "string":
$type = "text";
case "text":
2017-03-09 22:14:26 +00:00
$value = $in;
2017-02-16 20:29:42 +00:00
break;
case "json":
if(is_array($in) || is_object($in)) {
$value = json_encode($in);
} else {
2017-03-09 22:14:26 +00:00
$value = $in;
}
2017-02-16 20:29:42 +00: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 22:14:26 +00:00
$value = $in;
2017-02-16 20:29:42 +00:00
break;
}
2017-03-09 22:14:26 +00:00
return (bool) $this->db->prepare("REPLACE INTO newssync_settings(key,value,type) values(?,?,?)", "str", "str", "str")->run($key, $value, $type)->changes();
2017-02-16 20:29:42 +00:00
}
2016-10-17 20:49:39 +00:00
2017-02-16 20:29:42 +00:00
public function settingRemove(string $key): bool {
$this->db->prepare("DELETE from newssync_settings where key is ?", "str")->run($key);
2017-02-16 20:29:42 +00:00
return true;
}
2016-10-17 20:49:39 +00:00
2017-02-16 20:29:42 +00:00
public function userExists(string $user): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
return (bool) $this->db->prepare("SELECT count(*) from newssync_users where id is ?", "str")->run($user)->getValue();
2017-02-16 20:29:42 +00:00
}
public function userAdd(string $user, string $password = null): string {
2017-02-16 20:29:42 +00:00
if(!$this->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($this->data->conf->userTempPasswordLength)->get();
$hash = "";
if(strlen($password) > 0) $hash = password_hash($password, \PASSWORD_DEFAULT);
$this->db->prepare("INSERT INTO newssync_users(id,password) values(?,?)", "str", "str")->runArray([$user,$hash]);
return $password;
2017-02-16 20:29:42 +00:00
}
2017-02-16 20:29:42 +00:00
public function userRemove(string $user): bool {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
if($this->db->prepare("DELETE from newssync_users where id is ?", "str")->run($user)->changes() < 1) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
2017-02-16 20:29:42 +00:00
return true;
}
2017-02-16 20:29:42 +00:00
public function userList(string $domain = null): array {
if($domain !== null) {
if(!$this->data->user->authorize("@".$domain, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]);
$domain = str_replace(["\\","%","_"],["\\\\", "\\%", "\\_"], $domain);
$domain = "%@".$domain;
return $this->db->prepare("SELECT id from newssync_users where id like ?", "str")->run($domain)->getAll();
2017-02-16 20:29:42 +00:00
} else {
2017-02-21 00:04:08 +00:00
if(!$this->data->user->authorize("", __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "global"]);
return $this->db->prepare("SELECT id from newssync_users")->run()->getAll();
2017-02-16 20:29:42 +00:00
}
}
2017-02-16 20:29:42 +00:00
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)) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
return (string) $this->db->prepare("SELECT password from newssync_users where id is ?", "str")->run($user)->getValue();
2017-02-16 20:29:42 +00:00
}
public function userPasswordSet(string $user, string $password = null): string {
2017-02-16 20:29:42 +00:00
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", ["action" => __FUNCTION__, "user" => $user]);
if($password===null) $password = (new PassGen)->length($this->data->conf->userTempPasswordLength)->get();
$hash = "";
if(strlen($password > 0)) $hash = password_hash($password, \PASSWORD_DEFAULT);
$this->db->prepare("UPDATE newssync_users set password = ? where id is ?", "str", "str")->run($hash, $user);
return $password;
2017-02-16 20:29:42 +00:00
}
2017-02-16 20:29:42 +00:00
public function userPropertiesGet(string $user): array {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
$prop = $this->db->prepare("SELECT name,rights from newssync_users where id is ?", "str")->run($user)->getRow();
2017-02-16 20:29:42 +00:00
if(!$prop) return [];
return $prop;
}
2017-02-16 20:29:42 +00:00
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",
2017-02-16 20:29:42 +00:00
];
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);
2017-02-16 20:29:42 +00:00
}
$this->db->commit();
return $this->userPropertiesGet($user);
}
2016-11-04 02:54:27 +00:00
2017-02-16 20:29:42 +00:00
public function userRightsGet(string $user): int {
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
return (int) $this->db->prepare("SELECT rights from newssync_users where id is ?", "str")->run($user)->getValue();
2017-02-16 20:29:42 +00:00
}
2016-11-04 02:54:27 +00:00
2017-02-16 20:29:42 +00:00
public function userRightsSet(string $user, int $rights): bool {
if(!$this->data->user->authorize($user, __FUNCTION__, $rights)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
2017-02-16 20:29:42 +00:00
if(!$this->userExists($user)) return false;
$this->db->prepare("UPDATE newssync_users set rights = ? where id is ?", "int", "str")->run($rights, $user);
return true;
}
2017-02-16 20:29:42 +00: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 (!$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__]);
}
2017-02-16 20:29:42 +00:00
$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.
2017-02-16 20:29:42 +00:00
$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)->getValue();
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);
}
$feedID = $this->db->prepare(
"INSERT INTO newssync_feeds(url,title,favicon,source,updated,modified,etag,username,password) values(?,?,?,?,?,?,?,?,?)",
"str", "str", "str", "str", "datetime", "datetime", "str", "str", "str"
)->run(
$url,
$feed->title,
// Grab the favicon for the Goodfeed; returns an empty string if it cannot find one.
(new \PicoFeed\Reader\Favicon)->find($url),
$feed->siteUrl,
$feed->date,
$resource->getLastModified(),
$resource->getEtag(),
$fetchUser,
$fetchPassword
)->lastId();
// TODO: Populate newssync_articles with contents of what was obtained from PicoFeed.
2017-02-16 20:29:42 +00:00
}
// Add the feed to the user's subscriptions.
$sub = $this->db->prepare("INSERT INTO newssync_subscriptions(owner,feed) values(?,?)", "str", "int")->run($user, $feedID)->lastId();
2017-02-16 20:29:42 +00:00
$this->db->commit();
return $sub;
}
2016-11-04 02:54:27 +00:00
2017-02-16 20:29:42 +00:00
public function subscriptionRemove(int $id): bool {
$this->db->begin();
$user = $this->db->prepare("SELECT owner from newssync_subscriptions where id is ?", "int")->run($id)->getValue();
2017-02-16 20:29:42 +00:00
if($user===null) return false;
if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
return (bool) $this->db->prepare("DELETE from newssync_subscriptions where id is ?", "int")->run($id)->changes();
}
public function folderAdd(string $user, array $data): int {
// 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__]);
}
// if the desired folder name is missing or invalid, throw an exception
if(!array_key_exists("name", $data)) {
throw new Db\ExceptionInput("missing", ["action" => __FUNCTION__, "field" => "name"]);
} else if(!strlen(trim($data['name']))) {
throw new Db\ExceptionInput("whitespace", ["action" => __FUNCTION__, "field" => "name"]);
} else if(iconv_strlen($data['name']) > 100) {
throw new Db\ExceptionInput("tooLong", ["action" => __FUNCTION__, "field" => "name", 'max' => 100]);
}
// 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;
$root = 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,root from newssync_folders where owner is ? and id is ?", "str", "int")->run($user, $parent)->getRow();
if(!$p) {
throw new Db\ExceptionInput("idMissing", ["action" => __FUNCTION__, "field" => "parent", 'id' => $parent]);
} else {
// if the parent does not have a root specified (because it is a first-level folder) use the parent ID as the root ID
$root = $p['root']===null ? $parent : $p['root'];
}
}
// 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?
if($this->db->prepare("SELECT count(*) from newssync_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 newssync_folders(owner,parent,root,name) values(?,?,?,?)", "str", "int", "int", "str")->run($user, $parent, $root, $data['name'])->lastId();
}
2016-10-02 21:07:17 +00:00
}