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

Merge pull request #1565 from causefx/v2-develop

V2 develop
causefx 5 лет назад
Родитель
Сommit
02cccdc9eb

+ 14 - 0
.github/workflows/lock.yml

@@ -0,0 +1,14 @@
+name: Lock closed stale issue
+
+on:
+  issues:
+    types: [closed]
+
+jobs:
+  lock:
+    if: github.event.label.name == 'closed-no-issue-activity'
+    runs-on: ubuntu-latest
+    steps:
+      - uses: OSDKDev/lock-issues@v1.1
+        with:
+          repo-token: "${{ secrets.GITHUB_TOKEN }}"

+ 19 - 0
.github/workflows/stale.yml

@@ -0,0 +1,19 @@
+name: Close stale issues
+
+on:
+  schedule:
+    - cron: "30 3 * * *"
+
+jobs:
+  stale:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/stale@v3
+        with:
+          repo-token: ${{ secrets.GITHUB_TOKEN }}
+          stale-issue-message: "This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs."
+          close-issue-message: "This issue has been closed due to lack of activity, if this issue still persists, please re-open it."
+          close-issue-label: "closed-no-issue-activity"
+          stale-issue-label: "no-issue-activity"
+          days-before-stale: 15
+          days-before-close: 90

+ 2 - 1
.gitignore

@@ -167,4 +167,5 @@ api/pages/custom/*.php
 /plugins/images/cache/tautulli-artist.svg
 /plugins/images/cache/tautulli-artist.svg
 /plugins/images/cache/tautulli-movie.svg
 /plugins/images/cache/tautulli-movie.svg
 /plugins/images/cache/tautulli-windows.svg
 /plugins/images/cache/tautulli-windows.svg
-plugins/images/cache/tautulli-samsung.svg
+/plugins/images/cache/tautulli-samsung.svg
+/plugins/images/cache/tautulli-chrome.svg

+ 202 - 110
api/classes/organizr.class.php

@@ -58,7 +58,7 @@ class Organizr
 	
 	
 	// ===================================
 	// ===================================
 	// Organizr Version
 	// Organizr Version
-	public $version = '2.1.120';
+	public $version = '2.1.165';
 	// ===================================
 	// ===================================
 	// Quick php Version check
 	// Quick php Version check
 	public $minimumPHP = '7.2';
 	public $minimumPHP = '7.2';
@@ -145,6 +145,7 @@ class Organizr
 				$this->db = new Connection([
 				$this->db = new Connection([
 					'driver' => 'sqlite3',
 					'driver' => 'sqlite3',
 					'database' => $this->config['dbLocation'] . $this->config['dbName'],
 					'database' => $this->config['dbLocation'] . $this->config['dbName'],
+					//'onConnect' => array('PRAGMA journal_mode=WAL'),
 				]);
 				]);
 			} catch (Dibi\Exception $e) {
 			} catch (Dibi\Exception $e) {
 				$this->db = null;
 				$this->db = null;
@@ -216,7 +217,8 @@ class Organizr
 			}
 			}
 			if ($group !== null) {
 			if ($group !== null) {
 				if ((isset($_SERVER['HTTP_X_FORWARDED_SERVER']) && $_SERVER['HTTP_X_FORWARDED_SERVER'] == 'traefik') || $this->config['traefikAuthEnable']) {
 				if ((isset($_SERVER['HTTP_X_FORWARDED_SERVER']) && $_SERVER['HTTP_X_FORWARDED_SERVER'] == 'traefik') || $this->config['traefikAuthEnable']) {
-					$redirect = 'Location: ' . $this->getServerPath();
+					$return = (isset($_SERVER['HTTP_X_FORWARDED_HOST']) && isset($_SERVER['HTTP_X_FORWARDED_URI']) && isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) ? '?return=' . $_SERVER['HTTP_X_FORWARDED_PROTO'] . '://' . $_SERVER['HTTP_X_FORWARDED_HOST'] . $_SERVER['HTTP_X_FORWARDED_URI'] : '';
+					$redirect = 'Location: ' . $this->getServerPath() . $return;
 				}
 				}
 				if ($this->qualifyRequest($group) && $unlocked) {
 				if ($this->qualifyRequest($group) && $unlocked) {
 					header("X-Organizr-User: $currentUser");
 					header("X-Organizr-User: $currentUser");
@@ -343,12 +345,12 @@ class Organizr
 		if ($this->config['gaTrackingID'] !== '') {
 		if ($this->config['gaTrackingID'] !== '') {
 			return '
 			return '
 				<script async src="https://www.googletagmanager.com/gtag/js?id=' . $this->config['gaTrackingID'] . '"></script>
 				<script async src="https://www.googletagmanager.com/gtag/js?id=' . $this->config['gaTrackingID'] . '"></script>
-    			<script>
-				    window.dataLayer = window.dataLayer || [];
-				    function gtag(){dataLayer.push(arguments);}
-				    gtag("js", new Date());
-				    gtag("config","' . $this->config['gaTrackingID'] . '");
-    			</script>
+				<script>
+					window.dataLayer = window.dataLayer || [];
+					function gtag(){dataLayer.push(arguments);}
+					gtag("js", new Date());
+					gtag("config","' . $this->config['gaTrackingID'] . '");
+				</script>
 			';
 			';
 		}
 		}
 		return null;
 		return null;
@@ -1299,6 +1301,12 @@ class Organizr
 					'label' => 'Show GitHub Repo Link',
 					'label' => 'Show GitHub Repo Link',
 					'value' => $this->config['githubMenuLink']
 					'value' => $this->config['githubMenuLink']
 				),
 				),
+				array(
+					'type' => 'switch',
+					'name' => 'organizrFeatureRequestLink',
+					'label' => 'Show Organizr Feature Request Link',
+					'value' => $this->config['organizrFeatureRequestLink']
+				),
 				array(
 				array(
 					'type' => 'switch',
 					'type' => 'switch',
 					'name' => 'organizrSupportMenuLink',
 					'name' => 'organizrSupportMenuLink',
@@ -1317,6 +1325,12 @@ class Organizr
 					'label' => 'Show Organizr Sign out & in Button on Sidebar',
 					'label' => 'Show Organizr Sign out & in Button on Sidebar',
 					'value' => $this->config['organizrSignoutMenuLink']
 					'value' => $this->config['organizrSignoutMenuLink']
 				),
 				),
+				array(
+					'type' => 'switch',
+					'name' => 'expandCategoriesByDefault',
+					'label' => 'Expand All Categories',
+					'value' => $this->config['expandCategoriesByDefault']
+				),
 				array(
 				array(
 					'type' => 'select',
 					'type' => 'select',
 					'name' => 'unsortedTabs',
 					'name' => 'unsortedTabs',
@@ -1348,19 +1362,19 @@ class Organizr
 					'label' => 'Custom CSS [Can replace colors from above]',
 					'label' => 'Custom CSS [Can replace colors from above]',
 					'html' => '
 					'html' => '
 					<div class="row">
 					<div class="row">
-					    <div class="col-lg-12">
-					        <div class="panel panel-info">
-					            <div class="panel-heading">
-					                <span lang="en">Notice</span>
-					            </div>
-					            <div class="panel-wrapper collapse in" aria-expanded="true">
-					                <div class="panel-body">
-					                    <span lang="en">The value of #987654 is just a placeholder, you can change to any value you like.</span>
-					                    <span lang="en">To revert back to default, save with no value defined in the relevant field.</span>
-					                </div>
-					            </div>
-					        </div>
-					    </div>
+						<div class="col-lg-12">
+							<div class="panel panel-info">
+								<div class="panel-heading">
+									<span lang="en">Notice</span>
+								</div>
+								<div class="panel-wrapper collapse in" aria-expanded="true">
+									<div class="panel-body">
+										<span lang="en">The value of #987654 is just a placeholder, you can change to any value you like.</span>
+										<span lang="en">To revert back to default, save with no value defined in the relevant field.</span>
+									</div>
+								</div>
+							</div>
+						</div>
 					</div>
 					</div>
 					',
 					',
 				),
 				),
@@ -2081,6 +2095,21 @@ class Organizr
 					'help' => 'Enables the local address forward if on local address and accessed from WAN Domain',
 					'help' => 'Enables the local address forward if on local address and accessed from WAN Domain',
 					'value' => $this->config['enableLocalAddressForward'],
 					'value' => $this->config['enableLocalAddressForward'],
 				),
 				),
+				array(
+					'type' => 'switch',
+					'name' => 'disableRecoverPassword',
+					'label' => 'Disable Recover Password',
+					'help' => 'Disables recover password area',
+					'value' => $this->config['disableRecoverPassword'],
+				),
+				array(
+					'type' => 'input',
+					'name' => 'customForgotPasswordText',
+					'label' => 'Custom Recover Password Text',
+					'value' => $this->config['customForgotPasswordText'],
+					'placeholder' => '',
+					'help' => 'Text or HTML for recovery password section'
+				),
 			),
 			),
 			'Auth Proxy' => array(
 			'Auth Proxy' => array(
 				array(
 				array(
@@ -2184,18 +2213,18 @@ class Organizr
 					'override' => 12,
 					'override' => 12,
 					'html' => '
 					'html' => '
 				<div class="row">
 				<div class="row">
-						    <div class="col-lg-12">
-						        <div class="panel panel-info">
-						            <div class="panel-heading">
-						                <span lang="en">Notice</span>
-						            </div>
-						            <div class="panel-wrapper collapse in" aria-expanded="true">
-						                <div class="panel-body">
-						                    <span lang="en">This is not the same as database authentication - i.e. Plex Authentication | Emby Authentication | FTP Authentication<br/>Click Main on the sub-menu above.</span>
-						                </div>
-						            </div>
-						        </div>
-						    </div>
+							<div class="col-lg-12">
+								<div class="panel panel-info">
+									<div class="panel-heading">
+										<span lang="en">Notice</span>
+									</div>
+									<div class="panel-wrapper collapse in" aria-expanded="true">
+										<div class="panel-body">
+											<span lang="en">This is not the same as database authentication - i.e. Plex Authentication | Emby Authentication | FTP Authentication<br/>Click Main on the sub-menu above.</span>
+										</div>
+									</div>
+								</div>
+							</div>
 						</div>
 						</div>
 				'
 				'
 				)
 				)
@@ -2268,6 +2297,43 @@ class Organizr
 					'value' => $this->config['ssoTautulli']
 					'value' => $this->config['ssoTautulli']
 				)
 				)
 			),
 			),
+			'Overseerr' => array(
+				array(
+					'type' => 'input',
+					'name' => 'overseerrURL',
+					'label' => 'Overseerr URL',
+					'value' => $this->config['overseerrURL'],
+					'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+					'placeholder' => 'http(s)://hostname:port'
+				),
+				array(
+					'type' => 'password-alt',
+					'name' => 'overseerrToken',
+					'label' => 'Token',
+					'value' => $this->config['overseerrToken']
+				),
+				array(
+					'type' => 'input',
+					'name' => 'overseerrFallbackUser',
+					'label' => 'Overseerr Fallback User',
+					'value' => $this->config['overseerrFallbackUser'],
+					'help' => 'Organizr will request an Overseerr User Token based off of this user credentials',
+					'attr' => 'disabled'
+				),
+				array(
+					'type' => 'password-alt',
+					'name' => 'overseerrFallbackPassword',
+					'label' => 'Overseerr Fallback Password',
+					'value' => $this->config['overseerrFallbackPassword'],
+					'attr' => 'disabled'
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'ssoOverseerr',
+					'label' => 'Enable',
+					'value' => $this->config['ssoOverseerr']
+				)
+			),
 			'Ombi' => array(
 			'Ombi' => array(
 				array(
 				array(
 					'type' => 'input',
 					'type' => 'input',
@@ -2514,92 +2580,92 @@ class Organizr
 			array(
 			array(
 				'function' => 'query',
 				'function' => 'query',
 				'query' => 'CREATE TABLE `chatroom` (
 				'query' => 'CREATE TABLE `chatroom` (
-			        `id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
-			        `username`	TEXT,
-			        `gravatar`	TEXT,
-			        `uid`	TEXT,
-			        `date` DATE,
-			        `ip` TEXT,
-			        `message` TEXT
-			    );'
+					`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
+					`username`	TEXT,
+					`gravatar`	TEXT,
+					`uid`	TEXT,
+					`date` DATE,
+					`ip` TEXT,
+					`message` TEXT
+				);'
 			),
 			),
 			array(
 			array(
 				'function' => 'query',
 				'function' => 'query',
 				'query' => 'CREATE TABLE `tokens` (
 				'query' => 'CREATE TABLE `tokens` (
-			        `id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
-			        `token`	TEXT UNIQUE,
-			        `user_id`	INTEGER,
-			        `browser`	TEXT,
-			        `ip`	TEXT,
-			        `created` DATE,
-			        `expires` DATE
-			    );'
+					`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
+					`token`	TEXT UNIQUE,
+					`user_id`	INTEGER,
+					`browser`	TEXT,
+					`ip`	TEXT,
+					`created` DATE,
+					`expires` DATE
+				);'
 			),
 			),
 			array(
 			array(
 				'function' => 'query',
 				'function' => 'query',
 				'query' => 'CREATE TABLE `groups` (
 				'query' => 'CREATE TABLE `groups` (
-			        `id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
-			        `group`	TEXT UNIQUE,
-			        `group_id`	INTEGER,
-			        `image`	TEXT,
-			        `default` INTEGER
-			    );'
+					`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
+					`group`	TEXT UNIQUE,
+					`group_id`	INTEGER,
+					`image`	TEXT,
+					`default` INTEGER
+				);'
 			),
 			),
 			array(
 			array(
 				'function' => 'query',
 				'function' => 'query',
 				'query' => 'CREATE TABLE `categories` (
 				'query' => 'CREATE TABLE `categories` (
-			        `id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
-			        `order`	INTEGER,
-			        `category`	TEXT UNIQUE,
-			        `category_id`	INTEGER,
-			        `image`	TEXT,
-			        `default` INTEGER
-			    );'
+					`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
+					`order`	INTEGER,
+					`category`	TEXT UNIQUE,
+					`category_id`	INTEGER,
+					`image`	TEXT,
+					`default` INTEGER
+				);'
 			),
 			),
 			array(
 			array(
 				'function' => 'query',
 				'function' => 'query',
 				'query' => 'CREATE TABLE `tabs` (
 				'query' => 'CREATE TABLE `tabs` (
-			        `id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
-			        `order`	INTEGER,
-			        `category_id`	INTEGER,
-			        `name`	TEXT,
-			        `url`	TEXT,
-			        `url_local`	TEXT,
-			        `default`	INTEGER,
-			        `enabled`	INTEGER,
-			        `group_id`	INTEGER,
-			        `image`	TEXT,
-			        `type`	INTEGER,
-			        `splash`	INTEGER,
-			        `ping`		INTEGER,
-			        `ping_url`	TEXT,
-			        `timeout`	INTEGER,
-			        `timeout_ms`	INTEGER,
-			        `preload`	INTEGER
-			    );'
+					`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
+					`order`	INTEGER,
+					`category_id`	INTEGER,
+					`name`	TEXT,
+					`url`	TEXT,
+					`url_local`	TEXT,
+					`default`	INTEGER,
+					`enabled`	INTEGER,
+					`group_id`	INTEGER,
+					`image`	TEXT,
+					`type`	INTEGER,
+					`splash`	INTEGER,
+					`ping`		INTEGER,
+					`ping_url`	TEXT,
+					`timeout`	INTEGER,
+					`timeout_ms`	INTEGER,
+					`preload`	INTEGER
+				);'
 			),
 			),
 			array(
 			array(
 				'function' => 'query',
 				'function' => 'query',
 				'query' => 'CREATE TABLE `options` (
 				'query' => 'CREATE TABLE `options` (
-			        `id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
-			        `name`	TEXT UNIQUE,
-			        `value`	TEXT
-			    );'
+					`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
+					`name`	TEXT UNIQUE,
+					`value`	TEXT
+				);'
 			),
 			),
 			array(
 			array(
 				'function' => 'query',
 				'function' => 'query',
 				'query' => 'CREATE TABLE `invites` (
 				'query' => 'CREATE TABLE `invites` (
-			        `id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
-			        `code`	TEXT UNIQUE,
-			        `date`	TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-			        `email`	TEXT,
-			        `username`	TEXT,
-			        `dateused`	TIMESTAMP,
-			        `usedby`	TEXT,
-			        `ip`	TEXT,
-			        `valid`	TEXT,
-			        `type` TEXT
-			    );'
+					`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
+					`code`	TEXT UNIQUE,
+					`date`	TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+					`email`	TEXT,
+					`username`	TEXT,
+					`dateused`	TIMESTAMP,
+					`usedby`	TEXT,
+					`ip`	TEXT,
+					`valid`	TEXT,
+					`type` TEXT
+				);'
 			),
 			),
 		];
 		];
 		return $this->processQueries($response, $migration);
 		return $this->processQueries($response, $migration);
@@ -2787,11 +2853,11 @@ class Organizr
 		->identifiedBy('4f1g23a12aa', true)// Configures the id (jti claim), replicating as a header item
 		->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)
 		->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)
 		->expiresAt(time() + (86400 * $days))// Configures the expiration time of the token (exp claim)
-		->withClaim('username', $result['username'])// Configures a new claim, called "username"
-		->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('username', $result['username'])// Configures a new claim, called "username"
+		//->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"
 		->withClaim('userID', $result['id'])// Configures a new claim, called "image"
 		->sign($signer, $this->config['organizrHash'])// creates a signature using "testing" as key
 		->sign($signer, $this->config['organizrHash'])// creates a signature using "testing" as key
 		->getToken(); // Retrieves the generated token
 		->getToken(); // Retrieves the generated token
@@ -2964,8 +3030,7 @@ class Organizr
 				if ($createToken) {
 				if ($createToken) {
 					$this->writeLoginLog($username, 'success');
 					$this->writeLoginLog($username, 'success');
 					$this->writeLog('success', 'Login Function - A User has logged in', $username);
 					$this->writeLog('success', 'Login Function - A User has logged in', $username);
-					$ssoUser = ((empty($result['email'])) ? $result['username'] : (strpos($result['email'], 'placeholder') !== false)) ? $result['username'] : $result['email'];
-					$this->ssoCheck($ssoUser, $password, $token); //need to work on this
+					$this->ssoCheck($result, $password, $token); //need to work on this
 					return ($output) ? array('name' => $this->cookieName, 'token' => (string)$createToken) : true;
 					return ($output) ? array('name' => $this->cookieName, 'token' => (string)$createToken) : true;
 				} else {
 				} else {
 					$this->setAPIResponse('error', 'Token creation error', 500);
 					$this->setAPIResponse('error', 'Token creation error', 500);
@@ -2996,6 +3061,8 @@ class Organizr
 		$this->coookie('delete', 'mpt');
 		$this->coookie('delete', 'mpt');
 		$this->coookie('delete', 'Auth');
 		$this->coookie('delete', 'Auth');
 		$this->coookie('delete', 'oAuth');
 		$this->coookie('delete', 'oAuth');
+		$this->coookie('delete', 'jellyfin_credentials');
+		$this->coookie('delete', 'connect.sid');
 		$this->clearTautulliTokens();
 		$this->clearTautulliTokens();
 		$this->revokeTokenCurrentUser($this->user['token']);
 		$this->revokeTokenCurrentUser($this->user['token']);
 		$this->user = null;
 		$this->user = null;
@@ -3421,12 +3488,14 @@ class Organizr
 				'debugArea' => $this->qualifyRequest($this->config['debugAreaAuth']),
 				'debugArea' => $this->qualifyRequest($this->config['debugAreaAuth']),
 				'debugErrors' => $this->config['debugErrors'],
 				'debugErrors' => $this->config['debugErrors'],
 				'sandbox' => $this->config['sandbox'],
 				'sandbox' => $this->config['sandbox'],
+				'expandCategoriesByDefault' => $this->config['expandCategoriesByDefault']
 			),
 			),
 			'menuLink' => array(
 			'menuLink' => array(
 				'githubMenuLink' => $this->config['githubMenuLink'],
 				'githubMenuLink' => $this->config['githubMenuLink'],
 				'organizrSupportMenuLink' => $this->config['organizrSupportMenuLink'],
 				'organizrSupportMenuLink' => $this->config['organizrSupportMenuLink'],
 				'organizrDocsMenuLink' => $this->config['organizrDocsMenuLink'],
 				'organizrDocsMenuLink' => $this->config['organizrDocsMenuLink'],
-				'organizrSignoutMenuLink' => $this->config['organizrSignoutMenuLink']
+				'organizrSignoutMenuLink' => $this->config['organizrSignoutMenuLink'],
+				'organizrFeatureRequestLink' => $this->config['organizrFeatureRequestLink']
 			)
 			)
 		);
 		);
 	}
 	}
@@ -3528,7 +3597,7 @@ class Organizr
 		file_put_contents($this->organizrLoginLog, $writeFailLog);
 		file_put_contents($this->organizrLoginLog, $writeFailLog);
 	}
 	}
 	
 	
-	public function writeLog($type = 'error', $message, $username = null)
+	public function writeLog($type = 'error', $message = null, $username = null)
 	{
 	{
 		$this->timeExecution = $this->timeExecution($this->timeExecution);
 		$this->timeExecution = $this->timeExecution($this->timeExecution);
 		$message = $message . ' [Execution Time: ' . $this->formatSeconds($this->timeExecution) . ']';
 		$message = $message . ' [Execution Time: ' . $this->formatSeconds($this->timeExecution) . ']';
@@ -4752,7 +4821,7 @@ class Organizr
 	public function getThemesGithub()
 	public function getThemesGithub()
 	{
 	{
 		$url = 'https://raw.githubusercontent.com/causefx/Organizr/v2-themes/themes.json';
 		$url = 'https://raw.githubusercontent.com/causefx/Organizr/v2-themes/themes.json';
-		$options = (localURL($url)) ? array('verify' => false) : array();
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
 		$response = Requests::get($url, array(), $options);
 		$response = Requests::get($url, array(), $options);
 		if ($response->success) {
 		if ($response->success) {
 			return json_decode($response->body, true);
 			return json_decode($response->body, true);
@@ -4763,7 +4832,7 @@ class Organizr
 	public function getPluginsGithub()
 	public function getPluginsGithub()
 	{
 	{
 		$url = 'https://raw.githubusercontent.com/causefx/Organizr/v2-plugins/plugins.json';
 		$url = 'https://raw.githubusercontent.com/causefx/Organizr/v2-plugins/plugins.json';
-		$options = (localURL($url)) ? array('verify' => false) : array();
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
 		$response = Requests::get($url, array(), $options);
 		$response = Requests::get($url, array(), $options);
 		if ($response->success) {
 		if ($response->success) {
 			return json_decode($response->body, true);
 			return json_decode($response->body, true);
@@ -4774,7 +4843,7 @@ class Organizr
 	public function getOpenCollectiveBackers()
 	public function getOpenCollectiveBackers()
 	{
 	{
 		$url = 'https://opencollective.com/organizr/members/users.json?limit=100&offset=0';
 		$url = 'https://opencollective.com/organizr/members/users.json?limit=100&offset=0';
-		$options = (localURL($url)) ? array('verify' => false) : array();
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
 		$response = Requests::get($url, array(), $options);
 		$response = Requests::get($url, array(), $options);
 		if ($response->success) {
 		if ($response->success) {
 			$api = json_decode($response->body, true);
 			$api = json_decode($response->body, true);
@@ -4785,6 +4854,29 @@ class Organizr
 		return false;
 		return false;
 	}
 	}
 	
 	
+	public function getOrganizrSmtpFromAPI()
+	{
+		$url = 'https://api.organizr.app/?cmd=smtp';
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		$response = Requests::get($url, array(), $options);
+		if ($response->success) {
+			return json_decode($response->body, true);
+		}
+		return false;
+	}
+	
+	public function saveOrganizrSmtpFromAPI()
+	{
+		$api = $this->getOrganizrSmtpFromAPI();
+		if ($api) {
+			$this->updateConfigItems($api['response']['data']);
+			$this->setAPIResponse(null, 'SMTP activated with Organizr SMTP account');
+			return true;
+		} else {
+			return false;
+		}
+	}
+	
 	public function guestHash($start, $end)
 	public function guestHash($start, $end)
 	{
 	{
 		$ip = $_SERVER['REMOTE_ADDR'];
 		$ip = $_SERVER['REMOTE_ADDR'];
@@ -5597,9 +5689,9 @@ class Organizr
 			$response = Requests::post($url, $headers, $data, array());
 			$response = Requests::post($url, $headers, $data, array());
 			$json = json_decode($response->body, true);
 			$json = json_decode($response->body, true);
 			$errors = !empty($json['errors']);
 			$errors = !empty($json['errors']);
-			$success = !empty($json['user']);
+			$success = empty($json['errors']);
 			//Use This for later
 			//Use This for later
-			$errorMessage = "";
+			$errorMessage = '';
 			if ($errors) {
 			if ($errors) {
 				foreach ($json['errors'] as $error) {
 				foreach ($json['errors'] as $error) {
 					if (isset($error['message']) && isset($error['field'])) {
 					if (isset($error['message']) && isset($error['field'])) {
@@ -5923,4 +6015,4 @@ class Organizr
 		return count($request) > 1 ? $results : $results[$firstKey];
 		return count($request) > 1 ? $results : $results[$firstKey];
 	}
 	}
 	
 	
-}
+}

+ 15 - 4
api/config/default.php

@@ -13,8 +13,8 @@ return array(
 	'authBaseDN' => '',
 	'authBaseDN' => '',
 	'authBackendDomain' => '',
 	'authBackendDomain' => '',
 	'ldapType' => '1',
 	'ldapType' => '1',
-	'logo' => 'plugins/images/organizr/logo-wide.png',
-	'loginLogo' => 'plugins/images/organizr/logo-wide.png',
+	'logo' => 'plugins/images/organizr/organizr-logo-h.png',
+	'loginLogo' => 'plugins/images/organizr/organizr-logo-h.png',
 	'loginWallpaper' => '',
 	'loginWallpaper' => '',
 	'title' => 'Organizr V2',
 	'title' => 'Organizr V2',
 	'useLogo' => false,
 	'useLogo' => false,
@@ -53,10 +53,15 @@ return array(
 	'ombiAlias' => false,
 	'ombiAlias' => false,
 	'ombiFallbackUser' => '',
 	'ombiFallbackUser' => '',
 	'ombiFallbackPassword' => '',
 	'ombiFallbackPassword' => '',
+	'overseerrURL' => '',
+	'overseerrToken' => '',
+	'overseerrFallbackUser' => '',
+	'overseerrFallbackPassword' => '',
 	'ssoPlex' => false,
 	'ssoPlex' => false,
 	'ssoOmbi' => false,
 	'ssoOmbi' => false,
 	'ssoTautulli' => false,
 	'ssoTautulli' => false,
 	'ssoJellyfin' => false,
 	'ssoJellyfin' => false,
+	'ssoOverseerr' => false,
 	'sonarrURL' => '',
 	'sonarrURL' => '',
 	'sonarrUnmonitored' => false,
 	'sonarrUnmonitored' => false,
 	'sonarrToken' => '',
 	'sonarrToken' => '',
@@ -240,10 +245,12 @@ return array(
 	'homepageEmbyStreamsAuth' => '1',
 	'homepageEmbyStreamsAuth' => '1',
 	'homepageEmbyRecent' => false,
 	'homepageEmbyRecent' => false,
 	'homepageEmbyRecentAuth' => '1',
 	'homepageEmbyRecentAuth' => '1',
+	'homepageEmbyLink' => ' http://app.emby.media/#!/item/item.html?id={id}&serverId={serverId}',
 	'homepageJellyfinStreams' => false,
 	'homepageJellyfinStreams' => false,
 	'homepageJellyStreamsAuth' => '1',
 	'homepageJellyStreamsAuth' => '1',
 	'homepageJellyfinRecent' => false,
 	'homepageJellyfinRecent' => false,
 	'homepageJellyfinRecentAuth' => '1',
 	'homepageJellyfinRecentAuth' => '1',
+	'homepageJellyfinLink' => 'http://hostname:port/jellyfin/web/index.html#!/details?id={id}&serverId={serverId}',
 	'calendarDefault' => 'month',
 	'calendarDefault' => 'month',
 	'calendarFirstDay' => '1',
 	'calendarFirstDay' => '1',
 	'calendarStart' => '14',
 	'calendarStart' => '14',
@@ -446,5 +453,9 @@ return array(
 	'organizrSupportMenuLink' => true,
 	'organizrSupportMenuLink' => true,
 	'organizrDocsMenuLink' => true,
 	'organizrDocsMenuLink' => true,
 	'organizrSignoutMenuLink' => true,
 	'organizrSignoutMenuLink' => true,
-	'breezometerToken' => 'd95ab607392d4fa5bf64bb26a5cb2a06'
-);
+	'organizrFeatureRequestLink' => true,
+	'breezometerToken' => 'd95ab607392d4fa5bf64bb26a5cb2a06',
+	'customForgotPasswordText' => '',
+	'disableRecoverPassword' => false,
+	'expandCategoriesByDefault' => false
+);

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

@@ -352,7 +352,7 @@ trait NormalFunctions
 		}
 		}
 	}
 	}
 	
 	
-	public function coookieSeconds($type, $name, $value = '', $ms, $http = true, $path = '/')
+	public function coookieSeconds($type, $name, $value = '', $ms = null, $http = true, $path = '/')
 	{
 	{
 		if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == "https") {
 		if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == "https") {
 			$Secure = true;
 			$Secure = true;

+ 19 - 0
api/functions/organizr-functions.php

@@ -600,4 +600,23 @@ trait OrganizrFunctions
 		}
 		}
 		return $approved;
 		return $approved;
 	}
 	}
+	
+	public function userDefinedIdReplacementLink($link, $variables)
+	{
+		return strtr($link, $variables);
+	}
+	
+	public function requestOptions($url, $override = false, $timeout = null)
+	{
+		$options = [];
+		if (is_numeric($timeout)) {
+			$timeout = $timeout / 1000;
+			array_push($options, array('timeout' => $timeout));
+		}
+		if ($this->localURL($url, $override)) {
+			array_push($options, array('verify' => false));
+			
+		}
+		return $options;
+	}
 }
 }

+ 66 - 13
api/functions/sso-functions.php

@@ -2,21 +2,31 @@
 
 
 trait SSOFunctions
 trait SSOFunctions
 {
 {
+	public function getSSOUserFor($app, $userobj)
+	{
+		$map = array(
+			'jellyfin' => 'username',
+			'ombi' => 'username',
+			'overseerr' => 'username',
+			'tautulli' => 'username'
+		);
+		return $userobj[$map[$app]];
+	}
 	
 	
-	public function ssoCheck($username, $password, $token = null)
+	public function ssoCheck($userobj, $password, $token = null)
 	{
 	{
 		if ($this->config['ssoPlex'] && $token) {
 		if ($this->config['ssoPlex'] && $token) {
 			$this->coookie('set', 'mpt', $token, $this->config['rememberMeDays'], false);
 			$this->coookie('set', 'mpt', $token, $this->config['rememberMeDays'], false);
 		}
 		}
 		if ($this->config['ssoOmbi']) {
 		if ($this->config['ssoOmbi']) {
 			$fallback = ($this->config['ombiFallbackUser'] !== '' && $this->config['ombiFallbackPassword'] !== '');
 			$fallback = ($this->config['ombiFallbackUser'] !== '' && $this->config['ombiFallbackPassword'] !== '');
-			$ombiToken = $this->getOmbiToken($username, $password, $token, $fallback);
+			$ombiToken = $this->getOmbiToken($this->getSSOUserFor('ombi', $userobj), $password, $token, $fallback);
 			if ($ombiToken) {
 			if ($ombiToken) {
 				$this->coookie('set', 'Auth', $ombiToken, $this->config['rememberMeDays'], false);
 				$this->coookie('set', 'Auth', $ombiToken, $this->config['rememberMeDays'], false);
 			}
 			}
 		}
 		}
 		if ($this->config['ssoTautulli']) {
 		if ($this->config['ssoTautulli']) {
-			$tautulliToken = $this->getTautulliToken($username, $password, $token);
+			$tautulliToken = $this->getTautulliToken($this->getSSOUserFor('tautulli', $userobj), $password, $token);
 			if ($tautulliToken) {
 			if ($tautulliToken) {
 				foreach ($tautulliToken as $key => $value) {
 				foreach ($tautulliToken as $key => $value) {
 					$this->coookie('set', 'tautulli_token_' . $value['uuid'], $value['token'], $this->config['rememberMeDays'], true, $value['path']);
 					$this->coookie('set', 'tautulli_token_' . $value['uuid'], $value['token'], $this->config['rememberMeDays'], true, $value['path']);
@@ -24,15 +34,20 @@ trait SSOFunctions
 			}
 			}
 		}
 		}
 		if ($this->config['ssoJellyfin']) {
 		if ($this->config['ssoJellyfin']) {
-			$jellyfinToken = $this->getJellyfinToken($username, $password);
+			$jellyfinToken = $this->getJellyfinToken($this->getSSOUserFor('jellyfin', $userobj), $password);
 			if ($jellyfinToken) {
 			if ($jellyfinToken) {
 				$this->coookie('set', 'jellyfin_credentials', $jellyfinToken, $this->config['rememberMeDays'], false);
 				$this->coookie('set', 'jellyfin_credentials', $jellyfinToken, $this->config['rememberMeDays'], false);
-				$this->writeLog('success', 'ITTATOKEN: ' . $jellyfinToken);
+			}
+		}
+		if ($this->config['ssoOverseerr']) {
+			$overseerrToken = $this->getOverseerrToken($this->getSSOUserFor('overseerr', $userobj), $password, $token);
+			if ($overseerrToken) {
+				$this->coookie('set', 'connect.sid', $overseerrToken, $this->config['rememberMeDays'], false);
 			}
 			}
 		}
 		}
 		return true;
 		return true;
 	}
 	}
-
+	
 	public function getJellyfinToken($username, $password)
 	public function getJellyfinToken($username, $password)
 	{
 	{
 		$token = null;
 		$token = null;
@@ -48,7 +63,7 @@ trait SSOFunctions
 				"Pw" => $password
 				"Pw" => $password
 			);
 			);
 			$endpoint = '/Users/authenticatebyname';
 			$endpoint = '/Users/authenticatebyname';
-			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$options = $this->requestOptions($url, false, 60);
 			$response = Requests::post($url . $endpoint, $headers, json_encode($data), $options);
 			$response = Requests::post($url . $endpoint, $headers, json_encode($data), $options);
 			if ($response->success) {
 			if ($response->success) {
 				$token = json_decode($response->body, true);
 				$token = json_decode($response->body, true);
@@ -59,10 +74,9 @@ trait SSOFunctions
 		} catch (Requests_Exception $e) {
 		} catch (Requests_Exception $e) {
 			$this->writeLog('error', 'Jellyfin Token Function - Error: ' . $e->getMessage(), $username);
 			$this->writeLog('error', 'Jellyfin Token Function - Error: ' . $e->getMessage(), $username);
 		}
 		}
-		
-		return '{"Servers":[{"ManualAddress":"'. $url . '","Id":"' . $token['ServerId'] . '","UserId":"' . $token['User']['Id'] . '","AccessToken":"' . $token['AccessToken'] . '"}]}';
+		return '{"Servers":[{"ManualAddress":"' . $url . '","Id":"' . $token['ServerId'] . '","UserId":"' . $token['User']['Id'] . '","AccessToken":"' . $token['AccessToken'] . '"}]}';
 	}
 	}
-
+	
 	public function getOmbiToken($username, $password, $oAuthToken = null, $fallback = false)
 	public function getOmbiToken($username, $password, $oAuthToken = null, $fallback = false)
 	{
 	{
 		$token = null;
 		$token = null;
@@ -79,7 +93,7 @@ trait SSOFunctions
 				"plexToken" => $oAuthToken
 				"plexToken" => $oAuthToken
 			);
 			);
 			$endpoint = ($oAuthToken) ? '/api/v1/Token/plextoken' : '/api/v1/Token';
 			$endpoint = ($oAuthToken) ? '/api/v1/Token/plextoken' : '/api/v1/Token';
-			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$options = $this->requestOptions($url, false, 60);
 			$response = Requests::post($url . $endpoint, $headers, json_encode($data), $options);
 			$response = Requests::post($url . $endpoint, $headers, json_encode($data), $options);
 			if ($response->success) {
 			if ($response->success) {
 				$token = json_decode($response->body, true)['access_token'];
 				$token = json_decode($response->body, true)['access_token'];
@@ -122,7 +136,7 @@ trait SSOFunctions
 						"token" => $plexToken,
 						"token" => $plexToken,
 						"remember_me" => 1,
 						"remember_me" => 1,
 					);
 					);
-					$options = ($this->localURL($url)) ? array('verify' => false) : array();
+					$options = $this->requestOptions($url, false, 60);
 					$response = Requests::post($url . '/auth/signin', $headers, $data, $options);
 					$response = Requests::post($url . '/auth/signin', $headers, $data, $options);
 					if ($response->success) {
 					if ($response->success) {
 						$qualifiedURL = $this->qualifyURL($url, true);
 						$qualifiedURL = $this->qualifyURL($url, true);
@@ -142,4 +156,43 @@ trait SSOFunctions
 		return ($token) ? $token : false;
 		return ($token) ? $token : false;
 	}
 	}
 	
 	
-}
+	public function getOverseerrToken($username, $password, $oAuthToken = null, $fallback = false)
+	{
+		$token = null;
+		try {
+			$url = $this->qualifyURL($this->config['overseerrURL']);
+			$headers = array(
+				"Content-Type" => "application/json"
+			);
+			$data = array(
+				//"username" => ($oAuthToken ? "" : $username), // not needed yet
+				//"password" => ($oAuthToken ? "" : $password), // not needed yet
+				"authToken" => $oAuthToken
+			);
+			$endpoint = '/api/v1/auth/login';
+			$options = $this->requestOptions($url, false, 60);
+			$response = Requests::post($url . $endpoint, $headers, json_encode($data), $options);
+			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['username']);
+			} else {
+				if ($fallback) {
+					$this->writeLog('error', 'Overseerr Token Function - Overseerr did not return Token - Will retry using fallback credentials', $username);
+				} else {
+					$this->writeLog('error', 'Overseerr Token Function - Overseerr did not return Token', $username);
+				}
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Overseerr Token Function - Error: ' . $e->getMessage(), $username);
+		}
+		if ($token) {
+			return urldecode($token);
+		} elseif ($fallback) {
+			return $this->getOverseerrToken($this->config['overseerrFallbackUser'], $this->decrypt($this->config['overseerrFallbackPassword']), null, false);
+		} else {
+			return false;
+		}
+	}
+	
+}

+ 6 - 6
api/functions/token-functions.php

@@ -25,15 +25,15 @@ trait TokenFunctions
 				$data->setAudience('Organizr');
 				$data->setAudience('Organizr');
 				if ($jwttoken->validate($data)) {
 				if ($jwttoken->validate($data)) {
 					$result['valid'] = true;
 					$result['valid'] = true;
-					$result['username'] = $jwttoken->getClaim('username');
-					$result['group'] = $jwttoken->getClaim('group');
-					$result['groupID'] = $jwttoken->getClaim('groupID');
+					//$result['username'] = $jwttoken->getClaim('username');
+					//$result['group'] = $jwttoken->getClaim('group');
+					//$result['groupID'] = $jwttoken->getClaim('groupID');
 					$result['userID'] = $jwttoken->getClaim('userID');
 					$result['userID'] = $jwttoken->getClaim('userID');
-					$result['email'] = $jwttoken->getClaim('email');
-					$result['image'] = $jwttoken->getClaim('image');
+					//$result['email'] = $jwttoken->getClaim('email');
+					//$result['image'] = $jwttoken->getClaim('image');
 					$result['tokenExpire'] = $jwttoken->getClaim('exp');
 					$result['tokenExpire'] = $jwttoken->getClaim('exp');
 					$result['tokenDate'] = $jwttoken->getClaim('iat');
 					$result['tokenDate'] = $jwttoken->getClaim('iat');
-					$result['token'] = $jwttoken->getClaim('exp');
+					//$result['token'] = $jwttoken->getClaim('exp');
 				}
 				}
 			}
 			}
 			if ($result['valid'] == true) {
 			if ($result['valid'] == true) {

+ 12 - 2
api/homepage/emby.php

@@ -105,6 +105,13 @@ trait EmbyHomepageItem
 					),
 					),
 				),
 				),
 				'Misc Options' => array(
 				'Misc Options' => array(
+					array(
+						'type' => 'input',
+						'name' => 'homepageEmbyLink',
+						'label' => 'Emby Homepage Link URL',
+						'value' => $this->config['homepageEmbyLink'],
+						'help' => 'Available variables: {id} {serverId}'
+					),
 					array(
 					array(
 						'type' => 'input',
 						'type' => 'input',
 						'name' => 'embyTabName',
 						'name' => 'embyTabName',
@@ -538,8 +545,11 @@ trait EmbyHomepageItem
 		$embyItem['user'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? @(string)$itemDetails['UserName'] : "";
 		$embyItem['user'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? @(string)$itemDetails['UserName'] : "";
 		$embyItem['userThumb'] = '';
 		$embyItem['userThumb'] = '';
 		$embyItem['userAddress'] = (isset($itemDetails['RemoteEndPoint']) ? $itemDetails['RemoteEndPoint'] : "x.x.x.x");
 		$embyItem['userAddress'] = (isset($itemDetails['RemoteEndPoint']) ? $itemDetails['RemoteEndPoint'] : "x.x.x.x");
-		$embyURL = 'https://app.emby.media/#!/item/item.html?id=';
-		$embyItem['address'] = $this->config['embyTabURL'] ? rtrim($this->config['embyTabURL'], '/') . "/web/#!/item/item.html?id=" . $embyItem['uid'] : $embyURL . $embyItem['uid'] . "&serverId=" . $embyItem['id'];
+		$embyVariablesForLink = [
+			'{id}' => $embyItem['uid'],
+			'{serverId}' => $embyItem['id']
+		];
+		$embyItem['address'] = $this->userDefinedIdReplacementLink($this->config['homepageEmbyLink'], $embyVariablesForLink);
 		$embyItem['nowPlayingOriginalImage'] = 'api/v2/homepage/image?source=emby&type=' . $embyItem['nowPlayingImageType'] . '&img=' . $embyItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $embyItem['nowPlayingKey'] . '$' . $this->randString();
 		$embyItem['nowPlayingOriginalImage'] = 'api/v2/homepage/image?source=emby&type=' . $embyItem['nowPlayingImageType'] . '&img=' . $embyItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $embyItem['nowPlayingKey'] . '$' . $this->randString();
 		$embyItem['originalImage'] = 'api/v2/homepage/image?source=emby&type=' . $embyItem['imageType'] . '&img=' . $embyItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $embyItem['key'] . '$' . $this->randString();
 		$embyItem['originalImage'] = 'api/v2/homepage/image?source=emby&type=' . $embyItem['imageType'] . '&img=' . $embyItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $embyItem['key'] . '$' . $this->randString();
 		$embyItem['openTab'] = $this->config['embyTabURL'] && $this->config['embyTabName'] ? true : false;
 		$embyItem['openTab'] = $this->config['embyTabURL'] && $this->config['embyTabName'] ? true : false;

+ 2 - 2
api/homepage/jdownloader.php

@@ -95,7 +95,7 @@ trait JDownloaderHomepageItem
 		}
 		}
 		$url = $this->qualifyURL($this->config['jdownloaderURL']);
 		$url = $this->qualifyURL($this->config['jdownloaderURL']);
 		try {
 		try {
-			$options = ($this->localURL($url)) ? array('verify' => false, 'timeout' => 30) : array('timeout' => 30);
+			$options = $this->requestOptions($this->config['jdownloaderURL'], false, $this->config['homepageDownloadRefresh']);
 			$response = Requests::get($url, array(), $options);
 			$response = Requests::get($url, array(), $options);
 			if ($response->success) {
 			if ($response->success) {
 				$this->setAPIResponse('success', 'API Connection succeeded', 200);
 				$this->setAPIResponse('success', 'API Connection succeeded', 200);
@@ -161,7 +161,7 @@ trait JDownloaderHomepageItem
 		}
 		}
 		$url = $this->qualifyURL($this->config['jdownloaderURL']);
 		$url = $this->qualifyURL($this->config['jdownloaderURL']);
 		try {
 		try {
-			$options = ($this->localURL($url)) ? array('verify' => false, 'timeout' => 30) : array('timeout' => 30);
+			$options = $this->requestOptions($this->config['jdownloaderURL'], false, $this->config['homepageDownloadRefresh']);
 			$response = Requests::get($url, array(), $options);
 			$response = Requests::get($url, array(), $options);
 			if ($response->success) {
 			if ($response->success) {
 				$temp = json_decode($response->body, true);
 				$temp = json_decode($response->body, true);

+ 12 - 2
api/homepage/jellyfin.php

@@ -106,6 +106,13 @@ trait JellyfinHomepageItem
 					),
 					),
 				),
 				),
 				'Misc Options' => array(
 				'Misc Options' => array(
+					array(
+						'type' => 'input',
+						'name' => 'homepageJellyfinLink',
+						'label' => 'Jellyfin Homepage Link URL',
+						'value' => $this->config['homepageJellyfinLink'],
+						'help' => 'Available variables: {id} {serverId}'
+					),
 					array(
 					array(
 						'type' => 'input',
 						'type' => 'input',
 						'name' => 'jellyfinTabName',
 						'name' => 'jellyfinTabName',
@@ -539,8 +546,11 @@ trait JellyfinHomepageItem
 		$jellyfinItem['user'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? @(string)$itemDetails['UserName'] : "";
 		$jellyfinItem['user'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? @(string)$itemDetails['UserName'] : "";
 		$jellyfinItem['userThumb'] = '';
 		$jellyfinItem['userThumb'] = '';
 		$jellyfinItem['userAddress'] = (isset($itemDetails['RemoteEndPoint']) ? $itemDetails['RemoteEndPoint'] : "x.x.x.x");
 		$jellyfinItem['userAddress'] = (isset($itemDetails['RemoteEndPoint']) ? $itemDetails['RemoteEndPoint'] : "x.x.x.x");
-		$jellyfinURL = $this->config['jellyfinURL'] . '/web/index.html#!/details?id=';
-		$jellyfinItem['address'] = $this->config['jellyfinTabURL'] ? rtrim($this->config['jellyfinTabURL'], '/') . "/web/#!/item/item.html?id=" . $jellyfinItem['uid'] : $jellyfinURL . $jellyfinItem['uid'] . "&serverId=" . $jellyfinItem['id'];
+		$jellfinVariablesForLink = [
+			'{id}' => $jellyfinItem['uid'],
+			'{serverId}' => $jellyfinItem['id']
+		];
+		$jellyfinItem['address'] = $this->userDefinedIdReplacementLink($this->config['homepageJellyfinLink'], $jellfinVariablesForLink);
 		$jellyfinItem['nowPlayingOriginalImage'] = 'api/v2/homepage/image?source=jellyfin&type=' . $jellyfinItem['nowPlayingImageType'] . '&img=' . $jellyfinItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $jellyfinItem['nowPlayingKey'] . '$' . $this->randString();
 		$jellyfinItem['nowPlayingOriginalImage'] = 'api/v2/homepage/image?source=jellyfin&type=' . $jellyfinItem['nowPlayingImageType'] . '&img=' . $jellyfinItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $jellyfinItem['nowPlayingKey'] . '$' . $this->randString();
 		$jellyfinItem['originalImage'] = 'api/v2/homepage/image?source=jellyfin&type=' . $jellyfinItem['imageType'] . '&img=' . $jellyfinItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $jellyfinItem['key'] . '$' . $this->randString();
 		$jellyfinItem['originalImage'] = 'api/v2/homepage/image?source=jellyfin&type=' . $jellyfinItem['imageType'] . '&img=' . $jellyfinItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $jellyfinItem['key'] . '$' . $this->randString();
 		$jellyfinItem['openTab'] = $this->config['jellyfinTabURL'] && $this->config['jellyfinTabName'] ? true : false;
 		$jellyfinItem['openTab'] = $this->config['jellyfinTabURL'] && $this->config['jellyfinTabName'] ? true : false;

+ 18 - 17
api/homepage/monitorr.php

@@ -118,7 +118,8 @@ trait MonitorrHomepageItem
 		$url = $this->qualifyURL($this->config['monitorrURL']);
 		$url = $this->qualifyURL($this->config['monitorrURL']);
 		$dataUrl = $url . '/assets/php/loop.php';
 		$dataUrl = $url . '/assets/php/loop.php';
 		try {
 		try {
-			$response = Requests::get($dataUrl, ['Token' => $this->config['organizrAPI']], []);
+			$options = $this->requestOptions($this->config['monitorrURL'], false, $this->config['homepageMonitorrRefresh']);
+			$response = Requests::get($dataUrl, ['Token' => $this->config['organizrAPI']], $options);
 			if ($response->success) {
 			if ($response->success) {
 				$html = html_entity_decode($response->body);
 				$html = html_entity_decode($response->body);
 				// This section grabs the names of all services by regex
 				// This section grabs the names of all services by regex
@@ -127,25 +128,24 @@ trait MonitorrHomepageItem
 				$servicePattern = '/<div id="servicetitle(?:offline|nolink)?".*><div>(.*)<\/div><\/div><div class="(?:btnonline|btnoffline|btnunknown)".*>(Online|Offline|Unresponsive)<\/div>(:?<\/a>)?<\/div><\/div>/';
 				$servicePattern = '/<div id="servicetitle(?:offline|nolink)?".*><div>(.*)<\/div><\/div><div class="(?:btnonline|btnoffline|btnunknown)".*>(Online|Offline|Unresponsive)<\/div>(:?<\/a>)?<\/div><\/div>/';
 				preg_match_all($servicePattern, $html, $servicesMatch);
 				preg_match_all($servicePattern, $html, $servicesMatch);
 				$services = array_filter($servicesMatch[1]);
 				$services = array_filter($servicesMatch[1]);
-                $status = array_filter($servicesMatch[2]);
+				$status = array_filter($servicesMatch[2]);
 				$statuses = [];
 				$statuses = [];
 				foreach ($services as $key => $service) {
 				foreach ($services as $key => $service) {
 					$match = $status[$key];
 					$match = $status[$key];
 					$statuses[$service] = $match;
 					$statuses[$service] = $match;
-                    if ($match == 'Online') {
-                        $statuses[$service] = [
-                            'status' => true
-                        ];
-                    } else if ($match == 'Offline') {
-                        $statuses[$service] = [
-                            'status' => false
-                        ];
-                    } else if ($match == 'Unresponsive') {
-                        $statuses[$service] = [
-                            'status' => 'unresponsive'
-                        ];
-                    }
-
+					if ($match == 'Online') {
+						$statuses[$service] = [
+							'status' => true
+						];
+					} else if ($match == 'Offline') {
+						$statuses[$service] = [
+							'status' => false
+						];
+					} else if ($match == 'Unresponsive') {
+						$statuses[$service] = [
+							'status' => 'unresponsive'
+						];
+					}
 					$statuses[$service]['sort'] = $key;
 					$statuses[$service]['sort'] = $key;
 					$imageMatch = [];
 					$imageMatch = [];
 					$imgPattern = '/assets\/img\/\.\.(.*)" class="serviceimg" alt=.*><\/div><\/div><div id="servicetitle"><div>' . $service . '|assets\/img\/\.\.(.*)" class="serviceimg imgoffline" alt=.*><\/div><\/div><div id="servicetitleoffline".*><div>' . $service . '|assets\/img\/\.\.(.*)" class="serviceimg" alt=.*><\/div><\/div><div id="servicetitlenolink".*><div>' . $service . '/';
 					$imgPattern = '/assets\/img\/\.\.(.*)" class="serviceimg" alt=.*><\/div><\/div><div id="servicetitle"><div>' . $service . '|assets\/img\/\.\.(.*)" class="serviceimg imgoffline" alt=.*><\/div><\/div><div id="servicetitleoffline".*><div>' . $service . '|assets\/img\/\.\.(.*)" class="serviceimg" alt=.*><\/div><\/div><div id="servicetitlenolink".*><div>' . $service . '/';
@@ -162,7 +162,8 @@ trait MonitorrHomepageItem
 					$ext = $ext[key(array_slice($ext, -1, 1, true))];
 					$ext = $ext[key(array_slice($ext, -1, 1, true))];
 					$imageUrl = $url . '/assets' . $image;
 					$imageUrl = $url . '/assets' . $image;
 					$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
 					$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
-					$img = Requests::get($imageUrl, ['Token' => $this->config['organizrAPI']], []);
+					$options = $this->requestOptions($this->config['monitorrURL'], false, $this->config['homepageMonitorrRefresh']);
+					$img = Requests::get($imageUrl, ['Token' => $this->config['organizrAPI']], $options);
 					if ($img->success) {
 					if ($img->success) {
 						$base64 = 'data:image/' . $ext . ';base64,' . base64_encode($img->body);
 						$base64 = 'data:image/' . $ext . ';base64,' . base64_encode($img->body);
 						$statuses[$service]['image'] = $base64;
 						$statuses[$service]['image'] = $base64;

+ 11 - 8
api/homepage/tautulli.php

@@ -164,7 +164,7 @@ trait TautulliHomepageItem
 			)
 			)
 		);
 		);
 	}
 	}
-
+	
 	public function testConnectionTautulli()
 	public function testConnectionTautulli()
 	{
 	{
 		if (empty($this->config['tautulliURL'])) {
 		if (empty($this->config['tautulliURL'])) {
@@ -179,7 +179,8 @@ trait TautulliHomepageItem
 		$apiURL = $url . '/api/v2?apikey=' . $this->config['tautulliApikey'];
 		$apiURL = $url . '/api/v2?apikey=' . $this->config['tautulliApikey'];
 		try {
 		try {
 			$homestatsUrl = $apiURL . '&cmd=get_home_stats&grouping=1';
 			$homestatsUrl = $apiURL . '&cmd=get_home_stats&grouping=1';
-			$homestats = Requests::get($homestatsUrl, [], []);
+			$options = $this->requestOptions($this->config['tautulliURL'], false, $this->config['homepageTautulliRefresh']);
+			$homestats = Requests::get($homestatsUrl, [], $options);
 			if ($homestats->success) {
 			if ($homestats->success) {
 				$this->setAPIResponse('success', 'API Connection succeeded', 200);
 				$this->setAPIResponse('success', 'API Connection succeeded', 200);
 				return true;
 				return true;
@@ -193,7 +194,7 @@ trait TautulliHomepageItem
 			return false;
 			return false;
 		}
 		}
 	}
 	}
-
+	
 	public function tautulliHomepagePermissions($key = null)
 	public function tautulliHomepagePermissions($key = null)
 	{
 	{
 		$permissions = [
 		$permissions = [
@@ -218,7 +219,7 @@ trait TautulliHomepageItem
 			return [];
 			return [];
 		}
 		}
 	}
 	}
-
+	
 	public function homepageOrdertautulli()
 	public function homepageOrdertautulli()
 	{
 	{
 		if ($this->homepageItemPermissions($this->tautulliHomepagePermissions('main'))) {
 		if ($this->homepageItemPermissions($this->tautulliHomepagePermissions('main'))) {
@@ -234,7 +235,7 @@ trait TautulliHomepageItem
 				';
 				';
 		}
 		}
 	}
 	}
-
+	
 	public function getTautulliHomepageData()
 	public function getTautulliHomepageData()
 	{
 	{
 		if (!$this->homepageItemPermissions($this->tautulliHomepagePermissions('main'), true)) {
 		if (!$this->homepageItemPermissions($this->tautulliHomepagePermissions('main'), true)) {
@@ -249,7 +250,8 @@ trait TautulliHomepageItem
 		$nowPlayingWidth = $this->getCacheImageSize('npw');
 		$nowPlayingWidth = $this->getCacheImageSize('npw');
 		try {
 		try {
 			$homestatsUrl = $apiURL . '&cmd=get_home_stats&grouping=1';
 			$homestatsUrl = $apiURL . '&cmd=get_home_stats&grouping=1';
-			$homestats = Requests::get($homestatsUrl, [], []);
+			$options = $this->requestOptions($this->config['tautulliURL'], false, $this->config['homepageTautulliRefresh']);
+			$homestats = Requests::get($homestatsUrl, [], $options);
 			if ($homestats->success) {
 			if ($homestats->success) {
 				$homestats = json_decode($homestats->body, true);
 				$homestats = json_decode($homestats->body, true);
 				$api['homestats'] = $homestats['response'];
 				$api['homestats'] = $homestats['response'];
@@ -270,7 +272,8 @@ trait TautulliHomepageItem
 				$this->cacheImage($url . '/images/platforms/' . $platform . '.svg', 'tautulli-' . $platform, 'svg');
 				$this->cacheImage($url . '/images/platforms/' . $platform . '.svg', 'tautulli-' . $platform, 'svg');
 			}
 			}
 			$libstatsUrl = $apiURL . '&cmd=get_libraries';
 			$libstatsUrl = $apiURL . '&cmd=get_libraries';
-			$libstats = Requests::get($libstatsUrl, [], []);
+			$options = $this->requestOptions($this->config['tautulliURL'], false, $this->config['homepageTautulliRefresh']);
+			$libstats = Requests::get($libstatsUrl, [], $options);
 			if ($libstats->success) {
 			if ($libstats->success) {
 				$libstats = json_decode($libstats->body, true);
 				$libstats = json_decode($libstats->body, true);
 				$api['libstats'] = $libstats['response'];
 				$api['libstats'] = $libstats['response'];
@@ -327,4 +330,4 @@ trait TautulliHomepageItem
 		$this->setAPIResponse('success', null, 200, $api);
 		$this->setAPIResponse('success', null, 200, $api);
 		return $api;
 		return $api;
 	}
 	}
-}
+}

+ 9 - 9
api/homepage/unifi.php

@@ -5,7 +5,7 @@ trait UnifiHomepageItem
 	public function unifiSettingsArray()
 	public function unifiSettingsArray()
 	{
 	{
 		return array(
 		return array(
-			'name' => 'Unifi',
+			'name' => 'UniFi',
 			'enabled' => true,
 			'enabled' => true,
 			'image' => 'plugins/images/tabs/unifi.png',
 			'image' => 'plugins/images/tabs/unifi.png',
 			'category' => 'Monitor',
 			'category' => 'Monitor',
@@ -92,7 +92,7 @@ trait UnifiHomepageItem
 			)
 			)
 		);
 		);
 	}
 	}
-	
+
 	public function unifiHomepagePermissions($key = null)
 	public function unifiHomepagePermissions($key = null)
 	{
 	{
 		$permissions = [
 		$permissions = [
@@ -118,7 +118,7 @@ trait UnifiHomepageItem
 			return [];
 			return [];
 		}
 		}
 	}
 	}
-	
+
 	public function homepageOrderunifi()
 	public function homepageOrderunifi()
 	{
 	{
 		if ($this->homepageItemPermissions($this->unifiHomepagePermissions('main'))) {
 		if ($this->homepageItemPermissions($this->unifiHomepagePermissions('main'))) {
@@ -134,7 +134,7 @@ trait UnifiHomepageItem
 				';
 				';
 		}
 		}
 	}
 	}
-	
+
 	public function getUnifiSiteName()
 	public function getUnifiSiteName()
 	{
 	{
 		if (empty($this->config['unifiURL'])) {
 		if (empty($this->config['unifiURL'])) {
@@ -183,9 +183,9 @@ trait UnifiHomepageItem
 			$this->setAPIResponse('error', $e->getMessage(), 500);
 			$this->setAPIResponse('error', $e->getMessage(), 500);
 			return false;
 			return false;
 		}
 		}
-		
+
 	}
 	}
-	
+
 	public function testConnectionUnifi()
 	public function testConnectionUnifi()
 	{
 	{
 		if (empty($this->config['unifiURL'])) {
 		if (empty($this->config['unifiURL'])) {
@@ -227,7 +227,7 @@ trait UnifiHomepageItem
 				$cookie['csrf_token'] = ($response->cookies['csrf_token']->value) ?? false;
 				$cookie['csrf_token'] = ($response->cookies['csrf_token']->value) ?? false;
 				$cookie['Token'] = ($response->cookies['Token']->value) ?? false;
 				$cookie['Token'] = ($response->cookies['Token']->value) ?? false;
 				$options['cookies'] = $response->cookies;
 				$options['cookies'] = $response->cookies;
-				
+
 			} else {
 			} else {
 				$this->setAPIResponse('error', 'Unifi response error - Check Credentials', 409);
 				$this->setAPIResponse('error', 'Unifi response error - Check Credentials', 409);
 				return false;
 				return false;
@@ -251,7 +251,7 @@ trait UnifiHomepageItem
 		$this->setAPIResponse('success', 'API Connection succeeded', 200);
 		$this->setAPIResponse('success', 'API Connection succeeded', 200);
 		return true;
 		return true;
 	}
 	}
-	
+
 	public function getUnifiHomepageData()
 	public function getUnifiHomepageData()
 	{
 	{
 		if (!$this->homepageItemPermissions($this->unifiHomepagePermissions('main'), true)) {
 		if (!$this->homepageItemPermissions($this->unifiHomepagePermissions('main'), true)) {
@@ -284,7 +284,7 @@ trait UnifiHomepageItem
 				$cookie['csrf_token'] = ($response->cookies['csrf_token']->value) ?? false;
 				$cookie['csrf_token'] = ($response->cookies['csrf_token']->value) ?? false;
 				$cookie['Token'] = ($response->cookies['Token']->value) ?? false;
 				$cookie['Token'] = ($response->cookies['Token']->value) ?? false;
 				$options['cookies'] = $response->cookies;
 				$options['cookies'] = $response->cookies;
-				
+
 			} else {
 			} else {
 				$this->setAPIResponse('error', 'Unifi response error - Check Credentials', 409);
 				$this->setAPIResponse('error', 'Unifi response error - Check Credentials', 409);
 				return false;
 				return false;

+ 143 - 140
api/pages/login.php

@@ -11,6 +11,9 @@ function get_page_login($Organizr)
 	$hideOrganizrLogin = ($Organizr->checkoAuth()) ? 'collapse' : 'collapse in';
 	$hideOrganizrLogin = ($Organizr->checkoAuth()) ? 'collapse' : 'collapse in';
 	$hideOrganizrLoginHeader = ($Organizr->checkoAuthOnly()) ? 'hidden' : '';
 	$hideOrganizrLoginHeader = ($Organizr->checkoAuthOnly()) ? 'hidden' : '';
 	$hideOrganizrLoginHeader2 = ($Organizr->checkoAuth()) ? '' : 'hidden';
 	$hideOrganizrLoginHeader2 = ($Organizr->checkoAuth()) ? '' : 'hidden';
+	$hideOrganizrRecoveryPassword = ($Organizr->config['disableRecoverPassword']) ? 'hidden' : '';
+	$customForgotPasswordText = (empty($Organizr->config['customForgotPasswordText'])) ? 'Enter your Email and instructions will be sent to you!' : $Organizr->config['customForgotPasswordText'];
+	$customForgotPasswordText = ($Organizr->config['disableRecoverPassword']) ? 'Disabled' : $customForgotPasswordText;
 	return '
 	return '
 <script>
 <script>
 if(activeInfo.settings.login.rememberMe){
 if(activeInfo.settings.login.rememberMe){
@@ -18,145 +21,145 @@ if(activeInfo.settings.login.rememberMe){
 }
 }
 </script>
 </script>
 <section id="wrapper" class="login-register">
 <section id="wrapper" class="login-register">
-  <div class="login-box login-sidebar animated slideInRight">
-    <div class="white-box">
-      <form class="form-horizontal" id="loginform" onsubmit="return false;">
-      	<input id="login-attempts" class="form-control" name="loginAttempts" type="hidden">
-        <a href="javascript:void(0)" class="text-center db visible-xs" id="login-logo">' . $Organizr->logoOrText() . '</a>
-        <div id="oAuth-div" class="form-group hidden">
-          <div class="col-xs-12">
-            <div class="panel panel-success animated tada">
-                <div class="panel-heading">oAuth Successful - Please wait...</div>
-            </div>
-          </div>
-        </div>
-		<div id="tfa-div" class="form-group hidden">
-          <div class="col-xs-12">
-            <div class="panel panel-warning animated tada">
-                <div class="panel-heading"> 2FA
-                    <div class="pull-right"><a href="#" data-perform="panel-collapse"><i class="ti-minus"></i></a> <a href="#" data-perform="panel-dismiss"><i class="ti-close"></i></a> </div>
-                </div>
-                <div class="panel-wrapper collapse in" aria-expanded="true">
-                    <div class="panel-body">
-	                    <div class="input-group" style="width: 100%;">
-	                        <div class="input-group-addon hidden-xs"><i class="ti-lock"></i></div>
-	                        <input type="text" class="form-control tfa-input" name="tfaCode" placeholder="Code" data-lpignore="true" autocomplete="off" autocorrect="off" autocapitalize="off" maxlength="6" spellcheck="false" autofocus="">
-	                    </div>
-	                    <button class="btn btn-warning btn-lg btn-block text-uppercase waves-effect waves-light login-button m-t-10" type="submit" lang="en">Login</button>
-                    </div>
-                </div>
-            </div>
-          </div>
-        </div>
-        <div class="panel-group" id="login-panels" data-type="accordion" aria-multiselectable="true" role="tablist">
-	        <!-- ORGANIZR LOGIN -->
-	        <div class="panel">
-	            <div class="panel-heading bg-org ' . $hideOrganizrLoginHeader . ' ' . $hideOrganizrLoginHeader2 . '" id="organizr-login-heading" role="tab">
-	            	<a class="panel-title collapsed" data-toggle="collapse" href="#organizr-login-collapse" data-parent="#login-panels" aria-expanded="false" aria-controls="organizr-login-collapse">
-                        <img class="lazyload loginTitle" data-src="plugins/images/organizr/logo-no-border.png"> &nbsp;
-                        <span class="text-uppercase fw300" lang="en">Login with Organizr</span>
-	            	</a>
-	            	<div class="clearfix"></div>
-	            </div>
-	            <div class="panel-collapse ' . $hideOrganizrLogin . '" id="organizr-login-collapse" aria-labelledby="organizr-login-heading" role="tabpanel">
-	                <div class="panel-body">
-	                
-	                	<div class="form-group">
-				          <div class="col-xs-12">
-				            <input id="login-username-Input" class="form-control" name="username" type="text" required="" placeholder="Username" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" lang="en" autofocus>
-				          </div>
-				        </div>
-				        <div class="form-group">
-				          <div class="col-xs-12">
-				            <input id="login-password-Input" class="form-control" name="password" type="password" required="" placeholder="Password" lang="en">
-				          </div>
-				        </div>
-				        <div class="form-group">
-				          <div class="col-md-12">
-				            <div class="checkbox checkbox-primary pull-left p-t-0 remember-me">
-				              <input id="checkbox-login" name="remember" type="checkbox">
-				              <label for="checkbox-login" lang="en">Remember Me</label>
-				            </div>
-				            </div>
-				        </div>
-				        <div class="form-group text-center m-t-20 m-b-0">
-				          <div class="col-xs-12">
-				            <button class="btn btn-info btn-lg btn-block text-uppercase waves-effect waves-light login-button" type="submit" lang="en">Login</button>
-				          </div>
-				        </div>
-				        <div class="form-group m-b-0">
-				          <div class="col-sm-12 text-center">
-				            <input id="oAuth-Input" class="form-control" name="oAuth" type="hidden">
-				            <input id="oAuthType-Input" class="form-control" name="oAuthType" type="hidden">
-				            ' . $Organizr->showLogin() . '
-				          </div>
-				        </div>
-	                </div>
-	            </div>
-	        </div>
-	        <!-- END ORGANIZR LOGIN -->
-        	<!-- PLEX OAUTH LOGIN -->
-	        ' . $Organizr->showoAuth() . '
-	        <!-- END PLEX OAUTH LOGIN -->
-        </div>
-      </form>
-      <form class="form-horizontal form-material hidden" id="registerForm" onsubmit="return false;">
-        <div class="form-group m-t-40">
-          <div class="col-xs-12">
-            <input class="form-control" type="text" name="registrationPassword" required="" placeholder="Registration Password" lang="en" autofocus>
-          </div>
-        </div>
-        <div class="form-group">
-          <div class="col-xs-12">
-            <input class="form-control" name="username" type="text" required="" placeholder="Username" lang="en">
-          </div>
-        </div>
-        <div class="form-group">
-          <div class="col-xs-12">
-            <input class="form-control" name="email" type="text" required="" placeholder="Email" lang="en">
-          </div>
-        </div>
-        <div class="form-group">
-          <div class="col-xs-12">
-            <input class="form-control" name="password" type="password" required="" placeholder="Password" lang="en">
-          </div>
-        </div>
-        <div class="form-group text-center m-t-20">
-          <div class="col-xs-12">
-            <button class="btn btn-info btn-lg btn-block text-uppercase waves-effect waves-light register-button" type="submit" lang="en">Register</button>
-          </div>
-        </div>
-        <div class="form-group text-center m-t-20">
-          <div class="col-xs-12">
-            <button id="leave-registration" class="btn btn-primary btn-lg btn-block text-uppercase waves-effect waves-light" type="button" lang="en">Go Back</button>
-          </div>
-        </div>
-      </form>
-      <form class="form-horizontal" id="recoverform" onsubmit="return false;">
-        <div class="form-group ">
-          <div class="col-xs-12">
-            <h3 lang="en">Recover Password</h3>
-            <p class="text-muted" lang="en">Enter your Email and instructions will be sent to you!</p>
-          </div>
-        </div>
-        <div class="form-group ">
-          <div class="col-xs-12">
-            <input id="recover-input" class="form-control" name="email" type="text" placeholder="Email" lang="en" required>
-          </div>
-        </div>
-        <div class="form-group text-center m-t-20">
-          <div class="col-xs-12">
-            <button class="btn btn-primary btn-lg btn-block text-uppercase waves-effect waves-light reset-button" type="submit" lang="en">Reset</button>
-          </div>
-        </div>
-        <div class="form-group text-center m-t-20">
-          <div class="col-xs-12">
-            <button id="leave-recover" class="btn btn-primary btn-lg btn-block text-uppercase waves-effect waves-light" type="button" lang="en">Go Back</button>
-          </div>
-        </div>
-      </form>
-    </div>
-  </div>
+	<div class="login-box login-sidebar animated slideInRight">
+		<div class="white-box">
+			<form class="form-horizontal" id="loginform" onsubmit="return false;">
+				<input id="login-attempts" class="form-control" name="loginAttempts" type="hidden">
+				<a href="javascript:void(0)" class="text-center db visible-xs" id="login-logo">' . $Organizr->logoOrText() . '</a>
+				<div id="oAuth-div" class="form-group hidden">
+					<div class="col-xs-12">
+						<div class="panel panel-success animated tada">
+							<div class="panel-heading">oAuth Successful - Please wait...</div>
+						</div>
+					</div>
+				</div>
+				<div id="tfa-div" class="form-group hidden">
+				  <div class="col-xs-12">
+					<div class="panel panel-warning animated tada">
+						<div class="panel-heading"> 2FA
+							<div class="pull-right"><a href="#" data-perform="panel-collapse"><i class="ti-minus"></i></a> <a href="#" data-perform="panel-dismiss"><i class="ti-close"></i></a> </div>
+						</div>
+						<div class="panel-wrapper collapse in" aria-expanded="true">
+							<div class="panel-body">
+								<div class="input-group" style="width: 100%;">
+									<div class="input-group-addon hidden-xs"><i class="ti-lock"></i></div>
+									<input type="text" class="form-control tfa-input" name="tfaCode" placeholder="Code" data-lpignore="true" autocomplete="off" autocorrect="off" autocapitalize="off" maxlength="6" spellcheck="false" autofocus="">
+								</div>
+								<button class="btn btn-warning btn-lg btn-block text-uppercase waves-effect waves-light login-button m-t-10" type="submit" lang="en">Login</button>
+							</div>
+						</div>
+					</div>
+				  </div>
+				</div>
+				<div class="panel-group" id="login-panels" data-type="accordion" aria-multiselectable="true" role="tablist">
+					<!-- ORGANIZR LOGIN -->
+					<div class="panel">
+						<div class="panel-heading bg-org ' . $hideOrganizrLoginHeader . ' ' . $hideOrganizrLoginHeader2 . '" id="organizr-login-heading" role="tab">
+							<a class="panel-title collapsed" data-toggle="collapse" href="#organizr-login-collapse" data-parent="#login-panels" aria-expanded="false" aria-controls="organizr-login-collapse">
+								<img class="lazyload loginTitle" data-src="plugins/images/organizr/logo-no-border.png"> &nbsp;
+								<span class="text-uppercase fw300" lang="en">Login with Organizr</span>
+							</a>
+							<div class="clearfix"></div>
+						</div>
+						<div class="panel-collapse ' . $hideOrganizrLogin . '" id="organizr-login-collapse" aria-labelledby="organizr-login-heading" role="tabpanel">
+							<div class="panel-body">
+							
+								<div class="form-group">
+									<div class="col-xs-12">
+										<input id="login-username-Input" class="form-control" name="username" type="text" required="" placeholder="Username" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" lang="en" autofocus>
+									</div>
+								</div>
+								<div class="form-group">
+									<div class="col-xs-12">
+										<input id="login-password-Input" class="form-control" name="password" type="password" required="" placeholder="Password" lang="en">
+									</div>
+								</div>
+								<div class="form-group">
+									<div class="col-md-12">
+										<div class="checkbox checkbox-primary pull-left p-t-0 remember-me">
+											<input id="checkbox-login" name="remember" type="checkbox">
+											<label for="checkbox-login" lang="en">Remember Me</label>
+										</div>
+									</div>
+								</div>
+								<div class="form-group text-center m-t-20 m-b-0">
+									<div class="col-xs-12">
+										<button class="btn btn-info btn-lg btn-block text-uppercase waves-effect waves-light login-button" type="submit" lang="en">Login</button>
+									</div>
+								</div>
+								<div class="form-group m-b-0">
+									<div class="col-sm-12 text-center">
+										<input id="oAuth-Input" class="form-control" name="oAuth" type="hidden">
+										<input id="oAuthType-Input" class="form-control" name="oAuthType" type="hidden">
+										' . $Organizr->showLogin() . '
+									</div>
+								</div>
+							</div>
+						</div>
+					</div>
+					<!-- END ORGANIZR LOGIN -->
+					<!-- PLEX OAUTH LOGIN -->
+					' . $Organizr->showoAuth() . '
+					<!-- END PLEX OAUTH LOGIN -->
+				</div>
+			</form>
+			<form class="form-horizontal form-material hidden" id="registerForm" onsubmit="return false;">
+				<div class="form-group m-t-40">
+					<div class="col-xs-12">
+						<input class="form-control" type="text" name="registrationPassword" required="" placeholder="Registration Password" lang="en" autofocus>
+					</div>
+				</div>
+				<div class="form-group">
+					<div class="col-xs-12">
+						<input class="form-control" name="username" type="text" required="" placeholder="Username" lang="en">
+					</div>
+				</div>
+				<div class="form-group">
+					<div class="col-xs-12">
+						<input class="form-control" name="email" type="text" required="" placeholder="Email" lang="en">
+					</div>
+				</div>
+				<div class="form-group">
+					<div class="col-xs-12">
+						<input class="form-control" name="password" type="password" required="" placeholder="Password" lang="en">
+					</div>
+				</div>
+				<div class="form-group text-center m-t-20">
+					<div class="col-xs-12">
+						<button class="btn btn-info btn-lg btn-block text-uppercase waves-effect waves-light register-button" type="submit" lang="en">Register</button>
+					</div>
+				</div>
+				<div class="form-group text-center m-t-20">
+					<div class="col-xs-12">
+						<button id="leave-registration" class="btn btn-primary btn-lg btn-block text-uppercase waves-effect waves-light" type="button" lang="en">Go Back</button>
+					</div>
+				</div>
+			</form>
+			<form class="form-horizontal" id="recoverform" onsubmit="return false;">
+				<div class="form-group ">
+					<div class="col-xs-12">
+						<h3 lang="en">Recover Password</h3>
+						<p class="text-muted" lang="en">' . $customForgotPasswordText . '</p>
+					</div>
+				</div>
+				<div class="form-group ' . $hideOrganizrRecoveryPassword . '">
+					<div class="col-xs-12">
+						<input id="recover-input" class="form-control" name="email" type="text" placeholder="Email" lang="en" required>
+					</div>
+				</div>
+				<div class="form-group text-center m-t-20 ' . $hideOrganizrRecoveryPassword . '">
+					<div class="col-xs-12">
+						<button class="btn btn-primary btn-lg btn-block text-uppercase waves-effect waves-light reset-button" type="submit" lang="en">Reset</button>
+					</div>
+				</div>
+				<div class="form-group text-center m-t-20">
+					<div class="col-xs-12">
+						<button id="leave-recover" class="btn btn-primary btn-lg btn-block text-uppercase waves-effect waves-light" type="button" lang="en">Go Back</button>
+					</div>
+				</div>
+			</form>
+		</div>
+	</div>
 </section>
 </section>
 ';
 ';
-}
+}

+ 0 - 31
api/pages/settings-settings-sso.php

@@ -26,36 +26,5 @@ function get_page_settings_settings_sso($Organizr)
         </div>
         </div>
     </div>
     </div>
 </div>
 </div>
-<form id="sso-plex-token-form" class="mfp-hide white-popup-block mfp-with-anim">
-    <h1 lang="en">Get Plex Token</h1>
-    <div class="panel ssoPlexTokenHeader">
-        <div class="panel-heading ssoPlexTokenMessage" lang="en">Enter Plex Details</div>
-    </div>
-    <fieldset style="border:0;">
-        <div class="form-group">
-            <label class="control-label" for="sso-plex-token-form-username" lang="en">Plex Username</label>
-            <input type="text" class="form-control" id="sso-plex-token-form-username" name="username" required="" autofocus>
-        </div>
-        <div class="form-group">
-            <label class="control-label" for="sso-plex-token-form-password" lang="en">Plex Password</label>
-            <input type="password" class="form-control" id="sso-plex-token-form-password" name="password"  required="">
-        </div>
-    </fieldset>
-    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none getSSOPlexToken" type="button"><span class="btn-label"><i class="fa fa-ticket"></i></span><span lang="en">Grab It</span></button>
-    <div class="clearfix"></div>
-</form>
-<form id="sso-plex-machine-form" class="mfp-hide white-popup-block mfp-with-anim">
-    <h1 lang="en">Get Plex Machine</h1>
-    <div class="panel ssoPlexMachineHeader">
-        <div class="panel-heading ssoPlexMachineMessage" lang="en"></div>
-    </div>
-    <fieldset style="border:0;">
-        <div class="form-group">
-            <label class="control-label" for="sso-plex-machine-form-machine" lang="en">Plex Machine</label>
-            <div class="ssoPlexMachineListing"></div>
-        </div>
-    </fieldset>
-    <div class="clearfix"></div>
-</form>
 ';
 ';
 }
 }

+ 8 - 8
api/plugins/js/invites.js

@@ -88,7 +88,7 @@ function joinPlex(){
                 message('Invite Error',' '+response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
                 message('Invite Error',' '+response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
             }
             }
     	}).fail(function(xhr) {
     	}).fail(function(xhr) {
-    		console.error("Organizr Function: API Connection Failed");
+	        OrganizrApiError(xhr, 'Plex Signup Error');
     	});
     	});
     }
     }
 }
 }
@@ -120,7 +120,7 @@ function joinEmby(){
                 message('Invite Error',' '+response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
                 message('Invite Error',' '+response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
             }
             }
     	}).fail(function(xhr) {
     	}).fail(function(xhr) {
-    		console.error("Organizr Function: API Connection Failed");
+	        OrganizrApiError(xhr, 'Emby Signup Error');
     	});
     	});
     }
     }
 }
 }
@@ -173,7 +173,7 @@ function hasPlexUsername(){
             }
             }
             ajaxloader();;
             ajaxloader();;
         }).fail(function(xhr) {
         }).fail(function(xhr) {
-            console.error("Organizr Function: API Connection Failed");
+	        OrganizrApiError(xhr);
             ajaxloader();
             ajaxloader();
         });
         });
     }
     }
@@ -202,7 +202,7 @@ function hasEmbyUsername(){
             }
             }
             ajaxloader();;
             ajaxloader();;
         }).fail(function(xhr) {
         }).fail(function(xhr) {
-            console.error("Organizr Function: API Connection Failed");
+	        OrganizrApiError(xhr);
             ajaxloader();
             ajaxloader();
         });
         });
     }
     }
@@ -223,7 +223,7 @@ function verifyInvite(){
         }
         }
         ajaxloader();;
         ajaxloader();;
     }).fail(function(xhr) {
     }).fail(function(xhr) {
-        console.error("Organizr Function: API Connection Failed");
+	    OrganizrApiError(xhr);
         ajaxloader();
         ajaxloader();
     });
     });
 }
 }
@@ -267,7 +267,7 @@ function createNewInvite(){
             ajaxloader();
             ajaxloader();
             message('Invite',' Invite Created',activeInfo.settings.notifications.position,'#FFF','success','5000');
             message('Invite',' Invite Created',activeInfo.settings.notifications.position,'#FFF','success','5000');
         }).fail(function(xhr) {
         }).fail(function(xhr) {
-            console.error("Organizr Function: API Connection Failed");
+	        OrganizrApiError(xhr);
             ajaxloader();
             ajaxloader();
             message('Invite Error',' An Error Occured',activeInfo.settings.notifications.position,'#FFF','error','5000');
             message('Invite Error',' An Error Occured',activeInfo.settings.notifications.position,'#FFF','error','5000');
         });
         });
@@ -415,7 +415,7 @@ $(document).on('click', '.inviteModal', function() {
                             <button class="btn btn-block btn-info" onclick="joinPlex();">Submit</button>
                             <button class="btn btn-block btn-info" onclick="joinPlex();">Submit</button>
                         </div>
                         </div>
                         <div class="form-group invite-step-4-plex-accept hidden">
                         <div class="form-group invite-step-4-plex-accept hidden">
-                            <h4 class="" lang="en">You have been invited.  Please goto <a href="https://plex.tv" target="_blank">PLEX.TV</a> and login to accept the invite.  Once you have done that, you may head back here and login with your credentials.</h4>
+                            <h4 class="" lang="en">You have been invited.  Please check your email or goto <a href="https://plex.tv" target="_blank">PLEX.TV</a> and login to accept the invite.  Once you have done that, you may head back here and login with your credentials.</h4>
                         </div>
                         </div>
                         <!-- Begin Emby Invites -->
                         <!-- Begin Emby Invites -->
                         <div class="form-group invite-step-3-emby-yes hidden">
                         <div class="form-group invite-step-3-emby-yes hidden">
@@ -465,4 +465,4 @@ $(document).on('click', '#INVITES-settings-button', function() {
         console.error("Organizr Function: API Connection Failed");
         console.error("Organizr Function: API Connection Failed");
     });
     });
     ajaxloader();
     ajaxloader();
-});
+});

+ 1 - 2
api/plugins/js/php-mailer.js

@@ -256,8 +256,7 @@ $(document).on('click', '.phpmSendTestEmail', function() {
             messageSingle('',response.message,activeInfo.settings.notifications.position,'#FFF','error','5000');
             messageSingle('',response.message,activeInfo.settings.notifications.position,'#FFF','error','5000');
         }
         }
     }).fail(function(xhr, data) {
     }).fail(function(xhr, data) {
-    	console.log(data)
-        console.error("Organizr Function: API Connection Failed");
+	    OrganizrApiError(xhr, 'Mailer Error');
     });
     });
     ajaxloader();
     ajaxloader();
 });
 });

+ 13 - 0
api/v2/routes/help.php

@@ -0,0 +1,13 @@
+<?php
+$app->get('/help/smtp', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->checkRoute($request)) {
+		if ($Organizr->qualifyRequest(1, true)) {
+			$GLOBALS['api']['response']['data'] = $Organizr->saveOrganizrSmtpFromAPI();
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});

+ 4 - 1
css/organizr.css

@@ -1192,6 +1192,9 @@ ul.nav.customtab.nav-tabs.nav-low-margin {
 #menu-Organizr-Docs i {
 #menu-Organizr-Docs i {
     color: #707cd2;
     color: #707cd2;
 }
 }
+#menu-Feature-Request i {
+    color: #2cabe3;
+}
 .ping {
 .ping {
     position: relative;
     position: relative;
     margin-top: 0;
     margin-top: 0;
@@ -4548,4 +4551,4 @@ html {
     background: #1f1f1f;
     background: #1f1f1f;
     padding-right: 0px !important;
     padding-right: 0px !important;
     padding-left: 0px !important;
     padding-left: 0px !important;
-}
+}

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
css/organizr.min.css


+ 4 - 3
js/custom.js

@@ -924,7 +924,8 @@ $(document).on("click", ".addNewTab", function () {
 	    organizrAPI2('POST','api/v2/tabs',tabInfo,true).success(function(data) {
 	    organizrAPI2('POST','api/v2/tabs',tabInfo,true).success(function(data) {
 		    try {
 		    try {
 			    var response = data.response;
 			    var response = data.response;
-			    console.log(response);
+			    $('.tabIconImageList').val(null).trigger('change');
+			    $('.tabIconIconList').val(null).trigger('change');
 		    }catch(e) {
 		    }catch(e) {
 			    organizrCatchError(e,data);
 			    organizrCatchError(e,data);
 		    }
 		    }
@@ -1748,7 +1749,7 @@ $(document).on('click', ".help-modal", function(){
             break;
             break;
         default:
         default:
             return null;
             return null;
-        
+
     }
     }
     $('#help-modal-title').html(title);
     $('#help-modal-title').html(title);
     $('#help-modal-body').html(body);
     $('#help-modal-body').html(body);
@@ -1861,4 +1862,4 @@ $(document).on('click', '#homepage-Plex-form li a[aria-controls="Misc Options"]'
         $('#homepageCustomStreamNamesText').val(jsonEditor.getValue());
         $('#homepageCustomStreamNamesText').val(jsonEditor.getValue());
         $('#customize-appearance-form-save').removeClass('hidden');
         $('#customize-appearance-form-save').removeClass('hidden');
     });
     });
-}); 
+});

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
js/custom.min.js


+ 60 - 20
js/functions.js

@@ -943,7 +943,10 @@ function closeCurrentTab(event){
 	}
 	}
 }
 }
 function tabActions(event,name, type){
 function tabActions(event,name, type){
-	if(event.ctrlKey && !event.shiftKey && !event.altKey){
+	if(event.which == 3){
+		return false;
+	}
+	if((event.ctrlKey && !event.shiftKey && !event.altKey)  || event.which == 2){
 		popTab(cleanClass(name), type);
 		popTab(cleanClass(name), type);
 	}else if(event.altKey && !event.shiftKey && !event.ctrlKey){
 	}else if(event.altKey && !event.shiftKey && !event.ctrlKey){
         closeTab(name);
         closeTab(name);
@@ -2662,14 +2665,48 @@ function userMenu(user){
 	console.info("%c "+window.lang.translate('Welcome')+" %c ".concat(user.data.user.username, " "), "color: white; background: #AD80FD; font-weight: 700;", "color: #AD80FD; background: white; font-weight: 700;");
 	console.info("%c "+window.lang.translate('Welcome')+" %c ".concat(user.data.user.username, " "), "color: white; background: #AD80FD; font-weight: 700;", "color: #AD80FD; background: white; font-weight: 700;");
 }
 }
 function menuExtras(active){
 function menuExtras(active){
-    var supportFrame = buildFrameContainer('Organizr Support','https://organizr.app/support',1);
-    var docsFrame = buildFrameContainer('Organizr Docs','https://docs.organizr.app',1);
-    var adminMenu = '<li class="devider"></li>';
-    adminMenu += (activeInfo.user.groupID <= 1 && activeInfo.settings.menuLink.githubMenuLink) ? buildMenuList('GitHub Repo','https://github.com/causefx/organizr',2,'fontawesome::github') : '';
-    adminMenu += (activeInfo.user.groupID <= 1 && activeInfo.settings.menuLink.organizrSupportMenuLink) ? buildMenuList('Organizr Support','https://organizr.app/support',1,'fontawesome::life-ring') : '';
-    adminMenu += (activeInfo.user.groupID <= 1 && activeInfo.settings.menuLink.organizrDocsMenuLink) ? buildMenuList('Organizr Docs','https://docs.organizr.app',1,'simpleline::docs') : '';
-    $(supportFrame).appendTo($('.iFrame-listing'));
-    $(docsFrame).appendTo($('.iFrame-listing'));
+	let adminMenu = '<li class="devider"></li>';
+	let extraOrganizrLinks = [
+		{
+			'type':2,
+			'group_id':1,
+			'name':'Github Repo',
+			'url':'https://github.com/causefx/organizr',
+			'icon':'fontawesome::github',
+			'active':activeInfo.settings.menuLink.githubMenuLink
+		},
+		{
+			'type':1,
+			'group_id':1,
+			'name':'Organizr Support',
+			'url':'https://organizr.app/support',
+			'icon':'fontawesome::life-ring',
+			'active':activeInfo.settings.menuLink.organizrSupportMenuLink
+		},
+		{
+			'type':2,
+			'group_id':1,
+			'name':'Organizr Docs',
+			'url':'https://docs.organizr.app',
+			'icon':'simpleline::docs',
+			'active':activeInfo.settings.menuLink.organizrDocsMenuLink
+		},
+		{
+			'type':1,
+			'group_id':1,
+			'name':'Feature Request',
+			'url':'https://vote.organizr.app',
+			'icon':'simpleline::arrow-up-circle',
+			'active':activeInfo.settings.menuLink.organizrFeatureRequestLink
+		}
+	];
+	$.each(extraOrganizrLinks, function(i,v) {
+		if(v.type == 1){
+			let frame = buildFrameContainer(v.name,v.url,v.type);
+			$(frame).appendTo($('.iFrame-listing'));
+		}
+		adminMenu += (activeInfo.user.groupID <= v.group_id && v.active) ? buildMenuList(v.name,v.url,v.type,v.icon) : '';
+	});
 	if(active === true){
 	if(active === true){
 		return (activeInfo.settings.menuLink.organizrSignoutMenuLink) ? `
 		return (activeInfo.settings.menuLink.organizrSignoutMenuLink) ? `
 			<li class="devider"></li>
 			<li class="devider"></li>
@@ -2684,13 +2721,16 @@ function menuExtras(active){
 }
 }
 function categoryProcess(arrayItems){
 function categoryProcess(arrayItems){
 	var menuList = '';
 	var menuList = '';
+	let categoryIn = activeInfo.settings.misc.expandCategoriesByDefault ? 'in' : '';
+	let categoryActive = activeInfo.settings.misc.expandCategoriesByDefault ? 'active' : '';
+	let categoryExpanded = activeInfo.settings.misc.expandCategoriesByDefault ? 'true' : 'false';
 	if (Array.isArray(arrayItems['data']['categories']) && Array.isArray(arrayItems['data']['tabs'])) {
 	if (Array.isArray(arrayItems['data']['categories']) && Array.isArray(arrayItems['data']['tabs'])) {
 		$.each(arrayItems['data']['categories'], function(i,v) {
 		$.each(arrayItems['data']['categories'], function(i,v) {
 			if(v.count !== 0 && v.category_id !== 0){
 			if(v.count !== 0 && v.category_id !== 0){
 				menuList += `
 				menuList += `
-					<li class="allGroupsList" data-group-name="`+cleanClass(v.category)+`">
+					<li class="allGroupsList `+categoryActive+`" data-group-name="`+cleanClass(v.category)+`">
 						<a class="waves-effect" href="javascript:void(0)">`+iconPrefix(v.image)+`<span class="hide-menu">`+v.category+` <span class="fa arrow"></span> <span class="label label-rouded label-inverse pull-right">`+v.count+`</span></span><div class="menu-category-ping" data-good="0" data-bad="0"></div></a>
 						<a class="waves-effect" href="javascript:void(0)">`+iconPrefix(v.image)+`<span class="hide-menu">`+v.category+` <span class="fa arrow"></span> <span class="label label-rouded label-inverse pull-right">`+v.count+`</span></span><div class="menu-category-ping" data-good="0" data-bad="0"></div></a>
-						<ul class="nav nav-second-level category-`+v.category_id+` collapse"></ul>
+						<ul class="nav nav-second-level category-`+v.category_id+` collapse `+categoryIn+`" aria-expanded="`+categoryExpanded+`"></ul>
 					</li>
 					</li>
 				`;
 				`;
 			}
 			}
@@ -2718,7 +2758,7 @@ function buildInternalContainer(name,url,type, split = null){
 function buildMenuList(name,url,type,icon,ping=null,category_id = null,group_id = null){
 function buildMenuList(name,url,type,icon,ping=null,category_id = null,group_id = null){
     var ping = (ping !== null) ? `<small class="menu-`+cleanClass(ping)+`-ping-ms hidden-xs label label-rouded label-inverse pull-right pingTime hidden">
     var ping = (ping !== null) ? `<small class="menu-`+cleanClass(ping)+`-ping-ms hidden-xs label label-rouded label-inverse pull-right pingTime hidden">
 </small><div class="menu-`+cleanClass(ping)+`-ping" data-tab-name="`+name+`" data-previous-state=""></div>` : '';
 </small><div class="menu-`+cleanClass(ping)+`-ping" data-tab-name="`+name+`" data-previous-state=""></div>` : '';
-	return `<li class="allTabsList" id="menu-`+cleanClass(name)+`" data-tab-name="`+cleanClass(name)+`" type="`+type+`" data-group-id="`+group_id+`" data-category-id="`+category_id+`" data-url="`+url+`"><a class="waves-effect"  onclick="tabActions(event,'`+cleanClass(name)+`',`+type+`);">`+iconPrefix(icon)+`<span class="hide-menu elip sidebar-tabName">`+name+`</span>`+ping+`</a></li>`;
+	return `<li class="allTabsList" id="menu-`+cleanClass(name)+`" data-tab-name="`+cleanClass(name)+`" type="`+type+`" data-group-id="`+group_id+`" data-category-id="`+category_id+`" data-url="`+url+`"><a class="waves-effect"  onclick="tabActions(event,'`+cleanClass(name)+`',`+type+`);" onauxclick="tabActions(event,'`+cleanClass(name)+`',`+type+`);">`+iconPrefix(icon)+`<span class="hide-menu elip sidebar-tabName">`+name+`</span>`+ping+`</a></li>`;
 }
 }
 function tabProcess(arrayItems) {
 function tabProcess(arrayItems) {
 	var iFrameList = '';
 	var iFrameList = '';
@@ -3873,7 +3913,7 @@ function organizrAPI2(type,path,data=null,asyncValue=true){
 	switch(path){
 	switch(path){
 		case 'api/v2/windows/update':
 		case 'api/v2/windows/update':
 		case 'api/v2/docker/update':
 		case 'api/v2/docker/update':
-			timeout = 120000;
+			timeout = 240000;
 			break;
 			break;
 		default:
 		default:
 			timeout = 60000;
 			timeout = 60000;
@@ -6245,7 +6285,7 @@ function buildHealthChecks(array){
     if(array === false){ return ''; }
     if(array === false){ return ''; }
     var checks = (typeof array.content.checks !== 'undefined') ? array.content.checks.length : false;
     var checks = (typeof array.content.checks !== 'undefined') ? array.content.checks.length : false;
     return (checks) ? `
     return (checks) ? `
-	<div id="allHealthChecks">
+	<div id="allHealthChecks" class="m-b-30">
 		<div class="el-element-overlay row">
 		<div class="el-element-overlay row">
 		    <div class="col-md-12">
 		    <div class="col-md-12">
 		        <h4 class="pull-left homepage-element-title"><span lang="en">Health Checks</span> : </h4><h4 class="pull-left">&nbsp;<span class="label label-info m-l-20 checkbox-circle good-health-checks mouse" onclick="homepageHealthChecks()">`+checks+`</span></h4>
 		        <h4 class="pull-left homepage-element-title"><span lang="en">Health Checks</span> : </h4><h4 class="pull-left">&nbsp;<span class="label label-info m-l-20 checkbox-circle good-health-checks mouse" onclick="homepageHealthChecks()">`+checks+`</span></h4>
@@ -6292,7 +6332,7 @@ function buildUnifi(array){
 	<div id="allUnifi">
 	<div id="allUnifi">
 		<div class="row">
 		<div class="row">
 		    <div class="col-md-12">
 		    <div class="col-md-12">
-		        <h4 class="pull-left homepage-element-title"><span lang="en">Unifi</span> : </h4><h4 class="pull-left">&nbsp;</h4>
+		        <h4 class="pull-left homepage-element-title"><span lang="en">UniFi</span> : </h4><h4 class="pull-left">&nbsp;</h4>
 		        <hr class="hidden-xs">
 		        <hr class="hidden-xs">
 		    </div>
 		    </div>
 			<div class="clearfix"></div>
 			<div class="clearfix"></div>
@@ -7707,7 +7747,7 @@ function buildSpeedtest(array){
     var maximum = array.data.maximum;
     var maximum = array.data.maximum;
     var minimum = array.data.minimum;
     var minimum = array.data.minimum;
     var options = array.options;
     var options = array.options;
-  
+
     html += `
     html += `
     <div id="allSpeedtest">
     <div id="allSpeedtest">
     `;
     `;
@@ -8138,7 +8178,7 @@ function buildNetdataItem(array){
             html += buildGaugeChart(e,i,size,easySize,display);
             html += buildGaugeChart(e,i,size,easySize,display);
         }
         }
     });
     });
-    
+
     return html;
     return html;
 }
 }
 function buildNetdata(array){
 function buildNetdata(array){
@@ -8281,7 +8321,7 @@ function buildNetdata(array){
     `;
     `;
 
 
     html += `
     html += `
-    <div class="row">
+    <div class="row m-b-30">
         
         
             <div class="d-block text-center all-netdata">
             <div class="d-block text-center all-netdata">
     `;
     `;
@@ -8290,7 +8330,7 @@ function buildNetdata(array){
             </div>
             </div>
         
         
     </div>`;
     </div>`;
-   
+
     return (array) ? html : '';
     return (array) ? html : '';
 }
 }
 function homepageNetdata(timeout){
 function homepageNetdata(timeout){
@@ -10450,4 +10490,4 @@ function launch(){
 			orgErrorAlert('<h3>Webserver Error:</h3>' + xhr.responseText);
 			orgErrorAlert('<h3>Webserver Error:</h3>' + xhr.responseText);
 		}
 		}
 	});
 	});
-}
+}

+ 25 - 25
js/langpack/da[Danish].json

@@ -342,7 +342,7 @@
         "Generate": "Generer",
         "Generate": "Generer",
         "Generate New API Key": "Generer ny API nøgle",
         "Generate New API Key": "Generer ny API nøgle",
         "Organizr API": "Organizr API\n",
         "Organizr API": "Organizr API\n",
-        "Force Install Branch": "Force Install Branch",
+        "Force Install Branch": "Tving Branch Installation",
         "Branch": "Branch",
         "Branch": "Branch",
         "SSO": "SSO",
         "SSO": "SSO",
         "Enable": "Aktiver",
         "Enable": "Aktiver",
@@ -351,19 +351,19 @@
         "Plex Note": "Plex Note",
         "Plex Note": "Plex Note",
         "Admin username for Plex": "Admin brugernavn til Plex",
         "Admin username for Plex": "Admin brugernavn til Plex",
         "Admin Username": "Admin brugernavn",
         "Admin Username": "Admin brugernavn",
-        "Click Main on the sub-menu above.": "Click Main on the sub-menu above.",
+        "Click Main on the sub-menu above.": "Klik på Hoved i undermenuen ovenfor",
         "Important Information": "Vigtig information",
         "Important Information": "Vigtig information",
         "PING": "Ping",
         "PING": "Ping",
-        "Custom HTML/JavaScript": "Custom HTML/JavaScript",
-        "CustomHTML-2": "CustomHTML-2",
-        "CustomHTML-1": "CustomHTML-1",
+        "Custom HTML/JavaScript": "Brugerdefineret HTML/JavaScript",
+        "CustomHTML-2": "BrugerdefineretHTML-2",
+        "CustomHTML-1": "BrugerdefineretHTML-1",
         "Refresh Seconds": "Genopfrisk sekunder",
         "Refresh Seconds": "Genopfrisk sekunder",
         "Limit to User": "Begræns til bruger",
         "Limit to User": "Begræns til bruger",
-        "Minimum Group to Request": "Minimum Group to Request",
+        "Minimum Group to Request": "Minimums Gruppe for at anmode",
         "Token": "Token",
         "Token": "Token",
         "URL": "URL",
         "URL": "URL",
         "Ombi": "Ombi",
         "Ombi": "Ombi",
-        "Items Per Day": "Items Per Day",
+        "Items Per Day": "Ting per dag",
         "Time Format": "Tidsformat",
         "Time Format": "Tidsformat",
         "Default View": "Standard view",
         "Default View": "Standard view",
         "Start Day": "Start dag",
         "Start Day": "Start dag",
@@ -375,12 +375,12 @@
         "# of Days Before": "Antal af dage før",
         "# of Days Before": "Antal af dage før",
         "Radarr": "Radarr",
         "Radarr": "Radarr",
         "Lidarr": "Lidarr",
         "Lidarr": "Lidarr",
-        "Show Unmonitored": "Show Unmonitored",
+        "Show Unmonitored": "Vis uovervåget",
         "Sonarr": "Sonarr",
         "Sonarr": "Sonarr",
         "Hide Completed": "Gem udførte",
         "Hide Completed": "Gem udførte",
-        "Hide Seeding": "Hide Seeding",
+        "Hide Seeding": "Skjul Seeding",
         "Deluge": "Deluge",
         "Deluge": "Deluge",
-        "Order": "Order",
+        "Order": "Bestil",
         "Status: [": "Status: [",
         "Status: [": "Status: [",
         "]": "]",
         "]": "]",
         "rTorrent": "rTorrent",
         "rTorrent": "rTorrent",
@@ -390,16 +390,16 @@
         "NZBGet": "NZBGet",
         "NZBGet": "NZBGet",
         "SabNZBD": "SabNZBD",
         "SabNZBD": "SabNZBD",
         "Image Cache Size": "Billede cache størrelse",
         "Image Cache Size": "Billede cache størrelse",
-        "Emby Tab WAN URL": "Emby Tab WAN URL",
+        "Emby Tab WAN URL": "Emby Fane WAN URL",
         "Only use if you have Emby in a reverse proxy": "Brug kun hvis du har Embi i reverse proxy",
         "Only use if you have Emby in a reverse proxy": "Brug kun hvis du har Embi i reverse proxy",
-        "Emby Tab Name": "Emby tab navn",
-        "Item Limit": "Item Limit",
-        "Minimum Authorization": "Minimum Authorization",
+        "Emby Tab Name": "Emby Fane navn",
+        "Item Limit": "Ting begrænser",
+        "Minimum Authorization": "Minimums Bemyndigelse",
         "User Information": "Bruger information",
         "User Information": "Bruger information",
         "Emby": "Emby",
         "Emby": "Emby",
-        "Plex Tab WAN URL": "Plex Tab WAN URL",
+        "Plex Tab WAN URL": "Plex Fane WAN URL",
         "Only use if you have Plex in a reverse proxy": "Brug kun hvis du har Plex i reverse proxy",
         "Only use if you have Plex in a reverse proxy": "Brug kun hvis du har Plex i reverse proxy",
-        "Plex Tab Name": "Plex Tab Name",
+        "Plex Tab Name": "Plex Fane Navn",
         "Media Server": "Medie server",
         "Media Server": "Medie server",
         "Plex": "Plex",
         "Plex": "Plex",
         "separate by comma's": "Seperer med komma",
         "separate by comma's": "Seperer med komma",
@@ -407,16 +407,16 @@
         "Enable iCal": "Aktiver iCal",
         "Enable iCal": "Aktiver iCal",
         "Calendar": "Kalender",
         "Calendar": "Kalender",
         "Theme Javascript": "Javascript tema",
         "Theme Javascript": "Javascript tema",
-        "Custom Javascript": "Custom Javascript",
-        "Theme CSS [Can replace colors from above]": "Theme CSS [Can replace colors from above]",
-        "Custom CSS [Can replace colors from above]": "Custom CSS [Can replace colors from above]",
-        "Copy code and paste inside left box": "Copy code and paste inside left box",
-        "Download and unzip file and place in": "Download and unzip file and place in",
-        "Click [Generate your Favicons and HTML code]": "Click [Generate your Favicons and HTML code]",
-        "Enter this path": "Enter this path",
+        "Custom Javascript": "Brugerdefinerede Javascipt",
+        "Theme CSS [Can replace colors from above]": "CSS Tema [Kan erstatte farver ovenfor]",
+        "Custom CSS [Can replace colors from above]": "Brugerdefinerede CSS",
+        "Copy code and paste inside left box": "Kopiere koden og indsæt i den venstre boks",
+        "Download and unzip file and place in": "Hent og udpak fil og sæt ind",
+        "Click [Generate your Favicons and HTML code]": "Klik [Generere dine Favicons og HTML kode]",
+        "Enter this path": "Indsæt denne sti",
         "At bottom of page on [Favicon Generator Options] under [Path] choose [I cannot or I do not want to place favicon files at the root of my web site.]": "At bottom of page on [Favicon Generator Options] under [Path] choose [I cannot or I do not want to place favicon files at the root of my web site.]",
         "At bottom of page on [Favicon Generator Options] under [Path] choose [I cannot or I do not want to place favicon files at the root of my web site.]": "At bottom of page on [Favicon Generator Options] under [Path] choose [I cannot or I do not want to place favicon files at the root of my web site.]",
-        "Edit settings to your liking": "Edit settings to your liking",
-        "Choose your image to use": "Choose your image to use",
+        "Edit settings to your liking": "Redigere indstillinger som du synes",
+        "Choose your image to use": "Vælg dit billede du vil bruge",
         "Click [Select your Favicon picture]": "Click [Select your Favicon picture]",
         "Click [Select your Favicon picture]": "Click [Select your Favicon picture]",
         "Instructions": "Instruktioner",
         "Instructions": "Instruktioner",
         "Fav Icon Code": "Fav Icon Code",
         "Fav Icon Code": "Fav Icon Code",

+ 1 - 1
js/langpack/pl[Polish].json

@@ -23,7 +23,7 @@
         "Don't have an account?": "Nie masz konta?",
         "Don't have an account?": "Nie masz konta?",
         "Forgot pwd?": "Zapomniałeś?",
         "Forgot pwd?": "Zapomniałeś?",
         "Remember Me": "Zapamiętaj",
         "Remember Me": "Zapamiętaj",
-        "Login": "Zaloguj się",
+        "Login": "Logowanie",
         "Installed": "Zainstalowane",
         "Installed": "Zainstalowane",
         "Install Update": "Zainstaluj aktualizację",
         "Install Update": "Zainstaluj aktualizację",
         "Organizr Versions": "Wersje Organizr",
         "Organizr Versions": "Wersje Organizr",

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

@@ -3,24 +3,24 @@
         "Navigation": "Навигация",
         "Navigation": "Навигация",
         "Date": "Дата",
         "Date": "Дата",
         "Type": "Тип",
         "Type": "Тип",
-        "IP Address": "IP-адрес",
-        "Username": "Логин",
+        "IP Address": "IP адрес",
+        "Username": "Имя пользователя",
         "Message": "Сообщение",
         "Message": "Сообщение",
         "My Profile": "Мой профиль",
         "My Profile": "Мой профиль",
         "Account Settings": "Настройки аккаунта",
         "Account Settings": "Настройки аккаунта",
-        "Login/Register": "Авторизация/Регистрация",
-        "Logout": "Завершить сеанс",
+        "Login/Register": "Вход/Регистрация",
+        "Logout": "Выход",
         "Inbox": "Входящие",
         "Inbox": "Входящие",
         "Go Back": "Вернуться",
         "Go Back": "Вернуться",
         "Reset": "Сбросить",
         "Reset": "Сбросить",
         "Email": "Электронная почта",
         "Email": "Электронная почта",
-        "Enter your Email and instructions will be sent to you!": "Укажите адрес электронной почты и мы вышлем Вам инструкции!",
+        "Enter your Email and instructions will be sent to you!": "Укажите адрес электронной почты и мы вышлем Вам инструкцию!",
         "Register": "Регистрация",
         "Register": "Регистрация",
         "Registration Password": "Пароль для регистрации",
         "Registration Password": "Пароль для регистрации",
-        "Recover Password": "Восстановить пароль",
+        "Recover Password": "Пароль для восстановления",
         "Password": "Пароль",
         "Password": "Пароль",
-        "Sign Up": "Войти",
-        "Don't have an account?": "Нету аккаунта?",
+        "Sign Up": "Регистрация",
+        "Don't have an account?": "Нет аккаунта?",
         "Forgot pwd?": "Забыли пароль?",
         "Forgot pwd?": "Забыли пароль?",
         "Remember Me": "Запомнить меня",
         "Remember Me": "Запомнить меня",
         "Login": "Логин",
         "Login": "Логин",
@@ -518,21 +518,21 @@
         "Strict Plex Friends": "Исключить друзей в Plex",
         "Strict Plex Friends": "Исключить друзей в Plex",
         "Unifi": "Unifi",
         "Unifi": "Unifi",
         "Pi-hole": "Pi-hole",
         "Pi-hole": "Pi-hole",
-        "Check For Updates": "Check For Updates",
-        "I will try and import new strings every Friday": "I will try and import new strings every Friday",
-        "Custom definitions": "Custom definitions",
-        "Show on small screens": "Show on small screens",
-        "Show on medium screens": "Show on medium screens",
-        "Show on large screens": "Show on large screens",
-        "Size": "Size",
-        "Colour": "Colour",
-        "Chart": "Chart",
-        "Data": "Data",
-        "Info": "Info",
-        "Netdata": "Netdata",
+        "Check For Updates": "Проверить наличие обновлений",
+        "I will try and import new strings every Friday": "Я постараюсь добавлять переводы каждую пятницу",
+        "Custom definitions": "Собственные определения",
+        "Show on small screens": "Отображать на маленьких экранах",
+        "Show on medium screens": "Отображать на средних экранах",
+        "Show on large screens": "Отображать на больших экранам",
+        "Size": "Размер",
+        "Colour": "Цвет",
+        "Chart": "График",
+        "Data": "Данные",
+        "Info": "Информация",
+        "Netdata": "Сеть",
         "Toggle Title": "Toggle Title",
         "Toggle Title": "Toggle Title",
-        "Speedtest": "Speedtest",
-        "Unit of Measurement": "Unit of Measurement",
+        "Speedtest": "Проверка скорости",
+        "Unit of Measurement": "ед",
         "Enable Pollen": "Enable Pollen",
         "Enable Pollen": "Enable Pollen",
         "Enable Air Quality": "Enable Air Quality",
         "Enable Air Quality": "Enable Air Quality",
         "Enable Weather": "Enable Weather",
         "Enable Weather": "Enable Weather",
@@ -591,14 +591,14 @@
         "Music Labels (comma separated)": "Music Labels (comma separated)",
         "Music Labels (comma separated)": "Music Labels (comma separated)",
         "Movies Labels (comma separated)": "Movies Labels (comma separated)",
         "Movies Labels (comma separated)": "Movies Labels (comma separated)",
         "TV Labels (comma separated)": "TV Labels (comma separated)",
         "TV Labels (comma separated)": "TV Labels (comma separated)",
-        "Template #1": "Template #1",
+        "Template #1": "Шаблон #1",
         "Enable Debug Output on Email Test": "Enable Debug Output on Email Test",
         "Enable Debug Output on Email Test": "Enable Debug Output on Email Test",
         "i.e. 10.0.0.0/24 or 10.0.0.20": "i.e. 10.0.0.0/24 or 10.0.0.20",
         "i.e. 10.0.0.0/24 or 10.0.0.20": "i.e. 10.0.0.0/24 or 10.0.0.20",
         "Auth Proxy Whitelist": "Auth Proxy Whitelist",
         "Auth Proxy Whitelist": "Auth Proxy Whitelist",
-        "i.e. X-Forwarded-User": "i.e. X-Forwarded-User",
-        "Auth Proxy Header Name": "Auth Proxy Header Name",
-        "Auth Proxy": "Auth Proxy",
-        "Enable Local Address Forward": "Enable Local Address Forward",
+        "i.e. X-Forwarded-User": "прим.: X-Forwarded-User",
+        "Auth Proxy Header Name": "Заголовок прокси сервера",
+        "Auth Proxy": "Прокси сервер",
+        "Enable Local Address Forward": "Включить переадресацию локального адреса",
         "http://home.local": "http://home.local",
         "http://home.local": "http://home.local",
         "Local Address": "Local Address",
         "Local Address": "Local Address",
         "only domain and tld - i.e. domain.com": "only domain and tld - i.e. domain.com",
         "only domain and tld - i.e. domain.com": "only domain and tld - i.e. domain.com",

+ 8 - 1
js/version.json

@@ -313,5 +313,12 @@
     "new": "Option to disable certain plex libraries on now playing (#1534)|Toggle to use friendly name in stats|jellyFin SSO|PHP8 support|add 2fa to plex token form|loading animation to homepage item loading|allow admin to make other admins",
     "new": "Option to disable certain plex libraries on now playing (#1534)|Toggle to use friendly name in stats|jellyFin SSO|PHP8 support|add 2fa to plex token form|loading animation to homepage item loading|allow admin to make other admins",
     "fixed": "fix Blue Light theme News background (#1538)|fix empty socks (#1520)|getServerPath function|tab saving on order change if nothing updated (#1530)|fix tab sort order saving with lots of tabs (#1175)|non admin from changing password|password reset|allow homepage item api lookup lowercase|change unsorted icon on new installs|unifi and ubnt image changes|missing ssoJellyfin var",
     "fixed": "fix Blue Light theme News background (#1538)|fix empty socks (#1520)|getServerPath function|tab saving on order change if nothing updated (#1530)|fix tab sort order saving with lots of tabs (#1175)|non admin from changing password|password reset|allow homepage item api lookup lowercase|change unsorted icon on new installs|unifi and ubnt image changes|missing ssoJellyfin var",
     "notes": "New tab images|Please join our discord if you have any issues|Please report bugs in GitHub issues page"
     "notes": "New tab images|Please join our discord if you have any issues|Please report bugs in GitHub issues page"
+  },
+  "2.1.165": {
+    "date": "2021-01-22 17:45",
+    "title": "Weekly update-ish",
+    "new": "Overseerr SSO|/help/smtp to api to help users without SMTP accounts|option to disable recover password and change text or html (#1245)|Allow Middle-click to open in new tab (#1474)|Option for keeping Categories expanded by default (#1550)|link and option for new feature request site to replace feathub|Tab Images for Overseerr - Cardigann|increase windows and docker update timeout",
+    "fixed": "plex errors not coming up on failed user creation (#1456)|plex invite (#1456)|Emby HomePage Add-in - Item Details URL Needs updating (#1290)|Tautulli timeout issue|Reflect homepage refresh for Monitorr (#1554)|update timeout for jdownloader (#1510)|update timeout of SSO function|change out organizr logo for nav bar and add more logos|Unable to use same tab image on multiple tab adds without selecting different image first (#1369)|Issue with using wrong combo of username/email/password for SSO|UniFi Homepage Item Title|function variables|error message to phpmailer test button|padding on some homepage items|Traefik return with redirect",
+    "notes": "We are now using FeatureUpvote - icon located on bottom side bar|Updated languages|Please join our discord if you have any issues|Please report bugs in GitHub issues page"
   }
   }
-}
+}

BIN
plugins/images/organizr/organizr-logo-h-d.png


BIN
plugins/images/organizr/organizr-logo-h.png


BIN
plugins/images/organizr/organizr_logo.png


BIN
plugins/images/organizr/organizr_logo_d.png


BIN
plugins/images/tabs/cardigann-white.png


BIN
plugins/images/tabs/overseerr.png


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