Преглед изворни кода

Merge pull request #1844 from causefx/v2-develop

V2 develop
causefx пре 4 година
родитељ
комит
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
 DemoTest.txt
 Dev.txt
+updateInProgress.txt
 config/cacert.pem
 config/custom.pem
 config/config.php

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

@@ -1,12 +1,17 @@
 <?php
 
+use Monolog\Handler\SlackWebhookHandler;
 use Nekonomokochan\PhpJsonLogger\Logger;
 use Nekonomokochan\PhpJsonLogger\LoggerBuilder;
 
 class OrganizrLogger extends LoggerBuilder
 {
 	public $isReady;
-	
+	/**
+	 * @var SlackWEbhookHandler
+	 */
+	private $slackWebhookHandler;
+
 	/**
 	 * @return boolean
 	 */
@@ -14,7 +19,7 @@ class OrganizrLogger extends LoggerBuilder
 	{
 		return $this->isReady;
 	}
-	
+
 	/**
 	 * @param boolean $readyStatus
 	 */
@@ -22,7 +27,7 @@ class OrganizrLogger extends LoggerBuilder
 	{
 		$this->isReady = $readyStatus;
 	}
-	
+
 	public function build(): Logger
 	{
 		if (!$this->isReady) {
@@ -32,4 +37,20 @@ class OrganizrLogger extends LoggerBuilder
 		}
 		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
-	public $version = '2.1.1890';
+	public $version = '2.1.2320';
 	// ===================================
 	// Quick php Version check
-	public $minimumPHP = '7.3';
+	public $minimumPHP = '7.4';
 	// ===================================
 	protected $db;
 	protected $otherDb;
@@ -83,10 +83,7 @@ class Organizr
 	public $commit;
 	public $fileHash;
 	public $cookieName;
-	public $log;
-	public $logger;
-	public $organizrLog;
-	public $organizrLoginLog;
+	public $logFile;
 	public $timeExecution;
 	public $root;
 	public $paths;
@@ -94,66 +91,72 @@ class Organizr
 	public $groupOptions;
 	public $warnings;
 	public $errors;
+	public bool $loggerSetup = false;
+	public \Nekonomokochan\PhpJsonLogger\Logger $logger;
 
 	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?
 		$this->updating = $updating;
-		// Set Project Root directory
+		// Set Project Root directory and paths
 		$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
-		$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
 		$this->docker = (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Docker.txt'));
 		// Set variable if install is for develop and set php Error levels
 		$this->dev = (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Dev.txt'));
-		$this->phpErrors();
 		// Set variable if install is for demo
 		$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;
-		// Set variable to be used as hash for files
 		$this->fileHash = ($this->commit) ?? $this->version;
 		$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
 		$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
-		$this->log = $this->setOrganizrLog();
+		$this->logFile = $this->setOrganizrLog();
 		$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
 		$this->connectDB();
 		// Check DB Writable
 		$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
 		$this->setCurrentUser();
 		// might just run this at index
@@ -169,6 +172,13 @@ class Organizr
 		$this->disconnectDB();
 	}
 
+	public function addDatabaseToPaths()
+	{
+		if ($this->hasConfig()) {
+			$this->paths = array_merge($this->paths, ['DB Folder' => $this->config['dbLocation']]);
+		}
+	}
+
 	public function chooseConfigFile()
 	{
 
@@ -302,7 +312,7 @@ class Organizr
 	public function setCurrentUser($validate = true)
 	{
 		$user = false;
-		if ($this->hasDB()) {
+		if ($this->hasDatabase()) {
 			if ($this->hasCookie()) {
 				$user = $this->getUserFromToken($_COOKIE[$this->cookieName]);
 			}
@@ -335,16 +345,33 @@ class Organizr
 	public function checkForOrganizrOAuth()
 	{
 		// 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()
 	{
-		if ($this->hasDB()) {
+		if ($this->hasConfig()) {
 			$currentIP = $this->userIP();
 			if ($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)
@@ -833,9 +860,21 @@ class Organizr
 		//$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)
@@ -1116,6 +1155,14 @@ class Organizr
 		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 = '')
 	{
 		$files = '';
@@ -1156,7 +1203,8 @@ class Organizr
 							}
 							if ($pluginEnabled || $settings) {
 								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':
 					foreach ($iteratorIterator as $info) {
 						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;
@@ -1437,8 +1487,7 @@ class Organizr
 		} else {
 			$status['action'] = 'launch';
 			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;
 			}
 		}
@@ -1569,14 +1618,14 @@ class Organizr
 		return $guest;
 	}
 
-	public function getAllUserTokens($id)
+	public function getAllUserTokens($id, $includeAllFields = true)
 	{
-
+		$select = $includeAllFields ? '*' : 'token, ip, id, expires, created';
 		$response = [
 			array(
 				'function' => 'fetchAll',
 				'query' => array(
-					'SELECT * FROM `tokens` WHERE user_id = ? AND expires > ?',
+					'SELECT ' . $select . ' FROM `tokens` WHERE user_id = ? AND expires > ?',
 					[$id],
 					[$this->currentTime]
 				)
@@ -1633,7 +1682,8 @@ class Organizr
 		if ($validated == true) {
 			$allTokens = $this->getAllUserTokens($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) {
 				$this->setLoggerChannel('Authentication');
 				$this->logger->debug('Token failed check against all token listings', $allTokens);
@@ -1643,6 +1693,21 @@ class Organizr
 				}
 				return false;
 			} 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) {
 					$this->setResponse(200, 'Token is valid');
 				}
@@ -1684,7 +1749,7 @@ class Organizr
 		$validated = (bool)$userInfo;
 		if ($validated == true) {
 			$user = $this->getUserById($userInfo['userID']);
-			$allTokens = $this->getAllUserTokens($userInfo['userID']);
+			$allTokens = $this->getAllUserTokens($userInfo['userID'], false);
 			return array(
 				'token' => $token,
 				'tokenDate' => $userInfo['tokenDate'],
@@ -1843,7 +1908,7 @@ class Organizr
 	public function getUserLevel()
 	{
 		// Grab token
-		$requesterToken = $this->getallheaders()['Token'] ?? ($_GET['apikey'] ?? false);
+		$requesterToken = $this->getallheadersi()['token'] ?? ($_GET['apikey'] ?? false);
 		$apiKey = ($this->config['organizrAPI']) ?? null;
 		// Check token or API key
 		// 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()
 	{
 		$allIconsPrep = array();
@@ -1924,7 +2001,11 @@ class Organizr
 			];
 		}
 		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;
 			}
 		}
@@ -2317,6 +2398,8 @@ class Organizr
 				$this->settingsOption('switch', 'lockoutSystem', ['label' => 'Inactivity Lock']),
 				$this->settingsOption('select', 'lockoutMinAuth', ['label' => 'Lockout Groups From', '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('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' => '{}']),
@@ -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('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('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' => [
 				$this->settingsOption('cron-file'),
 				$this->settingsOption('blank'),
 				$this->settingsOption('enable', 'autoUpdateCronEnabled', ['label' => 'Auto-Update Organizr']),
 				$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' => [
 				$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('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', '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);
 			return false;
 		}
-		$newItem = array();
+		$newItems = [];
+		$updatedItems = [];
 		foreach ($array as $k => $v) {
 			$v = $v ?? '';
 			switch ($v) {
@@ -2699,14 +2795,16 @@ class Organizr
 					break;
 			}
 			if (strtolower($k) !== 'formkey') {
-				$newItem[$k] = $v;
+				if ($this->config[$k] !== $v) {
+					$updatedItems[$k] = $v;
+				}
+				$newItems[$k] = $v;
 				$this->config[$k] = $v;
 			}
 		}
 		$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)
@@ -3202,6 +3300,8 @@ class Organizr
 					`default`	INTEGER,
 					`enabled`	INTEGER,
 					`group_id`	INTEGER,
+					`group_id_max` INTEGER DEFAULT \'0\',
+					`add_to_admin`	INTEGER DEFAULT \'0\',
 					`image`	TEXT,
 					`type`	INTEGER,
 					`splash`	INTEGER,
@@ -3447,30 +3547,39 @@ class Organizr
 		$days = ($days > 365) ? 365 : $days;
 		//Quick get user ID
 		$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
 		$addToken = [
-			'token' => (string)$jwttoken,
+			'token' => $token->toString(),
 			'user_id' => $result['id'],
 			'created' => gmdate('Y-m-d H:i:s'),
 			'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');
 		} else {
 			$this->logger->warning('Token creation error');
 		}
 		$this->logger->debug('Token creation function has finished');
-		return $jwttoken;
+		return $token->toString();
 	}
 
 	public function login($array)
@@ -3526,9 +3635,9 @@ class Organizr
 		}
 		// Check if Auth Proxy is enabled
 		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);
 				$this->setLoggerChannel('Authentication', $headerForLogin);
 				$this->logger->debug('Starting Auth Proxy verification');
@@ -3953,11 +4062,19 @@ class Organizr
 			array(
 				'function' => 'fetchAll',
 				'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'
 			),
+			array(
+				'function' => 'fetchAll',
+				'query' => array(
+					'SELECT * FROM tabs WHERE `add_to_admin` = 1 AND `enabled` = 1 ORDER BY `order` ' . $sort
+				),
+				'key' => 'tabs-admin'
+			),
 			array(
 				'function' => 'fetchAll',
 				'query' => array(
@@ -3968,15 +4085,40 @@ class Organizr
 		];
 		$queries = $this->processQueries($response);
 		$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'] = $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'];
 		}
 		$count = array_map(function ($element) {
 			return $element['category_id'];
-		}, $queries['tabs']);
+		}, $all['tabs']);
 		$count = (array_count_values($count));
 		foreach ($queries['categories'] as $k => $v) {
 			$v['count'] = $count[$v['category_id']] ?? 0;
@@ -4000,9 +4142,15 @@ class Organizr
 	public function refreshList()
 	{
 		$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;
 		}, ARRAY_FILTER_USE_KEY);
+		foreach ($list as $item => $value) {
+			if (!is_numeric($value)) {
+				unset($list[$item]);
+			}
+		}
+		return $list;
 	}
 
 	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)
 	{
-		$requesterToken = $this->getallheaders()['Token'] ?? ($_GET['apikey'] ?? false);
+		$requesterToken = $this->getallheadersi()['token'] ?? ($_GET['apikey'] ?? false);
 		$apiKey = ($this->config['organizrAPI']) ?? null;
 		if (isset($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 {
 			$formKey = false;
 		}
@@ -5013,6 +5135,9 @@ class Organizr
 				$this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409);
 				return false;
 			}
+			if (!$this->qualifyLength($array['name'], 50, true)) {
+				return false;
+			}
 		} else {
 			$this->setAPIResponse('error', 'Tab name was not supplied', 422);
 			return false;
@@ -5024,6 +5149,8 @@ class Organizr
 		if (!array_key_exists('image', $array)) {
 			$this->setAPIResponse('error', 'Tab image was not supplied', 422);
 			return false;
+		} else {
+			$array['image'] = $this->sanitizeUserString($array['image']);
 		}
 		$response = [
 			array(
@@ -5063,12 +5190,32 @@ class Organizr
 				$this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409);
 				return false;
 			}
+			if (!$this->qualifyLength($array['name'], 50, true)) {
+				return false;
+			}
 		}
 		if (array_key_exists('default', $array)) {
 			if ($array['default']) {
 				$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 = [
 			array(
 				'function' => 'query',
@@ -5135,6 +5282,9 @@ class Organizr
 				$this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409);
 				return false;
 			}
+			if (!$this->qualifyLength($array['category'], 50, true)) {
+				return false;
+			}
 		} else {
 			$this->setAPIResponse('error', 'Category name was not supplied', 422);
 			return false;
@@ -5183,6 +5333,9 @@ class Organizr
 				$this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409);
 				return false;
 			}
+			if (!$this->qualifyLength($array['category'], 50, true)) {
+				return false;
+			}
 		}
 		if (array_key_exists('image', $array)) {
 			$array['image'] = $this->sanitizeUserString($array['image']);
@@ -6374,6 +6527,9 @@ class Organizr
 				$this->setAPIResponse('error', 'Username: ' . $array['username'] . ' is already taken', 409);
 				return false;
 			}
+			if (!$this->qualifyLength($array['username'], 50, true)) {
+				return false;
+			}
 		}
 		if (array_key_exists('email', $array)) {
 			if ($array['email'] == '') {
@@ -6390,6 +6546,9 @@ class Organizr
 				$this->setAPIResponse('error', 'Email: ' . $array['email'] . ' is already taken', 409);
 				return false;
 			}
+			if (!$this->qualifyLength($array['email'], 50, true)) {
+				return false;
+			}
 		}
 		if (array_key_exists('group_id', $array)) {
 			if ($array['group_id'] == '') {
@@ -6509,6 +6668,15 @@ class Organizr
 			$this->setResponse(409, 'Email is not a valid email', ['email' => $email]);
 			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');
 		if ($this->createUser($username, $password, $email)) {
 			$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);
 			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();
 		$userInfo = [
 			'username' => $username,
@@ -6601,12 +6778,16 @@ class Organizr
 				$this->setAPIResponse('error', 'Group name: ' . $array['group'] . ' is already taken', 409);
 				return false;
 			}
+			if (!$this->qualifyLength($array['group'], 50, true)) {
+				return false;
+			}
 		}
 		if (array_key_exists('image', $array)) {
 			if ($array['image'] == '') {
 				$this->setAPIResponse('error', 'Image was set but empty', 409);
 				return false;
 			}
+			$array['image'] = $this->sanitizeUserString($array['image']);
 		}
 		if (array_key_exists('default', $array)) {
 			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);
 				return false;
 			}
+			if (!$this->qualifyLength($array['group'], 50, true)) {
+				return false;
+			}
 		} else {
 			$this->setAPIResponse('error', 'Group name was not supplied', 422);
 			return false;
@@ -6690,6 +6874,7 @@ class Organizr
 				$this->setAPIResponse('error', 'Group image cannot be empty', 422);
 				return false;
 			}
+			$array['image'] = $this->sanitizeUserString($array['image']);
 		} else {
 			$this->setAPIResponse('error', 'Group image was not supplied', 422);
 			return false;
@@ -7377,13 +7562,18 @@ class Organizr
 						);
 					}
 				}
-				$this->setAPIResponse('success', null, 200, $items);
+				$this->setResponse(200, null, $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) {
-			$this->setLoggerChannel('Plex Connection');
-			$this->logger->error($e);
+			$this->setLoggerChannel('Plex Connection')->error($e);
 			$this->setResponse(500, $e->getMessage());
+			return false;
 		}
 	}
 
@@ -7401,7 +7591,11 @@ class Organizr
 		$iconListing = json_decode($allIcons, true);
 		foreach ($iconListing as $setKey => $set) {
 			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;
 				}
 			}

+ 3 - 2
api/composer.json

@@ -1,7 +1,7 @@
 {
   "require": {
     "dibi/dibi": "^4.2",
-    "lcobucci/jwt": "3.3.1",
+    "lcobucci/jwt": "^4.1",
     "composer/semver": "^1.4",
     "phpmailer/phpmailer": "^6.2",
     "rmccue/requests": "^1.7",
@@ -21,6 +21,7 @@
     "bcremer/line-reader": "^1.1",
     "peppeocchi/php-cron-scheduler": "^4.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",
         "This file is @generated automatically"
     ],
-    "content-hash": "3f2f3b854484a2014e64d2327a1b8c29",
+    "content-hash": "ca4f53441305ffa3e09c532006fa9c19",
     "packages": [
         {
             "name": "adldap2/adldap2",
@@ -170,6 +170,66 @@
             },
             "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",
             "version": "1.7.2",
@@ -1018,38 +1078,104 @@
             "description": "PHP Sonarr API Wrapper",
             "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",
-            "version": "3.3.1",
+            "version": "4.1.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/lcobucci/jwt.git",
-                "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18"
+                "reference": "fe2d89f2eaa7087af4aa166c6f480ef04e000582"
             },
             "dist": {
                 "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": ""
             },
             "require": {
+                "ext-hash": "*",
+                "ext-json": "*",
                 "ext-mbstring": "*",
                 "ext-openssl": "*",
-                "php": "^5.6 || ^7.0"
+                "ext-sodium": "*",
+                "lcobucci/clock": "^2.0",
+                "php": "^7.4 || ^8.0"
             },
             "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",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.1-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Lcobucci\\JWT\\": "src"
@@ -1061,7 +1187,7 @@
             ],
             "authors": [
                 {
-                    "name": "Luís Otávio Cobucci Oblonczyk",
+                    "name": "Luís Cobucci",
                     "email": "lcobucci@gmail.com",
                     "role": "Developer"
                 }
@@ -1073,9 +1199,19 @@
             ],
             "support": {
                 "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",
@@ -1310,7 +1446,7 @@
             "require": {
                 "monolog/monolog": "^1.24",
                 "php": "~7.1",
-                "ramsey/uuid": "^3.8"
+                "ramsey/uuid": "^4.2"
             },
             "require-dev": {
                 "friendsofphp/php-cs-fixer": "^2.14",
@@ -2541,88 +2677,162 @@
             },
             "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",
-            "version": "3.9.6",
+            "version": "4.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/ramsey/uuid.git",
-                "reference": "ffa80ab953edd85d5b6c004f96181a538aad35a3"
+                "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df"
             },
             "dist": {
                 "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": ""
             },
             "require": {
+                "brick/math": "^0.8 || ^0.9",
                 "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": {
                 "rhumsaa/uuid": "self.version"
             },
             "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",
-                "nikic/php-parser": "<=4.5.0",
                 "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",
-                "yoast/phpunit-polyfills": "^1.0"
+                "vimeo/psalm": "^4.9"
             },
             "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",
-                "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."
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.x-dev"
+                    "dev-main": "4.x-dev"
+                },
+                "captainhook": {
+                    "force-install": true
                 }
             },
             "autoload": {
-                "psr-4": {
-                    "Ramsey\\Uuid\\": "src/"
-                },
                 "files": [
                     "src/functions.php"
-                ]
+                ],
+                "psr-4": {
+                    "Ramsey\\Uuid\\": "src/"
+                }
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
                 "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": [
                 "guid",
                 "identifier",
@@ -2630,9 +2840,7 @@
             ],
             "support": {
                 "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": [
                 {
@@ -2644,7 +2852,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-09-25T23:07:42+00:00"
+            "time": "2021-09-25T23:10:38+00:00"
         },
         {
             "name": "rmccue/requests",
@@ -3493,6 +3701,85 @@
             ],
             "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",
             "version": "v1.9.0",

+ 11 - 1
api/config/default.php

@@ -87,6 +87,7 @@ return [
 	'komgaURL' => '',
 	'komgaFallbackUser' => '',
 	'komgaFallbackPassword' => '',
+	'komgaSSOMasterPassword' => '',
 	'sonarrURL' => '',
 	'sonarrUnmonitored' => false,
 	'sonarrIcon' => true,
@@ -659,5 +660,14 @@ return [
 	'checkForThemeUpdate' => true,
 	'autoUpdateCronEnabled' => false,
 	'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
-// Set UTC timeone
+// Set UTC timezone
 date_default_timezone_set("UTC");
 // Autoload frameworks
 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
-$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);
 	$iteratorIterator = new RecursiveIteratorIterator($directoryIterator);
 	foreach ($iteratorIterator as $info) {
@@ -43,4 +34,21 @@ if (file_exists(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_S
 			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);
 		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);
 		return $this->processQueries($response);
 	}

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

@@ -51,7 +51,7 @@ trait AuthFunctions
 				$provider = $ad->connect();
 			} catch (\Adldap\Auth\BindException $e) {
 				$detailedError = $e->getDetailedError();
-				$this->writeLog('error', 'LDAP Function - Error: ' . $detailedError->getErrorMessage(), 'SYSTEM');
+				$this->setLoggerChannel('LDAP')->error($e);
 				$this->setAPIResponse('error', $detailedError->getErrorMessage(), 409);
 				return $detailedError->getErrorMessage();
 				// There was an issue binding / connecting to the server.
@@ -69,7 +69,7 @@ trait AuthFunctions
 			return false;
 		}
 	}
-	
+
 	public function testConnectionLdapLogin($array)
 	{
 		$username = $array['username'] ?? null;
@@ -142,19 +142,19 @@ trait AuthFunctions
 				}
 			} catch (\Adldap\Auth\BindException $e) {
 				$detailedError = $e->getDetailedError();
-				$this->writeLog('error', 'LDAP Function - Error: ' . $detailedError->getErrorMessage(), $username);
+				$this->setLoggerChannel('LDAP')->error($e);
 				$this->setAPIResponse('error', $detailedError->getErrorMessage(), 500);
 				return $detailedError->getErrorMessage();
 				// There was an issue binding / connecting to the server.
 			} catch (Adldap\Auth\UsernameRequiredException $e) {
 				$detailedError = $e->getDetailedError();
-				$this->writeLog('error', 'LDAP Function - Error: ' . $detailedError->getErrorMessage(), $username);
+				$this->setLoggerChannel('LDAP')->error($e);
 				$this->setAPIResponse('error', $detailedError->getErrorMessage(), 422);
 				return $detailedError->getErrorMessage();
 				// The user didn't supply a username.
 			} catch (Adldap\Auth\PasswordRequiredException $e) {
 				$detailedError = $e->getDetailedError();
-				$this->writeLog('error', 'LDAP Function - Error: ' . $detailedError->getErrorMessage(), $username);
+				$this->setLoggerChannel('LDAP')->error($e);
 				$this->setAPIResponse('error', $detailedError->getErrorMessage(), 422);
 				return $detailedError->getErrorMessage();
 				// The user didn't supply a password.
@@ -164,7 +164,7 @@ trait AuthFunctions
 			return false;
 		}
 	}
-	
+
 	public function checkPlexToken($token = '')
 	{
 		try {
@@ -183,11 +183,11 @@ trait AuthFunctions
 				return false;
 			}
 		} catch (Requests_Exception $e) {
-			$this->writeLog('success', 'Plex Token Check Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Plex')->error($e);
 		}
 		return false;
 	}
-	
+
 	public function checkPlexUser($username)
 	{
 		try {
@@ -204,7 +204,7 @@ trait AuthFunctions
 						$usernameLower = strtolower($username);
 						foreach ($userXML as $child) {
 							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;
 								if ($this->config['plexStrictFriends']) {
 									foreach ($child->Server as $server) {
@@ -216,10 +216,10 @@ trait AuthFunctions
 									$machineMatches = true;
 								}
 								if ($machineMatches) {
-									$this->writeLog('success', 'Plex User Check - User Approved for Login', $username);
+									$this->setLoggerChannel('Plex')->info('User Approved for Login');
 									return true;
 								} 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;
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Plex User Check Function - Error: ' . $e->getMessage(), $username);
+			$this->setLoggerChannel('Plex')->error($e);
 		}
 		return false;
 	}
-	
+
 	public function plugin_auth_plex($username, $password)
 	{
 		try {
@@ -267,11 +267,11 @@ trait AuthFunctions
 			}
 			return false;
 		} catch (Requests_Exception $e) {
-			$this->writeLog('success', 'Plex Auth Function - Error: ' . $e->getMessage(), $username);
+			$this->setLoggerChannel('Plex')->error($e);
 		}
 		return false;
 	}
-	
+
 	// Pass credentials to LDAP backend
 	public function plugin_auth_ldap($username, $password)
 	{
@@ -341,25 +341,25 @@ trait AuthFunctions
 					return false;
 				}
 			} 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.
 			} 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.
 			} 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.
 			}
 		}
 		return false;
 	}
-	
+
 	// Ldap Auth Missing Dependency
 	public function plugin_auth_ldap_disabled()
 	{
 		return 'LDAP - Disabled (Dependency: php-ldap missing!)';
 	}
-	
+
 	// Pass credentials to FTP backend
 	public function plugin_auth_ftp($username, $password)
 	{
@@ -391,7 +391,7 @@ trait AuthFunctions
 			return false;
 		}
 	}
-	
+
 	// Pass credentials to Emby Backend
 	public function plugin_auth_emby_local($username, $password)
 	{
@@ -424,11 +424,11 @@ trait AuthFunctions
 			}
 			return false;
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Emby Local Auth Function - Error: ' . $e->getMessage(), $username);
+			$this->setLoggerChannel('Emby')->error($e);
 		}
 		return false;
 	}
-	
+
 	// Pass credentials to JellyFin Backend
 	public function plugin_auth_jellyfin($username, $password)
 	{
@@ -446,7 +446,7 @@ trait AuthFunctions
 			if ($response->success) {
 				$json = json_decode($response->body, 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
 					$headers = array(
 						'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;
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'JellyFin Auth Function - Error: ' . $e->getMessage(), $username);
+			$this->setLoggerChannel('JellyFin')->error($e);
 		}
 		return false;
 	}
-	
+
 	// Authenticate against emby connect
 	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 (strtolower($value['ConnectUserName']) == strtolower($username) || strtolower($value['Name']) == strtolower($username)) {
 								$connectUserName = $value['ConnectUserName'];
-								$this->writeLog('success', 'Emby Connect Auth Function - Found User', $username);
+								$this->setLoggerChannel('Emby')->info('Found User');
 								break;
 							}
 						}
 					}
 					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';
 						$headers = array(
 							'Accept' => 'application/json',
@@ -508,21 +508,21 @@ trait AuthFunctions
 									//'image' => $json['User']['ImageUrl'],
 								);
 							} else {
-								$this->writeLog('error', 'Emby Connect Auth Function - Bad Response', $username);
+								$this->setLoggerChannel('Emby')->warning('Bad Response');
 							}
 						} else {
-							$this->writeLog('error', 'Emby Connect Auth Function - 401 From Emby Connect', $username);
+							$this->setLoggerChannel('Emby')->warning('401 From Emby Connect');
 						}
 					}
 				}
 			}
 			return false;
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Emby Connect Auth Function - Error: ' . $e->getMessage(), $username);
+			$this->setLoggerChannel('Emby')->error($e);
 			return false;
 		}
 	}
-	
+
 	// Authenticate Against Emby Local (first) and Emby Connect
 	public function plugin_auth_emby_all($username, $password)
 	{
@@ -536,5 +536,5 @@ trait AuthFunctions
 			return $this->plugin_auth_emby_connect($username, $password);
 		}
 	}
-	
+
 }

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

@@ -22,7 +22,7 @@ trait BackupFunctions
 		$filename = $path . $filename;
 		if ($ext == 'zip') {
 			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);
 				return (unlink($filename));
 			} else {
@@ -63,12 +63,13 @@ trait BackupFunctions
 				break;
 			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';
 		$zip = new ZipArchive;
 		$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;
 		$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($rootPath), RecursiveIteratorIterator::LEAVES_ONLY);
 
@@ -87,8 +88,28 @@ trait BackupFunctions
 
 
 		$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->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;
 	}
 

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

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

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

@@ -159,8 +159,8 @@ trait LogFunctions
 
 	public function getLatestLogFile()
 	{
-		if ($this->log) {
-			if (isset($this->log)) {
+		if ($this->logFile) {
+			if (isset($this->logFile)) {
 				$folder = $this->logLocation();
 				$directoryIterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS);
 				$iteratorIterator = new RecursiveIteratorIterator($directoryIterator);
@@ -185,8 +185,8 @@ trait LogFunctions
 
 	public function getLogFiles()
 	{
-		if ($this->log) {
-			if (isset($this->log)) {
+		if ($this->logFile) {
+			if (isset($this->logFile)) {
 				$folder = $this->logLocation();
 				$directoryIterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS);
 				$iteratorIterator = new RecursiveIteratorIterator($directoryIterator);
@@ -207,49 +207,49 @@ trait LogFunctions
 		return false;
 	}
 
+	public function log(...$params)
+	{
+		// Alias of setLoggerChannel
+		return $this->setLoggerChannel(...$params);
+	}
+
 	public function setLoggerChannel($channel = 'Organizr', $username = null)
 	{
+
 		if ($this->hasDB()) {
+			$channel = $channel ?: 'Organizr';
 			$setLogger = false;
 			if ($username) {
 				$username = $this->sanitizeUserString($username);
 			}
-			if ($this->logger) {
+			if ($this->loggerSetup) {
 				if ($channel) {
 					if (strtolower($this->logger->getChannel()) !== strtolower($channel)) {
+						$this->logger->setChannel($channel);
 						$setLogger = true;
 					}
 				}
 				if ($username) {
 					$currentUsername = $this->logger->getTraceId() !== '' ? strtolower($this->logger->getTraceId()) : '';
 					if ($currentUsername !== strtolower($username)) {
+						$this->logger->setUsername($username);
 						$setLogger = true;
 					}
 				}
+				if ($setLogger) {
+					return $this->setupLogger($channel, $username);
+				} else {
+					return $this->logger;
+				}
 			} else {
-				$setLogger = true;
-			}
-			if ($setLogger) {
-				$channel = $channel ?: 'Organizr';
 				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':
 				$logLevel = Nekonomokochan\PhpJsonLogger\LoggerBuilder::DEBUG;
 				break;
@@ -275,40 +275,67 @@ trait LogFunctions
 				$logLevel = Nekonomokochan\PhpJsonLogger\LoggerBuilder::WARNING;
 				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 {
+			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->loggerSetup = true;
 			return $this->logger;
 		} catch (Exception $e) {
 			// nothing so far
-			$this->logger = null;
 			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):
-		$this->setLoggerChannel('Plex Homepage');
 		normal log:
-		$this->logger->info('test');
+		$this->log('Plex Homepage')->info('test');
 		normal log with context ($context must be an array):
-		$this->logger->info('test', $context);
+		$this->log('Plex Homepage')->info('test', $context);
 		exception:
-		$this->logger->critical($exception, $context);
+		$this->log('Plex Homepage')->critical($exception, $context);
 		*/
 	}
 
 	public function tempLogIfNeeded()
 	{
-		if (!$this->log) {
+		if (!$this->logFile) {
 			return $this->root . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR . 'organizr-' . $this->randString() . '.log';
 		} else {
-			return $this->log;
+			return $this->logFile;
 		}
 	}
 
 	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 == 'all' || $number == 'combined-logs') {
 						$log = 'combined-logs';
@@ -337,7 +364,7 @@ trait LogFunctions
 	{
 		$this->setLoggerChannel('Logger');
 		$this->logger->debug('Starting log purge function');
-		if ($this->log) {
+		if ($this->logFile) {
 			$this->logger->debug('Checking if log id exists');
 			if ($number !== 0) {
 				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>';
 		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)
 	{
 		$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
@@ -503,10 +508,16 @@ trait NormalFunctions
 
 	public function convertIPToRange($ip)
 	{
+		$ip = trim($ip);
 		if (strpos($ip, '/') !== false) {
 			$explodeIP = explode('/', $ip);
 			$prefix = $explodeIP[1];
 			$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);
 			$start_ip_long = long2ip(ip2long($start_ip));
 			$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()
 	{
-		$mainArray = array(
-			array(
+		$mainArray = [
+			[
 				'from' => '10.0.0.0',
 				'to' => '10.255.255.255'
-			),
-			array(
+			],
+			[
 				'from' => '172.16.0.0',
 				'to' => '172.31.255.255'
-			),
-			array(
+			],
+			[
 				'from' => '192.168.0.0',
 				'to' => '192.168.255.255'
-			),
-			array(
+			],
+			[
 				'from' => '127.0.0.1',
 				'to' => '127.255.255.255'
-			),
-		);
+			],
+		];
 		if (isset($this->config['localIPList'])) {
 			if ($this->config['localIPList'] !== '') {
 				$ipListing = explode(',', $this->config['localIPList']);

+ 1 - 1
api/functions/oauth.php

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

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

@@ -458,7 +458,7 @@ trait OptionsFunction
 				break;
 			case 'calendarlinkurl':
 				$settingMerge = [
-					'type' => 'select',
+					'type' => 'select-input',
 					'label' => 'Target URL',
 					'help' => 'Set the primary URL used when clicking on calendar icon.',
 					'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';
 				Requests::Post($url, $headers, json_encode($data), array());
 			} catch (Requests_Exception $e) {
-				$this->writeLog('error', 'Emby Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Emby')->error($e);
 				$this->setResponse(500, $e->getMessage());
 				return false;
 			}
 			$this->setAPIResponse('success', 'User has joined Emby', 200);
 			return true;
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Emby create Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Emby')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			return false;
 		}
@@ -175,8 +175,6 @@ trait OrganizrFunctions
 		$response = Requests::Get($url, $headers, array());
 		$response = $response->body;
 		$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 = null;
 		foreach ($response as $element) {
@@ -184,14 +182,12 @@ trait OrganizrFunctions
 				$correct = $element;
 			}
 		}
-		$this->writeLog('error', 'Correct user:' . json_encode($correct), 'SYSTEM');
 		if ($correct == null) {
 			//return empty JSON if user incorrectly configured template
 			return "{}";
 		}
 		//select policy section and remove possibly dangerous rows.
 		$policy = $correct['Policy'];
-		//writeLog('error', 'policy update'.$policy, 'SYSTEM');
 		unset($policy['AuthenticationProviderId']);
 		unset($policy['InvalidLoginAttemptCount']);
 		unset($policy['DisablePremiumFeatures']);
@@ -754,12 +750,12 @@ trait OrganizrFunctions
 				$options = $this->requestOptions($url, 60000, true, false);
 				$response = Requests::post($url . '/api/v1/users/logout', ['X-Auth-Token' => $_COOKIE['komga_token']], $options);
 				if ($response->success) {
-					$this->writeLog('success', 'Komga Token Function - Logged User out', 'SYSTEM');
+					$this->setLoggerChannel('Komga')->info('Logged User out');
 				} 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) {
-				$this->writeLog('error', 'Komga Token Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Komga')->error($e);
 			}
 			$this->coookie('delete', 'komga_token');
 		}
@@ -821,7 +817,7 @@ trait OrganizrFunctions
 				$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)
@@ -832,9 +828,9 @@ trait OrganizrFunctions
 			'<!DOCTYPE html>
 			<html lang="en">
 			<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 name="description" content="Trakt OAuth">
+				<meta name="description" content="' . $title . '">
 				<meta name="viewport" content="width=device-width, initial-scale=1.0">
 				<title>' . $title . '</title>
 			</head>
@@ -847,11 +843,11 @@ trait OrganizrFunctions
 			<body ' . $close . '>
 				<main>
 					<section>
-						<aside>
+						<div>
 							<h3>' . $title . '</h3>
 							<p>' . $notice . '</p>
 							' . $closeMessage . '
-						</aside>
+						</div>
 					</section>
 				</main>
 			</body>
@@ -885,4 +881,22 @@ trait OrganizrFunctions
 	{
 		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)
 	{
 		$token = null;
+		$useMaster = false;
 		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)));
 			$url = $this->qualifyURL($this->config['komgaURL']);
 			$options = $this->requestOptions($url, 60000, true, false, $credentials);
 			$response = Requests::get($url . '/api/v1/users/me', ['X-Auth-Token' => 'organizrSSO'], $options);
 			if ($response->success) {
 				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'];
 				} else {
-					$this->writeLog('error', 'Komga Token Function - Komga did not return Token', $email);
+					$this->setLoggerChannel('Komga')->warning('Komga did not return Token');
 				}
 			} else {
 				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 {
-					$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) {
-			$this->writeLog('error', 'Komga Token Function - Error: ' . $e->getMessage(), $email);
+			$this->setLoggerChannel('Komga')->error($e);
 		}
 		if ($token) {
 			return $token;
@@ -170,16 +183,16 @@ trait SSOFunctions
 			$response = Requests::post($url . $endpoint, $headers, json_encode($data), $options);
 			if ($response->success) {
 				$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'];
 				$jellyfin[$key] = json_encode($token['User']);
 				$jellyfin['jellyfin_credentials'] = '{"Servers":[{"ManualAddress":"' . $ssoUrl . '","Id":"' . $token['ServerId'] . '","UserId":"' . $token['User']['Id'] . '","AccessToken":"' . $token['AccessToken'] . '"}]}';
 				return $jellyfin;
 			} 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) {
-			$this->writeLog('error', 'Jellyfin Token Function - Error: ' . $e->getMessage(), $username);
+			$this->setLoggerChannel('Jellyfin')->error($e);
 		}
 		return false;
 	}
@@ -205,16 +218,16 @@ trait SSOFunctions
 			$response = Requests::post($url . $endpoint, $headers, json_encode($data), $options);
 			if ($response->success) {
 				$token = json_decode($response->body, true)['access_token'];
-				$this->writeLog('success', 'Ombi Token Function - Grabbed token.', $username);
+				$this->setLoggerChannel('Ombi')->info('Grabbed token');
 			} else {
 				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 {
-					$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) {
-			$this->writeLog('error', 'Ombi Token Function - Error: ' . $e->getMessage(), $username);
+			$this->setLoggerChannel('Ombi')->error($e);
 		}
 		if ($token) {
 			return $token;
@@ -253,12 +266,12 @@ trait SSOFunctions
 						$token[$key]['token'] = json_decode($response->body, true)['token'];
 						$token[$key]['uuid'] = json_decode($response->body, true)['uuid'];
 						$token[$key]['path'] = $path;
-						$this->writeLog('success', 'Tautulli Token Function - Grabbed token from: ' . $url, $username);
+						$this->setLoggerChannel('Tautulli')->info('Grabbed token from: ' . $url);
 					} else {
-						$this->writeLog('error', 'Tautulli Token Function - Error on URL: ' . $url, $username);
+						$this->setLoggerChannel('Tautulli')->warning('Error on URL: ' . $url);
 					}
 				} 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) {
 				$user = json_decode($response->body, true); // not really needed yet
 				$token = $response->cookies['connect.sid']->value;
-				$this->writeLog('success', 'Overseerr Token Function - Grabbed token', $user['plexUsername'] ?? $email);
+				$this->setLoggerChannel('Overseerr')->info('Grabbed token');
 			} else {
 				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 {
-					$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) {
-			$this->writeLog('error', 'Overseerr Token Function - Error: ' . $e->getMessage(), $email);
+			$this->setLoggerChannel('Overseerr')->error($e);
 		}
 		if ($token) {
 			return urldecode($token);
@@ -329,16 +342,16 @@ trait SSOFunctions
 			if ($response->success) {
 				$user = json_decode($response->body, true)['user'];
 				$token = json_decode($response->body, true)['token'];
-				$this->writeLog('success', 'Petio Token Function - Grabbed token', $user['username']);
+				$this->setLoggerChannel('Petio')->info('Grabbed token');
 			} else {
 				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 {
-					$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) {
-			$this->writeLog('error', 'Petio Token Function - Error: ' . $e->getMessage(), $username);
+			$this->setLoggerChannel('Petio')->error($e);
 		}
 		if ($token) {
 			return $token;

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

@@ -2,52 +2,54 @@
 
 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 {
-			$result = array();
-			$result['valid'] = false;
+			$result = [];
 			// Check Token with JWT
 			// Set key
 			if (!isset($this->config['organizrHash'])) {
 				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;
 			}
-		} 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;
 		}
 	}

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

@@ -12,13 +12,40 @@ trait UpdateFunctions
 			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()
 	{
 		if (!$this->docker) {
 			$this->setResponse(409, 'Your install type is not Docker');
 			return false;
 		}
+		if ($this->hasUpdateStatusFile()) {
+			$this->setResponse(500, 'Already Update in progress');
+			return false;
+		} else {
+			$this->createUpdateStatusFile();
+		}
 		$dockerUpdate = null;
 		ini_set('max_execution_time', 0);
 		set_time_limit(0);
@@ -29,6 +56,7 @@ trait UpdateFunctions
 		} elseif (file_exists('./40-install')) {
 			$dockerUpdate = shell_exec('./40-install');
 		}
+		$this->removeUpdateStatusFile();
 		if ($dockerUpdate) {
 			$this->setAPIResponse('success', $dockerUpdate, 200);
 			return true;
@@ -37,19 +65,26 @@ trait UpdateFunctions
 			return false;
 		}
 	}
-	
+
 	public function windowsUpdate()
 	{
 		if ($this->docker || $this->getOS() !== 'win') {
 			$this->setResponse(409, 'Your install type is not Windows');
 			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';
 		ini_set('max_execution_time', 0);
 		set_time_limit(0);
 		$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';
 		$windowsUpdate = shell_exec($windowsScript);
+		$this->removeUpdateStatusFile();
 		if ($windowsUpdate) {
 			$this->setAPIResponse('success', $windowsUpdate, 200);
 			return true;
@@ -58,19 +93,26 @@ trait UpdateFunctions
 			return false;
 		}
 	}
-	
+
 	public function linuxUpdate()
 	{
 		if ($this->docker || $this->getOS() == 'win') {
 			$this->setResponse(409, 'Your install type is not Linux');
 			return false;
 		}
+		if ($this->hasUpdateStatusFile()) {
+			$this->setResponse(500, 'Already Update in progress');
+			return false;
+		} else {
+			$this->createUpdateStatusFile();
+		}
 		$branch = $this->config['branch'];
 		ini_set('max_execution_time', 0);
 		set_time_limit(0);
 		$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';
 		$update = shell_exec($script);
+		$this->removeUpdateStatusFile();
 		if ($update) {
 			$this->setAPIResponse('success', $update, 200);
 			return true;
@@ -79,7 +121,7 @@ trait UpdateFunctions
 			return false;
 		}
 	}
-	
+
 	public function upgradeInstall($branch = 'v2-master', $stage = '1')
 	{
 		// may kill this function in place for php script to run elsewhere
@@ -102,29 +144,29 @@ trait UpdateFunctions
 			$destination = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR;
 			switch ($stage) {
 				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)) {
-						$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);
 						return true;
 					} 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);
 						return false;
 					}
 				case '2':
 					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);
 						return true;
 					} 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);
 						return false;
 					}
 				case '3':
 					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';
 						if (!file_exists($updateComplete)) {
 							touch($updateComplete);
@@ -132,18 +174,18 @@ trait UpdateFunctions
 						$this->setAPIResponse('success', 'Files replaced successfully', 200);
 						return true;
 					} 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);
 						return false;
 					}
 				case '4':
 					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);
 						return true;
 					} 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);
 						return false;
 					}
@@ -155,6 +197,5 @@ trait UpdateFunctions
 			$this->setAPIResponse('error', 'File permissions not set correctly', 500);
 			return false;
 		}
-		
 	}
 }

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

@@ -88,6 +88,14 @@ trait UpgradeFunctions
 				$this->upgradeToVersion($versionCheck);
 			}
 			// 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) {
 				//return 'Upgraded Needed - Current Version '.$oldVer.' - New Version: '.$versionCheck;
 				// Upgrade database to latest version
@@ -96,8 +104,7 @@ trait UpgradeFunctions
 			// Update config.php version if different to the installed version
 			if ($updateSuccess && $this->version !== $this->config['configVersion']) {
 				$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) {
 				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')
 	{
 		if ($table == '' || $columnName == '' || $definition == '') {
@@ -329,26 +386,81 @@ trait UpgradeFunctions
 		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')
 	{
+		$this->setLoggerChannel('Upgrade')->notice('Starting upgrade to version ' . $version);
 		switch ($version) {
 			case '2.1.0':
 				$this->upgradeSettingsTabURL();
 				$this->upgradeHomepageTabURL();
+				break;
 			case '2.1.400':
 				$this->removeOldPluginDirectoriesAndFiles();
+				break;
 			case '2.1.525':
 				$this->removeOldCustomHTML();
+				break;
 			case '2.1.860':
 				$this->upgradeInstalledPluginsConfigItem();
+				break;
 			case '2.1.1500':
 				$this->upgradeDataToFolder();
+				break;
 			case '2.1.1860':
 				$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()
@@ -614,4 +726,40 @@ trait UpgradeFunctions
 		}
 		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);
 				$calendar = $this->formatCouchCalendar($downloader->getMediaList(array('status' => 'active,done')), $key);
 			} catch (Exception $e) {
-				$this->writeLog('error', 'Radarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setLoggerChannel('Radarr')->error($e);
 			}
 			if (!empty($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);
 			return true;
 		} catch (Exception $e) {
-			$this->writeLog('error', 'NZBGet Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Deluge')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			return false;
 		}
@@ -144,12 +144,12 @@ trait DelugeHomepageItem
 			}
 			$api['content']['queueItems'] = (empty($api['content']['queueItems'])) ? [] : $api['content']['queueItems'];
 			$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());
 			return false;
 		}
-		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$api['content'] = $api['content'] ?? false;
 		$this->setAPIResponse('success', null, 200, $api);
 		return $api;
 	}

+ 3 - 3
api/homepage/emby.php

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

+ 5 - 5
api/homepage/healthchecks.php

@@ -38,7 +38,7 @@ trait HealthChecksHomepageItem
 		];
 		return array_merge($homepageInformation, $homepageSettings);
 	}
-	
+
 	public function healthChecksHomepagePermissions($key = null)
 	{
 		$permissions = [
@@ -57,7 +57,7 @@ trait HealthChecksHomepageItem
 		];
 		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
-	
+
 	public function homepageOrderhealthchecks()
 	{
 		if ($this->homepageItemPermissions($this->healthChecksHomepagePermissions('main'))) {
@@ -73,7 +73,7 @@ trait HealthChecksHomepageItem
 				';
 		}
 	}
-	
+
 	public function getHealthChecks($tags = null)
 	{
 		if (!$this->homepageItemPermissions($this->healthChecksHomepagePermissions('main'), true)) {
@@ -93,7 +93,7 @@ trait HealthChecksHomepageItem
 					$api['content']['checks'] = array_merge($api['content']['checks'], $healthResults['checks']);
 				}
 			} 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) {
@@ -107,7 +107,7 @@ trait HealthChecksHomepageItem
 		$this->setAPIResponse('success', null, 200, $api);
 		return $api;
 	}
-	
+
 	public function healthChecksTags($tags)
 	{
 		$return = '?tag=';

+ 1 - 1
api/homepage/jackett.php

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

+ 3 - 3
api/homepage/jdownloader.php

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

+ 3 - 3
api/homepage/jellyfin.php

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

+ 4 - 4
api/homepage/lidarr.php

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

+ 1 - 1
api/homepage/monitorr.php

@@ -160,7 +160,7 @@ trait MonitorrHomepageItem
 				];
 			}
 		} 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);
 			return false;
 		};

+ 25 - 26
api/homepage/netdata.php

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

+ 2 - 2
api/homepage/nzbget.php

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

+ 1 - 1
api/homepage/octoprint.php

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

+ 5 - 5
api/homepage/ombi.php

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

+ 5 - 5
api/homepage/overseerr.php

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

+ 2 - 2
api/homepage/pihole.php

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

+ 4 - 4
api/homepage/qbittorrent.php

@@ -91,12 +91,12 @@ trait QBitTorrentHomepageItem
 					return true;
 				}
 			} 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);
 				return false;
 			}
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'qBittorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('qBittorrent')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			return false;
 		}
@@ -189,12 +189,12 @@ trait QBitTorrentHomepageItem
 					return $api;
 				}
 			} 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);
 				return false;
 			}
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'qBittorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('qBittorrent')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			return false;
 		}

+ 2 - 2
api/homepage/radarr.php

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

+ 2 - 2
api/homepage/rtorrent.php

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

+ 2 - 2
api/homepage/sickrage.php

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

+ 3 - 3
api/homepage/sonarr.php

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

+ 1 - 1
api/homepage/speedtest.php

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

+ 57 - 57
api/homepage/tautulli.php

@@ -157,6 +157,18 @@ trait TautulliHomepageItem
 		$width = $this->getCacheImageSize('w');
 		$nowPlayingHeight = $this->getCacheImageSize('nph');
 		$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 {
 			$homestatsUrl = $apiURL . '&cmd=get_home_stats&grouping=1';
 			$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'];
 					$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']);
 				}
+				$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) {
 			$this->logger->critical($e, [$url]);
 			$this->setResponse(500, $e->getMessage());
 			return false;
-		};
-		$api = isset($api) ? $api : false;
+		}
+		$api = $api ?? false;
 		$this->setAPIResponse('success', null, 200, $api);
 		return $api;
 	}
@@ -270,7 +270,7 @@ trait TautulliHomepageItem
 		$apiURL = $url . '/api/v2?apikey=' . $this->config['tautulliApikey'];
 		if (!empty($this->config['tautulliApikey']) && !empty($this->config['tautulliURL'])) {
 			$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 {
 				$liblist = Requests::get($liblistUrl, [], $options);
 				$libraryList = array();
@@ -283,7 +283,7 @@ trait TautulliHomepageItem
 					return $libraryList;
 				}
 			} 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;
 			}
 		}

+ 2 - 2
api/homepage/trakt.php

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

+ 4 - 4
api/homepage/transmission.php

@@ -79,12 +79,12 @@ trait TransmissionHomepageItem
 					return false;
 				}
 			} 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);
 				return false;
 			}
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Transmission Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Transmission')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			return false;
 		}
@@ -179,12 +179,12 @@ trait TransmissionHomepageItem
 					$api['content']['historyItems'] = false;
 				}
 			} 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);
 				return false;
 			}
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Transmission Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('Transmission')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			return false;
 		};

+ 5 - 5
api/homepage/unifi.php

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

+ 4 - 4
api/homepage/utorrent.php

@@ -78,7 +78,7 @@ trait uTorrentHomepageItem
 
 			$response = $this->getuTorrentToken();
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'uTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('uTorrent')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			return false;
 		}
@@ -134,7 +134,7 @@ trait uTorrentHomepageItem
 				$this->updateConfigItems($uTorrentConfig);
 			}
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'uTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('uTorrent')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			return false;
 		}
@@ -163,7 +163,7 @@ trait uTorrentHomepageItem
 			$response = Requests::get($url, $headers, $options);
 			$httpResponse = $response->status_code;
 			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();
 				$response = Requests::get($url, $headers, $options);
 				$httpResponse = $response->status_code;
@@ -223,7 +223,7 @@ trait uTorrentHomepageItem
 				return $api;
 			}
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'uTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('uTorrent')->error($e);
 			$this->setResponse(500, $e->getMessage());
 			return false;
 		}

+ 3 - 2
api/homepage/weather.php

@@ -111,7 +111,8 @@ trait WeatherHomepageItem
 		try {
 			if ($this->config['homepageWeatherAndAirWeatherEnabled']) {
 				$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) {
 					$apiData = json_decode($response->body, true);
 					$api['content']['weather'] = ($apiData['error'] === null) ? $apiData : false;
@@ -137,7 +138,7 @@ trait WeatherHomepageItem
 				}
 			}
 		} 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());
 			return false;
 		};

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

@@ -67,7 +67,6 @@ function get_page_settings_tab_editor_tabs($Organizr)
 	return '
 	<script>
 	buildTabEditor();
-	
 	' . $iconSelectors . '
 	</script>
 	<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="70" class="text-center"></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">ACTIVE</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">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">DELETE</th>
 						</tr>
 					</thead>
 					<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>
 				</table>
 			</form>

+ 12 - 0
api/pages/wizard.php

@@ -130,12 +130,24 @@ function get_page_wizard($Organizr)
                 return true;
             },
             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();
                 organizrAPI2(\'POST\',\'api/v2/wizard\',post).success(function(data) {
             		var html = data.response;
+            		message("Wizard Data accepted");
+            		$(\'.white-box\').unblock({});
                     location.reload();
             	}).fail(function(xhr) {
             	    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
 class Bookmark extends Organizr
 {
-	public function writeLog($type = 'error', $message = null, $username = null)
-	{
-		parent::writeLog($type, "Plugin 'Bookmark': " . $message, $username);
-	}
-
 	public function _bookmarkGetOrganizrTabInfo()
 	{
 		$response = [
@@ -549,7 +544,8 @@ class Bookmark extends Organizr
 		];
 		$tabInfo = $this->_getBookmarkTabById($id);
 		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);
 			return $this->processQueries($response);
 		} else {
@@ -575,6 +571,9 @@ class Bookmark extends Organizr
 				$this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409);
 				return false;
 			}
+			if (!$this->qualifyLength($array['name'], 50, true)) {
+				return false;
+			}
 		} else {
 			$this->setAPIResponse('error', 'Tab name was not supplied', 422);
 			return false;
@@ -619,7 +618,7 @@ class Bookmark extends Organizr
 			),
 		];
 		$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);
 	}
 
@@ -646,6 +645,9 @@ class Bookmark extends Organizr
 				$this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409);
 				return false;
 			}
+			if (!$this->qualifyLength($array['name'], 50, true)) {
+				return false;
+			}
 		}
 		if (array_key_exists('background_color', $array)) {
 			$array['background_color'] = $this->sanitizeUserString($array['background_color']);
@@ -676,7 +678,7 @@ class Bookmark extends Organizr
 			),
 		];
 		$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);
 	}
 
@@ -887,6 +889,9 @@ class Bookmark extends Organizr
 				$this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409);
 				return false;
 			}
+			if (!$this->qualifyLength($array['category'], 50, true)) {
+				return false;
+			}
 		} else {
 			$this->setAPIResponse('error', 'Category name was not supplied', 422);
 			return false;
@@ -901,7 +906,7 @@ class Bookmark extends Organizr
 			),
 		];
 		$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);
 		$this->_correctDefaultCategory();
 		return $result;
@@ -930,6 +935,9 @@ class Bookmark extends Organizr
 				$this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409);
 				return false;
 			}
+			if (!$this->qualifyLength($array['category'], 50, true)) {
+				return false;
+			}
 		}
 		if (array_key_exists('default', $array)) {
 			if ($array['default']) {
@@ -948,7 +956,7 @@ class Bookmark extends Organizr
 			),
 		];
 		$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);
 		$this->_correctDefaultCategory();
 		return $result;
@@ -1000,7 +1008,7 @@ class Bookmark extends Organizr
 		];
 		$categoryInfo = $this->_getBookmarkCategoryById($id);
 		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);
 			$result = $this->processQueries($response);
 			$this->_correctDefaultCategory();

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

@@ -130,7 +130,7 @@ class HealthChecks extends Organizr
 			)
 		);
 	}
-	
+
 	public function _healthCheckPluginTest($url)
 	{
 		$success = false;
@@ -156,12 +156,12 @@ class HealthChecks extends Organizr
 				}
 			}
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'HealthChecks Plugin - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setLoggerChannel('HealthChecks')->error($e);
 			return false;
 		}
 		return $success;
 	}
-	
+
 	public function _healthCheckSelfHostedURLValidation($url, $checkOnly = false)
 	{
 		$selfHosted = true;
@@ -175,7 +175,7 @@ class HealthChecks extends Organizr
 		}
 		return $checkOnly ? $selfHosted : $url;
 	}
-	
+
 	public function _healthCheckPluginStartUUID($uuid)
 	{
 		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());
 		return Requests::get($url . $uuid . '/start', [], $options);
 	}
-	
+
 	public function _healthCheckPluginUUID($uuid, $pass = false)
 	{
 		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());
 		return Requests::get($url . $uuid . $path, [], $options);
 	}
-	
+
 	public function _healthCheckPluginRun()
 	{
 		$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'])) {
 			$allItems = [];
 			foreach ($this->config['HEALTHCHECKS-all-items'] as $k => $v) {
-				
+
 				if ($k !== false) {
 					foreach ($v as $item) {
 						$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);
 		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']) {
 				$PhpMailer = new PhpMailer();
 				$emailTemplate = array(
@@ -255,7 +255,7 @@ class Invites extends Organizr
 				)
 			];
 			$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']);
 		} else {
 			return false;
@@ -295,7 +295,7 @@ class Invites extends Organizr
 							return $libraryList;
 						}
 					} catch (Requests_Exception $e) {
-						$this->writeLog('error', 'Plex Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+						$this->setLoggerChannel('Plex')->error($e);
 						return false;
 					};
 				}
@@ -526,36 +526,36 @@ class Invites extends Organizr
 								return false;
 						}
 						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);
 							return true;
 						} else {
 							switch ($response->status_code) {
 								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);
 									return false;
 								case 401:
-									$this->writeLog('error', 'Plex Invite Function - Incorrect Token', 'SYSTEM');
+									$this->setLoggerChannel('Plex')->warning('Incorrect Token');
 									$this->setAPIResponse('error', 'Incorrect Token', 409);
 									return false;
 								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);
 									return false;
 								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);
 									return false;
 							}
 						}
 					} 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);
 						return false;
-					};
+					}
 				} 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);
 					return false;
 				}
@@ -566,7 +566,7 @@ class Invites extends Organizr
 					$this->setAPIResponse('success', 'User now has access to system', 200);
 					return true;
 				} 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);
 					return false;
 				}

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

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

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

@@ -959,161 +959,7 @@ $email = '
                                     <tr>
                                     <![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]>
                                         </td>
@@ -1173,17 +1019,7 @@ $email = '
                                                             <!--[if mso]>
 				<td valign="top" width="600" style="width:600px;">
 				<![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]>
 				</td>
 				<![endif]-->

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

@@ -95,25 +95,6 @@ $email = '
 												</td>
 											</tr>
 											' . $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>
 									</table>
 								</td>

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

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

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

@@ -171,21 +171,6 @@ $email = '
 			</tr>
 		</tbody>
 	</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>
 </html>
 ';

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

@@ -177,12 +177,12 @@ class PhpMailer extends Organizr
 			$mail->Subject = $emailTemplate['subject'];
 			$mail->Body = $this->_phpMailerPluginBuildEmail($emailTemplate);
 			$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';
 			$this->setAPIResponse('success', $msg, 200);
 			return true;
 		} 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());
 			return false;
 		}
@@ -242,7 +242,7 @@ class PhpMailer extends Organizr
 			$mail->send();
 			return true;
 		} 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();
 		}
 	}

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

@@ -666,4 +666,26 @@ $app->post('/test/jackett', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->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
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/update/download/{branch}', function ($request, $response, $args) {
 	/**
@@ -51,7 +50,6 @@ $app->get('/update/download/{branch}', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/update/unzip/{branch}', function ($request, $response, $args) {
 	/**
@@ -75,7 +73,6 @@ $app->get('/update/unzip/{branch}', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/update/move/{branch}', function ($request, $response, $args) {
 	/**
@@ -99,7 +96,6 @@ $app->get('/update/move/{branch}', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/update/cleanup/{branch}', function ($request, $response, $args) {
 	/**
@@ -123,7 +119,6 @@ $app->get('/update/cleanup/{branch}', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/update/docker', function ($request, $response, $args) {
 	/**
@@ -147,7 +142,6 @@ $app->get('/update/docker', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/update/windows', function ($request, $response, $args) {
 	/**
@@ -171,7 +165,6 @@ $app->get('/update/windows', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/update/linux', function ($request, $response, $args) {
 	/**
@@ -195,7 +188,6 @@ $app->get('/update/linux', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-	
 });
 $app->get('/update/migrate/{version}', function ($request, $response, $args) {
 	/**
@@ -219,5 +211,27 @@ $app->get('/update/migrate/{version}', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->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
 
+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';
 
 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\\Type' => $vendorDir . '/dibi/dibi/src/Dibi/Type.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',
     'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.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',
     '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/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',
     '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
     '253c157292f75eb38082b5acb06f3f01' => $vendorDir . '/nikic/fast-route/src/functions.php',
     '3109cb1a231dcd04bee1f9f620d46975' => $vendorDir . '/paragonie/sodium_compat/autoload.php',
     'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php',
     'bd9634f2d41831496de0d3dfe4c94881' => $vendorDir . '/symfony/polyfill-php56/bootstrap.php',
-    'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
     'fe62ba7e10580d903cc46d808b5961a4' => $vendorDir . '/tightenco/collect/src/Collect/Support/helpers.php',
     'caf31cc6ec7cf2241cb6f12c226c3846' => $vendorDir . '/tightenco/collect/src/Collect/Support/alias.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'),
     'Tightenco\\Collect\\' => array($vendorDir . '/tightenco/collect/src/Collect'),
     '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\\Php72\\' => array($vendorDir . '/symfony/polyfill-php72'),
     'Symfony\\Polyfill\\Php56\\' => array($vendorDir . '/symfony/polyfill-php56'),
@@ -22,6 +23,7 @@ return array(
     'Slim\\' => array($vendorDir . '/slim/slim/Slim'),
     'Recurr\\' => array($vendorDir . '/simshaun/recurr/src/Recurr'),
     'Ramsey\\Uuid\\' => array($vendorDir . '/ramsey/uuid/src'),
+    'Ramsey\\Collection\\' => array($vendorDir . '/ramsey/collection/src'),
     'Pusher\\' => array($vendorDir . '/pusher/pusher-php-server/src'),
     'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
     'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
@@ -40,6 +42,7 @@ return array(
     'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
     'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-client/src'),
     'Lcobucci\\JWT\\' => array($vendorDir . '/lcobucci/jwt/src'),
+    'Lcobucci\\Clock\\' => array($vendorDir . '/lcobucci/clock/src'),
     'Kryptonit3\\Sonarr\\' => array($vendorDir . '/kryptonit3/sonarr/src'),
     'Kryptonit3\\SickRage\\' => array($vendorDir . '/kryptonit3/sickrage/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'),
     'Cron\\' => array($vendorDir . '/dragonmantank/cron-expression/src/Cron'),
     'Composer\\Semver\\' => array($vendorDir . '/composer/semver/src'),
+    'Brick\\Math\\' => array($vendorDir . '/brick/math/src'),
     'Bogstag\\OAuth2\\Client\\' => array($vendorDir . '/bogstag/oauth2-trakt/src'),
     'Bcremer\\LineReader\\' => array($vendorDir . '/bcremer/line-reader/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'));
 
         require __DIR__ . '/autoload_static.php';
-        \Composer\Autoload\ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb::getInitializer($loader)();
+        call_user_func(\Composer\Autoload\ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb::getInitializer($loader));
 
         $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',
         '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/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',
         '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
         '253c157292f75eb38082b5acb06f3f01' => __DIR__ . '/..' . '/nikic/fast-route/src/functions.php',
         '3109cb1a231dcd04bee1f9f620d46975' => __DIR__ . '/..' . '/paragonie/sodium_compat/autoload.php',
         'e39a8b23c42d4e1452234d762b03835a' => __DIR__ . '/..' . '/ramsey/uuid/src/functions.php',
         'bd9634f2d41831496de0d3dfe4c94881' => __DIR__ . '/..' . '/symfony/polyfill-php56/bootstrap.php',
-        'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
         'fe62ba7e10580d903cc46d808b5961a4' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/helpers.php',
         'caf31cc6ec7cf2241cb6f12c226c3846' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/alias.php',
         '0ccdf99b8f62f02c52cba55802e0c2e7' => __DIR__ . '/..' . '/zircote/swagger-php/src/functions.php',
@@ -38,6 +39,7 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         'S' => 
         array (
             'Symfony\\Polyfill\\Util\\' => 22,
+            'Symfony\\Polyfill\\Php81\\' => 23,
             'Symfony\\Polyfill\\Php80\\' => 23,
             'Symfony\\Polyfill\\Php72\\' => 23,
             'Symfony\\Polyfill\\Php56\\' => 23,
@@ -54,6 +56,7 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         array (
             'Recurr\\' => 7,
             'Ramsey\\Uuid\\' => 12,
+            'Ramsey\\Collection\\' => 18,
         ),
         'P' => 
         array (
@@ -87,6 +90,7 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         array (
             'League\\OAuth2\\Client\\' => 21,
             'Lcobucci\\JWT\\' => 13,
+            'Lcobucci\\Clock\\' => 15,
         ),
         'K' => 
         array (
@@ -128,6 +132,7 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         ),
         'B' => 
         array (
+            'Brick\\Math\\' => 11,
             'Bogstag\\OAuth2\\Client\\' => 22,
             'Bcremer\\LineReader\\' => 19,
         ),
@@ -150,6 +155,10 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-util',
         ),
+        'Symfony\\Polyfill\\Php81\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/symfony/polyfill-php81',
+        ),
         'Symfony\\Polyfill\\Php80\\' => 
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
@@ -202,6 +211,10 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         array (
             0 => __DIR__ . '/..' . '/ramsey/uuid/src',
         ),
+        'Ramsey\\Collection\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/ramsey/collection/src',
+        ),
         'Pusher\\' => 
         array (
             0 => __DIR__ . '/..' . '/pusher/pusher-php-server/src',
@@ -276,6 +289,10 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         array (
             0 => __DIR__ . '/..' . '/lcobucci/jwt/src',
         ),
+        'Lcobucci\\Clock\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/lcobucci/clock/src',
+        ),
         'Kryptonit3\\Sonarr\\' => 
         array (
             0 => __DIR__ . '/..' . '/kryptonit3/sonarr/src',
@@ -344,6 +361,10 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         array (
             0 => __DIR__ . '/..' . '/composer/semver/src',
         ),
+        'Brick\\Math\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/brick/math/src',
+        ),
         'Bogstag\\OAuth2\\Client\\' => 
         array (
             0 => __DIR__ . '/..' . '/bogstag/oauth2-trakt/src',
@@ -445,6 +466,7 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         'Dibi\\Translator' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Translator.php',
         'Dibi\\Type' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Type.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',
         'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.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"
         },
+        {
+            "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",
             "version": "1.7.2",
@@ -1063,40 +1126,109 @@
             "description": "PHP Sonarr API Wrapper",
             "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",
-            "version": "3.3.1",
-            "version_normalized": "3.3.1.0",
+            "version": "4.1.5",
+            "version_normalized": "4.1.5.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/lcobucci/jwt.git",
-                "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18"
+                "reference": "fe2d89f2eaa7087af4aa166c6f480ef04e000582"
             },
             "dist": {
                 "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": ""
             },
             "require": {
+                "ext-hash": "*",
+                "ext-json": "*",
                 "ext-mbstring": "*",
                 "ext-openssl": "*",
-                "php": "^5.6 || ^7.0"
+                "ext-sodium": "*",
+                "lcobucci/clock": "^2.0",
+                "php": "^7.4 || ^8.0"
             },
             "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",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "3.1-dev"
-                }
-            },
             "installation-source": "dist",
             "autoload": {
                 "psr-4": {
@@ -1109,7 +1241,7 @@
             ],
             "authors": [
                 {
-                    "name": "Luís Otávio Cobucci Oblonczyk",
+                    "name": "Luís Cobucci",
                     "email": "lcobucci@gmail.com",
                     "role": "Developer"
                 }
@@ -1121,8 +1253,18 @@
             ],
             "support": {
                 "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"
         },
         {
@@ -2642,91 +2784,168 @@
             "description": "A polyfill for 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",
-            "version": "3.9.6",
-            "version_normalized": "3.9.6.0",
+            "version": "4.2.3",
+            "version_normalized": "4.2.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/ramsey/uuid.git",
-                "reference": "ffa80ab953edd85d5b6c004f96181a538aad35a3"
+                "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df"
             },
             "dist": {
                 "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": ""
             },
             "require": {
+                "brick/math": "^0.8 || ^0.9",
                 "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": {
                 "rhumsaa/uuid": "self.version"
             },
             "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",
-                "nikic/php-parser": "<=4.5.0",
                 "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",
-                "yoast/phpunit-polyfills": "^1.0"
+                "vimeo/psalm": "^4.9"
             },
             "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",
-                "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."
             },
-            "time": "2021-09-25T23:07:42+00:00",
+            "time": "2021-09-25T23:10:38+00:00",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.x-dev"
+                    "dev-main": "4.x-dev"
+                },
+                "captainhook": {
+                    "force-install": true
                 }
             },
             "installation-source": "dist",
             "autoload": {
-                "psr-4": {
-                    "Ramsey\\Uuid\\": "src/"
-                },
                 "files": [
                     "src/functions.php"
-                ]
+                ],
+                "psr-4": {
+                    "Ramsey\\Uuid\\": "src/"
+                }
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
                 "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": [
                 "guid",
                 "identifier",
@@ -2734,9 +2953,7 @@
             ],
             "support": {
                 "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": [
                 {
@@ -3633,6 +3850,88 @@
             ],
             "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",
             "version": "v1.9.0",

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

@@ -5,7 +5,7 @@
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
-        'reference' => '3a141f74828baa2322d2a095494c4ddf646790d9',
+        'reference' => '1ae02fda50382bb86ea50b76579b5e773c4a1ff1',
         'name' => '__root__',
         'dev' => true,
     ),
@@ -16,7 +16,7 @@
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
-            'reference' => '3a141f74828baa2322d2a095494c4ddf646790d9',
+            'reference' => '1ae02fda50382bb86ea50b76579b5e773c4a1ff1',
             'dev_requirement' => false,
         ),
         'adldap2/adldap2' => array(
@@ -46,6 +46,15 @@
             'reference' => 'fbb9253d9e317e84dc2b3f1253afc1dcbb4414a2',
             '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(
             'pretty_version' => '1.7.2',
             'version' => '1.7.2.0',
@@ -178,13 +187,22 @@
             'reference' => 'e30c5c783a837270bcef81571ca9b95909c52e5e',
             '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(
-            'pretty_version' => '3.3.1',
-            'version' => '3.3.1.0',
+            'pretty_version' => '4.1.5',
+            'version' => '4.1.5.0',
             'type' => 'library',
             'install_path' => __DIR__ . '/../lcobucci/jwt',
             'aliases' => array(),
-            'reference' => 'a11ec5f4b4d75d1fcd04e133dede4c317aac9e18',
+            'reference' => 'fe2d89f2eaa7087af4aa166c6f480ef04e000582',
             'dev_requirement' => false,
         ),
         'league/oauth2-client' => array(
@@ -442,19 +460,28 @@
             'reference' => '120b605dfeb996808c31b6477290a714d356e822',
             '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(
-            'pretty_version' => '3.9.6',
-            'version' => '3.9.6.0',
+            'pretty_version' => '4.2.3',
+            'version' => '4.2.3.0',
             'type' => 'library',
             'install_path' => __DIR__ . '/../ramsey/uuid',
             'aliases' => array(),
-            'reference' => 'ffa80ab953edd85d5b6c004f96181a538aad35a3',
+            'reference' => 'fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df',
             'dev_requirement' => false,
         ),
         'rhumsaa/uuid' => array(
             'dev_requirement' => false,
             'replaced' => array(
-                0 => '3.9.6',
+                0 => '4.2.3',
             ),
         ),
         'rmccue/requests' => array(
@@ -565,6 +592,15 @@
             'reference' => 'dc3063ba22c2a1fd2f45ed856374d79114998f91',
             '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(
             'pretty_version' => 'v1.9.0',
             'version' => '1.9.0.0',

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

@@ -4,8 +4,8 @@
 
 $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) {

+ 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.
 
 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",
-    "description": "A simple library to work with JSON Web Token and JSON Web Signature",
     "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": [
         "JWT",
         "JWS"
@@ -16,17 +9,38 @@
     "license": [
         "BSD-3-Clause"
     ],
+    "authors": [
+        {
+            "name": "Luís Cobucci",
+            "email": "lcobucci@gmail.com",
+            "role": "Developer"
+        }
+    ],
     "require": {
-        "php": "^5.6 || ^7.0",
+        "php": "^7.4 || ^8.0",
+        "ext-hash": "*",
+        "ext-json": "*",
         "ext-mbstring": "*",
-        "ext-openssl": "*"
+        "ext-openssl": "*",
+        "ext-sodium": "*",
+        "lcobucci/clock": "^2.0"
     },
     "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": {
         "psr-4": {
@@ -36,14 +50,11 @@
     "autoload-dev": {
         "psr-4": {
             "Lcobucci\\JWT\\": [
+                "test/_keys",
                 "test/unit",
-                "test/functional"
-            ]
-        }
-    },
-    "extra": {
-        "branch-alias": {
-            "dev-master": "3.1-dev"
+                "test/performance"
+            ],
+            "Lcobucci\\JWT\\FunctionalTests\\": "test/functional"
         }
     }
 }

Неке датотеке нису приказане због велике количине промена