Просмотр исходного кода

Merge pull request #1844 from causefx/v2-develop

V2 develop
causefx 4 лет назад
Родитель
Сommit
4f2e6c5f0c
100 измененных файлов с 7708 добавлено и 1189 удалено
  1. 1 0
      .gitignore
  2. 24 3
      api/classes/logger.class.php
  3. 327 133
      api/classes/organizr.class.php
  4. 3 2
      api/composer.json
  5. 355 68
      api/composer.lock
  6. 11 1
      api/config/default.php
  7. 20 12
      api/functions.php
  8. 2 2
      api/functions/2fa-functions.php
  9. 33 33
      api/functions/auth-functions.php
  10. 26 5
      api/functions/backup-functions.php
  11. 5 5
      api/functions/homepage-connect-functions.php
  12. 83 34
      api/functions/log-functions.php
  13. 35 10
      api/functions/normal-functions.php
  14. 1 1
      api/functions/oauth.php
  15. 1 1
      api/functions/option-functions.php
  16. 28 14
      api/functions/organizr-functions.php
  17. 36 23
      api/functions/sso-functions.php
  18. 39 37
      api/functions/token-functions.php
  19. 56 15
      api/functions/update-functions.php
  20. 153 5
      api/functions/upgrade-functions.php
  21. 1 1
      api/homepage/couchpotato.php
  22. 4 4
      api/homepage/deluge.php
  23. 3 3
      api/homepage/emby.php
  24. 5 5
      api/homepage/healthchecks.php
  25. 1 1
      api/homepage/jackett.php
  26. 3 3
      api/homepage/jdownloader.php
  27. 3 3
      api/homepage/jellyfin.php
  28. 4 4
      api/homepage/lidarr.php
  29. 1 1
      api/homepage/monitorr.php
  30. 25 26
      api/homepage/netdata.php
  31. 2 2
      api/homepage/nzbget.php
  32. 1 1
      api/homepage/octoprint.php
  33. 5 5
      api/homepage/ombi.php
  34. 5 5
      api/homepage/overseerr.php
  35. 2 2
      api/homepage/pihole.php
  36. 7 5
      api/homepage/plex.php
  37. 4 4
      api/homepage/qbittorrent.php
  38. 2 2
      api/homepage/radarr.php
  39. 2 2
      api/homepage/rtorrent.php
  40. 2 2
      api/homepage/sickrage.php
  41. 3 3
      api/homepage/sonarr.php
  42. 1 1
      api/homepage/speedtest.php
  43. 57 57
      api/homepage/tautulli.php
  44. 2 2
      api/homepage/trakt.php
  45. 4 4
      api/homepage/transmission.php
  46. 5 5
      api/homepage/unifi.php
  47. 4 4
      api/homepage/utorrent.php
  48. 3 2
      api/homepage/weather.php
  49. 6 5
      api/pages/settings-tab-editor-tabs.php
  50. 12 0
      api/pages/wizard.php
  51. 19 11
      api/plugins/bookmark/plugin.php
  52. 7 7
      api/plugins/healthChecks/plugin.php
  53. 12 12
      api/plugins/invites/plugin.php
  54. 0 3
      api/plugins/php-mailer/misc/emailTemplates/dark.php
  55. 2 166
      api/plugins/php-mailer/misc/emailTemplates/default.php
  56. 0 19
      api/plugins/php-mailer/misc/emailTemplates/gray.php
  57. 0 3
      api/plugins/php-mailer/misc/emailTemplates/light.php
  58. 0 15
      api/plugins/php-mailer/misc/emailTemplates/plehex.php
  59. 3 3
      api/plugins/php-mailer/plugin.php
  60. 22 0
      api/v2/routes/connectionTester.php
  61. 23 9
      api/v2/routes/update.php
  62. 5 0
      api/vendor/autoload.php
  63. 415 0
      api/vendor/brick/math/CHANGELOG.md
  64. 20 0
      api/vendor/brick/math/LICENSE
  65. 17 0
      api/vendor/brick/math/SECURITY.md
  66. 35 0
      api/vendor/brick/math/composer.json
  67. 895 0
      api/vendor/brick/math/src/BigDecimal.php
  68. 1184 0
      api/vendor/brick/math/src/BigInteger.php
  69. 572 0
      api/vendor/brick/math/src/BigNumber.php
  70. 523 0
      api/vendor/brick/math/src/BigRational.php
  71. 41 0
      api/vendor/brick/math/src/Exception/DivisionByZeroException.php
  72. 27 0
      api/vendor/brick/math/src/Exception/IntegerOverflowException.php
  73. 14 0
      api/vendor/brick/math/src/Exception/MathException.php
  74. 12 0
      api/vendor/brick/math/src/Exception/NegativeNumberException.php
  75. 35 0
      api/vendor/brick/math/src/Exception/NumberFormatException.php
  76. 21 0
      api/vendor/brick/math/src/Exception/RoundingNecessaryException.php
  77. 756 0
      api/vendor/brick/math/src/Internal/Calculator.php
  78. 116 0
      api/vendor/brick/math/src/Internal/Calculator/BcMathCalculator.php
  79. 156 0
      api/vendor/brick/math/src/Internal/Calculator/GmpCalculator.php
  80. 634 0
      api/vendor/brick/math/src/Internal/Calculator/NativeCalculator.php
  81. 107 0
      api/vendor/brick/math/src/RoundingMode.php
  82. 1 0
      api/vendor/composer/autoload_classmap.php
  83. 2 1
      api/vendor/composer/autoload_files.php
  84. 4 0
      api/vendor/composer/autoload_psr4.php
  85. 1 1
      api/vendor/composer/autoload_real.php
  86. 23 1
      api/vendor/composer/autoload_static.php
  87. 367 68
      api/vendor/composer/installed.json
  88. 45 9
      api/vendor/composer/installed.php
  89. 2 2
      api/vendor/composer/platform_check.php
  90. 21 0
      api/vendor/lcobucci/clock/LICENSE
  91. 40 0
      api/vendor/lcobucci/clock/composer.json
  92. 11 0
      api/vendor/lcobucci/clock/src/Clock.php
  93. 32 0
      api/vendor/lcobucci/clock/src/FrozenClock.php
  94. 34 0
      api/vendor/lcobucci/clock/src/SystemClock.php
  95. 0 3
      api/vendor/lcobucci/jwt/.gitignore
  96. 0 56
      api/vendor/lcobucci/jwt/.scrutinizer.yml
  97. 0 15
      api/vendor/lcobucci/jwt/.travis.yml
  98. 1 1
      api/vendor/lcobucci/jwt/LICENSE
  99. 0 194
      api/vendor/lcobucci/jwt/README.md
  100. 33 22
      api/vendor/lcobucci/jwt/composer.json

+ 1 - 0
.gitignore

@@ -79,6 +79,7 @@ Github.txt
 Demo.txt
 Demo.txt
 DemoTest.txt
 DemoTest.txt
 Dev.txt
 Dev.txt
+updateInProgress.txt
 config/cacert.pem
 config/cacert.pem
 config/custom.pem
 config/custom.pem
 config/config.php
 config/config.php

+ 24 - 3
api/classes/logger.class.php

@@ -1,12 +1,17 @@
 <?php
 <?php
 
 
+use Monolog\Handler\SlackWebhookHandler;
 use Nekonomokochan\PhpJsonLogger\Logger;
 use Nekonomokochan\PhpJsonLogger\Logger;
 use Nekonomokochan\PhpJsonLogger\LoggerBuilder;
 use Nekonomokochan\PhpJsonLogger\LoggerBuilder;
 
 
 class OrganizrLogger extends LoggerBuilder
 class OrganizrLogger extends LoggerBuilder
 {
 {
 	public $isReady;
 	public $isReady;
-	
+	/**
+	 * @var SlackWEbhookHandler
+	 */
+	private $slackWebhookHandler;
+
 	/**
 	/**
 	 * @return boolean
 	 * @return boolean
 	 */
 	 */
@@ -14,7 +19,7 @@ class OrganizrLogger extends LoggerBuilder
 	{
 	{
 		return $this->isReady;
 		return $this->isReady;
 	}
 	}
-	
+
 	/**
 	/**
 	 * @param boolean $readyStatus
 	 * @param boolean $readyStatus
 	 */
 	 */
@@ -22,7 +27,7 @@ class OrganizrLogger extends LoggerBuilder
 	{
 	{
 		$this->isReady = $readyStatus;
 		$this->isReady = $readyStatus;
 	}
 	}
-	
+
 	public function build(): Logger
 	public function build(): Logger
 	{
 	{
 		if (!$this->isReady) {
 		if (!$this->isReady) {
@@ -32,4 +37,20 @@ class OrganizrLogger extends LoggerBuilder
 		}
 		}
 		return new Logger($this);
 		return new Logger($this);
 	}
 	}
+
+	/**
+	 * @return SlackWebhookHandler
+	 */
+	public function getSlackWebhookHandler(): ?SlackWebhookHandler
+	{
+		return $this->slackWebhookHandler;
+	}
+
+	/**
+	 * @param SlackWebhookHandler $slackWebhookHandler
+	 */
+	public function setSlackWebhookHandler(SlackWebhookHandler $slackWebhookHandler)
+	{
+		$this->slackWebhookHandler = $slackWebhookHandler;
+	}
 }
 }

+ 327 - 133
api/classes/organizr.class.php

@@ -65,10 +65,10 @@ class Organizr
 
 
 	// ===================================
 	// ===================================
 	// Organizr Version
 	// Organizr Version
-	public $version = '2.1.1890';
+	public $version = '2.1.2320';
 	// ===================================
 	// ===================================
 	// Quick php Version check
 	// Quick php Version check
-	public $minimumPHP = '7.3';
+	public $minimumPHP = '7.4';
 	// ===================================
 	// ===================================
 	protected $db;
 	protected $db;
 	protected $otherDb;
 	protected $otherDb;
@@ -83,10 +83,7 @@ class Organizr
 	public $commit;
 	public $commit;
 	public $fileHash;
 	public $fileHash;
 	public $cookieName;
 	public $cookieName;
-	public $log;
-	public $logger;
-	public $organizrLog;
-	public $organizrLoginLog;
+	public $logFile;
 	public $timeExecution;
 	public $timeExecution;
 	public $root;
 	public $root;
 	public $paths;
 	public $paths;
@@ -94,66 +91,72 @@ class Organizr
 	public $groupOptions;
 	public $groupOptions;
 	public $warnings;
 	public $warnings;
 	public $errors;
 	public $errors;
+	public bool $loggerSetup = false;
+	public \Nekonomokochan\PhpJsonLogger\Logger $logger;
 
 
 	public function __construct($updating = false)
 	public function __construct($updating = false)
 	{
 	{
-		$this->errors = E_ALL;//E_ALL & ~E_NOTICE
-		// Set custom Error handler
-		set_error_handler([$this, 'setAPIErrorResponse'], $this->errors);
-		// Next Check PHP Version
-		$this->checkPHP();
-		// Check Disk Space
-		$this->checkDiskSpace();
-		// Set UUID for device
-		$this->setDeviceUUID();
 		// Constructed from Updater?
 		// Constructed from Updater?
 		$this->updating = $updating;
 		$this->updating = $updating;
-		// Set Project Root directory
+		// Set Project Root directory and paths
 		$this->root = dirname(__DIR__, 2);
 		$this->root = dirname(__DIR__, 2);
-		// Set Start Execution Time
-		$this->timeExecution = $this->timeExecution();
-		// Set location path to user config path
-		$this->chooseConfigFile();
-		//$this->userConfigPath = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.php';
-		// Set location path to default config path
-		$this->defaultConfigPath = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'default.php';
+		$this->paths = [
+			'Root Folder' => $this->root . DIRECTORY_SEPARATOR,
+			'Cache Folder' => $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR,
+			'Tab Folder' => $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'userTabs' . DIRECTORY_SEPARATOR,
+			'API Folder' => dirname(__DIR__, 1) . DIRECTORY_SEPARATOR
+		];
+		// Temp Set Errors
+		$this->errors = E_ALL;//E_ALL & ~E_NOTICE
 		// Set current time
 		// Set current time
-		$this->currentTime = gmdate("Y-m-d\TH:i:s\Z");
+		$this->currentTime = gmdate('Y-m-d\TH:i:s\Z');
 		// Set variable if install is for official docker
 		// Set variable if install is for official docker
 		$this->docker = (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Docker.txt'));
 		$this->docker = (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Docker.txt'));
 		// Set variable if install is for develop and set php Error levels
 		// Set variable if install is for develop and set php Error levels
 		$this->dev = (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Dev.txt'));
 		$this->dev = (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Dev.txt'));
-		$this->phpErrors();
 		// Set variable if install is for demo
 		// Set variable if install is for demo
 		$this->demo = (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Demo.txt'));
 		$this->demo = (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Demo.txt'));
-		// Set variable if install has commit hash
+		// Set variable if install has commit hash and variable to be used as hash for files
 		$this->commit = ($this->docker && !$this->dev) ? file_get_contents(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Github.txt') : null;
 		$this->commit = ($this->docker && !$this->dev) ? file_get_contents(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Github.txt') : null;
-		// Set variable to be used as hash for files
 		$this->fileHash = ($this->commit) ?? $this->version;
 		$this->fileHash = ($this->commit) ?? $this->version;
 		$this->fileHash = trim($this->fileHash);
 		$this->fileHash = trim($this->fileHash);
+		// Set location path to user config path
+		$this->chooseConfigFile();
+		// Set location path to default config path
+		$this->defaultConfigPath = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'default.php';
 		// Load Config file
 		// Load Config file
 		$this->config = $this->config();
 		$this->config = $this->config();
+		// Set cookie name for Organizr Instance
+		$this->cookieName = ($this->hasConfig()) ? $this->config['uuid'] !== '' ? 'organizr_token_' . $this->config['uuid'] : 'organizr_token_temp' : 'organizr_token_temp';
+
+		// Set custom Error handler
+		set_error_handler([$this, 'setAPIErrorResponse'], $this->errors);
+		// Next Check PHP Version
+		$this->checkPHP();
+		// Check Disk Space
+		$this->checkDiskSpace();
+		// Set UUID for device
+		$this->setDeviceUUID();
+		// Add Plugin prefix to plugin global
+		$this->setPluginListNameFromConfigPrefix();
+		// Add database path to paths
+		$this->addDatabaseToPaths();
+		// Set Start Execution Time
+		$this->timeExecution = $this->timeExecution();
+
+
+		$this->phpErrors();
+
+
 		// Set organizr Logs and logger
 		// Set organizr Logs and logger
-		$this->log = $this->setOrganizrLog();
+		$this->logFile = $this->setOrganizrLog();
 		$this->setLoggerChannel();
 		$this->setLoggerChannel();
-		// Set organizr Log file location - will deprecate soon
-		$this->organizrLog = ($this->hasDB()) ? $this->config['dbLocation'] . 'organizrLog.json' : false;
-		// Set organizr Login Log file location - will deprecate soon
-		$this->organizrLoginLog = ($this->hasDB()) ? $this->config['dbLocation'] . 'organizrLoginLog.json' : false;
-		// Set Paths
-		$this->paths = array(
-			'Root Folder' => dirname(__DIR__, 2) . DIRECTORY_SEPARATOR,
-			'Cache Folder' => dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR,
-			'Tab Folder' => $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'userTabs' . DIRECTORY_SEPARATOR,
-			'API Folder' => dirname(__DIR__, 1) . DIRECTORY_SEPARATOR,
-			'DB Folder' => ($this->hasDB()) ? $this->config['dbLocation'] : false
-		);
+
 		// Connect to DB
 		// Connect to DB
 		$this->connectDB();
 		$this->connectDB();
 		// Check DB Writable
 		// Check DB Writable
 		$this->checkWritableDB();
 		$this->checkWritableDB();
-		// Set cookie name for Organizr Instance
-		$this->cookieName = ($this->hasDB()) ? $this->config['uuid'] !== '' ? 'organizr_token_' . $this->config['uuid'] : 'organizr_token_temp' : 'organizr_token_temp';
+
 		// Get token form cookie and validate
 		// Get token form cookie and validate
 		$this->setCurrentUser();
 		$this->setCurrentUser();
 		// might just run this at index
 		// might just run this at index
@@ -169,6 +172,13 @@ class Organizr
 		$this->disconnectDB();
 		$this->disconnectDB();
 	}
 	}
 
 
+	public function addDatabaseToPaths()
+	{
+		if ($this->hasConfig()) {
+			$this->paths = array_merge($this->paths, ['DB Folder' => $this->config['dbLocation']]);
+		}
+	}
+
 	public function chooseConfigFile()
 	public function chooseConfigFile()
 	{
 	{
 
 
@@ -302,7 +312,7 @@ class Organizr
 	public function setCurrentUser($validate = true)
 	public function setCurrentUser($validate = true)
 	{
 	{
 		$user = false;
 		$user = false;
-		if ($this->hasDB()) {
+		if ($this->hasDatabase()) {
 			if ($this->hasCookie()) {
 			if ($this->hasCookie()) {
 				$user = $this->getUserFromToken($_COOKIE[$this->cookieName]);
 				$user = $this->getUserFromToken($_COOKIE[$this->cookieName]);
 			}
 			}
@@ -335,16 +345,33 @@ class Organizr
 	public function checkForOrganizrOAuth()
 	public function checkForOrganizrOAuth()
 	{
 	{
 		// Oauth?
 		// Oauth?
-		if ($this->config['authProxyEnabled'] && ($this->config['authProxyHeaderName'] !== '' || $this->config['authProxyHeaderNameEmail'] !== '') && $this->config['authProxyWhitelist'] !== '') {
-			if (isset(getallheaders()[$this->config['authProxyHeaderName']]) || isset(getallheaders()[$this->config['authProxyHeaderNameEmail']])) {
-				$this->coookieSeconds('set', 'organizrOAuth', 'true', 20000, false);
+		if ($this->hasDB() && $this->user) {
+			if ($this->user['groupID'] == '999') {
+				$this->setLoggerChannel('OAuth')->debug('Starting OAuth login check');
+				$data = [
+					'enabled' => $this->config['authProxyEnabled'],
+					'header_name' => $this->config['authProxyHeaderName'],
+					'header_name_email' => $this->config['authProxyHeaderNameEmail'],
+					'whitelist' => $this->config['authProxyWhitelist'],
+				];
+				if ($this->config['authProxyEnabled'] && ($this->config['authProxyHeaderName'] !== '' || $this->config['authProxyHeaderNameEmail'] !== '') && $this->config['authProxyWhitelist'] !== '') {
+					if (isset($this->getallheadersi()[strtolower($this->config['authProxyHeaderName'])]) || isset($this->getallheadersi()[strtolower($this->config['authProxyHeaderNameEmail'])])) {
+						$this->coookieSeconds('set', 'organizrOAuth', 'true', 20000, false);
+						$this->setLoggerChannel('OAuth')->info('OAuth pre-check passed - adding organizrOAuth cookie', $data);
+					} else {
+						$data = array_merge($data, ['headers' => $this->getallheadersi()]);
+						$this->setLoggerChannel('OAuth')->debug('Headers not set', $data);
+					}
+				} else {
+					$this->setLoggerChannel('OAuth')->debug('OAuth not triggered', $data);
+				}
 			}
 			}
 		}
 		}
 	}
 	}
 
 
 	public function checkIfUserIsBlacklisted()
 	public function checkIfUserIsBlacklisted()
 	{
 	{
-		if ($this->hasDB()) {
+		if ($this->hasConfig()) {
 			$currentIP = $this->userIP();
 			$currentIP = $this->userIP();
 			if ($this->config['blacklisted'] !== '') {
 			if ($this->config['blacklisted'] !== '') {
 				if (in_array($currentIP, $this->arrayIP($this->config['blacklisted']))) {
 				if (in_array($currentIP, $this->arrayIP($this->config['blacklisted']))) {
@@ -818,7 +845,7 @@ class Organizr
 				];
 				];
 			}
 			}
 		}
 		}
-		$this->handleError($exceptions[$number], $message, $file, $line);
+		$this->handleError($exceptions[$number], $message, $file, $line, $type);
 	}
 	}
 
 
 	public function setErrorResponse($number, $message, $file, $line)
 	public function setErrorResponse($number, $message, $file, $line)
@@ -833,9 +860,21 @@ class Organizr
 		//$this->prettyPrint($error, true);
 		//$this->prettyPrint($error, true);
 	}
 	}
 
 
-	public function handleError($number, $message, $file, $line)
+	public function handleError($number, $message, $file, $line, $type)
 	{
 	{
-		error_log(sprintf('Organizr %s:  %s in %s on line %d', $number, $message, $file, $line));
+		$log = false;
+		$error = sprintf('Organizr %s:  %s in %s on line %d', $number, $message, $file, $line);
+		error_log($error);
+		if ($this->dev) {
+			$log = true;
+		} else {
+			if ($type == 'errors') {
+				$log = true;
+			}
+		}
+		if ($log && $this->hasDB()) {
+			$this->setLoggerChannel('Server Error')->warning('PHP Error', $error);
+		}
 	}
 	}
 
 
 	public function checkRoute($request)
 	public function checkRoute($request)
@@ -1116,6 +1155,14 @@ class Organizr
 		return $themes;
 		return $themes;
 	}
 	}
 
 
+	public function setPluginListNameFromConfigPrefix()
+	{
+		foreach ($GLOBALS['plugins'] as $pluginName => $pluginInfo) {
+			$GLOBALS['pluginInfo'][strtolower($pluginInfo['configPrefix'])] = $pluginInfo;
+			$GLOBALS['pluginInfo'][strtolower($pluginName)] = $pluginInfo;
+		}
+	}
+
 	public function pluginFilesFromDirectory($directory, $webDirectory, $type, $settings = false, $rootPath = '')
 	public function pluginFilesFromDirectory($directory, $webDirectory, $type, $settings = false, $rootPath = '')
 	{
 	{
 		$files = '';
 		$files = '';
@@ -1156,7 +1203,8 @@ class Organizr
 							}
 							}
 							if ($pluginEnabled || $settings) {
 							if ($pluginEnabled || $settings) {
 								if ($continue) {
 								if ($continue) {
-									$files .= '<script src="' . $rootPath . $webDirectory . basename(dirname($info->getPathname())) . '/' . basename($info->getFilename()) . '?v=' . $this->fileHash . '" defer="true"></script>';
+									$version = $GLOBALS['pluginInfo'][strtolower($key)]['version'] ?? $this->fileHash;
+									$files .= '<script src="' . $rootPath . $webDirectory . basename(dirname($info->getPathname())) . '/' . basename($info->getFilename()) . '?v=' . $version . '" defer="true"></script>';
 								}
 								}
 							}
 							}
 						}
 						}
@@ -1165,7 +1213,9 @@ class Organizr
 				case 'css':
 				case 'css':
 					foreach ($iteratorIterator as $info) {
 					foreach ($iteratorIterator as $info) {
 						if (pathinfo($info->getPathname(), PATHINFO_EXTENSION) == 'css') {
 						if (pathinfo($info->getPathname(), PATHINFO_EXTENSION) == 'css') {
-							$files .= '<link href="' . $rootPath . $webDirectory . basename(dirname($info->getPathname())) . '/' . basename($info->getFilename()) . '?v=' . $this->fileHash . '" rel="stylesheet">';
+							$key = basename(dirname($info->getPathname()));
+							$version = $GLOBALS['pluginInfo'][strtolower($key)]['version'] ?? $this->fileHash;
+							$files .= '<link href="' . $rootPath . $webDirectory . basename(dirname($info->getPathname())) . '/' . basename($info->getFilename()) . '?v=' . $version . '" rel="stylesheet">';
 						}
 						}
 					}
 					}
 					break;
 					break;
@@ -1437,8 +1487,7 @@ class Organizr
 		} else {
 		} else {
 			$status['action'] = 'launch';
 			$status['action'] = 'launch';
 			if ($action) {
 			if ($action) {
-				echo '<script type="text/javascript"> window.location.href="' . $this->getServerPath() . 'api/v2/organizr/error' . '";</script>';
-				die(header($this->getServerPath() . 'api/v2/organizr/error'));
+				echo '<script type="text/javascript"> window.location.href="' . $this->getServerPath() . 'api/v2/organizr/error/409' . '";</script>';
 				exit;
 				exit;
 			}
 			}
 		}
 		}
@@ -1569,14 +1618,14 @@ class Organizr
 		return $guest;
 		return $guest;
 	}
 	}
 
 
-	public function getAllUserTokens($id)
+	public function getAllUserTokens($id, $includeAllFields = true)
 	{
 	{
-
+		$select = $includeAllFields ? '*' : 'token, ip, id, expires, created';
 		$response = [
 		$response = [
 			array(
 			array(
 				'function' => 'fetchAll',
 				'function' => 'fetchAll',
 				'query' => array(
 				'query' => array(
-					'SELECT * FROM `tokens` WHERE user_id = ? AND expires > ?',
+					'SELECT ' . $select . ' FROM `tokens` WHERE user_id = ? AND expires > ?',
 					[$id],
 					[$id],
 					[$this->currentTime]
 					[$this->currentTime]
 				)
 				)
@@ -1633,7 +1682,8 @@ class Organizr
 		if ($validated == true) {
 		if ($validated == true) {
 			$allTokens = $this->getAllUserTokens($userInfo['userID']);
 			$allTokens = $this->getAllUserTokens($userInfo['userID']);
 			$user = $this->getUserById($userInfo['userID']);
 			$user = $this->getUserById($userInfo['userID']);
-			$tokenCheck = ($this->searchArray($allTokens, 'token', $token) !== false);
+			$tokenKey = $this->searchArray($allTokens, 'token', $token);
+			$tokenCheck = ($tokenKey !== false);
 			if (!$tokenCheck) {
 			if (!$tokenCheck) {
 				$this->setLoggerChannel('Authentication');
 				$this->setLoggerChannel('Authentication');
 				$this->logger->debug('Token failed check against all token listings', $allTokens);
 				$this->logger->debug('Token failed check against all token listings', $allTokens);
@@ -1643,6 +1693,21 @@ class Organizr
 				}
 				}
 				return false;
 				return false;
 			} else {
 			} else {
+				// Check if user is on same browser as token
+				if ($allTokens[$tokenKey]['browser'] !== $_SERVER ['HTTP_USER_AGENT']) {
+					if ($this->config['matchUserAgents']) {
+						$this->setLoggerChannel('Authentication')->warning('Mismatch of useragent', ['token' => $allTokens[$tokenKey]['browser'], 'browser' => $_SERVER ['HTTP_USER_AGENT']]);
+						$this->invalidToken($token);
+						return false;
+					}
+				}
+				if (($allTokens[$tokenKey]['ip'] !== $this->userIP()) && (!$this->isLocalOrServer())) {
+					if ($this->config['matchUserIP']) {
+						$this->setLoggerChannel('Authentication')->warning('Mismatch of user IP', ['token' => $allTokens[$tokenKey]['ip'], 'user' => $this->userIP()]);
+						$this->invalidToken($token);
+						return false;
+					}
+				}
 				if ($api) {
 				if ($api) {
 					$this->setResponse(200, 'Token is valid');
 					$this->setResponse(200, 'Token is valid');
 				}
 				}
@@ -1684,7 +1749,7 @@ class Organizr
 		$validated = (bool)$userInfo;
 		$validated = (bool)$userInfo;
 		if ($validated == true) {
 		if ($validated == true) {
 			$user = $this->getUserById($userInfo['userID']);
 			$user = $this->getUserById($userInfo['userID']);
-			$allTokens = $this->getAllUserTokens($userInfo['userID']);
+			$allTokens = $this->getAllUserTokens($userInfo['userID'], false);
 			return array(
 			return array(
 				'token' => $token,
 				'token' => $token,
 				'tokenDate' => $userInfo['tokenDate'],
 				'tokenDate' => $userInfo['tokenDate'],
@@ -1843,7 +1908,7 @@ class Organizr
 	public function getUserLevel()
 	public function getUserLevel()
 	{
 	{
 		// Grab token
 		// Grab token
-		$requesterToken = $this->getallheaders()['Token'] ?? ($_GET['apikey'] ?? false);
+		$requesterToken = $this->getallheadersi()['token'] ?? ($_GET['apikey'] ?? false);
 		$apiKey = ($this->config['organizrAPI']) ?? null;
 		$apiKey = ($this->config['organizrAPI']) ?? null;
 		// Check token or API key
 		// Check token or API key
 		// If API key, return 0 for admin
 		// If API key, return 0 for admin
@@ -1869,6 +1934,18 @@ class Organizr
 		}
 		}
 	}
 	}
 
 
+	public function qualifyLength($string, $length = 100, $api = false)
+	{
+		if (strlen($string) <= $length) {
+			return true;
+		} else {
+			if ($api) {
+				$this->setResponse(409, 'String is over limit of: ' . $length);
+			}
+			return false;
+		}
+	}
+
 	public function getImages()
 	public function getImages()
 	{
 	{
 		$allIconsPrep = array();
 		$allIconsPrep = array();
@@ -1924,7 +2001,11 @@ class Organizr
 			];
 			];
 		}
 		}
 		foreach ($newImageListing as $k => $v) {
 		foreach ($newImageListing as $k => $v) {
-			if (stripos($v['text'], $term) !== false || !$term) {
+			if ($term) {
+				if (stripos($v['text'], $term) !== false) {
+					$goodIcons['results'][] = $v;
+				}
+			} else {
 				$goodIcons['results'][] = $v;
 				$goodIcons['results'][] = $v;
 			}
 			}
 		}
 		}
@@ -2317,6 +2398,8 @@ class Organizr
 				$this->settingsOption('switch', 'lockoutSystem', ['label' => 'Inactivity Lock']),
 				$this->settingsOption('switch', 'lockoutSystem', ['label' => 'Inactivity Lock']),
 				$this->settingsOption('select', 'lockoutMinAuth', ['label' => 'Lockout Groups From', 'options' => $this->groupSelect()]),
 				$this->settingsOption('select', 'lockoutMinAuth', ['label' => 'Lockout Groups From', 'options' => $this->groupSelect()]),
 				$this->settingsOption('select', 'lockoutMaxAuth', ['label' => 'Lockout Groups To', 'options' => $this->groupSelect()]),
 				$this->settingsOption('select', 'lockoutMaxAuth', ['label' => 'Lockout Groups To', 'options' => $this->groupSelect()]),
+				$this->settingsOption('switch', 'matchUserAgents', ['label' => 'Match UserAgent', 'help' => 'Match Browser UserAgent to Token UserAgent - Can be very aggressive on matching']),
+				$this->settingsOption('switch', 'matchUserIP', ['label' => 'Match User IP', 'help' => 'Match User IP to Token IP - Also allows approval if user token is valid and is local to server']),
 				$this->settingsOption('switch', 'traefikAuthEnable', ['label' => 'Enable Traefik Auth Redirect', 'help' => 'This will enable the webserver to forward errors so traefik will accept them']),
 				$this->settingsOption('switch', 'traefikAuthEnable', ['label' => 'Enable Traefik Auth Redirect', 'help' => 'This will enable the webserver to forward errors so traefik will accept them']),
 				$this->settingsOption('input', 'traefikDomainOverride', ['label' => 'Traefik Domain for Return Override', 'help' => 'Please use a FQDN on this URL Override', 'placeholder' => 'http(s)://domain']),
 				$this->settingsOption('input', 'traefikDomainOverride', ['label' => 'Traefik Domain for Return Override', 'help' => 'Please use a FQDN on this URL Override', 'placeholder' => 'http(s)://domain']),
 				$this->settingsOption('select', 'debugAreaAuth', ['label' => 'Minimum Authentication for Debug Area', 'options' => $this->groupSelect(), 'settings' => '{}']),
 				$this->settingsOption('select', 'debugAreaAuth', ['label' => 'Minimum Authentication for Debug Area', 'options' => $this->groupSelect(), 'settings' => '{}']),
@@ -2331,12 +2414,23 @@ class Organizr
 				$this->settingsOption('number', 'maxLogFiles', ['label' => 'Maximum Log Files', 'help' => 'Number of log files to preserve', 'attr' => 'min="1"']),
 				$this->settingsOption('number', 'maxLogFiles', ['label' => 'Maximum Log Files', 'help' => 'Number of log files to preserve', 'attr' => 'min="1"']),
 				$this->settingsOption('select', 'logLiveUpdateRefresh', ['label' => 'Live Update Refresh', 'options' => $this->timeOptions()]),
 				$this->settingsOption('select', 'logLiveUpdateRefresh', ['label' => 'Live Update Refresh', 'options' => $this->timeOptions()]),
 				$this->settingsOption('select', 'logPageSize', ['label' => 'Log Page Size', 'options' => [['name' => '10 Items', 'value' => '10'], ['name' => '25 Items', 'value' => '25'], ['name' => '50 Items', 'value' => '50'], ['name' => '100 Items', 'value' => '100']]]),
 				$this->settingsOption('select', 'logPageSize', ['label' => 'Log Page Size', 'options' => [['name' => '10 Items', 'value' => '10'], ['name' => '25 Items', 'value' => '25'], ['name' => '50 Items', 'value' => '50'], ['name' => '100 Items', 'value' => '100']]]),
+				$this->settingsOption('switch', 'sendLogsToSlack', ['label' => 'Send Logs to Slack', 'help' => 'Send Logs to Slack as well']),
+				$this->settingsOption('select', 'slackLogLevel', ['label' => 'Slack Log Level', 'options' => $this->logLevels()]),
+				$this->settingsOption('url', 'slackLogWebhook', ['label' => 'Slack Webhook URL', 'help' => 'If using Discord make sure to end the URL with /slack']),
+				$this->settingsOption('input', 'slackLogWebHookChannel', ['label' => 'Slack Channel for Webhook', 'help' => 'Channel ID for webhook - Not needed for Discord']),
+				$this->settingsOption('blank'),
+				$this->settingsOption('test', 'slack-logs', ['label' => 'Test Slack', 'text' => 'Test Slack', 'help' => 'Test only sends a warning message so make sure Slack Log Level is Warning when testing']),
 			],
 			],
 			'Cron' => [
 			'Cron' => [
 				$this->settingsOption('cron-file'),
 				$this->settingsOption('cron-file'),
 				$this->settingsOption('blank'),
 				$this->settingsOption('blank'),
 				$this->settingsOption('enable', 'autoUpdateCronEnabled', ['label' => 'Auto-Update Organizr']),
 				$this->settingsOption('enable', 'autoUpdateCronEnabled', ['label' => 'Auto-Update Organizr']),
 				$this->settingsOption('cron', 'autoUpdateCronSchedule'),
 				$this->settingsOption('cron', 'autoUpdateCronSchedule'),
+				$this->settingsOption('enable', 'autoBackupCronEnabled', ['label' => 'Auto-Backup Organizr']),
+				$this->settingsOption('cron', 'autoBackupCronSchedule'),
+				$this->settingsOption('number', 'keepBackupsCountCron', ['label' => '# Backups Keep', 'help' => 'Number of backups to keep', 'attr' => 'min="1"']),
+				$this->settingsOption('blank'),
+
 			],
 			],
 			'Login' => [
 			'Login' => [
 				$this->settingsOption('password', 'registrationPassword', ['label' => 'Registration Password', 'help' => 'Sets the password for the Registration form on the login screen']),
 				$this->settingsOption('password', 'registrationPassword', ['label' => 'Registration Password', 'help' => 'Sets the password for the Registration form on the login screen']),
@@ -2506,6 +2600,7 @@ class Organizr
 				$this->settingsOption('blank'),
 				$this->settingsOption('blank'),
 				$this->settingsOption('username', 'komgaFallbackUser', ['label' => 'Komga Fallback Email', 'help' => 'DO NOT SET THIS TO YOUR ADMIN ACCOUNT. We recommend you create a local account as a "catch all" for when Organizr is unable to perform SSO.  Organizr will request a User Token based off of this user credentials']),
 				$this->settingsOption('username', 'komgaFallbackUser', ['label' => 'Komga Fallback Email', 'help' => 'DO NOT SET THIS TO YOUR ADMIN ACCOUNT. We recommend you create a local account as a "catch all" for when Organizr is unable to perform SSO.  Organizr will request a User Token based off of this user credentials']),
 				$this->settingsOption('password', 'komgaFallbackPassword', ['label' => 'Komga Fallback Password']),
 				$this->settingsOption('password', 'komgaFallbackPassword', ['label' => 'Komga Fallback Password']),
+				$this->settingsOption('password', 'komgaSSOMasterPassword', ['label' => 'Komga Master Password', 'help' => 'Sets master password if using oAuth backend - This will set the password on the login form for logins using oAuth where no password is supplied.']),
 			],
 			],
 		];
 		];
 	}
 	}
@@ -2669,7 +2764,8 @@ class Organizr
 			$this->setAPIResponse('error', 'No data submitted', 409);
 			$this->setAPIResponse('error', 'No data submitted', 409);
 			return false;
 			return false;
 		}
 		}
-		$newItem = array();
+		$newItems = [];
+		$updatedItems = [];
 		foreach ($array as $k => $v) {
 		foreach ($array as $k => $v) {
 			$v = $v ?? '';
 			$v = $v ?? '';
 			switch ($v) {
 			switch ($v) {
@@ -2699,14 +2795,16 @@ class Organizr
 					break;
 					break;
 			}
 			}
 			if (strtolower($k) !== 'formkey') {
 			if (strtolower($k) !== 'formkey') {
-				$newItem[$k] = $v;
+				if ($this->config[$k] !== $v) {
+					$updatedItems[$k] = $v;
+				}
+				$newItems[$k] = $v;
 				$this->config[$k] = $v;
 				$this->config[$k] = $v;
 			}
 			}
 		}
 		}
 		$this->setAPIResponse('success', 'Config items updated', 200);
 		$this->setAPIResponse('success', 'Config items updated', 200);
-		$this->setLoggerChannel('Config');
-		$this->logger->info('Config items updated', array_keys($array));
-		return (bool)$this->updateConfig($newItem);
+		$this->setLoggerChannel('Config')->notice('Config items updated', ['items' => array_keys($updatedItems)]);
+		return (bool)$this->updateConfig($newItems);
 	}
 	}
 
 
 	public function updateConfigItem($array)
 	public function updateConfigItem($array)
@@ -3202,6 +3300,8 @@ class Organizr
 					`default`	INTEGER,
 					`default`	INTEGER,
 					`enabled`	INTEGER,
 					`enabled`	INTEGER,
 					`group_id`	INTEGER,
 					`group_id`	INTEGER,
+					`group_id_max` INTEGER DEFAULT \'0\',
+					`add_to_admin`	INTEGER DEFAULT \'0\',
 					`image`	TEXT,
 					`image`	TEXT,
 					`type`	INTEGER,
 					`type`	INTEGER,
 					`splash`	INTEGER,
 					`splash`	INTEGER,
@@ -3447,30 +3547,39 @@ class Organizr
 		$days = ($days > 365) ? 365 : $days;
 		$days = ($days > 365) ? 365 : $days;
 		//Quick get user ID
 		//Quick get user ID
 		$result = $this->getUserByUsernameAndEmail($username, $email);
 		$result = $this->getUserByUsernameAndEmail($username, $email);
-		// Create JWT
-		// Set key
-		// SHA256 Encryption
-		$signer = new Lcobucci\JWT\Signer\Hmac\Sha256();
-		// Start Builder
-		$jwttoken = (new Lcobucci\JWT\Builder())->issuedBy('Organizr')// Configures the issuer (iss claim)
-		->permittedFor('Organizr')// Configures the audience (aud claim)
-		->identifiedBy('4f1g23a12aa', true)// Configures the id (jti claim), replicating as a header item
-		->issuedAt(time())// Configures the time that the token was issue (iat claim)
-		->expiresAt(time() + (86400 * $days))// Configures the expiration time of the token (exp claim)
-		->withClaim('name', $result['username'])// Configures a new claim, called "name"
-		->withClaim('group', $result['group'])// Configures a new claim, called "group"
-		->withClaim('groupID', $result['group_id'])// Configures a new claim, called "groupID"
-		->withClaim('email', $result['email'])// Configures a new claim, called "email"
-		->withClaim('image', $result['image'])// Configures a new claim, called "image"
-		->withClaim('userID', $result['id'])// Configures a new claim, called "image"
-		->sign($signer, $this->config['organizrHash'])// creates a signature using "testing" as key
-		->getToken(); // Retrieves the generated token
-		$jwttoken->getHeaders(); // Retrieves the token headers
-		$jwttoken->getClaims(); // Retrieves the token claims
-		$this->coookie('set', $this->cookieName, $jwttoken, $days);
+		$config = $this->configToken();
+		assert($config instanceof Lcobucci\JWT\Configuration);
+		$now = new DateTimeImmutable();
+		$token = $config->builder()
+			// Configures the issuer (iss claim)
+			->issuedBy('Organizr')
+			// Configures the audience (aud claim)
+			->permittedFor('Organizr')
+			// Configures the id (jti claim)
+			->identifiedBy('4f1g23a12aa')
+			// Configures the time that the token was issue (iat claim)
+			->issuedAt($now)
+			// Configures the time that the token can be used (nbf claim)
+			->canOnlyBeUsedAfter($now)
+			// Configures the expiration time of the token (exp claim)
+			->expiresAt($now->modify('+' . $days . ' days'))
+			// Configures a new claim, called "uid"
+			->withClaim('name', $result['username'])// Configures a new claim, called "name"
+			->withClaim('group', $result['group'])// Configures a new claim, called "group"
+			->withClaim('groupID', $result['group_id'])// Configures a new claim, called "groupID"
+			->withClaim('email', $result['email'])// Configures a new claim, called "email"
+			->withClaim('image', $result['image'])// Configures a new claim, called "image"
+			->withClaim('userID', $result['id'])// Configures a new claim, called "image"
+			// Configures a new header, called "foo"
+			//->withHeader('foo', 'bar')
+			// Builds a new token
+			->getToken($config->signer(), $config->signingKey());
+		//$token->headers(); // Retrieves the token headers
+		//$token->claims(); // Retrieves the token claims
+		$this->coookie('set', $this->cookieName, $token->toString(), $days);
 		// Add token to DB
 		// Add token to DB
 		$addToken = [
 		$addToken = [
-			'token' => (string)$jwttoken,
+			'token' => $token->toString(),
 			'user_id' => $result['id'],
 			'user_id' => $result['id'],
 			'created' => gmdate('Y-m-d H:i:s'),
 			'created' => gmdate('Y-m-d H:i:s'),
 			'browser' => $_SERVER ['HTTP_USER_AGENT'] ?? null,
 			'browser' => $_SERVER ['HTTP_USER_AGENT'] ?? null,
@@ -3486,14 +3595,14 @@ class Organizr
 				)
 				)
 			),
 			),
 		];
 		];
-		$token = $this->processQueries($response);
-		if ($jwttoken) {
+		$this->processQueries($response);
+		if ($token) {
 			$this->logger->debug('Token has been created');
 			$this->logger->debug('Token has been created');
 		} else {
 		} else {
 			$this->logger->warning('Token creation error');
 			$this->logger->warning('Token creation error');
 		}
 		}
 		$this->logger->debug('Token creation function has finished');
 		$this->logger->debug('Token creation function has finished');
-		return $jwttoken;
+		return $token->toString();
 	}
 	}
 
 
 	public function login($array)
 	public function login($array)
@@ -3526,9 +3635,9 @@ class Organizr
 		}
 		}
 		// Check if Auth Proxy is enabled
 		// Check if Auth Proxy is enabled
 		if ($this->config['authProxyEnabled'] && ($this->config['authProxyHeaderName'] !== '' || $this->config['authProxyHeaderNameEmail'] !== '') && $this->config['authProxyWhitelist'] !== '') {
 		if ($this->config['authProxyEnabled'] && ($this->config['authProxyHeaderName'] !== '' || $this->config['authProxyHeaderNameEmail'] !== '') && $this->config['authProxyWhitelist'] !== '') {
-			if (isset($this->getallheaders()[$this->config['authProxyHeaderName']]) || isset($this->getallheaders()[$this->config['authProxyHeaderNameEmail']])) {
-				$usernameHeader = $this->getallheaders()[$this->config['authProxyHeaderName']] ?? null;
-				$emailHeader = $this->getallheaders()[$this->config['authProxyHeaderNameEmail']] ?? null;
+			if (isset($this->getallheadersi()[strtolower($this->config['authProxyHeaderName'])]) || isset($this->getallheadersi()[strtolower($this->config['authProxyHeaderNameEmail'])])) {
+				$usernameHeader = $this->getallheadersi()[strtolower($this->config['authProxyHeaderName'])] ?? null;
+				$emailHeader = $this->getallheadersi()[strtolower($this->config['authProxyHeaderNameEmail'])] ?? null;
 				$headerForLogin = $usernameHeader ?: ($emailHeader ?: null);
 				$headerForLogin = $usernameHeader ?: ($emailHeader ?: null);
 				$this->setLoggerChannel('Authentication', $headerForLogin);
 				$this->setLoggerChannel('Authentication', $headerForLogin);
 				$this->logger->debug('Starting Auth Proxy verification');
 				$this->logger->debug('Starting Auth Proxy verification');
@@ -3953,11 +4062,19 @@ class Organizr
 			array(
 			array(
 				'function' => 'fetchAll',
 				'function' => 'fetchAll',
 				'query' => array(
 				'query' => array(
-					'SELECT * FROM tabs WHERE `group_id` >= ? AND `enabled` = 1 ORDER BY `order` ' . $sort,
-					$this->user['groupID']
+					'SELECT * FROM tabs WHERE `group_id` >= ? AND `group_id_max` <= ? AND `enabled` = 1 ORDER BY `order` ' . $sort,
+					$this->user['groupID'],
+					$this->user['groupID'],
 				),
 				),
 				'key' => 'tabs'
 				'key' => 'tabs'
 			),
 			),
+			array(
+				'function' => 'fetchAll',
+				'query' => array(
+					'SELECT * FROM tabs WHERE `add_to_admin` = 1 AND `enabled` = 1 ORDER BY `order` ' . $sort
+				),
+				'key' => 'tabs-admin'
+			),
 			array(
 			array(
 				'function' => 'fetchAll',
 				'function' => 'fetchAll',
 				'query' => array(
 				'query' => array(
@@ -3968,15 +4085,40 @@ class Organizr
 		];
 		];
 		$queries = $this->processQueries($response);
 		$queries = $this->processQueries($response);
 		$this->applyTabVariables($queries['tabs']);
 		$this->applyTabVariables($queries['tabs']);
-		$all['tabs'] = $queries['tabs'];
-		foreach ($queries['tabs'] as $k => $v) {
+		if ($this->qualifyRequest(1)) {
+			$this->applyTabVariables($queries['tabs-admin']);
+			$all['tabs'] = array_merge($queries['tabs'], $queries['tabs-admin']);
+			$newArray = [];
+			$ids = [];
+			foreach ($all['tabs'] as $key => $line) {
+				if (!in_array($line['id'], $ids)) {
+					$ids[] = $line['id'];
+					$newArray[$key] = $line;
+				}
+			}
+			$all['tabs'] = $newArray;
+			if (count($all['tabs']) > 0) {
+				usort($all['tabs'], function ($a, $b) {
+					if ($this->config['unsortedTabs'] == 'top') {
+						return $b['order'] <=> $a['order'];
+					} else {
+						return $a['order'] <=> $b['order'];
+					}
+				});
+			}
+			$newArray = NULL;
+			$ids = NULL;
+		} else {
+			$all['tabs'] = $queries['tabs'];
+		}
+		foreach ($all['tabs'] as $k => $v) {
 			$v['url_local'] = $v['type'] !== 0 ? $this->checkTabURL($v['url_local']) : $v['url_local'];
 			$v['url_local'] = $v['type'] !== 0 ? $this->checkTabURL($v['url_local']) : $v['url_local'];
 			$v['url'] = $v['type'] !== 0 ? $this->checkTabURL($v['url']) : $v['url'];
 			$v['url'] = $v['type'] !== 0 ? $this->checkTabURL($v['url']) : $v['url'];
 			$v['access_url'] = (!empty($v['url_local']) && ($v['url_local'] !== null) && ($v['url_local'] !== 'null') && $this->isLocal() && $v['type'] !== 0) ? $v['url_local'] : $v['url'];
 			$v['access_url'] = (!empty($v['url_local']) && ($v['url_local'] !== null) && ($v['url_local'] !== 'null') && $this->isLocal() && $v['type'] !== 0) ? $v['url_local'] : $v['url'];
 		}
 		}
 		$count = array_map(function ($element) {
 		$count = array_map(function ($element) {
 			return $element['category_id'];
 			return $element['category_id'];
-		}, $queries['tabs']);
+		}, $all['tabs']);
 		$count = (array_count_values($count));
 		$count = (array_count_values($count));
 		foreach ($queries['categories'] as $k => $v) {
 		foreach ($queries['categories'] as $k => $v) {
 			$v['count'] = $count[$v['category_id']] ?? 0;
 			$v['count'] = $count[$v['category_id']] ?? 0;
@@ -4000,9 +4142,15 @@ class Organizr
 	public function refreshList()
 	public function refreshList()
 	{
 	{
 		$searchTerm = "Refresh";
 		$searchTerm = "Refresh";
-		return array_filter($this->config, function ($k) use ($searchTerm) {
+		$list = array_filter($this->config, function ($k) use ($searchTerm) {
 			return stripos($k, $searchTerm) !== false;
 			return stripos($k, $searchTerm) !== false;
 		}, ARRAY_FILTER_USE_KEY);
 		}, ARRAY_FILTER_USE_KEY);
+		foreach ($list as $item => $value) {
+			if (!is_numeric($value)) {
+				unset($list[$item]);
+			}
+		}
+		return $list;
 	}
 	}
 
 
 	public function homepageOrderList()
 	public function homepageOrderList()
@@ -4222,40 +4370,14 @@ class Organizr
 		}
 		}
 	}
 	}
 
 
-	public function writeLog($type = 'error', $message = null, $username = null)
-	{
-		$this->timeExecution = $this->timeExecution($this->timeExecution);
-		$message = $message . ' [Execution Time: ' . $this->formatSeconds($this->timeExecution) . ']';
-		$username = ($username) ? htmlspecialchars($username, ENT_QUOTES) : $this->user['username'] ?? 'SYSTEM';
-		if ($this->checkLog($this->organizrLog)) {
-			$getLog = str_replace("\r\ndate", "date", file_get_contents($this->organizrLog));
-			$gotLog = json_decode($getLog, true);
-		}
-		$logEntryFirst = array('logType' => 'organizr_log', 'log_items' => array(array('date' => date("Y-m-d H:i:s"), 'utc_date' => $this->currentTime, 'type' => $type, 'username' => $username, 'ip' => $this->userIP(), 'message' => $message)));
-		$logEntry = array('date' => date("Y-m-d H:i:s"), 'utc_date' => $this->currentTime, 'type' => $type, 'username' => $username, 'ip' => $this->userIP(), 'message' => $message);
-		if (isset($gotLog)) {
-			array_push($gotLog["log_items"], $logEntry);
-			$writeFailLog = str_replace("date", "\r\ndate", json_encode($gotLog));
-		} else {
-			$writeFailLog = str_replace("date", "\r\ndate", json_encode($logEntryFirst));
-		}
-		file_put_contents($this->organizrLog, $writeFailLog);
-	}
-
 	public function isApprovedRequest($method, $data)
 	public function isApprovedRequest($method, $data)
 	{
 	{
-		$requesterToken = $this->getallheaders()['Token'] ?? ($_GET['apikey'] ?? false);
+		$requesterToken = $this->getallheadersi()['token'] ?? ($_GET['apikey'] ?? false);
 		$apiKey = ($this->config['organizrAPI']) ?? null;
 		$apiKey = ($this->config['organizrAPI']) ?? null;
 		if (isset($data['formKey'])) {
 		if (isset($data['formKey'])) {
 			$formKey = $data['formKey'];
 			$formKey = $data['formKey'];
-		} elseif (isset($this->getallheaders()['Formkey'])) {
-			$formKey = $this->getallheaders()['Formkey'];
-		} elseif (isset($this->getallheaders()['formkey'])) {
-			$formKey = $this->getallheaders()['formkey'];
-		} elseif (isset($this->getallheaders()['formKey'])) {
-			$formKey = $this->getallheaders()['formKey'];
-		} elseif (isset($this->getallheaders()['FormKey'])) {
-			$formKey = $this->getallheaders()['FormKey'];
+		} elseif (isset($this->getallheadersi()['formkey'])) {
+			$formKey = $this->getallheadersi()['formkey'];
 		} else {
 		} else {
 			$formKey = false;
 			$formKey = false;
 		}
 		}
@@ -5013,6 +5135,9 @@ class Organizr
 				$this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409);
 				$this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409);
 				return false;
 				return false;
 			}
 			}
+			if (!$this->qualifyLength($array['name'], 50, true)) {
+				return false;
+			}
 		} else {
 		} else {
 			$this->setAPIResponse('error', 'Tab name was not supplied', 422);
 			$this->setAPIResponse('error', 'Tab name was not supplied', 422);
 			return false;
 			return false;
@@ -5024,6 +5149,8 @@ class Organizr
 		if (!array_key_exists('image', $array)) {
 		if (!array_key_exists('image', $array)) {
 			$this->setAPIResponse('error', 'Tab image was not supplied', 422);
 			$this->setAPIResponse('error', 'Tab image was not supplied', 422);
 			return false;
 			return false;
+		} else {
+			$array['image'] = $this->sanitizeUserString($array['image']);
 		}
 		}
 		$response = [
 		$response = [
 			array(
 			array(
@@ -5063,12 +5190,32 @@ class Organizr
 				$this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409);
 				$this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409);
 				return false;
 				return false;
 			}
 			}
+			if (!$this->qualifyLength($array['name'], 50, true)) {
+				return false;
+			}
 		}
 		}
 		if (array_key_exists('default', $array)) {
 		if (array_key_exists('default', $array)) {
 			if ($array['default']) {
 			if ($array['default']) {
 				$this->clearTabDefault();
 				$this->clearTabDefault();
 			}
 			}
 		}
 		}
+		if (array_key_exists('image', $array)) {
+			$array['image'] = $this->sanitizeUserString($array['image']);
+		}
+		if (array_key_exists('group_id', $array)) {
+			$groupCheck = (array_key_exists('group_id_max', $array)) ? $array['group_id_max'] : $tabInfo['group_id_max'];
+			if ($array['group_id'] < $groupCheck) {
+				$this->setAPIResponse('error', 'Tab name: ' . $tabInfo['name'] . ' cannot have a lower Group Id Max than Group Id Min', 409);
+				return false;
+			}
+		}
+		if (array_key_exists('group_id_max', $array)) {
+			$groupCheck = (array_key_exists('group_id', $array)) ? $array['group_id'] : $tabInfo['group_id'];
+			if ($array['group_id_max'] > $groupCheck) {
+				$this->setAPIResponse('error', 'Tab name: ' . $tabInfo['name'] . ' cannot have a higher Group Id Min than Group Id Max', 409);
+				return false;
+			}
+		}
 		$response = [
 		$response = [
 			array(
 			array(
 				'function' => 'query',
 				'function' => 'query',
@@ -5135,6 +5282,9 @@ class Organizr
 				$this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409);
 				$this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409);
 				return false;
 				return false;
 			}
 			}
+			if (!$this->qualifyLength($array['category'], 50, true)) {
+				return false;
+			}
 		} else {
 		} else {
 			$this->setAPIResponse('error', 'Category name was not supplied', 422);
 			$this->setAPIResponse('error', 'Category name was not supplied', 422);
 			return false;
 			return false;
@@ -5183,6 +5333,9 @@ class Organizr
 				$this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409);
 				$this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409);
 				return false;
 				return false;
 			}
 			}
+			if (!$this->qualifyLength($array['category'], 50, true)) {
+				return false;
+			}
 		}
 		}
 		if (array_key_exists('image', $array)) {
 		if (array_key_exists('image', $array)) {
 			$array['image'] = $this->sanitizeUserString($array['image']);
 			$array['image'] = $this->sanitizeUserString($array['image']);
@@ -6374,6 +6527,9 @@ class Organizr
 				$this->setAPIResponse('error', 'Username: ' . $array['username'] . ' is already taken', 409);
 				$this->setAPIResponse('error', 'Username: ' . $array['username'] . ' is already taken', 409);
 				return false;
 				return false;
 			}
 			}
+			if (!$this->qualifyLength($array['username'], 50, true)) {
+				return false;
+			}
 		}
 		}
 		if (array_key_exists('email', $array)) {
 		if (array_key_exists('email', $array)) {
 			if ($array['email'] == '') {
 			if ($array['email'] == '') {
@@ -6390,6 +6546,9 @@ class Organizr
 				$this->setAPIResponse('error', 'Email: ' . $array['email'] . ' is already taken', 409);
 				$this->setAPIResponse('error', 'Email: ' . $array['email'] . ' is already taken', 409);
 				return false;
 				return false;
 			}
 			}
+			if (!$this->qualifyLength($array['email'], 50, true)) {
+				return false;
+			}
 		}
 		}
 		if (array_key_exists('group_id', $array)) {
 		if (array_key_exists('group_id', $array)) {
 			if ($array['group_id'] == '') {
 			if ($array['group_id'] == '') {
@@ -6509,6 +6668,15 @@ class Organizr
 			$this->setResponse(409, 'Email is not a valid email', ['email' => $email]);
 			$this->setResponse(409, 'Email is not a valid email', ['email' => $email]);
 			return false;
 			return false;
 		}
 		}
+		if (!$this->qualifyLength($username, 50, true)) {
+			return false;
+		}
+		if (!$this->qualifyLength($email, 50, true)) {
+			return false;
+		}
+		if (!$this->qualifyLength($password, 200, true)) {
+			return false;
+		}
 		$this->setLoggerChannel('User Management');
 		$this->setLoggerChannel('User Management');
 		if ($this->createUser($username, $password, $email)) {
 		if ($this->createUser($username, $password, $email)) {
 			$this->logger->info('Account created for [' . $username . ']');
 			$this->logger->info('Account created for [' . $username . ']');
@@ -6547,6 +6715,15 @@ class Organizr
 			$this->setAPIResponse('error', 'Username: ' . $username . ' or Email: ' . $email . ' is already taken', 409);
 			$this->setAPIResponse('error', 'Username: ' . $username . ' or Email: ' . $email . ' is already taken', 409);
 			return false;
 			return false;
 		}
 		}
+		if (!$this->qualifyLength($username, 50, true)) {
+			return false;
+		}
+		if (!$this->qualifyLength($email, 50, true)) {
+			return false;
+		}
+		if (!$this->qualifyLength($password, 200, true)) {
+			return false;
+		}
 		$defaults = $this->getDefaultGroup();
 		$defaults = $this->getDefaultGroup();
 		$userInfo = [
 		$userInfo = [
 			'username' => $username,
 			'username' => $username,
@@ -6601,12 +6778,16 @@ class Organizr
 				$this->setAPIResponse('error', 'Group name: ' . $array['group'] . ' is already taken', 409);
 				$this->setAPIResponse('error', 'Group name: ' . $array['group'] . ' is already taken', 409);
 				return false;
 				return false;
 			}
 			}
+			if (!$this->qualifyLength($array['group'], 50, true)) {
+				return false;
+			}
 		}
 		}
 		if (array_key_exists('image', $array)) {
 		if (array_key_exists('image', $array)) {
 			if ($array['image'] == '') {
 			if ($array['image'] == '') {
 				$this->setAPIResponse('error', 'Image was set but empty', 409);
 				$this->setAPIResponse('error', 'Image was set but empty', 409);
 				return false;
 				return false;
 			}
 			}
+			$array['image'] = $this->sanitizeUserString($array['image']);
 		}
 		}
 		if (array_key_exists('default', $array)) {
 		if (array_key_exists('default', $array)) {
 			if ($groupInfo['group_id'] == 0 || $groupInfo['group_id'] == 999) {
 			if ($groupInfo['group_id'] == 0 || $groupInfo['group_id'] == 999) {
@@ -6681,6 +6862,9 @@ class Organizr
 				$this->setAPIResponse('error', 'Group name: ' . $array['group'] . ' is already taken', 409);
 				$this->setAPIResponse('error', 'Group name: ' . $array['group'] . ' is already taken', 409);
 				return false;
 				return false;
 			}
 			}
+			if (!$this->qualifyLength($array['group'], 50, true)) {
+				return false;
+			}
 		} else {
 		} else {
 			$this->setAPIResponse('error', 'Group name was not supplied', 422);
 			$this->setAPIResponse('error', 'Group name was not supplied', 422);
 			return false;
 			return false;
@@ -6690,6 +6874,7 @@ class Organizr
 				$this->setAPIResponse('error', 'Group image cannot be empty', 422);
 				$this->setAPIResponse('error', 'Group image cannot be empty', 422);
 				return false;
 				return false;
 			}
 			}
+			$array['image'] = $this->sanitizeUserString($array['image']);
 		} else {
 		} else {
 			$this->setAPIResponse('error', 'Group image was not supplied', 422);
 			$this->setAPIResponse('error', 'Group image was not supplied', 422);
 			return false;
 			return false;
@@ -7377,13 +7562,18 @@ class Organizr
 						);
 						);
 					}
 					}
 				}
 				}
-				$this->setAPIResponse('success', null, 200, $items);
+				$this->setResponse(200, null, $items);
 				return $items;
 				return $items;
+			} else {
+				$message = $this->testAndFormatString($response->body);
+				$this->setResponse(500, 'Plex Error occurred', $message['data']);
+				$this->setLoggerChannel('Plex Connection')->warning('Plex Error', $message);
+				return $message;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->setLoggerChannel('Plex Connection');
-			$this->logger->error($e);
+			$this->setLoggerChannel('Plex Connection')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
+			return false;
 		}
 		}
 	}
 	}
 
 
@@ -7401,7 +7591,11 @@ class Organizr
 		$iconListing = json_decode($allIcons, true);
 		$iconListing = json_decode($allIcons, true);
 		foreach ($iconListing as $setKey => $set) {
 		foreach ($iconListing as $setKey => $set) {
 			foreach ($set['children'] as $k => $v) {
 			foreach ($set['children'] as $k => $v) {
-				if (stripos($v['text'], $term) !== false || !$term) {
+				if ($term) {
+					if (stripos($v['text'], $term) !== false) {
+						$goodIcons['results'][] = $v;
+					}
+				} else {
 					$goodIcons['results'][] = $v;
 					$goodIcons['results'][] = $v;
 				}
 				}
 			}
 			}

+ 3 - 2
api/composer.json

@@ -1,7 +1,7 @@
 {
 {
   "require": {
   "require": {
     "dibi/dibi": "^4.2",
     "dibi/dibi": "^4.2",
-    "lcobucci/jwt": "3.3.1",
+    "lcobucci/jwt": "^4.1",
     "composer/semver": "^1.4",
     "composer/semver": "^1.4",
     "phpmailer/phpmailer": "^6.2",
     "phpmailer/phpmailer": "^6.2",
     "rmccue/requests": "^1.7",
     "rmccue/requests": "^1.7",
@@ -21,6 +21,7 @@
     "bcremer/line-reader": "^1.1",
     "bcremer/line-reader": "^1.1",
     "peppeocchi/php-cron-scheduler": "^4.0",
     "peppeocchi/php-cron-scheduler": "^4.0",
     "simshaun/recurr": "^5.0",
     "simshaun/recurr": "^5.0",
-    "stripe/stripe-php": "^7.116"
+    "stripe/stripe-php": "^7.116",
+    "ramsey/uuid": "^4.2"
   }
   }
 }
 }

+ 355 - 68
api/composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
         "This file is @generated automatically"
     ],
     ],
-    "content-hash": "3f2f3b854484a2014e64d2327a1b8c29",
+    "content-hash": "ca4f53441305ffa3e09c532006fa9c19",
     "packages": [
     "packages": [
         {
         {
             "name": "adldap2/adldap2",
             "name": "adldap2/adldap2",
@@ -170,6 +170,66 @@
             },
             },
             "time": "2017-02-26T18:30:14+00:00"
             "time": "2017-02-26T18:30:14+00:00"
         },
         },
+        {
+            "name": "brick/math",
+            "version": "0.9.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/brick/math.git",
+                "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/brick/math/zipball/ca57d18f028f84f777b2168cd1911b0dee2343ae",
+                "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "php": "^7.1 || ^8.0"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "^2.2",
+                "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0",
+                "vimeo/psalm": "4.9.2"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Brick\\Math\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Arbitrary-precision arithmetic library",
+            "keywords": [
+                "Arbitrary-precision",
+                "BigInteger",
+                "BigRational",
+                "arithmetic",
+                "bigdecimal",
+                "bignum",
+                "brick",
+                "math"
+            ],
+            "support": {
+                "issues": "https://github.com/brick/math/issues",
+                "source": "https://github.com/brick/math/tree/0.9.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/BenMorel",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/brick/math",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-08-15T20:50:18+00:00"
+        },
         {
         {
             "name": "composer/semver",
             "name": "composer/semver",
             "version": "1.7.2",
             "version": "1.7.2",
@@ -1018,38 +1078,104 @@
             "description": "PHP Sonarr API Wrapper",
             "description": "PHP Sonarr API Wrapper",
             "time": "2017-06-30T01:25:49+00:00"
             "time": "2017-06-30T01:25:49+00:00"
         },
         },
+        {
+            "name": "lcobucci/clock",
+            "version": "2.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/lcobucci/clock.git",
+                "reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/lcobucci/clock/zipball/353d83fe2e6ae95745b16b3d911813df6a05bfb3",
+                "reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.4 || ^8.0"
+            },
+            "require-dev": {
+                "infection/infection": "^0.17",
+                "lcobucci/coding-standard": "^6.0",
+                "phpstan/extension-installer": "^1.0",
+                "phpstan/phpstan": "^0.12",
+                "phpstan/phpstan-deprecation-rules": "^0.12",
+                "phpstan/phpstan-phpunit": "^0.12",
+                "phpstan/phpstan-strict-rules": "^0.12",
+                "phpunit/php-code-coverage": "9.1.4",
+                "phpunit/phpunit": "9.3.7"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Lcobucci\\Clock\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Luís Cobucci",
+                    "email": "lcobucci@gmail.com"
+                }
+            ],
+            "description": "Yet another clock abstraction",
+            "support": {
+                "issues": "https://github.com/lcobucci/clock/issues",
+                "source": "https://github.com/lcobucci/clock/tree/2.0.x"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/lcobucci",
+                    "type": "github"
+                },
+                {
+                    "url": "https://www.patreon.com/lcobucci",
+                    "type": "patreon"
+                }
+            ],
+            "time": "2020-08-27T18:56:02+00:00"
+        },
         {
         {
             "name": "lcobucci/jwt",
             "name": "lcobucci/jwt",
-            "version": "3.3.1",
+            "version": "4.1.5",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/lcobucci/jwt.git",
                 "url": "https://github.com/lcobucci/jwt.git",
-                "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18"
+                "reference": "fe2d89f2eaa7087af4aa166c6f480ef04e000582"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/lcobucci/jwt/zipball/a11ec5f4b4d75d1fcd04e133dede4c317aac9e18",
-                "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18",
+                "url": "https://api.github.com/repos/lcobucci/jwt/zipball/fe2d89f2eaa7087af4aa166c6f480ef04e000582",
+                "reference": "fe2d89f2eaa7087af4aa166c6f480ef04e000582",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
+                "ext-hash": "*",
+                "ext-json": "*",
                 "ext-mbstring": "*",
                 "ext-mbstring": "*",
                 "ext-openssl": "*",
                 "ext-openssl": "*",
-                "php": "^5.6 || ^7.0"
+                "ext-sodium": "*",
+                "lcobucci/clock": "^2.0",
+                "php": "^7.4 || ^8.0"
             },
             },
             "require-dev": {
             "require-dev": {
-                "mikey179/vfsstream": "~1.5",
-                "phpmd/phpmd": "~2.2",
-                "phpunit/php-invoker": "~1.1",
-                "phpunit/phpunit": "^5.7 || ^7.3",
-                "squizlabs/php_codesniffer": "~2.3"
+                "infection/infection": "^0.21",
+                "lcobucci/coding-standard": "^6.0",
+                "mikey179/vfsstream": "^1.6.7",
+                "phpbench/phpbench": "^1.0",
+                "phpstan/extension-installer": "^1.0",
+                "phpstan/phpstan": "^0.12",
+                "phpstan/phpstan-deprecation-rules": "^0.12",
+                "phpstan/phpstan-phpunit": "^0.12",
+                "phpstan/phpstan-strict-rules": "^0.12",
+                "phpunit/php-invoker": "^3.1",
+                "phpunit/phpunit": "^9.5"
             },
             },
             "type": "library",
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.1-dev"
-                }
-            },
             "autoload": {
             "autoload": {
                 "psr-4": {
                 "psr-4": {
                     "Lcobucci\\JWT\\": "src"
                     "Lcobucci\\JWT\\": "src"
@@ -1061,7 +1187,7 @@
             ],
             ],
             "authors": [
             "authors": [
                 {
                 {
-                    "name": "Luís Otávio Cobucci Oblonczyk",
+                    "name": "Luís Cobucci",
                     "email": "lcobucci@gmail.com",
                     "email": "lcobucci@gmail.com",
                     "role": "Developer"
                     "role": "Developer"
                 }
                 }
@@ -1073,9 +1199,19 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/lcobucci/jwt/issues",
                 "issues": "https://github.com/lcobucci/jwt/issues",
-                "source": "https://github.com/lcobucci/jwt/tree/3.3"
+                "source": "https://github.com/lcobucci/jwt/tree/4.1.5"
             },
             },
-            "time": "2019-05-24T18:30:49+00:00"
+            "funding": [
+                {
+                    "url": "https://github.com/lcobucci",
+                    "type": "github"
+                },
+                {
+                    "url": "https://www.patreon.com/lcobucci",
+                    "type": "patreon"
+                }
+            ],
+            "time": "2021-09-28T19:34:56+00:00"
         },
         },
         {
         {
             "name": "league/oauth2-client",
             "name": "league/oauth2-client",
@@ -1310,7 +1446,7 @@
             "require": {
             "require": {
                 "monolog/monolog": "^1.24",
                 "monolog/monolog": "^1.24",
                 "php": "~7.1",
                 "php": "~7.1",
-                "ramsey/uuid": "^3.8"
+                "ramsey/uuid": "^4.2"
             },
             },
             "require-dev": {
             "require-dev": {
                 "friendsofphp/php-cs-fixer": "^2.14",
                 "friendsofphp/php-cs-fixer": "^2.14",
@@ -2541,88 +2677,162 @@
             },
             },
             "time": "2019-03-08T08:55:37+00:00"
             "time": "2019-03-08T08:55:37+00:00"
         },
         },
+        {
+            "name": "ramsey/collection",
+            "version": "1.2.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ramsey/collection.git",
+                "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ramsey/collection/zipball/cccc74ee5e328031b15640b51056ee8d3bb66c0a",
+                "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.3 || ^8",
+                "symfony/polyfill-php81": "^1.23"
+            },
+            "require-dev": {
+                "captainhook/captainhook": "^5.3",
+                "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
+                "ergebnis/composer-normalize": "^2.6",
+                "fakerphp/faker": "^1.5",
+                "hamcrest/hamcrest-php": "^2",
+                "jangregor/phpstan-prophecy": "^0.8",
+                "mockery/mockery": "^1.3",
+                "phpspec/prophecy-phpunit": "^2.0",
+                "phpstan/extension-installer": "^1",
+                "phpstan/phpstan": "^0.12.32",
+                "phpstan/phpstan-mockery": "^0.12.5",
+                "phpstan/phpstan-phpunit": "^0.12.11",
+                "phpunit/phpunit": "^8.5 || ^9",
+                "psy/psysh": "^0.10.4",
+                "slevomat/coding-standard": "^6.3",
+                "squizlabs/php_codesniffer": "^3.5",
+                "vimeo/psalm": "^4.4"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Ramsey\\Collection\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ben Ramsey",
+                    "email": "ben@benramsey.com",
+                    "homepage": "https://benramsey.com"
+                }
+            ],
+            "description": "A PHP library for representing and manipulating collections.",
+            "keywords": [
+                "array",
+                "collection",
+                "hash",
+                "map",
+                "queue",
+                "set"
+            ],
+            "support": {
+                "issues": "https://github.com/ramsey/collection/issues",
+                "source": "https://github.com/ramsey/collection/tree/1.2.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/ramsey",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/ramsey/collection",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-10-10T03:01:02+00:00"
+        },
         {
         {
             "name": "ramsey/uuid",
             "name": "ramsey/uuid",
-            "version": "3.9.6",
+            "version": "4.2.3",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/ramsey/uuid.git",
                 "url": "https://github.com/ramsey/uuid.git",
-                "reference": "ffa80ab953edd85d5b6c004f96181a538aad35a3"
+                "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/ramsey/uuid/zipball/ffa80ab953edd85d5b6c004f96181a538aad35a3",
-                "reference": "ffa80ab953edd85d5b6c004f96181a538aad35a3",
+                "url": "https://api.github.com/repos/ramsey/uuid/zipball/fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df",
+                "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
+                "brick/math": "^0.8 || ^0.9",
                 "ext-json": "*",
                 "ext-json": "*",
-                "paragonie/random_compat": "^1 | ^2 | ^9.99.99",
-                "php": "^5.4 | ^7.0 | ^8.0",
-                "symfony/polyfill-ctype": "^1.8"
+                "php": "^7.2 || ^8.0",
+                "ramsey/collection": "^1.0",
+                "symfony/polyfill-ctype": "^1.8",
+                "symfony/polyfill-php80": "^1.14"
             },
             },
             "replace": {
             "replace": {
                 "rhumsaa/uuid": "self.version"
                 "rhumsaa/uuid": "self.version"
             },
             },
             "require-dev": {
             "require-dev": {
-                "codeception/aspect-mock": "^1 | ^2",
-                "doctrine/annotations": "^1.2",
-                "goaop/framework": "1.0.0-alpha.2 | ^1 | >=2.1.0 <=2.3.2",
-                "mockery/mockery": "^0.9.11 | ^1",
+                "captainhook/captainhook": "^5.10",
+                "captainhook/plugin-composer": "^5.3",
+                "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
+                "doctrine/annotations": "^1.8",
+                "ergebnis/composer-normalize": "^2.15",
+                "mockery/mockery": "^1.3",
                 "moontoast/math": "^1.1",
                 "moontoast/math": "^1.1",
-                "nikic/php-parser": "<=4.5.0",
                 "paragonie/random-lib": "^2",
                 "paragonie/random-lib": "^2",
-                "php-mock/php-mock-phpunit": "^0.3 | ^1.1 | ^2.6",
-                "php-parallel-lint/php-parallel-lint": "^1.3",
-                "phpunit/phpunit": ">=4.8.36 <9.0.0 | >=9.3.0",
+                "php-mock/php-mock": "^2.2",
+                "php-mock/php-mock-mockery": "^1.3",
+                "php-parallel-lint/php-parallel-lint": "^1.1",
+                "phpbench/phpbench": "^1.0",
+                "phpstan/extension-installer": "^1.0",
+                "phpstan/phpstan": "^0.12",
+                "phpstan/phpstan-mockery": "^0.12",
+                "phpstan/phpstan-phpunit": "^0.12",
+                "phpunit/phpunit": "^8.5 || ^9",
+                "slevomat/coding-standard": "^7.0",
                 "squizlabs/php_codesniffer": "^3.5",
                 "squizlabs/php_codesniffer": "^3.5",
-                "yoast/phpunit-polyfills": "^1.0"
+                "vimeo/psalm": "^4.9"
             },
             },
             "suggest": {
             "suggest": {
-                "ext-ctype": "Provides support for PHP Ctype functions",
-                "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator",
-                "ext-openssl": "Provides the OpenSSL extension for use with the OpenSslGenerator",
-                "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator",
-                "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).",
+                "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.",
+                "ext-ctype": "Enables faster processing of character classification using ctype functions.",
+                "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.",
+                "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.",
                 "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter",
                 "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter",
-                "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid",
                 "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type."
                 "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type."
             },
             },
             "type": "library",
             "type": "library",
             "extra": {
             "extra": {
                 "branch-alias": {
                 "branch-alias": {
-                    "dev-master": "3.x-dev"
+                    "dev-main": "4.x-dev"
+                },
+                "captainhook": {
+                    "force-install": true
                 }
                 }
             },
             },
             "autoload": {
             "autoload": {
-                "psr-4": {
-                    "Ramsey\\Uuid\\": "src/"
-                },
                 "files": [
                 "files": [
                     "src/functions.php"
                     "src/functions.php"
-                ]
+                ],
+                "psr-4": {
+                    "Ramsey\\Uuid\\": "src/"
+                }
             },
             },
             "notification-url": "https://packagist.org/downloads/",
             "notification-url": "https://packagist.org/downloads/",
             "license": [
             "license": [
                 "MIT"
                 "MIT"
             ],
             ],
-            "authors": [
-                {
-                    "name": "Ben Ramsey",
-                    "email": "ben@benramsey.com",
-                    "homepage": "https://benramsey.com"
-                },
-                {
-                    "name": "Marijn Huizendveld",
-                    "email": "marijn.huizendveld@gmail.com"
-                },
-                {
-                    "name": "Thibaud Fabre",
-                    "email": "thibaud@aztech.io"
-                }
-            ],
-            "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).",
-            "homepage": "https://github.com/ramsey/uuid",
+            "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).",
             "keywords": [
             "keywords": [
                 "guid",
                 "guid",
                 "identifier",
                 "identifier",
@@ -2630,9 +2840,7 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/ramsey/uuid/issues",
                 "issues": "https://github.com/ramsey/uuid/issues",
-                "rss": "https://github.com/ramsey/uuid/releases.atom",
-                "source": "https://github.com/ramsey/uuid",
-                "wiki": "https://github.com/ramsey/uuid/wiki"
+                "source": "https://github.com/ramsey/uuid/tree/4.2.3"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -2644,7 +2852,7 @@
                     "type": "tidelift"
                     "type": "tidelift"
                 }
                 }
             ],
             ],
-            "time": "2021-09-25T23:07:42+00:00"
+            "time": "2021-09-25T23:10:38+00:00"
         },
         },
         {
         {
             "name": "rmccue/requests",
             "name": "rmccue/requests",
@@ -3493,6 +3701,85 @@
             ],
             ],
             "time": "2021-01-07T16:49:33+00:00"
             "time": "2021-01-07T16:49:33+00:00"
         },
         },
+        {
+            "name": "symfony/polyfill-php81",
+            "version": "v1.25.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php81.git",
+                "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f",
+                "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.23-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php81\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php81/tree/v1.25.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-09-13T13:58:11+00:00"
+        },
         {
         {
             "name": "symfony/polyfill-util",
             "name": "symfony/polyfill-util",
             "version": "v1.9.0",
             "version": "v1.9.0",

+ 11 - 1
api/config/default.php

@@ -87,6 +87,7 @@ return [
 	'komgaURL' => '',
 	'komgaURL' => '',
 	'komgaFallbackUser' => '',
 	'komgaFallbackUser' => '',
 	'komgaFallbackPassword' => '',
 	'komgaFallbackPassword' => '',
+	'komgaSSOMasterPassword' => '',
 	'sonarrURL' => '',
 	'sonarrURL' => '',
 	'sonarrUnmonitored' => false,
 	'sonarrUnmonitored' => false,
 	'sonarrIcon' => true,
 	'sonarrIcon' => true,
@@ -659,5 +660,14 @@ return [
 	'checkForThemeUpdate' => true,
 	'checkForThemeUpdate' => true,
 	'autoUpdateCronEnabled' => false,
 	'autoUpdateCronEnabled' => false,
 	'autoUpdateCronSchedule' => '@weekly',
 	'autoUpdateCronSchedule' => '@weekly',
-	'useRandomMediaImage' => false
+	'autoBackupCronEnabled' => false,
+	'autoBackupCronSchedule' => '@weekly',
+	'keepBackupsCountCron' => '20',
+	'useRandomMediaImage' => false,
+	'sendLogsToSlack' => false,
+	'slackLogLevel' => 'WARNING',
+	'slackLogWebhook' => '',
+	'slackLogWebHookChannel' => '',
+	'matchUserAgents' => false,
+	'matchUserIP' => false
 ];
 ];

+ 20 - 12
api/functions.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Set UTC timeone
+// Set UTC timezone
 date_default_timezone_set("UTC");
 date_default_timezone_set("UTC");
 // Autoload frameworks
 // Autoload frameworks
 require_once(__DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php');
 require_once(__DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php');
@@ -25,17 +25,8 @@ if (file_exists(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_S
 }
 }
 
 
 // Include all plugin files
 // Include all plugin files
-$folder = __DIR__ . DIRECTORY_SEPARATOR . 'plugins';
-$directoryIterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS);
-$iteratorIterator = new RecursiveIteratorIterator($directoryIterator);
-foreach ($iteratorIterator as $info) {
-	if ($info->getFilename() == 'plugin.php' || strpos($info->getFilename(), 'page.php') !== false || $info->getFilename() == 'cron.php') {
-		require_once $info->getPathname();
-	}
-}
-// Include all custom plugin files
-if (file_exists(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'plugins')) {
-	$folder = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'plugins';
+try {
+	$folder = __DIR__ . DIRECTORY_SEPARATOR . 'plugins';
 	$directoryIterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS);
 	$directoryIterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS);
 	$iteratorIterator = new RecursiveIteratorIterator($directoryIterator);
 	$iteratorIterator = new RecursiveIteratorIterator($directoryIterator);
 	foreach ($iteratorIterator as $info) {
 	foreach ($iteratorIterator as $info) {
@@ -43,4 +34,21 @@ if (file_exists(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_S
 			require_once $info->getPathname();
 			require_once $info->getPathname();
 		}
 		}
 	}
 	}
+} catch (UnexpectedValueException $e) {
+	// Folder doesn't exist or permission denied
+}
+// Include all custom plugin files
+try {
+	if (file_exists(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'plugins')) {
+		$folder = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'plugins';
+		$directoryIterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS);
+		$iteratorIterator = new RecursiveIteratorIterator($directoryIterator);
+		foreach ($iteratorIterator as $info) {
+			if ($info->getFilename() == 'plugin.php' || strpos($info->getFilename(), 'page.php') !== false || $info->getFilename() == 'cron.php') {
+				require_once $info->getPathname();
+			}
+		}
+	}
+} catch (UnexpectedValueException $e) {
+	// Permission denied
 }
 }

+ 2 - 2
api/functions/2fa-functions.php

@@ -83,7 +83,7 @@ trait TwoFAFunctions
 				)
 				)
 			),
 			),
 		];
 		];
-		$this->writeLog('success', 'User Management Function - User added 2FA', $this->user['username']);
+		$this->setLoggerChannel('Users')->info('User added 2FA');
 		$this->setAPIResponse('success', '2FA Added', 200);
 		$this->setAPIResponse('success', '2FA Added', 200);
 		return $this->processQueries($response);
 		return $this->processQueries($response);
 	}
 	}
@@ -101,7 +101,7 @@ trait TwoFAFunctions
 				)
 				)
 			),
 			),
 		];
 		];
-		$this->writeLog('success', 'User Management Function - User removed 2FA', $this->user['username']);
+		$this->setLoggerChannel('Users')->info('User removed 2FA');
 		$this->setAPIResponse('success', '2FA deleted', 204);
 		$this->setAPIResponse('success', '2FA deleted', 204);
 		return $this->processQueries($response);
 		return $this->processQueries($response);
 	}
 	}

+ 33 - 33
api/functions/auth-functions.php

@@ -51,7 +51,7 @@ trait AuthFunctions
 				$provider = $ad->connect();
 				$provider = $ad->connect();
 			} catch (\Adldap\Auth\BindException $e) {
 			} catch (\Adldap\Auth\BindException $e) {
 				$detailedError = $e->getDetailedError();
 				$detailedError = $e->getDetailedError();
-				$this->writeLog('error', 'LDAP Function - Error: ' . $detailedError->getErrorMessage(), 'SYSTEM');
+				$this->setLoggerChannel('LDAP')->error($e);
 				$this->setAPIResponse('error', $detailedError->getErrorMessage(), 409);
 				$this->setAPIResponse('error', $detailedError->getErrorMessage(), 409);
 				return $detailedError->getErrorMessage();
 				return $detailedError->getErrorMessage();
 				// There was an issue binding / connecting to the server.
 				// There was an issue binding / connecting to the server.
@@ -69,7 +69,7 @@ trait AuthFunctions
 			return false;
 			return false;
 		}
 		}
 	}
 	}
-	
+
 	public function testConnectionLdapLogin($array)
 	public function testConnectionLdapLogin($array)
 	{
 	{
 		$username = $array['username'] ?? null;
 		$username = $array['username'] ?? null;
@@ -142,19 +142,19 @@ trait AuthFunctions
 				}
 				}
 			} catch (\Adldap\Auth\BindException $e) {
 			} catch (\Adldap\Auth\BindException $e) {
 				$detailedError = $e->getDetailedError();
 				$detailedError = $e->getDetailedError();
-				$this->writeLog('error', 'LDAP Function - Error: ' . $detailedError->getErrorMessage(), $username);
+				$this->setLoggerChannel('LDAP')->error($e);
 				$this->setAPIResponse('error', $detailedError->getErrorMessage(), 500);
 				$this->setAPIResponse('error', $detailedError->getErrorMessage(), 500);
 				return $detailedError->getErrorMessage();
 				return $detailedError->getErrorMessage();
 				// There was an issue binding / connecting to the server.
 				// There was an issue binding / connecting to the server.
 			} catch (Adldap\Auth\UsernameRequiredException $e) {
 			} catch (Adldap\Auth\UsernameRequiredException $e) {
 				$detailedError = $e->getDetailedError();
 				$detailedError = $e->getDetailedError();
-				$this->writeLog('error', 'LDAP Function - Error: ' . $detailedError->getErrorMessage(), $username);
+				$this->setLoggerChannel('LDAP')->error($e);
 				$this->setAPIResponse('error', $detailedError->getErrorMessage(), 422);
 				$this->setAPIResponse('error', $detailedError->getErrorMessage(), 422);
 				return $detailedError->getErrorMessage();
 				return $detailedError->getErrorMessage();
 				// The user didn't supply a username.
 				// The user didn't supply a username.
 			} catch (Adldap\Auth\PasswordRequiredException $e) {
 			} catch (Adldap\Auth\PasswordRequiredException $e) {
 				$detailedError = $e->getDetailedError();
 				$detailedError = $e->getDetailedError();
-				$this->writeLog('error', 'LDAP Function - Error: ' . $detailedError->getErrorMessage(), $username);
+				$this->setLoggerChannel('LDAP')->error($e);
 				$this->setAPIResponse('error', $detailedError->getErrorMessage(), 422);
 				$this->setAPIResponse('error', $detailedError->getErrorMessage(), 422);
 				return $detailedError->getErrorMessage();
 				return $detailedError->getErrorMessage();
 				// The user didn't supply a password.
 				// The user didn't supply a password.
@@ -164,7 +164,7 @@ trait AuthFunctions
 			return false;
 			return false;
 		}
 		}
 	}
 	}
-	
+
 	public function checkPlexToken($token = '')
 	public function checkPlexToken($token = '')
 	{
 	{
 		try {
 		try {
@@ -183,11 +183,11 @@ trait AuthFunctions
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('success', 'Plex Token Check Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Plex')->error($e);
 		}
 		}
 		return false;
 		return false;
 	}
 	}
-	
+
 	public function checkPlexUser($username)
 	public function checkPlexUser($username)
 	{
 	{
 		try {
 		try {
@@ -204,7 +204,7 @@ trait AuthFunctions
 						$usernameLower = strtolower($username);
 						$usernameLower = strtolower($username);
 						foreach ($userXML as $child) {
 						foreach ($userXML as $child) {
 							if (isset($child['username']) && strtolower($child['username']) == $usernameLower || isset($child['email']) && strtolower($child['email']) == $usernameLower) {
 							if (isset($child['username']) && strtolower($child['username']) == $usernameLower || isset($child['email']) && strtolower($child['email']) == $usernameLower) {
-								$this->writeLog('success', 'Plex User Check - Found User on Friends List', $username);
+								$this->setLoggerChannel('Plex')->info('Found User on Friends List');
 								$machineMatches = false;
 								$machineMatches = false;
 								if ($this->config['plexStrictFriends']) {
 								if ($this->config['plexStrictFriends']) {
 									foreach ($child->Server as $server) {
 									foreach ($child->Server as $server) {
@@ -216,10 +216,10 @@ trait AuthFunctions
 									$machineMatches = true;
 									$machineMatches = true;
 								}
 								}
 								if ($machineMatches) {
 								if ($machineMatches) {
-									$this->writeLog('success', 'Plex User Check - User Approved for Login', $username);
+									$this->setLoggerChannel('Plex')->info('User Approved for Login');
 									return true;
 									return true;
 								} else {
 								} else {
-									$this->writeLog('error', 'Plex User Check - User not Approved User', $username);
+									$this->setLoggerChannel('Plex')->warning('User not Approved User');
 								}
 								}
 							}
 							}
 						}
 						}
@@ -228,11 +228,11 @@ trait AuthFunctions
 			}
 			}
 			return false;
 			return false;
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Plex User Check Function - Error: ' . $e->getMessage(), $username);
+			$this->setLoggerChannel('Plex')->error($e);
 		}
 		}
 		return false;
 		return false;
 	}
 	}
-	
+
 	public function plugin_auth_plex($username, $password)
 	public function plugin_auth_plex($username, $password)
 	{
 	{
 		try {
 		try {
@@ -267,11 +267,11 @@ trait AuthFunctions
 			}
 			}
 			return false;
 			return false;
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('success', 'Plex Auth Function - Error: ' . $e->getMessage(), $username);
+			$this->setLoggerChannel('Plex')->error($e);
 		}
 		}
 		return false;
 		return false;
 	}
 	}
-	
+
 	// Pass credentials to LDAP backend
 	// Pass credentials to LDAP backend
 	public function plugin_auth_ldap($username, $password)
 	public function plugin_auth_ldap($username, $password)
 	{
 	{
@@ -341,25 +341,25 @@ trait AuthFunctions
 					return false;
 					return false;
 				}
 				}
 			} catch (\Adldap\Auth\BindException $e) {
 			} catch (\Adldap\Auth\BindException $e) {
-				$this->writeLog('error', 'LDAP Function - Error: ' . $e->getMessage(), $username);
+				$this->setLoggerChannel('LDAP')->error($e);
 				// There was an issue binding / connecting to the server.
 				// There was an issue binding / connecting to the server.
 			} catch (Adldap\Auth\UsernameRequiredException $e) {
 			} catch (Adldap\Auth\UsernameRequiredException $e) {
-				$this->writeLog('error', 'LDAP Function - Error: ' . $e->getMessage(), $username);
+				$this->setLoggerChannel('LDAP')->error($e);
 				// The user didn't supply a username.
 				// The user didn't supply a username.
 			} catch (Adldap\Auth\PasswordRequiredException $e) {
 			} catch (Adldap\Auth\PasswordRequiredException $e) {
-				$this->writeLog('error', 'LDAP Function - Error: ' . $e->getMessage(), $username);
+				$this->setLoggerChannel('LDAP')->error($e);
 				// The user didn't supply a password.
 				// The user didn't supply a password.
 			}
 			}
 		}
 		}
 		return false;
 		return false;
 	}
 	}
-	
+
 	// Ldap Auth Missing Dependency
 	// Ldap Auth Missing Dependency
 	public function plugin_auth_ldap_disabled()
 	public function plugin_auth_ldap_disabled()
 	{
 	{
 		return 'LDAP - Disabled (Dependency: php-ldap missing!)';
 		return 'LDAP - Disabled (Dependency: php-ldap missing!)';
 	}
 	}
-	
+
 	// Pass credentials to FTP backend
 	// Pass credentials to FTP backend
 	public function plugin_auth_ftp($username, $password)
 	public function plugin_auth_ftp($username, $password)
 	{
 	{
@@ -391,7 +391,7 @@ trait AuthFunctions
 			return false;
 			return false;
 		}
 		}
 	}
 	}
-	
+
 	// Pass credentials to Emby Backend
 	// Pass credentials to Emby Backend
 	public function plugin_auth_emby_local($username, $password)
 	public function plugin_auth_emby_local($username, $password)
 	{
 	{
@@ -424,11 +424,11 @@ trait AuthFunctions
 			}
 			}
 			return false;
 			return false;
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Emby Local Auth Function - Error: ' . $e->getMessage(), $username);
+			$this->setLoggerChannel('Emby')->error($e);
 		}
 		}
 		return false;
 		return false;
 	}
 	}
-	
+
 	// Pass credentials to JellyFin Backend
 	// Pass credentials to JellyFin Backend
 	public function plugin_auth_jellyfin($username, $password)
 	public function plugin_auth_jellyfin($username, $password)
 	{
 	{
@@ -446,7 +446,7 @@ trait AuthFunctions
 			if ($response->success) {
 			if ($response->success) {
 				$json = json_decode($response->body, true);
 				$json = json_decode($response->body, true);
 				if (is_array($json) && isset($json['SessionInfo']) && isset($json['User']) && $json['User']['HasPassword'] == true) {
 				if (is_array($json) && isset($json['SessionInfo']) && isset($json['User']) && $json['User']['HasPassword'] == true) {
-					$this->writeLog('success', 'JellyFin Auth Function - Found User and Logged In', $username);
+					$this->setLoggerChannel('JellyFin')->info('Found User and Logged In');
 					// Login Success - Now Logout JellyFin Session As We No Longer Need It
 					// Login Success - Now Logout JellyFin Session As We No Longer Need It
 					$headers = array(
 					$headers = array(
 						'X-Emby-Authorization' => 'MediaBrowser Client="Organizr Auth", Device="Organizr", DeviceId="orgv2", Version="2.0", Token="' . $json['AccessToken'] . '"',
 						'X-Emby-Authorization' => 'MediaBrowser Client="Organizr Auth", Device="Organizr", DeviceId="orgv2", Version="2.0", Token="' . $json['AccessToken'] . '"',
@@ -460,11 +460,11 @@ trait AuthFunctions
 			}
 			}
 			return false;
 			return false;
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'JellyFin Auth Function - Error: ' . $e->getMessage(), $username);
+			$this->setLoggerChannel('JellyFin')->error($e);
 		}
 		}
 		return false;
 		return false;
 	}
 	}
-	
+
 	// Authenticate against emby connect
 	// Authenticate against emby connect
 	public function plugin_auth_emby_connect($username, $password)
 	public function plugin_auth_emby_connect($username, $password)
 	{
 	{
@@ -483,13 +483,13 @@ trait AuthFunctions
 						if (isset($value['ConnectUserName']) && isset($value['ConnectLinkType'])) { // Qualify as connect account
 						if (isset($value['ConnectUserName']) && isset($value['ConnectLinkType'])) { // Qualify as connect account
 							if (strtolower($value['ConnectUserName']) == strtolower($username) || strtolower($value['Name']) == strtolower($username)) {
 							if (strtolower($value['ConnectUserName']) == strtolower($username) || strtolower($value['Name']) == strtolower($username)) {
 								$connectUserName = $value['ConnectUserName'];
 								$connectUserName = $value['ConnectUserName'];
-								$this->writeLog('success', 'Emby Connect Auth Function - Found User', $username);
+								$this->setLoggerChannel('Emby')->info('Found User');
 								break;
 								break;
 							}
 							}
 						}
 						}
 					}
 					}
 					if ($connectUserName) {
 					if ($connectUserName) {
-						$this->writeLog('success', 'Emby Connect Auth Function - Attempting to Login with Emby ID: ' . $connectUserName, $username);
+						$this->setLoggerChannel('Emby')->info('Attempting to Login with Emby ID: ' . $connectUserName);
 						$connectURL = 'https://connect.emby.media/service/user/authenticate';
 						$connectURL = 'https://connect.emby.media/service/user/authenticate';
 						$headers = array(
 						$headers = array(
 							'Accept' => 'application/json',
 							'Accept' => 'application/json',
@@ -508,21 +508,21 @@ trait AuthFunctions
 									//'image' => $json['User']['ImageUrl'],
 									//'image' => $json['User']['ImageUrl'],
 								);
 								);
 							} else {
 							} else {
-								$this->writeLog('error', 'Emby Connect Auth Function - Bad Response', $username);
+								$this->setLoggerChannel('Emby')->warning('Bad Response');
 							}
 							}
 						} else {
 						} else {
-							$this->writeLog('error', 'Emby Connect Auth Function - 401 From Emby Connect', $username);
+							$this->setLoggerChannel('Emby')->warning('401 From Emby Connect');
 						}
 						}
 					}
 					}
 				}
 				}
 			}
 			}
 			return false;
 			return false;
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Emby Connect Auth Function - Error: ' . $e->getMessage(), $username);
+			$this->setLoggerChannel('Emby')->error($e);
 			return false;
 			return false;
 		}
 		}
 	}
 	}
-	
+
 	// Authenticate Against Emby Local (first) and Emby Connect
 	// Authenticate Against Emby Local (first) and Emby Connect
 	public function plugin_auth_emby_all($username, $password)
 	public function plugin_auth_emby_all($username, $password)
 	{
 	{
@@ -536,5 +536,5 @@ trait AuthFunctions
 			return $this->plugin_auth_emby_connect($username, $password);
 			return $this->plugin_auth_emby_connect($username, $password);
 		}
 		}
 	}
 	}
-	
+
 }
 }

+ 26 - 5
api/functions/backup-functions.php

@@ -22,7 +22,7 @@ trait BackupFunctions
 		$filename = $path . $filename;
 		$filename = $path . $filename;
 		if ($ext == 'zip') {
 		if ($ext == 'zip') {
 			if (file_exists($filename)) {
 			if (file_exists($filename)) {
-				$this->writeLog('success', 'Backup Manager Function -  Deleted Backup [' . pathinfo($filename, PATHINFO_BASENAME) . ']', $this->user['username']);
+				$this->setLoggerChannel('Backup')->info('Deleted Backup [' . pathinfo($filename, PATHINFO_BASENAME) . ']');
 				$this->setAPIResponse(null, pathinfo($filename, PATHINFO_BASENAME) . ' has been deleted', null);
 				$this->setAPIResponse(null, pathinfo($filename, PATHINFO_BASENAME) . ' has been deleted', null);
 				return (unlink($filename));
 				return (unlink($filename));
 			} else {
 			} else {
@@ -63,12 +63,13 @@ trait BackupFunctions
 				break;
 				break;
 			default:
 			default:
 		}
 		}
-
-		$this->writeLog('success', 'BACKUP: backup process started', 'SYSTEM');
+		$this->setLoggerChannel('Backup')->notice('Backing up Organizr');
 		$zipname = $directory . 'backup[' . date('Y-m-d_H-i') . ' - ' . $this->random_ascii_string(2) . '][' . $this->version . '].zip';
 		$zipname = $directory . 'backup[' . date('Y-m-d_H-i') . ' - ' . $this->random_ascii_string(2) . '][' . $this->version . '].zip';
 		$zip = new ZipArchive;
 		$zip = new ZipArchive;
 		$zip->open($zipname, ZipArchive::CREATE);
 		$zip->open($zipname, ZipArchive::CREATE);
-		$zip->addFile($this->config['dbLocation'] . $this->config['dbName'], basename($this->config['dbLocation'] . $this->config['dbName']));
+		if ($this->config['driver'] == 'sqlite3') {
+			$zip->addFile($this->config['dbLocation'] . $this->config['dbName'], basename($this->config['dbLocation'] . $this->config['dbName']));
+		}
 		$rootPath = $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR;
 		$rootPath = $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR;
 		$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($rootPath), RecursiveIteratorIterator::LEAVES_ONLY);
 		$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($rootPath), RecursiveIteratorIterator::LEAVES_ONLY);
 
 
@@ -87,8 +88,28 @@ trait BackupFunctions
 
 
 
 
 		$zip->close();
 		$zip->close();
-		$this->writeLog('success', 'BACKUP: backup process finished', 'SYSTEM');
+		$this->setLoggerChannel('Backup')->notice('Backup process finished');
 		$this->setAPIResponse('success', 'Backup has been created', 200);
 		$this->setAPIResponse('success', 'Backup has been created', 200);
+		$this->deleteBackupsLimit();
+		return true;
+	}
+
+	public function deleteBackupsLimit()
+	{
+		$backups = $this->getBackups();
+		if ($backups) {
+			$list = array_reverse($backups['files']);
+			$killCount = count($list) - $this->config['keepBackupsCountCron'];
+			if ($killCount >= 1) {
+				foreach ($list as $count => $backup) {
+					$count++;
+					if ($count <= $killCount) {
+						$this->log('Cron')->notice('Deleting organizr backup file as it is over limit', ['file' => $backup['name']]);
+						$this->deleteBackup($backup['name']);
+					}
+				}
+			}
+		}
 		return true;
 		return true;
 	}
 	}
 
 

+ 5 - 5
api/functions/homepage-connect-functions.php

@@ -8,8 +8,8 @@ trait HomepageConnectFunctions
 		$list = array();
 		$list = array();
 		$urlList = explode(',', $url);
 		$urlList = explode(',', $url);
 		$tokenList = explode(',', $token);
 		$tokenList = explode(',', $token);
-		if (count($urlList) == count($tokenList)) {
-			foreach ($urlList as $key => $value) {
+		foreach ($urlList as $key => $value) {
+			if (isset($tokenList[$key])) {
 				$list[$key] = array(
 				$list[$key] = array(
 					'url' => $this->qualifyURL($value),
 					'url' => $this->qualifyURL($value),
 					'token' => $tokenList[$key]
 					'token' => $tokenList[$key]
@@ -76,14 +76,14 @@ trait HomepageConnectFunctions
 						break;
 						break;
 				}
 				}
 				if ($response->success) {
 				if ($response->success) {
-					$this->writeLog('success', 'OMBI Connect Function - Ran User Import', 'SYSTEM');
+					$this->setLoggerChannel('Ombi')->info('Ran User Import');
 					return true;
 					return true;
 				} else {
 				} else {
-					$this->writeLog('error', 'OMBI Connect Function - Error: Connection Unsuccessful', 'SYSTEM');
+					$this->setLoggerChannel('Ombi')->warning('Unsuccessful connection');
 					return false;
 					return false;
 				}
 				}
 			} catch (Requests_Exception $e) {
 			} catch (Requests_Exception $e) {
-				$this->writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Ombi')->error($e);
 				return false;
 				return false;
 			}
 			}
 		}
 		}

+ 83 - 34
api/functions/log-functions.php

@@ -159,8 +159,8 @@ trait LogFunctions
 
 
 	public function getLatestLogFile()
 	public function getLatestLogFile()
 	{
 	{
-		if ($this->log) {
-			if (isset($this->log)) {
+		if ($this->logFile) {
+			if (isset($this->logFile)) {
 				$folder = $this->logLocation();
 				$folder = $this->logLocation();
 				$directoryIterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS);
 				$directoryIterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS);
 				$iteratorIterator = new RecursiveIteratorIterator($directoryIterator);
 				$iteratorIterator = new RecursiveIteratorIterator($directoryIterator);
@@ -185,8 +185,8 @@ trait LogFunctions
 
 
 	public function getLogFiles()
 	public function getLogFiles()
 	{
 	{
-		if ($this->log) {
-			if (isset($this->log)) {
+		if ($this->logFile) {
+			if (isset($this->logFile)) {
 				$folder = $this->logLocation();
 				$folder = $this->logLocation();
 				$directoryIterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS);
 				$directoryIterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS);
 				$iteratorIterator = new RecursiveIteratorIterator($directoryIterator);
 				$iteratorIterator = new RecursiveIteratorIterator($directoryIterator);
@@ -207,49 +207,49 @@ trait LogFunctions
 		return false;
 		return false;
 	}
 	}
 
 
+	public function log(...$params)
+	{
+		// Alias of setLoggerChannel
+		return $this->setLoggerChannel(...$params);
+	}
+
 	public function setLoggerChannel($channel = 'Organizr', $username = null)
 	public function setLoggerChannel($channel = 'Organizr', $username = null)
 	{
 	{
+
 		if ($this->hasDB()) {
 		if ($this->hasDB()) {
+			$channel = $channel ?: 'Organizr';
 			$setLogger = false;
 			$setLogger = false;
 			if ($username) {
 			if ($username) {
 				$username = $this->sanitizeUserString($username);
 				$username = $this->sanitizeUserString($username);
 			}
 			}
-			if ($this->logger) {
+			if ($this->loggerSetup) {
 				if ($channel) {
 				if ($channel) {
 					if (strtolower($this->logger->getChannel()) !== strtolower($channel)) {
 					if (strtolower($this->logger->getChannel()) !== strtolower($channel)) {
+						$this->logger->setChannel($channel);
 						$setLogger = true;
 						$setLogger = true;
 					}
 					}
 				}
 				}
 				if ($username) {
 				if ($username) {
 					$currentUsername = $this->logger->getTraceId() !== '' ? strtolower($this->logger->getTraceId()) : '';
 					$currentUsername = $this->logger->getTraceId() !== '' ? strtolower($this->logger->getTraceId()) : '';
 					if ($currentUsername !== strtolower($username)) {
 					if ($currentUsername !== strtolower($username)) {
+						$this->logger->setUsername($username);
 						$setLogger = true;
 						$setLogger = true;
 					}
 					}
 				}
 				}
+				if ($setLogger) {
+					return $this->setupLogger($channel, $username);
+				} else {
+					return $this->logger;
+				}
 			} else {
 			} else {
-				$setLogger = true;
-			}
-			if ($setLogger) {
-				$channel = $channel ?: 'Organizr';
 				return $this->setupLogger($channel, $username);
 				return $this->setupLogger($channel, $username);
-			} else {
-				return $this->logger;
 			}
 			}
 		}
 		}
 	}
 	}
 
 
-	public function setupLogger($channel = 'Organizr', $username = null)
+	public function getLogLevelClass($level, $slack = false)
 	{
 	{
-		if (!$username) {
-			$username = $this->user['username'] ?? 'System';
-		}
-		$loggerBuilder = new OrganizrLogger();
-		$loggerBuilder->setReadyStatus($this->hasDB() && $this->log);
-		$loggerBuilder->setMaxFiles($this->config['maxLogFiles']);
-		$loggerBuilder->setFileName($this->tempLogIfNeeded());
-		$loggerBuilder->setTraceId($username);
-		$loggerBuilder->setChannel(ucwords(strtolower($channel)));
-		switch ($this->config['logLevel']) {
+		switch ($level) {
 			case 'DEBUG':
 			case 'DEBUG':
 				$logLevel = Nekonomokochan\PhpJsonLogger\LoggerBuilder::DEBUG;
 				$logLevel = Nekonomokochan\PhpJsonLogger\LoggerBuilder::DEBUG;
 				break;
 				break;
@@ -275,40 +275,67 @@ trait LogFunctions
 				$logLevel = Nekonomokochan\PhpJsonLogger\LoggerBuilder::WARNING;
 				$logLevel = Nekonomokochan\PhpJsonLogger\LoggerBuilder::WARNING;
 				break;
 				break;
 		}
 		}
-		$loggerBuilder->setLogLevel($logLevel);
+		if ($slack) {
+			$organizrLogLevel = $this->getLogLevelClass($this->config['logLevel']);
+			if ($logLevel < $organizrLogLevel) {
+				$logLevel = $organizrLogLevel;
+			}
+		}
+		return $logLevel;
+	}
+
+	public function setupLogger($channel = 'Organizr', $username = null)
+	{
+		if (!$username) {
+			$username = $this->user['username'] ?? 'System';
+		}
+		$loggerBuilder = new OrganizrLogger();
+		$loggerBuilder->setReadyStatus($this->hasDB() && $this->logFile);
+		$loggerBuilder->setMaxFiles($this->config['maxLogFiles']);
+		$loggerBuilder->setFileName($this->tempLogIfNeeded());
+		$loggerBuilder->setTraceId($username);
+		$loggerBuilder->setChannel(ucwords(strtolower($channel)));
+		$loggerBuilder->setLogLevel($this->getLogLevelClass($this->config['logLevel']));
 		try {
 		try {
+			if ($this->config['sendLogsToSlack']) {
+				if ($this->config['slackLogWebhook'] !== '') {
+					$slackHandlerBuilder = new Nekonomokochan\PhpJsonLogger\SlackWebhookHandlerBuilder($this->config['slackLogWebhook'], $this->config['slackLogWebHookChannel']);
+					$slackHandlerBuilder->setLevel($this->getLogLevelClass($this->config['slackLogLevel'], true));
+					$loggerBuilder->setSlackWebhookHandler($slackHandlerBuilder->build());
+				}
+			}
 			$this->logger = $loggerBuilder->build();
 			$this->logger = $loggerBuilder->build();
+			$this->loggerSetup = true;
 			return $this->logger;
 			return $this->logger;
 		} catch (Exception $e) {
 		} catch (Exception $e) {
 			// nothing so far
 			// nothing so far
-			$this->logger = null;
 			return $this->logger;
 			return $this->logger;
 		}
 		}
-		/* setup:
+		/*
+		Setup:
 		set the log channel before you send log (You can set an optional Username (2nd Variable) | If user is logged already logged in, it will use their username):
 		set the log channel before you send log (You can set an optional Username (2nd Variable) | If user is logged already logged in, it will use their username):
-		$this->setLoggerChannel('Plex Homepage');
 		normal log:
 		normal log:
-		$this->logger->info('test');
+		$this->log('Plex Homepage')->info('test');
 		normal log with context ($context must be an array):
 		normal log with context ($context must be an array):
-		$this->logger->info('test', $context);
+		$this->log('Plex Homepage')->info('test', $context);
 		exception:
 		exception:
-		$this->logger->critical($exception, $context);
+		$this->log('Plex Homepage')->critical($exception, $context);
 		*/
 		*/
 	}
 	}
 
 
 	public function tempLogIfNeeded()
 	public function tempLogIfNeeded()
 	{
 	{
-		if (!$this->log) {
+		if (!$this->logFile) {
 			return $this->root . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR . 'organizr-' . $this->randString() . '.log';
 			return $this->root . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR . 'organizr-' . $this->randString() . '.log';
 		} else {
 		} else {
-			return $this->log;
+			return $this->logFile;
 		}
 		}
 	}
 	}
 
 
 	public function getLog($pageSize = 10, $offset = 0, $filter = 'NONE', $number = 0, $trace_id = null)
 	public function getLog($pageSize = 10, $offset = 0, $filter = 'NONE', $number = 0, $trace_id = null)
 	{
 	{
-		if ($this->log) {
-			if (isset($this->log)) {
+		if ($this->logFile) {
+			if (isset($this->logFile)) {
 				if ($number !== 0) {
 				if ($number !== 0) {
 					if ($number == 'all' || $number == 'combined-logs') {
 					if ($number == 'all' || $number == 'combined-logs') {
 						$log = 'combined-logs';
 						$log = 'combined-logs';
@@ -337,7 +364,7 @@ trait LogFunctions
 	{
 	{
 		$this->setLoggerChannel('Logger');
 		$this->setLoggerChannel('Logger');
 		$this->logger->debug('Starting log purge function');
 		$this->logger->debug('Starting log purge function');
-		if ($this->log) {
+		if ($this->logFile) {
 			$this->logger->debug('Checking if log id exists');
 			$this->logger->debug('Checking if log id exists');
 			if ($number !== 0) {
 			if ($number !== 0) {
 				if ($number == 'all' || $number == 'combined-logs') {
 				if ($number == 'all' || $number == 'combined-logs') {
@@ -427,4 +454,26 @@ trait LogFunctions
 		$dropdownItems .= '<li class="divider"></li><li><a href="javascript:toggleLogFilter(\'NONE\')"><span lang="en">None</span></a></li>';
 		$dropdownItems .= '<li class="divider"></li><li><a href="javascript:toggleLogFilter(\'NONE\')"><span lang="en">None</span></a></li>';
 		return '<button aria-expanded="false" data-toggle="dropdown" class="btn btn-inverse dropdown-toggle waves-effect waves-light pull-right m-r-5 hidden-xs" type="button"> <span class="log-filter-text m-r-5" lang="en">NONE</span><i class="fa fa-filter m-r-5"></i></button><ul role="menu" class="dropdown-menu log-filter-dropdown pull-right">' . $dropdownItems . '</ul>';
 		return '<button aria-expanded="false" data-toggle="dropdown" class="btn btn-inverse dropdown-toggle waves-effect waves-light pull-right m-r-5 hidden-xs" type="button"> <span class="log-filter-text m-r-5" lang="en">NONE</span><i class="fa fa-filter m-r-5"></i></button><ul role="menu" class="dropdown-menu log-filter-dropdown pull-right">' . $dropdownItems . '</ul>';
 	}
 	}
+
+	public function testConnectionSlackLogs()
+	{
+		if (!$this->config['sendLogsToSlack']) {
+			$this->setResponse(409, 'sendLogsToSlack is disabled');
+			return false;
+		}
+		if ($this->config['slackLogWebhook'] == '') {
+			$this->setResponse(409, 'slackLogWebhook is empty');
+			return false;
+		}
+		if ($this->config['slackLogWebHookChannel'] == '' && stripos($this->config['slackLogWebhook'], 'discord') === false) {
+			$this->setResponse(409, 'slackLogWebhook is empty');
+			return false;
+		}
+		$context = [
+			'test' => 'success',
+		];
+		$this->setupLogger('Slack Tester', $this->user['username'])->warning('Warning Test', $context);
+		$this->setResponse(200, 'Slack test connection completed - Please check Slack/Discord Channel');
+		return true;
+	}
 }
 }

+ 35 - 10
api/functions/normal-functions.php

@@ -201,6 +201,11 @@ trait NormalFunctions
 		}
 		}
 	}
 	}
 
 
+	public function getallheadersi()
+	{
+		return array_change_key_case($this->getallheaders(), CASE_LOWER);
+	}
+
 	public function random_ascii_string($length)
 	public function random_ascii_string($length)
 	{
 	{
 		$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
 		$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
@@ -503,10 +508,16 @@ trait NormalFunctions
 
 
 	public function convertIPToRange($ip)
 	public function convertIPToRange($ip)
 	{
 	{
+		$ip = trim($ip);
 		if (strpos($ip, '/') !== false) {
 		if (strpos($ip, '/') !== false) {
 			$explodeIP = explode('/', $ip);
 			$explodeIP = explode('/', $ip);
 			$prefix = $explodeIP[1];
 			$prefix = $explodeIP[1];
 			$start_ip = $explodeIP[0];
 			$start_ip = $explodeIP[0];
+			$explodeStart = explode('.', $start_ip);
+			if (count($explodeStart) == 4) {
+				$explodeStart[3] = $prefix == 32 ? $explodeStart[3] : 0;
+				$start_ip = implode('.', $explodeStart);
+			}
 			$ip_count = 1 << (32 - $prefix);
 			$ip_count = 1 << (32 - $prefix);
 			$start_ip_long = long2ip(ip2long($start_ip));
 			$start_ip_long = long2ip(ip2long($start_ip));
 			$last_ip_long = long2ip(ip2long($start_ip) + $ip_count - 1);
 			$last_ip_long = long2ip(ip2long($start_ip) + $ip_count - 1);
@@ -522,26 +533,40 @@ trait NormalFunctions
 		];
 		];
 	}
 	}
 
 
+	public function convertIPStringToRange($string = null)
+	{
+		$ips = [];
+		if ($string) {
+			$ipListing = explode(',', $string);
+			if (count($ipListing) > 0) {
+				foreach ($ipListing as $ip) {
+					$ips[] = $this->convertIPToRange($ip);
+				}
+			}
+		}
+		return $ips;
+	}
+
 	public function localIPRanges()
 	public function localIPRanges()
 	{
 	{
-		$mainArray = array(
-			array(
+		$mainArray = [
+			[
 				'from' => '10.0.0.0',
 				'from' => '10.0.0.0',
 				'to' => '10.255.255.255'
 				'to' => '10.255.255.255'
-			),
-			array(
+			],
+			[
 				'from' => '172.16.0.0',
 				'from' => '172.16.0.0',
 				'to' => '172.31.255.255'
 				'to' => '172.31.255.255'
-			),
-			array(
+			],
+			[
 				'from' => '192.168.0.0',
 				'from' => '192.168.0.0',
 				'to' => '192.168.255.255'
 				'to' => '192.168.255.255'
-			),
-			array(
+			],
+			[
 				'from' => '127.0.0.1',
 				'from' => '127.0.0.1',
 				'to' => '127.255.255.255'
 				'to' => '127.255.255.255'
-			),
-		);
+			],
+		];
 		if (isset($this->config['localIPList'])) {
 		if (isset($this->config['localIPList'])) {
 			if ($this->config['localIPList'] !== '') {
 			if ($this->config['localIPList'] !== '') {
 				$ipListing = explode(',', $this->config['localIPList']);
 				$ipListing = explode(',', $this->config['localIPList']);

+ 1 - 1
api/functions/oauth.php

@@ -93,7 +93,7 @@ trait OAuthFunctions
 					return true;
 					return true;
 				}
 				}
 			} catch (Requests_Exception $e) {
 			} catch (Requests_Exception $e) {
-				$this->writeLog('error', 'Trakt Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Trakt')->error($e);
 				$this->setResponse(500, $e->getMessage());
 				$this->setResponse(500, $e->getMessage());
 				return false;
 				return false;
 			}
 			}

+ 1 - 1
api/functions/option-functions.php

@@ -458,7 +458,7 @@ trait OptionsFunction
 				break;
 				break;
 			case 'calendarlinkurl':
 			case 'calendarlinkurl':
 				$settingMerge = [
 				$settingMerge = [
-					'type' => 'select',
+					'type' => 'select-input',
 					'label' => 'Target URL',
 					'label' => 'Target URL',
 					'help' => 'Set the primary URL used when clicking on calendar icon.',
 					'help' => 'Set the primary URL used when clicking on calendar icon.',
 					'options' => $this->makeOptionsFromValues($this->config[str_replace('CalendarLink', '', $name) . 'URL'], true, 'Use Default'),
 					'options' => $this->makeOptionsFromValues($this->config[str_replace('CalendarLink', '', $name) . 'URL'], true, 'Use Default'),

+ 28 - 14
api/functions/organizr-functions.php

@@ -150,14 +150,14 @@ trait OrganizrFunctions
 				$url = $this->config['embyURL'] . '/emby/Users/' . $userID . '/Connect/Link';
 				$url = $this->config['embyURL'] . '/emby/Users/' . $userID . '/Connect/Link';
 				Requests::Post($url, $headers, json_encode($data), array());
 				Requests::Post($url, $headers, json_encode($data), array());
 			} catch (Requests_Exception $e) {
 			} catch (Requests_Exception $e) {
-				$this->writeLog('error', 'Emby Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Emby')->error($e);
 				$this->setResponse(500, $e->getMessage());
 				$this->setResponse(500, $e->getMessage());
 				return false;
 				return false;
 			}
 			}
 			$this->setAPIResponse('success', 'User has joined Emby', 200);
 			$this->setAPIResponse('success', 'User has joined Emby', 200);
 			return true;
 			return true;
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Emby create Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Emby')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -175,8 +175,6 @@ trait OrganizrFunctions
 		$response = Requests::Get($url, $headers, array());
 		$response = Requests::Get($url, $headers, array());
 		$response = $response->body;
 		$response = $response->body;
 		$response = json_decode($response, true);
 		$response = json_decode($response, true);
-		//error_Log("response ".json_encode($response));
-		$this->writeLog('error', 'userList:' . json_encode($response), 'SYSTEM');
 		//$correct stores the template users object
 		//$correct stores the template users object
 		$correct = null;
 		$correct = null;
 		foreach ($response as $element) {
 		foreach ($response as $element) {
@@ -184,14 +182,12 @@ trait OrganizrFunctions
 				$correct = $element;
 				$correct = $element;
 			}
 			}
 		}
 		}
-		$this->writeLog('error', 'Correct user:' . json_encode($correct), 'SYSTEM');
 		if ($correct == null) {
 		if ($correct == null) {
 			//return empty JSON if user incorrectly configured template
 			//return empty JSON if user incorrectly configured template
 			return "{}";
 			return "{}";
 		}
 		}
 		//select policy section and remove possibly dangerous rows.
 		//select policy section and remove possibly dangerous rows.
 		$policy = $correct['Policy'];
 		$policy = $correct['Policy'];
-		//writeLog('error', 'policy update'.$policy, 'SYSTEM');
 		unset($policy['AuthenticationProviderId']);
 		unset($policy['AuthenticationProviderId']);
 		unset($policy['InvalidLoginAttemptCount']);
 		unset($policy['InvalidLoginAttemptCount']);
 		unset($policy['DisablePremiumFeatures']);
 		unset($policy['DisablePremiumFeatures']);
@@ -754,12 +750,12 @@ trait OrganizrFunctions
 				$options = $this->requestOptions($url, 60000, true, false);
 				$options = $this->requestOptions($url, 60000, true, false);
 				$response = Requests::post($url . '/api/v1/users/logout', ['X-Auth-Token' => $_COOKIE['komga_token']], $options);
 				$response = Requests::post($url . '/api/v1/users/logout', ['X-Auth-Token' => $_COOKIE['komga_token']], $options);
 				if ($response->success) {
 				if ($response->success) {
-					$this->writeLog('success', 'Komga Token Function - Logged User out', 'SYSTEM');
+					$this->setLoggerChannel('Komga')->info('Logged User out');
 				} else {
 				} else {
-					$this->writeLog('error', 'Komga Token Function - Unable to Logged User out', 'SYSTEM');
+					$this->setLoggerChannel('Komga')->warning('Unable to Logged User out');
 				}
 				}
 			} catch (Requests_Exception $e) {
 			} catch (Requests_Exception $e) {
-				$this->writeLog('error', 'Komga Token Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Komga')->error($e);
 			}
 			}
 			$this->coookie('delete', 'komga_token');
 			$this->coookie('delete', 'komga_token');
 		}
 		}
@@ -821,7 +817,7 @@ trait OrganizrFunctions
 				$options = array_merge($options, $extras);
 				$options = array_merge($options, $extras);
 			}
 			}
 		}
 		}
-		return $options;
+		return array_merge($options, array('useragent' => 'organizr/' . $this->version, 'connect_timeout' => 5));
 	}
 	}
 
 
 	public function showHTML(string $title = 'Organizr Alert', string $notice = '', bool $autoClose = false)
 	public function showHTML(string $title = 'Organizr Alert', string $notice = '', bool $autoClose = false)
@@ -832,9 +828,9 @@ trait OrganizrFunctions
 			'<!DOCTYPE html>
 			'<!DOCTYPE html>
 			<html lang="en">
 			<html lang="en">
 			<head>
 			<head>
-				<link rel="stylesheet" href="' . $this->getServerPath() . '/css/mvp.css">
+				<style>' . file_get_contents($this->root . DIRECTORY_SEPARATOR . 'css' . DIRECTORY_SEPARATOR . 'mvp.css') . '</style>
 				<meta charset="utf-8">
 				<meta charset="utf-8">
-				<meta name="description" content="Trakt OAuth">
+				<meta name="description" content="' . $title . '">
 				<meta name="viewport" content="width=device-width, initial-scale=1.0">
 				<meta name="viewport" content="width=device-width, initial-scale=1.0">
 				<title>' . $title . '</title>
 				<title>' . $title . '</title>
 			</head>
 			</head>
@@ -847,11 +843,11 @@ trait OrganizrFunctions
 			<body ' . $close . '>
 			<body ' . $close . '>
 				<main>
 				<main>
 					<section>
 					<section>
-						<aside>
+						<div>
 							<h3>' . $title . '</h3>
 							<h3>' . $title . '</h3>
 							<p>' . $notice . '</p>
 							<p>' . $notice . '</p>
 							' . $closeMessage . '
 							' . $closeMessage . '
-						</aside>
+						</div>
 					</section>
 					</section>
 				</main>
 				</main>
 			</body>
 			</body>
@@ -885,4 +881,22 @@ trait OrganizrFunctions
 	{
 	{
 		return is_string($string) && is_array(json_decode($string, true)) && (json_last_error() == JSON_ERROR_NONE);
 		return is_string($string) && is_array(json_decode($string, true)) && (json_last_error() == JSON_ERROR_NONE);
 	}
 	}
+
+	public function isXML($string)
+	{
+		libxml_use_internal_errors(true);
+		return (bool)simplexml_load_string($string);
+	}
+
+	public function testAndFormatString($string)
+	{
+		if ($this->isJSON($string)) {
+			return ['type' => 'json', 'data' => json_decode($string, true)];
+		} elseif ($this->isXML($string)) {
+			libxml_use_internal_errors(true);
+			return ['type' => 'xml', 'data' => simplexml_load_string($string)];
+		} else {
+			return ['type' => 'string', 'data' => $string];
+		}
+	}
 }
 }

+ 36 - 23
api/functions/sso-functions.php

@@ -118,27 +118,40 @@ trait SSOFunctions
 	public function getKomgaToken($email, $password, $fallback = false)
 	public function getKomgaToken($email, $password, $fallback = false)
 	{
 	{
 		$token = null;
 		$token = null;
+		$useMaster = false;
 		try {
 		try {
+			if ($password) {
+				if ($password == '') {
+					$useMaster = true;
+				}
+			} else {
+				$useMaster = true;
+			}
+			if ($useMaster) {
+				if ($this->config['komgaSSOMasterPassword'] !== '') {
+					$password = $this->decrypt($this->config['komgaSSOMasterPassword']);
+				}
+			}
 			$credentials = array('auth' => new Requests_Auth_Digest(array($email, $password)));
 			$credentials = array('auth' => new Requests_Auth_Digest(array($email, $password)));
 			$url = $this->qualifyURL($this->config['komgaURL']);
 			$url = $this->qualifyURL($this->config['komgaURL']);
 			$options = $this->requestOptions($url, 60000, true, false, $credentials);
 			$options = $this->requestOptions($url, 60000, true, false, $credentials);
 			$response = Requests::get($url . '/api/v1/users/me', ['X-Auth-Token' => 'organizrSSO'], $options);
 			$response = Requests::get($url . '/api/v1/users/me', ['X-Auth-Token' => 'organizrSSO'], $options);
 			if ($response->success) {
 			if ($response->success) {
 				if ($response->headers['x-auth-token']) {
 				if ($response->headers['x-auth-token']) {
-					$this->writeLog('success', 'Komga Token Function - Grabbed token.', $email);
+					$this->setLoggerChannel('Komga')->info('Grabbed token');
 					$token = $response->headers['x-auth-token'];
 					$token = $response->headers['x-auth-token'];
 				} else {
 				} else {
-					$this->writeLog('error', 'Komga Token Function - Komga did not return Token', $email);
+					$this->setLoggerChannel('Komga')->warning('Komga did not return Token');
 				}
 				}
 			} else {
 			} else {
 				if ($fallback) {
 				if ($fallback) {
-					$this->writeLog('error', 'Komga Token Function - Komga did not return Token - Will retry using fallback credentials', $email);
+					$this->setLoggerChannel('Komga')->warning('Komga did not return Token - Will retry using fallback credentials');
 				} else {
 				} else {
-					$this->writeLog('error', 'Komga Token Function - Komga did not return Token', $email);
+					$this->setLoggerChannel('Komga')->warning('Komga did not return Token');
 				}
 				}
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Komga Token Function - Error: ' . $e->getMessage(), $email);
+			$this->setLoggerChannel('Komga')->error($e);
 		}
 		}
 		if ($token) {
 		if ($token) {
 			return $token;
 			return $token;
@@ -170,16 +183,16 @@ trait SSOFunctions
 			$response = Requests::post($url . $endpoint, $headers, json_encode($data), $options);
 			$response = Requests::post($url . $endpoint, $headers, json_encode($data), $options);
 			if ($response->success) {
 			if ($response->success) {
 				$token = json_decode($response->body, true);
 				$token = json_decode($response->body, true);
-				$this->writeLog('success', 'Jellyfin Token Function - Grabbed token.', $username);
+				$this->setLoggerChannel('JellyFin')->info('Grabbed token');
 				$key = 'user-' . $token['User']['Id'] . '-' . $token['ServerId'];
 				$key = 'user-' . $token['User']['Id'] . '-' . $token['ServerId'];
 				$jellyfin[$key] = json_encode($token['User']);
 				$jellyfin[$key] = json_encode($token['User']);
 				$jellyfin['jellyfin_credentials'] = '{"Servers":[{"ManualAddress":"' . $ssoUrl . '","Id":"' . $token['ServerId'] . '","UserId":"' . $token['User']['Id'] . '","AccessToken":"' . $token['AccessToken'] . '"}]}';
 				$jellyfin['jellyfin_credentials'] = '{"Servers":[{"ManualAddress":"' . $ssoUrl . '","Id":"' . $token['ServerId'] . '","UserId":"' . $token['User']['Id'] . '","AccessToken":"' . $token['AccessToken'] . '"}]}';
 				return $jellyfin;
 				return $jellyfin;
 			} else {
 			} else {
-				$this->writeLog('error', 'Jellyfin Token Function - Jellyfin did not return Token', $username);
+				$this->setLoggerChannel('JellyFin')->warning('JellyFin did not return Token');
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Jellyfin Token Function - Error: ' . $e->getMessage(), $username);
+			$this->setLoggerChannel('Jellyfin')->error($e);
 		}
 		}
 		return false;
 		return false;
 	}
 	}
@@ -205,16 +218,16 @@ trait SSOFunctions
 			$response = Requests::post($url . $endpoint, $headers, json_encode($data), $options);
 			$response = Requests::post($url . $endpoint, $headers, json_encode($data), $options);
 			if ($response->success) {
 			if ($response->success) {
 				$token = json_decode($response->body, true)['access_token'];
 				$token = json_decode($response->body, true)['access_token'];
-				$this->writeLog('success', 'Ombi Token Function - Grabbed token.', $username);
+				$this->setLoggerChannel('Ombi')->info('Grabbed token');
 			} else {
 			} else {
 				if ($fallback) {
 				if ($fallback) {
-					$this->writeLog('error', 'Ombi Token Function - Ombi did not return Token - Will retry using fallback credentials', $username);
+					$this->setLoggerChannel('Ombi')->warning('Ombi did not return Token - Will retry using fallback credentials');
 				} else {
 				} else {
-					$this->writeLog('error', 'Ombi Token Function - Ombi did not return Token', $username);
+					$this->setLoggerChannel('Ombi')->warning('Ombi did not return Token');
 				}
 				}
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Ombi Token Function - Error: ' . $e->getMessage(), $username);
+			$this->setLoggerChannel('Ombi')->error($e);
 		}
 		}
 		if ($token) {
 		if ($token) {
 			return $token;
 			return $token;
@@ -253,12 +266,12 @@ trait SSOFunctions
 						$token[$key]['token'] = json_decode($response->body, true)['token'];
 						$token[$key]['token'] = json_decode($response->body, true)['token'];
 						$token[$key]['uuid'] = json_decode($response->body, true)['uuid'];
 						$token[$key]['uuid'] = json_decode($response->body, true)['uuid'];
 						$token[$key]['path'] = $path;
 						$token[$key]['path'] = $path;
-						$this->writeLog('success', 'Tautulli Token Function - Grabbed token from: ' . $url, $username);
+						$this->setLoggerChannel('Tautulli')->info('Grabbed token from: ' . $url);
 					} else {
 					} else {
-						$this->writeLog('error', 'Tautulli Token Function - Error on URL: ' . $url, $username);
+						$this->setLoggerChannel('Tautulli')->warning('Error on URL: ' . $url);
 					}
 					}
 				} catch (Requests_Exception $e) {
 				} catch (Requests_Exception $e) {
-					$this->writeLog('error', 'Tautulli Token Function - Error: [' . $url . ']' . $e->getMessage(), $username);
+					$this->setLoggerChannel('Tautulli')->error($e);
 				}
 				}
 			}
 			}
 		}
 		}
@@ -285,16 +298,16 @@ trait SSOFunctions
 			if ($response->success) {
 			if ($response->success) {
 				$user = json_decode($response->body, true); // not really needed yet
 				$user = json_decode($response->body, true); // not really needed yet
 				$token = $response->cookies['connect.sid']->value;
 				$token = $response->cookies['connect.sid']->value;
-				$this->writeLog('success', 'Overseerr Token Function - Grabbed token', $user['plexUsername'] ?? $email);
+				$this->setLoggerChannel('Overseerr')->info('Grabbed token');
 			} else {
 			} else {
 				if ($fallback) {
 				if ($fallback) {
-					$this->writeLog('error', 'Overseerr Token Function - Overseerr did not return Token - Will retry using fallback credentials', $email);
+					$this->setLoggerChannel('Overseerr')->warning('Overseerr did not return Token - Will retry using fallback credentials');
 				} else {
 				} else {
-					$this->writeLog('error', 'Overseerr Token Function - Overseerr did not return Token', $email);
+					$this->setLoggerChannel('Overseerr')->warning('Overseerr did not return Token');
 				}
 				}
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Overseerr Token Function - Error: ' . $e->getMessage(), $email);
+			$this->setLoggerChannel('Overseerr')->error($e);
 		}
 		}
 		if ($token) {
 		if ($token) {
 			return urldecode($token);
 			return urldecode($token);
@@ -329,16 +342,16 @@ trait SSOFunctions
 			if ($response->success) {
 			if ($response->success) {
 				$user = json_decode($response->body, true)['user'];
 				$user = json_decode($response->body, true)['user'];
 				$token = json_decode($response->body, true)['token'];
 				$token = json_decode($response->body, true)['token'];
-				$this->writeLog('success', 'Petio Token Function - Grabbed token', $user['username']);
+				$this->setLoggerChannel('Petio')->info('Grabbed token');
 			} else {
 			} else {
 				if ($fallback) {
 				if ($fallback) {
-					$this->writeLog('error', 'Petio Token Function - Petio did not return Token - Will retry using fallback credentials', $username);
+					$this->setLoggerChannel('Petio')->warning('Petio did not return Token - Will retry using fallback credentials');
 				} else {
 				} else {
-					$this->writeLog('error', 'Petio Token Function - Petio did not return Token', $username);
+					$this->setLoggerChannel('Petio')->warning('Petio did not return Token');
 				}
 				}
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Petio Token Function - Error: ' . $e->getMessage(), $username);
+			$this->setLoggerChannel('Petio')->error($e);
 		}
 		}
 		if ($token) {
 		if ($token) {
 			return $token;
 			return $token;

+ 39 - 37
api/functions/token-functions.php

@@ -2,52 +2,54 @@
 
 
 trait TokenFunctions
 trait TokenFunctions
 {
 {
-	public function jwtParse($token)
+	public function configToken()
+	{
+		return Lcobucci\JWT\Configuration::forSymmetricSigner(
+		// You may use any HMAC variations (256, 384, and 512)
+			new Lcobucci\JWT\Signer\Hmac\Sha256(),
+			// replace the value below with a key of your own!
+			Lcobucci\JWT\Signer\Key\InMemory::plainText($this->config['organizrHash'])
+		// You may also override the JOSE encoder/decoder if needed by providing extra arguments here
+		);
+	}
+
+	public function validationConstraints()
+	{
+		return [
+			new Lcobucci\JWT\Validation\Constraint\IssuedBy('Organizr'),
+			new Lcobucci\JWT\Validation\Constraint\PermittedFor('Organizr'),
+			new Lcobucci\JWT\Validation\Constraint\LooseValidAt(Lcobucci\Clock\SystemClock::fromUTC())
+		];
+	}
+
+	public function jwtParse($userToken)
 	{
 	{
 		try {
 		try {
-			$result = array();
-			$result['valid'] = false;
+			$result = [];
 			// Check Token with JWT
 			// Check Token with JWT
 			// Set key
 			// Set key
 			if (!isset($this->config['organizrHash'])) {
 			if (!isset($this->config['organizrHash'])) {
 				return null;
 				return null;
 			}
 			}
-			$key = $this->config['organizrHash'];
-			// SHA256 Encryption
-			$signer = new Lcobucci\JWT\Signer\Hmac\Sha256();
-			$jwttoken = (new Lcobucci\JWT\Parser())->parse((string)$token); // Parses from a string
-			$jwttoken->getHeaders(); // Retrieves the token header
-			$jwttoken->getClaims(); // Retrieves the token claims
-			// Start Validation
-			if ($jwttoken->verify($signer, $key)) {
-				$data = new Lcobucci\JWT\ValidationData(); // It will use the current time to validate (iat, nbf and exp)
-				$data->setIssuer('Organizr');
-				$data->setAudience('Organizr');
-				if ($jwttoken->validate($data)) {
-					$result['valid'] = true;
-					$result['username'] = ($jwttoken->hasClaim('name')) ? $jwttoken->getClaim('name') : 'N/A';
-					$result['group'] = ($jwttoken->hasClaim('group')) ? $jwttoken->getClaim('group') : 'N/A';
-					$result['groupID'] = $jwttoken->getClaim('groupID');
-					$result['userID'] = $jwttoken->getClaim('userID');
-					$result['email'] = $jwttoken->getClaim('email');
-					$result['image'] = $jwttoken->getClaim('image');
-					$result['tokenExpire'] = $jwttoken->getClaim('exp');
-					$result['tokenDate'] = $jwttoken->getClaim('iat');
-					//$result['token'] = $jwttoken->getClaim('exp');
-				}
-			}
-			if ($result['valid'] == true) {
-				return $result;
-			} else {
+			$config = $this->configToken();
+			assert($config instanceof Lcobucci\JWT\Configuration);
+			$token = $config->parser()->parse($userToken);
+			assert($token instanceof Lcobucci\JWT\UnencryptedToken);
+			$constraints = $this->validationConstraints();
+			if (!$config->validator()->validate($token, ...$constraints)) {
 				return false;
 				return false;
 			}
 			}
-		} catch (\RunException $e) {
-			return false;
-		} catch (\OutOfBoundsException $e) {
-			return false;
-		} catch (\RunTimeException $e) {
-			return false;
-		} catch (\InvalidArgumentException $e) {
+			$result['username'] = ($token->claims()->has('name')) ? $token->claims()->get('name') : 'N/A';
+			$result['group'] = ($token->claims()->has('group')) ? $token->claims()->get('group') : 'N/A';
+			$result['groupID'] = $token->claims()->get('groupID');
+			$result['userID'] = $token->claims()->get('userID');
+			$result['email'] = $token->claims()->get('email');
+			$result['image'] = $token->claims()->get('image');
+			$result['tokenExpire'] = $token->claims()->get('exp');
+			$result['tokenDate'] = $token->claims()->get('iat');
+			return $result;
+		} catch (\OutOfBoundsException | \RunTimeException | \InvalidArgumentException | \Lcobucci\JWT\Validation\RequiredConstraintsViolated $e) {
+			$this->setLoggerChannel('Token Error')->error($e);
 			return false;
 			return false;
 		}
 		}
 	}
 	}

+ 56 - 15
api/functions/update-functions.php

@@ -12,13 +12,40 @@ trait UpdateFunctions
 			return $this->linuxUpdate();
 			return $this->linuxUpdate();
 		}
 		}
 	}
 	}
-	
+
+	public function createUpdateStatusFile()
+	{
+		$file = $this->config['dbLocation'] . 'updateInProgress.txt';
+		touch($file);
+		return true;
+	}
+
+	public function removeUpdateStatusFile()
+	{
+		$file = $this->config['dbLocation'] . 'updateInProgress.txt';
+		if (file_exists($file)) {
+			@unlink($file);
+		}
+		return true;
+	}
+
+	public function hasUpdateStatusFile()
+	{
+		return file_exists($this->config['dbLocation'] . 'updateInProgress.txt');
+	}
+
 	public function dockerUpdate()
 	public function dockerUpdate()
 	{
 	{
 		if (!$this->docker) {
 		if (!$this->docker) {
 			$this->setResponse(409, 'Your install type is not Docker');
 			$this->setResponse(409, 'Your install type is not Docker');
 			return false;
 			return false;
 		}
 		}
+		if ($this->hasUpdateStatusFile()) {
+			$this->setResponse(500, 'Already Update in progress');
+			return false;
+		} else {
+			$this->createUpdateStatusFile();
+		}
 		$dockerUpdate = null;
 		$dockerUpdate = null;
 		ini_set('max_execution_time', 0);
 		ini_set('max_execution_time', 0);
 		set_time_limit(0);
 		set_time_limit(0);
@@ -29,6 +56,7 @@ trait UpdateFunctions
 		} elseif (file_exists('./40-install')) {
 		} elseif (file_exists('./40-install')) {
 			$dockerUpdate = shell_exec('./40-install');
 			$dockerUpdate = shell_exec('./40-install');
 		}
 		}
+		$this->removeUpdateStatusFile();
 		if ($dockerUpdate) {
 		if ($dockerUpdate) {
 			$this->setAPIResponse('success', $dockerUpdate, 200);
 			$this->setAPIResponse('success', $dockerUpdate, 200);
 			return true;
 			return true;
@@ -37,19 +65,26 @@ trait UpdateFunctions
 			return false;
 			return false;
 		}
 		}
 	}
 	}
-	
+
 	public function windowsUpdate()
 	public function windowsUpdate()
 	{
 	{
 		if ($this->docker || $this->getOS() !== 'win') {
 		if ($this->docker || $this->getOS() !== 'win') {
 			$this->setResponse(409, 'Your install type is not Windows');
 			$this->setResponse(409, 'Your install type is not Windows');
 			return false;
 			return false;
 		}
 		}
+		if ($this->hasUpdateStatusFile()) {
+			$this->setResponse(500, 'Already Update in progress');
+			return false;
+		} else {
+			$this->createUpdateStatusFile();
+		}
 		$branch = ($this->config['branch'] == 'v2-master') ? '-m' : '-d';
 		$branch = ($this->config['branch'] == 'v2-master') ? '-m' : '-d';
 		ini_set('max_execution_time', 0);
 		ini_set('max_execution_time', 0);
 		set_time_limit(0);
 		set_time_limit(0);
 		$logFile = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'log.txt';
 		$logFile = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'log.txt';
 		$windowsScript = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'windows-update.bat ' . $branch . ' > ' . $logFile . ' 2>&1';
 		$windowsScript = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'windows-update.bat ' . $branch . ' > ' . $logFile . ' 2>&1';
 		$windowsUpdate = shell_exec($windowsScript);
 		$windowsUpdate = shell_exec($windowsScript);
+		$this->removeUpdateStatusFile();
 		if ($windowsUpdate) {
 		if ($windowsUpdate) {
 			$this->setAPIResponse('success', $windowsUpdate, 200);
 			$this->setAPIResponse('success', $windowsUpdate, 200);
 			return true;
 			return true;
@@ -58,19 +93,26 @@ trait UpdateFunctions
 			return false;
 			return false;
 		}
 		}
 	}
 	}
-	
+
 	public function linuxUpdate()
 	public function linuxUpdate()
 	{
 	{
 		if ($this->docker || $this->getOS() == 'win') {
 		if ($this->docker || $this->getOS() == 'win') {
 			$this->setResponse(409, 'Your install type is not Linux');
 			$this->setResponse(409, 'Your install type is not Linux');
 			return false;
 			return false;
 		}
 		}
+		if ($this->hasUpdateStatusFile()) {
+			$this->setResponse(500, 'Already Update in progress');
+			return false;
+		} else {
+			$this->createUpdateStatusFile();
+		}
 		$branch = $this->config['branch'];
 		$branch = $this->config['branch'];
 		ini_set('max_execution_time', 0);
 		ini_set('max_execution_time', 0);
 		set_time_limit(0);
 		set_time_limit(0);
 		$logFile = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'log.txt';
 		$logFile = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'log.txt';
 		$script = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'linux-update.sh ' . $branch . ' > ' . $logFile . ' 2>&1';
 		$script = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'linux-update.sh ' . $branch . ' > ' . $logFile . ' 2>&1';
 		$update = shell_exec($script);
 		$update = shell_exec($script);
+		$this->removeUpdateStatusFile();
 		if ($update) {
 		if ($update) {
 			$this->setAPIResponse('success', $update, 200);
 			$this->setAPIResponse('success', $update, 200);
 			return true;
 			return true;
@@ -79,7 +121,7 @@ trait UpdateFunctions
 			return false;
 			return false;
 		}
 		}
 	}
 	}
-	
+
 	public function upgradeInstall($branch = 'v2-master', $stage = '1')
 	public function upgradeInstall($branch = 'v2-master', $stage = '1')
 	{
 	{
 		// may kill this function in place for php script to run elsewhere
 		// may kill this function in place for php script to run elsewhere
@@ -102,29 +144,29 @@ trait UpdateFunctions
 			$destination = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR;
 			$destination = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR;
 			switch ($stage) {
 			switch ($stage) {
 				case '1':
 				case '1':
-					$this->writeLog('success', 'Update Function -  Started Upgrade Process', $this->user['username']);
+					$this->setLoggerChannel('Update')->info('Started Upgrade Process');
 					if ($this->downloadFile($url, $file)) {
 					if ($this->downloadFile($url, $file)) {
-						$this->writeLog('success', 'Update Function -  Downloaded Update File for Branch: ' . $branch, $this->user['username']);
+						$this->setLoggerChannel('Update')->info('Downloaded Update File for Branch: ' . $branch);
 						$this->setAPIResponse('success', 'Downloaded file successfully', 200);
 						$this->setAPIResponse('success', 'Downloaded file successfully', 200);
 						return true;
 						return true;
 					} else {
 					} else {
-						$this->writeLog('error', 'Update Function -  Downloaded Update File Failed  for Branch: ' . $branch, $this->user['username']);
+						$this->setLoggerChannel('Update')->warning('Downloaded Update File Failed for Branch: ' . $branch);
 						$this->setAPIResponse('error', 'Download failed', 500);
 						$this->setAPIResponse('error', 'Download failed', 500);
 						return false;
 						return false;
 					}
 					}
 				case '2':
 				case '2':
 					if ($this->unzipFile($file)) {
 					if ($this->unzipFile($file)) {
-						$this->writeLog('success', 'Update Function -  Unzipped Update File for Branch: ' . $branch, $this->user['username']);
+						$this->setLoggerChannel('Update')->info('Unzipped Update File for Branch: ' . $branch);
 						$this->setAPIResponse('success', 'Unzipped file successfully', 200);
 						$this->setAPIResponse('success', 'Unzipped file successfully', 200);
 						return true;
 						return true;
 					} else {
 					} else {
-						$this->writeLog('error', 'Update Function -  Unzip Failed for Branch: ' . $branch, $this->user['username']);
+						$this->setLoggerChannel('Update')->warning('Unzip Failed for Branch: ' . $branch);
 						$this->setAPIResponse('error', 'Unzip failed', 500);
 						$this->setAPIResponse('error', 'Unzip failed', 500);
 						return false;
 						return false;
 					}
 					}
 				case '3':
 				case '3':
 					if ($this->rcopy($source, $destination)) {
 					if ($this->rcopy($source, $destination)) {
-						$this->writeLog('success', 'Update Function -  Files overwritten using Updated Files from Branch: ' . $branch, $this->user['username']);
+						$this->setLoggerChannel('Update')->info('Files overwritten using Updated Files from Branch: ' . $branch);
 						$updateComplete = $this->config['dbLocation'] . 'completed.txt';
 						$updateComplete = $this->config['dbLocation'] . 'completed.txt';
 						if (!file_exists($updateComplete)) {
 						if (!file_exists($updateComplete)) {
 							touch($updateComplete);
 							touch($updateComplete);
@@ -132,18 +174,18 @@ trait UpdateFunctions
 						$this->setAPIResponse('success', 'Files replaced successfully', 200);
 						$this->setAPIResponse('success', 'Files replaced successfully', 200);
 						return true;
 						return true;
 					} else {
 					} else {
-						$this->writeLog('error', 'Update Function -  Overwrite Failed for Branch: ' . $branch, $this->user['username']);
+						$this->setLoggerChannel('Update')->warning('Overwrite Failed for Branch: ' . $branch);
 						$this->setAPIResponse('error', 'File replacement failed', 500);
 						$this->setAPIResponse('error', 'File replacement failed', 500);
 						return false;
 						return false;
 					}
 					}
 				case '4':
 				case '4':
 					if ($this->rrmdir($cleanup)) {
 					if ($this->rrmdir($cleanup)) {
-						$this->writeLog('success', 'Update Function -  Deleted Update Files from Branch: ' . $branch, $this->user['username']);
-						$this->writeLog('success', 'Update Function -  Update Completed', $this->user['username']);
+						$this->setLoggerChannel('Update')->info('Deleted Update Files from Branch: ' . $branch);
+						$this->setLoggerChannel('Update')->info('Update Completed');
 						$this->setAPIResponse('success', 'Removed update files successfully', 200);
 						$this->setAPIResponse('success', 'Removed update files successfully', 200);
 						return true;
 						return true;
 					} else {
 					} else {
-						$this->writeLog('error', 'Update Function -  Removal of Update Files Failed for Branch: ' . $branch, $this->user['username']);
+						$this->setLoggerChannel('Update')->warning('Removal of Update Files Failed for Branch: ' . $branch);
 						$this->setAPIResponse('error', 'File removal failed', 500);
 						$this->setAPIResponse('error', 'File removal failed', 500);
 						return false;
 						return false;
 					}
 					}
@@ -155,6 +197,5 @@ trait UpdateFunctions
 			$this->setAPIResponse('error', 'File permissions not set correctly', 500);
 			$this->setAPIResponse('error', 'File permissions not set correctly', 500);
 			return false;
 			return false;
 		}
 		}
-		
 	}
 	}
 }
 }

+ 153 - 5
api/functions/upgrade-functions.php

@@ -88,6 +88,14 @@ trait UpgradeFunctions
 				$this->upgradeToVersion($versionCheck);
 				$this->upgradeToVersion($versionCheck);
 			}
 			}
 			// End Upgrade check start for version above
 			// End Upgrade check start for version above
+			// Upgrade check start for version below
+			$versionCheck = '2.1.2200';
+			if ($compare->lessThan($oldVer, $versionCheck)) {
+				$updateDB = false;
+				$oldVer = $versionCheck;
+				$this->upgradeToVersion($versionCheck);
+			}
+			// End Upgrade check start for version above
 			if ($updateDB == true) {
 			if ($updateDB == true) {
 				//return 'Upgraded Needed - Current Version '.$oldVer.' - New Version: '.$versionCheck;
 				//return 'Upgraded Needed - Current Version '.$oldVer.' - New Version: '.$versionCheck;
 				// Upgrade database to latest version
 				// Upgrade database to latest version
@@ -96,8 +104,7 @@ trait UpgradeFunctions
 			// Update config.php version if different to the installed version
 			// Update config.php version if different to the installed version
 			if ($updateSuccess && $this->version !== $this->config['configVersion']) {
 			if ($updateSuccess && $this->version !== $this->config['configVersion']) {
 				$this->updateConfig(array('apply_CONFIG_VERSION' => $this->version));
 				$this->updateConfig(array('apply_CONFIG_VERSION' => $this->version));
-				$this->setLoggerChannel('Update');
-				$this->logger->debug('Updated config version to ' . $this->version);
+				$this->setLoggerChannel('Update')->notice('Updated config version to ' . $this->version);
 			}
 			}
 			if ($updateSuccess == false) {
 			if ($updateSuccess == false) {
 				die($this->showHTML('Database update failed', 'Please manually check logs and fix - Then reload this page'));
 				die($this->showHTML('Database update failed', 'Please manually check logs and fix - Then reload this page'));
@@ -106,6 +113,56 @@ trait UpgradeFunctions
 		}
 		}
 	}
 	}
 
 
+	public function dropColumnFromDatabase($table = '', $columnName = '')
+	{
+		if ($table == '' || $columnName == '') {
+			return false;
+		}
+		if ($this->hasDB()) {
+			$columnExists = $this->checkIfColumnExists($table, $columnName);
+			if ($columnExists) {
+				$columnAlter = [
+					array(
+						'function' => 'query',
+						'query' => ['ALTER TABLE %n DROP %n',
+							(string)$table,
+							(string)$columnName,
+						]
+					)
+				];
+				$AlterQuery = $this->processQueries($columnAlter);
+				return (boolean)($AlterQuery);
+			}
+		}
+		return false;
+	}
+
+	public function checkIfColumnExists($table = '', $columnName = '')
+	{
+		if ($table == '' || $columnName == '') {
+			return false;
+		}
+		if ($this->hasDB()) {
+			if ($this->config['driver'] === 'sqlite3') {
+				$term = 'SELECT COUNT(*) AS has_column FROM pragma_table_info(?) WHERE name=?';
+			} else {
+				$term = 'SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = "' . $this->config['dbName'] . '" AND TABLE_NAME=? AND COLUMN_NAME=?';
+			}
+			$tableInfo = [
+				array(
+					'function' => 'fetchSingle',
+					'query' => array(
+						$term,
+						(string)$table,
+						(string)$columnName
+					)
+				)
+			];
+			$query = $this->processQueries($tableInfo);
+			return (boolean)($query);
+		}
+	}
+
 	public function addColumnToDatabase($table = '', $columnName = '', $definition = 'TEXT')
 	public function addColumnToDatabase($table = '', $columnName = '', $definition = 'TEXT')
 	{
 	{
 		if ($table == '' || $columnName == '' || $definition == '') {
 		if ($table == '' || $columnName == '' || $definition == '') {
@@ -329,26 +386,81 @@ trait UpgradeFunctions
 		return false;
 		return false;
 	}
 	}
 
 
+	public function resetUpdateFeature($feature = null)
+	{
+		if (!$feature) {
+			$this->setResponse(409, 'Feature not supplied');
+			return false;
+		}
+		$feature = strtolower($feature);
+		switch ($feature) {
+			case 'groupmax':
+			case 'groupidmax':
+			case 'group-max':
+			case 'group-id-max':
+			case 'group_id':
+			case 'group_id_max':
+				$columnExists = $this->checkIfColumnExists('tabs', 'group_id_max');
+				if ($columnExists) {
+					$query = [
+						[
+							'function' => 'query',
+							'query' => [
+								'UPDATE tabs SET group_id_max=0'
+							]
+						],
+					];
+					$tabs = $this->processQueries($query);
+					if (!$tabs) {
+						$this->setResponse(500, 'An error occurred');
+						return false;
+					}
+				} else {
+					$this->setResponse(500, 'group_id_max column does not exist');
+					return false;
+				}
+
+				break;
+			default:
+				$this->setResponse(404, 'Feature not found in reset update');
+				return false;
+		}
+		$this->setResponse(200, 'Ran reset update feature for ' . $feature);
+		return true;
+	}
+
 	public function upgradeToVersion($version = '2.1.0')
 	public function upgradeToVersion($version = '2.1.0')
 	{
 	{
+		$this->setLoggerChannel('Upgrade')->notice('Starting upgrade to version ' . $version);
 		switch ($version) {
 		switch ($version) {
 			case '2.1.0':
 			case '2.1.0':
 				$this->upgradeSettingsTabURL();
 				$this->upgradeSettingsTabURL();
 				$this->upgradeHomepageTabURL();
 				$this->upgradeHomepageTabURL();
+				break;
 			case '2.1.400':
 			case '2.1.400':
 				$this->removeOldPluginDirectoriesAndFiles();
 				$this->removeOldPluginDirectoriesAndFiles();
+				break;
 			case '2.1.525':
 			case '2.1.525':
 				$this->removeOldCustomHTML();
 				$this->removeOldCustomHTML();
+				break;
 			case '2.1.860':
 			case '2.1.860':
 				$this->upgradeInstalledPluginsConfigItem();
 				$this->upgradeInstalledPluginsConfigItem();
+				break;
 			case '2.1.1500':
 			case '2.1.1500':
 				$this->upgradeDataToFolder();
 				$this->upgradeDataToFolder();
+				break;
 			case '2.1.1860':
 			case '2.1.1860':
 				$this->upgradePluginsToDataFolder();
 				$this->upgradePluginsToDataFolder();
-			default:
-				$this->setAPIResponse('success', 'Ran update function for version: ' . $version, 200);
-				return true;
+				break;
+			case '2.1.2200':
+				$this->backupOrganizr();
+				$this->addGroupIdMaxToDatabase();
+				$this->addAddToAdminToDatabase();
+				break;
 		}
 		}
+		$this->setLoggerChannel('Upgrade')->notice('Finished upgrade to version ' . $version);
+		$this->setAPIResponse('success', 'Ran update function for version: ' . $version, 200);
+		return true;
 	}
 	}
 
 
 	public function removeOldCacheFolder()
 	public function removeOldCacheFolder()
@@ -614,4 +726,40 @@ trait UpgradeFunctions
 		}
 		}
 		return false;
 		return false;
 	}
 	}
+
+	public function addGroupIdMaxToDatabase()
+	{
+		$this->setLoggerChannel('Database Migration')->info('Starting database update');
+		$hasOldColumn = $this->checkIfColumnExists('tabs', 'group_id_min');
+		if ($hasOldColumn) {
+			$this->setLoggerChannel('Database Migration')->info('Cleaning up database by removing old group_id_min');
+			$removeColumn = $this->dropColumnFromDatabase('tabs', 'group_id_min');
+			if ($removeColumn) {
+				$this->setLoggerChannel('Database Migration')->info('Removed group_id_min from database');
+			} else {
+				$this->setLoggerChannel('Database Migration')->warning('Error removing group_id_min from database');
+			}
+		}
+		$addColumn = $this->addColumnToDatabase('tabs', 'group_id_max', 'INTEGER DEFAULT \'0\'');
+		if ($addColumn) {
+			$this->setLoggerChannel('Database Migration')->notice('Added group_id_max to database');
+			return true;
+		} else {
+			$this->setLoggerChannel('Database Migration')->warning('Could not update database');
+			return false;
+		}
+	}
+
+	public function addAddToAdminToDatabase()
+	{
+		$this->setLoggerChannel('Database Migration')->info('Starting database update');
+		$addColumn = $this->addColumnToDatabase('tabs', 'add_to_admin', 'INTEGER DEFAULT \'0\'');
+		if ($addColumn) {
+			$this->setLoggerChannel('Database Migration')->notice('Added add_to_admin to database');
+			return true;
+		} else {
+			$this->setLoggerChannel('Database Migration')->warning('Could not update database');
+			return false;
+		}
+	}
 }
 }

+ 1 - 1
api/homepage/couchpotato.php

@@ -75,7 +75,7 @@ trait CouchPotatoHomepageItem
 				$downloader = new Kryptonit3\CouchPotato\CouchPotato($value['url'], $value['token'], null, null, $options);
 				$downloader = new Kryptonit3\CouchPotato\CouchPotato($value['url'], $value['token'], null, null, $options);
 				$calendar = $this->formatCouchCalendar($downloader->getMediaList(array('status' => 'active,done')), $key);
 				$calendar = $this->formatCouchCalendar($downloader->getMediaList(array('status' => 'active,done')), $key);
 			} catch (Exception $e) {
 			} catch (Exception $e) {
-				$this->writeLog('error', 'Radarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Radarr')->error($e);
 			}
 			}
 			if (!empty($calendar)) {
 			if (!empty($calendar)) {
 				$calendarItems = array_merge($calendarItems, $calendar);
 				$calendarItems = array_merge($calendarItems, $calendar);

+ 4 - 4
api/homepage/deluge.php

@@ -77,7 +77,7 @@ trait DelugeHomepageItem
 			$this->setAPIResponse('success', 'API Connection succeeded', 200);
 			$this->setAPIResponse('success', 'API Connection succeeded', 200);
 			return true;
 			return true;
 		} catch (Exception $e) {
 		} catch (Exception $e) {
-			$this->writeLog('error', 'NZBGet Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Deluge')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -144,12 +144,12 @@ trait DelugeHomepageItem
 			}
 			}
 			$api['content']['queueItems'] = (empty($api['content']['queueItems'])) ? [] : $api['content']['queueItems'];
 			$api['content']['queueItems'] = (empty($api['content']['queueItems'])) ? [] : $api['content']['queueItems'];
 			$api['content']['historyItems'] = false;
 			$api['content']['historyItems'] = false;
-		} catch (Excecption $e) {
-			$this->writeLog('error', 'Deluge Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		} catch (Exception $e) {
+			$this->setLoggerChannel('Deluge')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
-		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$api['content'] = $api['content'] ?? false;
 		$this->setAPIResponse('success', null, 200, $api);
 		$this->setAPIResponse('success', null, 200, $api);
 		return $api;
 		return $api;
 	}
 	}

+ 3 - 3
api/homepage/emby.php

@@ -195,7 +195,7 @@ trait EmbyHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Emby Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Emby')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -253,7 +253,7 @@ trait EmbyHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Emby Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Emby')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -313,7 +313,7 @@ trait EmbyHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Emby Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Emby')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}

+ 5 - 5
api/homepage/healthchecks.php

@@ -38,7 +38,7 @@ trait HealthChecksHomepageItem
 		];
 		];
 		return array_merge($homepageInformation, $homepageSettings);
 		return array_merge($homepageInformation, $homepageSettings);
 	}
 	}
-	
+
 	public function healthChecksHomepagePermissions($key = null)
 	public function healthChecksHomepagePermissions($key = null)
 	{
 	{
 		$permissions = [
 		$permissions = [
@@ -57,7 +57,7 @@ trait HealthChecksHomepageItem
 		];
 		];
 		return $this->homepageCheckKeyPermissions($key, $permissions);
 		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	}
-	
+
 	public function homepageOrderhealthchecks()
 	public function homepageOrderhealthchecks()
 	{
 	{
 		if ($this->homepageItemPermissions($this->healthChecksHomepagePermissions('main'))) {
 		if ($this->homepageItemPermissions($this->healthChecksHomepagePermissions('main'))) {
@@ -73,7 +73,7 @@ trait HealthChecksHomepageItem
 				';
 				';
 		}
 		}
 	}
 	}
-	
+
 	public function getHealthChecks($tags = null)
 	public function getHealthChecks($tags = null)
 	{
 	{
 		if (!$this->homepageItemPermissions($this->healthChecksHomepagePermissions('main'), true)) {
 		if (!$this->homepageItemPermissions($this->healthChecksHomepagePermissions('main'), true)) {
@@ -93,7 +93,7 @@ trait HealthChecksHomepageItem
 					$api['content']['checks'] = array_merge($api['content']['checks'], $healthResults['checks']);
 					$api['content']['checks'] = array_merge($api['content']['checks'], $healthResults['checks']);
 				}
 				}
 			} catch (Requests_Exception $e) {
 			} catch (Requests_Exception $e) {
-				$this->writeLog('error', 'HealthChecks Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('HealthChecks')->error($e);
 			};
 			};
 		}
 		}
 		usort($api['content']['checks'], function ($a, $b) {
 		usort($api['content']['checks'], function ($a, $b) {
@@ -107,7 +107,7 @@ trait HealthChecksHomepageItem
 		$this->setAPIResponse('success', null, 200, $api);
 		$this->setAPIResponse('success', null, 200, $api);
 		return $api;
 		return $api;
 	}
 	}
-	
+
 	public function healthChecksTags($tags)
 	public function healthChecksTags($tags)
 	{
 	{
 		$return = '?tag=';
 		$return = '?tag=';

+ 1 - 1
api/homepage/jackett.php

@@ -162,7 +162,7 @@ trait JackettHomepageItem
 				unset($apiData);
 				unset($apiData);
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Jackett blackhole download failed ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Jackett')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		};
 		};

+ 3 - 3
api/homepage/jdownloader.php

@@ -79,10 +79,10 @@ trait JDownloaderHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'JDownloader Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('JDownloader')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
-		};
+		}
 	}
 	}
 
 
 	public function jDownloaderHomepagePermissions($key = null)
 	public function jDownloaderHomepagePermissions($key = null)
@@ -157,7 +157,7 @@ trait JDownloaderHomepageItem
 				$api['content']['$status'] = array($temp['downloader_state'], $temp['grabber_collecting'], $temp['update_ready']);
 				$api['content']['$status'] = array($temp['downloader_state'], $temp['grabber_collecting'], $temp['update_ready']);
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'JDownloader Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('JDownloader')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		};
 		};

+ 3 - 3
api/homepage/jellyfin.php

@@ -194,7 +194,7 @@ trait JellyfinHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Jellyfin Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Jellyfin')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -251,7 +251,7 @@ trait JellyfinHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Jellyfin Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('JellyFin')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -311,7 +311,7 @@ trait JellyfinHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Jellyfin Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('JellyFin')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}

+ 4 - 4
api/homepage/lidarr.php

@@ -41,7 +41,7 @@ trait LidarrHomepageItem
 					$this->settingsOption('calendar-time-format', 'calendarTimeFormat'),
 					$this->settingsOption('calendar-time-format', 'calendarTimeFormat'),
 					$this->settingsOption('calendar-locale', 'calendarLocale'),
 					$this->settingsOption('calendar-locale', 'calendarLocale'),
 					$this->settingsOption('calendar-limit', 'calendarLimit'),
 					$this->settingsOption('calendar-limit', 'calendarLimit'),
-					$this->settingsOption('refresh', 'calendarRefresh'),					
+					$this->settingsOption('refresh', 'calendarRefresh'),
 					$this->settingsOption('blank', '', ['type' => 'html', 'html' => '<hr />']),
 					$this->settingsOption('blank', '', ['type' => 'html', 'html' => '<hr />']),
 					$this->settingsOption('blank', '', ['type' => 'html', 'html' => '<hr />']),
 					$this->settingsOption('blank', '', ['type' => 'html', 'html' => '<hr />']),
 					$this->settingsOption('enable', 'lidarrIcon', ['label' => 'Show Lidarr Icon']),
 					$this->settingsOption('enable', 'lidarrIcon', ['label' => 'Show Lidarr Icon']),
@@ -93,7 +93,7 @@ trait LidarrHomepageItem
 				$failed = true;
 				$failed = true;
 				$ip = $value['url'];
 				$ip = $value['url'];
 				$errors .= $ip . ': ' . $e->getMessage();
 				$errors .= $ip . ': ' . $e->getMessage();
-				$this->writeLog('error', 'Lidarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Lidarr')->error($e);
 			}
 			}
 		}
 		}
 		if ($failed) {
 		if ($failed) {
@@ -160,7 +160,7 @@ trait LidarrHomepageItem
 					$queueItems = array_merge($queueItems, $queue);
 					$queueItems = array_merge($queueItems, $queue);
 				}
 				}
 			} catch (Exception $e) {
 			} catch (Exception $e) {
-				$this->writeLog('error', 'Lidarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Lidarr')->error($e);
 			}
 			}
 		}
 		}
 		$api['content']['queueItems'] = $queueItems;
 		$api['content']['queueItems'] = $queueItems;
@@ -194,7 +194,7 @@ trait LidarrHomepageItem
 					$calendar = '';
 					$calendar = '';
 				}
 				}
 			} catch (Exception $e) {
 			} catch (Exception $e) {
-				$this->writeLog('error', 'Lidarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Lidarr')->error($e);
 			}
 			}
 			if (!empty($calendar)) {
 			if (!empty($calendar)) {
 				$calendarItems = array_merge($calendarItems, $calendar);
 				$calendarItems = array_merge($calendarItems, $calendar);

+ 1 - 1
api/homepage/monitorr.php

@@ -160,7 +160,7 @@ trait MonitorrHomepageItem
 				];
 				];
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Monitorr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Monitorr')->error($e);
 			$this->setAPIResponse('error', $e->getMessage(), 401);
 			$this->setAPIResponse('error', $e->getMessage(), 401);
 			return false;
 			return false;
 		};
 		};

+ 25 - 26
api/homepage/netdata.php

@@ -82,9 +82,8 @@ trait NetDataHomepageItem
 		$api = $api ?? false;
 		$api = $api ?? false;
 		$this->setAPIResponse('success', null, 200, $api);
 		$this->setAPIResponse('success', null, 200, $api);
 		return $api;
 		return $api;
-		
 	}
 	}
-	
+
 	public function netdataSettingsArray($infoOnly = false)
 	public function netdataSettingsArray($infoOnly = false)
 	{
 	{
 		$homepageInformation = [
 		$homepageInformation = [
@@ -193,7 +192,7 @@ trait NetDataHomepageItem
 		);
 		);
 		return array_merge($homepageInformation, $homepageSettings);
 		return array_merge($homepageInformation, $homepageSettings);
 	}
 	}
-	
+
 	public function netdataHomepagePermissions($key = null)
 	public function netdataHomepagePermissions($key = null)
 	{
 	{
 		$permissions = [
 		$permissions = [
@@ -211,7 +210,7 @@ trait NetDataHomepageItem
 		];
 		];
 		return $this->homepageCheckKeyPermissions($key, $permissions);
 		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	}
-	
+
 	public function homepageOrderNetdata()
 	public function homepageOrderNetdata()
 	{
 	{
 		if ($this->homepageItemPermissions($this->netdataHomepagePermissions('main'))) {
 		if ($this->homepageItemPermissions($this->netdataHomepagePermissions('main'))) {
@@ -227,7 +226,7 @@ trait NetDataHomepageItem
 				';
 				';
 		}
 		}
 	}
 	}
-	
+
 	public function disk($dimension, $url)
 	public function disk($dimension, $url)
 	{
 	{
 		$data = [];
 		$data = [];
@@ -244,11 +243,11 @@ trait NetDataHomepageItem
 				$data['max'] = $json['max'];
 				$data['max'] = $json['max'];
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Netdata')->error($e);
 		};
 		};
 		return $data;
 		return $data;
 	}
 	}
-	
+
 	public function diskSpace($dimension, $url)
 	public function diskSpace($dimension, $url)
 	{
 	{
 		$data = [];
 		$data = [];
@@ -265,11 +264,11 @@ trait NetDataHomepageItem
 				$data['max'] = 100;
 				$data['max'] = 100;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
+			$this->setLoggerChannel('Netdata')->error($e);
+		}
 		return $data;
 		return $data;
 	}
 	}
-	
+
 	public function net($dimension, $url)
 	public function net($dimension, $url)
 	{
 	{
 		$data = [];
 		$data = [];
@@ -286,11 +285,11 @@ trait NetDataHomepageItem
 				$data['max'] = $json['max'];
 				$data['max'] = $json['max'];
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
+			$this->setLoggerChannel('Netdata')->error($e);
+		}
 		return $data;
 		return $data;
 	}
 	}
-	
+
 	public function cpu($url)
 	public function cpu($url)
 	{
 	{
 		$data = [];
 		$data = [];
@@ -306,11 +305,11 @@ trait NetDataHomepageItem
 				$data['units'] = '%';
 				$data['units'] = '%';
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
+			$this->setLoggerChannel('Netdata')->error($e);
+		}
 		return $data;
 		return $data;
 	}
 	}
-	
+
 	public function ram($url)
 	public function ram($url)
 	{
 	{
 		$data = [];
 		$data = [];
@@ -326,11 +325,11 @@ trait NetDataHomepageItem
 				$data['units'] = '%';
 				$data['units'] = '%';
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
+			$this->setLoggerChannel('Netdata')->error($e);
+		}
 		return $data;
 		return $data;
 	}
 	}
-	
+
 	public function swap($url)
 	public function swap($url)
 	{
 	{
 		$data = [];
 		$data = [];
@@ -346,11 +345,11 @@ trait NetDataHomepageItem
 				$data['units'] = '%';
 				$data['units'] = '%';
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
+			$this->setLoggerChannel('Netdata')->error($e);
+		}
 		return $data;
 		return $data;
 	}
 	}
-	
+
 	public function getPercent($val, $max)
 	public function getPercent($val, $max)
 	{
 	{
 		if ($max == 0) {
 		if ($max == 0) {
@@ -359,7 +358,7 @@ trait NetDataHomepageItem
 			return ($val / $max) * 100;
 			return ($val / $max) * 100;
 		}
 		}
 	}
 	}
-	
+
 	public function customNetdata($url, $id)
 	public function customNetdata($url, $id)
 	{
 	{
 		try {
 		try {
@@ -417,15 +416,15 @@ trait NetDataHomepageItem
 						}
 						}
 					}
 					}
 				} catch (Requests_Exception $e) {
 				} catch (Requests_Exception $e) {
-					$this->writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-				};
+					$this->setLoggerChannel('Netdata')->error($e);
+				}
 			} else {
 			} else {
 				$data['error'] = 'custom definition incomplete';
 				$data['error'] = 'custom definition incomplete';
 			}
 			}
 			return $data;
 			return $data;
 		}
 		}
 	}
 	}
-	
+
 	public function parseMutators($val, $mutators)
 	public function parseMutators($val, $mutators)
 	{
 	{
 		$mutators = explode(',', $mutators);
 		$mutators = explode(',', $mutators);

+ 2 - 2
api/homepage/nzbget.php

@@ -71,7 +71,7 @@ trait NZBGetHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'NZBGet Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('NZBGet')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -140,7 +140,7 @@ trait NZBGetHomepageItem
 			$this->setAPIResponse('success', null, 200, $api);
 			$this->setAPIResponse('success', null, 200, $api);
 			return $api;
 			return $api;
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'NZBGet Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('NZBGet')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}

+ 1 - 1
api/homepage/octoprint.php

@@ -98,7 +98,7 @@ trait OctoPrintHomepageItem
 					return false;
 					return false;
 				}
 				}
 			} catch (Requests_Exception $e) {
 			} catch (Requests_Exception $e) {
-				$this->writeLog('error', 'Octoprint Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Octoprint')->error($e);
 				$this->setResponse(500, $e->getMessage());
 				$this->setResponse(500, $e->getMessage());
 				return false;
 				return false;
 			};
 			};

+ 5 - 5
api/homepage/ombi.php

@@ -80,7 +80,7 @@ trait OmbiHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Ombi')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -222,7 +222,7 @@ trait OmbiHomepageItem
 				});
 				});
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Ombi')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		};
 		};
@@ -348,7 +348,7 @@ trait OmbiHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Ombi')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -426,10 +426,10 @@ trait OmbiHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Ombi')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
-		};
+		}
 	}
 	}
 
 
 	public function ombiTVDefault($type)
 	public function ombiTVDefault($type)

+ 5 - 5
api/homepage/overseerr.php

@@ -77,7 +77,7 @@ trait OverseerrHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Overseerr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Overseerr')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -237,7 +237,7 @@ trait OverseerrHomepageItem
 				});
 				});
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Overseerr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Overseerr')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -430,7 +430,7 @@ trait OverseerrHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Overseerr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Overseerr')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -521,7 +521,7 @@ trait OverseerrHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Overseerr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Overseerr')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -558,7 +558,7 @@ trait OverseerrHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Overseerr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Overseerr')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}

+ 2 - 2
api/homepage/pihole.php

@@ -68,7 +68,7 @@ trait PiHoleHomepageItem
 				$failed = true;
 				$failed = true;
 				$ip = $this->qualifyURL($url, true)['host'];
 				$ip = $this->qualifyURL($url, true)['host'];
 				$errors .= $ip . ': ' . $e->getMessage();
 				$errors .= $ip . ': ' . $e->getMessage();
-				$this->writeLog('error', 'Pi-hole Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('PiHole')->error($e);
 			};
 			};
 		}
 		}
 		if ($failed) {
 		if ($failed) {
@@ -134,7 +134,7 @@ trait PiHoleHomepageItem
 				}
 				}
 			} catch (Requests_Exception $e) {
 			} catch (Requests_Exception $e) {
 				$this->setResponse(500, $e->getMessage());
 				$this->setResponse(500, $e->getMessage());
-				$this->writeLog('error', 'Pi-hole Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('PiHole')->error($e);
 				return false;
 				return false;
 			};
 			};
 		}
 		}

+ 7 - 5
api/homepage/plex.php

@@ -663,10 +663,12 @@ trait PlexHomepageItem
 		$options = $this->requestOptions($url, null, $this->config['tautulliDisableCertCheck'], $this->config['tautulliUseCustomCertificate']);
 		$options = $this->requestOptions($url, null, $this->config['tautulliDisableCertCheck'], $this->config['tautulliUseCustomCertificate']);
 		try {
 		try {
 			$response = Requests::get($url, [], $options);
 			$response = Requests::get($url, [], $options);
-			$response = json_decode($response->body, true);
-			foreach ($response['response']['data'] as $user) {
-				if ($user['user_id'] != 0) {
-					$names[$user['username']] = $user['friendly_name'];
+			if ($response->success) {
+				$response = json_decode($response->body, true);
+				foreach ($response['response']['data'] as $user) {
+					if ($user['user_id'] != 0) {
+						$names[$user['username']] = $user['friendly_name'];
+					}
 				}
 				}
 			}
 			}
 		} catch (Exception $e) {
 		} catch (Exception $e) {
@@ -728,7 +730,7 @@ trait PlexHomepageItem
 					return $libraryList;
 					return $libraryList;
 				}
 				}
 			} catch (Requests_Exception $e) {
 			} catch (Requests_Exception $e) {
-				$this->writeLog('error', 'Plex Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Plex')->error($e);
 				return false;
 				return false;
 			};
 			};
 		}
 		}

+ 4 - 4
api/homepage/qbittorrent.php

@@ -91,12 +91,12 @@ trait QBitTorrentHomepageItem
 					return true;
 					return true;
 				}
 				}
 			} else {
 			} else {
-				$this->writeLog('error', 'qBittorrent Connect Function - Error: Could not get session ID', 'SYSTEM');
+				$this->setLoggerChannel('qBittorrent')->warning('Could not get session ID');
 				$this->setAPIResponse('error', 'qBittorrent Connect Function - Error: Could not get session ID', 409);
 				$this->setAPIResponse('error', 'qBittorrent Connect Function - Error: Could not get session ID', 409);
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'qBittorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('qBittorrent')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -189,12 +189,12 @@ trait QBitTorrentHomepageItem
 					return $api;
 					return $api;
 				}
 				}
 			} else {
 			} else {
-				$this->writeLog('error', 'qBittorrent Connect Function - Error: Could not get session ID', 'SYSTEM');
+				$this->setLoggerChannel('qBittorrent')->warning('Could not get session ID');
 				$this->setAPIResponse('error', 'qBittorrent Connect Function - Error: Could not get session ID', 409);
 				$this->setAPIResponse('error', 'qBittorrent Connect Function - Error: Could not get session ID', 409);
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'qBittorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('qBittorrent')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}

+ 2 - 2
api/homepage/radarr.php

@@ -103,7 +103,7 @@ trait RadarrHomepageItem
 				$failed = true;
 				$failed = true;
 				$ip = $value['url'];
 				$ip = $value['url'];
 				$errors .= $ip . ': ' . $e->getMessage();
 				$errors .= $ip . ': ' . $e->getMessage();
-				$this->writeLog('error', 'Radarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Radarr')->error($e);
 			}
 			}
 		}
 		}
 		if ($failed) {
 		if ($failed) {
@@ -224,7 +224,7 @@ trait RadarrHomepageItem
 					$calendar = '';
 					$calendar = '';
 				}
 				}
 			} catch (Exception $e) {
 			} catch (Exception $e) {
-				$this->writeLog('error', 'Radarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Radarr')->error($e);
 			}
 			}
 			if (!empty($calendar)) {
 			if (!empty($calendar)) {
 				$calendarItems = array_merge($calendarItems, $calendar);
 				$calendarItems = array_merge($calendarItems, $calendar);

+ 2 - 2
api/homepage/rtorrent.php

@@ -104,7 +104,7 @@ trait RTorrentHomepageItem
 			return false;
 			return false;
 		} catch
 		} catch
 		(Requests_Exception $e) {
 		(Requests_Exception $e) {
-			$this->writeLog('error', 'rTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('rTorrent')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -277,7 +277,7 @@ trait RTorrentHomepageItem
 			}
 			}
 		} catch
 		} catch
 		(Requests_Exception $e) {
 		(Requests_Exception $e) {
-			$this->writeLog('error', 'rTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('rTorrent')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		};
 		};

+ 2 - 2
api/homepage/sickrage.php

@@ -79,7 +79,7 @@ trait SickRageHomepageItem
 				$failed = true;
 				$failed = true;
 				$ip = $value['url'];
 				$ip = $value['url'];
 				$errors .= $ip . ': ' . $e->getMessage();
 				$errors .= $ip . ': ' . $e->getMessage();
-				$this->writeLog('error', 'SickRage Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('SickRage')->error($e);
 			}
 			}
 		}
 		}
 		if ($failed) {
 		if ($failed) {
@@ -130,7 +130,7 @@ trait SickRageHomepageItem
 					$calendarItems = array_merge($calendarItems, $sickrageHistory);
 					$calendarItems = array_merge($calendarItems, $sickrageHistory);
 				}
 				}
 			} catch (Exception $e) {
 			} catch (Exception $e) {
-				$this->writeLog('error', 'SickRage Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('SickRage')->error($e);
 			}
 			}
 		}
 		}
 		$this->setAPIResponse('success', null, 200, $calendarItems);
 		$this->setAPIResponse('success', null, 200, $calendarItems);

+ 3 - 3
api/homepage/sonarr.php

@@ -105,7 +105,7 @@ trait SonarrHomepageItem
 				$failed = true;
 				$failed = true;
 				$ip = $value['url'];
 				$ip = $value['url'];
 				$errors .= $ip . ': ' . $e->getMessage();
 				$errors .= $ip . ': ' . $e->getMessage();
-				$this->writeLog('error', 'Sonarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Sonarr')->error($e);
 			}
 			}
 		}
 		}
 		if ($failed) {
 		if ($failed) {
@@ -192,7 +192,7 @@ trait SonarrHomepageItem
 					$queueItems = array_merge($queueItems, $queue);
 					$queueItems = array_merge($queueItems, $queue);
 				}
 				}
 			} catch (Exception $e) {
 			} catch (Exception $e) {
-				$this->writeLog('error', 'Sonarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Sonarr')->error($e);
 			}
 			}
 		}
 		}
 		$api['content']['queueItems'] = $queueItems;
 		$api['content']['queueItems'] = $queueItems;
@@ -226,7 +226,7 @@ trait SonarrHomepageItem
 					$sonarrCalendar = '';
 					$sonarrCalendar = '';
 				}
 				}
 			} catch (Exception $e) {
 			} catch (Exception $e) {
-				$this->writeLog('error', 'Sonarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Sonarr')->error($e);
 			}
 			}
 			if (!empty($sonarrCalendar)) {
 			if (!empty($sonarrCalendar)) {
 				$calendarItems = array_merge($calendarItems, $sonarrCalendar);
 				$calendarItems = array_merge($calendarItems, $sonarrCalendar);

+ 1 - 1
api/homepage/speedtest.php

@@ -110,7 +110,7 @@ trait SpeedTestHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Speedtest Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Speedtest')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		};
 		};

+ 57 - 57
api/homepage/tautulli.php

@@ -157,6 +157,18 @@ trait TautulliHomepageItem
 		$width = $this->getCacheImageSize('w');
 		$width = $this->getCacheImageSize('w');
 		$nowPlayingHeight = $this->getCacheImageSize('nph');
 		$nowPlayingHeight = $this->getCacheImageSize('nph');
 		$nowPlayingWidth = $this->getCacheImageSize('npw');
 		$nowPlayingWidth = $this->getCacheImageSize('npw');
+		$api['options'] = [
+			'url' => $url,
+			'libraries' => $this->config['tautulliLibraries'],
+			'topMovies' => $this->config['tautulliTopMovies'],
+			'topTV' => $this->config['tautulliTopTV'],
+			'topUsers' => $this->config['tautulliTopUsers'],
+			'topPlatforms' => $this->config['tautulliTopPlatforms'],
+			'popularMovies' => $this->config['tautulliPopularMovies'],
+			'popularTV' => $this->config['tautulliPopularTV'],
+			'title' => $this->config['tautulliHeaderToggle'],
+			'friendlyName' => $this->config['tautulliFriendlyName'],
+		];
 		try {
 		try {
 			$homestatsUrl = $apiURL . '&cmd=get_home_stats&grouping=1';
 			$homestatsUrl = $apiURL . '&cmd=get_home_stats&grouping=1';
 			$options = $this->requestOptions($this->config['tautulliURL'], $this->config['homepageTautulliRefresh'], $this->config['tautulliDisableCertCheck'], $this->config['tautulliUseCustomCertificate']);
 			$options = $this->requestOptions($this->config['tautulliURL'], $this->config['homepageTautulliRefresh'], $this->config['tautulliDisableCertCheck'], $this->config['tautulliUseCustomCertificate']);
@@ -194,72 +206,60 @@ trait TautulliHomepageItem
 					$platform = $api['homestats']['data'][$key]['rows'][0]['platform_name'];
 					$platform = $api['homestats']['data'][$key]['rows'][0]['platform_name'];
 					$this->cacheImage($url . '/images/platforms/' . $platform . '.svg', 'tautulli-' . $platform, 'svg');
 					$this->cacheImage($url . '/images/platforms/' . $platform . '.svg', 'tautulli-' . $platform, 'svg');
 				}
 				}
-			}
-			$libstatsUrl = $apiURL . '&cmd=get_libraries_table';
-			$options = $this->requestOptions($this->config['tautulliURL'], $this->config['homepageTautulliRefresh'], $this->config['tautulliDisableCertCheck'], $this->config['tautulliUseCustomCertificate']);
-			$libstats = Requests::get($libstatsUrl, [], $options);
-			if ($libstats->success) {
-				$homepageTautulliLibraryStatsExclude = explode(",", $this->config['homepageTautulliLibraryStatsExclude']);
-				$libstats = json_decode($libstats->body, true);
-				foreach ($libstats['response']['data']['data'] as $i => $v) {
-					if (array_key_exists('section_id', $v)) {
-						if (in_array($v['section_id'], $homepageTautulliLibraryStatsExclude)) {
-							unset($libstats['response']['data']['data'][$i]);
+				$libstatsUrl = $apiURL . '&cmd=get_libraries_table';
+				$options = $this->requestOptions($this->config['tautulliURL'], $this->config['homepageTautulliRefresh'], $this->config['tautulliDisableCertCheck'], $this->config['tautulliUseCustomCertificate']);
+				$libstats = Requests::get($libstatsUrl, [], $options);
+				if ($libstats->success) {
+					$homepageTautulliLibraryStatsExclude = explode(',', $this->config['homepageTautulliLibraryStatsExclude']);
+					$libstats = json_decode($libstats->body, true);
+					foreach ($libstats['response']['data']['data'] as $i => $v) {
+						if (array_key_exists('section_id', $v)) {
+							if (in_array($v['section_id'], $homepageTautulliLibraryStatsExclude)) {
+								unset($libstats['response']['data']['data'][$i]);
+							}
 						}
 						}
 					}
 					}
+					$libstats['response']['data']['data'] = array_values($libstats['response']['data']['data']);
+					$api['libstats'] = $libstats['response']['data'];
+					$categories = ['movie.svg', 'show.svg', 'artist.svg'];
+					foreach ($categories as $cat) {
+						$parts = explode('.', $cat);
+						$this->cacheImage($url . '/images/libraries/' . $cat, 'tautulli-' . $parts[0], $parts[1]);
+					}
 				}
 				}
-				$libstats['response']['data']['data'] = array_values($libstats['response']['data']['data']);
-				$api['libstats'] = $libstats['response']['data'];
-				$categories = ['movie.svg', 'show.svg', 'artist.svg'];
-				foreach ($categories as $cat) {
-					$parts = explode('.', $cat);
-					$this->cacheImage($url . '/images/libraries/' . $cat, 'tautulli-' . $parts[0], $parts[1]);
+				$ids = []; // Array of stat_ids to remove from the returned array
+				if (!$this->qualifyRequest($this->config['homepageTautulliLibraryAuth'])) {
+					$api['options']['libraries'] = false;
+					unset($api['libstats']);
 				}
 				}
-			}
-			$api['options'] = [
-				'url' => $url,
-				'libraries' => $this->config['tautulliLibraries'],
-				'topMovies' => $this->config['tautulliTopMovies'],
-				'topTV' => $this->config['tautulliTopTV'],
-				'topUsers' => $this->config['tautulliTopUsers'],
-				'topPlatforms' => $this->config['tautulliTopPlatforms'],
-				'popularMovies' => $this->config['tautulliPopularMovies'],
-				'popularTV' => $this->config['tautulliPopularTV'],
-				'title' => $this->config['tautulliHeaderToggle'],
-				'friendlyName' => $this->config['tautulliFriendlyName'],
-			];
-			$ids = []; // Array of stat_ids to remove from the returned array
-			if (!$this->qualifyRequest($this->config['homepageTautulliLibraryAuth'])) {
-				$api['options']['libraries'] = false;
-				unset($api['libstats']);
-			}
-			if (!$this->qualifyRequest($this->config['homepageTautulliViewsAuth'])) {
-				$api['options']['topMovies'] = false;
-				$api['options']['topTV'] = false;
-				$api['options']['popularMovies'] = false;
-				$api['options']['popularTV'] = false;
-				$ids = array_merge(['top_movies', 'popular_movies', 'popular_tv', 'top_tv'], $ids);
-				$api['homestats']['data'] = array_values($api['homestats']['data']);
-			}
-			if (!$this->qualifyRequest($this->config['homepageTautulliMiscAuth'])) {
-				$api['options']['topUsers'] = false;
-				$api['options']['topPlatforms'] = false;
-				$ids = array_merge(['top_platforms', 'top_users'], $ids);
-				$api['homestats']['data'] = array_values($api['homestats']['data']);
-			}
-			$ids = array_merge(['top_music', 'popular_music', 'last_watched', 'most_concurrent'], $ids);
-			foreach ($ids as $id) {
-				if ($key = array_search($id, array_column($api['homestats']['data'], 'stat_id'))) {
-					unset($api['homestats']['data'][$key]);
+				if (!$this->qualifyRequest($this->config['homepageTautulliViewsAuth'])) {
+					$api['options']['topMovies'] = false;
+					$api['options']['topTV'] = false;
+					$api['options']['popularMovies'] = false;
+					$api['options']['popularTV'] = false;
+					$ids = array_merge(['top_movies', 'popular_movies', 'popular_tv', 'top_tv'], $ids);
+					$api['homestats']['data'] = array_values($api['homestats']['data']);
+				}
+				if (!$this->qualifyRequest($this->config['homepageTautulliMiscAuth'])) {
+					$api['options']['topUsers'] = false;
+					$api['options']['topPlatforms'] = false;
+					$ids = array_merge(['top_platforms', 'top_users'], $ids);
 					$api['homestats']['data'] = array_values($api['homestats']['data']);
 					$api['homestats']['data'] = array_values($api['homestats']['data']);
 				}
 				}
+				$ids = array_merge(['top_music', 'popular_music', 'last_watched', 'most_concurrent'], $ids);
+				foreach ($ids as $id) {
+					if ($key = array_search($id, array_column($api['homestats']['data'], 'stat_id'))) {
+						unset($api['homestats']['data'][$key]);
+						$api['homestats']['data'] = array_values($api['homestats']['data']);
+					}
+				}
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
 			$this->logger->critical($e, [$url]);
 			$this->logger->critical($e, [$url]);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
-		};
-		$api = isset($api) ? $api : false;
+		}
+		$api = $api ?? false;
 		$this->setAPIResponse('success', null, 200, $api);
 		$this->setAPIResponse('success', null, 200, $api);
 		return $api;
 		return $api;
 	}
 	}
@@ -270,7 +270,7 @@ trait TautulliHomepageItem
 		$apiURL = $url . '/api/v2?apikey=' . $this->config['tautulliApikey'];
 		$apiURL = $url . '/api/v2?apikey=' . $this->config['tautulliApikey'];
 		if (!empty($this->config['tautulliApikey']) && !empty($this->config['tautulliURL'])) {
 		if (!empty($this->config['tautulliApikey']) && !empty($this->config['tautulliURL'])) {
 			$liblistUrl = $apiURL . '&cmd=get_libraries';
 			$liblistUrl = $apiURL . '&cmd=get_libraries';
-			$options = $this->requestOptions($this->config['tautulliURL'], $this->config['homepageTautulliRefresh'], $this->config['tautulliDisableCertCheck'], $this->config['tautulliUseCustomCertificate']);
+			$options = $this->requestOptions($this->config['tautulliURL'], 10, $this->config['tautulliDisableCertCheck'], $this->config['tautulliUseCustomCertificate']);
 			try {
 			try {
 				$liblist = Requests::get($liblistUrl, [], $options);
 				$liblist = Requests::get($liblistUrl, [], $options);
 				$libraryList = array();
 				$libraryList = array();
@@ -283,7 +283,7 @@ trait TautulliHomepageItem
 					return $libraryList;
 					return $libraryList;
 				}
 				}
 			} catch (Requests_Exception $e) {
 			} catch (Requests_Exception $e) {
-				$this->writeLog('error', 'Tautulli Homepage Error - Unable to get list of libraries: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Tautulli')->error($e);
 				return false;
 				return false;
 			}
 			}
 		}
 		}

+ 2 - 2
api/homepage/trakt.php

@@ -107,7 +107,7 @@ trait TraktHomepageItem
 				}
 				}
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Trakt Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Trakt')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			$errors = true;
 			$errors = true;
 		}
 		}
@@ -122,7 +122,7 @@ trait TraktHomepageItem
 				}
 				}
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Trakt Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Trakt')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			$errors = true;
 			$errors = true;
 		}
 		}

+ 4 - 4
api/homepage/transmission.php

@@ -79,12 +79,12 @@ trait TransmissionHomepageItem
 					return false;
 					return false;
 				}
 				}
 			} else {
 			} else {
-				$this->writeLog('error', 'Transmission Connect Function - Error: Could not get session ID', 'SYSTEM');
+				$this->setLoggerChannel('Transmission')->warning('Could not get session ID');
 				$this->setAPIResponse('error', 'Transmission Connect Function - Error: Could not get session ID', 500);
 				$this->setAPIResponse('error', 'Transmission Connect Function - Error: Could not get session ID', 500);
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Transmission Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Transmission')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -179,12 +179,12 @@ trait TransmissionHomepageItem
 					$api['content']['historyItems'] = false;
 					$api['content']['historyItems'] = false;
 				}
 				}
 			} else {
 			} else {
-				$this->writeLog('error', 'Transmission Connect Function - Error: Could not get session ID', 'SYSTEM');
+				$this->setLoggerChannel('Transmission')->warning('Could not get session ID');
 				$this->setAPIResponse('error', 'Transmission Connect Function - Error: Could not get session ID', 500);
 				$this->setAPIResponse('error', 'Transmission Connect Function - Error: Could not get session ID', 500);
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Transmission Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Transmission')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		};
 		};

+ 5 - 5
api/homepage/unifi.php

@@ -116,7 +116,7 @@ trait UnifiHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Unifi Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Unifi')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -136,7 +136,7 @@ trait UnifiHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Unifi Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Unifi')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -169,7 +169,7 @@ trait UnifiHomepageItem
 				return false;
 				return false;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Unifi Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Unifi')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -198,7 +198,7 @@ trait UnifiHomepageItem
 				}
 				}
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Unifi Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Unifi')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -229,7 +229,7 @@ trait UnifiHomepageItem
 				}
 				}
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Unifi Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Unifi')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}

+ 4 - 4
api/homepage/utorrent.php

@@ -78,7 +78,7 @@ trait uTorrentHomepageItem
 
 
 			$response = $this->getuTorrentToken();
 			$response = $this->getuTorrentToken();
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'uTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('uTorrent')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -134,7 +134,7 @@ trait uTorrentHomepageItem
 				$this->updateConfigItems($uTorrentConfig);
 				$this->updateConfigItems($uTorrentConfig);
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'uTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('uTorrent')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -163,7 +163,7 @@ trait uTorrentHomepageItem
 			$response = Requests::get($url, $headers, $options);
 			$response = Requests::get($url, $headers, $options);
 			$httpResponse = $response->status_code;
 			$httpResponse = $response->status_code;
 			if ($httpResponse == 400) {
 			if ($httpResponse == 400) {
-				$this->writeLog('warn', 'uTorrent Token or Cookie Expired. Generating new session..', 'SYSTEM');
+				$this->setLoggerChannel('uTorrent')->warning('Token or Cookie Expired. Generating new session...');
 				$this->getuTorrentToken();
 				$this->getuTorrentToken();
 				$response = Requests::get($url, $headers, $options);
 				$response = Requests::get($url, $headers, $options);
 				$httpResponse = $response->status_code;
 				$httpResponse = $response->status_code;
@@ -223,7 +223,7 @@ trait uTorrentHomepageItem
 				return $api;
 				return $api;
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'uTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('uTorrent')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}

+ 3 - 2
api/homepage/weather.php

@@ -111,7 +111,8 @@ trait WeatherHomepageItem
 		try {
 		try {
 			if ($this->config['homepageWeatherAndAirWeatherEnabled']) {
 			if ($this->config['homepageWeatherAndAirWeatherEnabled']) {
 				$endpoint = '/weather/v1/forecast/hourly?hours=120&metadata=true';
 				$endpoint = '/weather/v1/forecast/hourly?hours=120&metadata=true';
-				$response = Requests::get($apiURL . $endpoint . $info);
+				$options = $this->requestOptions($apiURL, $this->config['homepageWeatherAndAirRefresh']);
+				$response = Requests::get($apiURL . $endpoint . $info, [], $options);
 				if ($response->success) {
 				if ($response->success) {
 					$apiData = json_decode($response->body, true);
 					$apiData = json_decode($response->body, true);
 					$api['content']['weather'] = ($apiData['error'] === null) ? $apiData : false;
 					$api['content']['weather'] = ($apiData['error'] === null) ? $apiData : false;
@@ -137,7 +138,7 @@ trait WeatherHomepageItem
 				}
 				}
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Weather And Air Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Weather & Air')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		};
 		};

+ 6 - 5
api/pages/settings-tab-editor-tabs.php

@@ -67,7 +67,6 @@ function get_page_settings_tab_editor_tabs($Organizr)
 	return '
 	return '
 	<script>
 	<script>
 	buildTabEditor();
 	buildTabEditor();
-	
 	' . $iconSelectors . '
 	' . $iconSelectors . '
 	</script>
 	</script>
 	<div class="panel bg-org panel-info">
 	<div class="panel bg-org panel-info">
@@ -85,20 +84,22 @@ function get_page_settings_tab_editor_tabs($Organizr)
 							<th width="20" class="text-center"></th>
 							<th width="20" class="text-center"></th>
 							<th width="70" class="text-center"></th>
 							<th width="70" class="text-center"></th>
 							<th lang="en">NAME</th>
 							<th lang="en">NAME</th>
-							<th lang="en">CATEGORY</th>
-							<th lang="en">GROUP</th>
-							<th lang="en">TYPE</th>
+							<th class="text-center" lang="en">CATEGORY</th>
+							<th class="text-center" lang="en">MIN GROUP</th>
+							<th class="text-center" lang="en">MAX GROUP</th>
+							<th class="text-center" lang="en">TYPE</th>
 							<th lang="en" style="text-align:center">DEFAULT</th>
 							<th lang="en" style="text-align:center">DEFAULT</th>
 							<th lang="en" style="text-align:center">ACTIVE</th>
 							<th lang="en" style="text-align:center">ACTIVE</th>
 							<th lang="en" style="text-align:center">SPLASH</th>
 							<th lang="en" style="text-align:center">SPLASH</th>
 							<th lang="en" style="text-align:center">PING</th>
 							<th lang="en" style="text-align:center">PING</th>
 							<th lang="en" style="text-align:center">PRELOAD</th>
 							<th lang="en" style="text-align:center">PRELOAD</th>
+							<th lang="en" style="text-align:center">ADD TO ADMIN</th>
 							<th lang="en" style="text-align:center">EDIT</th>
 							<th lang="en" style="text-align:center">EDIT</th>
 							<th lang="en" style="text-align:center">DELETE</th>
 							<th lang="en" style="text-align:center">DELETE</th>
 						</tr>
 						</tr>
 					</thead>
 					</thead>
 					<tbody id="tabEditorTable">
 					<tbody id="tabEditorTable">
-						<td class="text-center" colspan="12"><i class="fa fa-spin fa-spinner"></i></td>
+						<td class="text-center" colspan="15"><i class="fa fa-spin fa-spinner"></i></td>
 					</tbody>
 					</tbody>
 				</table>
 				</table>
 			</form>
 			</form>

+ 12 - 0
api/pages/wizard.php

@@ -130,12 +130,24 @@ function get_page_wizard($Organizr)
                 return true;
                 return true;
             },
             },
             onFinish: function() {
             onFinish: function() {
+                message("Submitting Wizard");
+                $(\'.white-box\').block({
+				    message: \'<h3><i class="fa fa-close"></i> Submitting Wizard Data...</h3>\',
+				    css: {
+					    color: \'#fff\',
+					    border: \'1px solid #1b1a1a\',
+					    backgroundColor: \'#2cabe3\'
+				    }
+				});
                 var post = $( \'#validation\' ).serializeToJSON();
                 var post = $( \'#validation\' ).serializeToJSON();
                 organizrAPI2(\'POST\',\'api/v2/wizard\',post).success(function(data) {
                 organizrAPI2(\'POST\',\'api/v2/wizard\',post).success(function(data) {
             		var html = data.response;
             		var html = data.response;
+            		message("Wizard Data accepted");
+            		$(\'.white-box\').unblock({});
                     location.reload();
                     location.reload();
             	}).fail(function(xhr) {
             	}).fail(function(xhr) {
             	    OrganizrApiError(xhr, \'API Error\');
             	    OrganizrApiError(xhr, \'API Error\');
+            	    $(\'.white-box\').unblock({});
             	});
             	});
             }
             }
         });
         });

+ 19 - 11
api/plugins/bookmark/plugin.php

@@ -20,11 +20,6 @@ $GLOBALS['plugins']['Bookmark'] = array( // Plugin Name
 // Logo image under Public Domain from https://openclipart.org/detail/182527/open-book
 // Logo image under Public Domain from https://openclipart.org/detail/182527/open-book
 class Bookmark extends Organizr
 class Bookmark extends Organizr
 {
 {
-	public function writeLog($type = 'error', $message = null, $username = null)
-	{
-		parent::writeLog($type, "Plugin 'Bookmark': " . $message, $username);
-	}
-
 	public function _bookmarkGetOrganizrTabInfo()
 	public function _bookmarkGetOrganizrTabInfo()
 	{
 	{
 		$response = [
 		$response = [
@@ -549,7 +544,8 @@ class Bookmark extends Organizr
 		];
 		];
 		$tabInfo = $this->_getBookmarkTabById($id);
 		$tabInfo = $this->_getBookmarkTabById($id);
 		if ($tabInfo) {
 		if ($tabInfo) {
-			$this->writeLog('success', 'Tab Delete Function -  Deleted Tab [' . $tabInfo['name'] . ']', $this->user['username']);
+
+			$this->setLoggerChannel('Bookmark')->info('Deleted Bookmark [' . $tabInfo['name'] . ']');
 			$this->setAPIResponse('success', 'Tab deleted', 204);
 			$this->setAPIResponse('success', 'Tab deleted', 204);
 			return $this->processQueries($response);
 			return $this->processQueries($response);
 		} else {
 		} else {
@@ -575,6 +571,9 @@ class Bookmark extends Organizr
 				$this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409);
 				$this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409);
 				return false;
 				return false;
 			}
 			}
+			if (!$this->qualifyLength($array['name'], 50, true)) {
+				return false;
+			}
 		} else {
 		} else {
 			$this->setAPIResponse('error', 'Tab name was not supplied', 422);
 			$this->setAPIResponse('error', 'Tab name was not supplied', 422);
 			return false;
 			return false;
@@ -619,7 +618,7 @@ class Bookmark extends Organizr
 			),
 			),
 		];
 		];
 		$this->setAPIResponse(null, 'Tab added');
 		$this->setAPIResponse(null, 'Tab added');
-		$this->writeLog('success', 'Tab Editor Function -  Added Tab for [' . $array['name'] . ']', $this->user['username']);
+		$this->setLoggerChannel('Bookmark')->info('Added Bookmark [' . $array['name'] . ']');
 		return $this->processQueries($response);
 		return $this->processQueries($response);
 	}
 	}
 
 
@@ -646,6 +645,9 @@ class Bookmark extends Organizr
 				$this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409);
 				$this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409);
 				return false;
 				return false;
 			}
 			}
+			if (!$this->qualifyLength($array['name'], 50, true)) {
+				return false;
+			}
 		}
 		}
 		if (array_key_exists('background_color', $array)) {
 		if (array_key_exists('background_color', $array)) {
 			$array['background_color'] = $this->sanitizeUserString($array['background_color']);
 			$array['background_color'] = $this->sanitizeUserString($array['background_color']);
@@ -676,7 +678,7 @@ class Bookmark extends Organizr
 			),
 			),
 		];
 		];
 		$this->setAPIResponse(null, 'Tab info updated');
 		$this->setAPIResponse(null, 'Tab info updated');
-		$this->writeLog('success', 'Tab Editor Function -  Edited Tab Info for [' . $tabInfo['name'] . ']', $this->user['username']);
+		$this->setLoggerChannel('Bookmark')->info('Edited Bookmark [' . $tabInfo['name'] . ']');
 		return $this->processQueries($response);
 		return $this->processQueries($response);
 	}
 	}
 
 
@@ -887,6 +889,9 @@ class Bookmark extends Organizr
 				$this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409);
 				$this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409);
 				return false;
 				return false;
 			}
 			}
+			if (!$this->qualifyLength($array['category'], 50, true)) {
+				return false;
+			}
 		} else {
 		} else {
 			$this->setAPIResponse('error', 'Category name was not supplied', 422);
 			$this->setAPIResponse('error', 'Category name was not supplied', 422);
 			return false;
 			return false;
@@ -901,7 +906,7 @@ class Bookmark extends Organizr
 			),
 			),
 		];
 		];
 		$this->setAPIResponse(null, 'Category added');
 		$this->setAPIResponse(null, 'Category added');
-		$this->writeLog('success', 'Category Editor Function -  Added Category for [' . $array['category'] . ']', $this->user['username']);
+		$this->setLoggerChannel('Bookmark')->info('Added Bookmark Category [' . $array['category'] . ']');
 		$result = $this->processQueries($response);
 		$result = $this->processQueries($response);
 		$this->_correctDefaultCategory();
 		$this->_correctDefaultCategory();
 		return $result;
 		return $result;
@@ -930,6 +935,9 @@ class Bookmark extends Organizr
 				$this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409);
 				$this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409);
 				return false;
 				return false;
 			}
 			}
+			if (!$this->qualifyLength($array['category'], 50, true)) {
+				return false;
+			}
 		}
 		}
 		if (array_key_exists('default', $array)) {
 		if (array_key_exists('default', $array)) {
 			if ($array['default']) {
 			if ($array['default']) {
@@ -948,7 +956,7 @@ class Bookmark extends Organizr
 			),
 			),
 		];
 		];
 		$this->setAPIResponse(null, 'Category info updated');
 		$this->setAPIResponse(null, 'Category info updated');
-		$this->writeLog('success', 'Category Editor Function -  Edited Category Info for [' . $categoryInfo['category'] . ']', $this->user['username']);
+		$this->setLoggerChannel('Bookmark')->info('Edited Bookmark Category [' . $categoryInfo['category'] . ']');
 		$result = $this->processQueries($response);
 		$result = $this->processQueries($response);
 		$this->_correctDefaultCategory();
 		$this->_correctDefaultCategory();
 		return $result;
 		return $result;
@@ -1000,7 +1008,7 @@ class Bookmark extends Organizr
 		];
 		];
 		$categoryInfo = $this->_getBookmarkCategoryById($id);
 		$categoryInfo = $this->_getBookmarkCategoryById($id);
 		if ($categoryInfo) {
 		if ($categoryInfo) {
-			$this->writeLog('success', 'Category Delete Function -  Deleted Category [' . $categoryInfo['category'] . ']', $this->user['username']);
+			$this->setLoggerChannel('Bookmark')->info('Deleted Bookmark Category [' . $categoryInfo['category'] . ']');
 			$this->setAPIResponse('success', 'Category deleted', 204);
 			$this->setAPIResponse('success', 'Category deleted', 204);
 			$result = $this->processQueries($response);
 			$result = $this->processQueries($response);
 			$this->_correctDefaultCategory();
 			$this->_correctDefaultCategory();

+ 7 - 7
api/plugins/healthChecks/plugin.php

@@ -130,7 +130,7 @@ class HealthChecks extends Organizr
 			)
 			)
 		);
 		);
 	}
 	}
-	
+
 	public function _healthCheckPluginTest($url)
 	public function _healthCheckPluginTest($url)
 	{
 	{
 		$success = false;
 		$success = false;
@@ -156,12 +156,12 @@ class HealthChecks extends Organizr
 				}
 				}
 			}
 			}
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'HealthChecks Plugin - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('HealthChecks')->error($e);
 			return false;
 			return false;
 		}
 		}
 		return $success;
 		return $success;
 	}
 	}
-	
+
 	public function _healthCheckSelfHostedURLValidation($url, $checkOnly = false)
 	public function _healthCheckSelfHostedURLValidation($url, $checkOnly = false)
 	{
 	{
 		$selfHosted = true;
 		$selfHosted = true;
@@ -175,7 +175,7 @@ class HealthChecks extends Organizr
 		}
 		}
 		return $checkOnly ? $selfHosted : $url;
 		return $checkOnly ? $selfHosted : $url;
 	}
 	}
-	
+
 	public function _healthCheckPluginStartUUID($uuid)
 	public function _healthCheckPluginStartUUID($uuid)
 	{
 	{
 		if (!$uuid || $this->config['HEALTHCHECKS-PingURL'] == '') {
 		if (!$uuid || $this->config['HEALTHCHECKS-PingURL'] == '') {
@@ -186,7 +186,7 @@ class HealthChecks extends Organizr
 		$options = ($this->localURL($url)) ? array('verify' => false) : array('verify' => $this->getCert());
 		$options = ($this->localURL($url)) ? array('verify' => false) : array('verify' => $this->getCert());
 		return Requests::get($url . $uuid . '/start', [], $options);
 		return Requests::get($url . $uuid . '/start', [], $options);
 	}
 	}
-	
+
 	public function _healthCheckPluginUUID($uuid, $pass = false)
 	public function _healthCheckPluginUUID($uuid, $pass = false)
 	{
 	{
 		if (!$uuid || $this->config['HEALTHCHECKS-PingURL'] == '') {
 		if (!$uuid || $this->config['HEALTHCHECKS-PingURL'] == '') {
@@ -198,7 +198,7 @@ class HealthChecks extends Organizr
 		$options = ($this->localURL($url)) ? array('verify' => false) : array('verify' => $this->getCert());
 		$options = ($this->localURL($url)) ? array('verify' => false) : array('verify' => $this->getCert());
 		return Requests::get($url . $uuid . $path, [], $options);
 		return Requests::get($url . $uuid . $path, [], $options);
 	}
 	}
-	
+
 	public function _healthCheckPluginRun()
 	public function _healthCheckPluginRun()
 	{
 	{
 		$continue = $this->config['HEALTHCHECKS-all-items'] !== '' ? $this->config['HEALTHCHECKS-all-items'] : false;
 		$continue = $this->config['HEALTHCHECKS-all-items'] !== '' ? $this->config['HEALTHCHECKS-all-items'] : false;
@@ -208,7 +208,7 @@ class HealthChecks extends Organizr
 		if ($continue && $this->config['HEALTHCHECKS-enabled'] && !empty($this->config['HEALTHCHECKS-PingURL']) && $this->qualifyRequest($this->config['HEALTHCHECKS-Auth-include'])) {
 		if ($continue && $this->config['HEALTHCHECKS-enabled'] && !empty($this->config['HEALTHCHECKS-PingURL']) && $this->qualifyRequest($this->config['HEALTHCHECKS-Auth-include'])) {
 			$allItems = [];
 			$allItems = [];
 			foreach ($this->config['HEALTHCHECKS-all-items'] as $k => $v) {
 			foreach ($this->config['HEALTHCHECKS-all-items'] as $k => $v) {
-				
+
 				if ($k !== false) {
 				if ($k !== false) {
 					foreach ($v as $item) {
 					foreach ($v as $item) {
 						$allItems[$k][$item['label']] = $item['value'];
 						$allItems[$k][$item['label']] = $item['value'];

+ 12 - 12
api/plugins/invites/plugin.php

@@ -137,7 +137,7 @@ class Invites extends Organizr
 		];
 		];
 		$query = $this->processQueries($response);
 		$query = $this->processQueries($response);
 		if ($query) {
 		if ($query) {
-			$this->writeLog('success', 'Invite Management Function -  Added Invite [' . $code . ']', $this->user['username']);
+			$this->setLoggerChannel('Invites')->info('Added Invite [' . $code . ']');
 			if ($this->config['PHPMAILER-enabled']) {
 			if ($this->config['PHPMAILER-enabled']) {
 				$PhpMailer = new PhpMailer();
 				$PhpMailer = new PhpMailer();
 				$emailTemplate = array(
 				$emailTemplate = array(
@@ -255,7 +255,7 @@ class Invites extends Organizr
 				)
 				)
 			];
 			];
 			$query = $this->processQueries($response);
 			$query = $this->processQueries($response);
-			$this->writeLog('success', 'Invite Management Function -  Invite Used [' . $code . ']', 'SYSTEM');
+			$this->setLoggerChannel('Invites')->info('Invite Used [' . $code . ']');
 			return $this->_invitesPluginAction($usedBy, 'share', $this->config['INVITES-type-include']);
 			return $this->_invitesPluginAction($usedBy, 'share', $this->config['INVITES-type-include']);
 		} else {
 		} else {
 			return false;
 			return false;
@@ -295,7 +295,7 @@ class Invites extends Organizr
 							return $libraryList;
 							return $libraryList;
 						}
 						}
 					} catch (Requests_Exception $e) {
 					} catch (Requests_Exception $e) {
-						$this->writeLog('error', 'Plex Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+						$this->setLoggerChannel('Plex')->error($e);
 						return false;
 						return false;
 					};
 					};
 				}
 				}
@@ -526,36 +526,36 @@ class Invites extends Organizr
 								return false;
 								return false;
 						}
 						}
 						if ($response->success) {
 						if ($response->success) {
-							$this->writeLog('success', 'Plex Invite Function - Plex User now has access to system', $username);
+							$this->setLoggerChannel('Invites')->info('Plex User now has access to system');
 							$this->setAPIResponse('success', 'Plex User now has access to system', 200);
 							$this->setAPIResponse('success', 'Plex User now has access to system', 200);
 							return true;
 							return true;
 						} else {
 						} else {
 							switch ($response->status_code) {
 							switch ($response->status_code) {
 								case 400:
 								case 400:
-									$this->writeLog('error', 'Plex Invite Function - Plex User already has access', $username);
+									$this->setLoggerChannel('Plex')->warning('Plex User already has access');
 									$this->setAPIResponse('error', 'Plex User already has access', 409);
 									$this->setAPIResponse('error', 'Plex User already has access', 409);
 									return false;
 									return false;
 								case 401:
 								case 401:
-									$this->writeLog('error', 'Plex Invite Function - Incorrect Token', 'SYSTEM');
+									$this->setLoggerChannel('Plex')->warning('Incorrect Token');
 									$this->setAPIResponse('error', 'Incorrect Token', 409);
 									$this->setAPIResponse('error', 'Incorrect Token', 409);
 									return false;
 									return false;
 								case 404:
 								case 404:
-									$this->writeLog('error', 'Plex Invite Function - Libraries not setup correct [' . $this->config['INVITES-plexLibraries'] . ']', 'SYSTEM');
+									$this->setLoggerChannel('Plex')->warning('Libraries not setup correctly');
 									$this->setAPIResponse('error', 'Libraries not setup correct', 409);
 									$this->setAPIResponse('error', 'Libraries not setup correct', 409);
 									return false;
 									return false;
 								default:
 								default:
-									$this->writeLog('error', 'Plex Invite Function - An error occurred [' . $response->status_code . ']', $username);
+									$this->setLoggerChannel('Plex')->warning('An error occurred [' . $response->status_code . ']');
 									$this->setAPIResponse('error', 'An Error Occurred', 409);
 									$this->setAPIResponse('error', 'An Error Occurred', 409);
 									return false;
 									return false;
 							}
 							}
 						}
 						}
 					} catch (Requests_Exception $e) {
 					} catch (Requests_Exception $e) {
-						$this->writeLog('error', 'Plex Invite Function - Error: ' . $e->getMessage(), 'SYSTEM');
+						$this->setLoggerChannel('Plex')->error($e);
 						$this->setAPIResponse('error', $e->getMessage(), 409);
 						$this->setAPIResponse('error', $e->getMessage(), 409);
 						return false;
 						return false;
-					};
+					}
 				} else {
 				} else {
-					$this->writeLog('error', 'Plex Invite Function - Plex Token/ID not set', 'SYSTEM');
+					$this->setLoggerChannel('Plex')->warning('Plex Token/ID not set');
 					$this->setAPIResponse('error', 'Plex Token/ID not set', 409);
 					$this->setAPIResponse('error', 'Plex Token/ID not set', 409);
 					return false;
 					return false;
 				}
 				}
@@ -566,7 +566,7 @@ class Invites extends Organizr
 					$this->setAPIResponse('success', 'User now has access to system', 200);
 					$this->setAPIResponse('success', 'User now has access to system', 200);
 					return true;
 					return true;
 				} catch (Requests_Exception $e) {
 				} catch (Requests_Exception $e) {
-					$this->writeLog('error', 'Emby Invite Function - Error: ' . $e->getMessage(), 'SYSTEM');
+					$this->setLoggerChannel('Emby')->error($e);
 					$this->setAPIResponse('error', $e->getMessage(), 409);
 					$this->setAPIResponse('error', $e->getMessage(), 409);
 					return false;
 					return false;
 				}
 				}

+ 0 - 3
api/plugins/php-mailer/misc/emailTemplates/dark.php

@@ -56,9 +56,6 @@ $email = '
  					</tbody>
  					</tbody>
  				</table>
  				</table>
  			</div>
  			</div>
- 			<div style="text-align: center; font-size: 12px; color: #b2b2b5; margin-top: 20px">
- 				<p>Powered by Organizr<br></p>
- 			</div>
  		</div>
  		</div>
  	</div>
  	</div>
  </body>
  </body>

+ 2 - 166
api/plugins/php-mailer/misc/emailTemplates/default.php

@@ -959,161 +959,7 @@ $email = '
                                     <tr>
                                     <tr>
                                     <![endif]-->
                                     <![endif]-->
 
 
-                                                                                                            <!--[if mso]>
-                                        <td align="center" valign="top">
-                                        <![endif]-->
-
-                                                                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" style="display:inline;">
-                                                                                                                <tbody>
-                                                                                                                    <tr>
-                                                                                                                        <td valign="top" style="padding-right:10px; padding-bottom:9px;" class="mcnFollowContentItemContainer">
-                                                                                                                            <table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnFollowContentItem">
-                                                                                                                                <tbody>
-                                                                                                                                    <tr>
-                                                                                                                                        <td align="left" valign="middle" style="padding-top:5px; padding-right:10px; padding-bottom:5px; padding-left:9px;">
-                                                                                                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" width="">
-                                                                                                                                                <tbody>
-                                                                                                                                                    <tr>
-
-                                                                                                                                                        <td align="center" valign="middle" width="24" class="mcnFollowIconContent">
-                                                                                                                                                            <a href="https://github.com/causefx/organizr" target="_blank"><img src="https://cdn-images.mailchimp.com/icons/social-block-v2/color-github-48.png" style="display:block;" height="24" width="24" class=""></a>
-                                                                                                                                                        </td>
-
-                                                                                                                                                        <td align="left" valign="middle" class="mcnFollowTextContent" style="padding-left:5px;">
-                                                                                                                                                            <a href="https://github.com/causefx/organizr" target="" style="font-family: Helvetica;font-size: 12px;text-decoration: none;color: #FFFFFF;font-weight: bold;">GitHub</a>
-                                                                                                                                                        </td>
-
-                                                                                                                                                    </tr>
-                                                                                                                                                </tbody>
-                                                                                                                                            </table>
-                                                                                                                                        </td>
-                                                                                                                                    </tr>
-                                                                                                                                </tbody>
-                                                                                                                            </table>
-                                                                                                                        </td>
-                                                                                                                    </tr>
-                                                                                                                </tbody>
-                                                                                                            </table>
-
-                                                                                                            <!--[if mso]>
-                                        </td>
-                                        <![endif]-->
-
-                                                                                                            <!--[if mso]>
-                                        <td align="center" valign="top">
-                                        <![endif]-->
-
-                                                                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" style="display:inline;">
-                                                                                                                <tbody>
-                                                                                                                    <tr>
-                                                                                                                        <td valign="top" style="padding-right:10px; padding-bottom:9px;" class="mcnFollowContentItemContainer">
-                                                                                                                            <table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnFollowContentItem">
-                                                                                                                                <tbody>
-                                                                                                                                    <tr>
-                                                                                                                                        <td align="left" valign="middle" style="padding-top:5px; padding-right:10px; padding-bottom:5px; padding-left:9px;">
-                                                                                                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" width="">
-                                                                                                                                                <tbody>
-                                                                                                                                                    <tr>
-
-                                                                                                                                                        <td align="center" valign="middle" width="24" class="mcnFollowIconContent">
-                                                                                                                                                            <a href="https://www.reddit.com/r/organizr" target="_blank"><img src="https://cdn-images.mailchimp.com/icons/social-block-v2/color-reddit-48.png" style="display:block;" height="24" width="24" class=""></a>
-                                                                                                                                                        </td>
-
-                                                                                                                                                        <td align="left" valign="middle" class="mcnFollowTextContent" style="padding-left:5px;">
-                                                                                                                                                            <a href="https://www.reddit.com/r/organizr" target="" style="font-family: Helvetica;font-size: 12px;text-decoration: none;color: #FFFFFF;font-weight: bold;">Reddit</a>
-                                                                                                                                                        </td>
-
-                                                                                                                                                    </tr>
-                                                                                                                                                </tbody>
-                                                                                                                                            </table>
-                                                                                                                                        </td>
-                                                                                                                                    </tr>
-                                                                                                                                </tbody>
-                                                                                                                            </table>
-                                                                                                                        </td>
-                                                                                                                    </tr>
-                                                                                                                </tbody>
-                                                                                                            </table>
-
-                                                                                                            <!--[if mso]>
-                                        </td>
-                                        <![endif]-->
-
-                                                                                                            <!--[if mso]>
-                                        <td align="center" valign="top">
-                                        <![endif]-->
-
-                                                                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" style="display:inline;">
-                                                                                                                <tbody>
-                                                                                                                    <tr>
-                                                                                                                        <td valign="top" style="padding-right:10px; padding-bottom:9px;" class="mcnFollowContentItemContainer">
-                                                                                                                            <table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnFollowContentItem">
-                                                                                                                                <tbody>
-                                                                                                                                    <tr>
-                                                                                                                                        <td align="left" valign="middle" style="padding-top:5px; padding-right:10px; padding-bottom:5px; padding-left:9px;">
-                                                                                                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" width="">
-                                                                                                                                                <tbody>
-                                                                                                                                                    <tr>
-
-                                                                                                                                                        <td align="center" valign="middle" width="24" class="mcnFollowIconContent">
-                                                                                                                                                            <a href="https://organizr.app" target="_blank"><img src="https://cdn-images.mailchimp.com/icons/social-block-v2/color-link-48.png" style="display:block;" height="24" width="24" class=""></a>
-                                                                                                                                                        </td>
-
-                                                                                                                                                        <td align="left" valign="middle" class="mcnFollowTextContent" style="padding-left:5px;">
-                                                                                                                                                            <a href="https://organizr.app" target="" style="font-family: Helvetica;font-size: 12px;text-decoration: none;color: #FFFFFF;font-weight: bold;">Website</a>
-                                                                                                                                                        </td>
-
-                                                                                                                                                    </tr>
-                                                                                                                                                </tbody>
-                                                                                                                                            </table>
-                                                                                                                                        </td>
-                                                                                                                                    </tr>
-                                                                                                                                </tbody>
-                                                                                                                            </table>
-                                                                                                                        </td>
-                                                                                                                    </tr>
-                                                                                                                </tbody>
-                                                                                                            </table>
-
-                                                                                                            <!--[if mso]>
-                                        </td>
-                                        <![endif]-->
-
-                                                                                                            <!--[if mso]>
-                                        <td align="center" valign="top">
-                                        <![endif]-->
-
-                                                                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" style="display:inline;">
-                                                                                                                <tbody>
-                                                                                                                    <tr>
-                                                                                                                        <td valign="top" style="padding-right:0; padding-bottom:9px;" class="mcnFollowContentItemContainer">
-                                                                                                                            <table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnFollowContentItem">
-                                                                                                                                <tbody>
-                                                                                                                                    <tr>
-                                                                                                                                        <td align="left" valign="middle" style="padding-top:5px; padding-right:10px; padding-bottom:5px; padding-left:9px;">
-                                                                                                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" width="">
-                                                                                                                                                <tbody>
-                                                                                                                                                    <tr>
-
-                                                                                                                                                        <td align="center" valign="middle" width="24" class="mcnFollowIconContent">
-                                                                                                                                                            <a href="https://discord.gg/TrNtY7N" target="_blank"><img src="https://cdn-images.mailchimp.com/icons/social-block-v2/color-link-48.png" style="display:block;" height="24" width="24" class=""></a>
-                                                                                                                                                        </td>
-
-                                                                                                                                                        <td align="left" valign="middle" class="mcnFollowTextContent" style="padding-left:5px;">
-                                                                                                                                                            <a href="https://discord.gg/TrNtY7N" target="" style="font-family: Helvetica;font-size: 12px;text-decoration: none;color: #FFFFFF;font-weight: bold;">Discord</a>
-                                                                                                                                                        </td>
-
-                                                                                                                                                    </tr>
-                                                                                                                                                </tbody>
-                                                                                                                                            </table>
-                                                                                                                                        </td>
-                                                                                                                                    </tr>
-                                                                                                                                </tbody>
-                                                                                                                            </table>
-                                                                                                                        </td>
-                                                                                                                    </tr>
-                                                                                                                </tbody>
-                                                                                                            </table>
+                                                                                                           
 
 
                                                                                                             <!--[if mso]>
                                                                                                             <!--[if mso]>
                                         </td>
                                         </td>
@@ -1173,17 +1019,7 @@ $email = '
                                                             <!--[if mso]>
                                                             <!--[if mso]>
 				<td valign="top" width="600" style="width:600px;">
 				<td valign="top" width="600" style="width:600px;">
 				<![endif]-->
 				<![endif]-->
-                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" style="max-width:100%; min-width:100%;" width="100%" class="mcnTextContentContainer">
-                                                                <tbody>
-                                                                    <tr>
-
-                                                                        <td valign="top" class="mcnTextContent" style="padding-top:0; padding-right:18px; padding-bottom:9px; padding-left:18px;">
-
-                                                                            <em>Powered By: Organizr</em>
-                                                                        </td>
-                                                                    </tr>
-                                                                </tbody>
-                                                            </table>
+                                                            
                                                             <!--[if mso]>
                                                             <!--[if mso]>
 				</td>
 				</td>
 				<![endif]-->
 				<![endif]-->

+ 0 - 19
api/plugins/php-mailer/misc/emailTemplates/gray.php

@@ -95,25 +95,6 @@ $email = '
 												</td>
 												</td>
 											</tr>
 											</tr>
 											' . $button . '
 											' . $button . '
-											<tr>
-												<td class="very-bottom" style="font-family:\'Helvetica\', Arial, sans-serif;color:#73747C;padding:0;text-align:left;font-size:14px;">
-													<table border="0" cellspacing="0" class="full-width" style="width: 100%;">
-														<tbody>
-															<tr>
-																<td class="round-bottom" style="font-family:\'Helvetica\', Arial, sans-serif;color:#73747C;padding:32px;text-align:left;border:1px solid #e9e9e9;border-top:0;border-radius:0 0 10px 10px;background-color:#ffffff;">
-																	<table border="0" class="table-basic full-width" style="border-collapse:collapse;border:0px solid #000;width:100%;">
-																		<tbody>
-																			<tr>
-																				<td style="font-family: \'Helvetica\', Arial, sans-serif; color: #73747C; padding: 0; text-align: left;" width="50%"><b>Powered By:</b> Organizr</td>
-																			</tr>
-																		</tbody>
-																	</table>
-																</td>
-															</tr>
-														</tbody>
-													</table>
-												</td>
-											</tr>
 										</tbody>
 										</tbody>
 									</table>
 									</table>
 								</td>
 								</td>

+ 0 - 3
api/plugins/php-mailer/misc/emailTemplates/light.php

@@ -56,9 +56,6 @@ $email = '
  					</tbody>
  					</tbody>
  				</table>
  				</table>
  			</div>
  			</div>
- 			<div style="text-align: center; font-size: 12px; color: #b2b2b5; margin-top: 20px">
- 				<p>Powered by Organizr<br></p>
- 			</div>
  		</div>
  		</div>
  	</div>
  	</div>
  </body>
  </body>

+ 0 - 15
api/plugins/php-mailer/misc/emailTemplates/plehex.php

@@ -171,21 +171,6 @@ $email = '
 			</tr>
 			</tr>
 		</tbody>
 		</tbody>
 	</table>
 	</table>
-	<table bgcolor="#3F4245" border="0" cellpadding="0" cellspacing="0" class="100p" style="background-color: #3f4245; min-width: 100%;" width="100%">
-		<tbody>
-			<tr>
-				<td align="center" class="container-mobile" style="padding: 60px 25px 60px 25px;" valign="top">
-					<table border="0" cellpadding="0" cellspacing="0" class="100p" style="width: 600px;" width="600">
-						<tbody>
-							<tr>
-								<td align="center" style="font-size: 14px; line-height: 20px; font-family: \'Roboto\', Helvetica, Arial, sans-serif; color: #65686a; font-weight: 400; padding-top: 30px;" valign="top">Powered By: Organizr</td>
-							</tr>
-						</tbody>
-					</table>
-				</td>
-			</tr>
-		</tbody>
-	</table>
 </body>
 </body>
 </html>
 </html>
 ';
 ';

+ 3 - 3
api/plugins/php-mailer/plugin.php

@@ -177,12 +177,12 @@ class PhpMailer extends Organizr
 			$mail->Subject = $emailTemplate['subject'];
 			$mail->Subject = $emailTemplate['subject'];
 			$mail->Body = $this->_phpMailerPluginBuildEmail($emailTemplate);
 			$mail->Body = $this->_phpMailerPluginBuildEmail($emailTemplate);
 			$mail->send();
 			$mail->send();
-			$this->writeLog('success', 'Mail Function -  E-Mail Test Sent', $this->user['username']);
+			$this->setLoggerChannel('Email')->info('E-Mail Test Sent');
 			$msg = ($this->config['PHPMAILER-debugTesting']) ? $this->config['phpmOriginalDebug'] : 'Email sent';
 			$msg = ($this->config['PHPMAILER-debugTesting']) ? $this->config['phpmOriginalDebug'] : 'Email sent';
 			$this->setAPIResponse('success', $msg, 200);
 			$this->setAPIResponse('success', $msg, 200);
 			return true;
 			return true;
 		} catch (PHPMailer\PHPMailer\Exception $e) {
 		} catch (PHPMailer\PHPMailer\Exception $e) {
-			$this->writeLog('error', 'Mail Function -  E-Mail Test Failed[' . $mail->ErrorInfo . ']', $this->user['username']);
+			$this->setLoggerChannel('Email')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());
 			return false;
 			return false;
 		}
 		}
@@ -242,7 +242,7 @@ class PhpMailer extends Organizr
 			$mail->send();
 			$mail->send();
 			return true;
 			return true;
 		} catch (PHPMailer\PHPMailer\Exception $e) {
 		} catch (PHPMailer\PHPMailer\Exception $e) {
-			$this->writeLog('error', 'Mail Function -  E-Mail Test Failed[' . $mail->ErrorInfo . ']', $this->user['username']);
+			$this->setLoggerChannel('Email')->error($e);
 			return $e->errorMessage();
 			return $e->errorMessage();
 		}
 		}
 	}
 	}

+ 22 - 0
api/v2/routes/connectionTester.php

@@ -666,4 +666,26 @@ $app->post('/test/jackett', function ($request, $response, $args) {
 	return $response
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 		->withStatus($GLOBALS['responseCode']);
+});
+$app->post('/test/slack-logs', function ($request, $response, $args) {
+	/**
+	 * @OA\Post(
+	 *     security={{ "api_key":{} }},
+	 *     tags={"test connection"},
+	 *     path="/api/v2/test/slack-logs",
+	 *     summary="Test connection to Slack/Discord",
+	 *     @OA\Response(response="200",description="Success",@OA\JsonContent(ref="#/components/schemas/success-message")),
+	 *     @OA\Response(response="401",description="Unauthorized",@OA\JsonContent(ref="#/components/schemas/unauthorized-message")),
+	 *     @OA\Response(response="422",description="Error",@OA\JsonContent(ref="#/components/schemas/error-message")),
+	 *     @OA\Response(response="500",description="Error",@OA\JsonContent(ref="#/components/schemas/error-message")),
+	 * )
+	 */
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->qualifyRequest(1, true)) {
+		$Organizr->testConnectionSlackLogs();
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
 });
 });

+ 23 - 9
api/v2/routes/update.php

@@ -27,7 +27,6 @@ $app->get('/update', function ($request, $response, $args) {
 	return $response
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 });
 $app->get('/update/download/{branch}', function ($request, $response, $args) {
 $app->get('/update/download/{branch}', function ($request, $response, $args) {
 	/**
 	/**
@@ -51,7 +50,6 @@ $app->get('/update/download/{branch}', function ($request, $response, $args) {
 	return $response
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 });
 $app->get('/update/unzip/{branch}', function ($request, $response, $args) {
 $app->get('/update/unzip/{branch}', function ($request, $response, $args) {
 	/**
 	/**
@@ -75,7 +73,6 @@ $app->get('/update/unzip/{branch}', function ($request, $response, $args) {
 	return $response
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 });
 $app->get('/update/move/{branch}', function ($request, $response, $args) {
 $app->get('/update/move/{branch}', function ($request, $response, $args) {
 	/**
 	/**
@@ -99,7 +96,6 @@ $app->get('/update/move/{branch}', function ($request, $response, $args) {
 	return $response
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 });
 $app->get('/update/cleanup/{branch}', function ($request, $response, $args) {
 $app->get('/update/cleanup/{branch}', function ($request, $response, $args) {
 	/**
 	/**
@@ -123,7 +119,6 @@ $app->get('/update/cleanup/{branch}', function ($request, $response, $args) {
 	return $response
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 });
 $app->get('/update/docker', function ($request, $response, $args) {
 $app->get('/update/docker', function ($request, $response, $args) {
 	/**
 	/**
@@ -147,7 +142,6 @@ $app->get('/update/docker', function ($request, $response, $args) {
 	return $response
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 });
 $app->get('/update/windows', function ($request, $response, $args) {
 $app->get('/update/windows', function ($request, $response, $args) {
 	/**
 	/**
@@ -171,7 +165,6 @@ $app->get('/update/windows', function ($request, $response, $args) {
 	return $response
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 });
 $app->get('/update/linux', function ($request, $response, $args) {
 $app->get('/update/linux', function ($request, $response, $args) {
 	/**
 	/**
@@ -195,7 +188,6 @@ $app->get('/update/linux', function ($request, $response, $args) {
 	return $response
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 });
 $app->get('/update/migrate/{version}', function ($request, $response, $args) {
 $app->get('/update/migrate/{version}', function ($request, $response, $args) {
 	/**
 	/**
@@ -219,5 +211,27 @@ $app->get('/update/migrate/{version}', function ($request, $response, $args) {
 	return $response
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 		->withStatus($GLOBALS['responseCode']);
-	
+});
+$app->get('/update/reset/{feature}', function ($request, $response, $args) {
+	/**
+	 * @OA\Get(
+	 *     security={{ "api_key":{} }},
+	 *     tags={"update"},
+	 *     path="/api/v2/update/reset/{feature}",
+	 *     summary="Reset an Organizr feature back to default values",
+	 *     @OA\Response(response="200",description="Success",@OA\JsonContent(ref="#/components/schemas/success-message")),
+	 *     @OA\Response(response="401",description="Unauthorized",@OA\JsonContent(ref="#/components/schemas/unauthorized-message")),
+	 *     @OA\Response(response="404",description="Error",@OA\JsonContent(ref="#/components/schemas/error-message")),
+	 *     @OA\Response(response="422",description="Error",@OA\JsonContent(ref="#/components/schemas/error-message")),
+	 *     @OA\Response(response="500",description="Error",@OA\JsonContent(ref="#/components/schemas/error-message")),
+	 * )
+	 */
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->qualifyRequest(1, true)) {
+		$Organizr->resetUpdateFeature($args['feature']);
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
 });
 });

+ 5 - 0
api/vendor/autoload.php

@@ -2,6 +2,11 @@
 
 
 // autoload.php @generated by Composer
 // autoload.php @generated by Composer
 
 
+if (PHP_VERSION_ID < 50600) {
+    echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+    exit(1);
+}
+
 require_once __DIR__ . '/composer/autoload_real.php';
 require_once __DIR__ . '/composer/autoload_real.php';
 
 
 return ComposerAutoloaderInitcbdc783d76f8e7563dcce7d8af053ecb::getLoader();
 return ComposerAutoloaderInitcbdc783d76f8e7563dcce7d8af053ecb::getLoader();

+ 415 - 0
api/vendor/brick/math/CHANGELOG.md

@@ -0,0 +1,415 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [0.9.3](https://github.com/brick/math/releases/tag/0.9.3) - 2021-08-15
+
+🚀 **Compatibility with PHP 8.1**
+
+- Support for custom object serialization; this removes a warning on PHP 8.1 due to the `Serializable` interface being deprecated (thanks @TRowbotham)
+
+## [0.9.2](https://github.com/brick/math/releases/tag/0.9.2) - 2021-01-20
+
+🐛 **Bug fix**
+
+- Incorrect results could be returned when using the BCMath calculator, with a default scale set with `bcscale()`, on PHP >= 7.2 (#55).
+
+## [0.9.1](https://github.com/brick/math/releases/tag/0.9.1) - 2020-08-19
+
+✨ New features
+
+- `BigInteger::not()` returns the bitwise `NOT` value
+
+🐛 **Bug fixes**
+
+- `BigInteger::toBytes()` could return an incorrect binary representation for some numbers
+- The bitwise operations `and()`, `or()`, `xor()` on `BigInteger` could return an incorrect result when the GMP extension is not available
+
+## [0.9.0](https://github.com/brick/math/releases/tag/0.9.0) - 2020-08-18
+
+👌 **Improvements**
+
+- `BigNumber::of()` now accepts `.123` and `123.` formats, both of which return a `BigDecimal`
+
+💥 **Breaking changes**
+
+- Deprecated method `BigInteger::powerMod()` has been removed - use `modPow()` instead
+- Deprecated method `BigInteger::parse()` has been removed - use `fromBase()` instead
+
+## [0.8.17](https://github.com/brick/math/releases/tag/0.8.17) - 2020-08-19
+
+🐛 **Bug fix**
+
+- `BigInteger::toBytes()` could return an incorrect binary representation for some numbers
+- The bitwise operations `and()`, `or()`, `xor()` on `BigInteger` could return an incorrect result when the GMP extension is not available
+
+## [0.8.16](https://github.com/brick/math/releases/tag/0.8.16) - 2020-08-18
+
+🚑 **Critical fix**
+
+- This version reintroduces the deprecated `BigInteger::parse()` method, that has been removed by mistake in version `0.8.9` and should have lasted for the whole `0.8` release cycle.
+
+✨ **New features**
+
+- `BigInteger::modInverse()` calculates a modular multiplicative inverse
+- `BigInteger::fromBytes()` creates a `BigInteger` from a byte string
+- `BigInteger::toBytes()` converts a `BigInteger` to a byte string
+- `BigInteger::randomBits()` creates a pseudo-random `BigInteger` of a given bit length
+- `BigInteger::randomRange()` creates a pseudo-random `BigInteger` between two bounds
+
+💩 **Deprecations**
+
+- `BigInteger::powerMod()` is now deprecated in favour of `modPow()`
+
+## [0.8.15](https://github.com/brick/math/releases/tag/0.8.15) - 2020-04-15
+
+🐛 **Fixes**
+
+- added missing `ext-json` requirement, due to `BigNumber` implementing `JsonSerializable`
+
+⚡️ **Optimizations**
+
+- additional optimization in `BigInteger::remainder()`
+
+## [0.8.14](https://github.com/brick/math/releases/tag/0.8.14) - 2020-02-18
+
+✨ **New features**
+
+- `BigInteger::getLowestSetBit()` returns the index of the rightmost one bit
+
+## [0.8.13](https://github.com/brick/math/releases/tag/0.8.13) - 2020-02-16
+
+✨ **New features**
+
+- `BigInteger::isEven()` tests whether the number is even
+- `BigInteger::isOdd()` tests whether the number is odd
+- `BigInteger::testBit()` tests if a bit is set
+- `BigInteger::getBitLength()` returns the number of bits in the minimal representation of the number
+
+## [0.8.12](https://github.com/brick/math/releases/tag/0.8.12) - 2020-02-03
+
+🛠️ **Maintenance release**
+
+Classes are now annotated for better static analysis with [psalm](https://psalm.dev/).
+
+This is a maintenance release: no bug fixes, no new features, no breaking changes.
+
+## [0.8.11](https://github.com/brick/math/releases/tag/0.8.11) - 2020-01-23
+
+✨ **New feature**
+
+`BigInteger::powerMod()` performs a power-with-modulo operation. Useful for crypto.
+
+## [0.8.10](https://github.com/brick/math/releases/tag/0.8.10) - 2020-01-21
+
+✨ **New feature**
+
+`BigInteger::mod()` returns the **modulo** of two numbers. The *modulo* differs from the *remainder* when the signs of the operands are different.
+
+## [0.8.9](https://github.com/brick/math/releases/tag/0.8.9) - 2020-01-08
+
+⚡️ **Performance improvements**
+
+A few additional optimizations in `BigInteger` and `BigDecimal` when one of the operands can be returned as is. Thanks to @tomtomsen in #24.
+
+## [0.8.8](https://github.com/brick/math/releases/tag/0.8.8) - 2019-04-25
+
+🐛 **Bug fixes**
+
+- `BigInteger::toBase()` could return an empty string for zero values (BCMath & Native calculators only, GMP calculator unaffected)
+
+✨ **New features**
+
+- `BigInteger::toArbitraryBase()` converts a number to an arbitrary base, using a custom alphabet
+- `BigInteger::fromArbitraryBase()` converts a string in an arbitrary base, using a custom alphabet, back to a number
+
+These methods can be used as the foundation to convert strings between different bases/alphabets, using BigInteger as an intermediate representation.
+
+💩 **Deprecations**
+
+- `BigInteger::parse()` is now deprecated in favour of `fromBase()`
+
+`BigInteger::fromBase()` works the same way as `parse()`, with 2 minor differences:
+
+- the `$base` parameter is required, it does not default to `10`
+- it throws a `NumberFormatException` instead of an `InvalidArgumentException` when the number is malformed
+
+## [0.8.7](https://github.com/brick/math/releases/tag/0.8.7) - 2019-04-20
+
+**Improvements**
+
+- Safer conversion from `float` when using custom locales
+- **Much faster** `NativeCalculator` implementation 🚀
+
+You can expect **at least a 3x performance improvement** for common arithmetic operations when using the library on systems without GMP or BCMath; it gets exponentially faster on multiplications with a high number of digits. This is due to calculations now being performed on whole blocks of digits (the block size depending on the platform, 32-bit or 64-bit) instead of digit-by-digit as before.
+
+## [0.8.6](https://github.com/brick/math/releases/tag/0.8.6) - 2019-04-11
+
+**New method**
+
+`BigNumber::sum()` returns the sum of one or more numbers.
+
+## [0.8.5](https://github.com/brick/math/releases/tag/0.8.5) - 2019-02-12
+
+**Bug fix**: `of()` factory methods could fail when passing a `float` in environments using a `LC_NUMERIC` locale with a decimal separator other than `'.'` (#20).
+
+Thanks @manowark 👍
+
+## [0.8.4](https://github.com/brick/math/releases/tag/0.8.4) - 2018-12-07
+
+**New method**
+
+`BigDecimal::sqrt()` calculates the square root of a decimal number, to a given scale.
+
+## [0.8.3](https://github.com/brick/math/releases/tag/0.8.3) - 2018-12-06
+
+**New method**
+
+`BigInteger::sqrt()` calculates the square root of a number (thanks @peter279k).
+
+**New exception**
+
+`NegativeNumberException` is thrown when calling `sqrt()` on a negative number.
+
+## [0.8.2](https://github.com/brick/math/releases/tag/0.8.2) - 2018-11-08
+
+**Performance update**
+
+- Further improvement of `toInt()` performance
+- `NativeCalculator` can now perform some multiplications more efficiently
+
+## [0.8.1](https://github.com/brick/math/releases/tag/0.8.1) - 2018-11-07
+
+Performance optimization of `toInt()` methods.
+
+## [0.8.0](https://github.com/brick/math/releases/tag/0.8.0) - 2018-10-13
+
+**Breaking changes**
+
+The following deprecated methods have been removed. Use the new method name instead:
+
+| Method removed | Replacement method |
+| --- | --- |
+| `BigDecimal::getIntegral()` | `BigDecimal::getIntegralPart()` |
+| `BigDecimal::getFraction()` | `BigDecimal::getFractionalPart()` |
+
+---
+
+**New features**
+
+`BigInteger` has been augmented with 5 new methods for bitwise operations:
+
+| New method | Description |
+| --- | --- |
+| `and()` | performs a bitwise `AND` operation on two numbers |
+| `or()` | performs a bitwise `OR` operation on two numbers |
+| `xor()` | performs a bitwise `XOR` operation on two numbers |
+| `shiftedLeft()` | returns the number shifted left by a number of bits |
+| `shiftedRight()` | returns the number shifted right by a number of bits |
+
+Thanks to @DASPRiD 👍
+
+## [0.7.3](https://github.com/brick/math/releases/tag/0.7.3) - 2018-08-20
+
+**New method:** `BigDecimal::hasNonZeroFractionalPart()`
+
+**Renamed/deprecated methods:**
+
+- `BigDecimal::getIntegral()` has been renamed to `getIntegralPart()` and is now deprecated
+- `BigDecimal::getFraction()` has been renamed to `getFractionalPart()` and is now deprecated
+
+## [0.7.2](https://github.com/brick/math/releases/tag/0.7.2) - 2018-07-21
+
+**Performance update**
+
+`BigInteger::parse()` and `toBase()` now use GMP's built-in base conversion features when available.
+
+## [0.7.1](https://github.com/brick/math/releases/tag/0.7.1) - 2018-03-01
+
+This is a maintenance release, no code has been changed.
+
+- When installed with `--no-dev`, the autoloader does not autoload tests anymore
+- Tests and other files unnecessary for production are excluded from the dist package
+
+This will help make installations more compact.
+
+## [0.7.0](https://github.com/brick/math/releases/tag/0.7.0) - 2017-10-02
+
+Methods renamed:
+
+- `BigNumber:sign()` has been renamed to `getSign()`
+- `BigDecimal::unscaledValue()` has been renamed to `getUnscaledValue()`
+- `BigDecimal::scale()` has been renamed to `getScale()`
+- `BigDecimal::integral()` has been renamed to `getIntegral()`
+- `BigDecimal::fraction()` has been renamed to `getFraction()`
+- `BigRational::numerator()` has been renamed to `getNumerator()`
+- `BigRational::denominator()` has been renamed to `getDenominator()`
+
+Classes renamed:
+
+- `ArithmeticException` has been renamed to `MathException`
+
+## [0.6.2](https://github.com/brick/math/releases/tag/0.6.2) - 2017-10-02
+
+The base class for all exceptions is now `MathException`.
+`ArithmeticException` has been deprecated, and will be removed in 0.7.0.
+
+## [0.6.1](https://github.com/brick/math/releases/tag/0.6.1) - 2017-10-02
+
+A number of methods have been renamed:
+
+- `BigNumber:sign()` is deprecated; use `getSign()` instead
+- `BigDecimal::unscaledValue()` is deprecated; use `getUnscaledValue()` instead
+- `BigDecimal::scale()` is deprecated; use `getScale()` instead
+- `BigDecimal::integral()` is deprecated; use `getIntegral()` instead
+- `BigDecimal::fraction()` is deprecated; use `getFraction()` instead
+- `BigRational::numerator()` is deprecated; use `getNumerator()` instead
+- `BigRational::denominator()` is deprecated; use `getDenominator()` instead
+
+The old methods will be removed in version 0.7.0.
+
+## [0.6.0](https://github.com/brick/math/releases/tag/0.6.0) - 2017-08-25
+
+- Minimum PHP version is now [7.1](https://gophp71.org/); for PHP 5.6 and PHP 7.0 support, use version `0.5`
+- Deprecated method `BigDecimal::withScale()` has been removed; use `toScale()` instead
+- Method `BigNumber::toInteger()` has been renamed to `toInt()`
+
+## [0.5.4](https://github.com/brick/math/releases/tag/0.5.4) - 2016-10-17
+
+`BigNumber` classes now implement [JsonSerializable](http://php.net/manual/en/class.jsonserializable.php).
+The JSON output is always a string.
+
+## [0.5.3](https://github.com/brick/math/releases/tag/0.5.3) - 2016-03-31
+
+This is a bugfix release. Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
+
+## [0.5.2](https://github.com/brick/math/releases/tag/0.5.2) - 2015-08-06
+
+The `$scale` parameter of `BigDecimal::dividedBy()` is now optional again.
+
+## [0.5.1](https://github.com/brick/math/releases/tag/0.5.1) - 2015-07-05
+
+**New method: `BigNumber::toScale()`**
+
+This allows to convert any `BigNumber` to a `BigDecimal` with a given scale, using rounding if necessary.
+
+## [0.5.0](https://github.com/brick/math/releases/tag/0.5.0) - 2015-07-04
+
+**New features**
+- Common `BigNumber` interface for all classes, with the following methods:
+  - `sign()` and derived methods (`isZero()`, `isPositive()`, ...)
+  - `compareTo()` and derived methods (`isEqualTo()`, `isGreaterThan()`, ...) that work across different `BigNumber` types
+  - `toBigInteger()`, `toBigDecimal()`, `toBigRational`() conversion methods
+  - `toInteger()` and `toFloat()` conversion methods to native types
+- Unified `of()` behaviour: every class now accepts any type of number, provided that it can be safely converted to the current type
+- New method: `BigDecimal::exactlyDividedBy()`; this method automatically computes the scale of the result, provided that the division yields a finite number of digits
+- New methods: `BigRational::quotient()` and `remainder()`
+- Fine-grained exceptions: `DivisionByZeroException`, `RoundingNecessaryException`, `NumberFormatException`
+- Factory methods `zero()`, `one()` and `ten()` available in all classes
+- Rounding mode reintroduced in `BigInteger::dividedBy()`
+
+This release also comes with many performance improvements.
+
+---
+
+**Breaking changes**
+- `BigInteger`:
+  - `getSign()` is renamed to `sign()`
+  - `toString()` is renamed to `toBase()`
+  - `BigInteger::dividedBy()` now throws an exception by default if the remainder is not zero; use `quotient()` to get the previous behaviour
+- `BigDecimal`:
+  - `getSign()` is renamed to `sign()`
+  - `getUnscaledValue()` is renamed to `unscaledValue()`
+  - `getScale()` is renamed to `scale()`
+  - `getIntegral()` is renamed to `integral()`
+  - `getFraction()` is renamed to `fraction()`
+  - `divideAndRemainder()` is renamed to `quotientAndRemainder()`
+  - `dividedBy()` now takes a **mandatory** `$scale` parameter **before** the rounding mode
+  - `toBigInteger()` does not accept a `$roundingMode` parameter any more
+  - `toBigRational()` does not simplify the fraction any more; explicitly add `->simplified()` to get the previous behaviour
+- `BigRational`:
+  - `getSign()` is renamed to `sign()`
+  - `getNumerator()` is renamed to  `numerator()`
+  - `getDenominator()` is renamed to  `denominator()`
+  - `of()` is renamed to `nd()`, while `parse()` is renamed to `of()`
+- Miscellaneous:
+  - `ArithmeticException` is moved to an `Exception\` sub-namespace
+  - `of()` factory methods now throw `NumberFormatException` instead of `InvalidArgumentException`
+
+## [0.4.3](https://github.com/brick/math/releases/tag/0.4.3) - 2016-03-31
+
+Backport of two bug fixes from the 0.5 branch:
+- `BigInteger::parse()` did not always throw `InvalidArgumentException` as expected
+- Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
+
+## [0.4.2](https://github.com/brick/math/releases/tag/0.4.2) - 2015-06-16
+
+New method: `BigDecimal::stripTrailingZeros()`
+
+## [0.4.1](https://github.com/brick/math/releases/tag/0.4.1) - 2015-06-12
+
+Introducing a `BigRational` class, to perform calculations on fractions of any size.
+
+## [0.4.0](https://github.com/brick/math/releases/tag/0.4.0) - 2015-06-12
+
+Rounding modes have been removed from `BigInteger`, and are now a concept specific to `BigDecimal`.
+
+`BigInteger::dividedBy()` now always returns the quotient of the division.
+
+## [0.3.5](https://github.com/brick/math/releases/tag/0.3.5) - 2016-03-31
+
+Backport of two bug fixes from the 0.5 branch:
+
+- `BigInteger::parse()` did not always throw `InvalidArgumentException` as expected
+- Dividing by a negative power of 1 with the same scale as the dividend could trigger an incorrect optimization which resulted in a wrong result. See #6.
+
+## [0.3.4](https://github.com/brick/math/releases/tag/0.3.4) - 2015-06-11
+
+New methods:
+- `BigInteger::remainder()` returns the remainder of a division only
+- `BigInteger::gcd()` returns the greatest common divisor of two numbers
+
+## [0.3.3](https://github.com/brick/math/releases/tag/0.3.3) - 2015-06-07
+
+Fix `toString()` not handling negative numbers.
+
+## [0.3.2](https://github.com/brick/math/releases/tag/0.3.2) - 2015-06-07
+
+`BigInteger` and `BigDecimal` now have a `getSign()` method that returns:
+- `-1` if the number is negative
+- `0` if the number is zero
+- `1` if the number is positive
+
+## [0.3.1](https://github.com/brick/math/releases/tag/0.3.1) - 2015-06-05
+
+Minor performance improvements
+
+## [0.3.0](https://github.com/brick/math/releases/tag/0.3.0) - 2015-06-04
+
+The `$roundingMode` and `$scale` parameters have been swapped in `BigDecimal::dividedBy()`.
+
+## [0.2.2](https://github.com/brick/math/releases/tag/0.2.2) - 2015-06-04
+
+Stronger immutability guarantee for `BigInteger` and `BigDecimal`.
+
+So far, it would have been possible to break immutability of these classes by calling the `unserialize()` internal function. This release fixes that.
+
+## [0.2.1](https://github.com/brick/math/releases/tag/0.2.1) - 2015-06-02
+
+Added `BigDecimal::divideAndRemainder()`
+
+## [0.2.0](https://github.com/brick/math/releases/tag/0.2.0) - 2015-05-22
+
+- `min()` and `max()` do not accept an `array` any more, but a variable number of parameters
+- **minimum PHP version is now 5.6**
+- continuous integration with PHP 7
+
+## [0.1.1](https://github.com/brick/math/releases/tag/0.1.1) - 2014-09-01
+
+- Added `BigInteger::power()`
+- Added HHVM support
+
+## [0.1.0](https://github.com/brick/math/releases/tag/0.1.0) - 2014-08-31
+
+First beta release.
+

+ 20 - 0
api/vendor/brick/math/LICENSE

@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2013-present Benjamin Morel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 17 - 0
api/vendor/brick/math/SECURITY.md

@@ -0,0 +1,17 @@
+# Security Policy
+
+## Supported Versions
+
+Only the last two release streams are supported.
+
+| Version | Supported          |
+| ------- | ------------------ |
+| 0.9.x   | :white_check_mark: |
+| 0.8.x   | :white_check_mark: |
+| < 0.8   | :x:                |
+
+## Reporting a Vulnerability
+
+To report a security vulnerability, please use the
+[Tidelift security contact](https://tidelift.com/security).
+Tidelift will coordinate the fix and disclosure.

+ 35 - 0
api/vendor/brick/math/composer.json

@@ -0,0 +1,35 @@
+{
+    "name": "brick/math",
+    "description": "Arbitrary-precision arithmetic library",
+    "type": "library",
+    "keywords": [
+        "Brick",
+        "Math",
+        "Arbitrary-precision",
+        "Arithmetic",
+        "BigInteger",
+        "BigDecimal",
+        "BigRational",
+        "Bignum"
+    ],
+    "license": "MIT",
+    "require": {
+        "php": "^7.1 || ^8.0",
+        "ext-json": "*"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0",
+        "php-coveralls/php-coveralls": "^2.2",
+        "vimeo/psalm": "4.9.2"
+    },
+    "autoload": {
+        "psr-4": {
+            "Brick\\Math\\": "src/"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "Brick\\Math\\Tests\\": "tests/"
+        }
+    }
+}

+ 895 - 0
api/vendor/brick/math/src/BigDecimal.php

@@ -0,0 +1,895 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Brick\Math;
+
+use Brick\Math\Exception\DivisionByZeroException;
+use Brick\Math\Exception\MathException;
+use Brick\Math\Exception\NegativeNumberException;
+use Brick\Math\Internal\Calculator;
+
+/**
+ * Immutable, arbitrary-precision signed decimal numbers.
+ *
+ * @psalm-immutable
+ */
+final class BigDecimal extends BigNumber
+{
+    /**
+     * The unscaled value of this decimal number.
+     *
+     * This is a string of digits with an optional leading minus sign.
+     * No leading zero must be present.
+     * No leading minus sign must be present if the value is 0.
+     *
+     * @var string
+     */
+    private $value;
+
+    /**
+     * The scale (number of digits after the decimal point) of this decimal number.
+     *
+     * This must be zero or more.
+     *
+     * @var int
+     */
+    private $scale;
+
+    /**
+     * Protected constructor. Use a factory method to obtain an instance.
+     *
+     * @param string $value The unscaled value, validated.
+     * @param int    $scale The scale, validated.
+     */
+    protected function __construct(string $value, int $scale = 0)
+    {
+        $this->value = $value;
+        $this->scale = $scale;
+    }
+
+    /**
+     * Creates a BigDecimal of the given value.
+     *
+     * @param BigNumber|int|float|string $value
+     *
+     * @return BigDecimal
+     *
+     * @throws MathException If the value cannot be converted to a BigDecimal.
+     *
+     * @psalm-pure
+     */
+    public static function of($value) : BigNumber
+    {
+        return parent::of($value)->toBigDecimal();
+    }
+
+    /**
+     * Creates a BigDecimal from an unscaled value and a scale.
+     *
+     * Example: `(12345, 3)` will result in the BigDecimal `12.345`.
+     *
+     * @param BigNumber|int|float|string $value The unscaled value. Must be convertible to a BigInteger.
+     * @param int                        $scale The scale of the number, positive or zero.
+     *
+     * @return BigDecimal
+     *
+     * @throws \InvalidArgumentException If the scale is negative.
+     *
+     * @psalm-pure
+     */
+    public static function ofUnscaledValue($value, int $scale = 0) : BigDecimal
+    {
+        if ($scale < 0) {
+            throw new \InvalidArgumentException('The scale cannot be negative.');
+        }
+
+        return new BigDecimal((string) BigInteger::of($value), $scale);
+    }
+
+    /**
+     * Returns a BigDecimal representing zero, with a scale of zero.
+     *
+     * @return BigDecimal
+     *
+     * @psalm-pure
+     */
+    public static function zero() : BigDecimal
+    {
+        /**
+         * @psalm-suppress ImpureStaticVariable
+         * @var BigDecimal|null $zero
+         */
+        static $zero;
+
+        if ($zero === null) {
+            $zero = new BigDecimal('0');
+        }
+
+        return $zero;
+    }
+
+    /**
+     * Returns a BigDecimal representing one, with a scale of zero.
+     *
+     * @return BigDecimal
+     *
+     * @psalm-pure
+     */
+    public static function one() : BigDecimal
+    {
+        /**
+         * @psalm-suppress ImpureStaticVariable
+         * @var BigDecimal|null $one
+         */
+        static $one;
+
+        if ($one === null) {
+            $one = new BigDecimal('1');
+        }
+
+        return $one;
+    }
+
+    /**
+     * Returns a BigDecimal representing ten, with a scale of zero.
+     *
+     * @return BigDecimal
+     *
+     * @psalm-pure
+     */
+    public static function ten() : BigDecimal
+    {
+        /**
+         * @psalm-suppress ImpureStaticVariable
+         * @var BigDecimal|null $ten
+         */
+        static $ten;
+
+        if ($ten === null) {
+            $ten = new BigDecimal('10');
+        }
+
+        return $ten;
+    }
+
+    /**
+     * Returns the sum of this number and the given one.
+     *
+     * The result has a scale of `max($this->scale, $that->scale)`.
+     *
+     * @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigDecimal.
+     *
+     * @return BigDecimal The result.
+     *
+     * @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
+     */
+    public function plus($that) : BigDecimal
+    {
+        $that = BigDecimal::of($that);
+
+        if ($that->value === '0' && $that->scale <= $this->scale) {
+            return $this;
+        }
+
+        if ($this->value === '0' && $this->scale <= $that->scale) {
+            return $that;
+        }
+
+        [$a, $b] = $this->scaleValues($this, $that);
+
+        $value = Calculator::get()->add($a, $b);
+        $scale = $this->scale > $that->scale ? $this->scale : $that->scale;
+
+        return new BigDecimal($value, $scale);
+    }
+
+    /**
+     * Returns the difference of this number and the given one.
+     *
+     * The result has a scale of `max($this->scale, $that->scale)`.
+     *
+     * @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigDecimal.
+     *
+     * @return BigDecimal The result.
+     *
+     * @throws MathException If the number is not valid, or is not convertible to a BigDecimal.
+     */
+    public function minus($that) : BigDecimal
+    {
+        $that = BigDecimal::of($that);
+
+        if ($that->value === '0' && $that->scale <= $this->scale) {
+            return $this;
+        }
+
+        [$a, $b] = $this->scaleValues($this, $that);
+
+        $value = Calculator::get()->sub($a, $b);
+        $scale = $this->scale > $that->scale ? $this->scale : $that->scale;
+
+        return new BigDecimal($value, $scale);
+    }
+
+    /**
+     * Returns the product of this number and the given one.
+     *
+     * The result has a scale of `$this->scale + $that->scale`.
+     *
+     * @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigDecimal.
+     *
+     * @return BigDecimal The result.
+     *
+     * @throws MathException If the multiplier is not a valid number, or is not convertible to a BigDecimal.
+     */
+    public function multipliedBy($that) : BigDecimal
+    {
+        $that = BigDecimal::of($that);
+
+        if ($that->value === '1' && $that->scale === 0) {
+            return $this;
+        }
+
+        if ($this->value === '1' && $this->scale === 0) {
+            return $that;
+        }
+
+        $value = Calculator::get()->mul($this->value, $that->value);
+        $scale = $this->scale + $that->scale;
+
+        return new BigDecimal($value, $scale);
+    }
+
+    /**
+     * Returns the result of the division of this number by the given one, at the given scale.
+     *
+     * @param BigNumber|int|float|string $that         The divisor.
+     * @param int|null                   $scale        The desired scale, or null to use the scale of this number.
+     * @param int                        $roundingMode An optional rounding mode.
+     *
+     * @return BigDecimal
+     *
+     * @throws \InvalidArgumentException If the scale or rounding mode is invalid.
+     * @throws MathException             If the number is invalid, is zero, or rounding was necessary.
+     */
+    public function dividedBy($that, ?int $scale = null, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
+    {
+        $that = BigDecimal::of($that);
+
+        if ($that->isZero()) {
+            throw DivisionByZeroException::divisionByZero();
+        }
+
+        if ($scale === null) {
+            $scale = $this->scale;
+        } elseif ($scale < 0) {
+            throw new \InvalidArgumentException('Scale cannot be negative.');
+        }
+
+        if ($that->value === '1' && $that->scale === 0 && $scale === $this->scale) {
+            return $this;
+        }
+
+        $p = $this->valueWithMinScale($that->scale + $scale);
+        $q = $that->valueWithMinScale($this->scale - $scale);
+
+        $result = Calculator::get()->divRound($p, $q, $roundingMode);
+
+        return new BigDecimal($result, $scale);
+    }
+
+    /**
+     * Returns the exact result of the division of this number by the given one.
+     *
+     * The scale of the result is automatically calculated to fit all the fraction digits.
+     *
+     * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
+     *
+     * @return BigDecimal The result.
+     *
+     * @throws MathException If the divisor is not a valid number, is not convertible to a BigDecimal, is zero,
+     *                       or the result yields an infinite number of digits.
+     */
+    public function exactlyDividedBy($that) : BigDecimal
+    {
+        $that = BigDecimal::of($that);
+
+        if ($that->value === '0') {
+            throw DivisionByZeroException::divisionByZero();
+        }
+
+        [, $b] = $this->scaleValues($this, $that);
+
+        $d = \rtrim($b, '0');
+        $scale = \strlen($b) - \strlen($d);
+
+        $calculator = Calculator::get();
+
+        foreach ([5, 2] as $prime) {
+            for (;;) {
+                $lastDigit = (int) $d[-1];
+
+                if ($lastDigit % $prime !== 0) {
+                    break;
+                }
+
+                $d = $calculator->divQ($d, (string) $prime);
+                $scale++;
+            }
+        }
+
+        return $this->dividedBy($that, $scale)->stripTrailingZeros();
+    }
+
+    /**
+     * Returns this number exponentiated to the given value.
+     *
+     * The result has a scale of `$this->scale * $exponent`.
+     *
+     * @param int $exponent The exponent.
+     *
+     * @return BigDecimal The result.
+     *
+     * @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
+     */
+    public function power(int $exponent) : BigDecimal
+    {
+        if ($exponent === 0) {
+            return BigDecimal::one();
+        }
+
+        if ($exponent === 1) {
+            return $this;
+        }
+
+        if ($exponent < 0 || $exponent > Calculator::MAX_POWER) {
+            throw new \InvalidArgumentException(\sprintf(
+                'The exponent %d is not in the range 0 to %d.',
+                $exponent,
+                Calculator::MAX_POWER
+            ));
+        }
+
+        return new BigDecimal(Calculator::get()->pow($this->value, $exponent), $this->scale * $exponent);
+    }
+
+    /**
+     * Returns the quotient of the division of this number by this given one.
+     *
+     * The quotient has a scale of `0`.
+     *
+     * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
+     *
+     * @return BigDecimal The quotient.
+     *
+     * @throws MathException If the divisor is not a valid decimal number, or is zero.
+     */
+    public function quotient($that) : BigDecimal
+    {
+        $that = BigDecimal::of($that);
+
+        if ($that->isZero()) {
+            throw DivisionByZeroException::divisionByZero();
+        }
+
+        $p = $this->valueWithMinScale($that->scale);
+        $q = $that->valueWithMinScale($this->scale);
+
+        $quotient = Calculator::get()->divQ($p, $q);
+
+        return new BigDecimal($quotient, 0);
+    }
+
+    /**
+     * Returns the remainder of the division of this number by this given one.
+     *
+     * The remainder has a scale of `max($this->scale, $that->scale)`.
+     *
+     * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
+     *
+     * @return BigDecimal The remainder.
+     *
+     * @throws MathException If the divisor is not a valid decimal number, or is zero.
+     */
+    public function remainder($that) : BigDecimal
+    {
+        $that = BigDecimal::of($that);
+
+        if ($that->isZero()) {
+            throw DivisionByZeroException::divisionByZero();
+        }
+
+        $p = $this->valueWithMinScale($that->scale);
+        $q = $that->valueWithMinScale($this->scale);
+
+        $remainder = Calculator::get()->divR($p, $q);
+
+        $scale = $this->scale > $that->scale ? $this->scale : $that->scale;
+
+        return new BigDecimal($remainder, $scale);
+    }
+
+    /**
+     * Returns the quotient and remainder of the division of this number by the given one.
+     *
+     * The quotient has a scale of `0`, and the remainder has a scale of `max($this->scale, $that->scale)`.
+     *
+     * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal.
+     *
+     * @return BigDecimal[] An array containing the quotient and the remainder.
+     *
+     * @throws MathException If the divisor is not a valid decimal number, or is zero.
+     */
+    public function quotientAndRemainder($that) : array
+    {
+        $that = BigDecimal::of($that);
+
+        if ($that->isZero()) {
+            throw DivisionByZeroException::divisionByZero();
+        }
+
+        $p = $this->valueWithMinScale($that->scale);
+        $q = $that->valueWithMinScale($this->scale);
+
+        [$quotient, $remainder] = Calculator::get()->divQR($p, $q);
+
+        $scale = $this->scale > $that->scale ? $this->scale : $that->scale;
+
+        $quotient = new BigDecimal($quotient, 0);
+        $remainder = new BigDecimal($remainder, $scale);
+
+        return [$quotient, $remainder];
+    }
+
+    /**
+     * Returns the square root of this number, rounded down to the given number of decimals.
+     *
+     * @param int $scale
+     *
+     * @return BigDecimal
+     *
+     * @throws \InvalidArgumentException If the scale is negative.
+     * @throws NegativeNumberException If this number is negative.
+     */
+    public function sqrt(int $scale) : BigDecimal
+    {
+        if ($scale < 0) {
+            throw new \InvalidArgumentException('Scale cannot be negative.');
+        }
+
+        if ($this->value === '0') {
+            return new BigDecimal('0', $scale);
+        }
+
+        if ($this->value[0] === '-') {
+            throw new NegativeNumberException('Cannot calculate the square root of a negative number.');
+        }
+
+        $value = $this->value;
+        $addDigits = 2 * $scale - $this->scale;
+
+        if ($addDigits > 0) {
+            // add zeros
+            $value .= \str_repeat('0', $addDigits);
+        } elseif ($addDigits < 0) {
+            // trim digits
+            if (-$addDigits >= \strlen($this->value)) {
+                // requesting a scale too low, will always yield a zero result
+                return new BigDecimal('0', $scale);
+            }
+
+            $value = \substr($value, 0, $addDigits);
+        }
+
+        $value = Calculator::get()->sqrt($value);
+
+        return new BigDecimal($value, $scale);
+    }
+
+    /**
+     * Returns a copy of this BigDecimal with the decimal point moved $n places to the left.
+     *
+     * @param int $n
+     *
+     * @return BigDecimal
+     */
+    public function withPointMovedLeft(int $n) : BigDecimal
+    {
+        if ($n === 0) {
+            return $this;
+        }
+
+        if ($n < 0) {
+            return $this->withPointMovedRight(-$n);
+        }
+
+        return new BigDecimal($this->value, $this->scale + $n);
+    }
+
+    /**
+     * Returns a copy of this BigDecimal with the decimal point moved $n places to the right.
+     *
+     * @param int $n
+     *
+     * @return BigDecimal
+     */
+    public function withPointMovedRight(int $n) : BigDecimal
+    {
+        if ($n === 0) {
+            return $this;
+        }
+
+        if ($n < 0) {
+            return $this->withPointMovedLeft(-$n);
+        }
+
+        $value = $this->value;
+        $scale = $this->scale - $n;
+
+        if ($scale < 0) {
+            if ($value !== '0') {
+                $value .= \str_repeat('0', -$scale);
+            }
+            $scale = 0;
+        }
+
+        return new BigDecimal($value, $scale);
+    }
+
+    /**
+     * Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part.
+     *
+     * @return BigDecimal
+     */
+    public function stripTrailingZeros() : BigDecimal
+    {
+        if ($this->scale === 0) {
+            return $this;
+        }
+
+        $trimmedValue = \rtrim($this->value, '0');
+
+        if ($trimmedValue === '') {
+            return BigDecimal::zero();
+        }
+
+        $trimmableZeros = \strlen($this->value) - \strlen($trimmedValue);
+
+        if ($trimmableZeros === 0) {
+            return $this;
+        }
+
+        if ($trimmableZeros > $this->scale) {
+            $trimmableZeros = $this->scale;
+        }
+
+        $value = \substr($this->value, 0, -$trimmableZeros);
+        $scale = $this->scale - $trimmableZeros;
+
+        return new BigDecimal($value, $scale);
+    }
+
+    /**
+     * Returns the absolute value of this number.
+     *
+     * @return BigDecimal
+     */
+    public function abs() : BigDecimal
+    {
+        return $this->isNegative() ? $this->negated() : $this;
+    }
+
+    /**
+     * Returns the negated value of this number.
+     *
+     * @return BigDecimal
+     */
+    public function negated() : BigDecimal
+    {
+        return new BigDecimal(Calculator::get()->neg($this->value), $this->scale);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function compareTo($that) : int
+    {
+        $that = BigNumber::of($that);
+
+        if ($that instanceof BigInteger) {
+            $that = $that->toBigDecimal();
+        }
+
+        if ($that instanceof BigDecimal) {
+            [$a, $b] = $this->scaleValues($this, $that);
+
+            return Calculator::get()->cmp($a, $b);
+        }
+
+        return - $that->compareTo($this);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSign() : int
+    {
+        return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1);
+    }
+
+    /**
+     * @return BigInteger
+     */
+    public function getUnscaledValue() : BigInteger
+    {
+        return BigInteger::create($this->value);
+    }
+
+    /**
+     * @return int
+     */
+    public function getScale() : int
+    {
+        return $this->scale;
+    }
+
+    /**
+     * Returns a string representing the integral part of this decimal number.
+     *
+     * Example: `-123.456` => `-123`.
+     *
+     * @return string
+     */
+    public function getIntegralPart() : string
+    {
+        if ($this->scale === 0) {
+            return $this->value;
+        }
+
+        $value = $this->getUnscaledValueWithLeadingZeros();
+
+        return \substr($value, 0, -$this->scale);
+    }
+
+    /**
+     * Returns a string representing the fractional part of this decimal number.
+     *
+     * If the scale is zero, an empty string is returned.
+     *
+     * Examples: `-123.456` => '456', `123` => ''.
+     *
+     * @return string
+     */
+    public function getFractionalPart() : string
+    {
+        if ($this->scale === 0) {
+            return '';
+        }
+
+        $value = $this->getUnscaledValueWithLeadingZeros();
+
+        return \substr($value, -$this->scale);
+    }
+
+    /**
+     * Returns whether this decimal number has a non-zero fractional part.
+     *
+     * @return bool
+     */
+    public function hasNonZeroFractionalPart() : bool
+    {
+        return $this->getFractionalPart() !== \str_repeat('0', $this->scale);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toBigInteger() : BigInteger
+    {
+        $zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0);
+
+        return BigInteger::create($zeroScaleDecimal->value);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toBigDecimal() : BigDecimal
+    {
+        return $this;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toBigRational() : BigRational
+    {
+        $numerator = BigInteger::create($this->value);
+        $denominator = BigInteger::create('1' . \str_repeat('0', $this->scale));
+
+        return BigRational::create($numerator, $denominator, false);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
+    {
+        if ($scale === $this->scale) {
+            return $this;
+        }
+
+        return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toInt() : int
+    {
+        return $this->toBigInteger()->toInt();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toFloat() : float
+    {
+        return (float) (string) $this;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function __toString() : string
+    {
+        if ($this->scale === 0) {
+            return $this->value;
+        }
+
+        $value = $this->getUnscaledValueWithLeadingZeros();
+
+        return \substr($value, 0, -$this->scale) . '.' . \substr($value, -$this->scale);
+    }
+
+    /**
+     * This method is required for serializing the object and SHOULD NOT be accessed directly.
+     *
+     * @internal
+     *
+     * @return array{value: string, scale: int}
+     */
+    public function __serialize(): array
+    {
+        return ['value' => $this->value, 'scale' => $this->scale];
+    }
+
+    /**
+     * This method is only here to allow unserializing the object and cannot be accessed directly.
+     *
+     * @internal
+     * @psalm-suppress RedundantPropertyInitializationCheck
+     *
+     * @param array{value: string, scale: int} $data
+     *
+     * @return void
+     *
+     * @throws \LogicException
+     */
+    public function __unserialize(array $data): void
+    {
+        if (isset($this->value)) {
+            throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
+        }
+
+        $this->value = $data['value'];
+        $this->scale = $data['scale'];
+    }
+
+    /**
+     * This method is required by interface Serializable and SHOULD NOT be accessed directly.
+     *
+     * @internal
+     *
+     * @return string
+     */
+    public function serialize() : string
+    {
+        return $this->value . ':' . $this->scale;
+    }
+
+    /**
+     * This method is only here to implement interface Serializable and cannot be accessed directly.
+     *
+     * @internal
+     * @psalm-suppress RedundantPropertyInitializationCheck
+     *
+     * @param string $value
+     *
+     * @return void
+     *
+     * @throws \LogicException
+     */
+    public function unserialize($value) : void
+    {
+        if (isset($this->value)) {
+            throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
+        }
+
+        [$value, $scale] = \explode(':', $value);
+
+        $this->value = $value;
+        $this->scale = (int) $scale;
+    }
+
+    /**
+     * Puts the internal values of the given decimal numbers on the same scale.
+     *
+     * @param BigDecimal $x The first decimal number.
+     * @param BigDecimal $y The second decimal number.
+     *
+     * @return array{string, string} The scaled integer values of $x and $y.
+     */
+    private function scaleValues(BigDecimal $x, BigDecimal $y) : array
+    {
+        $a = $x->value;
+        $b = $y->value;
+
+        if ($b !== '0' && $x->scale > $y->scale) {
+            $b .= \str_repeat('0', $x->scale - $y->scale);
+        } elseif ($a !== '0' && $x->scale < $y->scale) {
+            $a .= \str_repeat('0', $y->scale - $x->scale);
+        }
+
+        return [$a, $b];
+    }
+
+    /**
+     * @param int $scale
+     *
+     * @return string
+     */
+    private function valueWithMinScale(int $scale) : string
+    {
+        $value = $this->value;
+
+        if ($this->value !== '0' && $scale > $this->scale) {
+            $value .= \str_repeat('0', $scale - $this->scale);
+        }
+
+        return $value;
+    }
+
+    /**
+     * Adds leading zeros if necessary to the unscaled value to represent the full decimal number.
+     *
+     * @return string
+     */
+    private function getUnscaledValueWithLeadingZeros() : string
+    {
+        $value = $this->value;
+        $targetLength = $this->scale + 1;
+        $negative = ($value[0] === '-');
+        $length = \strlen($value);
+
+        if ($negative) {
+            $length--;
+        }
+
+        if ($length >= $targetLength) {
+            return $this->value;
+        }
+
+        if ($negative) {
+            $value = \substr($value, 1);
+        }
+
+        $value = \str_pad($value, $targetLength, '0', STR_PAD_LEFT);
+
+        if ($negative) {
+            $value = '-' . $value;
+        }
+
+        return $value;
+    }
+}

+ 1184 - 0
api/vendor/brick/math/src/BigInteger.php

@@ -0,0 +1,1184 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Brick\Math;
+
+use Brick\Math\Exception\DivisionByZeroException;
+use Brick\Math\Exception\IntegerOverflowException;
+use Brick\Math\Exception\MathException;
+use Brick\Math\Exception\NegativeNumberException;
+use Brick\Math\Exception\NumberFormatException;
+use Brick\Math\Internal\Calculator;
+
+/**
+ * An arbitrary-size integer.
+ *
+ * All methods accepting a number as a parameter accept either a BigInteger instance,
+ * an integer, or a string representing an arbitrary size integer.
+ *
+ * @psalm-immutable
+ */
+final class BigInteger extends BigNumber
+{
+    /**
+     * The value, as a string of digits with optional leading minus sign.
+     *
+     * No leading zeros must be present.
+     * No leading minus sign must be present if the number is zero.
+     *
+     * @var string
+     */
+    private $value;
+
+    /**
+     * Protected constructor. Use a factory method to obtain an instance.
+     *
+     * @param string $value A string of digits, with optional leading minus sign.
+     */
+    protected function __construct(string $value)
+    {
+        $this->value = $value;
+    }
+
+    /**
+     * Creates a BigInteger of the given value.
+     *
+     * @param BigNumber|int|float|string $value
+     *
+     * @return BigInteger
+     *
+     * @throws MathException If the value cannot be converted to a BigInteger.
+     *
+     * @psalm-pure
+     */
+    public static function of($value) : BigNumber
+    {
+        return parent::of($value)->toBigInteger();
+    }
+
+    /**
+     * Creates a number from a string in a given base.
+     *
+     * The string can optionally be prefixed with the `+` or `-` sign.
+     *
+     * Bases greater than 36 are not supported by this method, as there is no clear consensus on which of the lowercase
+     * or uppercase characters should come first. Instead, this method accepts any base up to 36, and does not
+     * differentiate lowercase and uppercase characters, which are considered equal.
+     *
+     * For bases greater than 36, and/or custom alphabets, use the fromArbitraryBase() method.
+     *
+     * @param string $number The number to convert, in the given base.
+     * @param int    $base   The base of the number, between 2 and 36.
+     *
+     * @return BigInteger
+     *
+     * @throws NumberFormatException     If the number is empty, or contains invalid chars for the given base.
+     * @throws \InvalidArgumentException If the base is out of range.
+     *
+     * @psalm-pure
+     */
+    public static function fromBase(string $number, int $base) : BigInteger
+    {
+        if ($number === '') {
+            throw new NumberFormatException('The number cannot be empty.');
+        }
+
+        if ($base < 2 || $base > 36) {
+            throw new \InvalidArgumentException(\sprintf('Base %d is not in range 2 to 36.', $base));
+        }
+
+        if ($number[0] === '-') {
+            $sign = '-';
+            $number = \substr($number, 1);
+        } elseif ($number[0] === '+') {
+            $sign = '';
+            $number = \substr($number, 1);
+        } else {
+            $sign = '';
+        }
+
+        if ($number === '') {
+            throw new NumberFormatException('The number cannot be empty.');
+        }
+
+        $number = \ltrim($number, '0');
+
+        if ($number === '') {
+            // The result will be the same in any base, avoid further calculation.
+            return BigInteger::zero();
+        }
+
+        if ($number === '1') {
+            // The result will be the same in any base, avoid further calculation.
+            return new BigInteger($sign . '1');
+        }
+
+        $pattern = '/[^' . \substr(Calculator::ALPHABET, 0, $base) . ']/';
+
+        if (\preg_match($pattern, \strtolower($number), $matches) === 1) {
+            throw new NumberFormatException(\sprintf('"%s" is not a valid character in base %d.', $matches[0], $base));
+        }
+
+        if ($base === 10) {
+            // The number is usable as is, avoid further calculation.
+            return new BigInteger($sign . $number);
+        }
+
+        $result = Calculator::get()->fromBase($number, $base);
+
+        return new BigInteger($sign . $result);
+    }
+
+    /**
+     * Parses a string containing an integer in an arbitrary base, using a custom alphabet.
+     *
+     * Because this method accepts an alphabet with any character, including dash, it does not handle negative numbers.
+     *
+     * @param string $number   The number to parse.
+     * @param string $alphabet The alphabet, for example '01' for base 2, or '01234567' for base 8.
+     *
+     * @return BigInteger
+     *
+     * @throws NumberFormatException     If the given number is empty or contains invalid chars for the given alphabet.
+     * @throws \InvalidArgumentException If the alphabet does not contain at least 2 chars.
+     *
+     * @psalm-pure
+     */
+    public static function fromArbitraryBase(string $number, string $alphabet) : BigInteger
+    {
+        if ($number === '') {
+            throw new NumberFormatException('The number cannot be empty.');
+        }
+
+        $base = \strlen($alphabet);
+
+        if ($base < 2) {
+            throw new \InvalidArgumentException('The alphabet must contain at least 2 chars.');
+        }
+
+        $pattern = '/[^' . \preg_quote($alphabet, '/') . ']/';
+
+        if (\preg_match($pattern, $number, $matches) === 1) {
+            throw NumberFormatException::charNotInAlphabet($matches[0]);
+        }
+
+        $number = Calculator::get()->fromArbitraryBase($number, $alphabet, $base);
+
+        return new BigInteger($number);
+    }
+
+    /**
+     * Translates a string of bytes containing the binary representation of a BigInteger into a BigInteger.
+     *
+     * The input string is assumed to be in big-endian byte-order: the most significant byte is in the zeroth element.
+     *
+     * If `$signed` is true, the input is assumed to be in two's-complement representation, and the leading bit is
+     * interpreted as a sign bit. If `$signed` is false, the input is interpreted as an unsigned number, and the
+     * resulting BigInteger will always be positive or zero.
+     *
+     * This method can be used to retrieve a number exported by `toBytes()`, as long as the `$signed` flags match.
+     *
+     * @param string $value  The byte string.
+     * @param bool   $signed Whether to interpret as a signed number in two's-complement representation with a leading
+     *                       sign bit.
+     *
+     * @return BigInteger
+     *
+     * @throws NumberFormatException If the string is empty.
+     */
+    public static function fromBytes(string $value, bool $signed = true) : BigInteger
+    {
+        if ($value === '') {
+            throw new NumberFormatException('The byte string must not be empty.');
+        }
+
+        $twosComplement = false;
+
+        if ($signed) {
+            $x = \ord($value[0]);
+
+            if (($twosComplement = ($x >= 0x80))) {
+                $value = ~$value;
+            }
+        }
+
+        $number = self::fromBase(\bin2hex($value), 16);
+
+        if ($twosComplement) {
+            return $number->plus(1)->negated();
+        }
+
+        return $number;
+    }
+
+    /**
+     * Generates a pseudo-random number in the range 0 to 2^numBits - 1.
+     *
+     * Using the default random bytes generator, this method is suitable for cryptographic use.
+     *
+     * @psalm-param callable(int): string $randomBytesGenerator
+     *
+     * @param int           $numBits              The number of bits.
+     * @param callable|null $randomBytesGenerator A function that accepts a number of bytes as an integer, and returns a
+     *                                            string of random bytes of the given length. Defaults to the
+     *                                            `random_bytes()` function.
+     *
+     * @return BigInteger
+     *
+     * @throws \InvalidArgumentException If $numBits is negative.
+     */
+    public static function randomBits(int $numBits, ?callable $randomBytesGenerator = null) : BigInteger
+    {
+        if ($numBits < 0) {
+            throw new \InvalidArgumentException('The number of bits cannot be negative.');
+        }
+
+        if ($numBits === 0) {
+            return BigInteger::zero();
+        }
+
+        if ($randomBytesGenerator === null) {
+            $randomBytesGenerator = 'random_bytes';
+        }
+
+        $byteLength = \intdiv($numBits - 1, 8) + 1;
+
+        $extraBits = ($byteLength * 8 - $numBits);
+        $bitmask   = \chr(0xFF >> $extraBits);
+
+        $randomBytes    = $randomBytesGenerator($byteLength);
+        $randomBytes[0] = $randomBytes[0] & $bitmask;
+
+        return self::fromBytes($randomBytes, false);
+    }
+
+    /**
+     * Generates a pseudo-random number between `$min` and `$max`.
+     *
+     * Using the default random bytes generator, this method is suitable for cryptographic use.
+     *
+     * @psalm-param (callable(int): string)|null $randomBytesGenerator
+     *
+     * @param BigNumber|int|float|string $min                  The lower bound. Must be convertible to a BigInteger.
+     * @param BigNumber|int|float|string $max                  The upper bound. Must be convertible to a BigInteger.
+     * @param callable|null              $randomBytesGenerator A function that accepts a number of bytes as an integer,
+     *                                                         and returns a string of random bytes of the given length.
+     *                                                         Defaults to the `random_bytes()` function.
+     *
+     * @return BigInteger
+     *
+     * @throws MathException If one of the parameters cannot be converted to a BigInteger,
+     *                       or `$min` is greater than `$max`.
+     */
+    public static function randomRange($min, $max, ?callable $randomBytesGenerator = null) : BigInteger
+    {
+        $min = BigInteger::of($min);
+        $max = BigInteger::of($max);
+
+        if ($min->isGreaterThan($max)) {
+            throw new MathException('$min cannot be greater than $max.');
+        }
+
+        if ($min->isEqualTo($max)) {
+            return $min;
+        }
+
+        $diff      = $max->minus($min);
+        $bitLength = $diff->getBitLength();
+
+        // try until the number is in range (50% to 100% chance of success)
+        do {
+            $randomNumber = self::randomBits($bitLength, $randomBytesGenerator);
+        } while ($randomNumber->isGreaterThan($diff));
+
+        return $randomNumber->plus($min);
+    }
+
+    /**
+     * Returns a BigInteger representing zero.
+     *
+     * @return BigInteger
+     *
+     * @psalm-pure
+     */
+    public static function zero() : BigInteger
+    {
+        /**
+         * @psalm-suppress ImpureStaticVariable
+         * @var BigInteger|null $zero
+         */
+        static $zero;
+
+        if ($zero === null) {
+            $zero = new BigInteger('0');
+        }
+
+        return $zero;
+    }
+
+    /**
+     * Returns a BigInteger representing one.
+     *
+     * @return BigInteger
+     *
+     * @psalm-pure
+     */
+    public static function one() : BigInteger
+    {
+        /**
+         * @psalm-suppress ImpureStaticVariable
+         * @var BigInteger|null $one
+         */
+        static $one;
+
+        if ($one === null) {
+            $one = new BigInteger('1');
+        }
+
+        return $one;
+    }
+
+    /**
+     * Returns a BigInteger representing ten.
+     *
+     * @return BigInteger
+     *
+     * @psalm-pure
+     */
+    public static function ten() : BigInteger
+    {
+        /**
+         * @psalm-suppress ImpureStaticVariable
+         * @var BigInteger|null $ten
+         */
+        static $ten;
+
+        if ($ten === null) {
+            $ten = new BigInteger('10');
+        }
+
+        return $ten;
+    }
+
+    /**
+     * Returns the sum of this number and the given one.
+     *
+     * @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigInteger.
+     *
+     * @return BigInteger The result.
+     *
+     * @throws MathException If the number is not valid, or is not convertible to a BigInteger.
+     */
+    public function plus($that) : BigInteger
+    {
+        $that = BigInteger::of($that);
+
+        if ($that->value === '0') {
+            return $this;
+        }
+
+        if ($this->value === '0') {
+            return $that;
+        }
+
+        $value = Calculator::get()->add($this->value, $that->value);
+
+        return new BigInteger($value);
+    }
+
+    /**
+     * Returns the difference of this number and the given one.
+     *
+     * @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigInteger.
+     *
+     * @return BigInteger The result.
+     *
+     * @throws MathException If the number is not valid, or is not convertible to a BigInteger.
+     */
+    public function minus($that) : BigInteger
+    {
+        $that = BigInteger::of($that);
+
+        if ($that->value === '0') {
+            return $this;
+        }
+
+        $value = Calculator::get()->sub($this->value, $that->value);
+
+        return new BigInteger($value);
+    }
+
+    /**
+     * Returns the product of this number and the given one.
+     *
+     * @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigInteger.
+     *
+     * @return BigInteger The result.
+     *
+     * @throws MathException If the multiplier is not a valid number, or is not convertible to a BigInteger.
+     */
+    public function multipliedBy($that) : BigInteger
+    {
+        $that = BigInteger::of($that);
+
+        if ($that->value === '1') {
+            return $this;
+        }
+
+        if ($this->value === '1') {
+            return $that;
+        }
+
+        $value = Calculator::get()->mul($this->value, $that->value);
+
+        return new BigInteger($value);
+    }
+
+    /**
+     * Returns the result of the division of this number by the given one.
+     *
+     * @param BigNumber|int|float|string $that         The divisor. Must be convertible to a BigInteger.
+     * @param int                        $roundingMode An optional rounding mode.
+     *
+     * @return BigInteger The result.
+     *
+     * @throws MathException If the divisor is not a valid number, is not convertible to a BigInteger, is zero,
+     *                       or RoundingMode::UNNECESSARY is used and the remainder is not zero.
+     */
+    public function dividedBy($that, int $roundingMode = RoundingMode::UNNECESSARY) : BigInteger
+    {
+        $that = BigInteger::of($that);
+
+        if ($that->value === '1') {
+            return $this;
+        }
+
+        if ($that->value === '0') {
+            throw DivisionByZeroException::divisionByZero();
+        }
+
+        $result = Calculator::get()->divRound($this->value, $that->value, $roundingMode);
+
+        return new BigInteger($result);
+    }
+
+    /**
+     * Returns this number exponentiated to the given value.
+     *
+     * @param int $exponent The exponent.
+     *
+     * @return BigInteger The result.
+     *
+     * @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
+     */
+    public function power(int $exponent) : BigInteger
+    {
+        if ($exponent === 0) {
+            return BigInteger::one();
+        }
+
+        if ($exponent === 1) {
+            return $this;
+        }
+
+        if ($exponent < 0 || $exponent > Calculator::MAX_POWER) {
+            throw new \InvalidArgumentException(\sprintf(
+                'The exponent %d is not in the range 0 to %d.',
+                $exponent,
+                Calculator::MAX_POWER
+            ));
+        }
+
+        return new BigInteger(Calculator::get()->pow($this->value, $exponent));
+    }
+
+    /**
+     * Returns the quotient of the division of this number by the given one.
+     *
+     * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger.
+     *
+     * @return BigInteger
+     *
+     * @throws DivisionByZeroException If the divisor is zero.
+     */
+    public function quotient($that) : BigInteger
+    {
+        $that = BigInteger::of($that);
+
+        if ($that->value === '1') {
+            return $this;
+        }
+
+        if ($that->value === '0') {
+            throw DivisionByZeroException::divisionByZero();
+        }
+
+        $quotient = Calculator::get()->divQ($this->value, $that->value);
+
+        return new BigInteger($quotient);
+    }
+
+    /**
+     * Returns the remainder of the division of this number by the given one.
+     *
+     * The remainder, when non-zero, has the same sign as the dividend.
+     *
+     * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger.
+     *
+     * @return BigInteger
+     *
+     * @throws DivisionByZeroException If the divisor is zero.
+     */
+    public function remainder($that) : BigInteger
+    {
+        $that = BigInteger::of($that);
+
+        if ($that->value === '1') {
+            return BigInteger::zero();
+        }
+
+        if ($that->value === '0') {
+            throw DivisionByZeroException::divisionByZero();
+        }
+
+        $remainder = Calculator::get()->divR($this->value, $that->value);
+
+        return new BigInteger($remainder);
+    }
+
+    /**
+     * Returns the quotient and remainder of the division of this number by the given one.
+     *
+     * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger.
+     *
+     * @return BigInteger[] An array containing the quotient and the remainder.
+     *
+     * @throws DivisionByZeroException If the divisor is zero.
+     */
+    public function quotientAndRemainder($that) : array
+    {
+        $that = BigInteger::of($that);
+
+        if ($that->value === '0') {
+            throw DivisionByZeroException::divisionByZero();
+        }
+
+        [$quotient, $remainder] = Calculator::get()->divQR($this->value, $that->value);
+
+        return [
+            new BigInteger($quotient),
+            new BigInteger($remainder)
+        ];
+    }
+
+    /**
+     * Returns the modulo of this number and the given one.
+     *
+     * The modulo operation yields the same result as the remainder operation when both operands are of the same sign,
+     * and may differ when signs are different.
+     *
+     * The result of the modulo operation, when non-zero, has the same sign as the divisor.
+     *
+     * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger.
+     *
+     * @return BigInteger
+     *
+     * @throws DivisionByZeroException If the divisor is zero.
+     */
+    public function mod($that) : BigInteger
+    {
+        $that = BigInteger::of($that);
+
+        if ($that->value === '0') {
+            throw DivisionByZeroException::modulusMustNotBeZero();
+        }
+
+        $value = Calculator::get()->mod($this->value, $that->value);
+
+        return new BigInteger($value);
+    }
+
+    /**
+     * Returns the modular multiplicative inverse of this BigInteger modulo $m.
+     *
+     * @param BigInteger $m
+     *
+     * @return BigInteger
+     *
+     * @throws DivisionByZeroException If $m is zero.
+     * @throws NegativeNumberException If $m is negative.
+     * @throws MathException           If this BigInteger has no multiplicative inverse mod m (that is, this BigInteger
+     *                                 is not relatively prime to m).
+     */
+    public function modInverse(BigInteger $m) : BigInteger
+    {
+        if ($m->value === '0') {
+            throw DivisionByZeroException::modulusMustNotBeZero();
+        }
+
+        if ($m->isNegative()) {
+            throw new NegativeNumberException('Modulus must not be negative.');
+        }
+
+        if ($m->value === '1') {
+            return BigInteger::zero();
+        }
+
+        $value = Calculator::get()->modInverse($this->value, $m->value);
+
+        if ($value === null) {
+            throw new MathException('Unable to compute the modInverse for the given modulus.');
+        }
+
+        return new BigInteger($value);
+    }
+
+    /**
+     * Returns this number raised into power with modulo.
+     *
+     * This operation only works on positive numbers.
+     *
+     * @param BigNumber|int|float|string $exp The exponent. Must be positive or zero.
+     * @param BigNumber|int|float|string $mod The modulus. Must be strictly positive.
+     *
+     * @return BigInteger
+     *
+     * @throws NegativeNumberException If any of the operands is negative.
+     * @throws DivisionByZeroException If the modulus is zero.
+     */
+    public function modPow($exp, $mod) : BigInteger
+    {
+        $exp = BigInteger::of($exp);
+        $mod = BigInteger::of($mod);
+
+        if ($this->isNegative() || $exp->isNegative() || $mod->isNegative()) {
+            throw new NegativeNumberException('The operands cannot be negative.');
+        }
+
+        if ($mod->isZero()) {
+            throw DivisionByZeroException::modulusMustNotBeZero();
+        }
+
+        $result = Calculator::get()->modPow($this->value, $exp->value, $mod->value);
+
+        return new BigInteger($result);
+    }
+
+    /**
+     * Returns the greatest common divisor of this number and the given one.
+     *
+     * The GCD is always positive, unless both operands are zero, in which case it is zero.
+     *
+     * @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number.
+     *
+     * @return BigInteger
+     */
+    public function gcd($that) : BigInteger
+    {
+        $that = BigInteger::of($that);
+
+        if ($that->value === '0' && $this->value[0] !== '-') {
+            return $this;
+        }
+
+        if ($this->value === '0' && $that->value[0] !== '-') {
+            return $that;
+        }
+
+        $value = Calculator::get()->gcd($this->value, $that->value);
+
+        return new BigInteger($value);
+    }
+
+    /**
+     * Returns the integer square root number of this number, rounded down.
+     *
+     * The result is the largest x such that x² ≤ n.
+     *
+     * @return BigInteger
+     *
+     * @throws NegativeNumberException If this number is negative.
+     */
+    public function sqrt() : BigInteger
+    {
+        if ($this->value[0] === '-') {
+            throw new NegativeNumberException('Cannot calculate the square root of a negative number.');
+        }
+
+        $value = Calculator::get()->sqrt($this->value);
+
+        return new BigInteger($value);
+    }
+
+    /**
+     * Returns the absolute value of this number.
+     *
+     * @return BigInteger
+     */
+    public function abs() : BigInteger
+    {
+        return $this->isNegative() ? $this->negated() : $this;
+    }
+
+    /**
+     * Returns the inverse of this number.
+     *
+     * @return BigInteger
+     */
+    public function negated() : BigInteger
+    {
+        return new BigInteger(Calculator::get()->neg($this->value));
+    }
+
+    /**
+     * Returns the integer bitwise-and combined with another integer.
+     *
+     * This method returns a negative BigInteger if and only if both operands are negative.
+     *
+     * @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number.
+     *
+     * @return BigInteger
+     */
+    public function and($that) : BigInteger
+    {
+        $that = BigInteger::of($that);
+
+        return new BigInteger(Calculator::get()->and($this->value, $that->value));
+    }
+
+    /**
+     * Returns the integer bitwise-or combined with another integer.
+     *
+     * This method returns a negative BigInteger if and only if either of the operands is negative.
+     *
+     * @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number.
+     *
+     * @return BigInteger
+     */
+    public function or($that) : BigInteger
+    {
+        $that = BigInteger::of($that);
+
+        return new BigInteger(Calculator::get()->or($this->value, $that->value));
+    }
+
+    /**
+     * Returns the integer bitwise-xor combined with another integer.
+     *
+     * This method returns a negative BigInteger if and only if exactly one of the operands is negative.
+     *
+     * @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number.
+     *
+     * @return BigInteger
+     */
+    public function xor($that) : BigInteger
+    {
+        $that = BigInteger::of($that);
+
+        return new BigInteger(Calculator::get()->xor($this->value, $that->value));
+    }
+
+    /**
+     * Returns the bitwise-not of this BigInteger.
+     *
+     * @return BigInteger
+     */
+    public function not() : BigInteger
+    {
+        return $this->negated()->minus(1);
+    }
+
+    /**
+     * Returns the integer left shifted by a given number of bits.
+     *
+     * @param int $distance The distance to shift.
+     *
+     * @return BigInteger
+     */
+    public function shiftedLeft(int $distance) : BigInteger
+    {
+        if ($distance === 0) {
+            return $this;
+        }
+
+        if ($distance < 0) {
+            return $this->shiftedRight(- $distance);
+        }
+
+        return $this->multipliedBy(BigInteger::of(2)->power($distance));
+    }
+
+    /**
+     * Returns the integer right shifted by a given number of bits.
+     *
+     * @param int $distance The distance to shift.
+     *
+     * @return BigInteger
+     */
+    public function shiftedRight(int $distance) : BigInteger
+    {
+        if ($distance === 0) {
+            return $this;
+        }
+
+        if ($distance < 0) {
+            return $this->shiftedLeft(- $distance);
+        }
+
+        $operand = BigInteger::of(2)->power($distance);
+
+        if ($this->isPositiveOrZero()) {
+            return $this->quotient($operand);
+        }
+
+        return $this->dividedBy($operand, RoundingMode::UP);
+    }
+
+    /**
+     * Returns the number of bits in the minimal two's-complement representation of this BigInteger, excluding a sign bit.
+     *
+     * For positive BigIntegers, this is equivalent to the number of bits in the ordinary binary representation.
+     * Computes (ceil(log2(this < 0 ? -this : this+1))).
+     *
+     * @return int
+     */
+    public function getBitLength() : int
+    {
+        if ($this->value === '0') {
+            return 0;
+        }
+
+        if ($this->isNegative()) {
+            return $this->abs()->minus(1)->getBitLength();
+        }
+
+        return \strlen($this->toBase(2));
+    }
+
+    /**
+     * Returns the index of the rightmost (lowest-order) one bit in this BigInteger.
+     *
+     * Returns -1 if this BigInteger contains no one bits.
+     *
+     * @return int
+     */
+    public function getLowestSetBit() : int
+    {
+        $n = $this;
+        $bitLength = $this->getBitLength();
+
+        for ($i = 0; $i <= $bitLength; $i++) {
+            if ($n->isOdd()) {
+                return $i;
+            }
+
+            $n = $n->shiftedRight(1);
+        }
+
+        return -1;
+    }
+
+    /**
+     * Returns whether this number is even.
+     *
+     * @return bool
+     */
+    public function isEven() : bool
+    {
+        return \in_array($this->value[-1], ['0', '2', '4', '6', '8'], true);
+    }
+
+    /**
+     * Returns whether this number is odd.
+     *
+     * @return bool
+     */
+    public function isOdd() : bool
+    {
+        return \in_array($this->value[-1], ['1', '3', '5', '7', '9'], true);
+    }
+
+    /**
+     * Returns true if and only if the designated bit is set.
+     *
+     * Computes ((this & (1<<n)) != 0).
+     *
+     * @param int $n The bit to test, 0-based.
+     *
+     * @return bool
+     *
+     * @throws \InvalidArgumentException If the bit to test is negative.
+     */
+    public function testBit(int $n) : bool
+    {
+        if ($n < 0) {
+            throw new \InvalidArgumentException('The bit to test cannot be negative.');
+        }
+
+        return $this->shiftedRight($n)->isOdd();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function compareTo($that) : int
+    {
+        $that = BigNumber::of($that);
+
+        if ($that instanceof BigInteger) {
+            return Calculator::get()->cmp($this->value, $that->value);
+        }
+
+        return - $that->compareTo($this);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSign() : int
+    {
+        return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toBigInteger() : BigInteger
+    {
+        return $this;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toBigDecimal() : BigDecimal
+    {
+        return BigDecimal::create($this->value);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toBigRational() : BigRational
+    {
+        return BigRational::create($this, BigInteger::one(), false);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
+    {
+        return $this->toBigDecimal()->toScale($scale, $roundingMode);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toInt() : int
+    {
+        $intValue = (int) $this->value;
+
+        if ($this->value !== (string) $intValue) {
+            throw IntegerOverflowException::toIntOverflow($this);
+        }
+
+        return $intValue;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toFloat() : float
+    {
+        return (float) $this->value;
+    }
+
+    /**
+     * Returns a string representation of this number in the given base.
+     *
+     * The output will always be lowercase for bases greater than 10.
+     *
+     * @param int $base
+     *
+     * @return string
+     *
+     * @throws \InvalidArgumentException If the base is out of range.
+     */
+    public function toBase(int $base) : string
+    {
+        if ($base === 10) {
+            return $this->value;
+        }
+
+        if ($base < 2 || $base > 36) {
+            throw new \InvalidArgumentException(\sprintf('Base %d is out of range [2, 36]', $base));
+        }
+
+        return Calculator::get()->toBase($this->value, $base);
+    }
+
+    /**
+     * Returns a string representation of this number in an arbitrary base with a custom alphabet.
+     *
+     * Because this method accepts an alphabet with any character, including dash, it does not handle negative numbers;
+     * a NegativeNumberException will be thrown when attempting to call this method on a negative number.
+     *
+     * @param string $alphabet The alphabet, for example '01' for base 2, or '01234567' for base 8.
+     *
+     * @return string
+     *
+     * @throws NegativeNumberException   If this number is negative.
+     * @throws \InvalidArgumentException If the given alphabet does not contain at least 2 chars.
+     */
+    public function toArbitraryBase(string $alphabet) : string
+    {
+        $base = \strlen($alphabet);
+
+        if ($base < 2) {
+            throw new \InvalidArgumentException('The alphabet must contain at least 2 chars.');
+        }
+
+        if ($this->value[0] === '-') {
+            throw new NegativeNumberException(__FUNCTION__ . '() does not support negative numbers.');
+        }
+
+        return Calculator::get()->toArbitraryBase($this->value, $alphabet, $base);
+    }
+
+    /**
+     * Returns a string of bytes containing the binary representation of this BigInteger.
+     *
+     * The string is in big-endian byte-order: the most significant byte is in the zeroth element.
+     *
+     * If `$signed` is true, the output will be in two's-complement representation, and a sign bit will be prepended to
+     * the output. If `$signed` is false, no sign bit will be prepended, and this method will throw an exception if the
+     * number is negative.
+     *
+     * The string will contain the minimum number of bytes required to represent this BigInteger, including a sign bit
+     * if `$signed` is true.
+     *
+     * This representation is compatible with the `fromBytes()` factory method, as long as the `$signed` flags match.
+     *
+     * @param bool $signed Whether to output a signed number in two's-complement representation with a leading sign bit.
+     *
+     * @return string
+     *
+     * @throws NegativeNumberException If $signed is false, and the number is negative.
+     */
+    public function toBytes(bool $signed = true) : string
+    {
+        if (! $signed && $this->isNegative()) {
+            throw new NegativeNumberException('Cannot convert a negative number to a byte string when $signed is false.');
+        }
+
+        $hex = $this->abs()->toBase(16);
+
+        if (\strlen($hex) % 2 !== 0) {
+            $hex = '0' . $hex;
+        }
+
+        $baseHexLength = \strlen($hex);
+
+        if ($signed) {
+            if ($this->isNegative()) {
+                $bin = \hex2bin($hex);
+                assert($bin !== false);
+
+                $hex = \bin2hex(~$bin);
+                $hex = self::fromBase($hex, 16)->plus(1)->toBase(16);
+
+                $hexLength = \strlen($hex);
+
+                if ($hexLength < $baseHexLength) {
+                    $hex = \str_repeat('0', $baseHexLength - $hexLength) . $hex;
+                }
+
+                if ($hex[0] < '8') {
+                    $hex = 'FF' . $hex;
+                }
+            } else {
+                if ($hex[0] >= '8') {
+                    $hex = '00' . $hex;
+                }
+            }
+        }
+
+        return \hex2bin($hex);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function __toString() : string
+    {
+        return $this->value;
+    }
+
+    /**
+     * This method is required for serializing the object and SHOULD NOT be accessed directly.
+     *
+     * @internal
+     *
+     * @return array{value: string}
+     */
+    public function __serialize(): array
+    {
+        return ['value' => $this->value];
+    }
+
+    /**
+     * This method is only here to allow unserializing the object and cannot be accessed directly.
+     *
+     * @internal
+     * @psalm-suppress RedundantPropertyInitializationCheck
+     *
+     * @param array{value: string} $data
+     *
+     * @return void
+     *
+     * @throws \LogicException
+     */
+    public function __unserialize(array $data): void
+    {
+        if (isset($this->value)) {
+            throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
+        }
+
+        $this->value = $data['value'];
+    }
+
+    /**
+     * This method is required by interface Serializable and SHOULD NOT be accessed directly.
+     *
+     * @internal
+     *
+     * @return string
+     */
+    public function serialize() : string
+    {
+        return $this->value;
+    }
+
+    /**
+     * This method is only here to implement interface Serializable and cannot be accessed directly.
+     *
+     * @internal
+     * @psalm-suppress RedundantPropertyInitializationCheck
+     *
+     * @param string $value
+     *
+     * @return void
+     *
+     * @throws \LogicException
+     */
+    public function unserialize($value) : void
+    {
+        if (isset($this->value)) {
+            throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
+        }
+
+        $this->value = $value;
+    }
+}

+ 572 - 0
api/vendor/brick/math/src/BigNumber.php

@@ -0,0 +1,572 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Brick\Math;
+
+use Brick\Math\Exception\DivisionByZeroException;
+use Brick\Math\Exception\MathException;
+use Brick\Math\Exception\NumberFormatException;
+use Brick\Math\Exception\RoundingNecessaryException;
+
+/**
+ * Common interface for arbitrary-precision rational numbers.
+ *
+ * @psalm-immutable
+ */
+abstract class BigNumber implements \Serializable, \JsonSerializable
+{
+    /**
+     * The regular expression used to parse integer, decimal and rational numbers.
+     */
+    private const PARSE_REGEXP =
+        '/^' .
+            '(?<sign>[\-\+])?' .
+            '(?:' .
+                '(?:' .
+                    '(?<integral>[0-9]+)?' .
+                    '(?<point>\.)?' .
+                    '(?<fractional>[0-9]+)?' .
+                    '(?:[eE](?<exponent>[\-\+]?[0-9]+))?' .
+                ')|(?:' .
+                    '(?<numerator>[0-9]+)' .
+                    '\/?' .
+                    '(?<denominator>[0-9]+)' .
+                ')' .
+            ')' .
+        '$/';
+
+    /**
+     * Creates a BigNumber of the given value.
+     *
+     * The concrete return type is dependent on the given value, with the following rules:
+     *
+     * - BigNumber instances are returned as is
+     * - integer numbers are returned as BigInteger
+     * - floating point numbers are converted to a string then parsed as such
+     * - strings containing a `/` character are returned as BigRational
+     * - strings containing a `.` character or using an exponential notation are returned as BigDecimal
+     * - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger
+     *
+     * @param BigNumber|int|float|string $value
+     *
+     * @return BigNumber
+     *
+     * @throws NumberFormatException   If the format of the number is not valid.
+     * @throws DivisionByZeroException If the value represents a rational number with a denominator of zero.
+     *
+     * @psalm-pure
+     */
+    public static function of($value) : BigNumber
+    {
+        if ($value instanceof BigNumber) {
+            return $value;
+        }
+
+        if (\is_int($value)) {
+            return new BigInteger((string) $value);
+        }
+
+        /** @psalm-suppress RedundantCastGivenDocblockType We cannot trust the untyped $value here! */
+        $value = \is_float($value) ? self::floatToString($value) : (string) $value;
+
+        $throw = static function() use ($value) : void {
+            throw new NumberFormatException(\sprintf(
+                'The given value "%s" does not represent a valid number.',
+                $value
+            ));
+        };
+
+        if (\preg_match(self::PARSE_REGEXP, $value, $matches) !== 1) {
+            $throw();
+        }
+
+        $getMatch = static function(string $value) use ($matches) : ?string {
+            return isset($matches[$value]) && $matches[$value] !== '' ? $matches[$value] : null;
+        };
+
+        $sign        = $getMatch('sign');
+        $numerator   = $getMatch('numerator');
+        $denominator = $getMatch('denominator');
+
+        if ($numerator !== null) {
+            assert($denominator !== null);
+
+            if ($sign !== null) {
+                $numerator = $sign . $numerator;
+            }
+
+            $numerator   = self::cleanUp($numerator);
+            $denominator = self::cleanUp($denominator);
+
+            if ($denominator === '0') {
+                throw DivisionByZeroException::denominatorMustNotBeZero();
+            }
+
+            return new BigRational(
+                new BigInteger($numerator),
+                new BigInteger($denominator),
+                false
+            );
+        }
+
+        $point      = $getMatch('point');
+        $integral   = $getMatch('integral');
+        $fractional = $getMatch('fractional');
+        $exponent   = $getMatch('exponent');
+
+        if ($integral === null && $fractional === null) {
+            $throw();
+        }
+
+        if ($integral === null) {
+            $integral = '0';
+        }
+
+        if ($point !== null || $exponent !== null) {
+            $fractional = ($fractional ?? '');
+            $exponent = ($exponent !== null) ? (int) $exponent : 0;
+
+            if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) {
+                throw new NumberFormatException('Exponent too large.');
+            }
+
+            $unscaledValue = self::cleanUp(($sign ?? ''). $integral . $fractional);
+
+            $scale = \strlen($fractional) - $exponent;
+
+            if ($scale < 0) {
+                if ($unscaledValue !== '0') {
+                    $unscaledValue .= \str_repeat('0', - $scale);
+                }
+                $scale = 0;
+            }
+
+            return new BigDecimal($unscaledValue, $scale);
+        }
+
+        $integral = self::cleanUp(($sign ?? '') . $integral);
+
+        return new BigInteger($integral);
+    }
+
+    /**
+     * Safely converts float to string, avoiding locale-dependent issues.
+     *
+     * @see https://github.com/brick/math/pull/20
+     *
+     * @param float $float
+     *
+     * @return string
+     *
+     * @psalm-pure
+     * @psalm-suppress ImpureFunctionCall
+     */
+    private static function floatToString(float $float) : string
+    {
+        $currentLocale = \setlocale(LC_NUMERIC, '0');
+        \setlocale(LC_NUMERIC, 'C');
+
+        $result = (string) $float;
+
+        \setlocale(LC_NUMERIC, $currentLocale);
+
+        return $result;
+    }
+
+    /**
+     * Proxy method to access protected constructors from sibling classes.
+     *
+     * @internal
+     *
+     * @param mixed ...$args The arguments to the constructor.
+     *
+     * @return static
+     *
+     * @psalm-pure
+     * @psalm-suppress TooManyArguments
+     * @psalm-suppress UnsafeInstantiation
+     */
+    protected static function create(... $args) : BigNumber
+    {
+        return new static(... $args);
+    }
+
+    /**
+     * Returns the minimum of the given values.
+     *
+     * @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
+     *                                              to an instance of the class this method is called on.
+     *
+     * @return static The minimum value.
+     *
+     * @throws \InvalidArgumentException If no values are given.
+     * @throws MathException             If an argument is not valid.
+     *
+     * @psalm-suppress LessSpecificReturnStatement
+     * @psalm-suppress MoreSpecificReturnType
+     * @psalm-pure
+     */
+    public static function min(...$values) : BigNumber
+    {
+        $min = null;
+
+        foreach ($values as $value) {
+            $value = static::of($value);
+
+            if ($min === null || $value->isLessThan($min)) {
+                $min = $value;
+            }
+        }
+
+        if ($min === null) {
+            throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
+        }
+
+        return $min;
+    }
+
+    /**
+     * Returns the maximum of the given values.
+     *
+     * @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible
+     *                                              to an instance of the class this method is called on.
+     *
+     * @return static The maximum value.
+     *
+     * @throws \InvalidArgumentException If no values are given.
+     * @throws MathException             If an argument is not valid.
+     *
+     * @psalm-suppress LessSpecificReturnStatement
+     * @psalm-suppress MoreSpecificReturnType
+     * @psalm-pure
+     */
+    public static function max(...$values) : BigNumber
+    {
+        $max = null;
+
+        foreach ($values as $value) {
+            $value = static::of($value);
+
+            if ($max === null || $value->isGreaterThan($max)) {
+                $max = $value;
+            }
+        }
+
+        if ($max === null) {
+            throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
+        }
+
+        return $max;
+    }
+
+    /**
+     * Returns the sum of the given values.
+     *
+     * @param BigNumber|int|float|string ...$values The numbers to add. All the numbers need to be convertible
+     *                                              to an instance of the class this method is called on.
+     *
+     * @return static The sum.
+     *
+     * @throws \InvalidArgumentException If no values are given.
+     * @throws MathException             If an argument is not valid.
+     *
+     * @psalm-suppress LessSpecificReturnStatement
+     * @psalm-suppress MoreSpecificReturnType
+     * @psalm-pure
+     */
+    public static function sum(...$values) : BigNumber
+    {
+        /** @var BigNumber|null $sum */
+        $sum = null;
+
+        foreach ($values as $value) {
+            $value = static::of($value);
+
+            $sum = $sum === null ? $value : self::add($sum, $value);
+        }
+
+        if ($sum === null) {
+            throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.');
+        }
+
+        return $sum;
+    }
+
+    /**
+     * Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException.
+     *
+     * @todo This could be better resolved by creating an abstract protected method in BigNumber, and leaving to
+     *       concrete classes the responsibility to perform the addition themselves or delegate it to the given number,
+     *       depending on their ability to perform the operation. This will also require a version bump because we're
+     *       potentially breaking custom BigNumber implementations (if any...)
+     *
+     * @param BigNumber $a
+     * @param BigNumber $b
+     *
+     * @return BigNumber
+     *
+     * @psalm-pure
+     */
+    private static function add(BigNumber $a, BigNumber $b) : BigNumber
+    {
+        if ($a instanceof BigRational) {
+            return $a->plus($b);
+        }
+
+        if ($b instanceof BigRational) {
+            return $b->plus($a);
+        }
+
+        if ($a instanceof BigDecimal) {
+            return $a->plus($b);
+        }
+
+        if ($b instanceof BigDecimal) {
+            return $b->plus($a);
+        }
+
+        /** @var BigInteger $a */
+
+        return $a->plus($b);
+    }
+
+    /**
+     * Removes optional leading zeros and + sign from the given number.
+     *
+     * @param string $number The number, validated as a non-empty string of digits with optional leading sign.
+     *
+     * @return string
+     *
+     * @psalm-pure
+     */
+    private static function cleanUp(string $number) : string
+    {
+        $firstChar = $number[0];
+
+        if ($firstChar === '+' || $firstChar === '-') {
+            $number = \substr($number, 1);
+        }
+
+        $number = \ltrim($number, '0');
+
+        if ($number === '') {
+            return '0';
+        }
+
+        if ($firstChar === '-') {
+            return '-' . $number;
+        }
+
+        return $number;
+    }
+
+    /**
+     * Checks if this number is equal to the given one.
+     *
+     * @param BigNumber|int|float|string $that
+     *
+     * @return bool
+     */
+    public function isEqualTo($that) : bool
+    {
+        return $this->compareTo($that) === 0;
+    }
+
+    /**
+     * Checks if this number is strictly lower than the given one.
+     *
+     * @param BigNumber|int|float|string $that
+     *
+     * @return bool
+     */
+    public function isLessThan($that) : bool
+    {
+        return $this->compareTo($that) < 0;
+    }
+
+    /**
+     * Checks if this number is lower than or equal to the given one.
+     *
+     * @param BigNumber|int|float|string $that
+     *
+     * @return bool
+     */
+    public function isLessThanOrEqualTo($that) : bool
+    {
+        return $this->compareTo($that) <= 0;
+    }
+
+    /**
+     * Checks if this number is strictly greater than the given one.
+     *
+     * @param BigNumber|int|float|string $that
+     *
+     * @return bool
+     */
+    public function isGreaterThan($that) : bool
+    {
+        return $this->compareTo($that) > 0;
+    }
+
+    /**
+     * Checks if this number is greater than or equal to the given one.
+     *
+     * @param BigNumber|int|float|string $that
+     *
+     * @return bool
+     */
+    public function isGreaterThanOrEqualTo($that) : bool
+    {
+        return $this->compareTo($that) >= 0;
+    }
+
+    /**
+     * Checks if this number equals zero.
+     *
+     * @return bool
+     */
+    public function isZero() : bool
+    {
+        return $this->getSign() === 0;
+    }
+
+    /**
+     * Checks if this number is strictly negative.
+     *
+     * @return bool
+     */
+    public function isNegative() : bool
+    {
+        return $this->getSign() < 0;
+    }
+
+    /**
+     * Checks if this number is negative or zero.
+     *
+     * @return bool
+     */
+    public function isNegativeOrZero() : bool
+    {
+        return $this->getSign() <= 0;
+    }
+
+    /**
+     * Checks if this number is strictly positive.
+     *
+     * @return bool
+     */
+    public function isPositive() : bool
+    {
+        return $this->getSign() > 0;
+    }
+
+    /**
+     * Checks if this number is positive or zero.
+     *
+     * @return bool
+     */
+    public function isPositiveOrZero() : bool
+    {
+        return $this->getSign() >= 0;
+    }
+
+    /**
+     * Returns the sign of this number.
+     *
+     * @return int -1 if the number is negative, 0 if zero, 1 if positive.
+     */
+    abstract public function getSign() : int;
+
+    /**
+     * Compares this number to the given one.
+     *
+     * @param BigNumber|int|float|string $that
+     *
+     * @return int [-1,0,1] If `$this` is lower than, equal to, or greater than `$that`.
+     *
+     * @throws MathException If the number is not valid.
+     */
+    abstract public function compareTo($that) : int;
+
+    /**
+     * Converts this number to a BigInteger.
+     *
+     * @return BigInteger The converted number.
+     *
+     * @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding.
+     */
+    abstract public function toBigInteger() : BigInteger;
+
+    /**
+     * Converts this number to a BigDecimal.
+     *
+     * @return BigDecimal The converted number.
+     *
+     * @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding.
+     */
+    abstract public function toBigDecimal() : BigDecimal;
+
+    /**
+     * Converts this number to a BigRational.
+     *
+     * @return BigRational The converted number.
+     */
+    abstract public function toBigRational() : BigRational;
+
+    /**
+     * Converts this number to a BigDecimal with the given scale, using rounding if necessary.
+     *
+     * @param int $scale        The scale of the resulting `BigDecimal`.
+     * @param int $roundingMode A `RoundingMode` constant.
+     *
+     * @return BigDecimal
+     *
+     * @throws RoundingNecessaryException If this number cannot be converted to the given scale without rounding.
+     *                                    This only applies when RoundingMode::UNNECESSARY is used.
+     */
+    abstract public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal;
+
+    /**
+     * Returns the exact value of this number as a native integer.
+     *
+     * If this number cannot be converted to a native integer without losing precision, an exception is thrown.
+     * Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit.
+     *
+     * @return int The converted value.
+     *
+     * @throws MathException If this number cannot be exactly converted to a native integer.
+     */
+    abstract public function toInt() : int;
+
+    /**
+     * Returns an approximation of this number as a floating-point value.
+     *
+     * Note that this method can discard information as the precision of a floating-point value
+     * is inherently limited.
+     *
+     * If the number is greater than the largest representable floating point number, positive infinity is returned.
+     * If the number is less than the smallest representable floating point number, negative infinity is returned.
+     *
+     * @return float The converted value.
+     */
+    abstract public function toFloat() : float;
+
+    /**
+     * Returns a string representation of this number.
+     *
+     * The output of this method can be parsed by the `of()` factory method;
+     * this will yield an object equal to this one, without any information loss.
+     *
+     * @return string
+     */
+    abstract public function __toString() : string;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function jsonSerialize() : string
+    {
+        return $this->__toString();
+    }
+}

+ 523 - 0
api/vendor/brick/math/src/BigRational.php

@@ -0,0 +1,523 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Brick\Math;
+
+use Brick\Math\Exception\DivisionByZeroException;
+use Brick\Math\Exception\MathException;
+use Brick\Math\Exception\NumberFormatException;
+use Brick\Math\Exception\RoundingNecessaryException;
+
+/**
+ * An arbitrarily large rational number.
+ *
+ * This class is immutable.
+ *
+ * @psalm-immutable
+ */
+final class BigRational extends BigNumber
+{
+    /**
+     * The numerator.
+     *
+     * @var BigInteger
+     */
+    private $numerator;
+
+    /**
+     * The denominator. Always strictly positive.
+     *
+     * @var BigInteger
+     */
+    private $denominator;
+
+    /**
+     * Protected constructor. Use a factory method to obtain an instance.
+     *
+     * @param BigInteger $numerator        The numerator.
+     * @param BigInteger $denominator      The denominator.
+     * @param bool       $checkDenominator Whether to check the denominator for negative and zero.
+     *
+     * @throws DivisionByZeroException If the denominator is zero.
+     */
+    protected function __construct(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator)
+    {
+        if ($checkDenominator) {
+            if ($denominator->isZero()) {
+                throw DivisionByZeroException::denominatorMustNotBeZero();
+            }
+
+            if ($denominator->isNegative()) {
+                $numerator   = $numerator->negated();
+                $denominator = $denominator->negated();
+            }
+        }
+
+        $this->numerator   = $numerator;
+        $this->denominator = $denominator;
+    }
+
+    /**
+     * Creates a BigRational of the given value.
+     *
+     * @param BigNumber|int|float|string $value
+     *
+     * @return BigRational
+     *
+     * @throws MathException If the value cannot be converted to a BigRational.
+     *
+     * @psalm-pure
+     */
+    public static function of($value) : BigNumber
+    {
+        return parent::of($value)->toBigRational();
+    }
+
+    /**
+     * Creates a BigRational out of a numerator and a denominator.
+     *
+     * If the denominator is negative, the signs of both the numerator and the denominator
+     * will be inverted to ensure that the denominator is always positive.
+     *
+     * @param BigNumber|int|float|string $numerator   The numerator. Must be convertible to a BigInteger.
+     * @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger.
+     *
+     * @return BigRational
+     *
+     * @throws NumberFormatException      If an argument does not represent a valid number.
+     * @throws RoundingNecessaryException If an argument represents a non-integer number.
+     * @throws DivisionByZeroException    If the denominator is zero.
+     *
+     * @psalm-pure
+     */
+    public static function nd($numerator, $denominator) : BigRational
+    {
+        $numerator   = BigInteger::of($numerator);
+        $denominator = BigInteger::of($denominator);
+
+        return new BigRational($numerator, $denominator, true);
+    }
+
+    /**
+     * Returns a BigRational representing zero.
+     *
+     * @return BigRational
+     *
+     * @psalm-pure
+     */
+    public static function zero() : BigRational
+    {
+        /**
+         * @psalm-suppress ImpureStaticVariable
+         * @var BigRational|null $zero
+         */
+        static $zero;
+
+        if ($zero === null) {
+            $zero = new BigRational(BigInteger::zero(), BigInteger::one(), false);
+        }
+
+        return $zero;
+    }
+
+    /**
+     * Returns a BigRational representing one.
+     *
+     * @return BigRational
+     *
+     * @psalm-pure
+     */
+    public static function one() : BigRational
+    {
+        /**
+         * @psalm-suppress ImpureStaticVariable
+         * @var BigRational|null $one
+         */
+        static $one;
+
+        if ($one === null) {
+            $one = new BigRational(BigInteger::one(), BigInteger::one(), false);
+        }
+
+        return $one;
+    }
+
+    /**
+     * Returns a BigRational representing ten.
+     *
+     * @return BigRational
+     *
+     * @psalm-pure
+     */
+    public static function ten() : BigRational
+    {
+        /**
+         * @psalm-suppress ImpureStaticVariable
+         * @var BigRational|null $ten
+         */
+        static $ten;
+
+        if ($ten === null) {
+            $ten = new BigRational(BigInteger::ten(), BigInteger::one(), false);
+        }
+
+        return $ten;
+    }
+
+    /**
+     * @return BigInteger
+     */
+    public function getNumerator() : BigInteger
+    {
+        return $this->numerator;
+    }
+
+    /**
+     * @return BigInteger
+     */
+    public function getDenominator() : BigInteger
+    {
+        return $this->denominator;
+    }
+
+    /**
+     * Returns the quotient of the division of the numerator by the denominator.
+     *
+     * @return BigInteger
+     */
+    public function quotient() : BigInteger
+    {
+        return $this->numerator->quotient($this->denominator);
+    }
+
+    /**
+     * Returns the remainder of the division of the numerator by the denominator.
+     *
+     * @return BigInteger
+     */
+    public function remainder() : BigInteger
+    {
+        return $this->numerator->remainder($this->denominator);
+    }
+
+    /**
+     * Returns the quotient and remainder of the division of the numerator by the denominator.
+     *
+     * @return BigInteger[]
+     */
+    public function quotientAndRemainder() : array
+    {
+        return $this->numerator->quotientAndRemainder($this->denominator);
+    }
+
+    /**
+     * Returns the sum of this number and the given one.
+     *
+     * @param BigNumber|int|float|string $that The number to add.
+     *
+     * @return BigRational The result.
+     *
+     * @throws MathException If the number is not valid.
+     */
+    public function plus($that) : BigRational
+    {
+        $that = BigRational::of($that);
+
+        $numerator   = $this->numerator->multipliedBy($that->denominator);
+        $numerator   = $numerator->plus($that->numerator->multipliedBy($this->denominator));
+        $denominator = $this->denominator->multipliedBy($that->denominator);
+
+        return new BigRational($numerator, $denominator, false);
+    }
+
+    /**
+     * Returns the difference of this number and the given one.
+     *
+     * @param BigNumber|int|float|string $that The number to subtract.
+     *
+     * @return BigRational The result.
+     *
+     * @throws MathException If the number is not valid.
+     */
+    public function minus($that) : BigRational
+    {
+        $that = BigRational::of($that);
+
+        $numerator   = $this->numerator->multipliedBy($that->denominator);
+        $numerator   = $numerator->minus($that->numerator->multipliedBy($this->denominator));
+        $denominator = $this->denominator->multipliedBy($that->denominator);
+
+        return new BigRational($numerator, $denominator, false);
+    }
+
+    /**
+     * Returns the product of this number and the given one.
+     *
+     * @param BigNumber|int|float|string $that The multiplier.
+     *
+     * @return BigRational The result.
+     *
+     * @throws MathException If the multiplier is not a valid number.
+     */
+    public function multipliedBy($that) : BigRational
+    {
+        $that = BigRational::of($that);
+
+        $numerator   = $this->numerator->multipliedBy($that->numerator);
+        $denominator = $this->denominator->multipliedBy($that->denominator);
+
+        return new BigRational($numerator, $denominator, false);
+    }
+
+    /**
+     * Returns the result of the division of this number by the given one.
+     *
+     * @param BigNumber|int|float|string $that The divisor.
+     *
+     * @return BigRational The result.
+     *
+     * @throws MathException If the divisor is not a valid number, or is zero.
+     */
+    public function dividedBy($that) : BigRational
+    {
+        $that = BigRational::of($that);
+
+        $numerator   = $this->numerator->multipliedBy($that->denominator);
+        $denominator = $this->denominator->multipliedBy($that->numerator);
+
+        return new BigRational($numerator, $denominator, true);
+    }
+
+    /**
+     * Returns this number exponentiated to the given value.
+     *
+     * @param int $exponent The exponent.
+     *
+     * @return BigRational The result.
+     *
+     * @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000.
+     */
+    public function power(int $exponent) : BigRational
+    {
+        if ($exponent === 0) {
+            $one = BigInteger::one();
+
+            return new BigRational($one, $one, false);
+        }
+
+        if ($exponent === 1) {
+            return $this;
+        }
+
+        return new BigRational(
+            $this->numerator->power($exponent),
+            $this->denominator->power($exponent),
+            false
+        );
+    }
+
+    /**
+     * Returns the reciprocal of this BigRational.
+     *
+     * The reciprocal has the numerator and denominator swapped.
+     *
+     * @return BigRational
+     *
+     * @throws DivisionByZeroException If the numerator is zero.
+     */
+    public function reciprocal() : BigRational
+    {
+        return new BigRational($this->denominator, $this->numerator, true);
+    }
+
+    /**
+     * Returns the absolute value of this BigRational.
+     *
+     * @return BigRational
+     */
+    public function abs() : BigRational
+    {
+        return new BigRational($this->numerator->abs(), $this->denominator, false);
+    }
+
+    /**
+     * Returns the negated value of this BigRational.
+     *
+     * @return BigRational
+     */
+    public function negated() : BigRational
+    {
+        return new BigRational($this->numerator->negated(), $this->denominator, false);
+    }
+
+    /**
+     * Returns the simplified value of this BigRational.
+     *
+     * @return BigRational
+     */
+    public function simplified() : BigRational
+    {
+        $gcd = $this->numerator->gcd($this->denominator);
+
+        $numerator = $this->numerator->quotient($gcd);
+        $denominator = $this->denominator->quotient($gcd);
+
+        return new BigRational($numerator, $denominator, false);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function compareTo($that) : int
+    {
+        return $this->minus($that)->getSign();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getSign() : int
+    {
+        return $this->numerator->getSign();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toBigInteger() : BigInteger
+    {
+        $simplified = $this->simplified();
+
+        if (! $simplified->denominator->isEqualTo(1)) {
+            throw new RoundingNecessaryException('This rational number cannot be represented as an integer value without rounding.');
+        }
+
+        return $simplified->numerator;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toBigDecimal() : BigDecimal
+    {
+        return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toBigRational() : BigRational
+    {
+        return $this;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toScale(int $scale, int $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal
+    {
+        return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toInt() : int
+    {
+        return $this->toBigInteger()->toInt();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toFloat() : float
+    {
+        return $this->numerator->toFloat() / $this->denominator->toFloat();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function __toString() : string
+    {
+        $numerator   = (string) $this->numerator;
+        $denominator = (string) $this->denominator;
+
+        if ($denominator === '1') {
+            return $numerator;
+        }
+
+        return $this->numerator . '/' . $this->denominator;
+    }
+
+    /**
+     * This method is required for serializing the object and SHOULD NOT be accessed directly.
+     *
+     * @internal
+     *
+     * @return array{numerator: BigInteger, denominator: BigInteger}
+     */
+    public function __serialize(): array
+    {
+        return ['numerator' => $this->numerator, 'denominator' => $this->denominator];
+    }
+
+    /**
+     * This method is only here to allow unserializing the object and cannot be accessed directly.
+     *
+     * @internal
+     * @psalm-suppress RedundantPropertyInitializationCheck
+     *
+     * @param array{numerator: BigInteger, denominator: BigInteger} $data
+     *
+     * @return void
+     *
+     * @throws \LogicException
+     */
+    public function __unserialize(array $data): void
+    {
+        if (isset($this->numerator)) {
+            throw new \LogicException('__unserialize() is an internal function, it must not be called directly.');
+        }
+
+        $this->numerator = $data['numerator'];
+        $this->denominator = $data['denominator'];
+    }
+
+    /**
+     * This method is required by interface Serializable and SHOULD NOT be accessed directly.
+     *
+     * @internal
+     *
+     * @return string
+     */
+    public function serialize() : string
+    {
+        return $this->numerator . '/' . $this->denominator;
+    }
+
+    /**
+     * This method is only here to implement interface Serializable and cannot be accessed directly.
+     *
+     * @internal
+     * @psalm-suppress RedundantPropertyInitializationCheck
+     *
+     * @param string $value
+     *
+     * @return void
+     *
+     * @throws \LogicException
+     */
+    public function unserialize($value) : void
+    {
+        if (isset($this->numerator)) {
+            throw new \LogicException('unserialize() is an internal function, it must not be called directly.');
+        }
+
+        [$numerator, $denominator] = \explode('/', $value);
+
+        $this->numerator   = BigInteger::of($numerator);
+        $this->denominator = BigInteger::of($denominator);
+    }
+}

+ 41 - 0
api/vendor/brick/math/src/Exception/DivisionByZeroException.php

@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Brick\Math\Exception;
+
+/**
+ * Exception thrown when a division by zero occurs.
+ */
+class DivisionByZeroException extends MathException
+{
+    /**
+     * @return DivisionByZeroException
+     *
+     * @psalm-pure
+     */
+    public static function divisionByZero() : DivisionByZeroException
+    {
+        return new self('Division by zero.');
+    }
+
+    /**
+     * @return DivisionByZeroException
+     *
+     * @psalm-pure
+     */
+    public static function modulusMustNotBeZero() : DivisionByZeroException
+    {
+        return new self('The modulus must not be zero.');
+    }
+
+    /**
+     * @return DivisionByZeroException
+     *
+     * @psalm-pure
+     */
+    public static function denominatorMustNotBeZero() : DivisionByZeroException
+    {
+        return new self('The denominator of a rational number cannot be zero.');
+    }
+}

+ 27 - 0
api/vendor/brick/math/src/Exception/IntegerOverflowException.php

@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Brick\Math\Exception;
+
+use Brick\Math\BigInteger;
+
+/**
+ * Exception thrown when an integer overflow occurs.
+ */
+class IntegerOverflowException extends MathException
+{
+    /**
+     * @param BigInteger $value
+     *
+     * @return IntegerOverflowException
+     *
+     * @psalm-pure
+     */
+    public static function toIntOverflow(BigInteger $value) : IntegerOverflowException
+    {
+        $message = '%s is out of range %d to %d and cannot be represented as an integer.';
+
+        return new self(\sprintf($message, (string) $value, PHP_INT_MIN, PHP_INT_MAX));
+    }
+}

+ 14 - 0
api/vendor/brick/math/src/Exception/MathException.php

@@ -0,0 +1,14 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Brick\Math\Exception;
+
+/**
+ * Base class for all math exceptions.
+ *
+ * This class is abstract to ensure that only fine-grained exceptions are thrown throughout the code.
+ */
+class MathException extends \RuntimeException
+{
+}

+ 12 - 0
api/vendor/brick/math/src/Exception/NegativeNumberException.php

@@ -0,0 +1,12 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Brick\Math\Exception;
+
+/**
+ * Exception thrown when attempting to perform an unsupported operation, such as a square root, on a negative number.
+ */
+class NegativeNumberException extends MathException
+{
+}

+ 35 - 0
api/vendor/brick/math/src/Exception/NumberFormatException.php

@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Brick\Math\Exception;
+
+/**
+ * Exception thrown when attempting to create a number from a string with an invalid format.
+ */
+class NumberFormatException extends MathException
+{
+    /**
+     * @param string $char The failing character.
+     *
+     * @return NumberFormatException
+     *
+     * @psalm-pure
+     */
+    public static function charNotInAlphabet(string $char) : self
+    {
+        $ord = \ord($char);
+
+        if ($ord < 32 || $ord > 126) {
+            $char = \strtoupper(\dechex($ord));
+
+            if ($ord < 10) {
+                $char = '0' . $char;
+            }
+        } else {
+            $char = '"' . $char . '"';
+        }
+
+        return new self(sprintf('Char %s is not a valid character in the given alphabet.', $char));
+    }
+}

+ 21 - 0
api/vendor/brick/math/src/Exception/RoundingNecessaryException.php

@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Brick\Math\Exception;
+
+/**
+ * Exception thrown when a number cannot be represented at the requested scale without rounding.
+ */
+class RoundingNecessaryException extends MathException
+{
+    /**
+     * @return RoundingNecessaryException
+     *
+     * @psalm-pure
+     */
+    public static function roundingNecessary() : RoundingNecessaryException
+    {
+        return new self('Rounding is necessary to represent the result of the operation at this scale.');
+    }
+}

+ 756 - 0
api/vendor/brick/math/src/Internal/Calculator.php

@@ -0,0 +1,756 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Brick\Math\Internal;
+
+use Brick\Math\Exception\RoundingNecessaryException;
+use Brick\Math\RoundingMode;
+
+/**
+ * Performs basic operations on arbitrary size integers.
+ *
+ * Unless otherwise specified, all parameters must be validated as non-empty strings of digits,
+ * without leading zero, and with an optional leading minus sign if the number is not zero.
+ *
+ * Any other parameter format will lead to undefined behaviour.
+ * All methods must return strings respecting this format, unless specified otherwise.
+ *
+ * @internal
+ *
+ * @psalm-immutable
+ */
+abstract class Calculator
+{
+    /**
+     * The maximum exponent value allowed for the pow() method.
+     */
+    public const MAX_POWER = 1000000;
+
+    /**
+     * The alphabet for converting from and to base 2 to 36, lowercase.
+     */
+    public const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
+
+    /**
+     * The Calculator instance in use.
+     *
+     * @var Calculator|null
+     */
+    private static $instance;
+
+    /**
+     * Sets the Calculator instance to use.
+     *
+     * An instance is typically set only in unit tests: the autodetect is usually the best option.
+     *
+     * @param Calculator|null $calculator The calculator instance, or NULL to revert to autodetect.
+     *
+     * @return void
+     */
+    final public static function set(?Calculator $calculator) : void
+    {
+        self::$instance = $calculator;
+    }
+
+    /**
+     * Returns the Calculator instance to use.
+     *
+     * If none has been explicitly set, the fastest available implementation will be returned.
+     *
+     * @return Calculator
+     *
+     * @psalm-pure
+     * @psalm-suppress ImpureStaticProperty
+     */
+    final public static function get() : Calculator
+    {
+        if (self::$instance === null) {
+            /** @psalm-suppress ImpureMethodCall */
+            self::$instance = self::detect();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * Returns the fastest available Calculator implementation.
+     *
+     * @codeCoverageIgnore
+     *
+     * @return Calculator
+     */
+    private static function detect() : Calculator
+    {
+        if (\extension_loaded('gmp')) {
+            return new Calculator\GmpCalculator();
+        }
+
+        if (\extension_loaded('bcmath')) {
+            return new Calculator\BcMathCalculator();
+        }
+
+        return new Calculator\NativeCalculator();
+    }
+
+    /**
+     * Extracts the sign & digits of the operands.
+     *
+     * @param string $a The first operand.
+     * @param string $b The second operand.
+     *
+     * @return array{bool, bool, string, string} Whether $a and $b are negative, followed by their digits.
+     */
+    final protected function init(string $a, string $b) : array
+    {
+        return [
+            $aNeg = ($a[0] === '-'),
+            $bNeg = ($b[0] === '-'),
+
+            $aNeg ? \substr($a, 1) : $a,
+            $bNeg ? \substr($b, 1) : $b,
+        ];
+    }
+
+    /**
+     * Returns the absolute value of a number.
+     *
+     * @param string $n The number.
+     *
+     * @return string The absolute value.
+     */
+    final public function abs(string $n) : string
+    {
+        return ($n[0] === '-') ? \substr($n, 1) : $n;
+    }
+
+    /**
+     * Negates a number.
+     *
+     * @param string $n The number.
+     *
+     * @return string The negated value.
+     */
+    final public function neg(string $n) : string
+    {
+        if ($n === '0') {
+            return '0';
+        }
+
+        if ($n[0] === '-') {
+            return \substr($n, 1);
+        }
+
+        return '-' . $n;
+    }
+
+    /**
+     * Compares two numbers.
+     *
+     * @param string $a The first number.
+     * @param string $b The second number.
+     *
+     * @return int [-1, 0, 1] If the first number is less than, equal to, or greater than the second number.
+     */
+    final public function cmp(string $a, string $b) : int
+    {
+        [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
+
+        if ($aNeg && ! $bNeg) {
+            return -1;
+        }
+
+        if ($bNeg && ! $aNeg) {
+            return 1;
+        }
+
+        $aLen = \strlen($aDig);
+        $bLen = \strlen($bDig);
+
+        if ($aLen < $bLen) {
+            $result = -1;
+        } elseif ($aLen > $bLen) {
+            $result = 1;
+        } else {
+            $result = $aDig <=> $bDig;
+        }
+
+        return $aNeg ? -$result : $result;
+    }
+
+    /**
+     * Adds two numbers.
+     *
+     * @param string $a The augend.
+     * @param string $b The addend.
+     *
+     * @return string The sum.
+     */
+    abstract public function add(string $a, string $b) : string;
+
+    /**
+     * Subtracts two numbers.
+     *
+     * @param string $a The minuend.
+     * @param string $b The subtrahend.
+     *
+     * @return string The difference.
+     */
+    abstract public function sub(string $a, string $b) : string;
+
+    /**
+     * Multiplies two numbers.
+     *
+     * @param string $a The multiplicand.
+     * @param string $b The multiplier.
+     *
+     * @return string The product.
+     */
+    abstract public function mul(string $a, string $b) : string;
+
+    /**
+     * Returns the quotient of the division of two numbers.
+     *
+     * @param string $a The dividend.
+     * @param string $b The divisor, must not be zero.
+     *
+     * @return string The quotient.
+     */
+    abstract public function divQ(string $a, string $b) : string;
+
+    /**
+     * Returns the remainder of the division of two numbers.
+     *
+     * @param string $a The dividend.
+     * @param string $b The divisor, must not be zero.
+     *
+     * @return string The remainder.
+     */
+    abstract public function divR(string $a, string $b) : string;
+
+    /**
+     * Returns the quotient and remainder of the division of two numbers.
+     *
+     * @param string $a The dividend.
+     * @param string $b The divisor, must not be zero.
+     *
+     * @return string[] An array containing the quotient and remainder.
+     */
+    abstract public function divQR(string $a, string $b) : array;
+
+    /**
+     * Exponentiates a number.
+     *
+     * @param string $a The base number.
+     * @param int    $e The exponent, validated as an integer between 0 and MAX_POWER.
+     *
+     * @return string The power.
+     */
+    abstract public function pow(string $a, int $e) : string;
+
+    /**
+     * @param string $a
+     * @param string $b The modulus; must not be zero.
+     *
+     * @return string
+     */
+    public function mod(string $a, string $b) : string
+    {
+        return $this->divR($this->add($this->divR($a, $b), $b), $b);
+    }
+
+    /**
+     * Returns the modular multiplicative inverse of $x modulo $m.
+     *
+     * If $x has no multiplicative inverse mod m, this method must return null.
+     *
+     * This method can be overridden by the concrete implementation if the underlying library has built-in support.
+     *
+     * @param string $x
+     * @param string $m The modulus; must not be negative or zero.
+     *
+     * @return string|null
+     */
+    public function modInverse(string $x, string $m) : ?string
+    {
+        if ($m === '1') {
+            return '0';
+        }
+
+        $modVal = $x;
+
+        if ($x[0] === '-' || ($this->cmp($this->abs($x), $m) >= 0)) {
+            $modVal = $this->mod($x, $m);
+        }
+
+        $x = '0';
+        $y = '0';
+        $g = $this->gcdExtended($modVal, $m, $x, $y);
+
+        if ($g !== '1') {
+            return null;
+        }
+
+        return $this->mod($this->add($this->mod($x, $m), $m), $m);
+    }
+
+    /**
+     * Raises a number into power with modulo.
+     *
+     * @param string $base The base number; must be positive or zero.
+     * @param string $exp  The exponent; must be positive or zero.
+     * @param string $mod  The modulus; must be strictly positive.
+     *
+     * @return string The power.
+     */
+    abstract public function modPow(string $base, string $exp, string $mod) : string;
+
+    /**
+     * Returns the greatest common divisor of the two numbers.
+     *
+     * This method can be overridden by the concrete implementation if the underlying library
+     * has built-in support for GCD calculations.
+     *
+     * @param string $a The first number.
+     * @param string $b The second number.
+     *
+     * @return string The GCD, always positive, or zero if both arguments are zero.
+     */
+    public function gcd(string $a, string $b) : string
+    {
+        if ($a === '0') {
+            return $this->abs($b);
+        }
+
+        if ($b === '0') {
+            return $this->abs($a);
+        }
+
+        return $this->gcd($b, $this->divR($a, $b));
+    }
+
+    private function gcdExtended(string $a, string $b, string &$x, string &$y) : string
+    {
+        if ($a === '0') {
+            $x = '0';
+            $y = '1';
+
+            return $b;
+        }
+
+        $x1 = '0';
+        $y1 = '0';
+
+        $gcd = $this->gcdExtended($this->mod($b, $a), $a, $x1, $y1);
+
+        $x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1));
+        $y = $x1;
+
+        return $gcd;
+    }
+
+    /**
+     * Returns the square root of the given number, rounded down.
+     *
+     * The result is the largest x such that x² ≤ n.
+     * The input MUST NOT be negative.
+     *
+     * @param string $n The number.
+     *
+     * @return string The square root.
+     */
+    abstract public function sqrt(string $n) : string;
+
+    /**
+     * Converts a number from an arbitrary base.
+     *
+     * This method can be overridden by the concrete implementation if the underlying library
+     * has built-in support for base conversion.
+     *
+     * @param string $number The number, positive or zero, non-empty, case-insensitively validated for the given base.
+     * @param int    $base   The base of the number, validated from 2 to 36.
+     *
+     * @return string The converted number, following the Calculator conventions.
+     */
+    public function fromBase(string $number, int $base) : string
+    {
+        return $this->fromArbitraryBase(\strtolower($number), self::ALPHABET, $base);
+    }
+
+    /**
+     * Converts a number to an arbitrary base.
+     *
+     * This method can be overridden by the concrete implementation if the underlying library
+     * has built-in support for base conversion.
+     *
+     * @param string $number The number to convert, following the Calculator conventions.
+     * @param int    $base   The base to convert to, validated from 2 to 36.
+     *
+     * @return string The converted number, lowercase.
+     */
+    public function toBase(string $number, int $base) : string
+    {
+        $negative = ($number[0] === '-');
+
+        if ($negative) {
+            $number = \substr($number, 1);
+        }
+
+        $number = $this->toArbitraryBase($number, self::ALPHABET, $base);
+
+        if ($negative) {
+            return '-' . $number;
+        }
+
+        return $number;
+    }
+
+    /**
+     * Converts a non-negative number in an arbitrary base using a custom alphabet, to base 10.
+     *
+     * @param string $number   The number to convert, validated as a non-empty string,
+     *                         containing only chars in the given alphabet/base.
+     * @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
+     * @param int    $base     The base of the number, validated from 2 to alphabet length.
+     *
+     * @return string The number in base 10, following the Calculator conventions.
+     */
+    final public function fromArbitraryBase(string $number, string $alphabet, int $base) : string
+    {
+        // remove leading "zeros"
+        $number = \ltrim($number, $alphabet[0]);
+
+        if ($number === '') {
+            return '0';
+        }
+
+        // optimize for "one"
+        if ($number === $alphabet[1]) {
+            return '1';
+        }
+
+        $result = '0';
+        $power = '1';
+
+        $base = (string) $base;
+
+        for ($i = \strlen($number) - 1; $i >= 0; $i--) {
+            $index = \strpos($alphabet, $number[$i]);
+
+            if ($index !== 0) {
+                $result = $this->add($result, ($index === 1)
+                    ? $power
+                    : $this->mul($power, (string) $index)
+                );
+            }
+
+            if ($i !== 0) {
+                $power = $this->mul($power, $base);
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Converts a non-negative number to an arbitrary base using a custom alphabet.
+     *
+     * @param string $number   The number to convert, positive or zero, following the Calculator conventions.
+     * @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum.
+     * @param int    $base     The base to convert to, validated from 2 to alphabet length.
+     *
+     * @return string The converted number in the given alphabet.
+     */
+    final public function toArbitraryBase(string $number, string $alphabet, int $base) : string
+    {
+        if ($number === '0') {
+            return $alphabet[0];
+        }
+
+        $base = (string) $base;
+        $result = '';
+
+        while ($number !== '0') {
+            [$number, $remainder] = $this->divQR($number, $base);
+            $remainder = (int) $remainder;
+
+            $result .= $alphabet[$remainder];
+        }
+
+        return \strrev($result);
+    }
+
+    /**
+     * Performs a rounded division.
+     *
+     * Rounding is performed when the remainder of the division is not zero.
+     *
+     * @param string $a            The dividend.
+     * @param string $b            The divisor, must not be zero.
+     * @param int    $roundingMode The rounding mode.
+     *
+     * @return string
+     *
+     * @throws \InvalidArgumentException  If the rounding mode is invalid.
+     * @throws RoundingNecessaryException If RoundingMode::UNNECESSARY is provided but rounding is necessary.
+     */
+    final public function divRound(string $a, string $b, int $roundingMode) : string
+    {
+        [$quotient, $remainder] = $this->divQR($a, $b);
+
+        $hasDiscardedFraction = ($remainder !== '0');
+        $isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-');
+
+        $discardedFractionSign = function() use ($remainder, $b) : int {
+            $r = $this->abs($this->mul($remainder, '2'));
+            $b = $this->abs($b);
+
+            return $this->cmp($r, $b);
+        };
+
+        $increment = false;
+
+        switch ($roundingMode) {
+            case RoundingMode::UNNECESSARY:
+                if ($hasDiscardedFraction) {
+                    throw RoundingNecessaryException::roundingNecessary();
+                }
+                break;
+
+            case RoundingMode::UP:
+                $increment = $hasDiscardedFraction;
+                break;
+
+            case RoundingMode::DOWN:
+                break;
+
+            case RoundingMode::CEILING:
+                $increment = $hasDiscardedFraction && $isPositiveOrZero;
+                break;
+
+            case RoundingMode::FLOOR:
+                $increment = $hasDiscardedFraction && ! $isPositiveOrZero;
+                break;
+
+            case RoundingMode::HALF_UP:
+                $increment = $discardedFractionSign() >= 0;
+                break;
+
+            case RoundingMode::HALF_DOWN:
+                $increment = $discardedFractionSign() > 0;
+                break;
+
+            case RoundingMode::HALF_CEILING:
+                $increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0;
+                break;
+
+            case RoundingMode::HALF_FLOOR:
+                $increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
+                break;
+
+            case RoundingMode::HALF_EVEN:
+                $lastDigit = (int) $quotient[-1];
+                $lastDigitIsEven = ($lastDigit % 2 === 0);
+                $increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0;
+                break;
+
+            default:
+                throw new \InvalidArgumentException('Invalid rounding mode.');
+        }
+
+        if ($increment) {
+            return $this->add($quotient, $isPositiveOrZero ? '1' : '-1');
+        }
+
+        return $quotient;
+    }
+
+    /**
+     * Calculates bitwise AND of two numbers.
+     *
+     * This method can be overridden by the concrete implementation if the underlying library
+     * has built-in support for bitwise operations.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function and(string $a, string $b) : string
+    {
+        return $this->bitwise('and', $a, $b);
+    }
+
+    /**
+     * Calculates bitwise OR of two numbers.
+     *
+     * This method can be overridden by the concrete implementation if the underlying library
+     * has built-in support for bitwise operations.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function or(string $a, string $b) : string
+    {
+        return $this->bitwise('or', $a, $b);
+    }
+
+    /**
+     * Calculates bitwise XOR of two numbers.
+     *
+     * This method can be overridden by the concrete implementation if the underlying library
+     * has built-in support for bitwise operations.
+     *
+     * @param string $a
+     * @param string $b
+     *
+     * @return string
+     */
+    public function xor(string $a, string $b) : string
+    {
+        return $this->bitwise('xor', $a, $b);
+    }
+
+    /**
+     * Performs a bitwise operation on a decimal number.
+     *
+     * @param string $operator The operator to use, must be "and", "or" or "xor".
+     * @param string $a        The left operand.
+     * @param string $b        The right operand.
+     *
+     * @return string
+     */
+    private function bitwise(string $operator, string $a, string $b) : string
+    {
+        [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
+
+        $aBin = $this->toBinary($aDig);
+        $bBin = $this->toBinary($bDig);
+
+        $aLen = \strlen($aBin);
+        $bLen = \strlen($bBin);
+
+        if ($aLen > $bLen) {
+            $bBin = \str_repeat("\x00", $aLen - $bLen) . $bBin;
+        } elseif ($bLen > $aLen) {
+            $aBin = \str_repeat("\x00", $bLen - $aLen) . $aBin;
+        }
+
+        if ($aNeg) {
+            $aBin = $this->twosComplement($aBin);
+        }
+        if ($bNeg) {
+            $bBin = $this->twosComplement($bBin);
+        }
+
+        switch ($operator) {
+            case 'and':
+                $value = $aBin & $bBin;
+                $negative = ($aNeg and $bNeg);
+                break;
+
+            case 'or':
+                $value = $aBin | $bBin;
+                $negative = ($aNeg or $bNeg);
+                break;
+
+            case 'xor':
+                $value = $aBin ^ $bBin;
+                $negative = ($aNeg xor $bNeg);
+                break;
+
+            // @codeCoverageIgnoreStart
+            default:
+                throw new \InvalidArgumentException('Invalid bitwise operator.');
+            // @codeCoverageIgnoreEnd
+        }
+
+        if ($negative) {
+            $value = $this->twosComplement($value);
+        }
+
+        $result = $this->toDecimal($value);
+
+        return $negative ? $this->neg($result) : $result;
+    }
+
+    /**
+     * @param string $number A positive, binary number.
+     *
+     * @return string
+     */
+    private function twosComplement(string $number) : string
+    {
+        $xor = \str_repeat("\xff", \strlen($number));
+
+        $number ^= $xor;
+
+        for ($i = \strlen($number) - 1; $i >= 0; $i--) {
+            $byte = \ord($number[$i]);
+
+            if (++$byte !== 256) {
+                $number[$i] = \chr($byte);
+                break;
+            }
+
+            $number[$i] = "\x00";
+
+            if ($i === 0) {
+                $number = "\x01" . $number;
+            }
+        }
+
+        return $number;
+    }
+
+    /**
+     * Converts a decimal number to a binary string.
+     *
+     * @param string $number The number to convert, positive or zero, only digits.
+     *
+     * @return string
+     */
+    private function toBinary(string $number) : string
+    {
+        $result = '';
+
+        while ($number !== '0') {
+            [$number, $remainder] = $this->divQR($number, '256');
+            $result .= \chr((int) $remainder);
+        }
+
+        return \strrev($result);
+    }
+
+    /**
+     * Returns the positive decimal representation of a binary number.
+     *
+     * @param string $bytes The bytes representing the number.
+     *
+     * @return string
+     */
+    private function toDecimal(string $bytes) : string
+    {
+        $result = '0';
+        $power = '1';
+
+        for ($i = \strlen($bytes) - 1; $i >= 0; $i--) {
+            $index = \ord($bytes[$i]);
+
+            if ($index !== 0) {
+                $result = $this->add($result, ($index === 1)
+                    ? $power
+                    : $this->mul($power, (string) $index)
+                );
+            }
+
+            if ($i !== 0) {
+                $power = $this->mul($power, '256');
+            }
+        }
+
+        return $result;
+    }
+}

+ 116 - 0
api/vendor/brick/math/src/Internal/Calculator/BcMathCalculator.php

@@ -0,0 +1,116 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Brick\Math\Internal\Calculator;
+
+use Brick\Math\Internal\Calculator;
+
+/**
+ * Calculator implementation built around the bcmath library.
+ *
+ * @internal
+ *
+ * @psalm-immutable
+ */
+class BcMathCalculator extends Calculator
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function add(string $a, string $b) : string
+    {
+        return \bcadd($a, $b, 0);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function sub(string $a, string $b) : string
+    {
+        return \bcsub($a, $b, 0);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function mul(string $a, string $b) : string
+    {
+        return \bcmul($a, $b, 0);
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * @psalm-suppress InvalidNullableReturnType
+     * @psalm-suppress NullableReturnStatement
+     */
+    public function divQ(string $a, string $b) : string
+    {
+        return \bcdiv($a, $b, 0);
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * @psalm-suppress InvalidNullableReturnType
+     * @psalm-suppress NullableReturnStatement
+     */
+    public function divR(string $a, string $b) : string
+    {
+        if (version_compare(PHP_VERSION, '7.2') >= 0) {
+            return \bcmod($a, $b, 0);
+        }
+
+        return \bcmod($a, $b);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function divQR(string $a, string $b) : array
+    {
+        $q = \bcdiv($a, $b, 0);
+
+        if (version_compare(PHP_VERSION, '7.2') >= 0) {
+            $r = \bcmod($a, $b, 0);
+        } else {
+            $r = \bcmod($a, $b);
+        }
+
+        assert($q !== null);
+        assert($r !== null);
+
+        return [$q, $r];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function pow(string $a, int $e) : string
+    {
+        return \bcpow($a, (string) $e, 0);
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * @psalm-suppress InvalidNullableReturnType
+     * @psalm-suppress NullableReturnStatement
+     */
+    public function modPow(string $base, string $exp, string $mod) : string
+    {
+        return \bcpowmod($base, $exp, $mod, 0);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @psalm-suppress NullableReturnStatement
+     * @psalm-suppress InvalidNullableReturnType
+     */
+    public function sqrt(string $n) : string
+    {
+        return \bcsqrt($n, 0);
+    }
+}

+ 156 - 0
api/vendor/brick/math/src/Internal/Calculator/GmpCalculator.php

@@ -0,0 +1,156 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Brick\Math\Internal\Calculator;
+
+use Brick\Math\Internal\Calculator;
+
+/**
+ * Calculator implementation built around the GMP library.
+ *
+ * @internal
+ *
+ * @psalm-immutable
+ */
+class GmpCalculator extends Calculator
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function add(string $a, string $b) : string
+    {
+        return \gmp_strval(\gmp_add($a, $b));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function sub(string $a, string $b) : string
+    {
+        return \gmp_strval(\gmp_sub($a, $b));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function mul(string $a, string $b) : string
+    {
+        return \gmp_strval(\gmp_mul($a, $b));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function divQ(string $a, string $b) : string
+    {
+        return \gmp_strval(\gmp_div_q($a, $b));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function divR(string $a, string $b) : string
+    {
+        return \gmp_strval(\gmp_div_r($a, $b));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function divQR(string $a, string $b) : array
+    {
+        [$q, $r] = \gmp_div_qr($a, $b);
+
+        return [
+            \gmp_strval($q),
+            \gmp_strval($r)
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function pow(string $a, int $e) : string
+    {
+        return \gmp_strval(\gmp_pow($a, $e));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function modInverse(string $x, string $m) : ?string
+    {
+        $result = \gmp_invert($x, $m);
+
+        if ($result === false) {
+            return null;
+        }
+
+        return \gmp_strval($result);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function modPow(string $base, string $exp, string $mod) : string
+    {
+        return \gmp_strval(\gmp_powm($base, $exp, $mod));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function gcd(string $a, string $b) : string
+    {
+        return \gmp_strval(\gmp_gcd($a, $b));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function fromBase(string $number, int $base) : string
+    {
+        return \gmp_strval(\gmp_init($number, $base));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toBase(string $number, int $base) : string
+    {
+        return \gmp_strval($number, $base);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function and(string $a, string $b) : string
+    {
+        return \gmp_strval(\gmp_and($a, $b));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function or(string $a, string $b) : string
+    {
+        return \gmp_strval(\gmp_or($a, $b));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function xor(string $a, string $b) : string
+    {
+        return \gmp_strval(\gmp_xor($a, $b));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function sqrt(string $n) : string
+    {
+        return \gmp_strval(\gmp_sqrt($n));
+    }
+}

+ 634 - 0
api/vendor/brick/math/src/Internal/Calculator/NativeCalculator.php

@@ -0,0 +1,634 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Brick\Math\Internal\Calculator;
+
+use Brick\Math\Internal\Calculator;
+
+/**
+ * Calculator implementation using only native PHP code.
+ *
+ * @internal
+ *
+ * @psalm-immutable
+ */
+class NativeCalculator extends Calculator
+{
+    /**
+     * The max number of digits the platform can natively add, subtract, multiply or divide without overflow.
+     * For multiplication, this represents the max sum of the lengths of both operands.
+     *
+     * For addition, it is assumed that an extra digit can hold a carry (1) without overflowing.
+     * Example: 32-bit: max number 1,999,999,999 (9 digits + carry)
+     *          64-bit: max number 1,999,999,999,999,999,999 (18 digits + carry)
+     *
+     * @var int
+     */
+    private $maxDigits;
+
+    /**
+     * Class constructor.
+     *
+     * @codeCoverageIgnore
+     */
+    public function __construct()
+    {
+        switch (PHP_INT_SIZE) {
+            case 4:
+                $this->maxDigits = 9;
+                break;
+
+            case 8:
+                $this->maxDigits = 18;
+                break;
+
+            default:
+                throw new \RuntimeException('The platform is not 32-bit or 64-bit as expected.');
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function add(string $a, string $b) : string
+    {
+        /**
+         * @psalm-var numeric-string $a
+         * @psalm-var numeric-string $b
+         */
+        $result = $a + $b;
+
+        if (is_int($result)) {
+            return (string) $result;
+        }
+
+        if ($a === '0') {
+            return $b;
+        }
+
+        if ($b === '0') {
+            return $a;
+        }
+
+        [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
+
+        $result = $aNeg === $bNeg ? $this->doAdd($aDig, $bDig) : $this->doSub($aDig, $bDig);
+
+        if ($aNeg) {
+            $result = $this->neg($result);
+        }
+
+        return $result;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function sub(string $a, string $b) : string
+    {
+        return $this->add($a, $this->neg($b));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function mul(string $a, string $b) : string
+    {
+        /**
+         * @psalm-var numeric-string $a
+         * @psalm-var numeric-string $b
+         */
+        $result = $a * $b;
+
+        if (is_int($result)) {
+            return (string) $result;
+        }
+
+        if ($a === '0' || $b === '0') {
+            return '0';
+        }
+
+        if ($a === '1') {
+            return $b;
+        }
+
+        if ($b === '1') {
+            return $a;
+        }
+
+        if ($a === '-1') {
+            return $this->neg($b);
+        }
+
+        if ($b === '-1') {
+            return $this->neg($a);
+        }
+
+        [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
+
+        $result = $this->doMul($aDig, $bDig);
+
+        if ($aNeg !== $bNeg) {
+            $result = $this->neg($result);
+        }
+
+        return $result;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function divQ(string $a, string $b) : string
+    {
+        return $this->divQR($a, $b)[0];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function divR(string $a, string $b): string
+    {
+        return $this->divQR($a, $b)[1];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function divQR(string $a, string $b) : array
+    {
+        if ($a === '0') {
+            return ['0', '0'];
+        }
+
+        if ($a === $b) {
+            return ['1', '0'];
+        }
+
+        if ($b === '1') {
+            return [$a, '0'];
+        }
+
+        if ($b === '-1') {
+            return [$this->neg($a), '0'];
+        }
+
+        /** @psalm-var numeric-string $a */
+        $na = $a * 1; // cast to number
+
+        if (is_int($na)) {
+            /** @psalm-var numeric-string $b */
+            $nb = $b * 1;
+
+            if (is_int($nb)) {
+                // the only division that may overflow is PHP_INT_MIN / -1,
+                // which cannot happen here as we've already handled a divisor of -1 above.
+                $r = $na % $nb;
+                $q = ($na - $r) / $nb;
+
+                assert(is_int($q));
+
+                return [
+                    (string) $q,
+                    (string) $r
+                ];
+            }
+        }
+
+        [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b);
+
+        [$q, $r] = $this->doDiv($aDig, $bDig);
+
+        if ($aNeg !== $bNeg) {
+            $q = $this->neg($q);
+        }
+
+        if ($aNeg) {
+            $r = $this->neg($r);
+        }
+
+        return [$q, $r];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function pow(string $a, int $e) : string
+    {
+        if ($e === 0) {
+            return '1';
+        }
+
+        if ($e === 1) {
+            return $a;
+        }
+
+        $odd = $e % 2;
+        $e -= $odd;
+
+        $aa = $this->mul($a, $a);
+
+        /** @psalm-suppress PossiblyInvalidArgument We're sure that $e / 2 is an int now */
+        $result = $this->pow($aa, $e / 2);
+
+        if ($odd === 1) {
+            $result = $this->mul($result, $a);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/
+     *
+     * {@inheritdoc}
+     */
+    public function modPow(string $base, string $exp, string $mod) : string
+    {
+        // special case: the algorithm below fails with 0 power 0 mod 1 (returns 1 instead of 0)
+        if ($base === '0' && $exp === '0' && $mod === '1') {
+            return '0';
+        }
+
+        // special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0)
+        if ($exp === '0' && $mod === '1') {
+            return '0';
+        }
+
+        $x = $base;
+
+        $res = '1';
+
+        // numbers are positive, so we can use remainder instead of modulo
+        $x = $this->divR($x, $mod);
+
+        while ($exp !== '0') {
+            if (in_array($exp[-1], ['1', '3', '5', '7', '9'])) { // odd
+                $res = $this->divR($this->mul($res, $x), $mod);
+            }
+
+            $exp = $this->divQ($exp, '2');
+            $x = $this->divR($this->mul($x, $x), $mod);
+        }
+
+        return $res;
+    }
+
+    /**
+     * Adapted from https://cp-algorithms.com/num_methods/roots_newton.html
+     *
+     * {@inheritDoc}
+     */
+    public function sqrt(string $n) : string
+    {
+        if ($n === '0') {
+            return '0';
+        }
+
+        // initial approximation
+        $x = \str_repeat('9', \intdiv(\strlen($n), 2) ?: 1);
+
+        $decreased = false;
+
+        for (;;) {
+            $nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2');
+
+            if ($x === $nx || $this->cmp($nx, $x) > 0 && $decreased) {
+                break;
+            }
+
+            $decreased = $this->cmp($nx, $x) < 0;
+            $x = $nx;
+        }
+
+        return $x;
+    }
+
+    /**
+     * Performs the addition of two non-signed large integers.
+     *
+     * @param string $a The first operand.
+     * @param string $b The second operand.
+     *
+     * @return string
+     */
+    private function doAdd(string $a, string $b) : string
+    {
+        [$a, $b, $length] = $this->pad($a, $b);
+
+        $carry = 0;
+        $result = '';
+
+        for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) {
+            $blockLength = $this->maxDigits;
+
+            if ($i < 0) {
+                $blockLength += $i;
+                /** @psalm-suppress LoopInvalidation */
+                $i = 0;
+            }
+
+            /** @psalm-var numeric-string $blockA */
+            $blockA = \substr($a, $i, $blockLength);
+
+            /** @psalm-var numeric-string $blockB */
+            $blockB = \substr($b, $i, $blockLength);
+
+            $sum = (string) ($blockA + $blockB + $carry);
+            $sumLength = \strlen($sum);
+
+            if ($sumLength > $blockLength) {
+                $sum = \substr($sum, 1);
+                $carry = 1;
+            } else {
+                if ($sumLength < $blockLength) {
+                    $sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
+                }
+                $carry = 0;
+            }
+
+            $result = $sum . $result;
+
+            if ($i === 0) {
+                break;
+            }
+        }
+
+        if ($carry === 1) {
+            $result = '1' . $result;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Performs the subtraction of two non-signed large integers.
+     *
+     * @param string $a The first operand.
+     * @param string $b The second operand.
+     *
+     * @return string
+     */
+    private function doSub(string $a, string $b) : string
+    {
+        if ($a === $b) {
+            return '0';
+        }
+
+        // Ensure that we always subtract to a positive result: biggest minus smallest.
+        $cmp = $this->doCmp($a, $b);
+
+        $invert = ($cmp === -1);
+
+        if ($invert) {
+            $c = $a;
+            $a = $b;
+            $b = $c;
+        }
+
+        [$a, $b, $length] = $this->pad($a, $b);
+
+        $carry = 0;
+        $result = '';
+
+        $complement = 10 ** $this->maxDigits;
+
+        for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) {
+            $blockLength = $this->maxDigits;
+
+            if ($i < 0) {
+                $blockLength += $i;
+                /** @psalm-suppress LoopInvalidation */
+                $i = 0;
+            }
+
+            /** @psalm-var numeric-string $blockA */
+            $blockA = \substr($a, $i, $blockLength);
+
+            /** @psalm-var numeric-string $blockB */
+            $blockB = \substr($b, $i, $blockLength);
+
+            $sum = $blockA - $blockB - $carry;
+
+            if ($sum < 0) {
+                $sum += $complement;
+                $carry = 1;
+            } else {
+                $carry = 0;
+            }
+
+            $sum = (string) $sum;
+            $sumLength = \strlen($sum);
+
+            if ($sumLength < $blockLength) {
+                $sum = \str_repeat('0', $blockLength - $sumLength) . $sum;
+            }
+
+            $result = $sum . $result;
+
+            if ($i === 0) {
+                break;
+            }
+        }
+
+        // Carry cannot be 1 when the loop ends, as a > b
+        assert($carry === 0);
+
+        $result = \ltrim($result, '0');
+
+        if ($invert) {
+            $result = $this->neg($result);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Performs the multiplication of two non-signed large integers.
+     *
+     * @param string $a The first operand.
+     * @param string $b The second operand.
+     *
+     * @return string
+     */
+    private function doMul(string $a, string $b) : string
+    {
+        $x = \strlen($a);
+        $y = \strlen($b);
+
+        $maxDigits = \intdiv($this->maxDigits, 2);
+        $complement = 10 ** $maxDigits;
+
+        $result = '0';
+
+        for ($i = $x - $maxDigits;; $i -= $maxDigits) {
+            $blockALength = $maxDigits;
+
+            if ($i < 0) {
+                $blockALength += $i;
+                /** @psalm-suppress LoopInvalidation */
+                $i = 0;
+            }
+
+            $blockA = (int) \substr($a, $i, $blockALength);
+
+            $line = '';
+            $carry = 0;
+
+            for ($j = $y - $maxDigits;; $j -= $maxDigits) {
+                $blockBLength = $maxDigits;
+
+                if ($j < 0) {
+                    $blockBLength += $j;
+                    /** @psalm-suppress LoopInvalidation */
+                    $j = 0;
+                }
+
+                $blockB = (int) \substr($b, $j, $blockBLength);
+
+                $mul = $blockA * $blockB + $carry;
+                $value = $mul % $complement;
+                $carry = ($mul - $value) / $complement;
+
+                $value = (string) $value;
+                $value = \str_pad($value, $maxDigits, '0', STR_PAD_LEFT);
+
+                $line = $value . $line;
+
+                if ($j === 0) {
+                    break;
+                }
+            }
+
+            if ($carry !== 0) {
+                $line = $carry . $line;
+            }
+
+            $line = \ltrim($line, '0');
+
+            if ($line !== '') {
+                $line .= \str_repeat('0', $x - $blockALength - $i);
+                $result = $this->add($result, $line);
+            }
+
+            if ($i === 0) {
+                break;
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Performs the division of two non-signed large integers.
+     *
+     * @param string $a The first operand.
+     * @param string $b The second operand.
+     *
+     * @return string[] The quotient and remainder.
+     */
+    private function doDiv(string $a, string $b) : array
+    {
+        $cmp = $this->doCmp($a, $b);
+
+        if ($cmp === -1) {
+            return ['0', $a];
+        }
+
+        $x = \strlen($a);
+        $y = \strlen($b);
+
+        // we now know that a >= b && x >= y
+
+        $q = '0'; // quotient
+        $r = $a; // remainder
+        $z = $y; // focus length, always $y or $y+1
+
+        for (;;) {
+            $focus = \substr($a, 0, $z);
+
+            $cmp = $this->doCmp($focus, $b);
+
+            if ($cmp === -1) {
+                if ($z === $x) { // remainder < dividend
+                    break;
+                }
+
+                $z++;
+            }
+
+            $zeros = \str_repeat('0', $x - $z);
+
+            $q = $this->add($q, '1' . $zeros);
+            $a = $this->sub($a, $b . $zeros);
+
+            $r = $a;
+
+            if ($r === '0') { // remainder == 0
+                break;
+            }
+
+            $x = \strlen($a);
+
+            if ($x < $y) { // remainder < dividend
+                break;
+            }
+
+            $z = $y;
+        }
+
+        return [$q, $r];
+    }
+
+    /**
+     * Compares two non-signed large numbers.
+     *
+     * @param string $a The first operand.
+     * @param string $b The second operand.
+     *
+     * @return int [-1, 0, 1]
+     */
+    private function doCmp(string $a, string $b) : int
+    {
+        $x = \strlen($a);
+        $y = \strlen($b);
+
+        $cmp = $x <=> $y;
+
+        if ($cmp !== 0) {
+            return $cmp;
+        }
+
+        return \strcmp($a, $b) <=> 0; // enforce [-1, 0, 1]
+    }
+
+    /**
+     * Pads the left of one of the given numbers with zeros if necessary to make both numbers the same length.
+     *
+     * The numbers must only consist of digits, without leading minus sign.
+     *
+     * @param string $a The first operand.
+     * @param string $b The second operand.
+     *
+     * @return array{string, string, int}
+     */
+    private function pad(string $a, string $b) : array
+    {
+        $x = \strlen($a);
+        $y = \strlen($b);
+
+        if ($x > $y) {
+            $b = \str_repeat('0', $x - $y) . $b;
+
+            return [$a, $b, $x];
+        }
+
+        if ($x < $y) {
+            $a = \str_repeat('0', $y - $x) . $a;
+
+            return [$a, $b, $y];
+        }
+
+        return [$a, $b, $x];
+    }
+}

+ 107 - 0
api/vendor/brick/math/src/RoundingMode.php

@@ -0,0 +1,107 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Brick\Math;
+
+/**
+ * Specifies a rounding behavior for numerical operations capable of discarding precision.
+ *
+ * Each rounding mode indicates how the least significant returned digit of a rounded result
+ * is to be calculated. If fewer digits are returned than the digits needed to represent the
+ * exact numerical result, the discarded digits will be referred to as the discarded fraction
+ * regardless the digits' contribution to the value of the number. In other words, considered
+ * as a numerical value, the discarded fraction could have an absolute value greater than one.
+ */
+final class RoundingMode
+{
+    /**
+     * Private constructor. This class is not instantiable.
+     *
+     * @codeCoverageIgnore
+     */
+    private function __construct()
+    {
+    }
+
+    /**
+     * Asserts that the requested operation has an exact result, hence no rounding is necessary.
+     *
+     * If this rounding mode is specified on an operation that yields a result that
+     * cannot be represented at the requested scale, a RoundingNecessaryException is thrown.
+     */
+    public const UNNECESSARY = 0;
+
+    /**
+     * Rounds away from zero.
+     *
+     * Always increments the digit prior to a nonzero discarded fraction.
+     * Note that this rounding mode never decreases the magnitude of the calculated value.
+     */
+    public const UP = 1;
+
+    /**
+     * Rounds towards zero.
+     *
+     * Never increments the digit prior to a discarded fraction (i.e., truncates).
+     * Note that this rounding mode never increases the magnitude of the calculated value.
+     */
+    public const DOWN = 2;
+
+    /**
+     * Rounds towards positive infinity.
+     *
+     * If the result is positive, behaves as for UP; if negative, behaves as for DOWN.
+     * Note that this rounding mode never decreases the calculated value.
+     */
+    public const CEILING = 3;
+
+    /**
+     * Rounds towards negative infinity.
+     *
+     * If the result is positive, behave as for DOWN; if negative, behave as for UP.
+     * Note that this rounding mode never increases the calculated value.
+     */
+    public const FLOOR = 4;
+
+    /**
+     * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round up.
+     *
+     * Behaves as for UP if the discarded fraction is >= 0.5; otherwise, behaves as for DOWN.
+     * Note that this is the rounding mode commonly taught at school.
+     */
+    public const HALF_UP = 5;
+
+    /**
+     * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down.
+     *
+     * Behaves as for UP if the discarded fraction is > 0.5; otherwise, behaves as for DOWN.
+     */
+    public const HALF_DOWN = 6;
+
+    /**
+     * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity.
+     *
+     * If the result is positive, behaves as for HALF_UP; if negative, behaves as for HALF_DOWN.
+     */
+    public const HALF_CEILING = 7;
+
+    /**
+     * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity.
+     *
+     * If the result is positive, behaves as for HALF_DOWN; if negative, behaves as for HALF_UP.
+     */
+    public const HALF_FLOOR = 8;
+
+    /**
+     * Rounds towards the "nearest neighbor" unless both neighbors are equidistant, in which case rounds towards the even neighbor.
+     *
+     * Behaves as for HALF_UP if the digit to the left of the discarded fraction is odd;
+     * behaves as for HALF_DOWN if it's even.
+     *
+     * Note that this is the rounding mode that statistically minimizes
+     * cumulative error when applied repeatedly over a sequence of calculations.
+     * It is sometimes known as "Banker's rounding", and is chiefly used in the USA.
+     */
+    public const HALF_EVEN = 9;
+}

+ 1 - 0
api/vendor/composer/autoload_classmap.php

@@ -75,6 +75,7 @@ return array(
     'Dibi\\Translator' => $vendorDir . '/dibi/dibi/src/Dibi/Translator.php',
     'Dibi\\Translator' => $vendorDir . '/dibi/dibi/src/Dibi/Translator.php',
     'Dibi\\Type' => $vendorDir . '/dibi/dibi/src/Dibi/Type.php',
     'Dibi\\Type' => $vendorDir . '/dibi/dibi/src/Dibi/Type.php',
     'Dibi\\UniqueConstraintViolationException' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',
     'Dibi\\UniqueConstraintViolationException' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',
+    'ReturnTypeWillChange' => $vendorDir . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php',
     'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
     'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
     'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
     'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
     'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
     'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',

+ 2 - 1
api/vendor/composer/autoload_files.php

@@ -13,13 +13,14 @@ return array(
     '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
     '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
     '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
     '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
     '25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
     '25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
+    'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
+    '23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php',
     '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
     '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
     '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
     '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
     '253c157292f75eb38082b5acb06f3f01' => $vendorDir . '/nikic/fast-route/src/functions.php',
     '253c157292f75eb38082b5acb06f3f01' => $vendorDir . '/nikic/fast-route/src/functions.php',
     '3109cb1a231dcd04bee1f9f620d46975' => $vendorDir . '/paragonie/sodium_compat/autoload.php',
     '3109cb1a231dcd04bee1f9f620d46975' => $vendorDir . '/paragonie/sodium_compat/autoload.php',
     'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php',
     'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php',
     'bd9634f2d41831496de0d3dfe4c94881' => $vendorDir . '/symfony/polyfill-php56/bootstrap.php',
     'bd9634f2d41831496de0d3dfe4c94881' => $vendorDir . '/symfony/polyfill-php56/bootstrap.php',
-    'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
     'fe62ba7e10580d903cc46d808b5961a4' => $vendorDir . '/tightenco/collect/src/Collect/Support/helpers.php',
     'fe62ba7e10580d903cc46d808b5961a4' => $vendorDir . '/tightenco/collect/src/Collect/Support/helpers.php',
     'caf31cc6ec7cf2241cb6f12c226c3846' => $vendorDir . '/tightenco/collect/src/Collect/Support/alias.php',
     'caf31cc6ec7cf2241cb6f12c226c3846' => $vendorDir . '/tightenco/collect/src/Collect/Support/alias.php',
     '0ccdf99b8f62f02c52cba55802e0c2e7' => $vendorDir . '/zircote/swagger-php/src/functions.php',
     '0ccdf99b8f62f02c52cba55802e0c2e7' => $vendorDir . '/zircote/swagger-php/src/functions.php',

+ 4 - 0
api/vendor/composer/autoload_psr4.php

@@ -9,6 +9,7 @@ return array(
     'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
     'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
     'Tightenco\\Collect\\' => array($vendorDir . '/tightenco/collect/src/Collect'),
     'Tightenco\\Collect\\' => array($vendorDir . '/tightenco/collect/src/Collect'),
     'Symfony\\Polyfill\\Util\\' => array($vendorDir . '/symfony/polyfill-util'),
     'Symfony\\Polyfill\\Util\\' => array($vendorDir . '/symfony/polyfill-util'),
+    'Symfony\\Polyfill\\Php81\\' => array($vendorDir . '/symfony/polyfill-php81'),
     'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
     'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
     'Symfony\\Polyfill\\Php72\\' => array($vendorDir . '/symfony/polyfill-php72'),
     'Symfony\\Polyfill\\Php72\\' => array($vendorDir . '/symfony/polyfill-php72'),
     'Symfony\\Polyfill\\Php56\\' => array($vendorDir . '/symfony/polyfill-php56'),
     'Symfony\\Polyfill\\Php56\\' => array($vendorDir . '/symfony/polyfill-php56'),
@@ -22,6 +23,7 @@ return array(
     'Slim\\' => array($vendorDir . '/slim/slim/Slim'),
     'Slim\\' => array($vendorDir . '/slim/slim/Slim'),
     'Recurr\\' => array($vendorDir . '/simshaun/recurr/src/Recurr'),
     'Recurr\\' => array($vendorDir . '/simshaun/recurr/src/Recurr'),
     'Ramsey\\Uuid\\' => array($vendorDir . '/ramsey/uuid/src'),
     'Ramsey\\Uuid\\' => array($vendorDir . '/ramsey/uuid/src'),
+    'Ramsey\\Collection\\' => array($vendorDir . '/ramsey/collection/src'),
     'Pusher\\' => array($vendorDir . '/pusher/pusher-php-server/src'),
     'Pusher\\' => array($vendorDir . '/pusher/pusher-php-server/src'),
     'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
     'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
     'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
     'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
@@ -40,6 +42,7 @@ return array(
     'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
     'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
     'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-client/src'),
     'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-client/src'),
     'Lcobucci\\JWT\\' => array($vendorDir . '/lcobucci/jwt/src'),
     'Lcobucci\\JWT\\' => array($vendorDir . '/lcobucci/jwt/src'),
+    'Lcobucci\\Clock\\' => array($vendorDir . '/lcobucci/clock/src'),
     'Kryptonit3\\Sonarr\\' => array($vendorDir . '/kryptonit3/sonarr/src'),
     'Kryptonit3\\Sonarr\\' => array($vendorDir . '/kryptonit3/sonarr/src'),
     'Kryptonit3\\SickRage\\' => array($vendorDir . '/kryptonit3/sickrage/src'),
     'Kryptonit3\\SickRage\\' => array($vendorDir . '/kryptonit3/sickrage/src'),
     'Kryptonit3\\CouchPotato\\' => array($vendorDir . '/kryptonit3/couchpotato/src'),
     'Kryptonit3\\CouchPotato\\' => array($vendorDir . '/kryptonit3/couchpotato/src'),
@@ -57,6 +60,7 @@ return array(
     'Doctrine\\Common\\Annotations\\' => array($vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations'),
     'Doctrine\\Common\\Annotations\\' => array($vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations'),
     'Cron\\' => array($vendorDir . '/dragonmantank/cron-expression/src/Cron'),
     'Cron\\' => array($vendorDir . '/dragonmantank/cron-expression/src/Cron'),
     'Composer\\Semver\\' => array($vendorDir . '/composer/semver/src'),
     'Composer\\Semver\\' => array($vendorDir . '/composer/semver/src'),
+    'Brick\\Math\\' => array($vendorDir . '/brick/math/src'),
     'Bogstag\\OAuth2\\Client\\' => array($vendorDir . '/bogstag/oauth2-trakt/src'),
     'Bogstag\\OAuth2\\Client\\' => array($vendorDir . '/bogstag/oauth2-trakt/src'),
     'Bcremer\\LineReader\\' => array($vendorDir . '/bcremer/line-reader/src'),
     'Bcremer\\LineReader\\' => array($vendorDir . '/bcremer/line-reader/src'),
     'Adldap\\' => array($vendorDir . '/adldap2/adldap2/src'),
     'Adldap\\' => array($vendorDir . '/adldap2/adldap2/src'),

+ 1 - 1
api/vendor/composer/autoload_real.php

@@ -29,7 +29,7 @@ class ComposerAutoloaderInitcbdc783d76f8e7563dcce7d8af053ecb
         spl_autoload_unregister(array('ComposerAutoloaderInitcbdc783d76f8e7563dcce7d8af053ecb', 'loadClassLoader'));
         spl_autoload_unregister(array('ComposerAutoloaderInitcbdc783d76f8e7563dcce7d8af053ecb', 'loadClassLoader'));
 
 
         require __DIR__ . '/autoload_static.php';
         require __DIR__ . '/autoload_static.php';
-        \Composer\Autoload\ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb::getInitializer($loader)();
+        call_user_func(\Composer\Autoload\ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb::getInitializer($loader));
 
 
         $loader->register(true);
         $loader->register(true);
 
 

+ 23 - 1
api/vendor/composer/autoload_static.php

@@ -14,13 +14,14 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
         '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
         '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
         '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
         '25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
         '25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
+        'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
+        '23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php',
         '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
         '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
         '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
         '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
         '253c157292f75eb38082b5acb06f3f01' => __DIR__ . '/..' . '/nikic/fast-route/src/functions.php',
         '253c157292f75eb38082b5acb06f3f01' => __DIR__ . '/..' . '/nikic/fast-route/src/functions.php',
         '3109cb1a231dcd04bee1f9f620d46975' => __DIR__ . '/..' . '/paragonie/sodium_compat/autoload.php',
         '3109cb1a231dcd04bee1f9f620d46975' => __DIR__ . '/..' . '/paragonie/sodium_compat/autoload.php',
         'e39a8b23c42d4e1452234d762b03835a' => __DIR__ . '/..' . '/ramsey/uuid/src/functions.php',
         'e39a8b23c42d4e1452234d762b03835a' => __DIR__ . '/..' . '/ramsey/uuid/src/functions.php',
         'bd9634f2d41831496de0d3dfe4c94881' => __DIR__ . '/..' . '/symfony/polyfill-php56/bootstrap.php',
         'bd9634f2d41831496de0d3dfe4c94881' => __DIR__ . '/..' . '/symfony/polyfill-php56/bootstrap.php',
-        'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
         'fe62ba7e10580d903cc46d808b5961a4' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/helpers.php',
         'fe62ba7e10580d903cc46d808b5961a4' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/helpers.php',
         'caf31cc6ec7cf2241cb6f12c226c3846' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/alias.php',
         'caf31cc6ec7cf2241cb6f12c226c3846' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/alias.php',
         '0ccdf99b8f62f02c52cba55802e0c2e7' => __DIR__ . '/..' . '/zircote/swagger-php/src/functions.php',
         '0ccdf99b8f62f02c52cba55802e0c2e7' => __DIR__ . '/..' . '/zircote/swagger-php/src/functions.php',
@@ -38,6 +39,7 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         'S' => 
         'S' => 
         array (
         array (
             'Symfony\\Polyfill\\Util\\' => 22,
             'Symfony\\Polyfill\\Util\\' => 22,
+            'Symfony\\Polyfill\\Php81\\' => 23,
             'Symfony\\Polyfill\\Php80\\' => 23,
             'Symfony\\Polyfill\\Php80\\' => 23,
             'Symfony\\Polyfill\\Php72\\' => 23,
             'Symfony\\Polyfill\\Php72\\' => 23,
             'Symfony\\Polyfill\\Php56\\' => 23,
             'Symfony\\Polyfill\\Php56\\' => 23,
@@ -54,6 +56,7 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         array (
         array (
             'Recurr\\' => 7,
             'Recurr\\' => 7,
             'Ramsey\\Uuid\\' => 12,
             'Ramsey\\Uuid\\' => 12,
+            'Ramsey\\Collection\\' => 18,
         ),
         ),
         'P' => 
         'P' => 
         array (
         array (
@@ -87,6 +90,7 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         array (
         array (
             'League\\OAuth2\\Client\\' => 21,
             'League\\OAuth2\\Client\\' => 21,
             'Lcobucci\\JWT\\' => 13,
             'Lcobucci\\JWT\\' => 13,
+            'Lcobucci\\Clock\\' => 15,
         ),
         ),
         'K' => 
         'K' => 
         array (
         array (
@@ -128,6 +132,7 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         ),
         ),
         'B' => 
         'B' => 
         array (
         array (
+            'Brick\\Math\\' => 11,
             'Bogstag\\OAuth2\\Client\\' => 22,
             'Bogstag\\OAuth2\\Client\\' => 22,
             'Bcremer\\LineReader\\' => 19,
             'Bcremer\\LineReader\\' => 19,
         ),
         ),
@@ -150,6 +155,10 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         array (
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-util',
             0 => __DIR__ . '/..' . '/symfony/polyfill-util',
         ),
         ),
+        'Symfony\\Polyfill\\Php81\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/symfony/polyfill-php81',
+        ),
         'Symfony\\Polyfill\\Php80\\' => 
         'Symfony\\Polyfill\\Php80\\' => 
         array (
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
             0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
@@ -202,6 +211,10 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         array (
         array (
             0 => __DIR__ . '/..' . '/ramsey/uuid/src',
             0 => __DIR__ . '/..' . '/ramsey/uuid/src',
         ),
         ),
+        'Ramsey\\Collection\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/ramsey/collection/src',
+        ),
         'Pusher\\' => 
         'Pusher\\' => 
         array (
         array (
             0 => __DIR__ . '/..' . '/pusher/pusher-php-server/src',
             0 => __DIR__ . '/..' . '/pusher/pusher-php-server/src',
@@ -276,6 +289,10 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         array (
         array (
             0 => __DIR__ . '/..' . '/lcobucci/jwt/src',
             0 => __DIR__ . '/..' . '/lcobucci/jwt/src',
         ),
         ),
+        'Lcobucci\\Clock\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/lcobucci/clock/src',
+        ),
         'Kryptonit3\\Sonarr\\' => 
         'Kryptonit3\\Sonarr\\' => 
         array (
         array (
             0 => __DIR__ . '/..' . '/kryptonit3/sonarr/src',
             0 => __DIR__ . '/..' . '/kryptonit3/sonarr/src',
@@ -344,6 +361,10 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         array (
         array (
             0 => __DIR__ . '/..' . '/composer/semver/src',
             0 => __DIR__ . '/..' . '/composer/semver/src',
         ),
         ),
+        'Brick\\Math\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/brick/math/src',
+        ),
         'Bogstag\\OAuth2\\Client\\' => 
         'Bogstag\\OAuth2\\Client\\' => 
         array (
         array (
             0 => __DIR__ . '/..' . '/bogstag/oauth2-trakt/src',
             0 => __DIR__ . '/..' . '/bogstag/oauth2-trakt/src',
@@ -445,6 +466,7 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         'Dibi\\Translator' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Translator.php',
         'Dibi\\Translator' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Translator.php',
         'Dibi\\Type' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Type.php',
         'Dibi\\Type' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Type.php',
         'Dibi\\UniqueConstraintViolationException' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',
         'Dibi\\UniqueConstraintViolationException' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',
+        'ReturnTypeWillChange' => __DIR__ . '/..' . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php',
         'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
         'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
         'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
         'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
         'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
         'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',

+ 367 - 68
api/vendor/composer/installed.json

@@ -173,6 +173,69 @@
             },
             },
             "install-path": "../bogstag/oauth2-trakt"
             "install-path": "../bogstag/oauth2-trakt"
         },
         },
+        {
+            "name": "brick/math",
+            "version": "0.9.3",
+            "version_normalized": "0.9.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/brick/math.git",
+                "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/brick/math/zipball/ca57d18f028f84f777b2168cd1911b0dee2343ae",
+                "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "php": "^7.1 || ^8.0"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "^2.2",
+                "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0",
+                "vimeo/psalm": "4.9.2"
+            },
+            "time": "2021-08-15T20:50:18+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Brick\\Math\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Arbitrary-precision arithmetic library",
+            "keywords": [
+                "Arbitrary-precision",
+                "BigInteger",
+                "BigRational",
+                "arithmetic",
+                "bigdecimal",
+                "bignum",
+                "brick",
+                "math"
+            ],
+            "support": {
+                "issues": "https://github.com/brick/math/issues",
+                "source": "https://github.com/brick/math/tree/0.9.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/BenMorel",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/brick/math",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../brick/math"
+        },
         {
         {
             "name": "composer/semver",
             "name": "composer/semver",
             "version": "1.7.2",
             "version": "1.7.2",
@@ -1063,40 +1126,109 @@
             "description": "PHP Sonarr API Wrapper",
             "description": "PHP Sonarr API Wrapper",
             "install-path": "../kryptonit3/sonarr"
             "install-path": "../kryptonit3/sonarr"
         },
         },
+        {
+            "name": "lcobucci/clock",
+            "version": "2.0.0",
+            "version_normalized": "2.0.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/lcobucci/clock.git",
+                "reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/lcobucci/clock/zipball/353d83fe2e6ae95745b16b3d911813df6a05bfb3",
+                "reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.4 || ^8.0"
+            },
+            "require-dev": {
+                "infection/infection": "^0.17",
+                "lcobucci/coding-standard": "^6.0",
+                "phpstan/extension-installer": "^1.0",
+                "phpstan/phpstan": "^0.12",
+                "phpstan/phpstan-deprecation-rules": "^0.12",
+                "phpstan/phpstan-phpunit": "^0.12",
+                "phpstan/phpstan-strict-rules": "^0.12",
+                "phpunit/php-code-coverage": "9.1.4",
+                "phpunit/phpunit": "9.3.7"
+            },
+            "time": "2020-08-27T18:56:02+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Lcobucci\\Clock\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Luís Cobucci",
+                    "email": "lcobucci@gmail.com"
+                }
+            ],
+            "description": "Yet another clock abstraction",
+            "support": {
+                "issues": "https://github.com/lcobucci/clock/issues",
+                "source": "https://github.com/lcobucci/clock/tree/2.0.x"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/lcobucci",
+                    "type": "github"
+                },
+                {
+                    "url": "https://www.patreon.com/lcobucci",
+                    "type": "patreon"
+                }
+            ],
+            "install-path": "../lcobucci/clock"
+        },
         {
         {
             "name": "lcobucci/jwt",
             "name": "lcobucci/jwt",
-            "version": "3.3.1",
-            "version_normalized": "3.3.1.0",
+            "version": "4.1.5",
+            "version_normalized": "4.1.5.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/lcobucci/jwt.git",
                 "url": "https://github.com/lcobucci/jwt.git",
-                "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18"
+                "reference": "fe2d89f2eaa7087af4aa166c6f480ef04e000582"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/lcobucci/jwt/zipball/a11ec5f4b4d75d1fcd04e133dede4c317aac9e18",
-                "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18",
+                "url": "https://api.github.com/repos/lcobucci/jwt/zipball/fe2d89f2eaa7087af4aa166c6f480ef04e000582",
+                "reference": "fe2d89f2eaa7087af4aa166c6f480ef04e000582",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
+                "ext-hash": "*",
+                "ext-json": "*",
                 "ext-mbstring": "*",
                 "ext-mbstring": "*",
                 "ext-openssl": "*",
                 "ext-openssl": "*",
-                "php": "^5.6 || ^7.0"
+                "ext-sodium": "*",
+                "lcobucci/clock": "^2.0",
+                "php": "^7.4 || ^8.0"
             },
             },
             "require-dev": {
             "require-dev": {
-                "mikey179/vfsstream": "~1.5",
-                "phpmd/phpmd": "~2.2",
-                "phpunit/php-invoker": "~1.1",
-                "phpunit/phpunit": "^5.7 || ^7.3",
-                "squizlabs/php_codesniffer": "~2.3"
+                "infection/infection": "^0.21",
+                "lcobucci/coding-standard": "^6.0",
+                "mikey179/vfsstream": "^1.6.7",
+                "phpbench/phpbench": "^1.0",
+                "phpstan/extension-installer": "^1.0",
+                "phpstan/phpstan": "^0.12",
+                "phpstan/phpstan-deprecation-rules": "^0.12",
+                "phpstan/phpstan-phpunit": "^0.12",
+                "phpstan/phpstan-strict-rules": "^0.12",
+                "phpunit/php-invoker": "^3.1",
+                "phpunit/phpunit": "^9.5"
             },
             },
-            "time": "2019-05-24T18:30:49+00:00",
+            "time": "2021-09-28T19:34:56+00:00",
             "type": "library",
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.1-dev"
-                }
-            },
             "installation-source": "dist",
             "installation-source": "dist",
             "autoload": {
             "autoload": {
                 "psr-4": {
                 "psr-4": {
@@ -1109,7 +1241,7 @@
             ],
             ],
             "authors": [
             "authors": [
                 {
                 {
-                    "name": "Luís Otávio Cobucci Oblonczyk",
+                    "name": "Luís Cobucci",
                     "email": "lcobucci@gmail.com",
                     "email": "lcobucci@gmail.com",
                     "role": "Developer"
                     "role": "Developer"
                 }
                 }
@@ -1121,8 +1253,18 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/lcobucci/jwt/issues",
                 "issues": "https://github.com/lcobucci/jwt/issues",
-                "source": "https://github.com/lcobucci/jwt/tree/3.3"
+                "source": "https://github.com/lcobucci/jwt/tree/4.1.5"
             },
             },
+            "funding": [
+                {
+                    "url": "https://github.com/lcobucci",
+                    "type": "github"
+                },
+                {
+                    "url": "https://www.patreon.com/lcobucci",
+                    "type": "patreon"
+                }
+            ],
             "install-path": "../lcobucci/jwt"
             "install-path": "../lcobucci/jwt"
         },
         },
         {
         {
@@ -2642,91 +2784,168 @@
             "description": "A polyfill for getallheaders.",
             "description": "A polyfill for getallheaders.",
             "install-path": "../ralouphie/getallheaders"
             "install-path": "../ralouphie/getallheaders"
         },
         },
+        {
+            "name": "ramsey/collection",
+            "version": "1.2.2",
+            "version_normalized": "1.2.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ramsey/collection.git",
+                "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ramsey/collection/zipball/cccc74ee5e328031b15640b51056ee8d3bb66c0a",
+                "reference": "cccc74ee5e328031b15640b51056ee8d3bb66c0a",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.3 || ^8",
+                "symfony/polyfill-php81": "^1.23"
+            },
+            "require-dev": {
+                "captainhook/captainhook": "^5.3",
+                "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
+                "ergebnis/composer-normalize": "^2.6",
+                "fakerphp/faker": "^1.5",
+                "hamcrest/hamcrest-php": "^2",
+                "jangregor/phpstan-prophecy": "^0.8",
+                "mockery/mockery": "^1.3",
+                "phpspec/prophecy-phpunit": "^2.0",
+                "phpstan/extension-installer": "^1",
+                "phpstan/phpstan": "^0.12.32",
+                "phpstan/phpstan-mockery": "^0.12.5",
+                "phpstan/phpstan-phpunit": "^0.12.11",
+                "phpunit/phpunit": "^8.5 || ^9",
+                "psy/psysh": "^0.10.4",
+                "slevomat/coding-standard": "^6.3",
+                "squizlabs/php_codesniffer": "^3.5",
+                "vimeo/psalm": "^4.4"
+            },
+            "time": "2021-10-10T03:01:02+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Ramsey\\Collection\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ben Ramsey",
+                    "email": "ben@benramsey.com",
+                    "homepage": "https://benramsey.com"
+                }
+            ],
+            "description": "A PHP library for representing and manipulating collections.",
+            "keywords": [
+                "array",
+                "collection",
+                "hash",
+                "map",
+                "queue",
+                "set"
+            ],
+            "support": {
+                "issues": "https://github.com/ramsey/collection/issues",
+                "source": "https://github.com/ramsey/collection/tree/1.2.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/ramsey",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/ramsey/collection",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../ramsey/collection"
+        },
         {
         {
             "name": "ramsey/uuid",
             "name": "ramsey/uuid",
-            "version": "3.9.6",
-            "version_normalized": "3.9.6.0",
+            "version": "4.2.3",
+            "version_normalized": "4.2.3.0",
             "source": {
             "source": {
                 "type": "git",
                 "type": "git",
                 "url": "https://github.com/ramsey/uuid.git",
                 "url": "https://github.com/ramsey/uuid.git",
-                "reference": "ffa80ab953edd85d5b6c004f96181a538aad35a3"
+                "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df"
             },
             },
             "dist": {
             "dist": {
                 "type": "zip",
                 "type": "zip",
-                "url": "https://api.github.com/repos/ramsey/uuid/zipball/ffa80ab953edd85d5b6c004f96181a538aad35a3",
-                "reference": "ffa80ab953edd85d5b6c004f96181a538aad35a3",
+                "url": "https://api.github.com/repos/ramsey/uuid/zipball/fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df",
+                "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df",
                 "shasum": ""
                 "shasum": ""
             },
             },
             "require": {
             "require": {
+                "brick/math": "^0.8 || ^0.9",
                 "ext-json": "*",
                 "ext-json": "*",
-                "paragonie/random_compat": "^1 | ^2 | ^9.99.99",
-                "php": "^5.4 | ^7.0 | ^8.0",
-                "symfony/polyfill-ctype": "^1.8"
+                "php": "^7.2 || ^8.0",
+                "ramsey/collection": "^1.0",
+                "symfony/polyfill-ctype": "^1.8",
+                "symfony/polyfill-php80": "^1.14"
             },
             },
             "replace": {
             "replace": {
                 "rhumsaa/uuid": "self.version"
                 "rhumsaa/uuid": "self.version"
             },
             },
             "require-dev": {
             "require-dev": {
-                "codeception/aspect-mock": "^1 | ^2",
-                "doctrine/annotations": "^1.2",
-                "goaop/framework": "1.0.0-alpha.2 | ^1 | >=2.1.0 <=2.3.2",
-                "mockery/mockery": "^0.9.11 | ^1",
+                "captainhook/captainhook": "^5.10",
+                "captainhook/plugin-composer": "^5.3",
+                "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
+                "doctrine/annotations": "^1.8",
+                "ergebnis/composer-normalize": "^2.15",
+                "mockery/mockery": "^1.3",
                 "moontoast/math": "^1.1",
                 "moontoast/math": "^1.1",
-                "nikic/php-parser": "<=4.5.0",
                 "paragonie/random-lib": "^2",
                 "paragonie/random-lib": "^2",
-                "php-mock/php-mock-phpunit": "^0.3 | ^1.1 | ^2.6",
-                "php-parallel-lint/php-parallel-lint": "^1.3",
-                "phpunit/phpunit": ">=4.8.36 <9.0.0 | >=9.3.0",
+                "php-mock/php-mock": "^2.2",
+                "php-mock/php-mock-mockery": "^1.3",
+                "php-parallel-lint/php-parallel-lint": "^1.1",
+                "phpbench/phpbench": "^1.0",
+                "phpstan/extension-installer": "^1.0",
+                "phpstan/phpstan": "^0.12",
+                "phpstan/phpstan-mockery": "^0.12",
+                "phpstan/phpstan-phpunit": "^0.12",
+                "phpunit/phpunit": "^8.5 || ^9",
+                "slevomat/coding-standard": "^7.0",
                 "squizlabs/php_codesniffer": "^3.5",
                 "squizlabs/php_codesniffer": "^3.5",
-                "yoast/phpunit-polyfills": "^1.0"
+                "vimeo/psalm": "^4.9"
             },
             },
             "suggest": {
             "suggest": {
-                "ext-ctype": "Provides support for PHP Ctype functions",
-                "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator",
-                "ext-openssl": "Provides the OpenSSL extension for use with the OpenSslGenerator",
-                "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator",
-                "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).",
+                "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.",
+                "ext-ctype": "Enables faster processing of character classification using ctype functions.",
+                "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.",
+                "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.",
                 "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter",
                 "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter",
-                "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid",
                 "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type."
                 "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type."
             },
             },
-            "time": "2021-09-25T23:07:42+00:00",
+            "time": "2021-09-25T23:10:38+00:00",
             "type": "library",
             "type": "library",
             "extra": {
             "extra": {
                 "branch-alias": {
                 "branch-alias": {
-                    "dev-master": "3.x-dev"
+                    "dev-main": "4.x-dev"
+                },
+                "captainhook": {
+                    "force-install": true
                 }
                 }
             },
             },
             "installation-source": "dist",
             "installation-source": "dist",
             "autoload": {
             "autoload": {
-                "psr-4": {
-                    "Ramsey\\Uuid\\": "src/"
-                },
                 "files": [
                 "files": [
                     "src/functions.php"
                     "src/functions.php"
-                ]
+                ],
+                "psr-4": {
+                    "Ramsey\\Uuid\\": "src/"
+                }
             },
             },
             "notification-url": "https://packagist.org/downloads/",
             "notification-url": "https://packagist.org/downloads/",
             "license": [
             "license": [
                 "MIT"
                 "MIT"
             ],
             ],
-            "authors": [
-                {
-                    "name": "Ben Ramsey",
-                    "email": "ben@benramsey.com",
-                    "homepage": "https://benramsey.com"
-                },
-                {
-                    "name": "Marijn Huizendveld",
-                    "email": "marijn.huizendveld@gmail.com"
-                },
-                {
-                    "name": "Thibaud Fabre",
-                    "email": "thibaud@aztech.io"
-                }
-            ],
-            "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).",
-            "homepage": "https://github.com/ramsey/uuid",
+            "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).",
             "keywords": [
             "keywords": [
                 "guid",
                 "guid",
                 "identifier",
                 "identifier",
@@ -2734,9 +2953,7 @@
             ],
             ],
             "support": {
             "support": {
                 "issues": "https://github.com/ramsey/uuid/issues",
                 "issues": "https://github.com/ramsey/uuid/issues",
-                "rss": "https://github.com/ramsey/uuid/releases.atom",
-                "source": "https://github.com/ramsey/uuid",
-                "wiki": "https://github.com/ramsey/uuid/wiki"
+                "source": "https://github.com/ramsey/uuid/tree/4.2.3"
             },
             },
             "funding": [
             "funding": [
                 {
                 {
@@ -3633,6 +3850,88 @@
             ],
             ],
             "install-path": "../symfony/polyfill-php80"
             "install-path": "../symfony/polyfill-php80"
         },
         },
+        {
+            "name": "symfony/polyfill-php81",
+            "version": "v1.25.0",
+            "version_normalized": "1.25.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php81.git",
+                "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f",
+                "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "time": "2021-09-13T13:58:11+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.23-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php81\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php81/tree/v1.25.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../symfony/polyfill-php81"
+        },
         {
         {
             "name": "symfony/polyfill-util",
             "name": "symfony/polyfill-util",
             "version": "v1.9.0",
             "version": "v1.9.0",

+ 45 - 9
api/vendor/composer/installed.php

@@ -5,7 +5,7 @@
         'type' => 'library',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
         'aliases' => array(),
-        'reference' => '3a141f74828baa2322d2a095494c4ddf646790d9',
+        'reference' => '1ae02fda50382bb86ea50b76579b5e773c4a1ff1',
         'name' => '__root__',
         'name' => '__root__',
         'dev' => true,
         'dev' => true,
     ),
     ),
@@ -16,7 +16,7 @@
             'type' => 'library',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
             'aliases' => array(),
-            'reference' => '3a141f74828baa2322d2a095494c4ddf646790d9',
+            'reference' => '1ae02fda50382bb86ea50b76579b5e773c4a1ff1',
             'dev_requirement' => false,
             'dev_requirement' => false,
         ),
         ),
         'adldap2/adldap2' => array(
         'adldap2/adldap2' => array(
@@ -46,6 +46,15 @@
             'reference' => 'fbb9253d9e317e84dc2b3f1253afc1dcbb4414a2',
             'reference' => 'fbb9253d9e317e84dc2b3f1253afc1dcbb4414a2',
             'dev_requirement' => false,
             'dev_requirement' => false,
         ),
         ),
+        'brick/math' => array(
+            'pretty_version' => '0.9.3',
+            'version' => '0.9.3.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../brick/math',
+            'aliases' => array(),
+            'reference' => 'ca57d18f028f84f777b2168cd1911b0dee2343ae',
+            'dev_requirement' => false,
+        ),
         'composer/semver' => array(
         'composer/semver' => array(
             'pretty_version' => '1.7.2',
             'pretty_version' => '1.7.2',
             'version' => '1.7.2.0',
             'version' => '1.7.2.0',
@@ -178,13 +187,22 @@
             'reference' => 'e30c5c783a837270bcef81571ca9b95909c52e5e',
             'reference' => 'e30c5c783a837270bcef81571ca9b95909c52e5e',
             'dev_requirement' => false,
             'dev_requirement' => false,
         ),
         ),
+        'lcobucci/clock' => array(
+            'pretty_version' => '2.0.0',
+            'version' => '2.0.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../lcobucci/clock',
+            'aliases' => array(),
+            'reference' => '353d83fe2e6ae95745b16b3d911813df6a05bfb3',
+            'dev_requirement' => false,
+        ),
         'lcobucci/jwt' => array(
         'lcobucci/jwt' => array(
-            'pretty_version' => '3.3.1',
-            'version' => '3.3.1.0',
+            'pretty_version' => '4.1.5',
+            'version' => '4.1.5.0',
             'type' => 'library',
             'type' => 'library',
             'install_path' => __DIR__ . '/../lcobucci/jwt',
             'install_path' => __DIR__ . '/../lcobucci/jwt',
             'aliases' => array(),
             'aliases' => array(),
-            'reference' => 'a11ec5f4b4d75d1fcd04e133dede4c317aac9e18',
+            'reference' => 'fe2d89f2eaa7087af4aa166c6f480ef04e000582',
             'dev_requirement' => false,
             'dev_requirement' => false,
         ),
         ),
         'league/oauth2-client' => array(
         'league/oauth2-client' => array(
@@ -442,19 +460,28 @@
             'reference' => '120b605dfeb996808c31b6477290a714d356e822',
             'reference' => '120b605dfeb996808c31b6477290a714d356e822',
             'dev_requirement' => false,
             'dev_requirement' => false,
         ),
         ),
+        'ramsey/collection' => array(
+            'pretty_version' => '1.2.2',
+            'version' => '1.2.2.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../ramsey/collection',
+            'aliases' => array(),
+            'reference' => 'cccc74ee5e328031b15640b51056ee8d3bb66c0a',
+            'dev_requirement' => false,
+        ),
         'ramsey/uuid' => array(
         'ramsey/uuid' => array(
-            'pretty_version' => '3.9.6',
-            'version' => '3.9.6.0',
+            'pretty_version' => '4.2.3',
+            'version' => '4.2.3.0',
             'type' => 'library',
             'type' => 'library',
             'install_path' => __DIR__ . '/../ramsey/uuid',
             'install_path' => __DIR__ . '/../ramsey/uuid',
             'aliases' => array(),
             'aliases' => array(),
-            'reference' => 'ffa80ab953edd85d5b6c004f96181a538aad35a3',
+            'reference' => 'fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df',
             'dev_requirement' => false,
             'dev_requirement' => false,
         ),
         ),
         'rhumsaa/uuid' => array(
         'rhumsaa/uuid' => array(
             'dev_requirement' => false,
             'dev_requirement' => false,
             'replaced' => array(
             'replaced' => array(
-                0 => '3.9.6',
+                0 => '4.2.3',
             ),
             ),
         ),
         ),
         'rmccue/requests' => array(
         'rmccue/requests' => array(
@@ -565,6 +592,15 @@
             'reference' => 'dc3063ba22c2a1fd2f45ed856374d79114998f91',
             'reference' => 'dc3063ba22c2a1fd2f45ed856374d79114998f91',
             'dev_requirement' => false,
             'dev_requirement' => false,
         ),
         ),
+        'symfony/polyfill-php81' => array(
+            'pretty_version' => 'v1.25.0',
+            'version' => '1.25.0.0',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../symfony/polyfill-php81',
+            'aliases' => array(),
+            'reference' => '5de4ba2d41b15f9bd0e19b2ab9674135813ec98f',
+            'dev_requirement' => false,
+        ),
         'symfony/polyfill-util' => array(
         'symfony/polyfill-util' => array(
             'pretty_version' => 'v1.9.0',
             'pretty_version' => 'v1.9.0',
             'version' => '1.9.0.0',
             'version' => '1.9.0.0',

+ 2 - 2
api/vendor/composer/platform_check.php

@@ -4,8 +4,8 @@
 
 
 $issues = array();
 $issues = array();
 
 
-if (!(PHP_VERSION_ID >= 70300)) {
-    $issues[] = 'Your Composer dependencies require a PHP version ">= 7.3.0". You are running ' . PHP_VERSION . '.';
+if (!(PHP_VERSION_ID >= 70400)) {
+    $issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.0". You are running ' . PHP_VERSION . '.';
 }
 }
 
 
 if ($issues) {
 if ($issues) {

+ 21 - 0
api/vendor/lcobucci/clock/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Luís Cobucci
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 40 - 0
api/vendor/lcobucci/clock/composer.json

@@ -0,0 +1,40 @@
+{
+    "name": "lcobucci/clock",
+    "description": "Yet another clock abstraction",
+    "type": "library",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Luís Cobucci",
+            "email": "lcobucci@gmail.com"
+        }
+    ],
+    "require": {
+        "php": "^7.4 || ^8.0"
+    },
+    "require-dev": {
+        "infection/infection": "^0.17",
+        "lcobucci/coding-standard": "^6.0",
+        "phpstan/extension-installer": "^1.0",
+        "phpstan/phpstan": "^0.12",
+        "phpstan/phpstan-deprecation-rules": "^0.12",
+        "phpstan/phpstan-phpunit": "^0.12",
+        "phpstan/phpstan-strict-rules": "^0.12",
+        "phpunit/php-code-coverage": "9.1.4",
+        "phpunit/phpunit": "9.3.7"
+    },
+    "autoload": {
+        "psr-4": {
+            "Lcobucci\\Clock\\": "src"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "Lcobucci\\Clock\\": "test"
+        }
+    },
+    "config": {
+        "preferred-install": "dist",
+        "sort-packages": true
+    }
+}

+ 11 - 0
api/vendor/lcobucci/clock/src/Clock.php

@@ -0,0 +1,11 @@
+<?php
+declare(strict_types=1);
+
+namespace Lcobucci\Clock;
+
+use DateTimeImmutable;
+
+interface Clock
+{
+    public function now(): DateTimeImmutable;
+}

+ 32 - 0
api/vendor/lcobucci/clock/src/FrozenClock.php

@@ -0,0 +1,32 @@
+<?php
+declare(strict_types=1);
+
+namespace Lcobucci\Clock;
+
+use DateTimeImmutable;
+use DateTimeZone;
+
+final class FrozenClock implements Clock
+{
+    private DateTimeImmutable $now;
+
+    public function __construct(DateTimeImmutable $now)
+    {
+        $this->now = $now;
+    }
+
+    public static function fromUTC(): self
+    {
+        return new self(new DateTimeImmutable('now', new DateTimeZone('UTC')));
+    }
+
+    public function setTo(DateTimeImmutable $now): void
+    {
+        $this->now = $now;
+    }
+
+    public function now(): DateTimeImmutable
+    {
+        return $this->now;
+    }
+}

+ 34 - 0
api/vendor/lcobucci/clock/src/SystemClock.php

@@ -0,0 +1,34 @@
+<?php
+declare(strict_types=1);
+
+namespace Lcobucci\Clock;
+
+use DateTimeImmutable;
+use DateTimeZone;
+
+use function date_default_timezone_get;
+
+final class SystemClock implements Clock
+{
+    private DateTimeZone $timezone;
+
+    public function __construct(DateTimeZone $timezone)
+    {
+        $this->timezone = $timezone;
+    }
+
+    public static function fromUTC(): self
+    {
+        return new self(new DateTimeZone('UTC'));
+    }
+
+    public static function fromSystemTimezone(): self
+    {
+        return new self(new DateTimeZone(date_default_timezone_get()));
+    }
+
+    public function now(): DateTimeImmutable
+    {
+        return new DateTimeImmutable('now', $this->timezone);
+    }
+}

+ 0 - 3
api/vendor/lcobucci/jwt/.gitignore

@@ -1,3 +0,0 @@
-/vendor
-/phpunit.xml
-/composer.lock

+ 0 - 56
api/vendor/lcobucci/jwt/.scrutinizer.yml

@@ -1,56 +0,0 @@
-build:
-    environment:
-        mysql: false
-        postgresql: false
-        redis: false
-        rabbitmq: false
-        php:
-            version: 5.6
-tools:
-    php_sim: true
-    php_pdepend: true
-    php_analyzer: true
-    php_changetracking: true
-    php_code_sniffer:
-        config:
-            standard: "PSR2"
-    php_mess_detector: true
-checks:
-    php:
-        code_rating: true
-        duplication: true
-        argument_type_checks: true
-        assignment_of_null_return: true
-        avoid_conflicting_incrementers: true
-        avoid_useless_overridden_methods: true
-        catch_class_exists: true
-        closure_use_modifiable: true
-        closure_use_not_conflicting: true
-        deprecated_code_usage: true
-        method_calls_on_non_object: true
-        missing_arguments: true
-        no_duplicate_arguments: true
-        no_non_implemented_abstract_methods: true
-        no_property_on_interface: true
-        parameter_non_unique: true
-        precedence_in_conditions: true
-        precedence_mistakes: true
-        require_php_tag_first: true
-        security_vulnerabilities: true
-        sql_injection_vulnerabilities: true
-        too_many_arguments: true
-        unreachable_code: true
-        unused_methods: true
-        unused_parameters: true
-        unused_properties: true
-        unused_variables: true
-        use_statement_alias_conflict: true
-        useless_calls: true
-        variable_existence: true
-        verify_access_scope_valid: true
-        verify_argument_usable_as_reference: true
-        verify_property_names: true
-
-filter:
-    excluded_paths:
-        - test/*

+ 0 - 15
api/vendor/lcobucci/jwt/.travis.yml

@@ -1,15 +0,0 @@
-language: php
-php:
-  - 5.6
-  - 7.0
-  - 7.1
-  - 7.2
-  - 7.3
-  - nightly
-
-matrix:
-  allow_failures:
-    - php: nightly
-
-before_script:
-  - composer install --prefer-dist -o

+ 1 - 1
api/vendor/lcobucci/jwt/LICENSE

@@ -1,4 +1,4 @@
-Copyright (c) 2014-2015, Luís Otávio Cobucci Oblonczyk
+Copyright (c) 2014, Luís Cobucci
 All rights reserved.
 All rights reserved.
 
 
 Redistribution and use in source and binary forms, with or without
 Redistribution and use in source and binary forms, with or without

+ 0 - 194
api/vendor/lcobucci/jwt/README.md

@@ -1,194 +0,0 @@
-# JWT
-[![Gitter](https://img.shields.io/badge/GITTER-JOIN%20CHAT%20%E2%86%92-brightgreen.svg?style=flat-square)](https://gitter.im/lcobucci/jwt?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Total Downloads](https://img.shields.io/packagist/dt/lcobucci/jwt.svg?style=flat-square)](https://packagist.org/packages/lcobucci/jwt) [![Latest Stable Version](https://img.shields.io/packagist/v/lcobucci/jwt.svg?style=flat-square)](https://packagist.org/packages/lcobucci/jwt)
-
-![Branch master](https://img.shields.io/badge/branch-master-brightgreen.svg?style=flat-square)
-[![Build Status](https://img.shields.io/travis/lcobucci/jwt/master.svg?style=flat-square)](http://travis-ci.org/#!/lcobucci/jwt)
-[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/lcobucci/jwt/master.svg?style=flat-square)](https://scrutinizer-ci.com/g/lcobucci/jwt/?branch=master)
-[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/lcobucci/jwt/master.svg?style=flat-square)](https://scrutinizer-ci.com/g/lcobucci/jwt/?branch=master)
-
-A simple library to work with JSON Web Token and JSON Web Signature (requires PHP 5.6+).
-The implementation is based on the [RFC 7519](https://tools.ietf.org/html/rfc7519).
-
-## Installation
-
-Package is available on [Packagist](http://packagist.org/packages/lcobucci/jwt),
-you can install it using [Composer](http://getcomposer.org).
-
-```shell
-composer require lcobucci/jwt
-```
-
-### Dependencies
-
-- PHP 5.6+
-- OpenSSL Extension
-
-## Basic usage
-
-### Creating
-
-Just use the builder to create a new JWT/JWS tokens:
-
-```php
-use Lcobucci\JWT\Builder;
-
-$time = time();
-$token = (new Builder())->issuedBy('http://example.com') // Configures the issuer (iss claim)
-                        ->permittedFor('http://example.org') // Configures the audience (aud claim)
-                        ->identifiedBy('4f1g23a12aa', true) // Configures the id (jti claim), replicating as a header item
-                        ->issuedAt($time) // Configures the time that the token was issue (iat claim)
-                        ->canOnlyBeUsedAfter($time + 60) // Configures the time that the token can be used (nbf claim)
-                        ->expiresAt($time + 3600) // Configures the expiration time of the token (exp claim)
-                        ->withClaim('uid', 1) // Configures a new claim, called "uid"
-                        ->getToken(); // Retrieves the generated token
-
-
-$token->getHeaders(); // Retrieves the token headers
-$token->getClaims(); // Retrieves the token claims
-
-echo $token->getHeader('jti'); // will print "4f1g23a12aa"
-echo $token->getClaim('iss'); // will print "http://example.com"
-echo $token->getClaim('uid'); // will print "1"
-echo $token; // The string representation of the object is a JWT string (pretty easy, right?)
-```
-
-### Parsing from strings
-
-Use the parser to create a new token from a JWT string (using the previous token as example):
-
-```php
-use Lcobucci\JWT\Parser;
-
-$token = (new Parser())->parse((string) $token); // Parses from a string
-$token->getHeaders(); // Retrieves the token header
-$token->getClaims(); // Retrieves the token claims
-
-echo $token->getHeader('jti'); // will print "4f1g23a12aa"
-echo $token->getClaim('iss'); // will print "http://example.com"
-echo $token->getClaim('uid'); // will print "1"
-```
-
-### Validating
-
-We can easily validate if the token is valid (using the previous token and time as example):
-
-```php
-use Lcobucci\JWT\ValidationData;
-
-$data = new ValidationData(); // It will use the current time to validate (iat, nbf and exp)
-$data->setIssuer('http://example.com');
-$data->setAudience('http://example.org');
-$data->setId('4f1g23a12aa');
-
-var_dump($token->validate($data)); // false, because token cannot be used before now() + 60
-
-$data->setCurrentTime($time + 61); // changing the validation time to future
-
-var_dump($token->validate($data)); // true, because current time is between "nbf" and "exp" claims
-
-$data->setCurrentTime($time + 4000); // changing the validation time to future
-
-var_dump($token->validate($data)); // false, because token is expired since current time is greater than exp
-
-// We can also use the $leeway parameter to deal with clock skew (see notes below)
-// If token's claimed time is invalid but the difference between that and the validation time is less than $leeway, 
-// then token is still considered valid
-$dataWithLeeway = new ValidationData($time, 20); 
-$dataWithLeeway->setIssuer('http://example.com');
-$dataWithLeeway->setAudience('http://example.org');
-$dataWithLeeway->setId('4f1g23a12aa');
-
-var_dump($token->validate($dataWithLeeway)); // false, because token can't be used before now() + 60, not within leeway
-
-$dataWithLeeway->setCurrentTime($time + 51); // changing the validation time to future
-
-var_dump($token->validate($dataWithLeeway)); // true, because current time plus leeway is between "nbf" and "exp" claims
-
-$dataWithLeeway->setCurrentTime($time + 3610); // changing the validation time to future but within leeway
-
-var_dump($token->validate($dataWithLeeway)); // true, because current time - 20 seconds leeway is less than exp
-
-$dataWithLeeway->setCurrentTime($time + 4000); // changing the validation time to future outside of leeway
-
-var_dump($token->validate($dataWithLeeway)); // false, because token is expired since current time is greater than exp
-```
-
-#### Important
-
-- You have to configure ```ValidationData``` informing all claims you want to validate the token.
-- If ```ValidationData``` contains claims that are not being used in token or token has claims that are not
-configured in ```ValidationData``` they will be ignored by ```Token::validate()```.
-- ```exp```, ```nbf``` and ```iat``` claims are configured by default in ```ValidationData::__construct()```
-with the current UNIX time (```time()```).
-- The optional ```$leeway``` parameter of ```ValidationData``` will cause us to use that number of seconds of leeway 
-when validating the time-based claims, pretending we are further in the future for the "Issued At" (```iat```) and "Not 
-Before" (```nbf```) claims and pretending we are further in the past for the "Expiration Time" (```exp```) claim. This
-allows for situations where the clock of the issuing server has a different time than the clock of the verifying server, 
-as mentioned in [section 4.1 of RFC 7519](https://tools.ietf.org/html/rfc7519#section-4.1).
-
-## Token signature
-
-We can use signatures to be able to verify if the token was not modified after its generation. This library implements Hmac, RSA and ECDSA signatures (using 256, 384 and 512).
-
-### Important
-
-Do not allow the string sent to the Parser to dictate which signature algorithm
-to use, or else your application will be vulnerable to a [critical JWT security vulnerability](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries).
-
-The examples below are safe because the choice in `Signer` is hard-coded and
-cannot be influenced by malicious users.
-
-### Hmac
-
-Hmac signatures are really simple to be used:
-
-```php
-use Lcobucci\JWT\Builder;
-use Lcobucci\JWT\Signer\Key;
-use Lcobucci\JWT\Signer\Hmac\Sha256;
-
-$signer = new Sha256();
-$time = time();
-
-$token = (new Builder())->issuedBy('http://example.com') // Configures the issuer (iss claim)
-                        ->permittedFor('http://example.org') // Configures the audience (aud claim)
-                        ->identifiedBy('4f1g23a12aa', true) // Configures the id (jti claim), replicating as a header item
-                        ->issuedAt($time) // Configures the time that the token was issue (iat claim)
-                        ->canOnlyBeUsedAfter($time + 60) // Configures the time that the token can be used (nbf claim)
-                        ->expiresAt($time + 3600) // Configures the expiration time of the token (exp claim)
-                        ->withClaim('uid', 1) // Configures a new claim, called "uid"
-                        ->getToken($signer, new Key('testing')); // Retrieves the generated token
-
-
-var_dump($token->verify($signer, 'testing 1')); // false, because the key is different
-var_dump($token->verify($signer, 'testing')); // true, because the key is the same
-```
-
-### RSA and ECDSA
-
-RSA and ECDSA signatures are based on public and private keys so you have to generate using the private key and verify using the public key:
-
-```php
-use Lcobucci\JWT\Builder;
-use Lcobucci\JWT\Signer\Key;
-use Lcobucci\JWT\Signer\Rsa\Sha256; // you can use Lcobucci\JWT\Signer\Ecdsa\Sha256 if you're using ECDSA keys
-
-$signer = new Sha256();
-$privateKey = new Key('file://{path to your private key}');
-$time = time();
-
-$token = (new Builder())->issuedBy('http://example.com') // Configures the issuer (iss claim)
-                        ->permittedFor('http://example.org') // Configures the audience (aud claim)
-                        ->identifiedBy('4f1g23a12aa', true) // Configures the id (jti claim), replicating as a header item
-                        ->issuedAt($time) // Configures the time that the token was issue (iat claim)
-                        ->canOnlyBeUsedAfter($time + 60) // Configures the time that the token can be used (nbf claim)
-                        ->expiresAt($time + 3600) // Configures the expiration time of the token (exp claim)
-                        ->withClaim('uid', 1) // Configures a new claim, called "uid"
-                        ->getToken($signer,  $privateKey); // Retrieves the generated token
-
-$publicKey = new Key('file://{path to your public key}');
-
-var_dump($token->verify($signer, $publicKey)); // true when the public key was generated by the private one =)
-```
-
-**It's important to say that if you're using RSA keys you shouldn't invoke ECDSA signers (and vice-versa), otherwise ```sign()``` and ```verify()``` will raise an exception!**

+ 33 - 22
api/vendor/lcobucci/jwt/composer.json

@@ -1,14 +1,7 @@
 {
 {
     "name": "lcobucci/jwt",
     "name": "lcobucci/jwt",
-    "description": "A simple library to work with JSON Web Token and JSON Web Signature",
     "type": "library",
     "type": "library",
-    "authors": [
-        {
-            "name": "Luís Otávio Cobucci Oblonczyk",
-            "email": "lcobucci@gmail.com",
-            "role": "Developer"
-        }
-    ],
+    "description": "A simple library to work with JSON Web Token and JSON Web Signature",
     "keywords": [
     "keywords": [
         "JWT",
         "JWT",
         "JWS"
         "JWS"
@@ -16,17 +9,38 @@
     "license": [
     "license": [
         "BSD-3-Clause"
         "BSD-3-Clause"
     ],
     ],
+    "authors": [
+        {
+            "name": "Luís Cobucci",
+            "email": "lcobucci@gmail.com",
+            "role": "Developer"
+        }
+    ],
     "require": {
     "require": {
-        "php": "^5.6 || ^7.0",
+        "php": "^7.4 || ^8.0",
+        "ext-hash": "*",
+        "ext-json": "*",
         "ext-mbstring": "*",
         "ext-mbstring": "*",
-        "ext-openssl": "*"
+        "ext-openssl": "*",
+        "ext-sodium": "*",
+        "lcobucci/clock": "^2.0"
     },
     },
     "require-dev": {
     "require-dev": {
-        "phpunit/phpunit": "^5.7 || ^7.3",
-        "squizlabs/php_codesniffer": "~2.3",
-        "phpmd/phpmd": "~2.2",
-        "phpunit/php-invoker": "~1.1",
-        "mikey179/vfsstream": "~1.5"
+        "infection/infection": "^0.21",
+        "lcobucci/coding-standard": "^6.0",
+        "mikey179/vfsstream": "^1.6.7",
+        "phpbench/phpbench": "^1.0",
+        "phpstan/extension-installer": "^1.0",
+        "phpstan/phpstan": "^0.12",
+        "phpstan/phpstan-deprecation-rules": "^0.12",
+        "phpstan/phpstan-phpunit": "^0.12",
+        "phpstan/phpstan-strict-rules": "^0.12",
+        "phpunit/php-invoker": "^3.1",
+        "phpunit/phpunit": "^9.5"
+    },
+    "config": {
+        "preferred-install": "dist",
+        "sort-packages": true
     },
     },
     "autoload": {
     "autoload": {
         "psr-4": {
         "psr-4": {
@@ -36,14 +50,11 @@
     "autoload-dev": {
     "autoload-dev": {
         "psr-4": {
         "psr-4": {
             "Lcobucci\\JWT\\": [
             "Lcobucci\\JWT\\": [
+                "test/_keys",
                 "test/unit",
                 "test/unit",
-                "test/functional"
-            ]
-        }
-    },
-    "extra": {
-        "branch-alias": {
-            "dev-master": "3.1-dev"
+                "test/performance"
+            ],
+            "Lcobucci\\JWT\\FunctionalTests\\": "test/functional"
         }
         }
     }
     }
 }
 }

Некоторые файлы не были показаны из-за большого количества измененных файлов