Browse Source

Merge pull request #1817 from causefx/v2-develop

V2 develop
causefx 4 years ago
parent
commit
bef6870ff9

+ 120 - 71
api/classes/organizr.class.php

@@ -65,7 +65,7 @@ class Organizr
 
 	// ===================================
 	// Organizr Version
-	public $version = '2.1.1760';
+	public $version = '2.1.1790';
 	// ===================================
 	// Quick php Version check
 	public $minimumPHP = '7.3';
@@ -97,7 +97,9 @@ class Organizr
 
 	public function __construct($updating = false)
 	{
-		// First Check PHP Version
+		// Set custom Error handler
+		set_error_handler([$this, 'setErrorResponse']);
+		// Next Check PHP Version
 		$this->checkPHP();
 		// Check Disk Space
 		$this->checkDiskSpace();
@@ -748,6 +750,34 @@ class Organizr
 		}
 	}
 
+	public function setAPIErrorResponse($number, $message, $file, $line)
+	{
+		$GLOBALS['api']['response']['errors'][] = [
+			'error' => $number,
+			'message' => $message,
+			'file' => $file,
+			'line' => $line
+		];
+		$this->handleError($number, $message, $file, $line);
+	}
+
+	public function setErrorResponse($number, $message, $file, $line)
+	{
+		$error = [
+			'error' => $number,
+			'message' => $message,
+			'file' => $file,
+			'line' => $line
+		];
+		$this->handleError($number, $message, $file, $line);
+		//$this->prettyPrint($error, true);
+	}
+
+	public function handleError($number, $message, $file, $line)
+	{
+		error_log(sprintf('PHP %s:  %s in %s on line %d', $number, $message, $file, $line));
+	}
+
 	public function checkRoute($request)
 	{
 		$route = '/api/v2/' . explode('api/v2/', $request->getUri()->getPath())[1];
@@ -1014,10 +1044,12 @@ class Organizr
 			foreach ($iteratorIterator as $info) {
 				if (stripos($info->getFilename(), '.css') !== false) {
 					$file = preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($info->getFilename()));
-					$themes[] = [
-						'name' => ucwords(str_replace('_', ' ', $file)),
-						'value' => $file,
-					];
+					if (key_exists($file, $userThemesInformation)) {
+						$themes[] = [
+							'name' => ucwords(str_replace('_', ' ', $file)),
+							'value' => $file,
+						];
+					}
 				}
 			}
 		}
@@ -1869,6 +1901,22 @@ class Organizr
 		}
 	}
 
+	public function formatPingHost($host)
+	{
+		$host = $this->qualifyURL($host, true);
+		if ($host['port'] !== '') {
+			$host['port'] = str_replace(':', '', $host['port']);
+		}
+		if ($host['host'] == '' && $host['path'] !== '') {
+			$host['host'] = $host['path'];
+			$host['path'] = '';
+			if (strpos($host['host'], '/') !== false) {
+				$host['host'] = explode('/', $host['host'])[0];
+			}
+		}
+		return $host;
+	}
+
 	public function ping($pings)
 	{
 		if ($this->qualifyRequest($this->config['pingAuth'], true)) {
@@ -1885,14 +1933,12 @@ class Organizr
 				case "array":
 					$results = [];
 					foreach ($pings as $k => $v) {
-						if (strpos($v, ':') !== false) {
-							$domain = explode(':', $v)[0];
-							$port = explode(':', $v)[1];
-							$ping->setHost($domain);
-							$ping->setPort($port);
+						$pingFormatted = $this->formatPingHost($v);
+						$ping->setHost($pingFormatted['host']);
+						if ($pingFormatted['port'] !== '') {
+							$ping->setPort($pingFormatted['port']);
 							$latency = $ping->ping('fsockopen');
 						} else {
-							$ping->setHost($v);
 							$latency = $ping->ping();
 						}
 						if ($latency || $latency === 0) {
@@ -1903,14 +1949,12 @@ class Organizr
 					}
 					break;
 				case "string":
-					if (strpos($pings, ':') !== false) {
-						$domain = explode(':', $pings)[0];
-						$port = explode(':', $pings)[1];
-						$ping->setHost($domain);
-						$ping->setPort($port);
+					$pingFormatted = $this->formatPingHost($pings);
+					$ping->setHost($pingFormatted['host']);
+					if ($pingFormatted['port'] !== '') {
+						$ping->setPort($pingFormatted['port']);
 						$latency = $ping->ping('fsockopen');
 					} else {
-						$ping->setHost($pings);
 						$latency = $ping->ping();
 					}
 					if ($latency || $latency === 0) {
@@ -2176,6 +2220,8 @@ class Organizr
 				$this->settingsOption('input', 'authProxyWhitelist', ['label' => 'Auth Proxy Whitelist', 'placeholder' => 'i.e. 10.0.0.0/24 or 10.0.0.20', 'help' => 'IPv4 only at the moment - This must be set to work, will accept subnet or IP address']),
 				$this->settingsOption('input', 'authProxyHeaderName', ['label' => 'Auth Proxy Header Name', 'placeholder' => 'i.e. X-Forwarded-User', 'help' => 'Please choose a unique value for added security']),
 				$this->settingsOption('input', 'authProxyHeaderNameEmail', ['label' => 'Auth Proxy Header Name for Email', 'placeholder' => 'i.e. X-Forwarded-Email', 'help' => 'Please choose a unique value for added security']),
+				$this->settingsOption('switch', 'authProxyOverrideLogout', ['label' => 'Override Logout', 'help' => 'Enable option to set custom Logout URL for Auth Proxy']),
+				$this->settingsOption('input', 'authProxyLogoutURL', ['label' => 'Logout URL', 'help' => 'Logout URL to redirect user for Auth Proxy']),
 			],
 			'Ping' => [
 				$this->settingsOption('auth', 'pingAuth'),
@@ -3398,13 +3444,13 @@ class Organizr
 						$this->logger->debug('Starting Plex oAuth verification');
 						$tokenInfo = $this->checkPlexToken($oAuth);
 						if ($tokenInfo) {
-							$authSuccess = array(
+							$authSuccess = [
 								'username' => $tokenInfo['user']['username'],
 								'email' => $tokenInfo['user']['email'],
 								'image' => $tokenInfo['user']['thumb'],
 								'token' => $tokenInfo['user']['authToken'],
 								'oauth' => 'plex'
-							);
+							];
 							$this->logger->debug('User\'s Plex Token has been verified');
 							$this->coookie('set', 'oAuth', 'true', $this->config['rememberMeDays']);
 							$authSuccess = ((!empty($this->config['plexAdmin']) && strtolower($this->config['plexAdmin']) == strtolower($tokenInfo['user']['username'])) || (!empty($this->config['plexAdmin']) && strtolower($this->config['plexAdmin']) == strtolower($tokenInfo['user']['email'])) || $this->checkPlexUser($tokenInfo['user']['username'])) ? $authSuccess : false;
@@ -3488,7 +3534,8 @@ class Organizr
 				$createToken = $this->createToken($result['username'], $result['email'], $days);
 				if ($createToken) {
 					$this->logger->info('User has logged in');
-					$this->ssoCheck($result, $password, $token); //need to work on this
+					$ssoUserObject = ($token !== '') ? $token : $authSuccess;
+					$this->ssoCheck($ssoUserObject, $password, $token); //need to work on this
 					return ($output) ? array('name' => $this->cookieName, 'token' => (string)$createToken) : true;
 				} else {
 					$this->setAPIResponse('error', 'Token creation error', 500);
@@ -3857,18 +3904,18 @@ class Organizr
 	public function organizrSpecialSettings()
 	{
 		// js activeInfo
-		return array(
-			'homepage' => array(
+		return [
+			'homepage' => [
 				'refresh' => $this->refreshList(),
 				'order' => $this->homepageOrderList(),
-				'search' => array(
+				'search' => [
 					'enabled' => $this->qualifyRequest($this->config['mediaSearchAuth']) && $this->config['mediaSearch'] == true && $this->config['plexToken'],
 					'type' => $this->config['mediaSearchType'],
-				),
+				],
 				'requests' => [
 					'service' => $this->config['defaultRequestService'],
 				],
-				'ombi' => array(
+				'ombi' => [
 					'enabled' => $this->qualifyRequest($this->config['homepageOmbiAuth']) && $this->qualifyRequest($this->config['homepageOmbiRequestAuth']) && $this->config['homepageOmbiEnabled'] == true && $this->config['ssoOmbi'] && isset($_COOKIE['Auth']),
 					'authView' => $this->qualifyRequest($this->config['homepageOmbiAuth']),
 					'authRequest' => $this->qualifyRequest($this->config['homepageOmbiRequestAuth']),
@@ -3880,8 +3927,8 @@ class Organizr
 					'ombiDefaultFilterApproved' => (bool)$this->config['ombiDefaultFilterApproved'],
 					'ombiDefaultFilterUnapproved' => (bool)$this->config['ombiDefaultFilterUnapproved'],
 					'ombiDefaultFilterDenied' => (bool)$this->config['ombiDefaultFilterDenied']
-				),
-				'overseerr' => array(
+				],
+				'overseerr' => [
 					'enabled' => $this->qualifyRequest($this->config['homepageOverseerrAuth']) && $this->qualifyRequest($this->config['homepageOverseerrRequestAuth']) && $this->config['homepageOverseerrEnabled'] == true && $this->config['ssoOverseerr'] && isset($_COOKIE['connect_sid']),
 					'authView' => $this->qualifyRequest($this->config['homepageOverseerrAuth']),
 					'authRequest' => $this->qualifyRequest($this->config['homepageOverseerrRequestAuth']),
@@ -3893,28 +3940,28 @@ class Organizr
 					'overseerrDefaultFilterApproved' => (bool)$this->config['overseerrDefaultFilterApproved'],
 					'overseerrDefaultFilterUnapproved' => (bool)$this->config['overseerrDefaultFilterUnapproved'],
 					'overseerrDefaultFilterDenied' => (bool)$this->config['overseerrDefaultFilterDenied']
-				),
-				'jackett' => array(
+				],
+				'jackett' => [
 					'homepageJackettBackholeDownload' => $this->config['homepageJackettBackholeDownload'] ? true : false
-				),
-				'options' => array(
+				],
+				'options' => [
 					'alternateHomepageHeaders' => $this->config['alternateHomepageHeaders'],
 					'healthChecksTags' => $this->config['healthChecksTags'],
-					'titles' => array(
+					'titles' => [
 						'tautulli' => $this->config['tautulliHeader']
-					)
-				),
-				'media' => array(
+					]
+				],
+				'media' => [
 					'jellyfin' => $this->config['homepageJellyfinInstead']
-				)
-			),
-			'sso' => array(
-				'misc' => array(
+				]
+			],
+			'sso' => [
+				'misc' => [
 					'oAuthLogin' => isset($_COOKIE['oAuth']),
 					'rememberMe' => $this->config['rememberMe'],
 					'rememberMeDays' => $this->config['rememberMeDays']
-				),
-				'plex' => array(
+				],
+				'plex' => [
 					'enabled' => (bool)$this->config['ssoPlex'],
 					'cookie' => isset($_COOKIE['mpt']),
 					'machineID' => strlen($this->config['plexID']) == 40,
@@ -3923,42 +3970,42 @@ class Organizr
 					'strict' => (bool)$this->config['plexStrictFriends'],
 					'oAuthEnabled' => (bool)$this->config['plexoAuth'],
 					'backend' => $this->config['authBackend'] == 'plex',
-				),
-				'tautulli' => array(
+				],
+				'tautulli' => [
 					'enabled' => (bool)$this->config['ssoTautulli'],
 					'cookie' => !empty($this->tautulliList()),
 					'url' => ($this->config['tautulliURL'] !== '') ? $this->config['tautulliURL'] : false,
-				),
-				'overseerr' => array(
+				],
+				'overseerr' => [
 					'enabled' => (bool)$this->config['ssoOverseerr'],
 					'cookie' => isset($_COOKIE['connect.sid']),
 					'url' => ($this->config['overseerrURL'] !== '') ? $this->config['overseerrURL'] : false,
 					'api' => $this->config['overseerrToken'] !== '',
-				),
-				'petio' => array(
+				],
+				'petio' => [
 					'enabled' => (bool)$this->config['ssoPetio'],
 					'cookie' => isset($_COOKIE['petio_jwt']),
 					'url' => ($this->config['petioURL'] !== '') ? $this->config['petioURL'] : false,
 					'api' => $this->config['petioToken'] !== '',
-				),
-				'ombi' => array(
+				],
+				'ombi' => [
 					'enabled' => (bool)$this->config['ssoOmbi'],
 					'cookie' => isset($_COOKIE['Auth']),
 					'url' => ($this->config['ombiURL'] !== '') ? $this->config['ombiURL'] : false,
 					'api' => $this->config['ombiToken'] !== '',
-				),
-				'jellyfin' => array(
+				],
+				'jellyfin' => [
 					'enabled' => (bool)$this->config['ssoJellyfin'],
 					'url' => ($this->config['jellyfinURL'] !== '') ? $this->config['jellyfinURL'] : false,
 					'ssoUrl' => ($this->config['jellyfinSSOURL'] !== '') ? $this->config['jellyfinSSOURL'] : false,
-				),
+				],
 				'komga' => [
 					'enabled' => (bool)$this->config['ssoKomga'],
 					'cookie' => isset($_COOKIE['komga_token']),
 					'url' => ($this->config['komgaURL'] !== '') ? $this->config['komgaURL'] : false,
 				]
-			),
-			'ping' => array(
+			],
+			'ping' => [
 				'onlineSound' => $this->config['pingOnlineSound'],
 				'offlineSound' => $this->config['pingOfflineSound'],
 				'statusSounds' => $this->config['statusSounds'],
@@ -3968,34 +4015,34 @@ class Organizr
 				'ms' => $this->config['pingMs'],
 				'adminRefresh' => $this->config['adminPingRefresh'],
 				'everyoneRefresh' => $this->config['otherPingRefresh'],
-			),
-			'notifications' => array(
+			],
+			'notifications' => [
 				'backbone' => $this->config['notificationBackbone'],
 				'position' => $this->config['notificationPosition']
-			),
-			'lockout' => array(
+			],
+			'lockout' => [
 				'enabled' => $this->config['lockoutSystem'],
 				'timer' => $this->config['lockoutTimeout'],
 				'minGroup' => $this->config['lockoutMinAuth'],
 				'maxGroup' => $this->config['lockoutMaxAuth']
-			),
-			'user' => array(
+			],
+			'user' => [
 				'agent' => isset($_SERVER ['HTTP_USER_AGENT']) ? $_SERVER ['HTTP_USER_AGENT'] : null,
 				'oAuthLogin' => isset($_COOKIE['oAuth']),
 				'local' => $this->isLocal(),
 				'ip' => $this->userIP()
-			),
-			'login' => array(
+			],
+			'login' => [
 				'rememberMe' => $this->config['rememberMe'],
 				'rememberMeDays' => $this->config['rememberMeDays'],
 				'wanDomain' => $this->config['wanDomain'],
 				'localAddress' => $this->config['localAddress'],
 				'enableLocalAddressForward' => $this->config['enableLocalAddressForward'],
-			),
-			'misc' => array(
+			],
+			'misc' => [
 				'installedPlugins' => $this->qualifyRequest(1) ? $this->config['installedPlugins'] : '',
 				'installedThemes' => $this->qualifyRequest(1) ? $this->config['installedThemes'] : '',
-				'return' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : false,
+				'return' => $_SERVER['HTTP_REFERER'] ?? false,
 				'authDebug' => $this->config['authDebug'],
 				'minimalLoginScreen' => $this->config['minimalLoginScreen'],
 				'unsortedTabs' => $this->config['unsortedTabs'],
@@ -4013,16 +4060,18 @@ class Organizr
 				'autoCollapseCategories' => $this->config['autoCollapseCategories'],
 				'autoExpandNavBar' => $this->config['autoExpandNavBar'],
 				'sideMenuCollapsed' => $this->config['allowCollapsableSideMenu'] && $this->config['sideMenuCollapsed'],
-				'collapseSideMenuOnClick' => $this->config['allowCollapsableSideMenu'] && $this->config['collapseSideMenuOnClick']
-			),
-			'menuLink' => array(
+				'collapseSideMenuOnClick' => $this->config['allowCollapsableSideMenu'] && $this->config['collapseSideMenuOnClick'],
+				'authProxyOverrideLogout' => $this->config['authProxyOverrideLogout'],
+				'authProxyLogoutURL' => $this->config['authProxyLogoutURL'],
+			],
+			'menuLink' => [
 				'githubMenuLink' => $this->config['githubMenuLink'],
 				'organizrSupportMenuLink' => $this->config['organizrSupportMenuLink'],
 				'organizrDocsMenuLink' => $this->config['organizrDocsMenuLink'],
 				'organizrSignoutMenuLink' => $this->config['organizrSignoutMenuLink'],
 				'organizrFeatureRequestLink' => $this->config['organizrFeatureRequestLink']
-			)
-		);
+			]
+		];
 	}
 
 	public function checkLog($path)

+ 2 - 0
api/config/default.php

@@ -472,6 +472,8 @@ return [
 	'authProxyHeaderName' => '',
 	'authProxyHeaderNameEmail' => '',
 	'authProxyWhitelist' => '',
+	'authProxyOverrideLogout' => false,
+	'authProxyLogoutURL' => '',
 	'ignoreTFALocal' => false,
 	'unifiURL' => '',
 	'unifiUsername' => '',

+ 11 - 2
api/functions/normal-functions.php

@@ -88,10 +88,19 @@ trait NormalFunctions
 	}
 
 	// Print output all purrty
-	public function prettyPrint($v)
+	public function prettyPrint($v, $error = false)
 	{
+		if ($error) {
+			$background = 'red';
+			$border = 'black';
+			$text = 'white';
+		} else {
+			$background = '#f2f2f2';
+			$border = 'black';
+			$text = 'black';
+		}
 		$trace = debug_backtrace()[0];
-		echo '<pre style="white-space: pre; text-overflow: ellipsis; overflow: hidden; background-color: #f2f2f2; border: 2px solid black; border-radius: 5px; padding: 5px; margin: 5px;">' . $trace['file'] . ':' . $trace['line'] . ' ' . gettype($v) . "\n\n" . print_r($v, 1) . '</pre><br/>';
+		echo '<pre style="white-space: pre; text-overflow: ellipsis; overflow: hidden; color: ' . $text . '; background-color: ' . $background . '; border: 2px solid ' . $border . '; border-radius: 5px; padding: 5px; margin: 5px;">' . $trace['file'] . ':' . $trace['line'] . ' ' . gettype($v) . "\n\n" . print_r($v, 1) . '</pre><br/>';
 	}
 
 	public function gen_uuid()

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

@@ -458,7 +458,7 @@ trait OptionsFunction
 					'type' => 'select',
 					'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'),
+					'options' => $this->makeOptionsFromValues($this->config[str_replace('CalendarLink', '', $name) . 'URL'], true, 'Use Default'),
 				];
 				break;
 			case 'calendarframetarget':
@@ -466,7 +466,7 @@ trait OptionsFunction
 					'type' => 'select',
 					'label' => 'Target Tab',
 					'help' => 'Set the tab used when clicking on calendar icon. If not set, link will open in new window.',
-					'options' => $this->getIframeTabs($this->config[str_replace('FrameTarget','CalendarLink',$name)])
+					'options' => $this->getIframeTabs($this->config[str_replace('FrameTarget', 'CalendarLink', $name)])
 				];
 				break;
 			default:
@@ -484,10 +484,10 @@ trait OptionsFunction
 		}
 		return $setting;
 	}
-	
+
 	public function getIframeTabs($url = "")
-	{	
-		if (!empty($url)){
+	{
+		if (!empty($url)) {
 			$response = [
 				array(
 					'function' => 'fetchAll',
@@ -512,10 +512,10 @@ trait OptionsFunction
 			'name' => 'Open in New Window',
 			'value' => ''
 		];
-		foreach($this->processQueries($response) as $result) {
+		foreach ($this->processQueries($response) as $result) {
 			$formattedValues[] = [
 				'name' => $result['name'],
-				'value' => $result['name']
+				'value' => $result['id']
 			];
 		}
 		return $formattedValues;
@@ -523,7 +523,7 @@ trait OptionsFunction
 
 	public function makeOptionsFromValues($values = null, $appendBlank = null, $blankLabel = null)
 	{
-		if ($appendBlank === true){
+		if ($appendBlank === true) {
 			$formattedValues[] = [
 				'name' => (!empty($blankLabel)) ? $blankLabel : 'Select option...',
 				'value' => ''

+ 26 - 12
api/functions/organizr-functions.php

@@ -495,30 +495,44 @@ trait OrganizrFunctions
 				# code...
 				break;
 		}
+		if (strpos($key, '-') !== false) {
+			$noImage = 'no-' . explode('-', $key)[1] . '.png';
+		} else {
+			$noImage = 'no-np.png';
+		}
+		$noImage = $this->root . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'homepage' . DIRECTORY_SEPARATOR . $noImage;
 		if (isset($image_url) && isset($image_height) && isset($image_width) && isset($image_src)) {
 			$cachefile = $cacheDirectory . $key . '.jpg';
 			$cachetime = 604800;
 			// Serve from the cache if it is younger than $cachetime
-			if (file_exists($cachefile) && time() - $cachetime < filemtime($cachefile) && $refresh == false) {
-				header("Content-type: image/jpeg");
-				@readfile($cachefile);
+			if (file_exists($cachefile) && (time() - $cachetime < filemtime($cachefile)) && $refresh == false) {
+				header('Content-type: image/jpeg');
+				if (filesize($cachefile) > 0) {
+					@readfile($cachefile);
+				} else {
+					@readfile($noImage);
+				}
 				exit;
 			}
-			ob_start(); // Start the output buffer
-			header('Content-type: image/jpeg');
 			$options = array('verify' => false);
 			$response = Requests::get($image_src, array(), $options);
 			if ($response->success) {
+				ob_start(); // Start the output buffer
+				header('Content-type: image/jpeg');
 				echo $response->body;
+				// Cache the output to a file
+				$fp = fopen($cachefile, 'wb');
+				fwrite($fp, ob_get_contents());
+				fclose($fp);
+				ob_end_flush(); // Send the output to the browser
+				die();
+			} else {
+				header('Content-type: image/jpeg');
+				@readfile($noImage);
 			}
-			// Cache the output to a file
-			$fp = fopen($cachefile, 'wb');
-			fwrite($fp, ob_get_contents());
-			fclose($fp);
-			ob_end_flush(); // Send the output to the browser
-			die();
 		} else {
-			die($this->showHTML('Invalid Request', 'No image returned'));
+			header('Content-type: image/jpeg');
+			@readfile($noImage);
 		}
 	}
 

+ 1 - 1
api/homepage/ical.php

@@ -75,7 +75,7 @@ trait ICalHomepageItem
 				$timezone = 'Europe/Lisbon';
 				break;
 		}
-		return $timezone;
+		return in_array($timezone, timezone_identifiers_list()) ? $timezone : 'UTC';
 	}
 
 	public function getCalendarExtraDates($start, $rule, $timezone)

+ 46 - 6
api/homepage/jackett.php

@@ -30,11 +30,15 @@ trait JackettHomepageItem
 				'Options' => [
 					$this->settingsOption('switch', 'homepageJackettBackholeDownload', ['label' => 'Prefer black hole download', 'help' => 'Prefer black hole download link instead of direct/magnet download']),
 				],
+				'Test Connection' => [
+					$this->settingsOption('blank', null, ['label' => 'Please Save before Testing']),
+					$this->settingsOption('test', 'jackett'),
+				]
 			]
 		];
 		return array_merge($homepageInformation, $homepageSettings);
 	}
-	
+
 	public function jackettHomepagePermissions($key = null)
 	{
 		$permissions = [
@@ -49,11 +53,20 @@ trait JackettHomepageItem
 					'jackettURL',
 					'jackettToken'
 				]
+			],
+			'test' => [
+				'auth' => [
+					'homepageJackettAuth'
+				],
+				'not_empty' => [
+					'jackettURL',
+					'jackettToken'
+				]
 			]
 		];
 		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
-	
+
 	public function homepageOrderJackett()
 	{
 		if ($this->homepageItemPermissions($this->jackettHomepagePermissions('main'))) {
@@ -69,7 +82,35 @@ trait JackettHomepageItem
 				';
 		}
 	}
-	
+
+	public function testConnectionJackett()
+	{
+		if (!$this->homepageItemPermissions($this->jackettHomepagePermissions('test'), true)) {
+			return false;
+		}
+		$apiURL = $this->qualifyURL($this->config['jackettURL']);
+		$endpoint = $apiURL . '/api/v2.0/indexers/all/results?apikey=' . $this->config['jackettToken'] . '&Query=this-is-just-a-test-for-organizr';
+		try {
+			$headers = [];
+			$options = $this->requestOptions($apiURL, 120, $this->config['jackettDisableCertCheck'], $this->config['jackettUseCustomCertificate']);
+			$response = Requests::get($endpoint, $headers, $options);
+			if ($response->success) {
+				$apiData = json_decode($response->body, true);
+				$api['content'] = $apiData;
+				unset($apiData);
+			} else {
+				$this->setResponse(403, 'Error connecting to Jackett');
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->setResponse(500, $e->getMessage());
+			return false;
+		};
+		$api['content'] = $api['content'] ?? false;
+		$this->setResponse(200, null, $api);
+		return $api;
+	}
+
 	public function searchJackettIndexers($query = null)
 	{
 		if (!$this->homepageItemPermissions($this->jackettHomepagePermissions('main'), true)) {
@@ -91,15 +132,14 @@ trait JackettHomepageItem
 				unset($apiData);
 			}
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Weather And Air Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-			$this->setAPIResponse('error', $e->getMessage(), 500);
+			$this->setResponse(500, $e->getMessage());
 			return false;
 		};
 		$api['content'] = isset($api['content']) ? $api['content'] : false;
 		$this->setAPIResponse('success', null, 200, $api);
 		return $api;
 	}
-	
+
 	public function performJackettBackHoleDownload($url = null)
 	{
 		if (!$this->homepageItemPermissions($this->jackettHomepagePermissions('main'), true)) {

+ 38 - 3
api/homepage/overseerr.php

@@ -159,14 +159,36 @@ trait OverseerrHomepageItem
 		$url = $this->qualifyURL($this->config['overseerrURL']);
 		try {
 			$options = $this->requestOptions($url, $this->config['overseerrRefresh'], $this->config['overseerrDisableCertCheck'], $this->config['overseerrUseCustomCertificate']);
-			$request = Requests::get($url . "/api/v1/request?take=" . $limit, $headers, $options);
+			$request = Requests::get($url . "/api/v1/request?take=" . $limit . '&skip=' . $offset, $headers, $options);
 			if ($request->success) {
+				$requestAll = [];
 				$requestsData = json_decode($request->body, true);
 				foreach ($requestsData['results'] as $key => $value) {
 					$requester = ($value['requestedBy']['username'] !== '') ? $value['requestedBy']['username'] : $value['requestedBy']['plexUsername'];
 					$requesterEmail = $value['requestedBy']['email'];
 					$proceed = (($this->config['overseerrLimitUser']) && strtolower($this->user['username']) == strtolower($requester)) || (strtolower($requester) == strtolower($this->config['overseerrFallbackUser'])) || (!$this->config['overseerrLimitUser']) || $this->qualifyRequest(1);
 					if ($proceed) {
+						$requestAll[$value['media']['tmdbId']] = [
+							'url' => $url . '/api/v1/' . $value['type'] . '/' . $value['media']['tmdbId'],
+							'headers' => $headers,
+							'type' => Requests::GET,
+						];
+						$api['count'][$value['type']]++;
+						$requests[$value['media']['tmdbId']] = [
+							'id' => $value['media']['tmdbId'],
+							'approved' => $value['status'] == 2,
+							'available' => $value['media']['status'] == 5,
+							'denied' => $value['status'] == 3,
+							'deniedReason' => 'n/a',
+							'user' => $requester,
+							'userAlias' => $value['requestedBy']['displayName'],
+							'request_id' => $value['id'],
+							'request_date' => $value['createdAt'],
+							'type' => $value['type'],
+							'icon' => 'mdi mdi-' . ($value['type'] == 'movie') ? 'filmstrip' : 'television',
+							'color' => ($value['type'] == 'movie') ? 'palette-Deep-Purple-900 bg white' : 'grayish-blue-bg',
+						];
+						/* OLD WAY
 						$requestItem = Requests::get($url . '/api/v1/' . $value['type'] . '/' . $value['media']['tmdbId'], $headers, $options);
 						$requestsItemData = json_decode($requestItem->body, true);
 						if ($requestItem->success) {
@@ -190,7 +212,20 @@ trait OverseerrHomepageItem
 								'icon' => 'mdi mdi-' . ($value['type'] == 'movie') ? 'filmstrip' : 'television',
 								'color' => ($value['type'] == 'movie') ? 'palette-Deep-Purple-900 bg white' : 'grayish-blue-bg',
 							);
-						}
+						}*/
+					}
+				}
+				$requestItems = Requests::request_multiple($requestAll, $options);
+				foreach ($requestItems as $key => $requestedItem) {
+					if ($requestedItem->success) {
+						$requestsItemData = json_decode($requestedItem->body, true);
+						$requests[$key]['title'] = $requestsItemData['title'] ?? $requestsItemData['name'];
+						$requests[$key]['release_date'] = $requestsItemData['releaseDate'] ?? $requestsItemData['firstAirDate'];
+						$requests[$key]['background'] = (isset($requestsItemData['backdropPath']) && $requestsItemData['backdropPath'] !== '') ? 'https://image.tmdb.org/t/p/w1280/' . $requestsItemData['backdropPath'] : '';
+						$requests[$key]['poster'] = (isset($requestsItemData['posterPath']) && $requestsItemData['posterPath'] !== '') ? 'https://image.tmdb.org/t/p/w300/' . $requestsItemData['posterPath'] : 'plugins/images/homepage/no-list.png';
+						$requests[$key]['overview'] = $requestsItemData['overview'];
+					} else {
+						unset($requests[$key]);
 					}
 				}
 				//sort here
@@ -206,7 +241,7 @@ trait OverseerrHomepageItem
 			$this->setResponse(500, $e->getMessage());
 			return false;
 		}
-		$api['content'] = isset($requests) ? array_slice($requests, $offset, $limit) : false;
+		$api['content'] = $requests ?? false;
 		$this->setResponse(200, null, $api);
 		return $api;
 	}

+ 1 - 1
api/homepage/tautulli.php

@@ -93,7 +93,7 @@ trait TautulliHomepageItem
 		$apiURL = $url . '/api/v2?apikey=' . $this->config['tautulliApikey'];
 		try {
 			$homestatsUrl = $apiURL . '&cmd=get_home_stats&grouping=1';
-			$options = $this->requestOptions($this->config['tautulliURL'], $this->config['homepageTautulliRefresh'], $this->config['tautulliDisableCertCheck'], $this->config['tautulliUseCustomCertificate']);
+			$options = $this->requestOptions($this->config['tautulliURL'], $this->config['homepageTautulliRefresh'], $this->config['tautulliDisableCertCheck'], $this->config['tautulliUseCustomCertificate'], ['follow_redirects' => false]);
 			$homestats = Requests::get($homestatsUrl, [], $options);
 			if ($homestats->success) {
 				$this->setAPIResponse('success', 'API Connection succeeded', 200);

+ 1 - 1
api/plugins/chat/main.js

@@ -5,7 +5,7 @@ $('body').arrive('#activeInfo', {onceOnly: true}, function() {
 function chatLaunch(){
 	if(activeInfo.plugins["CHAT-enabled"] == true && activeInfo.plugins.includes["CHAT-authKey-include"] !== '' && activeInfo.plugins.includes["CHAT-appID-include"] !== '' && activeInfo.plugins.includes["CHAT-cluster-include"] !== ''){
 		if (activeInfo.user.groupID <= activeInfo.plugins.includes["CHAT-Auth-include"]) {
-			var menuList = `<li><a class=""  href="javascript:void(0)" onclick="tabActions(event,'chat','plugin');chatEntry();"><i class="fa fa-comments-o fa-fw"></i> <span lang="en">Chat</span><small class="chat-counter label label-rouded label-info pull-right hidden">0</small></a></li>`;
+			var menuList = `<li><a class=""  href="javascript:void(0)" onclick="switchToPlugin('chat');chatEntry();"><i class="fa fa-comments-o fa-fw"></i> <span lang="en">Chat</span><small class="chat-counter label label-rouded label-info pull-right hidden">0</small></a></li>`;
 			var htmlDOM = `
 			<div id="container-plugin-chat" class="plugin-container hidden">
 				<div class="chat-main-box bg-org">

+ 3 - 1
api/v2/index.php

@@ -52,7 +52,7 @@ $GLOBALS['bypass'] = array(
 $GLOBALS['responseCode'] = 200;
 function jsonE($json)
 {
-	return safe_json_encode($json, JSON_HEX_QUOT | JSON_HEX_TAG | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+	return safe_json_encode($json, JSON_HEX_QUOT | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); // JSON_HEX_TAG
 }
 
 function getBasePath()
@@ -87,6 +87,8 @@ $app->add(function ($request, $handler) {
 	// add the organizr to your request as [READ-ONLY]
 	$Organizr = new Organizr();
 	$request = $request->withAttribute('Organizr', $Organizr);
+	// set custom error handler
+	set_error_handler([$Organizr, 'setAPIErrorResponse']);
 	return $handler->handle($request);
 });
 //$app->add(new Lowercase());

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

@@ -644,4 +644,26 @@ $app->post('/test/database', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
+});
+$app->post('/test/jackett', function ($request, $response, $args) {
+	/**
+	 * @OA\Post(
+	 *     security={{ "api_key":{} }},
+	 *     tags={"test connection"},
+	 *     path="/api/v2/test/jackett",
+	 *     summary="Test connection to Jackett",
+	 *     @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->testConnectionJackett();
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
 });

+ 1 - 1
api/v2/routes/homepage.php

@@ -358,7 +358,7 @@ $app->get('/homepage/overseerr/metadata/{type}/{id}', function ($request, $respo
 });
 $app->get('/homepage/overseerr/requests[/{type}[/{limit}[/{offset}]]]', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
-	$args['limit'] = $args['limit'] ?? $Organizr->config['ombiLimit'];
+	$args['limit'] = $args['limit'] ?? $Organizr->config['overseerrLimit'];
 	$args['offset'] = $args['offset'] ?? 0;
 	$Organizr->getOverseerrRequests($args['limit'], $args['offset']);
 	$response->getBody()->write(jsonE($GLOBALS['api']));

+ 1 - 28
api/vendor/kryptonit3/sonarr/src/Sonarr.php

@@ -3,7 +3,6 @@
 namespace Kryptonit3\Sonarr;
 
 use GuzzleHttp\Client;
-use Composer\Semver\Comparator;
 
 class Sonarr
 {
@@ -595,7 +594,7 @@ class Sonarr
             'data' => []
         ];
 
-        return $this->preProcessRequest($response);
+        return $this->processRequest($response);
     }
 
     /**
@@ -660,10 +659,6 @@ class Sonarr
     protected function processRequest(array $request)
     {
 	    try {
-		    $versionCheck = $this->getSystemStatus();
-		    $versionCheck = json_decode($versionCheck, true);
-		    $versionCheck = (is_array($versionCheck) && array_key_exists('version', $versionCheck)) ? $versionCheck['version'] : '1.0';
-		    $compare = new Comparator;
 		    switch ($this->type){
 			    case 'sonarr':
 				    $versionCheck = 'v3/';
@@ -707,28 +702,6 @@ class Sonarr
 
         return $response->getBody()->getContents();
     }
-    protected function preProcessRequest(array $request)
-    {
-	    try {
-		    $response = $this->_request(
-			    [
-				    'uri' => $request['uri'],
-				    'type' => $request['type'],
-				    'data' => $request['data']
-			    ]
-		    );
-	    } catch ( \Exception $e ) {
-		    return json_encode(array(
-			    'error' => array(
-				    'msg' => $e->getMessage(),
-				    'code' => $e->getCode(),
-			    ),
-		    ));
-		
-		    exit();
-	    }
-	    return $response->getBody()->getContents();
-    }
 
     /**
      * Verify date is in proper format

+ 55 - 0
docs/api.json

@@ -2811,6 +2811,61 @@
                 ]
             }
         },
+        "/api/v2/test/jackett": {
+            "post": {
+                "tags": [
+                    "test connection"
+                ],
+                "summary": "Test connection to Jackett",
+                "responses": {
+                    "200": {
+                        "description": "Success",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/success-message"
+                                }
+                            }
+                        }
+                    },
+                    "401": {
+                        "description": "Unauthorized",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/unauthorized-message"
+                                }
+                            }
+                        }
+                    },
+                    "422": {
+                        "description": "Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/error-message"
+                                }
+                            }
+                        }
+                    },
+                    "500": {
+                        "description": "Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/error-message"
+                                }
+                            }
+                        }
+                    }
+                },
+                "security": [
+                    {
+                        "api_key": []
+                    }
+                ]
+            }
+        },
         "/api/v2/emby/register": {
             "post": {
                 "tags": [

+ 3 - 1
js/custom.js

@@ -858,6 +858,8 @@ function convertMinutesToMs(minutes){
 $(document).on("click", ".editTab", function () {
     var originalTabName = $('#originalTabName').html();
     var tabInfo = $('#edit-tab-form').serializeToJSON();
+    let tabNameLower = tabInfo.name.toLowerCase();
+    let originalTabNameLower = originalTabName.toLowerCase();
     if (typeof tabInfo.id == 'undefined' || tabInfo.id == '') {
         message('Edit Tab Error',' Could not get Tab ID',activeInfo.settings.notifications.position,'#FFF','error','5000');
 	    return false;
@@ -874,7 +876,7 @@ $(document).on("click", ".editTab", function () {
         message('Edit Tab Error',' Please set a Tab URL or Local URL',activeInfo.settings.notifications.position,'#FFF','warning','5000');
 	    return false;
     }
-    if(checkIfTabNameExists(tabInfo.name) && originalTabName !== tabInfo.name){
+    if(checkIfTabNameExists(tabInfo.name) && originalTabNameLower !== tabNameLower){
         message('Edit Tab Error',' Tab name already used',activeInfo.settings.notifications.position,'#FFF','warning','5000');
         return false;
     }

File diff suppressed because it is too large
+ 0 - 0
js/custom.min.js


File diff suppressed because it is too large
+ 346 - 220
js/functions.js


+ 105 - 105
js/langpack/ru[Russian].json

@@ -535,7 +535,7 @@
         "Unit of Measurement": "ед",
         "Enable Pollen": "Enable Pollen",
         "Enable Air Quality": "Enable Air Quality",
-        "Enable Weather": "Enable Weather",
+        "Enable Weather": "Включить отображение погоды",
         "Need Help With Coordinates?": "Помочь с координатами?",
         "Longitude": "Долгота",
         "Latitude": "Широта",
@@ -552,19 +552,19 @@
         "Youtube API Key": "Youtube API Key",
         "Misc": "Misc",
         "Multiple tags using CSV - tag1,tag2": "Multiple tags using CSV - tag1,tag2",
-        "Tags": "Tags",
+        "Tags": "Тэги",
         "HealthChecks API URL": "HealthChecks API URL",
         "HealthChecks": "HealthChecks",
         "Get Unifi Site": "Get Unifi Site",
         "Grab Unifi Site": "Grab Unifi Site",
-        "Site Name": "Site Name",
+        "Site Name": "Название сайта",
         "Unifi API URL": "Unifi API URL",
         "Show Denied": "Показать Отклонённые",
         "Show Unapproved": "Показать Неподтвержденные",
         "Show Approved": "Показать Подтвержденные",
         "Show Unavailable": "Show Unavailable",
         "Show Available": "Показать Доступные",
-        "Disable Certificate Check": "Disable Certificate Check",
+        "Disable Certificate Check": "Отключить проверку сертификата",
         "Organizr appends the url with": "Organizr appends the url with",
         "unless the URL ends in": "unless the URL ends in",
         "ATTENTION": "ВНИМАНИЕ",
@@ -577,15 +577,15 @@
         "To revert back to default, save with no value defined in the relevant field.": "To revert back to default, save with no value defined in the relevant field.",
         "e.g. UA-XXXXXXXXX-X": "e.g. UA-XXXXXXXXX-X",
         "Google Analytics Tracking ID": "Google Analytics Tracking ID",
-        "Show Debug Errors": "Show Debug Errors",
-        "Use Logo instead of Title on Login Page": "Use Logo instead of Title on Login Page",
-        "Login Logo": "Login Logo",
+        "Show Debug Errors": "Показывать ошибки отладки",
+        "Use Logo instead of Title on Login Page": "Использовать логотип вместо заголовка на странице входа",
+        "Login Logo": "Логотип при входе",
         "Meta Description": "Meta Description",
         "HealthChecks Settings": "HealthChecks Settings",
         "AdamSmith": "AdamSmith",
         "Emby User to be used as template for new users": "Emby User to be used as template for new users",
         "localhost:8086": "localhost:8086",
-        "Emby server adress": "Emby server adress",
+        "Emby server adress": "Адрес сервера Emby",
         "enter key from emby": "enter key from emby",
         "Emby API key": "Emby API key",
         "Music Labels (comma separated)": "Music Labels (comma separated)",
@@ -600,23 +600,23 @@
         "Auth Proxy": "Прокси сервер",
         "Enable Local Address Forward": "Включить переадресацию локального адреса",
         "http://home.local": "http://home.local",
-        "Local Address": "Local Address",
+        "Local Address": "Локальный адрес",
         "only domain and tld - i.e. domain.com": "only domain and tld - i.e. domain.com",
         "WAN Domain": "WAN Domain",
         "i.e. 123.123.123.123": "i.e. 123.123.123.123",
-        "Override Local IP To": "Override Local IP To",
-        "Override Local IP From": "Override Local IP From",
+        "Override Local IP To": "Переопределить локальный IP на",
+        "Override Local IP From": "Переопределить локальный IP c",
         "Disable Image Dropdown": "Disable Image Dropdown",
         "Disable Icon Dropdown": "Disable Icon Dropdown",
-        "Enable Traefik Auth Redirect": "Enable Traefik Auth Redirect",
+        "Enable Traefik Auth Redirect": "Включить перенаправление Traefik Auth",
         "iFrame Sandbox": "iFrame Sandbox",
         "Login Lockout Seconds": "Login Lockout Seconds",
-        "Max Login Attempts": "Max Login Attempts",
+        "Max Login Attempts": "Максимальное количество попыток входа",
         "Test Login": "Test Login",
-        "Ignore External 2FA on Local Subnet": "Ignore External 2FA on Local Subnet",
+        "Ignore External 2FA on Local Subnet": "Игнорировать сторонний 2FA в локальной подсети",
         "Large modal": "Large modal",
-        "An Error Occurred": "An Error Occurred",
-        "Type your message": "Type your message",
+        "An Error Occurred": "Произошла ошибка",
+        "Type your message": "Введите ваше сообщение",
         "Bookmark Tabs": "Bookmark Tabs",
         "Bookmark Categories": "Bookmark Categories",
         "Open Collective": "Open Collective",
@@ -636,32 +636,32 @@
         "API V2 TESTING almost complete": "API V2 TESTING almost complete",
         "Important Messages - Each message can now be ignored using ignore button": "Important Messages - Each message can now be ignored using ignore button",
         "Minimum PHP Version change": "Minimum PHP Version change",
-        "You": "You",
-        "Drop Certificate file here to upload": "Drop Certificate file here to upload",
-        "Custom Certificate Loaded": "Custom Certificate Loaded",
-        "By default, Organizr uses certificates from https://curl.se/docs/caextract.html": "By default, Organizr uses certificates from https://curl.se/docs/caextract.html",
-        "If you would like to use your own certificate, please upload it below.  You will then need to enable each homepage item to use it.": "If you would like to use your own certificate, please upload it below.  You will then need to enable each homepage item to use it.",
+        "You": "Вы",
+        "Drop Certificate file here to upload": "Перетащите сюда файл сертификата для загрузки",
+        "Custom Certificate Loaded": "Пользовательский сертификат загружен",
+        "By default, Organizr uses certificates from https://curl.se/docs/caextract.html": "По умолчанию Organizr использует сертификаты из https://curl.se/docs/caextract.html",
+        "If you would like to use your own certificate, please upload it below.  You will then need to enable each homepage item to use it.": "Если вы хотите использовать свой собственный сертификат, пожалуйста, загрузите его ниже. Затем вам нужно будет включить каждый элемент домашней страницы, чтобы использовать его.",
         "i.e. X-Forwarded-Email": "i.e. X-Forwarded-Email",
         "Auth Proxy Header Name for Email": "Auth Proxy Header Name for Email",
         "Custom Recover Password Text": "Custom Recover Password Text",
-        "Disable Recover Password": "Disable Recover Password",
-        "Blacklisted Error Message": "Blacklisted Error Message",
-        "Blacklisted IP's": "Blacklisted IP's",
+        "Disable Recover Password": "Отключить восстановление пароля",
+        "Blacklisted Error Message": "Чёрный список сообщений о ошибках",
+        "Blacklisted IP's": "Чёрный список IP",
         "http(s)://domain": "http(s)://domain",
         "Traefik Domain for Return Override": "Traefik Domain for Return Override",
-        "Jellyfin Token": "Jellyfin Token",
+        "Jellyfin Token": "Jellyfin Токен",
         "Jellyfin URL": "Jellyfin URL",
-        "Enable LDAP TLS": "Enable LDAP TLS",
-        "Enable LDAP SSL": "Enable LDAP SSL",
+        "Enable LDAP TLS": "Включить LDAP TLS",
+        "Enable LDAP SSL": "Включить LDAP SSL",
         "Bind Password": "Bind Password",
         "http(s) | ftp(s) | ldap(s)://hostname:port": "http(s) | ftp(s) | ldap(s)://hostname:port",
         "Plex Admin Username": "Plex Admin Username",
         "Default Settings Tab": "Default Settings Tab",
-        "Certificate": "Certificate",
+        "Certificate": "Сертификат",
         "Ping": "Ping",
         "API": "API",
         "Github": "Github",
-        "Settings Page": "Settings Page",
+        "Settings Page": "Страница настроек",
         "http(s)://domain.com": "http(s)://domain.com",
         "Jellyfin SSO URL": "Jellyfin SSO URL",
         "Jellyfin API URL": "Jellyfin API URL",
@@ -675,97 +675,97 @@
         "Overseerr URL": "Overseerr URL",
         "Multiple URL's": "Multiple URL's",
         "Using multiple SSO application will cause your Cookie Header item to increase.  If you haven't increased it by now, please follow this guide": "Using multiple SSO application will cause your Cookie Header item to increase.  If you haven't increased it by now, please follow this guide",
-        "Please Read First": "Please Read First",
+        "Please Read First": "Пожалуйста, сначала прочтите это",
         "Jellyfin": "Jellyfin",
         "Petio": "Petio",
         "Overseerr": "Overseerr",
         "FYI": "FYI",
         "https://app.plex.tv/auth#?resetPassword": "https://app.plex.tv/auth#?resetPassword",
-        "Change Password on Plex Website": "Change Password on Plex Website",
-        "Action": "Action",
+        "Change Password on Plex Website": "Сменить пароль на сайте Plex",
+        "Action": "Действие",
         "IP": "IP",
-        "Browser": "Browser",
-        "Expires": "Expires",
-        "Created": "Created",
-        "Version": "Version",
-        "Files": "Files",
+        "Browser": "Браузер",
+        "Expires": "Истекает",
+        "Created": "Создан",
+        "Version": "Версия",
+        "Files": "Файлы",
         "Backup Organizr": "Backup Organizr",
-        "Create Backup": "Create Backup",
-        "Select or type Image": "Select or type Image",
-        "Choose": "Choose",
+        "Create Backup": "Создать бэкап",
+        "Select or type Image": "Выберите или вставьте изображение",
+        "Choose": "Выберите",
         "Choose Blackberry Theme Icon": "Choose Blackberry Theme Icon",
-        "Save Tab Order": "Save Tab Order",
+        "Save Tab Order": "Сохранить порядок закладок",
         "Drag Homepage Items to Order Them": "Drag Homepage Items to Order Them",
-        "Preview": "Preview",
-        "Text Color": "Text Color",
-        "Background Color": "Background Color",
+        "Preview": "Предпросмотр",
+        "Text Color": "Цвет текста",
+        "Background Color": "Цвет фона",
         "Bookmark Tab Editor": "Bookmark Tab Editor",
-        "Add New Bookmark Category": "Add New Bookmark Category",
-        "Bookmark Category Editor": "Bookmark Category Editor",
+        "Add New Bookmark Category": "Добавить новую категорию закладок",
+        "Bookmark Category Editor": "Редактор категорий закладок",
         "Auto-Expand Nav Bar": "Auto-Expand Nav Bar",
         "Auto-Collapse Categories": "Auto-Collapse Categories",
-        "Expand All Categories": "Expand All Categories",
+        "Expand All Categories": "Расширить список всех категорий",
         "Show Organizr Sign out & in Button on Sidebar": "Show Organizr Sign out & in Button on Sidebar",
         "Show Organizr Docs Link": "Show Organizr Docs Link",
         "Show Organizr Support Link": "Show Organizr Support Link",
         "Show Organizr Feature Request Link": "Show Organizr Feature Request Link",
-        "Show GitHub Repo Link": "Show GitHub Repo Link",
-        "Theme CSS": "Theme CSS",
-        "Custom CSS": "Custom CSS",
+        "Show GitHub Repo Link": "Показывать ссылку на GitHub репозиторий",
+        "Theme CSS": "Тема CSS",
+        "Custom CSS": "Пользовательский CSS",
         "FavIcon": "FavIcon",
-        "Notifications": "Notifications",
-        "Colors & Themes": "Colors & Themes",
-        "Options": "Options",
-        "Login Page": "Login Page",
+        "Notifications": "Уведомления",
+        "Colors & Themes": "Цвета и темы",
+        "Options": "Опции",
+        "Login Page": "Страница входа",
         "Top Bar": "Top Bar",
-        "Bookmark Settings": "Bookmark Settings",
-        "HnL Settings": "HnL Settings",
-        "Not Installed": "Not Installed",
-        "Money not an option?  No problem.  Show some love to this Google Ad below:": "Money not an option?  No problem.  Show some love to this Google Ad below:",
-        "Please click the button to continue.": "Please click the button to continue.",
+        "Bookmark Settings": "Настройки закладок",
+        "HnL Settings": "Настройки HnL",
+        "Not Installed": "Не установлен",
+        "Money not an option?  No problem.  Show some love to this Google Ad below:": "Деньги не вариант? Без проблем. Покажите свою любовь к этому объявлению Google ниже:",
+        "Please click the button to continue.": "Пожалуйста, нажмите кнопку, чтобы продолжить.",
         "Need specialized support or just want to support Organizr?  If so head to Open Collective...": "Need specialized support or just want to support Organizr?  If so head to Open Collective...",
         "Need specialized support or just want to support Organizr?  If so head to Patreon...": "Need specialized support or just want to support Organizr?  If so head to Patreon...",
         "Want to donate a small amount of Crypto?.": "Want to donate a small amount of Crypto?.",
-        "Please use the QR Code or Wallet ID.": "Please use the QR Code or Wallet ID.",
+        "Please use the QR Code or Wallet ID.": "Используйте QR-код или Wallet ID.",
         "If you use the Square Cash App, you can donate with that if you like.": "If you use the Square Cash App, you can donate with that if you like.",
         "I have chosen to go with PayPal Pools so everyone can see how much people have donated.": "I have chosen to go with PayPal Pools so everyone can see how much people have donated.",
-        "Want to show support on Github?  Sponsor me :)": "Want to show support on Github?  Sponsor me :)",
-        "If messages get stuck sending, please turn this option off.": "If messages get stuck sending, please turn this option off.",
-        "Save and reload!": "Save and reload!",
-        "Copy and paste the 4 values into Organizr": "Copy and paste the 4 values into Organizr",
+        "Want to show support on Github?  Sponsor me :)": "Хотите показать поддержку на Github? Задонатьте мне :)",
+        "If messages get stuck sending, please turn this option off.": "Если сообщения зависают при отправке, отключите эту опцию.",
+        "Save and reload!": "Сохранить и перезагрузить!",
+        "Copy and paste the 4 values into Organizr": "Скопируйте и вставьте 4 значения в Organizr",
         "Click the overview tab on top left": "Click the overview tab on top left",
         "Frontend (JQuery) - Backend (PHP)": "Frontend (JQuery) - Backend (PHP)",
         "Create an App called whatever you like and choose a cluster (Close to you)": "Create an App called whatever you like and choose a cluster (Close to you)",
         "Signup for Pusher [FREE]": "Signup for Pusher [FREE]",
-        "Connection": "Connection",
-        "Enabled": "Enabled",
-        "Internal URL": "Internal URL",
-        "External URL": "External URL",
+        "Connection": "Подключение",
+        "Enabled": "Включено",
+        "Internal URL": "Внутренний URL",
+        "External URL": "Внешний URL",
         "UUID": "UUID",
-        "Service Name": "Service Name",
+        "Service Name": "Название сервиса",
         "Make sure to save before using the import button on Services tab": "Make sure to save before using the import button on Services tab",
         "Do not use a Read-Only Token as that will not give a correct UUID for sending the results to HealthChecks.io": "Do not use a Read-Only Token as that will not give a correct UUID for sending the results to HealthChecks.io",
-        "Please use a Full Access Token": "Please use a Full Access Token",
+        "Please use a Full Access Token": "Пожалуйста используйте Full Access Token",
         "URL for HealthChecks API": "URL for HealthChecks API",
         "403 Error as Success": "403 Error as Success",
         "401 Error as Success": "401 Error as Success",
         "HealthChecks Ping URL": "HealthChecks Ping URL",
         "URL for HealthChecks Ping": "URL for HealthChecks Ping",
         "As often as you like - i.e. every 1 minute": "As often as you like - i.e. every 1 minute",
-        "Frequency": "Frequency",
+        "Frequency": "Частота",
         "CRON Job URL": "CRON Job URL",
         "Once this plugin is setup, you will need to setup a CRON job": "Once this plugin is setup, you will need to setup a CRON job",
-        "Services": "Services",
-        "Import Services": "Import Services",
-        "Add New Service": "Add New Service",
+        "Services": "Сервисы",
+        "Import Services": "Импортировать сервисы",
+        "Add New Service": "Добавить новые сервисы",
         "After enabling for the first time, please reload the page - Menu is located under User menu on top right": "After enabling for the first time, please reload the page - Menu is located under User menu on top right",
-        "Emby Settings": "Emby Settings",
-        "Plex Settings": "Plex Settings",
-        "Backend": "Backend",
-        "Templates": "Templates",
+        "Emby Settings": "Настройки Emby",
+        "Plex Settings": "Настройки Plex",
+        "Backend": "Бэкенд",
+        "Templates": "Шаблоны",
         "Test & Options": "Test & Options",
-        "Sender Information": "Sender Information",
-        "Host": "Host",
+        "Sender Information": "Информация об отправителе",
+        "Host": "Хост",
         "Open your custom Bookmark page via menu.": "Open your custom Bookmark page via menu.",
         "Create Bookmark tabs in the new area in": "Create Bookmark tabs in the new area in",
         "Create Bookmark categories in the new area in": "Create Bookmark categories in the new area in",
@@ -774,8 +774,8 @@
         "Checking for bookmark default category...": "Checking for bookmark default category...",
         "Checking for Bookmark tab...": "Checking for Bookmark tab...",
         "Automatic Setup Tasks": "Automatic Setup Tasks",
-        "Located at": "Located at",
-        "Custom Certificate Status": "Custom Certificate Status",
+        "Located at": "Расположен на",
+        "Custom Certificate Status": "Состояние пользовательских сертификатов",
         "Will play a sound if the server goes down and will play sound if comes back up.": "Will play a sound if the server goes down and will play sound if comes back up.",
         "Please choose a unique value for added security": "Please choose a unique value for added security",
         "IPv4 only at the moment - This must be set to work, will accept subnet or IP address": "IPv4 only at the moment - This must be set to work, will accept subnet or IP address",
@@ -790,14 +790,14 @@
         "Number of days cookies and tokens will be valid for": "Number of days cookies and tokens will be valid for",
         "Enable this to hide the Registration button on the login screen": "Enable this to hide the Registration button on the login screen",
         "Sets the password for the Registration form on the login screen": "Sets the password for the Registration form on the login screen",
-        "WARNING! This will block anyone with these IP's": "WARNING! This will block anyone with these IP's",
+        "WARNING! This will block anyone with these IP's": "ВНИМАНИЕ! Это заблокирует любого с этими IP-адресами.",
         "WARNING! This can potentially mess up your iFrames": "WARNING! This can potentially mess up your iFrames",
         "Please use a FQDN on this URL Override": "Please use a FQDN on this URL Override",
         "This will enable the webserver to forward errors so traefik will accept them": "This will enable the webserver to forward errors so traefik will accept them",
         "Please make sure to use local IP address and port - You also may use local dns name too.": "Please make sure to use local IP address and port - You also may use local dns name too.",
         "Remember! Please save before using the test button!": "Remember! Please save before using the test button!",
-        "This will enable the use of TLS for LDAP connections": "This will enable the use of TLS for LDAP connections",
-        "This will enable the use of SSL for LDAP connections": "This will enable the use of SSL for LDAP connections",
+        "This will enable the use of TLS for LDAP connections": "Это включит TLS для LDAP подключений",
+        "This will enable the use of SSL for LDAP connections": "Это включит SSL для LDAP подключений",
         "Enabling this will bypass external 2FA security if user is on local Subnet": "Enabling this will bypass external 2FA security if user is on local Subnet",
         "Enabling this will only allow Friends that have shares to the Machine ID entered above to login, Having this disabled will allow all Friends on your Friends list to login": "Enabling this will only allow Friends that have shares to the Machine ID entered above to login, Having this disabled will allow all Friends on your Friends list to login",
         "Since you are using the official Docker image, you can just restart your Docker container to update Organizr": "Since you are using the official Docker image, you can just restart your Docker container to update Organizr",
@@ -806,54 +806,54 @@
         "Please make sure to use the same (sub)domain to access Jellyfin as Organizr's": "Please make sure to use the same (sub)domain to access Jellyfin as Organizr's",
         "Please make sure to use the local address to the API": "Please make sure to use the local address to the API",
         "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": "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",
-        "Purge Log": "Purge Log",
-        "Avatar": "Avatar",
-        "Date Registered": "Date Registered",
-        "Group": "Group",
-        "Locked": "Locked",
-        "Copy to Clipboard": "Copy to Clipboard",
-        "Choose action:": "Choose action:",
+        "Purge Log": "Удалить логи",
+        "Avatar": "Аватар",
+        "Date Registered": "Дата регистрации",
+        "Group": "Группа",
+        "Locked": "Заблокирован",
+        "Copy to Clipboard": "Копировать в буфер обмена",
+        "Choose action:": "Выберите действие: ",
         "You may enter multiple URL's using the CSV format.  i.e. link#1,link#2,link#3": "You may enter multiple URL's using the CSV format.  i.e. link#1,link#2,link#3",
         "Used to set the description for SEO meta tags": "Used to set the description for SEO meta tags",
-        "Also sets the title of your site": "Also sets the title of your site",
+        "Also sets the title of your site": "Также устанавливает название вашего сайта",
         "Up to date": "Up to date",
-        "Loading Pihole...": "Loading Pihole...",
-        "Loading Unifi...": "Loading Unifi...",
-        "Loading Weather...": "Loading Weather...",
+        "Loading Pihole...": "Загрузка Pihole...",
+        "Loading Unifi...": "Загрузка Unifi...",
+        "Loading Weather...": "Загрузка погоды...",
         "Loading Tautulli...": "Loading Tautulli...",
         "Loading Health Checks...": "Loading Health Checks...",
         "Health Checks": "Health Checks",
         "UniFi": "UniFi",
-        "Connection Error to rTorrent": "Connection Error to rTorrent",
+        "Connection Error to rTorrent": "Ошибка подключения у rTorrent",
         "Request a Show or Movie": "Request a Show or Movie",
         "Set": "Set",
         "Set WAL Mode": "Set WAL Mode",
         "Set DELETE Mode (Default)": "Set DELETE Mode (Default)",
         "Journal Mode Status": "Journal Mode Status",
         "This feature is experimental - You may face unexpected database is locked errors in logs": "This feature is experimental - You may face unexpected database is locked errors in logs",
-        "Warning": "Warning",
+        "Warning": "Предупреждение",
         "Tab Help": "Tab Help",
         "Toggle this tab to loaded in the background on page load": "Toggle this tab to loaded in the background on page load",
-        "Preload": "Preload",
+        "Preload": "Предзагрузить",
         "Enable Organizr to ping the status of the local URL of this tab": "Enable Organizr to ping the status of the local URL of this tab",
         "Toggle this to add the tab to the Splash Page on page load": "Toggle this to add the tab to the Splash Page on page load",
         "Splash": "Splash",
         "Either mark a tab as active or inactive": "Either mark a tab as active or inactive",
         "You can choose one tab to be the first opened tab on page load": "You can choose one tab to be the first opened tab on page load",
-        "Default": "Default",
+        "Default": "По умолчанию",
         "Internal is for Organizr pages": "Internal is for Organizr pages",
         "iFrame is for all others": "iFrame is for all others",
         "New Window is for items to open in a new window": "New Window is for items to open in a new window",
         "The lowest Group that will have access to this tab": "The lowest Group that will have access to this tab",
         "Each Tab is assigned a Category, the default is unsorted.  You may create new categories on the Category settings tab": "Each Tab is assigned a Category, the default is unsorted.  You may create new categories on the Category settings tab",
-        "Category": "Category",
+        "Category": "Категория",
         "The text that will be displayed for that certain tab": "The text that will be displayed for that certain tab",
         "Please Save before Testing. Note that using a blank password might not work correctly.": "Please Save before Testing. Note that using a blank password might not work correctly.",
-        "Use Custom Certificate": "Use Custom Certificate",
+        "Use Custom Certificate": "Использовать пользовательский сертификат",
         "Note that using a blank password might not work correctly.": "Note that using a blank password might not work correctly.",
-        "Database Password": "Database Password",
-        "Database Username": "Database Username",
-        "Database Host": "Database Host",
+        "Database Password": "Пароль базы данных",
+        "Database Username": "Имя пользователя базы данных",
+        "Database Host": "Хост базы данных",
         ".": "."
     }
 }

+ 0 - 0
js/langpack/zh[Chinese (Traditional)].json → js/langpack/zh-Hant[Chinese (Traditional)].json


+ 7 - 0
js/version.json

@@ -523,5 +523,12 @@
     "new": "",
     "fixed": "fixed theme marketplace downloading of themes with spaces",
     "notes": ""
+  },
+  "2.1.1790": {
+    "date": "2022-04-08 16:22",
+    "title": "Weekly Update",
+    "new": "added a check to see if theme in user theme folder is actually installed|added custom authProxy logout url (#1771)|added default config values authProxyOverrideLogout and authProxyLogoutURL|added menuExtras to tabInformation|added setAPIErrorResponse to api response|added setErrorResponse to organizr class|added test connection to jackett (#1800)",
+    "fixed": "added formatPingHost function to fix incorrectly saved ping URLs|fixed chat plugin not closing with close tab button (#1816)|fixed checking of tab name if changing casing only|fixed chinese tradition not being set (#1813)|fixed issue with catchall for ical timezones (#1772)|fixed overseerr limit variable being incorrect|fixed ping URLs that were incorrect|fixed tautulli connection tester|fix preloading issue with new tabActions",
+    "notes": "changed getHomepageMediaImage slightly to use default image if error|changed js-switch to default size medium|changed overseerr from a foreach call to request_multiple method|changed tabActions to use tab id instead of tab name|comment out prettyprint for errors for now|removed semver from sonarr/radarr/lidarr class|renamed tabActions for plugins to switchToPlugin|replace organizr user object if token set with token object for sso|restored old getSystemStatus method from sonarr/radarr/lidarr class|switched value of name to id for getIframeTabs function|update api.json file|updated prettyPrint function to include red error|Updated the following languages: [Russian]"
   }
 }

Some files were not shown because too many files changed in this diff