diff --git a/.gitattributes b/.gitattributes
index 412eeda7..2431c400 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -10,13 +10,13 @@
 *.dbproj merge=union
 
 # Standard to msysgit
-*.doc	 diff=astextplain
-*.DOC	 diff=astextplain
+*.doc     diff=astextplain
+*.DOC     diff=astextplain
 *.docx diff=astextplain
 *.DOCX diff=astextplain
 *.dot  diff=astextplain
 *.DOT  diff=astextplain
 *.pdf  diff=astextplain
-*.PDF	 diff=astextplain
-*.rtf	 diff=astextplain
-*.RTF	 diff=astextplain
+*.PDF     diff=astextplain
+*.rtf     diff=astextplain
+*.RTF     diff=astextplain
diff --git a/composer.json b/composer.json
index 4bd67182..ead7a5b5 100644
--- a/composer.json
+++ b/composer.json
@@ -1,40 +1,40 @@
 {
-	"name": "jkingweb/arsse",
-	"type": "library",
-	"description": "TODO",
-	"keywords": ["rss"],
-	"license": "MIT",
-	"authors": [
-		{
-			"name": "J. King",
-			"email": "jking@jkingweb.ca",
-			"homepage": "https://jkingweb.ca/"
-		},
-		{
-			"name": "Dustin Wilson",
-			"email": "dustin@dustinwilson.com",
-			"homepage": "https://dustinwilson.com/"
-		}
+    "name": "jkingweb/arsse",
+    "type": "library",
+    "description": "TODO",
+    "keywords": ["rss"],
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "J. King",
+            "email": "jking@jkingweb.ca",
+            "homepage": "https://jkingweb.ca/"
+        },
+        {
+            "name": "Dustin Wilson",
+            "email": "dustin@dustinwilson.com",
+            "homepage": "https://dustinwilson.com/"
+        }
 
-	],
-	"require": {
-		"php": "^7.0.0",
-		"jkingweb/druuid": "^3.0.0",
-		"phpseclib/phpseclib": "^2.0.4",
-		"webmozart/glob": "^4.1.0",
-		"fguillot/picoFeed": ">=0.1.31"
-	},
-	"require-dev": {
-		"mikey179/vfsStream": "^1.6.4"
-	},
-	"autoload": {
-		"psr-4": {
-			"JKingWeb\\NewsSync\\": "lib/"
-		}
-	},
-	"autoload-dev": {
-		"psr-4": {
-			"JKingWeb\\NewsSync\\Test\\": "tests/lib/"
-		}
-	}
+    ],
+    "require": {
+        "php": "^7.0.0",
+        "jkingweb/druuid": "^3.0.0",
+        "phpseclib/phpseclib": "^2.0.4",
+        "webmozart/glob": "^4.1.0",
+        "fguillot/picoFeed": ">=0.1.31"
+    },
+    "require-dev": {
+        "mikey179/vfsStream": "^1.6.4"
+    },
+    "autoload": {
+        "psr-4": {
+            "JKingWeb\\NewsSync\\": "lib/"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "JKingWeb\\NewsSync\\Test\\": "tests/lib/"
+        }
+    }
 }
\ No newline at end of file
diff --git a/lib/AbstractException.php b/lib/AbstractException.php
index b5807745..f98d1996 100644
--- a/lib/AbstractException.php
+++ b/lib/AbstractException.php
@@ -4,57 +4,57 @@ namespace JKingWeb\NewsSync;
 
 abstract class AbstractException extends \Exception {
 
-	const CODES = [
-		"Exception.uncoded"                     => -1,
-		"Exception.invalid"                     => 1, // this exception MUST NOT have a message string defined
-		"Exception.unknown"						=> 10000,
-		"Lang/Exception.defaultFileMissing"		=> 10101,
-		"Lang/Exception.fileMissing"			=> 10102,
-		"Lang/Exception.fileUnreadable"			=> 10103,
-		"Lang/Exception.fileCorrupt"			=> 10104,
-		"Lang/Exception.stringMissing" 			=> 10105,
-		"Db/Exception.extMissing"				=> 10201,
-		"Db/Exception.fileMissing"				=> 10202,
-		"Db/Exception.fileUnusable"				=> 10203,
-		"Db/Exception.fileUnreadable"			=> 10204,
-		"Db/Exception.fileUnwritable"			=> 10205,
-		"Db/Exception.fileUncreatable"			=> 10206,
-		"Db/Exception.fileCorrupt"				=> 10207,
-		"Db/Update/Exception.tooNew"			=> 10211,
-		"Db/Update/Exception.fileMissing"		=> 10212,
-		"Db/Update/Exception.fileUnusable"		=> 10213,
-		"Db/Update/Exception.fileUnreadable"	=> 10214,
-		"Db/Update/Exception.manual"			=> 10215,
-		"Db/Update/Exception.manualOnly"		=> 10216,
-		"Conf/Exception.fileMissing"			=> 10302,
-		"Conf/Exception.fileUnusable"			=> 10303,
-		"Conf/Exception.fileUnreadable"			=> 10304,
-		"Conf/Exception.fileUnwritable"			=> 10305,
-		"Conf/Exception.fileUncreatable"		=> 10306,
-		"Conf/Exception.fileCorrupt"			=> 10307,
-		"User/Exception.functionNotImplemented"	=> 10401, 
-		"User/Exception.doesNotExist"			=> 10402,
-		"User/Exception.alreadyExists"			=> 10403,
-		"User/Exception.authMissing"			=> 10411,
-		"User/Exception.authFailed"				=> 10412,
-		"User/Exception.notAuthorized" 			=> 10421,
-	];
+    const CODES = [
+        "Exception.uncoded"                     => -1,
+        "Exception.invalid"                     => 1, // this exception MUST NOT have a message string defined
+        "Exception.unknown"                     => 10000,
+        "Lang/Exception.defaultFileMissing"     => 10101,
+        "Lang/Exception.fileMissing"            => 10102,
+        "Lang/Exception.fileUnreadable"         => 10103,
+        "Lang/Exception.fileCorrupt"            => 10104,
+        "Lang/Exception.stringMissing"          => 10105,
+        "Db/Exception.extMissing"               => 10201,
+        "Db/Exception.fileMissing"              => 10202,
+        "Db/Exception.fileUnusable"             => 10203,
+        "Db/Exception.fileUnreadable"           => 10204,
+        "Db/Exception.fileUnwritable"           => 10205,
+        "Db/Exception.fileUncreatable"          => 10206,
+        "Db/Exception.fileCorrupt"              => 10207,
+        "Db/Update/Exception.tooNew"            => 10211,
+        "Db/Update/Exception.fileMissing"       => 10212,
+        "Db/Update/Exception.fileUnusable"      => 10213,
+        "Db/Update/Exception.fileUnreadable"    => 10214,
+        "Db/Update/Exception.manual"            => 10215,
+        "Db/Update/Exception.manualOnly"        => 10216,
+        "Conf/Exception.fileMissing"            => 10302,
+        "Conf/Exception.fileUnusable"           => 10303,
+        "Conf/Exception.fileUnreadable"         => 10304,
+        "Conf/Exception.fileUnwritable"         => 10305,
+        "Conf/Exception.fileUncreatable"        => 10306,
+        "Conf/Exception.fileCorrupt"            => 10307,
+        "User/Exception.functionNotImplemented" => 10401,
+        "User/Exception.doesNotExist"           => 10402,
+        "User/Exception.alreadyExists"          => 10403,
+        "User/Exception.authMissing"            => 10411,
+        "User/Exception.authFailed"             => 10412,
+        "User/Exception.notAuthorized"          => 10421,
+    ];
 
-	public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {
-		if($msgID=="") {
-			$msg = "Exception.unknown";
-			$code = 10000;
-		} else {
-			$class = get_called_class();
-			$codeID = str_replace("\\", "/", str_replace(NS_BASE, "", $class)).".$msgID";
-			if(!array_key_exists($codeID, self::CODES)) {
-				throw new Exception("uncoded");
-			} else {
-				$code = self::CODES[$codeID];
-				$msg = "Exception.".str_replace("\\", "/", $class).".$msgID";
-			}
-			$msg = Lang::msg($msg, $vars);
-		}
-		parent::__construct($msg, $code, $e);
-	}
+    public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {
+        if($msgID=="") {
+            $msg = "Exception.unknown";
+            $code = 10000;
+        } else {
+            $class = get_called_class();
+            $codeID = str_replace("\\", "/", str_replace(NS_BASE, "", $class)).".$msgID";
+            if(!array_key_exists($codeID, self::CODES)) {
+                throw new Exception("uncoded");
+            } else {
+                $code = self::CODES[$codeID];
+                $msg = "Exception.".str_replace("\\", "/", $class).".$msgID";
+            }
+            $msg = Lang::msg($msg, $vars);
+        }
+        parent::__construct($msg, $code, $e);
+    }
 }
\ No newline at end of file
diff --git a/lib/Conf.php b/lib/Conf.php
index c9d91947..9ed98994 100644
--- a/lib/Conf.php
+++ b/lib/Conf.php
@@ -3,64 +3,64 @@ declare(strict_types=1);
 namespace JKingWeb\NewsSync;
 
 class Conf {
-	public $lang 					= "en";
-	
-	public $dbDriver				= Db\DriverSQLite3::class;
-	public $dbSQLite3File 			= BASE."newssync.db";
-	public $dbSQLite3Key 			= "";
-	public $dbSQLite3AutoUpd 		= true;
-	public $dbPostgreSQLHost 		= "localhost";
-	public $dbPostgreSQLUser 		= "newssync";
-	public $dbPostgreSQLPass 		= "";
-	public $dbPostgreSQLPort 		= 5432;
-	public $dbPostgreSQLDb 			= "newssync";
-	public $dbPostgreSQLSchema 		= "";
-	public $dbPostgreSQLAutoUpd 	= false;
-	public $dbMySQLHost 			= "localhost";
-	public $dbMySQLUser 			= "newssync";
-	public $dbMySQLPass 			= "";
-	public $dbMySQLPort 			= 3306;
-	public $dbMySQLDb 				= "newssync";
-	public $dbMySQLAutoUpd 			= false;
+    public $lang                    = "en";
 
-	public $userDriver 				= User\DriverInternal::class;
-	public $userAuthPreferHTTP 		= false;
-	public $userComposeNames 		= true;
+    public $dbDriver                = Db\DriverSQLite3::class;
+    public $dbSQLite3File           = BASE."newssync.db";
+    public $dbSQLite3Key            = "";
+    public $dbSQLite3AutoUpd        = true;
+    public $dbPostgreSQLHost        = "localhost";
+    public $dbPostgreSQLUser        = "newssync";
+    public $dbPostgreSQLPass        = "";
+    public $dbPostgreSQLPort        = 5432;
+    public $dbPostgreSQLDb          = "newssync";
+    public $dbPostgreSQLSchema      = "";
+    public $dbPostgreSQLAutoUpd     = false;
+    public $dbMySQLHost             = "localhost";
+    public $dbMySQLUser             = "newssync";
+    public $dbMySQLPass             = "";
+    public $dbMySQLPort             = 3306;
+    public $dbMySQLDb               = "newssync";
+    public $dbMySQLAutoUpd          = false;
 
-	public $simplepieCache 			= BASE.".cache";
+    public $userDriver              = User\DriverInternal::class;
+    public $userAuthPreferHTTP      = false;
+    public $userComposeNames        = true;
+
+    public $simplepieCache          = BASE.".cache";
 
 
-	public function __construct(string $import_file = "") {
-		if($import_file != "") $this->importFile($import_file);
-	}
+    public function __construct(string $import_file = "") {
+        if($import_file != "") $this->importFile($import_file);
+    }
 
-	public function importFile(string $file): self {
-		if(!file_exists($file)) throw new Conf\Exception("fileMissing", $file);
-		if(!is_readable($file)) throw new Conf\Exception("fileUnreadable", $file);
-		try {
-			ob_start();
-			$arr = (@include $file);
-		} catch(\Throwable $e) {
-			$arr = null;
-		} finally {
-			ob_end_clean();
-		}
-		if(!is_array($arr)) throw new Conf\Exception("fileCorrupt", $file);
-		return $this->import($arr);
-	}
+    public function importFile(string $file): self {
+        if(!file_exists($file)) throw new Conf\Exception("fileMissing", $file);
+        if(!is_readable($file)) throw new Conf\Exception("fileUnreadable", $file);
+        try {
+            ob_start();
+            $arr = (@include $file);
+        } catch(\Throwable $e) {
+            $arr = null;
+        } finally {
+            ob_end_clean();
+        }
+        if(!is_array($arr)) throw new Conf\Exception("fileCorrupt", $file);
+        return $this->import($arr);
+    }
 
-	public function import(array $arr): self {
-		foreach($arr as $key => $value) {
-			$this->$key = $value;
-		}
-		return $this;
-	}
+    public function import(array $arr): self {
+        foreach($arr as $key => $value) {
+            $this->$key = $value;
+        }
+        return $this;
+    }
 
-	public function export(string $file = ""): string {
-		// TODO
-	}
+    public function export(string $file = ""): string {
+        // TODO
+    }
 
-	public function __toString(): string {
-		return $this->export();
-	}
+    public function __toString(): string {
+        return $this->export();
+    }
 }
\ No newline at end of file
diff --git a/lib/Database.php b/lib/Database.php
index 59ff2a43..ef924aa6 100644
--- a/lib/Database.php
+++ b/lib/Database.php
@@ -3,275 +3,275 @@ declare(strict_types=1);
 namespace JKingWeb\NewsSync;
 
 class Database {
-	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;
+    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;
 
-	protected function cleanName(string $name): string {
-		return (string) preg_filter("[^0-9a-zA-Z_\.]", "", $name);
-	}
+    protected function cleanName(string $name): string {
+        return (string) preg_filter("[^0-9a-zA-Z_\.]", "", $name);
+    }
 
-	public function __construct(RuntimeData $data) {
-		$this->data = $data;
-		$driver = $data->conf->dbDriver;
-		$this->db = $driver::create($data, INSTALL);
-		$ver = $this->db->schemaVersion();
-		if(!INSTALL && $ver < self::SCHEMA_VERSION) {
-			$this->db->update(self::SCHEMA_VERSION);
-		}
-	}
+    public function __construct(RuntimeData $data) {
+        $this->data = $data;
+        $driver = $data->conf->dbDriver;
+        $this->db = $driver::create($data, INSTALL);
+        $ver = $this->db->schemaVersion();
+        if(!INSTALL && $ver < self::SCHEMA_VERSION) {
+            $this->db->update(self::SCHEMA_VERSION);
+        }
+    }
 
-	static public function listDrivers(): array {
-		$sep = \DIRECTORY_SEPARATOR;
-		$path = __DIR__.$sep."Db".$sep;
-		$classes = [];
-		foreach(glob($path."Driver?*.php") as $file) {
-			$name = basename($file, ".php");
-			if(substr($name,-3) != "PDO") {
-				$name = NS_BASE."Db\\$name";
-				if(class_exists($name)) {
-					$classes[$name] = $name::driverName();
-				}
-			}			 
-		}
-		return $classes;
-	}
+    static public function listDrivers(): array {
+        $sep = \DIRECTORY_SEPARATOR;
+        $path = __DIR__.$sep."Db".$sep;
+        $classes = [];
+        foreach(glob($path."Driver?*.php") as $file) {
+            $name = basename($file, ".php");
+            if(substr($name,-3) != "PDO") {
+                $name = NS_BASE."Db\\$name";
+                if(class_exists($name)) {
+                    $classes[$name] = $name::driverName();
+                }
+            }             
+        }
+        return $classes;
+    }
 
-	public function schemaVersion(): int {
-		return $this->db->schemaVersion();
-	}
+    public function schemaVersion(): int {
+        return $this->db->schemaVersion();
+    }
 
-	public function dbUpdate(): bool {
-		if($this->db->schemaVersion() < self::SCHEMA_VERSION) return $this->db->update(self::SCHEMA_VERSION);
-		return false;
-	}
+    public function dbUpdate(): bool {
+        if($this->db->schemaVersion() < self::SCHEMA_VERSION) return $this->db->update(self::SCHEMA_VERSION);
+        return false;
+    }
 
-	public function settingGet(string $key) {
-		$row = $this->db->prepare("SELECT value, type from newssync_settings where key = ?", "str")->run($key)->get();
-		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'];
-		}
-	}
+    public function settingGet(string $key) {
+        $row = $this->db->prepare("SELECT value, type from newssync_settings where key = ?", "str")->run($key)->get();
+        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'];
+        }
+    }
 
-	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;
-				case "string":
-				case "array": 			$type = "json"; break;
-				case "resource":
-				case "unknown type":
-				case "NULL": 			$type = "null"; break;
-				case "object":
-					if($in instanceof DateTimeInterface) {
-						$type = "timestamp";
-					} else {
-						$type = "text";
-					}
-					break;
-				default: 				$type = 'null'; break;
-			}
-		}
-		$type = strtolower($type);
-		switch($type) {
-			case "integer":
-				$type = "int";
-			case "int":
-				$value =& $in;
-				break;
-			case "float":
-			case "double":
-			case "real":
-				$type = "numeric";
-			case "numeric":
-				$value =& $in;
-				break;
-			case "str":
-			case "string":
-				$type = "text";
-			case "text":
-				$value =& $in;
-				break;
-			case "json":
-				if(is_array($in) || is_object($in)) {
-					$value = json_encode($in);
-				} else {
-					$value =& $in;
-				} 
-				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";
-				$value =& $in;
-				break;
-		}
-		$this->db->prepare("REPLACE INTO newssync_settings(key,value,type) values(?,?,?)", "str", (($type=="null") ? "null" : "str"), "str")->run($key, $value, "text");
-	}
+    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;
+                case "string":
+                case "array":             $type = "json"; break;
+                case "resource":
+                case "unknown type":
+                case "NULL":             $type = "null"; break;
+                case "object":
+                    if($in instanceof DateTimeInterface) {
+                        $type = "timestamp";
+                    } else {
+                        $type = "text";
+                    }
+                    break;
+                default:                 $type = 'null'; break;
+            }
+        }
+        $type = strtolower($type);
+        switch($type) {
+            case "integer":
+                $type = "int";
+            case "int":
+                $value =& $in;
+                break;
+            case "float":
+            case "double":
+            case "real":
+                $type = "numeric";
+            case "numeric":
+                $value =& $in;
+                break;
+            case "str":
+            case "string":
+                $type = "text";
+            case "text":
+                $value =& $in;
+                break;
+            case "json":
+                if(is_array($in) || is_object($in)) {
+                    $value = json_encode($in);
+                } else {
+                    $value =& $in;
+                } 
+                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";
+                $value =& $in;
+                break;
+        }
+        $this->db->prepare("REPLACE INTO newssync_settings(key,value,type) values(?,?,?)", "str", (($type=="null") ? "null" : "str"), "str")->run($key, $value, "text");
+    }
 
-	public function settingRemove(string $key): bool {
-		$this->db->prepare("DELETE from newssync_settings where key = ?", "str")->run($key);
-		return true;
-	}
+    public function settingRemove(string $key): bool {
+        $this->db->prepare("DELETE from newssync_settings where key = ?", "str")->run($key);
+        return true;
+    }
 
-	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)->getSingle();
-	}
+    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)->getSingle();
+    }
 
-	public function userAdd(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;
-		if(strlen($password) > 0) $password = password_hash($password, \PASSWORD_DEFAULT);
-		$this->db->prepare("INSERT INTO newssync_users(id,password) values(?,?)", "str", "str", "str")->run($user,$password,$admin);
-		return true;
-	}
+    public function userAdd(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;
+        if(strlen($password) > 0) $password = password_hash($password, \PASSWORD_DEFAULT);
+        $this->db->prepare("INSERT INTO newssync_users(id,password) values(?,?)", "str", "str", "str")->run($user,$password,$admin);
+        return true;
+    }
 
-	public function userRemove(string $user): bool {
-		if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
-		$this->db->prepare("DELETE from newssync_users where id is ?", "str")->run($user);
-		return true;
-	}
+    public function userRemove(string $user): bool {
+        if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
+        $this->db->prepare("DELETE from newssync_users where id is ?", "str")->run($user);
+        return true;
+    }
 
-	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;
-			$set = $this->db->prepare("SELECT id from newssync_users where id like ?", "str")->run($domain);
-		} else {
-			if(!$this->data->user->authorize("", __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "all users"]);
-			$set = $this->db->prepare("SELECT id from newssync_users")->run();
-		}
-		$out = [];
-		foreach($set as $row) {
-			$out[] = $row["id"];
-		}
-		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;
-		if(strlen($password > 0)) $password = password_hash($password);
-		$this->db->prepare("UPDATE newssync_users set password = ? where id is ?", "str", "str")->run($password, $user);
-		return true;
-	}
+    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;
+            $set = $this->db->prepare("SELECT id from newssync_users where id like ?", "str")->run($domain);
+        } else {
+            if(!$this->data->user->authorize("", __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "all users"]);
+            $set = $this->db->prepare("SELECT id from newssync_users")->run();
+        }
+        $out = [];
+        foreach($set as $row) {
+            $out[] = $row["id"];
+        }
+        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;
+        if(strlen($password > 0)) $password = password_hash($password);
+        $this->db->prepare("UPDATE newssync_users set password = ? where id is ?", "str", "str")->run($password, $user);
+        return true;
+    }
 
-	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)->get();
-		if(!$prop) return [];
-		return $prop;
-	}
+    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)->get();
+        if(!$prop) return [];
+        return $prop;
+    }
 
-	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", 
-		];
-		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->commit();
-		return $this->userPropertiesGet($user);
-	}
+    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", 
+        ];
+        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->commit();
+        return $this->userPropertiesGet($user);
+    }
 
-	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)->getSingle();
-	}
+    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)->getSingle();
+    }
 
-	public function userRightsSet(string $user, int $rights): bool {
-		if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
-		if(!$this->userExists($user)) return false;
-		$this->db->prepare("UPDATE newssync_users set rights = ? where id is ?", "int", "str")->run($rights, $user);
-		return true;
-	}
+    public function userRightsSet(string $user, int $rights): bool {
+        if(!$this->data->user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
+        if(!$this->userExists($user)) return false;
+        $this->db->prepare("UPDATE newssync_users set rights = ? where id is ?", "int", "str")->run($rights, $user);
+        return true;
+    }
 
-	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__]);
-		$this->db->begin();
-		$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);
-			$feed = $qFeed->run($url, $fetchUser, $fetchPassword)->getSingle();
-		}
-		$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();
-		return $sub;
-	}
+    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__]);
+        $this->db->begin();
+        $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);
+            $feed = $qFeed->run($url, $fetchUser, $fetchPassword)->getSingle();
+        }
+        $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();
+        return $sub;
+    }
 
-	public function subscriptionRemove(int $id): bool {
-		$this->db->begin();
-		$user = $this->db->prepare("SELECT owner from newssync_subscriptions where id is ?", "int")->run($id)->getSingle();
-		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 subscriptionRemove(int $id): bool {
+        $this->db->begin();
+        $user = $this->db->prepare("SELECT owner from newssync_subscriptions where id is ?", "int")->run($id)->getSingle();
+        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();
+    }
 
 }
\ No newline at end of file
diff --git a/lib/Db/Common.php b/lib/Db/Common.php
index ceb64eb7..27a1926e 100644
--- a/lib/Db/Common.php
+++ b/lib/Db/Common.php
@@ -4,70 +4,70 @@ namespace JKingWeb\NewsSync\Db;
 use JKingWeb\DrUUID\UUID as UUID;
 
 Trait Common {
-	protected $transDepth = 0;
-	
-	public function schemaVersion(): int {
-		try {
-			return $this->data->db->settingGet("schema_version");
-		} catch(\Throwable $e) {
-			return 0;
-		}
-	}
-	
-	public function begin(): bool {
-		$this->exec("SAVEPOINT newssync_".($this->transDepth));
-		$this->transDepth += 1;
-		return true;
-	}
+    protected $transDepth = 0;
+    
+    public function schemaVersion(): int {
+        try {
+            return $this->data->db->settingGet("schema_version");
+        } catch(\Throwable $e) {
+            return 0;
+        }
+    }
+    
+    public function begin(): bool {
+        $this->exec("SAVEPOINT newssync_".($this->transDepth));
+        $this->transDepth += 1;
+        return true;
+    }
 
-	public function commit(bool $all = false): bool {
-		if($this->transDepth==0) return false;
-		if(!$all) {
-			$this->exec("RELEASE SAVEPOINT newssync_".($this->transDepth - 1));
-			$this->transDepth -= 1;
-		} else {
-			$this->exec("COMMIT TRANSACTION");
-			$this->transDepth = 0;	
-		}
-		return true;
-	}
+    public function commit(bool $all = false): bool {
+        if($this->transDepth==0) return false;
+        if(!$all) {
+            $this->exec("RELEASE SAVEPOINT newssync_".($this->transDepth - 1));
+            $this->transDepth -= 1;
+        } else {
+            $this->exec("COMMIT TRANSACTION");
+            $this->transDepth = 0;    
+        }
+        return true;
+    }
 
-	public function rollback(bool $all = false): bool {
-		if($this->transDepth==0) return false;
-		if(!$all) {
-			$this->exec("ROLLBACK TRANSACTION TO SAVEPOINT newssync_".($this->transDepth - 1));
-			// rollback to savepoint does not collpase the savepoint
-			$this->commit();
-			$this->transDepth -= 1;
-			if($this->transDepth==0) $this->exec("ROLLBACK TRANSACTION");
-		} else {
-			$this->exec("ROLLBACK TRANSACTION");
-			$this->transDepth = 0;
-		}
-		return true;
-	}
+    public function rollback(bool $all = false): bool {
+        if($this->transDepth==0) return false;
+        if(!$all) {
+            $this->exec("ROLLBACK TRANSACTION TO SAVEPOINT newssync_".($this->transDepth - 1));
+            // rollback to savepoint does not collpase the savepoint
+            $this->commit();
+            $this->transDepth -= 1;
+            if($this->transDepth==0) $this->exec("ROLLBACK TRANSACTION");
+        } else {
+            $this->exec("ROLLBACK TRANSACTION");
+            $this->transDepth = 0;
+        }
+        return true;
+    }
 
-	public function lock(): bool {
-		if($this->schemaVersion() < 1) return true;
-		if($this->isLocked()) return false;
-		$uuid = UUID::mintStr();
-		if(!$this->data->db->settingSet("lock", $uuid)) return false;
-		sleep(1);
-		if($this->data->db->settingGet("lock") != $uuid) return false;
-		return true;
-	}
+    public function lock(): bool {
+        if($this->schemaVersion() < 1) return true;
+        if($this->isLocked()) return false;
+        $uuid = UUID::mintStr();
+        if(!$this->data->db->settingSet("lock", $uuid)) return false;
+        sleep(1);
+        if($this->data->db->settingGet("lock") != $uuid) return false;
+        return true;
+    }
 
-	public function unlock(): bool {
-		return $this->data->db->settingRemove("lock");
-	}
+    public function unlock(): bool {
+        return $this->data->db->settingRemove("lock");
+    }
 
-	public function isLocked(): bool {
-		if($this->schemaVersion() < 1) return false;
-		return ($this->query("SELECT count(*) from newssync_settings where key = 'lock'")->getSingle() > 0);
-	}
+    public function isLocked(): bool {
+        if($this->schemaVersion() < 1) return false;
+        return ($this->query("SELECT count(*) from newssync_settings where key = 'lock'")->getSingle() > 0);
+    }
 
-	public function prepare(string $query, string ...$paramType): Statement {
-		return $this->prepareArray($query, $paramType);
-	}
+    public function prepare(string $query, string ...$paramType): Statement {
+        return $this->prepareArray($query, $paramType);
+    }
 
 }
\ No newline at end of file
diff --git a/lib/Db/CommonPDO.php b/lib/Db/CommonPDO.php
index 308d918f..4489c9a0 100644
--- a/lib/Db/CommonPDO.php
+++ b/lib/Db/CommonPDO.php
@@ -3,15 +3,15 @@ declare(strict_types=1);
 namespace JKingWeb\NewsSync\Db;
 
 Trait CommonPDO {
-	public function query(string $query): Result {
-		return new ResultPDO($this->db->query($query));
-	}
+    public function query(string $query): Result {
+        return new ResultPDO($this->db->query($query));
+    }
 
-	public function prepareArray(string $query, array $paramTypes): Statement {
-		return new StatementPDO($query, $paramTypes);
-	}
+    public function prepareArray(string $query, array $paramTypes): Statement {
+        return new StatementPDO($query, $paramTypes);
+    }
 
-	public function prepare(string $query, string ...$paramType): Statement {
-		return $this->prepareArray($query, $paramType);
-	}
+    public function prepare(string $query, string ...$paramType): Statement {
+        return $this->prepareArray($query, $paramType);
+    }
 }
\ No newline at end of file
diff --git a/lib/Db/CommonSQLite3.php b/lib/Db/CommonSQLite3.php
index 66ad8589..ab5ceea6 100644
--- a/lib/Db/CommonSQLite3.php
+++ b/lib/Db/CommonSQLite3.php
@@ -3,48 +3,48 @@ declare(strict_types=1);
 namespace JKingWeb\NewsSync\Db;
 
 Trait CommonSQLite3 {
-	
-	static public function driverName(): string {
-		return "SQLite 3";
-	}
+    
+    static public function driverName(): string {
+        return "SQLite 3";
+    }
 
-	public function schemaVersion(): int {
-		return $this->query("PRAGMA user_version")->getSingle();
-	}
+    public function schemaVersion(): int {
+        return $this->query("PRAGMA user_version")->getSingle();
+    }
 
-	public function update(int $to): bool {
-		$ver = $this->schemaVersion();
-		if(!$this->data->conf->dbSQLite3AutoUpd)  throw new Update\Exception("manual", ['version' => $ver, 'driver_name' => $this->driverName()]);
-		if($ver >= $to) throw new Update\Exception("tooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]);
-		$sep = \DIRECTORY_SEPARATOR;
-		$path = \JKingWeb\NewsSync\BASE."sql".$sep."SQLite3".$sep;
-		$this->lock();
-		$this->begin();
-		for($a = $ver; $a < $to; $a++) {
-			$this->begin();
-			try {
-				$file = $path.$a.".sql";
-				if(!file_exists($file)) throw new Update\Exception("fileMissing", ['file' => $file, 'driver_name' => $this->driverName()]);
-				if(!is_readable($file)) throw new Update\Exception("fileUnreadable", ['file' => $file, 'driver_name' => $this->driverName()]);
-				$sql = @file_get_contents($file);
-				if($sql===false) throw new Update\Exception("fileUnusable", ['file' => $file, 'driver_name' => $this->driverName()]);
-				$this->exec($sql);
-			} catch(\Throwable $e) {
-				// undo any partial changes from the failed update
-				$this->rollback();
-				// commit any successful updates if updating by more than one version
-				$this->commit(true);
-				// throw the error received
-				throw $e;
-			}
-			$this->commit();
-		}
-		$this->unlock();
-		$this->commit();
-		return true;
-	}
+    public function update(int $to): bool {
+        $ver = $this->schemaVersion();
+        if(!$this->data->conf->dbSQLite3AutoUpd)  throw new Update\Exception("manual", ['version' => $ver, 'driver_name' => $this->driverName()]);
+        if($ver >= $to) throw new Update\Exception("tooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]);
+        $sep = \DIRECTORY_SEPARATOR;
+        $path = \JKingWeb\NewsSync\BASE."sql".$sep."SQLite3".$sep;
+        $this->lock();
+        $this->begin();
+        for($a = $ver; $a < $to; $a++) {
+            $this->begin();
+            try {
+                $file = $path.$a.".sql";
+                if(!file_exists($file)) throw new Update\Exception("fileMissing", ['file' => $file, 'driver_name' => $this->driverName()]);
+                if(!is_readable($file)) throw new Update\Exception("fileUnreadable", ['file' => $file, 'driver_name' => $this->driverName()]);
+                $sql = @file_get_contents($file);
+                if($sql===false) throw new Update\Exception("fileUnusable", ['file' => $file, 'driver_name' => $this->driverName()]);
+                $this->exec($sql);
+            } catch(\Throwable $e) {
+                // undo any partial changes from the failed update
+                $this->rollback();
+                // commit any successful updates if updating by more than one version
+                $this->commit(true);
+                // throw the error received
+                throw $e;
+            }
+            $this->commit();
+        }
+        $this->unlock();
+        $this->commit();
+        return true;
+    }
 
-	public function exec(string $query): bool {
-		return (bool) $this->db->exec($query);
-	}
+    public function exec(string $query): bool {
+        return (bool) $this->db->exec($query);
+    }
 }
\ No newline at end of file
diff --git a/lib/Db/Driver.php b/lib/Db/Driver.php
index 7ec5c5c3..de05cc69 100644
--- a/lib/Db/Driver.php
+++ b/lib/Db/Driver.php
@@ -2,29 +2,29 @@
 declare(strict_types=1);
 namespace JKingWeb\NewsSync\Db;
 
-interface Driver {	
-	// returns an instance of a class implementing this interface. Implemented as a static method so that classes may return their PDO equivalents instead of themselves
-	static function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver;
-	// returns a human-friendly name for the driver (for display in installer, for example)
-	static function driverName(): string;
-	// returns the version of the scheme of the opened database; if uninitialized should return 0
-	function schemaVersion(): int;
-	// begin a real or synthetic transactions, with real or synthetic nesting
-	function begin(): bool;
-	// commit either the latest or all pending nested transactions; use of this method should assume a partial commit is a no-op
-	function commit(bool $all = false): bool;
-	// rollback either the latest or all pending nested transactions; use of this method should assume a partial rollback will not work
-	function rollback(bool $all = false): bool;
-	// attempt to advise other processes that they should not attempt to access the database; used during live upgrades
-	function lock(): bool;
-	function unlock(): bool;
-	function isLocked(): bool;
-	// attempt to perform an in-place upgrade of the database schema; this may be a no-op which always throws an exception
-	function update(int $to): bool;
-	// execute one or more unsanitized SQL queries and return an indication of success
-	function exec(string $query): bool;
-	// perform a single unsanitized query and return a result set
-	function query(string $query): Result;
-	// ready a prepared statement for later execution
-	function prepare(string $query, string ...$paramType): Statement;
+interface Driver {    
+    // returns an instance of a class implementing this interface. Implemented as a static method so that classes may return their PDO equivalents instead of themselves
+    static function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver;
+    // returns a human-friendly name for the driver (for display in installer, for example)
+    static function driverName(): string;
+    // returns the version of the scheme of the opened database; if uninitialized should return 0
+    function schemaVersion(): int;
+    // begin a real or synthetic transactions, with real or synthetic nesting
+    function begin(): bool;
+    // commit either the latest or all pending nested transactions; use of this method should assume a partial commit is a no-op
+    function commit(bool $all = false): bool;
+    // rollback either the latest or all pending nested transactions; use of this method should assume a partial rollback will not work
+    function rollback(bool $all = false): bool;
+    // attempt to advise other processes that they should not attempt to access the database; used during live upgrades
+    function lock(): bool;
+    function unlock(): bool;
+    function isLocked(): bool;
+    // attempt to perform an in-place upgrade of the database schema; this may be a no-op which always throws an exception
+    function update(int $to): bool;
+    // execute one or more unsanitized SQL queries and return an indication of success
+    function exec(string $query): bool;
+    // perform a single unsanitized query and return a result set
+    function query(string $query): Result;
+    // ready a prepared statement for later execution
+    function prepare(string $query, string ...$paramType): Statement;
 }
\ No newline at end of file
diff --git a/lib/Db/DriverSQLite3.php b/lib/Db/DriverSQLite3.php
index 7cff0263..7ca9307c 100644
--- a/lib/Db/DriverSQLite3.php
+++ b/lib/Db/DriverSQLite3.php
@@ -3,57 +3,57 @@ declare(strict_types=1);
 namespace JKingWeb\NewsSync\Db;
 
 class DriverSQLite3 implements Driver {
-	use Common, CommonSQLite3 {
-		CommonSQLite3::schemaVersion insteadof Common;
-	}
-	
-	protected $db;
-	protected $data;
-	
-	private function __construct(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false) {
-		$this->data = $data;
-		$file = $data->conf->dbSQLite3File;
-		// if the file exists (or we're initializing the database), try to open it and set initial options
-		try {
-			$this->db = new \SQLite3($file, ($install) ? \SQLITE3_OPEN_READWRITE | \SQLITE3_OPEN_CREATE : \SQLITE3_OPEN_READWRITE, $data->conf->dbSQLite3Key);
-			$this->db->enableExceptions(true);
-			$this->exec("PRAGMA journal_mode = wal");
-			$this->exec("PRAGMA foreign_keys = yes");
-		} catch(\Throwable $e) {
-			// if opening the database doesn't work, check various pre-conditions to find out what the problem might be
-			if(!file_exists($file)) {
-				if($install && !is_writable(dirname($file))) throw new Exception("fileUncreatable", dirname($file));
-				throw new Exception("fileMissing", $file);
-			}
-			if(!is_readable($file) && !is_writable($file)) throw new Exception("fileUnusable", $file);
-			if(!is_readable($file)) throw new Exception("fileUnreadable", $file);
-			if(!is_writable($file)) throw new Exception("fileUnwritable", $file);
-			// otherwise the database is probably corrupt
-			throw new Exception("fileCorrupt", $mainfile);
-		}
-	}
+    use Common, CommonSQLite3 {
+        CommonSQLite3::schemaVersion insteadof Common;
+    }
+    
+    protected $db;
+    protected $data;
+    
+    private function __construct(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false) {
+        $this->data = $data;
+        $file = $data->conf->dbSQLite3File;
+        // if the file exists (or we're initializing the database), try to open it and set initial options
+        try {
+            $this->db = new \SQLite3($file, ($install) ? \SQLITE3_OPEN_READWRITE | \SQLITE3_OPEN_CREATE : \SQLITE3_OPEN_READWRITE, $data->conf->dbSQLite3Key);
+            $this->db->enableExceptions(true);
+            $this->exec("PRAGMA journal_mode = wal");
+            $this->exec("PRAGMA foreign_keys = yes");
+        } catch(\Throwable $e) {
+            // if opening the database doesn't work, check various pre-conditions to find out what the problem might be
+            if(!file_exists($file)) {
+                if($install && !is_writable(dirname($file))) throw new Exception("fileUncreatable", dirname($file));
+                throw new Exception("fileMissing", $file);
+            }
+            if(!is_readable($file) && !is_writable($file)) throw new Exception("fileUnusable", $file);
+            if(!is_readable($file)) throw new Exception("fileUnreadable", $file);
+            if(!is_writable($file)) throw new Exception("fileUnwritable", $file);
+            // otherwise the database is probably corrupt
+            throw new Exception("fileCorrupt", $mainfile);
+        }
+    }
 
-	public function __destruct() {
-		$this->db->close();
-		unset($this->db);
-	}
+    public function __destruct() {
+        $this->db->close();
+        unset($this->db);
+    }
 
-	static public function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver {
-		// check to make sure required extensions are loaded
-		if(class_exists("SQLite3")) {
-			return new self($data, $install);
-		} else if(class_exists("PDO") && in_array("sqlite",\PDO::getAvailableDrivers())) {
-			return new DriverSQLite3PDO($data, $install);
-		} else {
-			throw new Exception("extMissing", self::driverName());
-		}
-	}
+    static public function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver {
+        // check to make sure required extensions are loaded
+        if(class_exists("SQLite3")) {
+            return new self($data, $install);
+        } else if(class_exists("PDO") && in_array("sqlite",\PDO::getAvailableDrivers())) {
+            return new DriverSQLite3PDO($data, $install);
+        } else {
+            throw new Exception("extMissing", self::driverName());
+        }
+    }
 
-	public function query(string $query): Result {
-		return new ResultSQLite3($this->db->query($query), $this->db->changes());
-	}
+    public function query(string $query): Result {
+        return new ResultSQLite3($this->db->query($query), $this->db->changes());
+    }
 
-	public function prepareArray(string $query, array $paramTypes): Statement {
-		return new StatementSQLite3($this->db, $this->db->prepare($query), $paramTypes);
-	}
+    public function prepareArray(string $query, array $paramTypes): Statement {
+        return new StatementSQLite3($this->db, $this->db->prepare($query), $paramTypes);
+    }
 }
\ No newline at end of file
diff --git a/lib/Db/DriverSQLite3PDO.php b/lib/Db/DriverSQLite3PDO.php
index 166b059f..72d02528 100644
--- a/lib/Db/DriverSQLite3PDO.php
+++ b/lib/Db/DriverSQLite3PDO.php
@@ -3,26 +3,26 @@ declare(strict_types=1);
 namespace JKingWeb\NewsSync\Db;
 
 class DriverSQLite3 implements Driver {
-	use CommonPDO, CommonSQLite3;
-	
-	protected $db;
-	
-	private function __construct(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false) {
-		// FIXME: stub
-	}
+    use CommonPDO, CommonSQLite3;
+    
+    protected $db;
+    
+    private function __construct(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false) {
+        // FIXME: stub
+    }
 
-	public function __destruct() {
-		// FIXME: stub
-	}
+    public function __destruct() {
+        // FIXME: stub
+    }
 
-	static public function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver {
-		// check to make sure required extensions are loaded
-		if(class_exists("PDO") && in_array("sqlite",\PDO::getAvailableDrivers())) {
-			return new self($data, $install);
-		} else if(class_exists("SQLite3")) {
-			return new DriverSQLite3($data, $install);
-		} else {
-			throw new Exception("extMissing", self::driverName());
-		}
-	}
+    static public function create(\JKingWeb\NewsSync\RuntimeData $data, bool $install = false): Driver {
+        // check to make sure required extensions are loaded
+        if(class_exists("PDO") && in_array("sqlite",\PDO::getAvailableDrivers())) {
+            return new self($data, $install);
+        } else if(class_exists("SQLite3")) {
+            return new DriverSQLite3($data, $install);
+        } else {
+            throw new Exception("extMissing", self::driverName());
+        }
+    }
 }
\ No newline at end of file
diff --git a/lib/Db/Result.php b/lib/Db/Result.php
index 78ad90ab..ea34b39b 100644
--- a/lib/Db/Result.php
+++ b/lib/Db/Result.php
@@ -3,13 +3,13 @@ declare(strict_types=1);
 namespace JKingWeb\NewsSync\Db;
 
 interface Result extends \Iterator {
-	function current();
-	function key();
-	function next();
-	function rewind();
-	function valid();
+    function current();
+    function key();
+    function next();
+    function rewind();
+    function valid();
 
-	function get();
-	function getSingle();
-	function changes();
+    function get();
+    function getSingle();
+    function changes();
 }
\ No newline at end of file
diff --git a/lib/Db/ResultSQLite3.php b/lib/Db/ResultSQLite3.php
index 10400497..af7202f8 100644
--- a/lib/Db/ResultSQLite3.php
+++ b/lib/Db/ResultSQLite3.php
@@ -3,62 +3,62 @@ declare(strict_types=1);
 namespace JKingWeb\NewsSync\Db;
 
 class ResultSQLite3 implements Result {
-	protected $st;
-	protected $set;
-	protected $pos = 0;
-	protected $cur = null;
-	protected $rows = 0;
+    protected $st;
+    protected $set;
+    protected $pos = 0;
+    protected $cur = null;
+    protected $rows = 0;
 
-	public function __construct($result, $changes, $statement = null) {
-		$this->st = $statement; //keeps the statement from being destroyed, invalidating the result set
-		$this->set = $result;
-		$this->rows = $changes;
-	}
+    public function __construct($result, $changes, $statement = null) {
+        $this->st = $statement; //keeps the statement from being destroyed, invalidating the result set
+        $this->set = $result;
+        $this->rows = $changes;
+    }
 
-	public function __destruct() {
-		$this->set->finalize();
-		unset($this->set);
-	}
+    public function __destruct() {
+        $this->set->finalize();
+        unset($this->set);
+    }
 
-	public function valid() {
-		$this->cur = $this->set->fetchArray(\SQLITE3_ASSOC);
-		return ($this->cur !== false);
-	}
+    public function valid() {
+        $this->cur = $this->set->fetchArray(\SQLITE3_ASSOC);
+        return ($this->cur !== false);
+    }
 
-	public function next() {
-		$this->cur = null;
-		$this->pos += 1;
-	}
+    public function next() {
+        $this->cur = null;
+        $this->pos += 1;
+    }
 
-	public function current() {
-		return $this->cur;
-	}
+    public function current() {
+        return $this->cur;
+    }
 
-	public function key() {
-		return $this->pos;
-	}
+    public function key() {
+        return $this->pos;
+    }
 
-	public function rewind() {
-		$this->pos = 0;
-		$this->cur = null;
-		$this->set->reset();
-	}
+    public function rewind() {
+        $this->pos = 0;
+        $this->cur = null;
+        $this->set->reset();
+    }
 
-	public function getSingle() {
-		$this->next();
-		if($this->valid()) {
-			$keys = array_keys($this->cur);
-			return $this->cur[array_shift($keys)];
-		}
-		return null;
-	}
+    public function getSingle() {
+        $this->next();
+        if($this->valid()) {
+            $keys = array_keys($this->cur);
+            return $this->cur[array_shift($keys)];
+        }
+        return null;
+    }
 
-	public function get() {
-		$this->next();
-		return ($this->valid() ? $this->cur : null);
-	}
+    public function get() {
+        $this->next();
+        return ($this->valid() ? $this->cur : null);
+    }
 
-	public function changes() {
-		return $this->rows;
-	}
+    public function changes() {
+        return $this->rows;
+    }
 }
\ No newline at end of file
diff --git a/lib/Db/Statement.php b/lib/Db/Statement.php
index 4dee6d9a..a127a89a 100644
--- a/lib/Db/Statement.php
+++ b/lib/Db/Statement.php
@@ -3,7 +3,7 @@ declare(strict_types=1);
 namespace JKingWeb\NewsSync\Db;
 
 interface Statement {
-	function __invoke(&...$values); // alias of run()
-	function run(&...$values): Result;
-	function runArray(array &$values): Result;
+    function __invoke(&...$values); // alias of run()
+    function run(&...$values): Result;
+    function runArray(array &$values): Result;
 }
\ No newline at end of file
diff --git a/lib/Db/StatementSQLite3.php b/lib/Db/StatementSQLite3.php
index e0b235c3..bc87c7b2 100644
--- a/lib/Db/StatementSQLite3.php
+++ b/lib/Db/StatementSQLite3.php
@@ -3,71 +3,71 @@ declare(strict_types=1);
 namespace JKingWeb\NewsSync\Db;
 
 class StatementSQLite3 implements Statement {
-	protected $db;
-	protected $st;
-	protected $types;
+    protected $db;
+    protected $st;
+    protected $types;
 
-	public function __construct($db, $st, array $bindings = null) {
-		$this->db = $db;
-		$this->st = $st;
-		$this->types = [];
-		foreach($bindings as $binding) {
-			switch(trim(strtolower($binding))) {
-				case "int":
-				case "integer":
-					$this->types[] = \SQLITE3_INTEGER; break;
-				case "float":
-				case "double":
-				case "real":
-				case "numeric":
-					$this->types[] = \SQLITE3_FLOAT; break;
-				case "date":
-				case "time":
-				case "datetime":
-				case "timestamp":
-					$this->types[] = \SQLITE3_TEXT; break;
-				case "blob":
-				case "bin":
-				case "binary":
-					$this->types[] = \SQLITE3_BLOB; break;
-				case "text":
-				case "string":
-				case "str":
-					$this->types[] = \SQLITE3_TEXT; break;
-				case "bool":
-				case "boolean":
-				case "bit":
-					$this->types[] = \SQLITE3_INTEGER; break;
-				default:
-					$this->types[] = \SQLITE3_TEXT; break;
-			}
-		}
-	}
+    public function __construct($db, $st, array $bindings = null) {
+        $this->db = $db;
+        $this->st = $st;
+        $this->types = [];
+        foreach($bindings as $binding) {
+            switch(trim(strtolower($binding))) {
+                case "int":
+                case "integer":
+                    $this->types[] = \SQLITE3_INTEGER; break;
+                case "float":
+                case "double":
+                case "real":
+                case "numeric":
+                    $this->types[] = \SQLITE3_FLOAT; break;
+                case "date":
+                case "time":
+                case "datetime":
+                case "timestamp":
+                    $this->types[] = \SQLITE3_TEXT; break;
+                case "blob":
+                case "bin":
+                case "binary":
+                    $this->types[] = \SQLITE3_BLOB; break;
+                case "text":
+                case "string":
+                case "str":
+                    $this->types[] = \SQLITE3_TEXT; break;
+                case "bool":
+                case "boolean":
+                case "bit":
+                    $this->types[] = \SQLITE3_INTEGER; break;
+                default:
+                    $this->types[] = \SQLITE3_TEXT; break;
+            }
+        }
+    }
 
-	public function __destruct() {
-		$this->st->close();
-		unset($this->st);
-	}
+    public function __destruct() {
+        $this->st->close();
+        unset($this->st);
+    }
 
-	public function __invoke(&...$values) {
-		return $this->runArray($values);
-	}
+    public function __invoke(&...$values) {
+        return $this->runArray($values);
+    }
 
-	public function run(&...$values): Result {
-		return $this->runArray($values);
-	}
+    public function run(&...$values): Result {
+        return $this->runArray($values);
+    }
 
-	public function runArray(array &$values = null): Result {
-		$this->st->clear();
-		$l = sizeof($values);
-		for($a = 0; $a < $l; $a++) {
-			if($values[$a]===null) {
-				$type = \SQLITE3_NULL;
-			} else {
-				$type = (array_key_exists($a,$this->types)) ? $this->types[$a] : \SQLITE3_TEXT;
-			}
-			$this->st->bindParam($a+1, $values[$a], $type);
-		}
-		return new ResultSQLite3($this->st->execute(), $this->db->changes(), $this);
-	}
+    public function runArray(array &$values = null): Result {
+        $this->st->clear();
+        $l = sizeof($values);
+        for($a = 0; $a < $l; $a++) {
+            if($values[$a]===null) {
+                $type = \SQLITE3_NULL;
+            } else {
+                $type = (array_key_exists($a,$this->types)) ? $this->types[$a] : \SQLITE3_TEXT;
+            }
+            $this->st->bindParam($a+1, $values[$a], $type);
+        }
+        return new ResultSQLite3($this->st->execute(), $this->db->changes(), $this);
+    }
 }
\ No newline at end of file
diff --git a/lib/ExceptionFatal.php b/lib/ExceptionFatal.php
index fcb7a961..af12d35f 100644
--- a/lib/ExceptionFatal.php
+++ b/lib/ExceptionFatal.php
@@ -3,7 +3,7 @@ declare(strict_types=1);
 namespace JKingWeb\NewsSync;
 
 class ExceptionFatal extends AbstractException {
-	public function __construct($msg = "", $code = 0, $e = null) {
-		\Exception::__construct($msg, $code, $e);
-	}
+    public function __construct($msg = "", $code = 0, $e = null) {
+        \Exception::__construct($msg, $code, $e);
+    }
 }
\ No newline at end of file
diff --git a/lib/Lang.php b/lib/Lang.php
index d5578660..f1572216 100644
--- a/lib/Lang.php
+++ b/lib/Lang.php
@@ -4,161 +4,161 @@ namespace JKingWeb\NewsSync;
 use \Webmozart\Glob\Glob;
 
 class Lang {
-	const DEFAULT = "en";
-	const REQUIRED = [
-		'Exception.JKingWeb/NewsSync/Exception.uncoded'                     => 'The specified exception symbol {0} has no code specified in Exception.php',
-		'Exception.JKingWeb/NewsSync/Exception.unknown'						=> 'An unknown error has occurred',
-		'Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing'		=> 'Default language file "{0}" missing',
-		'Exception.JKingWeb/NewsSync/Lang/Exception.fileMissing'			=> 'Language file "{0}" is not available',
-		'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.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})',
-	];
+    const DEFAULT = "en";
+    const REQUIRED = [
+        'Exception.JKingWeb/NewsSync/Exception.uncoded'                     => 'The specified exception symbol {0} has no code specified in Exception.php',
+        'Exception.JKingWeb/NewsSync/Exception.unknown'                     => 'An unknown error has occurred',
+        'Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing'     => 'Default language file "{0}" missing',
+        'Exception.JKingWeb/NewsSync/Lang/Exception.fileMissing'            => 'Language file "{0}" is not available',
+        '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.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 public    $path = BASE."locale".DIRECTORY_SEPARATOR;
-	static protected $requirementsMet = false;
-	static protected $synched = false;
-	static protected $wanted = self::DEFAULT;
-	static protected $locale = "";
-	static protected $loaded = [];
-	static protected $strings = self::REQUIRED;
+    static public    $path = BASE."locale".DIRECTORY_SEPARATOR;
+    static protected $requirementsMet = false;
+    static protected $synched = false;
+    static protected $wanted = self::DEFAULT;
+    static protected $locale = "";
+    static protected $loaded = [];
+    static protected $strings = self::REQUIRED;
 
-	protected function __construct() {}
+    protected function __construct() {}
 
-	static public function set(string $locale, bool $immediate = false): string {
-		if(!self::$requirementsMet) self::checkRequirements();
-		if($locale==self::$wanted) return $locale;
-		if($locale != "") {
-			$list = self::listFiles();
-			if(!in_array(self::DEFAULT, $list)) throw new Lang\Exception("defaultFileMissing", self::DEFAULT);
-			self::$wanted = self::match($locale, $list);
-		} else {
-			self::$wanted = "";
-		}
-		self::$synched = false;
-		if($immediate) self::load();
-		return self::$wanted;
-	}
+    static public function set(string $locale, bool $immediate = false): string {
+        if(!self::$requirementsMet) self::checkRequirements();
+        if($locale==self::$wanted) return $locale;
+        if($locale != "") {
+            $list = self::listFiles();
+            if(!in_array(self::DEFAULT, $list)) throw new Lang\Exception("defaultFileMissing", self::DEFAULT);
+            self::$wanted = self::match($locale, $list);
+        } else {
+            self::$wanted = "";
+        }
+        self::$synched = false;
+        if($immediate) self::load();
+        return self::$wanted;
+    }
 
-	static public function get(): string {
-		return (self::$locale=="") ? self::DEFAULT : self::$locale;
-	}
+    static public function get(): string {
+        return (self::$locale=="") ? self::DEFAULT : self::$locale;
+    }
 
-	static public function dump(): array {
-		return self::$strings;
-	}
+    static public function dump(): array {
+        return self::$strings;
+    }
 
-	static public function msg(string $msgID, $vars = null): string {
-		// if we're trying to load the system default language and it fails, we have a chicken and egg problem, so we catch the exception and load no language file instead
-		if(!self::$synched) try {self::load();} catch(Lang\Exception $e) {
-			if(self::$wanted==self::DEFAULT) {
-				self::set("", true);
-			} else {
-				throw $e;
-			} 
-		}
-		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
-		$msg = self::$strings[$msgID];
-		if($vars===null) {
-			return $msg;
-		} else if(!is_array($vars)) {
-			$vars = [$vars];
-		}
-		$msg = \MessageFormatter::formatMessage(self::$locale, $msg, $vars);
-		if($msg===false) throw new Lang\Exception("stringInvalid", ['msgID' => $msgID, 'fileList' => implode(", ",self::$loaded)]);
-		return $msg;
-	}
+    static public function msg(string $msgID, $vars = null): string {
+        // if we're trying to load the system default language and it fails, we have a chicken and egg problem, so we catch the exception and load no language file instead
+        if(!self::$synched) try {self::load();} catch(Lang\Exception $e) {
+            if(self::$wanted==self::DEFAULT) {
+                self::set("", true);
+            } else {
+                throw $e;
+            }
+        }
+        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
+        $msg = self::$strings[$msgID];
+        if($vars===null) {
+            return $msg;
+        } else if(!is_array($vars)) {
+            $vars = [$vars];
+        }
+        $msg = \MessageFormatter::formatMessage(self::$locale, $msg, $vars);
+        if($msg===false) throw new Lang\Exception("stringInvalid", ['msgID' => $msgID, 'fileList' => implode(", ",self::$loaded)]);
+        return $msg;
+    }
 
-	static public function list(string $locale = ""): array {
-		$out = [];
-		$files = self::listFiles();
-		foreach($files as $tag) {
-			$out[$tag] = \Locale::getDisplayName($tag, ($locale=="") ? $tag : $locale); 		
-		}
-		return $out;
-	}
+    static public function list(string $locale = ""): array {
+        $out = [];
+        $files = self::listFiles();
+        foreach($files as $tag) {
+            $out[$tag] = \Locale::getDisplayName($tag, ($locale=="") ? $tag : $locale);
+        }
+        return $out;
+    }
 
-	static public function match(string $locale, array $list = null): string {
-		if($list===null) $list = self::listFiles();
-		$default = (self::$locale=="") ? self::DEFAULT : self::$locale;
-		return \Locale::lookup($list,$locale, true, $default);
-	}
+    static public function match(string $locale, array $list = null): string {
+        if($list===null) $list = self::listFiles();
+        $default = (self::$locale=="") ? self::DEFAULT : self::$locale;
+        return \Locale::lookup($list,$locale, true, $default);
+    }
 
-	static protected function checkRequirements(): bool {
-		if(!extension_loaded("intl")) throw new ExceptionFatal("The \"Intl\" extension is required, but not loaded");
-		self::$requirementsMet = true;
-		return true;
-	}
-	
-	static protected function listFiles(): array {
-		$out = glob(self::$path."*.php");
-		// built-in glob doesn't work with vfsStream (and this other glob doesn't seem to work with Windows paths), so we try both
-		if(empty($out)) $out = Glob::glob(self::$path."*.php");
-		$out = array_map(function($file) {
-			$file = str_replace(DIRECTORY_SEPARATOR, "/", $file);
-			$file = substr($file, strrpos($file, "/")+1);
-			return strtolower(substr($file,0,strrpos($file,".")));
-		},$out);
-		natsort($out);
-		return $out;
-	}
+    static protected function checkRequirements(): bool {
+        if(!extension_loaded("intl")) throw new ExceptionFatal("The \"Intl\" extension is required, but not loaded");
+        self::$requirementsMet = true;
+        return true;
+    }
 
-	static protected function load(): bool {
-		if(!self::$requirementsMet) self::checkRequirements();
-		// if we've requested no locale (""), just load the fallback strings and return
-		if(self::$wanted=="") {
-			self::$strings = self::REQUIRED;
-			self::$locale = self::$wanted;
-			self::$synched = true;
-			return true;
-		}
-		// decompose the requested locale from specific to general, building a list of files to load
-		$tags = \Locale::parseLocale(self::$wanted);
-		$files = [];
-		while(sizeof($tags) > 0) {
-			$files[] = strtolower(\Locale::composeLocale($tags));
-			$tag = array_pop($tags);
-		}
-		// include the default locale as the base if the most general locale requested is not the default
-		if($tag != self::DEFAULT) $files[] = self::DEFAULT;
-		// save the list of files to be loaded for later reference
-		$loaded = $files;
-		// reduce the list of files to be loaded to the minimum necessary (e.g. if we go from "fr" to "fr_ca", we don't need to load "fr" or "en")
-		$files = [];
-		foreach($loaded as $file) {
-			if($file==self::$locale) break;
-			$files[] = $file;
-		}
-		// if we need to load all files, start with the fallback strings
-		$strings = [];
-		if($files==$loaded) {
-			$strings[] = self::REQUIRED;
-		} else {
-			// otherwise start with the strings we already have if we're going from e.g. "fr" to "fr_ca"
-			$strings[] = self::$strings;
-		}
-		// read files in reverse order
-		$files = array_reverse($files);
-		foreach($files as $file) {
-			if(!file_exists(self::$path."$file.php")) throw new Lang\Exception("fileMissing", $file);
-			if(!is_readable(self::$path."$file.php")) throw new Lang\Exception("fileUnreadable", $file);
-			try {
-				ob_start();
-				$arr = (include self::$path."$file.php");
-			} catch(\Throwable $e) {
-				$arr = null;
-			} finally {
-				ob_end_clean();
-			}
-			if(!is_array($arr)) throw new Lang\Exception("fileCorrupt", $file);
-			$strings[] = $arr;
-		}
-		// apply the results and return
-		self::$strings = call_user_func_array("array_replace_recursive", $strings);
-		self::$loaded = $loaded;
-		self::$locale = self::$wanted;
-		return true;
-	}
+    static protected function listFiles(): array {
+        $out = glob(self::$path."*.php");
+        // built-in glob doesn't work with vfsStream (and this other glob doesn't seem to work with Windows paths), so we try both
+        if(empty($out)) $out = Glob::glob(self::$path."*.php");
+        $out = array_map(function($file) {
+            $file = str_replace(DIRECTORY_SEPARATOR, "/", $file);
+            $file = substr($file, strrpos($file, "/")+1);
+            return strtolower(substr($file,0,strrpos($file,".")));
+        },$out);
+        natsort($out);
+        return $out;
+    }
+
+    static protected function load(): bool {
+        if(!self::$requirementsMet) self::checkRequirements();
+        // if we've requested no locale (""), just load the fallback strings and return
+        if(self::$wanted=="") {
+            self::$strings = self::REQUIRED;
+            self::$locale = self::$wanted;
+            self::$synched = true;
+            return true;
+        }
+        // decompose the requested locale from specific to general, building a list of files to load
+        $tags = \Locale::parseLocale(self::$wanted);
+        $files = [];
+        while(sizeof($tags) > 0) {
+            $files[] = strtolower(\Locale::composeLocale($tags));
+            $tag = array_pop($tags);
+        }
+        // include the default locale as the base if the most general locale requested is not the default
+        if($tag != self::DEFAULT) $files[] = self::DEFAULT;
+        // save the list of files to be loaded for later reference
+        $loaded = $files;
+        // reduce the list of files to be loaded to the minimum necessary (e.g. if we go from "fr" to "fr_ca", we don't need to load "fr" or "en")
+        $files = [];
+        foreach($loaded as $file) {
+            if($file==self::$locale) break;
+            $files[] = $file;
+        }
+        // if we need to load all files, start with the fallback strings
+        $strings = [];
+        if($files==$loaded) {
+            $strings[] = self::REQUIRED;
+        } else {
+            // otherwise start with the strings we already have if we're going from e.g. "fr" to "fr_ca"
+            $strings[] = self::$strings;
+        }
+        // read files in reverse order
+        $files = array_reverse($files);
+        foreach($files as $file) {
+            if(!file_exists(self::$path."$file.php")) throw new Lang\Exception("fileMissing", $file);
+            if(!is_readable(self::$path."$file.php")) throw new Lang\Exception("fileUnreadable", $file);
+            try {
+                ob_start();
+                $arr = (include self::$path."$file.php");
+            } catch(\Throwable $e) {
+                $arr = null;
+            } finally {
+                ob_end_clean();
+            }
+            if(!is_array($arr)) throw new Lang\Exception("fileCorrupt", $file);
+            $strings[] = $arr;
+        }
+        // apply the results and return
+        self::$strings = call_user_func_array("array_replace_recursive", $strings);
+        self::$loaded = $loaded;
+        self::$locale = self::$wanted;
+        return true;
+    }
 }
\ No newline at end of file
diff --git a/lib/Lang/Exception.php b/lib/Lang/Exception.php
index e280322c..8db223a6 100644
--- a/lib/Lang/Exception.php
+++ b/lib/Lang/Exception.php
@@ -3,22 +3,22 @@ declare(strict_types=1);
 namespace JKingWeb\NewsSync\Lang;
 
 class Exception extends \JKingWeb\NewsSync\AbstractException {
-	static $test = false; // used during PHPUnit testing only
+    static $test = false; // used during PHPUnit testing only
 
-	function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {
-		if(!self::$test) {
-			parent::__construct($msgID, $vars, $e);
-		} else {
-			$codeID = "Lang/Exception.$msgID";
-			if(!array_key_exists($codeID,self::CODES)) {
-				$code = -1;
-				$msg = "Exception.".str_replace("\\","/",parent::class).".uncoded";
-				$vars = $msgID;
-			} else {
-				$code = self::CODES[$codeID];
-				$msg = "Exception.".str_replace("\\","/",__CLASS__).".$msgID";
-			}
-			\Exception::__construct($msg, $code, $e);
-		}
-	}
+    function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {
+        if(!self::$test) {
+            parent::__construct($msgID, $vars, $e);
+        } else {
+            $codeID = "Lang/Exception.$msgID";
+            if(!array_key_exists($codeID,self::CODES)) {
+                $code = -1;
+                $msg = "Exception.".str_replace("\\","/",parent::class).".uncoded";
+                $vars = $msgID;
+            } else {
+                $code = self::CODES[$codeID];
+                $msg = "Exception.".str_replace("\\","/",__CLASS__).".$msgID";
+            }
+            \Exception::__construct($msg, $code, $e);
+        }
+    }
 }
\ No newline at end of file
diff --git a/lib/RuntimeData.php b/lib/RuntimeData.php
index 54ca06f7..42ac9b99 100644
--- a/lib/RuntimeData.php
+++ b/lib/RuntimeData.php
@@ -3,14 +3,14 @@ declare(strict_types=1);
 namespace JKingWeb\NewsSync;
 
 class RuntimeData {
-	public $conf;
-	public $db;
-	public $auth;
+    public $conf;
+    public $db;
+    public $auth;
 
-	public function __construct(Conf $conf) {
-		$this->conf = $conf;
-		Lang::set($conf->lang);
-		$this->db = new Database($this);
-		$this->user = new User($this);
-	}
+    public function __construct(Conf $conf) {
+        $this->conf = $conf;
+        Lang::set($conf->lang);
+        $this->db = new Database($this);
+        $this->user = new User($this);
+    }
 }
\ No newline at end of file
diff --git a/lib/User.php b/lib/User.php
index ad7cc369..87300121 100644
--- a/lib/User.php
+++ b/lib/User.php
@@ -3,241 +3,241 @@ declare(strict_types=1);
 namespace JKingWeb\NewsSync;
 
 class User {
-	public  $id = null;
+    public  $id = null;
 
-	protected $data;
-	protected $u;
-	protected $authz = true;
-	protected $existSupported = 0;
-	protected $authzSupported = 0;
-	
-	static public function listDrivers(): array {
-		$sep = \DIRECTORY_SEPARATOR;
-		$path = __DIR__.$sep."User".$sep;
-		$classes = [];
-		foreach(glob($path."Driver?*.php") as $file) {
-			$name = basename($file, ".php");
-			$name = NS_BASE."Db\\$name";
-			if(class_exists($name)) {
-				$classes[$name] = $name::driverName();
-			}			 
-		}
-		return $classes;
-	}
+    protected $data;
+    protected $u;
+    protected $authz = true;
+    protected $existSupported = 0;
+    protected $authzSupported = 0;
+    
+    static public function listDrivers(): array {
+        $sep = \DIRECTORY_SEPARATOR;
+        $path = __DIR__.$sep."User".$sep;
+        $classes = [];
+        foreach(glob($path."Driver?*.php") as $file) {
+            $name = basename($file, ".php");
+            $name = NS_BASE."Db\\$name";
+            if(class_exists($name)) {
+                $classes[$name] = $name::driverName();
+            }             
+        }
+        return $classes;
+    }
 
-	public function __construct(\JKingWeb\NewsSync\RuntimeData $data) {
-		$this->data = $data;
-		$driver = $data->conf->userDriver;
-		$this->u = $driver::create($data);
-		$this->existSupported = $this->u->driverFunctions("userExists");
-		$this->authzSupported = $this->u->driverFunctions("authorize");
-	}
+    public function __construct(\JKingWeb\NewsSync\RuntimeData $data) {
+        $this->data = $data;
+        $driver = $data->conf->userDriver;
+        $this->u = $driver::create($data);
+        $this->existSupported = $this->u->driverFunctions("userExists");
+        $this->authzSupported = $this->u->driverFunctions("authorize");
+    }
 
-	public function __toString() {
-		if($this->id===null) $this->credentials();
-		return (string) $this->id;
-	}
+    public function __toString() {
+        if($this->id===null) $this->credentials();
+        return (string) $this->id;
+    }
 
-	public function credentials(): array {
-		if($this->data->conf->userAuthPreferHTTP) {
-			return $this->credentialsHTTP();
-		} else {
-			return $this->credentialsForm();
-		}
-	}
+    public function credentials(): array {
+        if($this->data->conf->userAuthPreferHTTP) {
+            return $this->credentialsHTTP();
+        } else {
+            return $this->credentialsForm();
+        }
+    }
 
-	public function credentialsForm(): array {
-		// FIXME: stub
-		$this->id = "john.doe@example.com";
-		return ["user" => "john.doe@example.com", "password" => "secret"];
-	}
+    public function credentialsForm(): array {
+        // FIXME: stub
+        $this->id = "john.doe@example.com";
+        return ["user" => "john.doe@example.com", "password" => "secret"];
+    }
 
-	public function credentialsHTTP(): array {
-		if($_SERVER['PHP_AUTH_USER']) {
-			$out = ["user" => $_SERVER['PHP_AUTH_USER'], "password" => $_SERVER['PHP_AUTH_PW']];
-		} else if($_SERVER['REMOTE_USER']) {
-			$out = ["user" => $_SERVER['REMOTE_USER'], "password" => ""];
-		} else {
-			$out = ["user" => "", "password" => ""];
-		}
-		if($this->data->conf->userComposeNames && $out["user"] != "") {
-			$out["user"] = $this->composeName($out["user"]);
-		}
-		$this->id = $out["user"];
-		return $out;
-	}
+    public function credentialsHTTP(): array {
+        if($_SERVER['PHP_AUTH_USER']) {
+            $out = ["user" => $_SERVER['PHP_AUTH_USER'], "password" => $_SERVER['PHP_AUTH_PW']];
+        } else if($_SERVER['REMOTE_USER']) {
+            $out = ["user" => $_SERVER['REMOTE_USER'], "password" => ""];
+        } else {
+            $out = ["user" => "", "password" => ""];
+        }
+        if($this->data->conf->userComposeNames && $out["user"] != "") {
+            $out["user"] = $this->composeName($out["user"]);
+        }
+        $this->id = $out["user"];
+        return $out;
+    }
 
-	public function auth(string $user = null, string $password = null): bool {
-		if($user===null) {
-			if($this->data->conf->userAuthPreferHTTP) return $this->authHTTP();
-			return $this->authForm();
-		} else {
-			if($this->u->auth($user, $password)) {
-				$this->authPostProcess($user, $password);
-				return true;
-			}
-			return false;
-		}
-	}
+    public function auth(string $user = null, string $password = null): bool {
+        if($user===null) {
+            if($this->data->conf->userAuthPreferHTTP) return $this->authHTTP();
+            return $this->authForm();
+        } else {
+            if($this->u->auth($user, $password)) {
+                $this->authPostProcess($user, $password);
+                return true;
+            }
+            return false;
+        }
+    }
 
-	public function authForm(): bool {
-		$cred = $this->credentialsForm();
-		if(!$cred["user"]) return $this->challengeForm();
-		if(!$this->u->auth($cred["user"], $cred["password"])) return $this->challengeForm();
-		$this->authPostProcess($cred["user"], $cred["password"]);
-		return true;
-	}
+    public function authForm(): bool {
+        $cred = $this->credentialsForm();
+        if(!$cred["user"]) return $this->challengeForm();
+        if(!$this->u->auth($cred["user"], $cred["password"])) return $this->challengeForm();
+        $this->authPostProcess($cred["user"], $cred["password"]);
+        return true;
+    }
 
-	public function authHTTP(): bool {
-		$cred = $this->credentialsHTTP();
-		if(!$cred["user"]) return $this->challengeHTTP();
-		if(!$this->u->auth($cred["user"], $cred["password"])) return $this->challengeHTTP();
-		$this->authPostProcess($cred["user"], $cred["password"]);
-		return true;
-	}
+    public function authHTTP(): bool {
+        $cred = $this->credentialsHTTP();
+        if(!$cred["user"]) return $this->challengeHTTP();
+        if(!$this->u->auth($cred["user"], $cred["password"])) return $this->challengeHTTP();
+        $this->authPostProcess($cred["user"], $cred["password"]);
+        return true;
+    }
 
-	public function driverFunctions(string $function = null) {
-		return $this->u->driverFunctions($function);
-	}
-	
-	public function list(string $domain = null): array {
-		if($this->u->driverFunctions("userList")==User\Driver::FUNC_EXTERNAL) {
-			if($domain===null) {
-				if(!$this->data->user->authorize("@".$domain, "userList")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userList", "user" => $domain]);
-			} else {
-				if(!$this->data->user->authorize("", "userList")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userList", "user" => "all users"]);
-			}
-			return $this->u->userList($domain);
-		} else {
-			return $this->data->db->userList($domain);
-		}
-	}
+    public function driverFunctions(string $function = null) {
+        return $this->u->driverFunctions($function);
+    }
+    
+    public function list(string $domain = null): array {
+        if($this->u->driverFunctions("userList")==User\Driver::FUNC_EXTERNAL) {
+            if($domain===null) {
+                if(!$this->data->user->authorize("@".$domain, "userList")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userList", "user" => $domain]);
+            } else {
+                if(!$this->data->user->authorize("", "userList")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userList", "user" => "all users"]);
+            }
+            return $this->u->userList($domain);
+        } else {
+            return $this->data->db->userList($domain);
+        }
+    }
 
-	public function authorize(string $affectedUser, string $action, int $promoteLevel = 0): bool {
-		if(!$this->authz) return true;
-		if($this->id===null) $this->credentials();
-		if($this->authzSupported) return $this->u->authorize($affectedUser, $action, $promoteLevel);
-		// if the driver does not implement authorization, only allow operation for the current user (this means no new users can be added)
-		if($affectedUser==$this->id && $action != "userRightsSet") return true;
-		return false;		
-	}
+    public function authorize(string $affectedUser, string $action, int $promoteLevel = 0): bool {
+        if(!$this->authz) return true;
+        if($this->id===null) $this->credentials();
+        if($this->authzSupported) return $this->u->authorize($affectedUser, $action, $promoteLevel);
+        // if the driver does not implement authorization, only allow operation for the current user (this means no new users can be added)
+        if($affectedUser==$this->id && $action != "userRightsSet") return true;
+        return false;        
+    }
 
-	public function authorizationEnabled(bool $setting = null): bool {
-		if($setting===null) return $this->authz;
-		$this->authz = $setting;
-		return $setting;
-	}
-	
-	public function exists(string $user): bool {
-		if($this->u->driverFunctions("userExists") != User\Driver::FUNC_INTERNAL) {
-			if(!$this->data->user->authorize($user, "userExists")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userExists", "user" => $user]);
-		}
-		if(!$this->existSupported) return true;
-		$out = $this->u->userExists($user);
-		if($out && $this->existSupported==User\Driver::FUNC_EXTERNAL && !$this->data->db->userExist($user)) {
-			try {$this->data->db->userAdd($user);} catch(\Throwable $e) {}
-		}
-		return $out;
-	}
+    public function authorizationEnabled(bool $setting = null): bool {
+        if($setting===null) return $this->authz;
+        $this->authz = $setting;
+        return $setting;
+    }
+    
+    public function exists(string $user): bool {
+        if($this->u->driverFunctions("userExists") != User\Driver::FUNC_INTERNAL) {
+            if(!$this->data->user->authorize($user, "userExists")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userExists", "user" => $user]);
+        }
+        if(!$this->existSupported) return true;
+        $out = $this->u->userExists($user);
+        if($out && $this->existSupported==User\Driver::FUNC_EXTERNAL && !$this->data->db->userExist($user)) {
+            try {$this->data->db->userAdd($user);} catch(\Throwable $e) {}
+        }
+        return $out;
+    }
 
-	public function add($user, $password = null): bool {
-		if($this->u->driverFunctions("userAdd") != User\Driver::FUNC_INTERNAL) {
-			if(!$this->data->user->authorize($user, "userAdd")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userAdd", "user" => $user]);
-		}
-		if($this->exists($user)) return false;
-		$out = $this->u->userAdd($user, $password);
-		if($out && $this->u->driverFunctions("userAdd") != User\Driver::FUNC_INTERNAL) {
-			try {
-				if(!$this->data->db->userExists($user)) $this->data->db->userAdd($user, $password);
-			} catch(\Throwable $e) {}
-		}
-		return $out;
-	}
+    public function add($user, $password = null): bool {
+        if($this->u->driverFunctions("userAdd") != User\Driver::FUNC_INTERNAL) {
+            if(!$this->data->user->authorize($user, "userAdd")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userAdd", "user" => $user]);
+        }
+        if($this->exists($user)) return false;
+        $out = $this->u->userAdd($user, $password);
+        if($out && $this->u->driverFunctions("userAdd") != User\Driver::FUNC_INTERNAL) {
+            try {
+                if(!$this->data->db->userExists($user)) $this->data->db->userAdd($user, $password);
+            } catch(\Throwable $e) {}
+        }
+        return $out;
+    }
 
-	public function remove(string $user): bool {
-		if($this->u->driverFunctions("userRemove") != User\Driver::FUNC_INTERNAL) {
-			if(!$this->data->user->authorize($user, "userRemove")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userRemove", "user" => $user]);
-		}
-		if(!$this->exists($user)) return false;
-		$out = $this->u->userRemove($user);
-		if($out && $this->u->driverFunctions("userRemove") != User\Driver::FUNC_INTERNAL) {
-			try {
-				if($this->data->db->userExists($user)) $this->data->db->userRemove($user);
-			} catch(\Throwable $e) {}
-		}
-		return $out;
-	}
+    public function remove(string $user): bool {
+        if($this->u->driverFunctions("userRemove") != User\Driver::FUNC_INTERNAL) {
+            if(!$this->data->user->authorize($user, "userRemove")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userRemove", "user" => $user]);
+        }
+        if(!$this->exists($user)) return false;
+        $out = $this->u->userRemove($user);
+        if($out && $this->u->driverFunctions("userRemove") != User\Driver::FUNC_INTERNAL) {
+            try {
+                if($this->data->db->userExists($user)) $this->data->db->userRemove($user);
+            } catch(\Throwable $e) {}
+        }
+        return $out;
+    }
 
-	public function passwordSet(string $user, string $password): bool {
-		if($this->u->driverFunctions("userPasswordSet") != User\Driver::FUNC_INTERNAL) {
-			if(!$this->data->user->authorize($user, "userPasswordSet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userPasswordSet", "user" => $user]);
-		}
-		if(!$this->exists($user)) return false;
-		return $this->u->userPasswordSet($user, $password);
-	}
+    public function passwordSet(string $user, string $password): bool {
+        if($this->u->driverFunctions("userPasswordSet") != User\Driver::FUNC_INTERNAL) {
+            if(!$this->data->user->authorize($user, "userPasswordSet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userPasswordSet", "user" => $user]);
+        }
+        if(!$this->exists($user)) return false;
+        return $this->u->userPasswordSet($user, $password);
+    }
 
-	public function propertiesGet(string $user): array {
-		if($this->u->driverFunctions("userPropertiesGet") != User\Driver::FUNC_INTERNAL) {
-			if(!$this->data->user->authorize($user, "userPropertiesGet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userPropertiesGet", "user" => $user]);
-		}
-		if(!$this->exists($user)) return false;
-		$domain = null;
-		if($this->data->conf->userComposeNames) $domain = substr($user,strrpos($user,"@")+1);
-		$init = [
-			"id"     => $user,
-			"name"   => $user,
-			"rights" => User\Driver::RIGHTS_NONE,
-			"domain" => $domain
-		];
-		if($this->u->driverFunctions("userPropertiesGet") != User\Driver::FUNC_NOT_IMPLEMENTED) {
-			return array_merge($init, $this->u->userPropertiesGet($user));
-		}
-		return $init;
-	}
+    public function propertiesGet(string $user): array {
+        if($this->u->driverFunctions("userPropertiesGet") != User\Driver::FUNC_INTERNAL) {
+            if(!$this->data->user->authorize($user, "userPropertiesGet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userPropertiesGet", "user" => $user]);
+        }
+        if(!$this->exists($user)) return false;
+        $domain = null;
+        if($this->data->conf->userComposeNames) $domain = substr($user,strrpos($user,"@")+1);
+        $init = [
+            "id"     => $user,
+            "name"   => $user,
+            "rights" => User\Driver::RIGHTS_NONE,
+            "domain" => $domain
+        ];
+        if($this->u->driverFunctions("userPropertiesGet") != User\Driver::FUNC_NOT_IMPLEMENTED) {
+            return array_merge($init, $this->u->userPropertiesGet($user));
+        }
+        return $init;
+    }
 
-	public function propertiesSet(string $user, array $properties): array {
-		if($this->u->driverFunctions("userPropertiesSet") != User\Driver::FUNC_INTERNAL) {
-			if(!$this->data->user->authorize($user, "userPropertiesSet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userPropertiesSet", "user" => $user]);
-		}
-		if(!$this->exists($user)) throw new User\Exception("doesNotExist", ["user" => $user, "action" => "userPropertiesSet"]);
-		return $this->u->userPropertiesSet($user, $properties);
-	}
+    public function propertiesSet(string $user, array $properties): array {
+        if($this->u->driverFunctions("userPropertiesSet") != User\Driver::FUNC_INTERNAL) {
+            if(!$this->data->user->authorize($user, "userPropertiesSet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userPropertiesSet", "user" => $user]);
+        }
+        if(!$this->exists($user)) throw new User\Exception("doesNotExist", ["user" => $user, "action" => "userPropertiesSet"]);
+        return $this->u->userPropertiesSet($user, $properties);
+    }
 
-	public function rightsGet(string $user): int {
-		if($this->u->driverFunctions("userRightsGet") != User\Driver::FUNC_INTERNAL) {
-			if(!$this->data->user->authorize($user, "userRightsGet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userRightsGet", "user" => $user]);
-		}
-		// we do not throw an exception here if the user does not exist, because it makes no material difference
-		if(!$this->exists($user)) return User\Driver::RIGHTS_NONE;
-		return $this->u->userRightsGet($user);
-	}
-	
-	public function rightsSet(string $user, int $level): bool {
-		if($this->u->driverFunctions("userRightsSet") != User\Driver::FUNC_INTERNAL) {
-			if(!$this->data->user->authorize($user, "userRightsSet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userRightsSet", "user" => $user]);
-		}
-		if(!$this->exists($user)) return false;
-		return $this->u->userRightsSet($user, $level);
-	}
-	
-	// FIXME: stubs
-	public function challenge(): bool     {throw new User\Exception("authFailed");}
-	public function challengeForm(): bool {throw new User\Exception("authFailed");}
-	public function challengeHTTP(): bool {throw new User\Exception("authFailed");}
+    public function rightsGet(string $user): int {
+        if($this->u->driverFunctions("userRightsGet") != User\Driver::FUNC_INTERNAL) {
+            if(!$this->data->user->authorize($user, "userRightsGet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userRightsGet", "user" => $user]);
+        }
+        // we do not throw an exception here if the user does not exist, because it makes no material difference
+        if(!$this->exists($user)) return User\Driver::RIGHTS_NONE;
+        return $this->u->userRightsGet($user);
+    }
+    
+    public function rightsSet(string $user, int $level): bool {
+        if($this->u->driverFunctions("userRightsSet") != User\Driver::FUNC_INTERNAL) {
+            if(!$this->data->user->authorize($user, "userRightsSet")) throw new User\ExceptionAuthz("notAuthorized", ["action" => "userRightsSet", "user" => $user]);
+        }
+        if(!$this->exists($user)) return false;
+        return $this->u->userRightsSet($user, $level);
+    }
+    
+    // FIXME: stubs
+    public function challenge(): bool     {throw new User\Exception("authFailed");}
+    public function challengeForm(): bool {throw new User\Exception("authFailed");}
+    public function challengeHTTP(): bool {throw new User\Exception("authFailed");}
 
-	protected function composeName(string $user): string {
-		if(preg_match("/.+?@[^@]+$/",$user)) {
-			return $user;
-		} else {
-			return $user."@".$_SERVER['HTTP_HOST'];
-		}
-	}
+    protected function composeName(string $user): string {
+        if(preg_match("/.+?@[^@]+$/",$user)) {
+            return $user;
+        } else {
+            return $user."@".$_SERVER['HTTP_HOST'];
+        }
+    }
 
-	protected function authPostprocess(string $user, string $password): bool {
-		if($this->u->driverFunctions("auth") != User\Driver::FUNC_INTERNAL && !$this->data->db->userExists($user)) {
-			if($password=="") $password = null;
-			try {$this->data->db->userAdd($user, $password);} catch(\Throwable $e) {}
-		}
-		return true;
-	}
+    protected function authPostprocess(string $user, string $password): bool {
+        if($this->u->driverFunctions("auth") != User\Driver::FUNC_INTERNAL && !$this->data->db->userExists($user)) {
+            if($password=="") $password = null;
+            try {$this->data->db->userAdd($user, $password);} catch(\Throwable $e) {}
+        }
+        return true;
+    }
 }
\ No newline at end of file
diff --git a/lib/User/Driver.php b/lib/User/Driver.php
index 245a395a..eed2e25c 100644
--- a/lib/User/Driver.php
+++ b/lib/User/Driver.php
@@ -3,28 +3,28 @@ declare(strict_types=1);
 namespace JKingWeb\NewsSync\User;
 
 Interface Driver {
-	const FUNC_NOT_IMPLEMENTED = 0;
-	const FUNC_INTERNAL = 1;
-	const FUNC_EXTERNAL = 2;
+    const FUNC_NOT_IMPLEMENTED = 0;
+    const FUNC_INTERNAL = 1;
+    const FUNC_EXTERNAL = 2;
 
-	const RIGHTS_NONE           = 0;
-	const RIGHTS_DOMAIN_MANAGER = 25;
-	const RIGHTS_DOMAIN_ADMIN   = 50;
-	const RIGHTS_GLOBAL_MANAGER = 75;
-	const RIGHTS_GLOBAL_ADMIN   = 100;
+    const RIGHTS_NONE           = 0;
+    const RIGHTS_DOMAIN_MANAGER = 25;
+    const RIGHTS_DOMAIN_ADMIN   = 50;
+    const RIGHTS_GLOBAL_MANAGER = 75;
+    const RIGHTS_GLOBAL_ADMIN   = 100;
 
-	static function create(\JKingWeb\NewsSync\RuntimeData $data): Driver;
-	static function driverName(): string;
-	function driverFunctions(string $function = null);
-	function auth(string $user, string $password): bool;
-	function authorize(string $affectedUser, string $action): bool;
-	function userExists(string $user): bool;
-	function userAdd(string $user, string $password = null): bool;
-	function userRemove(string $user): bool;
-	function userList(string $domain = null): array;
-	function userPasswordSet(string $user, string $newPassword, string $oldPassword): bool;
-	function userPropertiesGet(string $user): array;
-	function userPropertiesSet(string $user, array $properties): array;
-	function userRightsGet(string $user): int;
-	function userRightsSet(string $user, int $level): bool;
+    static function create(\JKingWeb\NewsSync\RuntimeData $data): Driver;
+    static function driverName(): string;
+    function driverFunctions(string $function = null);
+    function auth(string $user, string $password): bool;
+    function authorize(string $affectedUser, string $action): bool;
+    function userExists(string $user): bool;
+    function userAdd(string $user, string $password = null): bool;
+    function userRemove(string $user): bool;
+    function userList(string $domain = null): array;
+    function userPasswordSet(string $user, string $newPassword, string $oldPassword): bool;
+    function userPropertiesGet(string $user): array;
+    function userPropertiesSet(string $user, array $properties): array;
+    function userRightsGet(string $user): int;
+    function userRightsSet(string $user, int $level): bool;
 }
\ No newline at end of file
diff --git a/lib/User/DriverInternal.php b/lib/User/DriverInternal.php
index fbbe22e7..7e85cf36 100644
--- a/lib/User/DriverInternal.php
+++ b/lib/User/DriverInternal.php
@@ -3,43 +3,43 @@ declare(strict_types=1);
 namespace JKingWeb\NewsSync\User;
 
 class DriverInternal implements Driver {
-	use InternalFunctions;
+    use InternalFunctions;
 
-	protected $data;
-	protected $db;
-	protected $functions = [
-		"auth"					=> Driver::FUNC_INTERNAL,
-		"authorize"				=> Driver::FUNC_INTERNAL,
-		"userList"				=> Driver::FUNC_INTERNAL,
-		"userExists"			=> Driver::FUNC_INTERNAL,
-		"userAdd"				=> Driver::FUNC_INTERNAL,
-		"userRemove"			=> Driver::FUNC_INTERNAL,
-		"userPasswordSet"		=> Driver::FUNC_INTERNAL,
-		"userPropertiesGet"		=> Driver::FUNC_INTERNAL,
-		"userPropertiesSet"		=> Driver::FUNC_INTERNAL,
-		"userRightsGet"			=> Driver::FUNC_INTERNAL,
-		"userRightsSet"			=> Driver::FUNC_INTERNAL,
-	];
+    protected $data;
+    protected $db;
+    protected $functions = [
+        "auth"                    => Driver::FUNC_INTERNAL,
+        "authorize"               => Driver::FUNC_INTERNAL,
+        "userList"                => Driver::FUNC_INTERNAL,
+        "userExists"              => Driver::FUNC_INTERNAL,
+        "userAdd"                 => Driver::FUNC_INTERNAL,
+        "userRemove"              => Driver::FUNC_INTERNAL,
+        "userPasswordSet"         => Driver::FUNC_INTERNAL,
+        "userPropertiesGet"       => Driver::FUNC_INTERNAL,
+        "userPropertiesSet"       => Driver::FUNC_INTERNAL,
+        "userRightsGet"           => Driver::FUNC_INTERNAL,
+        "userRightsSet"           => Driver::FUNC_INTERNAL,
+    ];
 
-	static public function create(\JKingWeb\NewsSync\RuntimeData $data): Driver {
-		return new static($data);
-	}
+    static public function create(\JKingWeb\NewsSync\RuntimeData $data): Driver {
+        return new static($data);
+    }
 
-	public function __construct(\JKingWeb\NewsSync\RuntimeData $data) {
-		$this->data = $data;
-		$this->db = $this->data->db;
-	}
+    public function __construct(\JKingWeb\NewsSync\RuntimeData $data) {
+        $this->data = $data;
+        $this->db = $this->data->db;
+    }
 
-	static public function driverName(): string {
-		return "Internal";
-	}
+    static public function driverName(): string {
+        return "Internal";
+    }
 
-	public function driverFunctions(string $function = null) {
-		if($function===null) return $this->functions;
-		if(array_key_exists($function, $this->functions)) {
-			return $this->functions[$function];
-		} else {
-			return Driver::FUNC_NOT_IMPLEMENTED;
-		}
-	}
+    public function driverFunctions(string $function = null) {
+        if($function===null) return $this->functions;
+        if(array_key_exists($function, $this->functions)) {
+            return $this->functions[$function];
+        } else {
+            return Driver::FUNC_NOT_IMPLEMENTED;
+        }
+    }
 }
\ No newline at end of file
diff --git a/lib/User/InternalFunctions.php b/lib/User/InternalFunctions.php
index 5912b771..43fc285d 100644
--- a/lib/User/InternalFunctions.php
+++ b/lib/User/InternalFunctions.php
@@ -2,78 +2,78 @@
 declare(strict_types=1);
 namespace JKingWeb\NewsSync\User;
 
-trait InternalFunctions {	
-	protected $actor = [];
-	
-	function auth(string $user, string $password): bool {
-		if(!$this->data->user->exists($user)) return false;
-		$hash = $this->db->userPasswordGet($user);
-		if(!$hash) return false;
-		return password_verify($password, $hash);
-	}
+trait InternalFunctions {    
+    protected $actor = [];
+    
+    function auth(string $user, string $password): bool {
+        if(!$this->data->user->exists($user)) return false;
+        $hash = $this->db->userPasswordGet($user);
+        if(!$hash) return false;
+        return password_verify($password, $hash);
+    }
 
-	function authorize(string $affectedUser, string $action, int $newRightsLevel = 0): bool {
-		// if the affected user is the actor and the actor is not trying to grant themselves rights, accept the request
-		if($affectedUser==$this->data->user->id && $action != "userRightsSet") return true;
-		// get properties of actor if not already available
-		if(!sizeof($this->actor)) $this->actor = $this->data->user->propertiesGet($this->data->user->id);
-		$rights =& $this->actor["rights"];
-		// if actor is a global admin, accept the request
-		if($rights==self::RIGHTS_GLOBAL_ADMIN) return true;
-		// if actor is a common user, deny the request
-		if($rights==self::RIGHTS_NONE) return false;
-		// if actor is not some other sort of admin, deny the request
-		if(!in_array($rights,[self::RIGHTS_GLOBAL_MANAGER,self::RIGHTS_DOMAIN_MANAGER,self::RIGHTS_DOMAIN_ADMIN],true)) return false;
-		// if actor is a domain admin/manager and domains don't match, deny the request
-		if($this->data->conf->userComposeNames && $this->actor["domain"] && $rights != self::RIGHTS_GLOBAL_MANAGER) {
-			$test = "@".$this->actor["domain"];
-			if(substr($affectedUser,-1*strlen($test)) != $test) return false;
-		}
-		// certain actions shouldn't check affected user's rights
-		if(in_array($action, ["userRightsGet","userExists","userList"], true)) return true;
-		if($action=="userRightsSet") {
-			// setting rights above your own (or equal to your own, for managers) is not allowed
-			if($newRightsLevel > $rights || ($rights != self::RIGHTS_DOMAIN_ADMIN && $newRightsLevel==$rights)) return false;
-		}
-		$affectedRights = $this->data->user->rightsGet($affectedUser);
-		// acting for users with rights greater than your own (or equal, for managers) is not allowed
-		if($affectedRights > $rights || ($rights != self::RIGHTS_DOMAIN_ADMIN && $affectedRights==$rights)) return false;
-		return true;
-	}
+    function authorize(string $affectedUser, string $action, int $newRightsLevel = 0): bool {
+        // if the affected user is the actor and the actor is not trying to grant themselves rights, accept the request
+        if($affectedUser==$this->data->user->id && $action != "userRightsSet") return true;
+        // get properties of actor if not already available
+        if(!sizeof($this->actor)) $this->actor = $this->data->user->propertiesGet($this->data->user->id);
+        $rights =& $this->actor["rights"];
+        // if actor is a global admin, accept the request
+        if($rights==self::RIGHTS_GLOBAL_ADMIN) return true;
+        // if actor is a common user, deny the request
+        if($rights==self::RIGHTS_NONE) return false;
+        // if actor is not some other sort of admin, deny the request
+        if(!in_array($rights,[self::RIGHTS_GLOBAL_MANAGER,self::RIGHTS_DOMAIN_MANAGER,self::RIGHTS_DOMAIN_ADMIN],true)) return false;
+        // if actor is a domain admin/manager and domains don't match, deny the request
+        if($this->data->conf->userComposeNames && $this->actor["domain"] && $rights != self::RIGHTS_GLOBAL_MANAGER) {
+            $test = "@".$this->actor["domain"];
+            if(substr($affectedUser,-1*strlen($test)) != $test) return false;
+        }
+        // certain actions shouldn't check affected user's rights
+        if(in_array($action, ["userRightsGet","userExists","userList"], true)) return true;
+        if($action=="userRightsSet") {
+            // setting rights above your own (or equal to your own, for managers) is not allowed
+            if($newRightsLevel > $rights || ($rights != self::RIGHTS_DOMAIN_ADMIN && $newRightsLevel==$rights)) return false;
+        }
+        $affectedRights = $this->data->user->rightsGet($affectedUser);
+        // acting for users with rights greater than your own (or equal, for managers) is not allowed
+        if($affectedRights > $rights || ($rights != self::RIGHTS_DOMAIN_ADMIN && $affectedRights==$rights)) return false;
+        return true;
+    }
 
-	function userExists(string $user): bool {
-		return $this->db->userExists($user);
-	}
+    function userExists(string $user): bool {
+        return $this->db->userExists($user);
+    }
 
-	function userAdd(string $user, string $password = null): bool {
-		return $this->db->userAdd($user, $password);
-	}
+    function userAdd(string $user, string $password = null): bool {
+        return $this->db->userAdd($user, $password);
+    }
 
-	function userRemove(string $user): bool {
-		return $this->db->userRemove($user);
-	}
+    function userRemove(string $user): bool {
+        return $this->db->userRemove($user);
+    }
 
-	function userList(string $domain = null): array {
-		return $this->db->userList($domain);
-	}
-	
-	function userPasswordSet(string $user, string $newPassword, string $oldPassword): bool {
-		return $this->db->userPasswordSet($user, $newPassword);
-	}
+    function userList(string $domain = null): array {
+        return $this->db->userList($domain);
+    }
+    
+    function userPasswordSet(string $user, string $newPassword, string $oldPassword): bool {
+        return $this->db->userPasswordSet($user, $newPassword);
+    }
 
-	function userPropertiesGet(string $user): array {
-		return $this->db->userPropertiesGet($user);
-	}
+    function userPropertiesGet(string $user): array {
+        return $this->db->userPropertiesGet($user);
+    }
 
-	function userPropertiesSet(string $user, array $properties): array {
-		return $this->db->userPropertiesSet($user, $properties);
-	}
+    function userPropertiesSet(string $user, array $properties): array {
+        return $this->db->userPropertiesSet($user, $properties);
+    }
 
-	function userRightsGet(string $user): int {
-		return $this->db->userRightsGet($user);
-	}
-	
-	function userRightsSet(string $user, int $level): bool {
-		return $this->db->userRightsSet($user, $level);
-	} 
+    function userRightsGet(string $user): int {
+        return $this->db->userRightsGet($user);
+    }
+    
+    function userRightsSet(string $user, int $level): bool {
+        return $this->db->userRightsSet($user, $level);
+    } 
 }
\ No newline at end of file
diff --git a/locale/en.php b/locale/en.php
index 163392f7..e6eb3b7a 100644
--- a/locale/en.php
+++ b/locale/en.php
@@ -1,52 +1,52 @@
 <?php
 return [
-	'Exception.JKingWeb/NewsSync/Exception.uncoded'						=> 'The specified exception symbol {0} has no code specified in Exception.php',
-	//this should not usually be encountered
-	'Exception.JKingWeb/NewsSync/Exception.unknown'						=> 'An unknown error has occurred',
+    'Exception.JKingWeb/NewsSync/Exception.uncoded'                     => 'The specified exception symbol {0} has no code specified in Exception.php',
+    //this should not usually be encountered
+    'Exception.JKingWeb/NewsSync/Exception.unknown'                     => 'An unknown error has occurred',
 
-	'Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing'		=> 'Default language file "{0}" missing',
-	'Exception.JKingWeb/NewsSync/Lang/Exception.fileMissing'			=> 'Language file "{0}" is not available',
-	'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.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/Lang/Exception.defaultFileMissing'     => 'Default language file "{0}" missing',
+    'Exception.JKingWeb/NewsSync/Lang/Exception.fileMissing'            => 'Language file "{0}" is not available',
+    '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.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.fileUnreadable'			=> 'Insufficient permissions to read configuration file "{0}"',
-	'Exception.JKingWeb/NewsSync/Conf/Exception.fileUncreatable'		=> 'Insufficient permissions to write new configuration file "{0}"',
-	'Exception.JKingWeb/NewsSync/Conf/Exception.fileUnwritable'			=> 'Insufficient permissions to overwrite configuration file "{0}"',
-	'Exception.JKingWeb/NewsSync/Conf/Exception.fileCorrupt'			=> 'Configuration file "{0}" is corrupt or does not conform to expected format',
-	
-	'Exception.JKingWeb/NewsSync/Db/Exception.extMissing'				=> 'Required PHP extension for driver "{0}" not installed',
-	'Exception.JKingWeb/NewsSync/Db/Exception.fileMissing'				=> 'Database file "{0}" does not exist',
-	'Exception.JKingWeb/NewsSync/Db/Exception.fileUnreadable'			=> 'Insufficient permissions to open database file "{0}" for reading',
-	'Exception.JKingWeb/NewsSync/Db/Exception.fileUnwritable'			=> 'Insufficient permissions to open database file "{0}" for 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.fileCorrupt'				=> 'Database file "{0}" is corrupt or not a valid database',
-	
-	'Exception.JKingWeb/NewsSync/Db/Update/Exception.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 {current} to version {target}}
-		}',
-	'Exception.JKingWeb/NewsSync/Db/Update/Exception.manualOnly'		=> 
-		'{from_version, select, 
-			0 {{driver_name} database must be updated manually and is not initialized; please populate the database with the base schema}	
-			other {{driver_name} database must be updated manually; please update from schema version {current} to version {target}}
-		}',
-	'Exception.JKingWeb/NewsSync/Db/Update/Exception.fileMissing'		=> 'Automatic updating of the {driver_name} database failed due to instructions for updating from version {current} not being available',
-	'Exception.JKingWeb/NewsSync/Db/Update/Exception.fileUnreadable'	=> 'Automatic updating of the {driver_name} database failed due to insufficient permissions to read instructions for updating from version {current}',
-	'Exception.JKingWeb/NewsSync/Db/Update/Exception.fileUnusable'		=> 'Automatic updating of the {driver_name} database failed due to an error reading instructions for updating from version {current}',
-	'Exception.JKingWeb/NewsSync/Db/Update/Exception.tooNew'			=> 
-		'{difference, select,
-			0 {Automatic updating of the {driver_name} database failed because it is already up to date with the requested version, {target}}
-			other {Automatic updating of the {driver_name} database failed because its version, {current}, is newer than the requested version, {target}}
-		}',
+    '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.fileUncreatable'        => 'Insufficient permissions to write new configuration file "{0}"',
+    'Exception.JKingWeb/NewsSync/Conf/Exception.fileUnwritable'         => 'Insufficient permissions to overwrite configuration file "{0}"',
+    'Exception.JKingWeb/NewsSync/Conf/Exception.fileCorrupt'            => 'Configuration file "{0}" is corrupt or does not conform to expected format',
 
-	'Exception.JKingWeb/NewsSync/User/Exception.alreadyExists'			=> 'Could not perform action "{action}" because the user {user} already exists',
-	'Exception.JKingWeb/NewsSync/User/Exception.doesNotExist'			=> 'Could not perform action "{action}" because the user {user} does not exist',
-	'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/Db/Exception.extMissing'               => 'Required PHP extension for driver "{0}" not installed',
+    'Exception.JKingWeb/NewsSync/Db/Exception.fileMissing'              => 'Database file "{0}" does not exist',
+    'Exception.JKingWeb/NewsSync/Db/Exception.fileUnreadable'           => 'Insufficient permissions to open database file "{0}" for reading',
+    'Exception.JKingWeb/NewsSync/Db/Exception.fileUnwritable'           => 'Insufficient permissions to open database file "{0}" for 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.fileCorrupt'              => 'Database file "{0}" is corrupt or not a valid database',
+
+    'Exception.JKingWeb/NewsSync/Db/Update/Exception.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 {current} to version {target}}
+        }',
+    'Exception.JKingWeb/NewsSync/Db/Update/Exception.manualOnly'        =>
+        '{from_version, select,
+            0 {{driver_name} database must be updated manually and is not initialized; please populate the database with the base schema}
+            other {{driver_name} database must be updated manually; please update from schema version {current} to version {target}}
+        }',
+    'Exception.JKingWeb/NewsSync/Db/Update/Exception.fileMissing'       => 'Automatic updating of the {driver_name} database failed due to instructions for updating from version {current} not being available',
+    'Exception.JKingWeb/NewsSync/Db/Update/Exception.fileUnreadable'    => 'Automatic updating of the {driver_name} database failed due to insufficient permissions to read instructions for updating from version {current}',
+    'Exception.JKingWeb/NewsSync/Db/Update/Exception.fileUnusable'      => 'Automatic updating of the {driver_name} database failed due to an error reading instructions for updating from version {current}',
+    'Exception.JKingWeb/NewsSync/Db/Update/Exception.tooNew'            =>
+        '{difference, select,
+            0 {Automatic updating of the {driver_name} database failed because it is already up to date with the requested version, {target}}
+            other {Automatic updating of the {driver_name} database failed because its version, {current}, is newer than the requested version, {target}}
+        }',
+
+    'Exception.JKingWeb/NewsSync/User/Exception.alreadyExists'          => 'Could not perform action "{action}" because the user {user} already exists',
+    'Exception.JKingWeb/NewsSync/User/Exception.doesNotExist'           => 'Could not perform action "{action}" because the user {user} does not exist',
+    '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}',
 ];
\ No newline at end of file
diff --git a/sql/SQLite3/0.sql b/sql/SQLite3/0.sql
index 96117923..59c38bb1 100644
--- a/sql/SQLite3/0.sql
+++ b/sql/SQLite3/0.sql
@@ -1,110 +1,110 @@
 -- newsfeeds, deduplicated
 create table 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,																						-- time at which the feed last actually changed
-	etag TEXT,																								-- HTTP ETag hash used for cache validation, changes each time the content changes
-	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 not null default '',																		-- HTTP authentication username
-	password TEXT not null default '',																		-- HTTP authentication password (this is stored in plain text)
-	unique(url,username,password)																			-- a URL with particular credentials should only appear once
+    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,                                                                                      -- time at which the feed last actually changed
+    etag TEXT,                                                                                              -- HTTP ETag hash used for cache validation, changes each time the content changes
+    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 not null default '',                                                                      -- HTTP authentication username
+    password TEXT not null default '',                                                                      -- 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 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
+    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 newssync_enclosures(
-	article integer not null references newssync_articles(id) on delete cascade,
-	url TEXT,
-	type varchar(255)
+    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 newssync_tags(
-	article integer not null references newssync_articles(id) on delete cascade,
-	name TEXT
+    article integer not null references newssync_articles(id) on delete cascade,
+    name TEXT
 );
 
 -- settings
 create table 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','null','json')
-	) default 'text'																						--
+    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','null','json')
+    ) default 'text'                                                                                        --
 );
 
 -- users
 create table 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_url TEXT,																						-- external URL to avatar
-	avatar_type TEXT,																						-- internal avatar image's MIME content type
-	avatar_data BLOB,																						-- internal avatar image's binary data
-	rights integer not null default 0 																		-- any administrative rights the user may have
+    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_url TEXT,                                                                                        -- external URL to avatar
+    avatar_type TEXT,                                                                                       -- internal avatar image's MIME content type
+    avatar_data BLOB,                                                                                       -- internal avatar image's binary data
+    rights integer not null default 0                                                                       -- any administrative rights the user may have
 );
 
 -- TT-RSS categories and ownCloud folders
 create table 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
+    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 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 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
+    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 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 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
+    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 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
+    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 newssync_label_names on newssync_labels(name);
 
diff --git a/tests/TestConf.php b/tests/TestConf.php
index fbbacf6d..e7fcd82c 100644
--- a/tests/TestConf.php
+++ b/tests/TestConf.php
@@ -5,101 +5,101 @@ use \org\bovigo\vfs\vfsStream;
 
 
 class TestConf extends \PHPUnit\Framework\TestCase {
-	use Test\Tools;
-	
-	static $vfs;
-	static $path;
-
-	static function setUpBeforeClass() {
-		self::$vfs = vfsStream::setup("root", null, [
-			'confGood'       => '<?php return Array("lang" => "xx");',
-			'confNotArray'   => '<?php return 0;',
-			'confCorrupt'    => '<?php return 0',
-			'confNotPHP'     => 'DEAD BEEF',
-			'confEmpty'      => '',
-			'confUnreadable' => '',
-		]);
-		self::$path = self::$vfs->url()."/";
-		// set up a file without read access
-		chmod(self::$path."confUnreadable", 0000);
-	}
-
-	static function tearDownAfterClass() {
-		self::$path = null;
-		self::$vfs = null;
-	}
-	
-	function testLoadDefaultValues() {
-		$this->assertInstanceOf(Conf::class, new Conf());
-	}
+    use Test\Tools;
     
-	/**
+    static $vfs;
+    static $path;
+
+    static function setUpBeforeClass() {
+        self::$vfs = vfsStream::setup("root", null, [
+            'confGood'       => '<?php return Array("lang" => "xx");',
+            'confNotArray'   => '<?php return 0;',
+            'confCorrupt'    => '<?php return 0',
+            'confNotPHP'     => 'DEAD BEEF',
+            'confEmpty'      => '',
+            'confUnreadable' => '',
+        ]);
+        self::$path = self::$vfs->url()."/";
+        // set up a file without read access
+        chmod(self::$path."confUnreadable", 0000);
+    }
+
+    static function tearDownAfterClass() {
+        self::$path = null;
+        self::$vfs = null;
+    }
+    
+    function testLoadDefaultValues() {
+        $this->assertInstanceOf(Conf::class, new Conf());
+    }
+    
+    /**
      * @depends testLoadDefaultValues
      */
-	function testImportFromArray() {
-		$arr = ['lang' => "xx"];
-		$conf = new Conf();
-		$conf->import($arr);
-		$this->assertEquals("xx", $conf->lang);
-	}
+    function testImportFromArray() {
+        $arr = ['lang' => "xx"];
+        $conf = new Conf();
+        $conf->import($arr);
+        $this->assertEquals("xx", $conf->lang);
+    }
 
-	/**
+    /**
      * @depends testImportFromArray
      */
-	function testImportFromFile() {
-		$conf = new Conf();
-		$conf->importFile(self::$path."confGood");
-		$this->assertEquals("xx", $conf->lang);
-		$conf = new Conf(self::$path."confGood");
-		$this->assertEquals("xx", $conf->lang);
-	}
+    function testImportFromFile() {
+        $conf = new Conf();
+        $conf->importFile(self::$path."confGood");
+        $this->assertEquals("xx", $conf->lang);
+        $conf = new Conf(self::$path."confGood");
+        $this->assertEquals("xx", $conf->lang);
+    }
 
-	/**
+    /**
      * @depends testImportFromFile
      */
-	function testImportFromMissingFile() {
-		$this->assertException("fileMissing", "Conf");
-		$conf = new Conf(self::$path."confMissing");
-	}
+    function testImportFromMissingFile() {
+        $this->assertException("fileMissing", "Conf");
+        $conf = new Conf(self::$path."confMissing");
+    }
 
-	/**
+    /**
      * @depends testImportFromFile
      */
-	function testImportFromEmptyFile() {
-		$this->assertException("fileCorrupt", "Conf");
-		$conf = new Conf(self::$path."confEmpty");
-	}
+    function testImportFromEmptyFile() {
+        $this->assertException("fileCorrupt", "Conf");
+        $conf = new Conf(self::$path."confEmpty");
+    }
 
-	/**
+    /**
      * @depends testImportFromFile
      */
-	function testImportFromFileWithoutReadPermission() {
-		$this->assertException("fileUnreadable", "Conf");
-		$conf = new Conf(self::$path."confUnreadable");
-	}
+    function testImportFromFileWithoutReadPermission() {
+        $this->assertException("fileUnreadable", "Conf");
+        $conf = new Conf(self::$path."confUnreadable");
+    }
 
-	/**
+    /**
      * @depends testImportFromFile
      */
-	function testImportFromFileWhichIsNotAnArray() {
-		$this->assertException("fileCorrupt", "Conf");
-		$conf = new Conf(self::$path."confNotArray");
-	}
+    function testImportFromFileWhichIsNotAnArray() {
+        $this->assertException("fileCorrupt", "Conf");
+        $conf = new Conf(self::$path."confNotArray");
+    }
 
-	/**
+    /**
      * @depends testImportFromFile
      */
-	function testImportFromFileWhichIsNotPhp() {
-		$this->assertException("fileCorrupt", "Conf");
-		// this should not print the output of the non-PHP file
-		$conf = new Conf(self::$path."confNotPHP");
-	}
+    function testImportFromFileWhichIsNotPhp() {
+        $this->assertException("fileCorrupt", "Conf");
+        // this should not print the output of the non-PHP file
+        $conf = new Conf(self::$path."confNotPHP");
+    }
 
-	/**
+    /**
      * @depends testImportFromFile
      */
-	function testImportFromCorruptFile() {
-		$this->assertException("fileCorrupt", "Conf");
-		$conf = new Conf(self::$path."confCorrupt");
-	}
+    function testImportFromCorruptFile() {
+        $this->assertException("fileCorrupt", "Conf");
+        $conf = new Conf(self::$path."confCorrupt");
+    }
 }
diff --git a/tests/TestException.php b/tests/TestException.php
index 902f3282..7281d37b 100644
--- a/tests/TestException.php
+++ b/tests/TestException.php
@@ -4,67 +4,67 @@ namespace JKingWeb\NewsSync;
 
 
 class TestException extends \PHPUnit\Framework\TestCase {
-	use Test\Tools;
+    use Test\Tools;
 
-	static function setUpBeforeClass() {
-		Lang::set("");
-	}
+    static function setUpBeforeClass() {
+        Lang::set("");
+    }
 
-	static function tearDownAfterClass() {
-		Lang::set(Lang::DEFAULT);
-	}
-	
-	function testBaseClass() {
-		$this->assertException("unknown");
-		throw new Exception("unknown");
-	}
+    static function tearDownAfterClass() {
+        Lang::set(Lang::DEFAULT);
+    }
+    
+    function testBaseClass() {
+        $this->assertException("unknown");
+        throw new Exception("unknown");
+    }
 
-	/**
+    /**
      * @depends testBaseClass
      */
-	function testBaseClassWithoutMessage() {
-		$this->assertException("unknown");
-		throw new Exception();
-	}
-	
-	/**
+    function testBaseClassWithoutMessage() {
+        $this->assertException("unknown");
+        throw new Exception();
+    }
+    
+    /**
      * @depends testBaseClass
      */
-	function testDerivedClass() {
-		$this->assertException("fileMissing", "Lang");
-		throw new Lang\Exception("fileMissing");
-	}
+    function testDerivedClass() {
+        $this->assertException("fileMissing", "Lang");
+        throw new Lang\Exception("fileMissing");
+    }
 
-	/**
+    /**
      * @depends testDerivedClass
      */
-	function testDerivedClassWithMessageParameters() {
-		$this->assertException("fileMissing", "Lang");
-		throw new Lang\Exception("fileMissing", "en");
-	}
+    function testDerivedClassWithMessageParameters() {
+        $this->assertException("fileMissing", "Lang");
+        throw new Lang\Exception("fileMissing", "en");
+    }
 
-	/**
+    /**
      * @depends testBaseClass
      */
-	function testBaseClassWithUnknownCode() {
-		$this->assertException("uncoded");
-		throw new Exception("testThisExceptionMessageDoesNotExist");
-	}
+    function testBaseClassWithUnknownCode() {
+        $this->assertException("uncoded");
+        throw new Exception("testThisExceptionMessageDoesNotExist");
+    }
 
-	/**
+    /**
      * @depends testBaseClass
      */
-	function testBaseClassWithMissingMessage() {
-		$this->assertException("stringMissing", "Lang");
-		throw new Exception("invalid");
-	}
+    function testBaseClassWithMissingMessage() {
+        $this->assertException("stringMissing", "Lang");
+        throw new Exception("invalid");
+    }
 
-	/**
+    /**
      * @depends testBaseClassWithUnknownCode
      */
-	function testDerivedClassWithMissingMessage() {
-		$this->assertException("uncoded");
-		throw new Lang\Exception("testThisExceptionMessageDoesNotExist");
-	}
-	
+    function testDerivedClassWithMissingMessage() {
+        $this->assertException("uncoded");
+        throw new Lang\Exception("testThisExceptionMessageDoesNotExist");
+    }
+    
 }
diff --git a/tests/TestLang.php b/tests/TestLang.php
index 9b81f53e..cf53a9e8 100644
--- a/tests/TestLang.php
+++ b/tests/TestLang.php
@@ -5,58 +5,58 @@ use \org\bovigo\vfs\vfsStream;
 
 
 class TestLang extends \PHPUnit\Framework\TestCase {
-	use Test\Tools, Test\Lang\Setup;
+    use Test\Tools, Test\Lang\Setup;
 
-	static $vfs;
-	static $path;
-	static $files;
-	static $defaultPath;
+    static $vfs;
+    static $path;
+    static $files;
+    static $defaultPath;
 
-	function testListLanguages() {
-		$this->assertCount(sizeof(self::$files), Lang::list("en"));
-	}
+    function testListLanguages() {
+        $this->assertCount(sizeof(self::$files), Lang::list("en"));
+    }
 
-	/**
+    /**
      * @depends testListLanguages
      */
-	function testSetLanguage() {
-		$this->assertEquals("en", Lang::set("en"));
-		$this->assertEquals("en_ca", Lang::set("en_ca"));
-		$this->assertEquals("de", Lang::set("de_ch"));
-		$this->assertEquals("en", Lang::set("en_gb_hixie"));
-		$this->assertEquals("en_ca", Lang::set("en_ca_jking"));
-		$this->assertEquals("en", Lang::set("es"));
-		$this->assertEquals("", Lang::set(""));
-	}
+    function testSetLanguage() {
+        $this->assertEquals("en", Lang::set("en"));
+        $this->assertEquals("en_ca", Lang::set("en_ca"));
+        $this->assertEquals("de", Lang::set("de_ch"));
+        $this->assertEquals("en", Lang::set("en_gb_hixie"));
+        $this->assertEquals("en_ca", Lang::set("en_ca_jking"));
+        $this->assertEquals("en", Lang::set("es"));
+        $this->assertEquals("", Lang::set(""));
+    }
 
-	/**
+    /**
      * @depends testSetLanguage
      */
-	function testLoadInternalStrings() {
-		$this->assertEquals("", Lang::set("", true));
-		$this->assertCount(sizeof(Lang::REQUIRED), Lang::dump());
-	}
+    function testLoadInternalStrings() {
+        $this->assertEquals("", Lang::set("", true));
+        $this->assertCount(sizeof(Lang::REQUIRED), Lang::dump());
+    }
 
-	/**
+    /**
      * @depends testLoadInternalStrings
      */
-	function testLoadDefaultLanguage() {
-		$this->assertEquals(Lang::DEFAULT, Lang::set(Lang::DEFAULT, true));
-		$str = Lang::dump();
-		$this->assertArrayHasKey('Exception.JKingWeb/NewsSync/Exception.uncoded', $str);
-		$this->assertArrayHasKey('Test.presentText', $str);
-	}
+    function testLoadDefaultLanguage() {
+        $this->assertEquals(Lang::DEFAULT, Lang::set(Lang::DEFAULT, true));
+        $str = Lang::dump();
+        $this->assertArrayHasKey('Exception.JKingWeb/NewsSync/Exception.uncoded', $str);
+        $this->assertArrayHasKey('Test.presentText', $str);
+    }
 
-	/**
+    /**
      * @depends testLoadDefaultLanguage
      */
-	function testLoadSupplementaryLanguage() {
-		Lang::set(Lang::DEFAULT, true);
-		$this->assertEquals("ja", Lang::set("ja", true));
-		$str = Lang::dump();
-		$this->assertArrayHasKey('Exception.JKingWeb/NewsSync/Exception.uncoded', $str);
-		$this->assertArrayHasKey('Test.presentText', $str);
-		$this->assertArrayHasKey('Test.absentText', $str);
-	}
+    function testLoadSupplementaryLanguage() {
+        Lang::set(Lang::DEFAULT, true);
+        $this->assertEquals("ja", Lang::set("ja", true));
+        $str = Lang::dump();
+        $this->assertArrayHasKey('Exception.JKingWeb/NewsSync/Exception.uncoded', $str);
+        $this->assertArrayHasKey('Test.presentText', $str);
+        $this->assertArrayHasKey('Test.absentText', $str);
+    }
 
 }
\ No newline at end of file
diff --git a/tests/TestLangErrors.php b/tests/TestLangErrors.php
index 0bc93c80..14075e7f 100644
--- a/tests/TestLangErrors.php
+++ b/tests/TestLangErrors.php
@@ -5,51 +5,51 @@ use \org\bovigo\vfs\vfsStream;
 
 
 class TestLangErrors extends \PHPUnit\Framework\TestCase {
-	use Test\Tools, Test\Lang\Setup;
+    use Test\Tools, Test\Lang\Setup;
 
-	static $vfs;
-	static $path;
-	static $files;
-	static $defaultPath;
+    static $vfs;
+    static $path;
+    static $files;
+    static $defaultPath;
 
-	function setUp() {
-		Lang::set("", true);
-	}
+    function setUp() {
+        Lang::set("", true);
+    }
 
-	function testLoadEmptyFile() {
-		$this->assertException("fileCorrupt", "Lang");
-		Lang::set("fr_ca", true);
-	}
+    function testLoadEmptyFile() {
+        $this->assertException("fileCorrupt", "Lang");
+        Lang::set("fr_ca", true);
+    }
 
-	function testLoadFileWhichDoesNotReturnAnArray() {
-		$this->assertException("fileCorrupt", "Lang");
-		Lang::set("it", true);
-	}
+    function testLoadFileWhichDoesNotReturnAnArray() {
+        $this->assertException("fileCorrupt", "Lang");
+        Lang::set("it", true);
+    }
 
-	function testLoadFileWhichIsNotPhp() {
-		$this->assertException("fileCorrupt", "Lang");
-		Lang::set("ko", true);
-	}
+    function testLoadFileWhichIsNotPhp() {
+        $this->assertException("fileCorrupt", "Lang");
+        Lang::set("ko", true);
+    }
 
-	function testLoadFileWhichIsCorrupt() {
-		$this->assertException("fileCorrupt", "Lang");
-		Lang::set("zh", true);
-	}
+    function testLoadFileWhichIsCorrupt() {
+        $this->assertException("fileCorrupt", "Lang");
+        Lang::set("zh", true);
+    }
 
-	function testLoadFileWithooutReadPermission() {
-		$this->assertException("fileUnreadable", "Lang");
-		Lang::set("ru", true);
-	}
+    function testLoadFileWithooutReadPermission() {
+        $this->assertException("fileUnreadable", "Lang");
+        Lang::set("ru", true);
+    }
 
-	function testLoadSubtagOfMissingLanguage() {
-		$this->assertException("fileMissing", "Lang");
-		Lang::set("pt_br", true);
-	}
+    function testLoadSubtagOfMissingLanguage() {
+        $this->assertException("fileMissing", "Lang");
+        Lang::set("pt_br", true);
+    }
 
-	function testLoadMissingDefaultLanguage() {
-		// this should be the last test of the series
-		unlink(self::$path.Lang::DEFAULT.".php");
-		$this->assertException("defaultFileMissing", "Lang");
-		Lang::set("fr", true);
-	}
+    function testLoadMissingDefaultLanguage() {
+        // this should be the last test of the series
+        unlink(self::$path.Lang::DEFAULT.".php");
+        $this->assertException("defaultFileMissing", "Lang");
+        Lang::set("fr", true);
+    }
 }
\ No newline at end of file
diff --git a/tests/lib/Lang/Setup.php b/tests/lib/Lang/Setup.php
index 05fc3166..8d1bcc5b 100644
--- a/tests/lib/Lang/Setup.php
+++ b/tests/lib/Lang/Setup.php
@@ -6,43 +6,43 @@ use \org\bovigo\vfs\vfsStream, \JKingWeb\NewsSync\Lang;
 
 
 trait Setup {
-	static function setUpBeforeClass() {
-		// this is required to keep from having exceptions in Lang::msg() in turn calling Lang::msg() and looping
-		\JKingWeb\NewsSync\Lang\Exception::$test = true;
-		// test files
-		self::$files = [
-			'en.php'    => '<?php return ["Test.presentText" => "and the Philosopher\'s Stone"];',
-			'en_ca.php' => '<?php return ["Test.presentText" => "{0} and {1}"];',
-			'en_us.php' => '<?php return ["Test.presentText" => "and the Sorcerer\'s Stone"];',
-			'fr.php'    => '<?php return ["Test.presentText" => "à l\'école des sorciers"];',
-			'ja.php'    => '<?php return ["Test.absentText"  => "賢者の石"];',
-			'de.php'    => '<?php return ["Test.presentText" => "und der Stein der Weisen"];',
-			'pt_br.php' => '<?php return ["Test.presentText" => "e a Pedra Filosofal"];',
-			'vi.php'    => '<?php return [];',
-			// corrupt files
-			'it.php'    => '<?php return 0;',
-			'zh.php'    => '<?php return 0',
-			'ko.php'    => 'DEAD BEEF',
-			'fr_ca.php' => '',
-			// unreadable file
-			'ru.php'    => '',
-		];
-		self::$vfs = vfsStream::setup("langtest", 0777, self::$files);
-		self::$path = self::$vfs->url()."/";
-		// set up a file without read access
-		chmod(self::$path."ru.php", 0000);
-		// make the Lang class use the vfs files
-		self::$defaultPath = Lang::$path;
-		Lang::$path = self::$path;
-	}
+    static function setUpBeforeClass() {
+        // this is required to keep from having exceptions in Lang::msg() in turn calling Lang::msg() and looping
+        \JKingWeb\NewsSync\Lang\Exception::$test = true;
+        // test files
+        self::$files = [
+            'en.php'    => '<?php return ["Test.presentText" => "and the Philosopher\'s Stone"];',
+            'en_ca.php' => '<?php return ["Test.presentText" => "{0} and {1}"];',
+            'en_us.php' => '<?php return ["Test.presentText" => "and the Sorcerer\'s Stone"];',
+            'fr.php'    => '<?php return ["Test.presentText" => "à l\'école des sorciers"];',
+            'ja.php'    => '<?php return ["Test.absentText"  => "賢者の石"];',
+            'de.php'    => '<?php return ["Test.presentText" => "und der Stein der Weisen"];',
+            'pt_br.php' => '<?php return ["Test.presentText" => "e a Pedra Filosofal"];',
+            'vi.php'    => '<?php return [];',
+            // corrupt files
+            'it.php'    => '<?php return 0;',
+            'zh.php'    => '<?php return 0',
+            'ko.php'    => 'DEAD BEEF',
+            'fr_ca.php' => '',
+            // unreadable file
+            'ru.php'    => '',
+        ];
+        self::$vfs = vfsStream::setup("langtest", 0777, self::$files);
+        self::$path = self::$vfs->url()."/";
+        // set up a file without read access
+        chmod(self::$path."ru.php", 0000);
+        // make the Lang class use the vfs files
+        self::$defaultPath = Lang::$path;
+        Lang::$path = self::$path;
+    }
 
-	static function tearDownAfterClass() {
-		\JKingWeb\NewsSync\Lang\Exception::$test = false;
-		Lang::$path = self::$defaultPath;
-		self::$path = null;
-		self::$vfs = null;
-		self::$files = null;
-		Lang::set("", true);
-		Lang::set(Lang::DEFAULT);
-	}
+    static function tearDownAfterClass() {
+        \JKingWeb\NewsSync\Lang\Exception::$test = false;
+        Lang::$path = self::$defaultPath;
+        self::$path = null;
+        self::$vfs = null;
+        self::$files = null;
+        Lang::set("", true);
+        Lang::set(Lang::DEFAULT);
+    }
 }
\ No newline at end of file
diff --git a/tests/lib/Tools.php b/tests/lib/Tools.php
index ba2b3533..8de83e17 100644
--- a/tests/lib/Tools.php
+++ b/tests/lib/Tools.php
@@ -4,15 +4,15 @@ namespace JKingWeb\NewsSync\Test;
 use \JKingWeb\NewsSync\Exception;
 
 trait Tools {
-	function assertException(string $msg, string $prefix = "", string $type = "Exception") {
-		$class = \JKingWeb\NewsSync\NS_BASE . ($prefix !== "" ? str_replace("/", "\\", $prefix) . "\\" : "") . $type;
-		$msgID = ($prefix !== "" ? $prefix . "/" : "") . $type. ".$msg";
-		if(array_key_exists($msgID, Exception::CODES)) {
-			$code = Exception::CODES[$msgID];
-		} else {
-			$code = 0;
-		}
-		$this->expectException($class);
-		$this->expectExceptionCode($code);
-	}
+    function assertException(string $msg, string $prefix = "", string $type = "Exception") {
+        $class = \JKingWeb\NewsSync\NS_BASE . ($prefix !== "" ? str_replace("/", "\\", $prefix) . "\\" : "") . $type;
+        $msgID = ($prefix !== "" ? $prefix . "/" : "") . $type. ".$msg";
+        if(array_key_exists($msgID, Exception::CODES)) {
+            $code = Exception::CODES[$msgID];
+        } else {
+            $code = 0;
+        }
+        $this->expectException($class);
+        $this->expectExceptionCode($code);
+    }
 }
\ No newline at end of file
diff --git a/tests/phpunit.xml b/tests/phpunit.xml
index efe33982..a7167ed1 100644
--- a/tests/phpunit.xml
+++ b/tests/phpunit.xml
@@ -1,23 +1,23 @@
 <?xml version="1.0"?>
 <phpunit
-	colors="true"
-	bootstrap="../bootstrap.php"
-	convertErrorsToExceptions="true"
-	convertNoticesToExceptions="true"
-	convertWarningsToExceptions="true"
-	beStrictAboutTestsThatDoNotTestAnything="true"
-	beStrictAboutOutputDuringTests="true"
-	beStrictAboutTestSize="true"
-	stopOnError="true">
+    colors="true"
+    bootstrap="../bootstrap.php"
+    convertErrorsToExceptions="true"
+    convertNoticesToExceptions="true"
+    convertWarningsToExceptions="true"
+    beStrictAboutTestsThatDoNotTestAnything="true"
+    beStrictAboutOutputDuringTests="true"
+    beStrictAboutTestSize="true"
+    stopOnError="true">
 
 <testsuite name="Localization and exceptions">
-	<file>TestLang.php</file>
-	<file>TestLangComplex.php</file>
-	<file>TestException.php</file>
-	<file>TestLangErrors.php</file>
+    <file>TestLang.php</file>
+    <file>TestLangComplex.php</file>
+    <file>TestException.php</file>
+    <file>TestLangErrors.php</file>
 </testsuite>
 
 <testsuite name="Configuration loading and saving">
-	<file>TestConf.php</file>
+    <file>TestConf.php</file>
 </testsuite>
 </phpunit>
\ No newline at end of file
diff --git a/tests/testLangComplex.php b/tests/testLangComplex.php
index 6ab5436d..60c55c9f 100644
--- a/tests/testLangComplex.php
+++ b/tests/testLangComplex.php
@@ -5,82 +5,82 @@ use \org\bovigo\vfs\vfsStream;
 
 
 class TestLangComplex extends \PHPUnit\Framework\TestCase {
-	use Test\Tools, Test\Lang\Setup;
+    use Test\Tools, Test\Lang\Setup;
 
-	static $vfs;
-	static $path;
-	static $files;
-	static $defaultPath;
+    static $vfs;
+    static $path;
+    static $files;
+    static $defaultPath;
 
-	function setUp() {
-		Lang::set(Lang::DEFAULT, true);
-	}
+    function setUp() {
+        Lang::set(Lang::DEFAULT, true);
+    }
 
-	function testLazyLoad() {
-		Lang::set("ja");
-		$this->assertArrayNotHasKey('Test.absentText', Lang::dump());
-	}
-	
-	function testLoadCascadeOfFiles() {
-		Lang::set("ja", true);
-		$this->assertEquals("de", Lang::set("de", true));
-		$str = Lang::dump();
-		$this->assertArrayNotHasKey('Test.absentText', $str);
-		$this->assertEquals('und der Stein der Weisen', $str['Test.presentText']);
-	}
+    function testLazyLoad() {
+        Lang::set("ja");
+        $this->assertArrayNotHasKey('Test.absentText', Lang::dump());
+    }
+    
+    function testLoadCascadeOfFiles() {
+        Lang::set("ja", true);
+        $this->assertEquals("de", Lang::set("de", true));
+        $str = Lang::dump();
+        $this->assertArrayNotHasKey('Test.absentText', $str);
+        $this->assertEquals('und der Stein der Weisen', $str['Test.presentText']);
+    }
 
-	/**
+    /**
      * @depends testLoadCascadeOfFiles
      */
-	function testLoadSubtag() {
-		$this->assertEquals("en_ca", Lang::set("en_ca", true));
-	}
-	
-	function testFetchAMessage() {
-		Lang::set("de", true);
-		$this->assertEquals('und der Stein der Weisen', Lang::msg('Test.presentText'));
-	}
+    function testLoadSubtag() {
+        $this->assertEquals("en_ca", Lang::set("en_ca", true));
+    }
+    
+    function testFetchAMessage() {
+        Lang::set("de", true);
+        $this->assertEquals('und der Stein der Weisen', Lang::msg('Test.presentText'));
+    }
 
-	/**
+    /**
      * @depends testFetchAMessage
      */
-	function testFetchAMessageWithSingleNumericParameter() {
-		Lang::set("en_ca", true);
-		$this->assertEquals('Default language file "en" missing', Lang::msg('Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing', Lang::DEFAULT));
-	}
+    function testFetchAMessageWithSingleNumericParameter() {
+        Lang::set("en_ca", true);
+        $this->assertEquals('Default language file "en" missing', Lang::msg('Exception.JKingWeb/NewsSync/Lang/Exception.defaultFileMissing', Lang::DEFAULT));
+    }
 
-	/**
+    /**
      * @depends testFetchAMessage
      */
-	function testFetchAMessageWithMultipleNumericParameters() {
-		Lang::set("en_ca", true);
-		$this->assertEquals('Happy Rotter and the Philosopher\'s Stone', Lang::msg('Test.presentText', ['Happy Rotter', 'the Philosopher\'s Stone']));
-	}
+    function testFetchAMessageWithMultipleNumericParameters() {
+        Lang::set("en_ca", true);
+        $this->assertEquals('Happy Rotter and the Philosopher\'s Stone', Lang::msg('Test.presentText', ['Happy Rotter', 'the Philosopher\'s Stone']));
+    }
 
-	/**
+    /**
      * @depends testFetchAMessage
      */
-	function testFetchAMessageWithNamedParameters() {
-		$this->assertEquals('Message string "Test.absentText" missing from all loaded language files (en)', Lang::msg('Exception.JKingWeb/NewsSync/Lang/Exception.stringMissing', ['msgID' => 'Test.absentText', 'fileList' => 'en']));
-	}
+    function testFetchAMessageWithNamedParameters() {
+        $this->assertEquals('Message string "Test.absentText" missing from all loaded language files (en)', Lang::msg('Exception.JKingWeb/NewsSync/Lang/Exception.stringMissing', ['msgID' => 'Test.absentText', 'fileList' => 'en']));
+    }
 
-	/**
+    /**
      * @depends testFetchAMessage
      */
-	function testReloadDefaultStrings() {
-		Lang::set("de", true);
-		Lang::set("en", true);
-		$this->assertEquals('and the Philosopher\'s Stone', Lang::msg('Test.presentText'));
-	}
+    function testReloadDefaultStrings() {
+        Lang::set("de", true);
+        Lang::set("en", true);
+        $this->assertEquals('and the Philosopher\'s Stone', Lang::msg('Test.presentText'));
+    }
 
-	/**
+    /**
      * @depends testFetchAMessage
      */
-	function testReloadGeneralTagAfterSubtag() {
-		Lang::set("en", true);
-		Lang::set("en_us", true);
-		$this->assertEquals('and the Sorcerer\'s Stone', Lang::msg('Test.presentText'));
-		Lang::set("en", true);
-		$this->assertEquals('and the Philosopher\'s Stone', Lang::msg('Test.presentText'));
-	}
+    function testReloadGeneralTagAfterSubtag() {
+        Lang::set("en", true);
+        Lang::set("en_us", true);
+        $this->assertEquals('and the Sorcerer\'s Stone', Lang::msg('Test.presentText'));
+        Lang::set("en", true);
+        $this->assertEquals('and the Philosopher\'s Stone', Lang::msg('Test.presentText'));
+    }
 }
\ No newline at end of file