1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-22 13:12:41 +00:00

Start of higher-level database interface

This commit is contained in:
J. King 2016-10-15 09:45:23 -04:00
parent 84675bc404
commit b2b71c4557
13 changed files with 227 additions and 153 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
#dependencies #dependencies
vendor/simplepie/* vendor/simplepie/*
vendor/JKingWeb/DrUUID/*
#temp files #temp files
cache/* cache/*

View file

@ -5,6 +5,7 @@ return [
"Exception.JKingWeb/NewsSync/Lang/Exception.fileUnreadable" => "Insufficient permissions to read language file \"{0}\"", "Exception.JKingWeb/NewsSync/Lang/Exception.fileUnreadable" => "Insufficient permissions to read language file \"{0}\"",
"Exception.JKingWeb/NewsSync/Lang/Exception.fileCorrupt" => "Language file \"{0}\" is corrupt or does not conform to expected format", "Exception.JKingWeb/NewsSync/Lang/Exception.fileCorrupt" => "Language file \"{0}\" is corrupt or does not conform to expected format",
"Exception.JKingWeb/NewsSync/Lang/Exception.stringMissing" => "Message string \"{msgID}\" missing from all loaded language files ({fileList})", "Exception.JKingWeb/NewsSync/Lang/Exception.stringMissing" => "Message string \"{msgID}\" missing from all loaded language files ({fileList})",
"Exception.JKingWeb/NewsSync/Lang/Exception.stringInvalid" => "Message string \"{msgID}\" is not a valid ICU message string (language files loaded: {fileList})",
"Exception.JKingWeb/NewsSync/Conf/Exception.fileMissing" => "Configuration file \"{0}\" does not exist", "Exception.JKingWeb/NewsSync/Conf/Exception.fileMissing" => "Configuration file \"{0}\" does not exist",
"Exception.JKingWeb/NewsSync/Conf/Exception.fileUnreadable" => "Insufficient permissions to read configuration file \"{0}\"", "Exception.JKingWeb/NewsSync/Conf/Exception.fileUnreadable" => "Insufficient permissions to read configuration file \"{0}\"",
@ -19,4 +20,14 @@ return [
"Exception.JKingWeb/NewsSync/Db/Exception.fileUnusable" => "Insufficient permissions to open database file \"{0}\" for reading or writing", "Exception.JKingWeb/NewsSync/Db/Exception.fileUnusable" => "Insufficient permissions to open database file \"{0}\" for reading or writing",
"Exception.JKingWeb/NewsSync/Db/Exception.fileUncreatable" => "Insufficient permissions to create new database file \"{0}\"", "Exception.JKingWeb/NewsSync/Db/Exception.fileUncreatable" => "Insufficient permissions to create new database file \"{0}\"",
"Exception.JKingWeb/NewsSync/Db/Exception.fileCorrupt" => "Database file \"{0}\" is corrupt or not a valid database", "Exception.JKingWeb/NewsSync/Db/Exception.fileCorrupt" => "Database file \"{0}\" is corrupt or not a valid database",
"Exception.JKingWeb/NewsSync/Db/ExceptionUpdate.manual" =>
"{from_version, select,
0 {{driver_name} database is configured for manual updates and is not initialized; please populate the database with the base schema}
other {{driver_name} database is configured for manual updates; please update from schema version {from_version} to version {to_version}}
}",
"Exception.JKingWeb/NewsSync/Db/ExceptionUpdate.failed" =>
"{reason select,
missing {Automatic updating of the {driver_name} database failed because instructions for updating from version {from_version} are not available}
}",
"Exception.JKingWeb/NewsSync/Db/ExceptionUpdate.tooNew" => "Automatic updating of the {driver_name} database failed because its version, {current}, is newer than the requested version, {target}"
]; ];

View file

@ -1,111 +0,0 @@
begin;
create table main.newssync_settings(
key varchar(255) primary key not null, --
value varchar(255), --
type varchar(255) not null check(
type in('numeric','text','timestamp', 'date', 'time', 'bool')
) --
);
insert into main.newssync_settings values('schema_version',1,'int');
-- users
create table main.newssync_users(
id TEXT primary key not null, -- user id
password TEXT, -- password, salted and hashed; if using external authentication this would be blank
name TEXT, -- display name
avatar_type TEXT, -- avatar image's MIME content type
avatar_data BLOB, -- avatar image's binary data
admin boolean not null default 0 -- whether the user is an administrator
);
-- TT-RSS categories and ownCloud folders
create table main.newssync_categories(
id integer primary key not null, -- sequence number
owner TEXT not null references users(id) on delete cascade on update cascade, -- owner of category
parent integer, -- parent category id
folder integer not null, -- first-level category (ownCloud folder)
name TEXT not null, -- category name
modified datetime not null default CURRENT_TIMESTAMP, --
unique(owner,name,parent) -- cannot have multiple categories with the same name under the same parent for the same owner
);
-- newsfeeds, deduplicated
create table feeds.newssync_feeds(
id integer primary key not null, -- sequence number
url TEXT not null, -- URL of feed
title TEXT, -- default title of feed
favicon TEXT, -- URL of favicon
source TEXT, -- URL of site to which the feed belongs
updated datetime, -- time at which the feed was last fetched
modified datetime not null default CURRENT_TIMESTAMP, --
err_count integer not null default 0, -- count of successive times update resulted in error since last successful update
err_msg TEXT, -- last error message
username TEXT, -- HTTP authentication username
password TEXT, -- HTTP authentication password (this is stored in plain text)
unique(url,username,password) -- a URL with particular credentials should only appear once
);
-- users' subscriptions to newsfeeds, with settings
create table main.newssync_subscriptions(
id integer primary key not null, -- sequence number
owner TEXT not null references users(id) on delete cascade on update cascade, -- owner of subscription
feed integer not null references feeds(id) on delete cascade, -- feed for the subscription
added datetime not null default CURRENT_TIMESTAMP, -- time at which feed was added
modified datetime not null default CURRENT_TIMESTAMP, -- date at which subscription properties were last modified
title TEXT, -- user-supplied title
order_type int not null default 0, -- ownCloud sort order
pinned boolean not null default 0, -- whether feed is pinned (always sorts at top)
category integer not null references categories(id) on delete set null, -- TT-RSS category (nestable); the first-level category (which acts as ownCloud folder) is joined in when needed
unique(owner,feed) -- a given feed should only appear once for a given owner
);
-- entries in newsfeeds
create table feeds.newssync_articles(
id integer primary key not null, -- sequence number
feed integer not null references feeds(id) on delete cascade, -- feed for the subscription
url TEXT not null, -- URL of article
title TEXT, -- article title
author TEXT, -- author's name
published datetime, -- time of original publication
edited datetime, -- time of last edit
guid TEXT, -- GUID
content TEXT, -- content, as (X)HTML
modified datetime not null default CURRENT_TIMESTAMP, -- date when article properties were last modified
hash varchar(64) not null, -- ownCloud hash
fingerprint varchar(64) not null, -- ownCloud fingerprint
enclosures_hash varchar(64), -- hash of enclosures, if any; since enclosures are not uniquely identified, we need to know when they change
tags_hash varchar(64) -- hash of RSS/Atom categories included in article; since these categories are not uniquely identified, we need to know when they change
);
-- users' actions on newsfeed entries
create table main.newssync_subscription_articles(
id integer primary key not null,
article integer not null references articles(id) on delete cascade,
read boolean not null default 0,
starred boolean not null default 0,
modified datetime not null default CURRENT_TIMESTAMP
);
-- enclosures associated with articles
create table main.newssync_enclosures(
article integer not null references articles(id) on delete cascade,
url TEXT,
type varchar(255)
);
-- author labels ("categories" in RSS/Atom parlance) associated with newsfeed entries
create table main.newssync_tags(
article integer not null references articles(id) on delete cascade,
name TEXT
);
-- user labels associated with newsfeed entries
create table main.newssync_labels(
sub_article integer not null references subscription_articles(id) on delete cascade,
owner TEXT not null references users(id) on delete cascade on update cascade,
name TEXT
);
create index main.newssync_label_names on newssync_labels(name);
commit;

113
sql/SQLite3/0.sql Normal file
View file

@ -0,0 +1,113 @@
-- newsfeeds, deduplicated
create table feeds.newssync_feeds(
id integer primary key not null, -- sequence number
url TEXT not null, -- URL of feed
title TEXT, -- default title of feed
favicon TEXT, -- URL of favicon
source TEXT, -- URL of site to which the feed belongs
updated datetime, -- time at which the feed was last fetched
modified datetime not null default CURRENT_TIMESTAMP, --
err_count integer not null default 0, -- count of successive times update resulted in error since last successful update
err_msg TEXT, -- last error message
username TEXT, -- HTTP authentication username
password TEXT, -- HTTP authentication password (this is stored in plain text)
unique(url,username,password) -- a URL with particular credentials should only appear once
);
-- entries in newsfeeds
create table feeds.newssync_articles(
id integer primary key not null, -- sequence number
feed integer not null references newssync_feeds(id) on delete cascade, -- feed for the subscription
url TEXT not null, -- URL of article
title TEXT, -- article title
author TEXT, -- author's name
published datetime, -- time of original publication
edited datetime, -- time of last edit
guid TEXT, -- GUID
content TEXT, -- content, as (X)HTML
modified datetime not null default CURRENT_TIMESTAMP, -- date when article properties were last modified
hash varchar(64) not null, -- ownCloud hash
fingerprint varchar(64) not null, -- ownCloud fingerprint
enclosures_hash varchar(64), -- hash of enclosures, if any; since enclosures are not uniquely identified, we need to know when they change
tags_hash varchar(64) -- hash of RSS/Atom categories included in article; since these categories are not uniquely identified, we need to know when they change
);
-- enclosures associated with articles
create table feeds.newssync_enclosures(
article integer not null references newssync_articles(id) on delete cascade,
url TEXT,
type varchar(255)
);
-- author labels ("categories" in RSS/Atom parlance) associated with newsfeed entries
create table feeds.newssync_tags(
article integer not null references newssync_articles(id) on delete cascade,
name TEXT
);
-- set version marker
pragma feeds.user_version = 1;
create table main.newssync_settings(
key varchar(255) primary key not null, --
value varchar(255), --
type varchar(255) not null check(
type in('int', 'numeric','text','timestamp', 'date', 'time', 'bool')
) --
);
-- users
create table main.newssync_users(
id TEXT primary key not null, -- user id
password TEXT, -- password, salted and hashed; if using external authentication this would be blank
name TEXT, -- display name
avatar_type TEXT, -- avatar image's MIME content type
avatar_data BLOB, -- avatar image's binary data
admin boolean not null default 0 -- whether the user is an administrator
);
-- TT-RSS categories and ownCloud folders
create table main.newssync_categories(
id integer primary key not null, -- sequence number
owner TEXT not null references newssync_users(id) on delete cascade on update cascade, -- owner of category
parent integer, -- parent category id
folder integer not null, -- first-level category (ownCloud folder)
name TEXT not null, -- category name
modified datetime not null default CURRENT_TIMESTAMP, --
unique(owner,name,parent) -- cannot have multiple categories with the same name under the same parent for the same owner
);
-- users' subscriptions to newsfeeds, with settings
create table main.newssync_subscriptions(
id integer primary key not null, -- sequence number
owner TEXT not null references newssync_users(id) on delete cascade on update cascade, -- owner of subscription
feed integer not null references newssync_feeds(id) on delete cascade, -- feed for the subscription
added datetime not null default CURRENT_TIMESTAMP, -- time at which feed was added
modified datetime not null default CURRENT_TIMESTAMP, -- date at which subscription properties were last modified
title TEXT, -- user-supplied title
order_type int not null default 0, -- ownCloud sort order
pinned boolean not null default 0, -- whether feed is pinned (always sorts at top)
category integer not null references newssync_categories(id) on delete set null, -- TT-RSS category (nestable); the first-level category (which acts as ownCloud folder) is joined in when needed
unique(owner,feed) -- a given feed should only appear once for a given owner
);
-- users' actions on newsfeed entries
create table main.newssync_subscription_articles(
id integer primary key not null,
article integer not null references newssync_articles(id) on delete cascade,
read boolean not null default 0,
starred boolean not null default 0,
modified datetime not null default CURRENT_TIMESTAMP
);
-- user labels associated with newsfeed entries
create table main.newssync_labels(
sub_article integer not null references newssync_subscription_articles(id) on delete cascade, --
owner TEXT not null references newssync_users(id) on delete cascade on update cascade,
name TEXT
);
create index main.newssync_label_names on newssync_labels(name);
-- set version marker
pragma main.user_version = 1;
insert into main.newssync_settings values('schema_version',1,'int');

View file

@ -3,28 +3,31 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync; namespace JKingWeb\NewsSync;
class Conf { class Conf {
public $lang = "en"; public $lang = "en";
public $dbClass = NS_BASE."Db\\DriverSQLite3"; public $dbClass = NS_BASE."Db\\DriverSQLite3";
public $dbSQLite3Path = BASE."db"; public $dbSQLite3Path = BASE."db";
public $dbSQLite3Key = ""; public $dbSQLite3Key = "";
public $dbPostgreSQLHost = "localhost"; public $dbSQLite3AutoUpd = true;
public $dbPostgreSQLUser = "newssync"; public $dbPostgreSQLHost = "localhost";
public $dbPostgreSQLPass = ""; public $dbPostgreSQLUser = "newssync";
public $dbPostgreSQLPort = 5432; public $dbPostgreSQLPass = "";
public $dbPostgreSQLDb = "newssync"; public $dbPostgreSQLPort = 5432;
public $dbPostgreSQLSchema = ""; public $dbPostgreSQLDb = "newssync";
public $dbMySQLHost = "localhost"; public $dbPostgreSQLSchema = "";
public $dbMySQLUser = "newssync"; public $dbPostgreSQLAutoUpd = false;
public $dbMySQLPass = ""; public $dbMySQLHost = "localhost";
public $dbMySQLPort = 3306; public $dbMySQLUser = "newssync";
public $dbMySQLDb = "newssync"; public $dbMySQLPass = "";
public $dbMySQLPort = 3306;
public $dbMySQLDb = "newssync";
public $dbMySQLAutoUpd = false;
public $authClass = NS_BASE."Auth\\DriverInternal"; public $authClass = NS_BASE."Auth\\DriverInternal";
public $authPreferHTTP = false; public $authPreferHTTP = false;
public $authProvision = false; public $authAutoAdd = false;
public $simplepieCache = BASE.".cache"; public $simplepieCache = BASE.".cache";
public function __construct(string $import_file = "") { public function __construct(string $import_file = "") {

View file

@ -3,11 +3,25 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync; namespace JKingWeb\NewsSync;
class Database { class Database {
protected $drv; const SCHEMA_VERSION = 1;
protected $db;
protected function clean_name(string $name): string {
return (string) preg_filter("[^0-9a-zA-Z_\.]", "", $name);
}
public function __construct(Conf $conf) { public function __construct(Conf $conf) {
$driver = $conf->dbClass; $driver = $conf->dbClass;
$this->drv = $driver::create($conf); $this->db = $driver::create($conf, INSTALL);
$ver = $this->db->schemaVersion();
if($ver < self::SCHEMA_VERSION) {
if($conf->dbSQLite3AutoUpd) {
$this->db->update(self::SCHEMA_VERSION);
} else {
throw new Db\Exception("updateManual", ['from_version' => $ver, 'to_version' => self::SCHEMA_VERSION, 'driver_name' => $this->db->driverName()]);
}
}
} }
static public function listDrivers(): array { static public function listDrivers(): array {
@ -27,6 +41,24 @@ class Database {
} }
public function schemaVersion(): int { public function schemaVersion(): int {
return $this->drv->schemaVersion(); return $this->db->schemaVersion();
} }
public function userAdd(string $username, string $password = null, bool $admin = false): string {
$this->db->prepare("INSERT INTO newssync_users(id,password,admin) values(?,?,?)", "str", "str", "bool")->run($username,$password,$admin);
return $username;
}
public function subscriptionAdd(string $user, string $url, string $fetchUser = null, string $fetchPassword = null): int {
$this->db->begin();
$qFeed = $this->db->prepare("SELECT id from newssync_feeds where url = ? and username = ? and password = ?", "str", "str", "str");
if(is_null($id = $qFeed->run($url, $fetchUser, $fetchPassword)->getSingle())) {
$this->db->prepare("INSERT INTO newssync_feeds(url,username,password) values(?,?,?)", "str", "str", "str")->run($url, $fetchUser, $fetchPassword);
$id = $qFeed->run($url, $fetchUser, $fetchPassword)->getSingle();
}
$this->db->prepare("INSERT INTO newssync_subscriptions(owner,feed) values(?,?)", "str", "int")->run($user,$id);
$this->db->commit();
return 0;
}
} }

View file

@ -3,14 +3,15 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync\Db; namespace JKingWeb\NewsSync\Db;
Trait Common { Trait Common {
protected $transDepth; protected $transDepth = 0;
public function fail(\Throwable $e, bool $bool = false) {
$this->rollback($all);
throw $e;
}
public function begin(): bool { public function begin(): bool {
if($this->transDepth==0) { $this->exec("SAVEPOINT newssync_".($this->transDepth));
$this->exec("BEGIN TRANSACTION");
} else{
$this->exec("SAVEPOINT newssync_".$this->transDepth);
}
$this->transDepth += 1; $this->transDepth += 1;
return true; return true;
} }
@ -18,7 +19,7 @@ Trait Common {
public function commit(bool $all = false): bool { public function commit(bool $all = false): bool {
if($this->transDepth==0) return false; if($this->transDepth==0) return false;
if(!$all) { if(!$all) {
$this->exec("RELEASE SAVEPOINT newssync_".$this->transDepth-1); $this->exec("RELEASE SAVEPOINT newssync_".($this->transDepth - 1));
$this->transDepth -= 1; $this->transDepth -= 1;
} else { } else {
$this->exec("COMMIT TRANSACTION"); $this->exec("COMMIT TRANSACTION");
@ -30,7 +31,7 @@ Trait Common {
public function rollback(bool $all = false): bool { public function rollback(bool $all = false): bool {
if($this->transDepth==0) return false; if($this->transDepth==0) return false;
if(!$all) { if(!$all) {
$this->exec("ROLLBACK TRANSACTION TO SAVEPOINT newssync_".$this->transDepth-1); $this->exec("ROLLBACK TRANSACTION TO SAVEPOINT newssync_".($this->transDepth - 1));
$this->transDepth -= 1; $this->transDepth -= 1;
if($this->transDepth==0) $this->exec("ROLLBACK TRANSACTION"); if($this->transDepth==0) $this->exec("ROLLBACK TRANSACTION");
} else { } else {

View file

@ -12,6 +12,21 @@ Trait CommonSQLite3 {
return $this->unsafeQuery("PRAGMA $schema.user_version")->getSingle(); return $this->unsafeQuery("PRAGMA $schema.user_version")->getSingle();
} }
public function update($to) {
$sep = \DIRECTORY_SEPARATOR;
$path = \JKingWeb\NewsSync\BASE."sql".$sep."SQLite3".$sep;
$this->begin();
for($a = $this->schemaVersion(); $a < $to; $a++) {
$file = $path.$a.".sql";
if(!file_exists($file)) $this->fail(new Exception("updateMissing", ['version' => $a, 'driver_name' => $this->driverName()]));
if(!is_readable($file)) $this->fail(new Exception("updateUnreadable", ['version' => $a, 'driver_name' => $this->driverName()]));
$sql = @file_get_contents($file);
if($sql===false) $this->fail(new Exception("updateUnusable", ['version' => $a, 'driver_name' => $this->driverName()]));
$this->exec($sql);
}
$this->commit();
}
public function exec(string $query): bool { public function exec(string $query): bool {
return (bool) $this->db->exec($query); return (bool) $this->db->exec($query);
} }

View file

@ -20,12 +20,12 @@ class DriverSQLite3 implements Driver {
$this->db->enableExceptions(true); $this->db->enableExceptions(true);
$attach = "'".$this->db->escapeString($feedfile)."'"; $attach = "'".$this->db->escapeString($feedfile)."'";
$this->exec("ATTACH DATABASE $attach AS feeds"); $this->exec("ATTACH DATABASE $attach AS feeds");
$this->exec("PRAGMA main.jounral_mode = wal"); $this->exec("PRAGMA main.journal_mode = wal");
$this->exec("PRAGMA feeds.jounral_mode = wal"); $this->exec("PRAGMA feeds.journal_mode = wal");
$this->exec("PRAGMA foreign_keys = yes"); $this->exec("PRAGMA foreign_keys = yes");
} catch(\Throwable $e) { } catch(\Throwable $e) {
// if opening the database doesn't work, check various pre-conditions to find out what the problem might be // if opening the database doesn't work, check various pre-conditions to find out what the problem might be
foreach([$mainfile, $mainfile."-wal", $mainfile."-shm", $feedfile, $feedfile."-wal", $feedfile."-shm"] as $file) { foreach([$mainfile, $feedfile] as $file) {
if(!file_exists($file)) { if(!file_exists($file)) {
if($install && !is_writable(dirname($file))) throw new Exception("fileUncreatable", dirname($file)); if($install && !is_writable(dirname($file))) throw new Exception("fileUncreatable", dirname($file));
throw new Exception("fileMissing", $file); throw new Exception("fileMissing", $file);
@ -60,6 +60,6 @@ class DriverSQLite3 implements Driver {
} }
public function prepareArray(string $query, array $paramTypes): Statement { public function prepareArray(string $query, array $paramTypes): Statement {
return new StatementSQLite3($query, $paramTypes); return new StatementSQLite3($this->db->prepare($query), $paramTypes);
} }
} }

View file

@ -24,7 +24,7 @@ class ResultSQLite3 implements Result {
public function getSingle() { public function getSingle() {
$res = $this->get(); $res = $this->get();
if($res===FALSE) return null; if($res===false) return null;
return array_shift($res); return array_shift($res);
} }
} }

View file

@ -3,7 +3,8 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync\Db; namespace JKingWeb\NewsSync\Db;
interface Statement { interface Statement {
function __invoke(...$bindings); // alias of run() function __construct($st, array $bindings = null);
function run(...$bindings): Result; function __invoke(&...$values); // alias of run()
function runArray(array $bindings): Result; function run(&...$values): Result;
function runArray(array &$values): Result;
} }

View file

@ -6,7 +6,7 @@ class StatementSQLite3 implements Statement {
protected $st; protected $st;
protected $types; protected $types;
public function __construct(\SQLite3Stmt $st, $bindings = null) { public function __construct(\SQLite3Stmt $st, array $bindings = null) {
$this->st = $st; $this->st = $st;
$this->types = []; $this->types = [];
foreach($bindings as $binding) { foreach($bindings as $binding) {
@ -31,6 +31,11 @@ class StatementSQLite3 implements Statement {
case "text": case "text":
case "string": case "string":
case "str": case "str":
$this->types[] = \SQLITE3_TEXT; break;
case "bool":
case "boolean":
case "bit":
$this->types[] = \SQLITE3_INTEGER; break;
default: default:
$this->types[] = \SQLITE3_TEXT; break; $this->types[] = \SQLITE3_TEXT; break;
} }
@ -59,8 +64,8 @@ class StatementSQLite3 implements Statement {
} else { } else {
$type = (array_key_exists($a,$this->types)) ? $this->types[$a] : \SQLITE3_TEXT; $type = (array_key_exists($a,$this->types)) ? $this->types[$a] : \SQLITE3_TEXT;
} }
$st->bindParam($a+1, $values[$a], $type); $this->st->bindParam($a+1, $values[$a], $type);
} }
return new ResultSQLite3($st->execute()); return new ResultSQLite3($this->st->execute());
} }
} }

View file

@ -11,6 +11,7 @@ class Lang {
"Exception.JKingWeb/NewsSync/Lang/Exception.fileUnreadable" => "Insufficient permissions to read language file \"{0}\"", "Exception.JKingWeb/NewsSync/Lang/Exception.fileUnreadable" => "Insufficient permissions to read language file \"{0}\"",
"Exception.JKingWeb/NewsSync/Lang/Exception.fileCorrupt" => "Language file \"{0}\" is corrupt or does not conform to expected format", "Exception.JKingWeb/NewsSync/Lang/Exception.fileCorrupt" => "Language file \"{0}\" is corrupt or does not conform to expected format",
"Exception.JKingWeb/NewsSync/Lang/Exception.stringMissing" => "Message string \"{msgID}\" missing from all loaded language files ({fileList})", "Exception.JKingWeb/NewsSync/Lang/Exception.stringMissing" => "Message string \"{msgID}\" missing from all loaded language files ({fileList})",
"Exception.JKingWeb/NewsSync/Lang/Exception.stringInvalid" => "Message string \"{msgID}\" is not a valid ICU message string (language files loaded: {fileList})",
]; ];
static protected $requirementsMet = false; static protected $requirementsMet = false;
@ -51,7 +52,9 @@ class Lang {
if(!array_key_exists($msgID, self::$strings)) throw new Lang\Exception("stringMissing", ['msgID' => $msgID, 'fileList' => implode(", ",self::$loaded)]); if(!array_key_exists($msgID, self::$strings)) throw new Lang\Exception("stringMissing", ['msgID' => $msgID, 'fileList' => implode(", ",self::$loaded)]);
// variables fed to MessageFormatter must be contained in array // variables fed to MessageFormatter must be contained in array
if($vars !== null && !is_array($vars)) $vars = [$vars]; if($vars !== null && !is_array($vars)) $vars = [$vars];
return \MessageFormatter::formatMessage(self::$locale, self::$strings[$msgID], $vars); $msg = \MessageFormatter::formatMessage(self::$locale, self::$strings[$msgID], $vars);
if($msg===false) throw new Lang\Exception("stringInvalid", ['msgID' => $msgID, 'fileList' => implode(", ",self::$loaded)]);
return $msg;
} }
static public function list(string $locale = ""): array { static public function list(string $locale = ""): array {