Przeglądaj źródła

Merge branch 'v2-develop' into netdata-tweaks

Henry Whitaker 6 lat temu
rodzic
commit
e4a41d4751

+ 2 - 0
.gitignore

@@ -115,6 +115,8 @@ plugins/theme_files/*
 plugins/plugin_files/*
 !plugins/theme_files/index.html
 !plugins/plugin_files/index.html
+plugins/images/userTabs/*
+!plugins/images/userTabs/index.html
 # =========================
 # Plugin files
 # =========================

+ 8 - 2
api/config/default.php

@@ -8,6 +8,8 @@ return array(
 	'authBackendHostSuffix' => '',
 	'ldapBindUsername' => '',
 	'ldapBindPassword' => '',
+	'ldapSSL' => false,
+	'ldapTLS' => false,
 	'authBaseDN' => '',
 	'authBackendDomain' => '',
 	'ldapType' => '1',
@@ -316,7 +318,7 @@ return array(
 	'speedtestHeaderToggle' => true,
 	'speedtestHeader' => 'Speedtest',
 	'homepageNetdataEnabled' => false,
-	'homepageNetdataRefresh' => '2500',
+	'homepageNetdataRefresh' => '10000',
 	'homepageNetdataAuth' => '1',
 	'netdataURL' => '',
 	'netdata1Title' => '',
@@ -384,5 +386,9 @@ return array(
 	'netdata7Enabled' => false,
 	'netdataCustom' => '{
     
-	}'
+	}',
+	'githubMenuLink' => true,
+	'organizrSupportMenuLink' => true,
+	'organizrDocsMenuLink' => true,
+	'organizrSignoutMenuLink' => true
 );

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

@@ -1154,6 +1154,12 @@ function importUsersType($array)
 			case 'plex':
 				return importUsers(allPlexUsers(true));
 				break;
+			case 'jellyfin':
+				return importUsers(allJellyfinUsers(true));
+				break;
+			case 'emby':
+				return importUsers(allEmbyUsers(true));
+				break;
 			default:
 				return false;
 		}
@@ -1288,7 +1294,7 @@ function youtubeSearch($query)
 	if (!$query) {
 		return 'no query provided!';
 	}
-	$keys = array('AIzaSyBsdt8nLJRMTwOq5PY5A5GLZ2q7scgn01w', 'AIzaSyD-8SHutB60GCcSM8q_Fle38rJUV7ujd8k', 'AIzaSyBzOpVBT6VII-b-8gWD0MOEosGg4hyhCsQ');
+	$keys = array('AIzaSyBsdt8nLJRMTwOq5PY5A5GLZ2q7scgn01w', 'AIzaSyD-8SHutB60GCcSM8q_Fle38rJUV7ujd8k', 'AIzaSyBzOpVBT6VII-b-8gWD0MOEosGg4hyhCsQ', 'AIzaSyBKnRe1P8fpfBHgooJpmT0WOsrdUtZ4cpk');
 	$randomKeyIndex = array_rand($keys);
 	$key = $keys[$randomKeyIndex];
 	$apikey = ($GLOBALS['youtubeAPI'] !== '') ? $GLOBALS['youtubeAPI'] : $key;

+ 129 - 17
api/functions/auth-functions.php

@@ -145,7 +145,87 @@ function allPlexUsers($newOnly = false)
 		}
 		return false;
 	} catch (Requests_Exception $e) {
-		writeLog('success', 'Plex User Function - Error: ' . $e->getMessage(), $username);
+		writeLog('success', 'Plex Import User Function - Error: ' . $e->getMessage(), 'SYSTEM');
+	}
+	return false;
+}
+
+function allJellyfinUsers($newOnly = false)
+{
+	try {
+		if (!empty($GLOBALS['embyURL']) && !empty($GLOBALS['embyToken'])) {
+			$url = qualifyURL($GLOBALS['embyURL']) . '/Users?api_key=' . $GLOBALS['embyToken'];
+			$headers = array();
+			$response = Requests::get($url, $headers);
+			if ($response->success) {
+				$users = json_decode($response->body, true);
+				if (is_array($users) || is_object($users)) {
+					$results = array();
+					foreach ($users as $child) {
+						// Jellyfin doesn't list emails for some reason
+						$email = random_ascii_string(10) . '@placeholder.eml';
+						if ($newOnly) {
+							$taken = usernameTaken((string)$child['Name'], $email);
+							if (!$taken) {
+								$results[] = array(
+									'username' => (string)$child['Name'],
+									'email' => $email
+								);
+							}
+						} else {
+							$results[] = array(
+								'username' => (string)$child['Name'],
+								'email' => $email,
+							);
+						}
+					}
+					return $results;
+				}
+			}
+		}
+		return false;
+	} catch (Requests_Exception $e) {
+		writeLog('success', 'Jellyfin Import User Function - Error: ' . $e->getMessage(), 'SYSTEM');
+	}
+	return false;
+}
+
+function allEmbyUsers($newOnly = false)
+{
+	try {
+		if (!empty($GLOBALS['embyURL']) && !empty($GLOBALS['embyToken'])) {
+			$url = qualifyURL($GLOBALS['embyURL']) . '/Users?api_key=' . $GLOBALS['embyToken'];
+			$headers = array();
+			$response = Requests::get($url, $headers);
+			if ($response->success) {
+				$users = json_decode($response->body, true);
+				if (is_array($users) || is_object($users)) {
+					$results = array();
+					foreach ($users as $child) {
+						// Emby doesn't list emails for some reason
+						$email = random_ascii_string(10) . '@placeholder.eml';
+						if ($newOnly) {
+							$taken = usernameTaken((string)$child['Name'], $email);
+							if (!$taken) {
+								$results[] = array(
+									'username' => (string)$child['Name'],
+									'email' => $email
+								);
+							}
+						} else {
+							$results[] = array(
+								'username' => (string)$child['Name'],
+								'email' => $email,
+							);
+						}
+					}
+					return $results;
+				}
+			}
+		}
+		return false;
+	} catch (Requests_Exception $e) {
+		writeLog('success', 'Emby Import User Function - Error: ' . $e->getMessage(), 'SYSTEM');
 	}
 	return false;
 }
@@ -225,8 +305,8 @@ if (function_exists('ldap_connect')) {
 				'account_suffix' => (empty($GLOBALS['authBackendHostSuffix'])) ? null : $GLOBALS['authBackendHostSuffix'],
 				'port' => $ldapPort,
 				'follow_referrals' => false,
-				'use_ssl' => false,
-				'use_tls' => false,
+				'use_ssl' => $GLOBALS['ldapSSL'],
+				'use_tls' => $GLOBALS['ldapTLS'],
 				'version' => 3,
 				'timeout' => 5,
 				// Custom LDAP Options
@@ -337,32 +417,67 @@ function plugin_auth_emby_local($username, $password)
 	return false;
 }
 
+// Pass credentials to JellyFin Backend
+function plugin_auth_jellyfin($username, $password)
+{
+	try {
+		$url = qualifyURL($GLOBALS['embyURL']) . '/Users/authenticatebyname';
+		$headers = array(
+			'X-Emby-Authorization' => 'MediaBrowser Client="Organizr Auth", Device="Organizr", DeviceId="orgv2", Version="2.0"',
+			'Content-Type' => 'application/json',
+		);
+		$data = array(
+			'Username' => $username,
+			'Pw' => $password
+		);
+		$response = Requests::post($url, $headers, json_encode($data));
+		if ($response->success) {
+			$json = json_decode($response->body, true);
+			if (is_array($json) && isset($json['SessionInfo']) && isset($json['User']) && $json['User']['HasPassword'] == true) {
+				writeLog('success', 'JellyFin Auth Function - Found User and Logged In', $username);
+				// Login Success - Now Logout JellyFin Session As We No Longer Need It
+				$headers = array(
+					'X-Emby-Authorization' => 'MediaBrowser Client="Organizr Auth", Device="Organizr", DeviceId="orgv2", Version="2.0", Token="' . $json['AccessToken'] . '"',
+					'Content-Type' => 'application/json',
+				);
+				$response = Requests::post(qualifyURL($GLOBALS['embyURL']) . '/Sessions/Logout', $headers, array());
+				if ($response->success) {
+					return true;
+				}
+			}
+		}
+		return false;
+	} catch (Requests_Exception $e) {
+		writeLog('error', 'JellyFin Auth Function - Error: ' . $e->getMessage(), $username);
+	}
+	return false;
+}
+
 // Authenticate against emby connect
 function plugin_auth_emby_connect($username, $password)
 {
 	// Emby disabled EmbyConnect on their API
 	// https://github.com/MediaBrowser/Emby/issues/3553
-	return plugin_auth_emby_local($username, $password);
-	/*
+	//return plugin_auth_emby_local($username, $password);
 	try {
 		// Get A User
-		$connectId = '';
+		$connectUserName = '';
 		$url = qualifyURL($GLOBALS['embyURL']) . '/Users?api_key=' . $GLOBALS['embyToken'];
 		$response = Requests::get($url);
 		if ($response->success) {
 			$json = json_decode($response->body, true);
 			if (is_array($json)) {
 				foreach ($json as $key => $value) { // Scan for this user
-					if (isset($value['ConnectUserName']) && isset($value['ConnectUserId'])) { // Qualify as connect account
-						if ($value['ConnectUserName'] == $username || $value['Name'] == $username) {
-							$connectId = $value['ConnectUserId'];
+					if (isset($value['ConnectUserName']) && isset($value['ConnectLinkType'])) { // Qualify as connect account
+						if (strtolower($value['ConnectUserName']) == $username || strtolower($value['Name']) == $username) {
+							$connectUserName = $value['ConnectUserName'];
 							writeLog('success', 'Emby Connect Auth Function - Found User', $username);
 							break;
 						}
 					}
 				}
-				if ($connectId) {
-					writeLog('success', 'Emby Connect Auth Function - Attempting to Login with Emby ID: ' . $connectId, $username);
+				if ($connectUserName) {
+					writeLog('success', 'Emby Connect Auth Function - Attempting to Login with Emby ID: ' . $connectUserName, $username);
 					$connectURL = 'https://connect.emby.media/service/user/authenticate';
 					$headers = array(
 						'Accept' => 'application/json',
@@ -375,10 +490,10 @@ function plugin_auth_emby_connect($username, $password)
 					$response = Requests::post($connectURL, $headers, $data);
 					if ($response->success) {
 						$json = json_decode($response->body, true);
-						if (is_array($json) && isset($json['AccessToken']) && isset($json['User']) && $json['User']['Id'] == $connectId) {
+						if (is_array($json) && isset($json['AccessToken']) && isset($json['User']) && $json['User']['Name'] == $connectUserName) {
 							return array(
 								'email' => $json['User']['Email'],
-								'image' => $json['User']['ImageUrl'],
+								//'image' => $json['User']['ImageUrl'],
 							);
 						} else {
 							writeLog('error', 'Emby Connect Auth Function - Bad Response', $username);
@@ -394,7 +509,6 @@ function plugin_auth_emby_connect($username, $password)
 		writeLog('error', 'Emby Connect Auth Function - Error: ' . $e->getMessage(), $username);
 		return false;
 	}
-	*/
 }
 
 // Authenticate Against Emby Local (first) and Emby Connect
@@ -403,12 +517,10 @@ function plugin_auth_emby_all($username, $password)
 	// Emby disabled EmbyConnect on their API
 	// https://github.com/MediaBrowser/Emby/issues/3553
 	$localResult = plugin_auth_emby_local($username, $password);
-	return $localResult;
-	/*
+	//return $localResult;
 	if ($localResult) {
 		return $localResult;
 	} else {
 		return plugin_auth_emby_connect($username, $password);
 	}
-	*/
 }

+ 70 - 73
api/functions/homepage-connect-functions.php

@@ -795,7 +795,7 @@ function embyConnect($action, $key = 'Latest', $skip = false)
 				}
 				// Get A User
 				$userIds = $url . "/Users?api_key=" . $GLOBALS['embyToken'];
-				$showPlayed = true;
+				$showPlayed = false;
 				try {
 					$options = (localURL($userIds)) ? array('verify' => false) : array();
 					$response = Requests::get($userIds, array(), $options);
@@ -1535,43 +1535,42 @@ function calendarDaysCheck($entryStart, $entryEnd)
 
 function calendarStandardizeTimezone($timezone)
 {
-    switch ($timezone) {
-        case('CST'):
-        case('Central Time'):
-        case('Central Standard Time'):
-            $timezone = 'America/Chicago';
-            break;
-        case('CET'):
-        case('Central European Time'):
-            $timezone = 'Europe/Berlin';
-            break;
-        case('EST'):
-        case('Eastern Time'):
-        case('Eastern Standard Time'):
-            $timezone = 'America/New_York';
-            break;
-        case('PST'):
-        case('Pacific Time'):
-        case('Pacific Standard Time'):
-            $timezone = 'America/Los_Angeles';
-            break;
-        case('China Time'):
-        case('China Standard Time'):
-            $timezone = 'Asia/Beijing';
-            break;
-        case('IST'):
-        case('India Time'):
-        case('India Standard Time'):
-            $timezone = 'Asia/New_Delhi';
-            break;
-        case('JST');
-        case('Japan Time'):
-        case('Japan Standard Time'):
-            $timezone = 'Asia/Tokyo';
-            break;
-    }
-
-    return $timezone;
+	switch ($timezone) {
+		case('CST'):
+		case('Central Time'):
+		case('Central Standard Time'):
+			$timezone = 'America/Chicago';
+			break;
+		case('CET'):
+		case('Central European Time'):
+			$timezone = 'Europe/Berlin';
+			break;
+		case('EST'):
+		case('Eastern Time'):
+		case('Eastern Standard Time'):
+			$timezone = 'America/New_York';
+			break;
+		case('PST'):
+		case('Pacific Time'):
+		case('Pacific Standard Time'):
+			$timezone = 'America/Los_Angeles';
+			break;
+		case('China Time'):
+		case('China Standard Time'):
+			$timezone = 'Asia/Beijing';
+			break;
+		case('IST'):
+		case('India Time'):
+		case('India Standard Time'):
+			$timezone = 'Asia/New_Delhi';
+			break;
+		case('JST');
+		case('Japan Time'):
+		case('Japan Standard Time'):
+			$timezone = 'Asia/Tokyo';
+			break;
+	}
+	return $timezone;
 }
 
 function getCalenderRepeat($value)
@@ -2636,21 +2635,12 @@ function getMonitorr()
 				// This section grabs the names of all services by regex
 				$services = [];
 				$servicesMatch = [];
-				$servicePattern = '/<div id="servicetitle"><div>(.*)<\/div><\/div><div class="btnonline">Online<\/div><\/a><\/div><\/div>|<div id="servicetitleoffline".*><div>(.*)<\/div><\/div><div class="btnoffline".*>Offline<\/div><\/div><\/div>|<div id="servicetitlenolink".*><div>(.*)<\/div><\/div><div class="btnonline".*>Online<\/div><\/div><\/div>/';
+				$servicePattern = '/<div id="servicetitle"><div>(.*)<\/div><\/div><div class="btnonline">Online<\/div><\/a><\/div><\/div>|<div id="servicetitleoffline".*><div>(.*)<\/div><\/div><div class="btnoffline".*>Offline<\/div><\/div><\/div>|<div id="servicetitlenolink".*><div>(.*)<\/div><\/div><div class="btnonline".*>Online<\/div><\/div><\/div>|<div id="servicetitle"><div>(.*)<\/div><\/div><div class="btnunknown">/';
 				preg_match_all($servicePattern, $html, $servicesMatch);
-				unset($servicesMatch[0]);
-				$servicesMatch = array_values($servicesMatch);
-				foreach ($servicesMatch as $group) {
-					foreach ($group as $service) {
-						if ($service !== '') {
-							array_push($services, $service);
-						}
-					}
-				}
-				// This section then grabs the status and image of that service with regex
+				$services = array_filter($servicesMatch[1]) + array_filter($servicesMatch[2]) + array_filter($servicesMatch[3]) + array_filter($servicesMatch[4]);
 				$statuses = [];
-				foreach ($services as $service) {
-					$statusPattern = '/' . $service . '<\/div><\/div><div class="btnonline">(Online)<\/div>|' . $service . '<\/div><\/div><div class="btnoffline".*>(Offline)<\/div><\/div><\/div>/';
+				foreach ($services as $key => $service) {
+					$statusPattern = '/' . $service . '<\/div><\/div><div class="btnonline">(Online)<\/div>|' . $service . '<\/div><\/div><div class="btnoffline".*>(Offline)<\/div><\/div><\/div>|' . $service . '<\/div><\/div><div class="btnunknown">(.*)<\/div><\/a>/';
 					$status = [];
 					preg_match($statusPattern, $html, $status);
 					$statuses[$service] = $status;
@@ -2663,8 +2653,13 @@ function getMonitorr()
 							$statuses[$service] = [
 								'status' => false
 							];
+						} else if ($match == 'Unresponsive') {
+							$statuses[$service] = [
+								'status' => 'unresponsive'
+							];
 						}
 					}
+					$statuses[$service]['sort'] = $key;
 					$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 . '/';
 					preg_match($imgPattern, $html, $imageMatch);
@@ -2698,7 +2693,15 @@ function getMonitorr()
 						}
 					}
 				}
-				ksort($statuses);
+				foreach($statuses as $status){
+					foreach($status as $key=>$value){
+						if(!isset($sortArray[$key])){
+							$sortArray[$key] = array();
+						}
+						$sortArray[$key][] = $value;
+					}
+				}
+				array_multisort($sortArray['status'], SORT_ASC, $sortArray['sort'], SORT_ASC, $statuses);
 				$api['services'] = $statuses;
 				$api['options'] = [
 					'title' => $GLOBALS['monitorrHeader'],
@@ -2708,6 +2711,7 @@ function getMonitorr()
 			}
 		} catch (Requests_Exception $e) {
 			writeLog('error', 'Monitorr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$api['error'] = $e->getMessage();
 		};
 		$api = isset($api) ? $api : false;
 		return $api;
@@ -2724,13 +2728,11 @@ function getSpeedtest()
 			$response = Requests::get($dataUrl);
 			if ($response->success) {
 				$json = json_decode($response->body, true);
-
 				$api['data'] = [
 					'current' => $json['data'],
 					'average' => $json['average'],
 					'max' => $json['max'],
 				];
-
 				$api['options'] = [
 					'title' => $GLOBALS['speedtestHeader'],
 					'titleToggle' => $GLOBALS['speedtestHeaderToggle'],
@@ -2751,12 +2753,10 @@ function getNetdata()
 		$api = [];
 		$api['data'] = [];
 		$api['url'] = $GLOBALS['netdataURL'];
-
 		$url = qualifyURL($GLOBALS['netdataURL']);
-
-		for($i = 1; $i < 8; $i++) {
-			if($GLOBALS['netdata'.($i).'Enabled']) {
-				switch($GLOBALS['netdata'.$i.'Data']) {
+		for ($i = 1; $i < 8; $i++) {
+			if ($GLOBALS['netdata' . ($i) . 'Enabled']) {
+				switch ($GLOBALS['netdata' . $i . 'Data']) {
 					case 'disk-read':
 						$data = disk('in', $url);
 						break;
@@ -2812,19 +2812,16 @@ function getNetdata()
 						];
 						break;
 				}
-
-				$data['title'] = $GLOBALS['netdata'.$i.'Title'];
-				$data['colour'] = $GLOBALS['netdata'.$i.'Colour'];
-				$data['chart'] = $GLOBALS['netdata'.$i.'Chart'];
-				$data['size'] = $GLOBALS['netdata'.$i.'Size'];
-				$data['lg'] = $GLOBALS['netdata'.($i).'lg'];
-				$data['md'] = $GLOBALS['netdata'.($i).'md'];
-				$data['sm'] = $GLOBALS['netdata'.($i).'sm'];
-
+				$data['title'] = $GLOBALS['netdata' . $i . 'Title'];
+				$data['colour'] = $GLOBALS['netdata' . $i . 'Colour'];
+				$data['chart'] = $GLOBALS['netdata' . $i . 'Chart'];
+				$data['size'] = $GLOBALS['netdata' . $i . 'Size'];
+				$data['lg'] = $GLOBALS['netdata' . ($i) . 'lg'];
+				$data['md'] = $GLOBALS['netdata' . ($i) . 'md'];
+				$data['sm'] = $GLOBALS['netdata' . ($i) . 'sm'];
 				array_push($api['data'], $data);
 			}
 		}
-
 		$api = isset($api) ? $api : false;
 		return $api;
 	}
@@ -3152,8 +3149,8 @@ function testAPIConnection($array)
 					'account_suffix' => (empty($GLOBALS['authBackendHostSuffix'])) ? null : $GLOBALS['authBackendHostSuffix'],
 					'port' => $ldapPort,
 					'follow_referrals' => false,
-					'use_ssl' => false,
-					'use_tls' => false,
+					'use_ssl' => $GLOBALS['ldapSSL'],
+					'use_tls' => $GLOBALS['ldapTLS'],
 					'version' => 3,
 					'timeout' => 5,
 					// Custom LDAP Options
@@ -3230,8 +3227,8 @@ function testAPIConnection($array)
 					'account_suffix' => '',
 					'port' => $ldapPort,
 					'follow_referrals' => false,
-					'use_ssl' => false,
-					'use_tls' => false,
+					'use_ssl' => $GLOBALS['ldapSSL'],
+					'use_tls' => $GLOBALS['ldapTLS'],
 					'version' => 3,
 					'timeout' => 5,
 					// Custom LDAP Options

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

@@ -2619,7 +2619,7 @@ function getHomepageList()
 		),
 		array(
 			'name' => 'Tautulli',
-			'enabled' => true,
+			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
 			'image' => 'plugins/images/tabs/tautulli.png',
 			'category' => 'Monitor',
 			'settings' => array(
@@ -3157,7 +3157,7 @@ function buildHomepageSettings()
 				break;
 		}
 		$homepageList .= '
-		<div class="col-md-3 col-xs-12 sort-homepage m-t-10 hvr-grow">
+		<div class="col-md-3 col-xs-12 sort-homepage m-t-10 hvr-grow clearfix">
 			<div class="homepage-drag fc-event ' . $class . ' lazyload"  data-src="' . $image . '">
 				<span class="ordinal-position text-uppercase badge bg-org homepage-number" data-link="' . $key . '" style="float:left;width: 30px;">' . $val . '</span>
 				<span class="homepage-text">&nbsp; ' . strtoupper(substr($key, 13)) . '</span>

+ 386 - 408
api/functions/netdata-functions.php

@@ -1,448 +1,426 @@
 <?php
-
 function netdataSettngsArray()
 {
-    $array = array(
-        'name' => 'Netdata',
-        'enabled' => true,
-        'image' => 'plugins/images/tabs/netdata.png',
-        'category' => 'Monitor',
-        'settings' => array(
-            'Enable' => array(
-                array(
-                    'type' => 'switch',
-                    'name' => 'homepageNetdataEnabled',
-                    'label' => 'Enable',
-                    'value' => $GLOBALS['homepageNetdataEnabled']
-                ),
-                array(
-                    'type' => 'select',
-                    'name' => 'homepageNetdataAuth',
-                    'label' => 'Minimum Authentication',
-                    'value' => $GLOBALS['homepageNetdataAuth'],
-                    'options' => groupSelect()
-                )
-            ),
-            'Connection' => array(
-                array(
-                    'type' => 'html',
-                    'override' => 12,
-                    'label' => 'Info',
-                    'html' => 'The URL needs to be on the same domain as your Organizr, and be proxied by subdomain. E.g. If Organizr is accessed at: https://domain.com, then your URL for netdata should be: https://netdata.domain.com'
-                ),
-                array(
-                    'type' => 'input',
-                    'name' => 'netdataURL',
-                    'label' => 'URL',
-                    'value' => $GLOBALS['netdataURL'],
-                    'help' => 'Please enter the local IP:PORT of your netdata instance'
-                ),
-                array(
-                    'type' => 'blank',
-                    'label' => ''
-                ),
-            ),
-        )
-    );
-
-    for($i = 1; $i <= 7; $i++) {
-        $array['settings']['Chart '.$i] = array(
-            array(
-                'type' => 'switch',
-                'name' => 'netdata'.$i.'Enabled',
-                'label' => 'Enable',
-                'value' => $GLOBALS['netdata'.$i.'Enabled']
-            ),
-            array(
-                'type' => 'blank',
-                'label' => ''
-            ),
-            array(
-                'type' => 'input',
-                'name' => 'netdata'.$i.'Title',
-                'label' => 'Title',
-                'value' => $GLOBALS['netdata'.$i.'Title'],
-                'help' => 'Title for the netdata graph'
-            ),
-            array(
-                'type' => 'select',
-                'name' => 'netdata'.$i.'Data',
-                'label' => 'Data',
-                'value' => $GLOBALS['netdata'.$i.'Data'],
-                'options' => netdataOptions(),
-            ),
-            array(
-                'type' => 'select',
-                'name' => 'netdata'.$i.'Chart',
-                'label' => 'Chart',
-                'value' => $GLOBALS['netdata'.$i.'Chart'],
-                'options' => netdataChartOptions(),
-            ),
-            array(
-                'type' => 'select',
-                'name' => 'netdata'.$i.'Colour',
-                'label' => 'Colour',
-                'value' => $GLOBALS['netdata'.$i.'Colour'],
-                'options' => netdataColourOptions(),
-            ),
-            array(
-                'type' => 'select',
-                'name' => 'netdata'.$i.'Size',
-                'label' => 'Size',
-                'value' => $GLOBALS['netdata'.$i.'Size'],
-                'options' => netdataSizeOptions(),
-            ),
-            array(
-                'type' => 'blank',
-                'label' => ''
-            ),
-            array(
-                'type' => 'switch',
-                'name' => 'netdata'.$i.'lg',
-                'label' => 'Show on large screens',
-                'value' => $GLOBALS['netdata'.$i.'lg']
-            ),
-            array(
-                'type' => 'switch',
-                'name' => 'netdata'.$i.'md',
-                'label' => 'Show on medium screens',
-                'value' => $GLOBALS['netdata'.$i.'md']
-            ),
-            array(
-                'type' => 'switch',
-                'name' => 'netdata'.$i.'sm',
-                'label' => 'Show on small screens',
-                'value' => $GLOBALS['netdata'.$i.'sm']
-            ),
-        );
-    }
-
-    $array['settings']['Custom data'] = [
-        [
-            'type' => 'html',
-            'label' => '',
-            'override' => 12,
-            'html' => <<<HTML
-            <div>
-                <p>This is where you can define custom data sources for your netdata charts. To use a custom source, you need to select 'Custom' in the data field for the chart.</p>
-                <p>To define a custom data source, you need to add an entry to the JSON below, where the key is the chart number you want the custom data to be used for. Here is an example to set chart 1's custom data source to RAM percentage:</p>
-                <pre>{
-                "1": {
-                    "url": "/api/v1/data?chart=system.ram&format=array&points=540&group=average&gtime=0&options=absolute|percentage|jsonwrap|nonzero&after=-540&dimensions=used|buffers|active|wired",
-                    "value": "result,0",
-                    "units": "%",
-                    "max": 100
-                }
-            }</pre>
-                <p>The URL is appended to your netdata URL and returns JSON formatted data. The value field tells Organizr how to return the value you want from the netdata API. This should be formatted as comma-separated keys to access the desired value.</p>
-                <table class="table table-striped">
-                    <thead>
-                        <tr>
-                            <th>Parameter</th>
-                            <th>Description</th>
-                            <th>Required</th>
-                        </tr>
-                    </thead>
-                    <tbody>
-                        <tr>
-                            <td>url</td>
-                            <td>Specifies the netdata API endpoint</td>
-                            <td><i class="fa fa-check text-success" aria-hidden="true"></i></td>
-                        </tr>
-                        <tr>
-                            <td>value</td>
-                            <td>Specifies the selector used to get the data form the netdata response</td>
-                            <td><i class="fa fa-check text-success" aria-hidden="true"></i></td>
-                        </tr>
-                        <tr>
-                            <td>units</td>
-                            <td>Specifies the units shown in the graph/chart. Defaults to %</td>
-                            <td><i class="fa fa-times text-danger" aria-hidden="true"></i></td>
-                        </tr>
-                        <tr>
-                            <td>max</td>
-                            <td>Specifies the maximum possible value for the data. Defaults to 100</td>
-                            <td><i class="fa fa-times text-danger" aria-hidden="true"></i></td>
-                        </tr>
-                        <tr>
-                            <td>mutator</td>
-                            <td>Used to perform simple mathematical operations on the result (+, -, /, *). For example: dividing the result by 1000 would be '/1000'. These operations can be chained together by putting them in a comma-seprated format.</td>
-                            <td><i class="fa fa-times text-danger" aria-hidden="true"></i></td>
-                        </tr>
-                        <tr>
-                            <td>netdata</td>
-                            <td>Can be used to override the netdata instance data is retrieved from (in the format: http://IP:PORT)</td>
-                            <td><i class="fa fa-times text-danger" aria-hidden="true"></i></td>
-                        </tr>
-                    </tbody>
-                </table>
-            </div>
-            HTML
-        ],
-        [
-            'type' => 'html',
-            'name' => 'netdataCustomTextAce',
-            'class' => 'jsonTextarea hidden',
-            'label' => 'Custom definitions',
-            'override' => 12,
-            'html' => '<div id="netdataCustomTextAce" style="height: 300px;">' . htmlentities($GLOBALS['netdataCustom']) . '</div>',
-        ],
-        [
-            'type' => 'textbox',
-            'name' => 'netdataCustom',
-            'class' => 'jsonTextarea hidden',
-            'id' => 'netdataCustomText',
-            'label' => '',
-            'value' => $GLOBALS['netdataCustom'],
-        ]
-    ];
-
-    $array['settings']['Options'] =  array(
-        array(
-            'type' => 'select',
-            'name' => 'homepageNetdataRefresh',
-            'label' => 'Refresh Seconds',
-            'value' => $GLOBALS['homepageNetdataRefresh'],
-            'options' => optionTime()
-        ),
-    );
-
-    return $array;
+	$array = array(
+		'name' => 'Netdata',
+		'enabled' => true,
+		'image' => 'plugins/images/tabs/netdata.png',
+		'category' => 'Monitor',
+		'settings' => array(
+			'Enable' => array(
+				array(
+					'type' => 'switch',
+					'name' => 'homepageNetdataEnabled',
+					'label' => 'Enable',
+					'value' => $GLOBALS['homepageNetdataEnabled']
+				),
+				array(
+					'type' => 'select',
+					'name' => 'homepageNetdataAuth',
+					'label' => 'Minimum Authentication',
+					'value' => $GLOBALS['homepageNetdataAuth'],
+					'options' => groupSelect()
+				)
+			),
+			'Connection' => array(
+				array(
+					'type' => 'html',
+					'override' => 12,
+					'label' => 'Info',
+					'html' => 'The URL needs to be on the same domain as your Organizr, and be proxied by subdomain. E.g. If Organizr is accessed at: https://domain.com, then your URL for netdata should be: https://netdata.domain.com'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'netdataURL',
+					'label' => 'URL',
+					'value' => $GLOBALS['netdataURL'],
+					'help' => 'Please enter the local IP:PORT of your netdata instance'
+				),
+				array(
+					'type' => 'blank',
+					'label' => ''
+				),
+			),
+		)
+	);
+	for ($i = 1; $i <= 7; $i++) {
+		$array['settings']['Chart ' . $i] = array(
+			array(
+				'type' => 'switch',
+				'name' => 'netdata' . $i . 'Enabled',
+				'label' => 'Enable',
+				'value' => $GLOBALS['netdata' . $i . 'Enabled']
+			),
+			array(
+				'type' => 'blank',
+				'label' => ''
+			),
+			array(
+				'type' => 'input',
+				'name' => 'netdata' . $i . 'Title',
+				'label' => 'Title',
+				'value' => $GLOBALS['netdata' . $i . 'Title'],
+				'help' => 'Title for the netdata graph'
+			),
+			array(
+				'type' => 'select',
+				'name' => 'netdata' . $i . 'Data',
+				'label' => 'Data',
+				'value' => $GLOBALS['netdata' . $i . 'Data'],
+				'options' => netdataOptions(),
+			),
+			array(
+				'type' => 'select',
+				'name' => 'netdata' . $i . 'Chart',
+				'label' => 'Chart',
+				'value' => $GLOBALS['netdata' . $i . 'Chart'],
+				'options' => netdataChartOptions(),
+			),
+			array(
+				'type' => 'select',
+				'name' => 'netdata' . $i . 'Colour',
+				'label' => 'Colour',
+				'value' => $GLOBALS['netdata' . $i . 'Colour'],
+				'options' => netdataColourOptions(),
+			),
+			array(
+				'type' => 'select',
+				'name' => 'netdata' . $i . 'Size',
+				'label' => 'Size',
+				'value' => $GLOBALS['netdata' . $i . 'Size'],
+				'options' => netdataSizeOptions(),
+			),
+			array(
+				'type' => 'blank',
+				'label' => ''
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'netdata' . $i . 'lg',
+				'label' => 'Show on large screens',
+				'value' => $GLOBALS['netdata' . $i . 'lg']
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'netdata' . $i . 'md',
+				'label' => 'Show on medium screens',
+				'value' => $GLOBALS['netdata' . $i . 'md']
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'netdata' . $i . 'sm',
+				'label' => 'Show on small screens',
+				'value' => $GLOBALS['netdata' . $i . 'sm']
+			),
+		);
+	}
+	$array['settings']['Custom data'] = array(
+		array(
+			'type' => 'html',
+			'label' => '',
+			'override' => 12,
+			'html' => '
+			<div>
+			    <p>This is where you can define custom data sources for your netdata charts. To use a custom source, you need to select "Custom" in the data field for the chart.</p>
+			    <p>To define a custom data source, you need to add an entry to the JSON below, where the key is the chart number you want the custom data to be used for. Here is an example to set chart 1 custom data source to RAM percentage:</p>
+			    <pre>{
+			    "1": {
+			        "url": "/api/v1/data?chart=system.ram&format=array&points=540&group=average&gtime=0&options=absolute|percentage|jsonwrap|nonzero&after=-540&dimensions=used|buffers|active|wired",
+			        "value": "result,0",
+			        "units": "%",
+			        "max": 100
+			    }
+			}</pre>
+			    <p>The URL is appended to your netdata URL and returns JSON formatted data. The value field tells Organizr how to return the value you want from the netdata API. This should be formatted as comma-separated keys to access the desired value.</p>
+			    <table class="table table-striped">
+			        <thead>
+			            <tr>
+			                <th>Parameter</th>
+			                <th>Description</th>
+			                <th>Required</th>
+			            </tr>
+			        </thead>
+			        <tbody>
+			            <tr>
+			                <td>url</td>
+			                <td>Specifies the netdata API endpoint</td>
+			                <td><i class="fa fa-check text-success" aria-hidden="true"></i></td>
+			            </tr>
+			            <tr>
+			                <td>value</td>
+			                <td>Specifies the selector used to get the data form the netdata response</td>
+			                <td><i class="fa fa-check text-success" aria-hidden="true"></i></td>
+			            </tr>
+			            <tr>
+			                <td>units</td>
+			                <td>Specifies the units shown in the graph/chart. Defaults to %</td>
+			                <td><i class="fa fa-times text-danger" aria-hidden="true"></i></td>
+			            </tr>
+			            <tr>
+			                <td>max</td>
+			                <td>Specifies the maximum possible value for the data. Defaults to 100</td>
+			                <td><i class="fa fa-times text-danger" aria-hidden="true"></i></td>
+			            </tr>
+			            <tr>
+			                <td>mutator</td>
+			                <td>Used to perform simple mathematical operations on the result (+, -, /, *). For example: dividing the result by 1000 would be "/1000". These operations can be chained together by putting them in a comma-seprated format.</td>
+			                <td><i class="fa fa-times text-danger" aria-hidden="true"></i></td>
+			            </tr>
+			            <tr>
+			                <td>netdata</td>
+			                <td>Can be used to override the netdata instance data is retrieved from (in the format: http://IP:PORT)</td>
+			                <td><i class="fa fa-times text-danger" aria-hidden="true"></i></td>
+			            </tr>
+			        </tbody>
+			    </table>
+			</div>'
+		),
+		array(
+			'type' => 'html',
+			'name' => 'netdataCustomTextAce',
+			'class' => 'jsonTextarea hidden',
+			'label' => 'Custom definitions',
+			'override' => 12,
+			'html' => '<div id="netdataCustomTextAce" style="height: 300px;">' . htmlentities($GLOBALS['netdataCustom']) . '</div>',
+		),
+		array(
+			'type' => 'textbox',
+			'name' => 'netdataCustom',
+			'class' => 'jsonTextarea hidden',
+			'id' => 'netdataCustomText',
+			'label' => '',
+			'value' => $GLOBALS['netdataCustom'],
+		)
+	);
+	$array['settings']['Options'] = array(
+		array(
+			'type' => 'select',
+			'name' => 'homepageNetdataRefresh',
+			'label' => 'Refresh Seconds',
+			'value' => $GLOBALS['homepageNetdataRefresh'],
+			'options' => optionTime()
+		),
+	);
+	return $array;
 }
 
 function disk($dimension, $url)
 {
-    $data = [];
-    // Get Data
-    $dataUrl = $url . '/api/v1/data?chart=system.io&dimensions='.$dimension.'&format=array&points=540&group=average&gtime=0&options=absolute|jsonwrap|nonzero&after=-540';
-    try {
-        $response = Requests::get($dataUrl);
-        if ($response->success) {
-            $json = json_decode($response->body, true);
-            $data['value'] = $json['latest_values'][0] / 1000;
-            $data['percent'] =  getPercent($json['latest_values'][0], $json['max']);
-            $data['units'] = 'MiB/s';
-            $data['max'] = $json['max'];
-        }
-    } catch (Requests_Exception $e) {
-        writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-    };
-
-    return $data;
+	$data = [];
+	// Get Data
+	$dataUrl = $url . '/api/v1/data?chart=system.io&dimensions=' . $dimension . '&format=array&points=540&group=average&gtime=0&options=absolute|jsonwrap|nonzero&after=-540';
+	try {
+		$response = Requests::get($dataUrl);
+		if ($response->success) {
+			$json = json_decode($response->body, true);
+			$data['value'] = $json['latest_values'][0] / 1000;
+			$data['percent'] = getPercent($json['latest_values'][0], $json['max']);
+			$data['units'] = 'MiB/s';
+			$data['max'] = $json['max'];
+		}
+	} catch (Requests_Exception $e) {
+		writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+	};
+	return $data;
 }
 
 function diskSpace($dimension, $url)
 {
-    $data = [];
-    // Get Data
-    $dataUrl = $url . '/api/v1/data?chart=disk_space._&format=json&points=509&group=average&gtime=0&options=ms|jsonwrap|nonzero&after=-540&dimension='.$dimension;
-    try {
-        $response = Requests::get($dataUrl);
-        if ($response->success) {
-            $json = json_decode($response->body, true);
-            $data['value'] = $json['result']['data'][0][1];
-            $data['percent'] = $data['value'];
-            $data['units'] = '%';
-            $data['max'] = 100;
-        }
-    } catch (Requests_Exception $e) {
-        writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-    };
-
-    return $data;
+	$data = [];
+	// Get Data
+	$dataUrl = $url . '/api/v1/data?chart=disk_space._&format=json&points=509&group=average&gtime=0&options=ms|jsonwrap|nonzero&after=-540&dimension=' . $dimension;
+	try {
+		$response = Requests::get($dataUrl);
+		if ($response->success) {
+			$json = json_decode($response->body, true);
+			$data['value'] = $json['result']['data'][0][1];
+			$data['percent'] = $data['value'];
+			$data['units'] = '%';
+			$data['max'] = 100;
+		}
+	} catch (Requests_Exception $e) {
+		writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+	};
+	return $data;
 }
 
 function net($dimension, $url)
 {
-    $data = [];
-
-    // Get Data
-    $dataUrl = $url . '/api/v1/data?chart=system.net&dimensions='.$dimension.'&format=array&points=540&group=average&gtime=0&options=absolute|jsonwrap|nonzero&after=-540';
-    try {
-        $response = Requests::get($dataUrl);
-        if ($response->success) {
-            $json = json_decode($response->body, true);
-            $data['value'] = $json['latest_values'][0] / 1000;
-            $data['percent'] = getPercent($json['latest_values'][0], $json['max']);
-            $data['units'] = 'Mbit/s';
-            $data['max'] = $json['max'];
-        }
-    } catch (Requests_Exception $e) {
-        writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-    };
-
-    return $data;
+	$data = [];
+	// Get Data
+	$dataUrl = $url . '/api/v1/data?chart=system.net&dimensions=' . $dimension . '&format=array&points=540&group=average&gtime=0&options=absolute|jsonwrap|nonzero&after=-540';
+	try {
+		$response = Requests::get($dataUrl);
+		if ($response->success) {
+			$json = json_decode($response->body, true);
+			$data['value'] = $json['latest_values'][0] / 1000;
+			$data['percent'] = getPercent($json['latest_values'][0], $json['max']);
+			$data['units'] = 'Mbit/s';
+			$data['max'] = $json['max'];
+		}
+	} catch (Requests_Exception $e) {
+		writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+	};
+	return $data;
 }
 
 function cpu($url)
 {
-    $data = [];
-    $dataUrl = $url . '/api/v1/data?chart=system.cpu&format=array';
-    try {
-        $response = Requests::get($dataUrl);
-        if ($response->success) {
-            $json = json_decode($response->body, true);
-            $data['value'] = $json[0];
-            $data['percent'] = $data['value'];
-            $data['max'] = 100;
-            $data['units'] = '%';
-        }
-    } catch (Requests_Exception $e) {
-        writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-    };
-
-    return $data;
+	$data = [];
+	$dataUrl = $url . '/api/v1/data?chart=system.cpu&format=array';
+	try {
+		$response = Requests::get($dataUrl);
+		if ($response->success) {
+			$json = json_decode($response->body, true);
+			$data['value'] = $json[0];
+			$data['percent'] = $data['value'];
+			$data['max'] = 100;
+			$data['units'] = '%';
+		}
+	} catch (Requests_Exception $e) {
+		writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+	};
+	return $data;
 }
 
 function ram($url)
 {
-    $data = [];
-    $dataUrl = $url . '/api/v1/data?chart=system.ram&format=array&points=540&group=average&gtime=0&options=absolute|percentage|jsonwrap|nonzero&after=-540&dimensions=used|buffers|active|wired';
-    try {
-        $response = Requests::get($dataUrl);
-        if ($response->success) {
-            $json = json_decode($response->body, true);
-            $data['value'] = $json['result'][0];
-            $data['percent'] = $data['value'];
-            $data['max'] = 100;
-            $data['units'] = '%';
-        }
-    } catch (Requests_Exception $e) {
-        writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-    };
-
-    return $data;
+	$data = [];
+	$dataUrl = $url . '/api/v1/data?chart=system.ram&format=array&points=540&group=average&gtime=0&options=absolute|percentage|jsonwrap|nonzero&after=-540&dimensions=used|buffers|active|wired';
+	try {
+		$response = Requests::get($dataUrl);
+		if ($response->success) {
+			$json = json_decode($response->body, true);
+			$data['value'] = $json['result'][0];
+			$data['percent'] = $data['value'];
+			$data['max'] = 100;
+			$data['units'] = '%';
+		}
+	} catch (Requests_Exception $e) {
+		writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+	};
+	return $data;
 }
 
 function swap($url)
 {
-    $data = [];
-    $dataUrl = $url . '/api/v1/data?chart=system.swap&format=array&points=540&group=average&gtime=0&options=absolute|percentage|jsonwrap|nonzero&after=-540&dimensions=used';
-    try {
-        $response = Requests::get($dataUrl);
-        if ($response->success) {
-            $json = json_decode($response->body, true);
-            $data['value'] = $json['result'][0];
-            $data['percent'] = $data['value'];
-            $data['max'] = 100;
-            $data['units'] = '%';
-        }
-    } catch (Requests_Exception $e) {
-        writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-    };
-
-    return $data;
+	$data = [];
+	$dataUrl = $url . '/api/v1/data?chart=system.swap&format=array&points=540&group=average&gtime=0&options=absolute|percentage|jsonwrap|nonzero&after=-540&dimensions=used';
+	try {
+		$response = Requests::get($dataUrl);
+		if ($response->success) {
+			$json = json_decode($response->body, true);
+			$data['value'] = $json['result'][0];
+			$data['percent'] = $data['value'];
+			$data['max'] = 100;
+			$data['units'] = '%';
+		}
+	} catch (Requests_Exception $e) {
+		writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+	};
+	return $data;
 }
 
 function getPercent($val, $max)
 {
-    if($max == 0) {
-        return 0;
-    } else {
-        return ( $val / $max ) * 100;
-    }
+	if ($max == 0) {
+		return 0;
+	} else {
+		return ($val / $max) * 100;
+	}
 }
 
 function customNetdata($url, $id)
 {
-    try {
-        $customs = json_decode($GLOBALS['netdataCustom'], true, 512, JSON_THROW_ON_ERROR);
-    } catch(Exception $e) {
-        $customs = false;
-    }
-        
-    if($customs == false) {
-        return [
-            'error' => 'unable to parse custom JSON'
-        ];
-    } else if(!isset($customs[$id])) {
-        return [
-            'error' => 'custom definition not found'
-        ];
-    } else {
-        $data = [];
-        $custom = $customs[$id];
-
-        if( isset($custom['url']) && isset($custom['value']) ) {
-            if( isset($custom['netdata']) && $custom['netdata'] != '' ) {
-                $url = qualifyURL($custom['netdata']);
-            }
-            $dataUrl = $url . '/' . $custom['url'];
-            try {
-                $response = Requests::get($dataUrl);
-                if ($response->success) {
-                    $json = json_decode($response->body, true);
-
-                    if( !isset($custom['max']) || $custom['max'] == '' ) {
-                        $custom['max'] = 100;
-                    }
-                    $data['max'] = $custom['max'];
-
-                    if( !isset($custom['units']) || $custom['units'] == '' ) {
-                        $custom['units'] = '%';
-                    }
-                    $data['units'] = $custom['units'];
-    
-                    $selectors = explode(',', $custom['value']);
-                    foreach($selectors as $selector) {
-                        if(is_numeric($selector)) {
-                            $selector = (int) $selector;
-                        }
-                        if(!isset($data['value'])) {
-                            $data['value'] = $json[$selector];
-                        } else {
-                            $data['value'] = $data['value'][$selector];
-                        }
-                    }
-
-                    if(isset($custom['mutator'])) {
-                        $data['value'] = parseMutators($data['value'], $custom['mutator']);
-                    }
-    
-                    if($data['max'] == 0) {
-                        $data['percent'] = 0;
-                    } else {
-                        $data['percent'] = ( $data['value'] / $data['max'] ) * 100;
-                    }
-                }
-            } catch (Requests_Exception $e) {
-                writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-            };
-        } else {
-            $data['error'] = 'custom definition incomplete';
-        }
-    
-        return $data;
-    }
+	try {
+		$customs = json_decode($GLOBALS['netdataCustom'], true, 512, JSON_THROW_ON_ERROR);
+	} catch (Exception $e) {
+		$customs = false;
+	}
+	if ($customs == false) {
+		return [
+			'error' => 'unable to parse custom JSON'
+		];
+	} else if (!isset($customs[$id])) {
+		return [
+			'error' => 'custom definition not found'
+		];
+	} else {
+		$data = [];
+		$custom = $customs[$id];
+		if (isset($custom['url']) && isset($custom['value'])) {
+			if (isset($custom['netdata']) && $custom['netdata'] != '') {
+				$url = qualifyURL($custom['netdata']);
+			}
+			$dataUrl = $url . '/' . $custom['url'];
+			try {
+				$response = Requests::get($dataUrl);
+				if ($response->success) {
+					$json = json_decode($response->body, true);
+					if (!isset($custom['max']) || $custom['max'] == '') {
+						$custom['max'] = 100;
+					}
+					$data['max'] = $custom['max'];
+					if (!isset($custom['units']) || $custom['units'] == '') {
+						$custom['units'] = '%';
+					}
+					$data['units'] = $custom['units'];
+					$selectors = explode(',', $custom['value']);
+					foreach ($selectors as $selector) {
+						if (is_numeric($selector)) {
+							$selector = (int)$selector;
+						}
+						if (!isset($data['value'])) {
+							$data['value'] = $json[$selector];
+						} else {
+							$data['value'] = $data['value'][$selector];
+						}
+					}
+					if (isset($custom['mutator'])) {
+						$data['value'] = parseMutators($data['value'], $custom['mutator']);
+					}
+					if ($data['max'] == 0) {
+						$data['percent'] = 0;
+					} else {
+						$data['percent'] = ($data['value'] / $data['max']) * 100;
+					}
+				}
+			} catch (Requests_Exception $e) {
+				writeLog('error', 'Netdata Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			};
+		} else {
+			$data['error'] = 'custom definition incomplete';
+		}
+		return $data;
+	}
 }
 
 function parseMutators($val, $mutators)
 {
-    $mutators = explode(',', $mutators);
-    foreach($mutators as $m) {
-        $op = $m[0];
-        try {
-            $m = (float) substr($m, 1);
-            switch($op) {
-                case '+':
-                    $val = $val + $m;
-                    break;
-                case '-':
-                    $val = $val - $m;
-                    break;
-                case '/':
-                    $val = $val / $m;
-                    break;
-                case '*':
-                    $val = $val * $m;
-                    break;
-                default:
-                    break;
-            }
-        } catch(Exception $e) {
-            //
-        }
-    }
-
-    return $val;
+	$mutators = explode(',', $mutators);
+	foreach ($mutators as $m) {
+		$op = $m[0];
+		try {
+			$m = (float)substr($m, 1);
+			switch ($op) {
+				case '+':
+					$val = $val + $m;
+					break;
+				case '-':
+					$val = $val - $m;
+					break;
+				case '/':
+					$val = $val / $m;
+					break;
+				case '*':
+					$val = $val * $m;
+					break;
+				default:
+					break;
+			}
+		} catch (Exception $e) {
+			//
+		}
+	}
+	return $val;
 }

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

@@ -77,8 +77,9 @@ function parseDomain($value, $force = false)
 }
 
 // Cookie Custom Function
-function coookie($type, $name, $value = '', $days = -1, $http = true)
+function coookie($type, $name, $value = '', $days = -1, $http = true, $path = '/')
 {
+	$days = ($days > 365) ? 365 : $days;
 	if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == "https") {
 		$Secure = true;
 		$HTTPOnly = true;
@@ -92,20 +93,19 @@ function coookie($type, $name, $value = '', $days = -1, $http = true)
 	if (!$http) {
 		$HTTPOnly = false;
 	}
-	$Path = '/';
 	$Domain = parseDomain($_SERVER['HTTP_HOST']);
 	$DomainTest = parseDomain($_SERVER['HTTP_HOST'], true);
 	if ($type == 'set') {
 		$_COOKIE[$name] = $value;
 		header('Set-Cookie: ' . rawurlencode($name) . '=' . rawurlencode($value)
 			. (empty($days) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', time() + (86400 * $days)) . ' GMT')
-			. (empty($Path) ? '' : '; path=' . $Path)
+			. (empty($path) ? '' : '; path=' . $path)
 			. (empty($Domain) ? '' : '; domain=' . $Domain)
 			. (!$Secure ? '' : '; SameSite=None; Secure')
 			. (!$HTTPOnly ? '' : '; HttpOnly'), false);
 		header('Set-Cookie: ' . rawurlencode($name) . '=' . rawurlencode($value)
 			. (empty($days) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', time() + (86400 * $days)) . ' GMT')
-			. (empty($Path) ? '' : '; path=' . $Path)
+			. (empty($path) ? '' : '; path=' . $path)
 			. (empty($Domain) ? '' : '; domain=' . $DomainTest)
 			. (!$Secure ? '' : '; SameSite=None; Secure')
 			. (!$HTTPOnly ? '' : '; HttpOnly'), false);
@@ -113,20 +113,20 @@ function coookie($type, $name, $value = '', $days = -1, $http = true)
 		unset($_COOKIE[$name]);
 		header('Set-Cookie: ' . rawurlencode($name) . '=' . rawurlencode($value)
 			. (empty($days) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', time() - 3600) . ' GMT')
-			. (empty($Path) ? '' : '; path=' . $Path)
+			. (empty($path) ? '' : '; path=' . $path)
 			. (empty($Domain) ? '' : '; domain=' . $Domain)
 			. (!$Secure ? '' : '; SameSite=None; Secure')
 			. (!$HTTPOnly ? '' : '; HttpOnly'), false);
 		header('Set-Cookie: ' . rawurlencode($name) . '=' . rawurlencode($value)
 			. (empty($days) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', time() - 3600) . ' GMT')
-			. (empty($Path) ? '' : '; path=' . $Path)
+			. (empty($path) ? '' : '; path=' . $path)
 			. (empty($Domain) ? '' : '; domain=' . $DomainTest)
 			. (!$Secure ? '' : '; SameSite=None; Secure')
 			. (!$HTTPOnly ? '' : '; HttpOnly'), false);
 	}
 }
 
-function coookieSeconds($type, $name, $value = '', $ms, $http = true)
+function coookieSeconds($type, $name, $value = '', $ms, $http = true, $path = '/')
 {
 	if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == "https") {
 		$Secure = true;
@@ -141,20 +141,19 @@ function coookieSeconds($type, $name, $value = '', $ms, $http = true)
 	if (!$http) {
 		$HTTPOnly = false;
 	}
-	$Path = '/';
 	$Domain = parseDomain($_SERVER['HTTP_HOST']);
 	$DomainTest = parseDomain($_SERVER['HTTP_HOST'], true);
 	if ($type == 'set') {
 		$_COOKIE[$name] = $value;
 		header('Set-Cookie: ' . rawurlencode($name) . '=' . rawurlencode($value)
 			. (empty($ms) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', time() + ($ms / 1000)) . ' GMT')
-			. (empty($Path) ? '' : '; path=' . $Path)
+			. (empty($path) ? '' : '; path=' . $path)
 			. (empty($Domain) ? '' : '; domain=' . $Domain)
 			. (!$Secure ? '' : '; SameSite=None; Secure')
 			. (!$HTTPOnly ? '' : '; HttpOnly'), false);
 		header('Set-Cookie: ' . rawurlencode($name) . '=' . rawurlencode($value)
 			. (empty($ms) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', time() + ($ms / 1000)) . ' GMT')
-			. (empty($Path) ? '' : '; path=' . $Path)
+			. (empty($path) ? '' : '; path=' . $path)
 			. (empty($Domain) ? '' : '; domain=' . $DomainTest)
 			. (!$Secure ? '' : '; SameSite=None; Secure')
 			. (!$HTTPOnly ? '' : '; HttpOnly'), false);
@@ -162,13 +161,13 @@ function coookieSeconds($type, $name, $value = '', $ms, $http = true)
 		unset($_COOKIE[$name]);
 		header('Set-Cookie: ' . rawurlencode($name) . '=' . rawurlencode($value)
 			. (empty($ms) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', time() - 3600) . ' GMT')
-			. (empty($Path) ? '' : '; path=' . $Path)
+			. (empty($path) ? '' : '; path=' . $path)
 			. (empty($Domain) ? '' : '; domain=' . $Domain)
 			. (!$Secure ? '' : '; SameSite=None; Secure')
 			. (!$HTTPOnly ? '' : '; HttpOnly'), false);
 		header('Set-Cookie: ' . rawurlencode($name) . '=' . rawurlencode($value)
 			. (empty($ms) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', time() - 3600) . ' GMT')
-			. (empty($Path) ? '' : '; path=' . $Path)
+			. (empty($path) ? '' : '; path=' . $path)
 			. (empty($Domain) ? '' : '; domain=' . $DomainTest)
 			. (!$Secure ? '' : '; SameSite=None; Secure')
 			. (!$HTTPOnly ? '' : '; HttpOnly'), false);

+ 87 - 11
api/functions/organizr-functions.php

@@ -131,6 +131,12 @@ function organizrSpecialSettings()
 			'debugArea' => qualifyRequest($GLOBALS['debugAreaAuth']),
 			'debugErrors' => $GLOBALS['debugErrors'],
 			'sandbox' => $GLOBALS['sandbox'],
+		),
+		'menuLink' => array(
+			'githubMenuLink' => $GLOBALS['githubMenuLink'],
+			'organizrSupportMenuLink' => $GLOBALS['organizrSupportMenuLink'],
+			'organizrDocsMenuLink' => $GLOBALS['organizrDocsMenuLink'],
+			'organizrSignoutMenuLink' => $GLOBALS['organizrSignoutMenuLink']
 		)
 	);
 }
@@ -160,6 +166,7 @@ function wizardConfig($array)
 		'organizrHash' => $hashKey,
 		'organizrAPI' => $api,
 		'registrationPassword' => $registrationPassword,
+		'uuid' => gen_uuid()
 	);
 	// Create Config
 	$GLOBALS['dbLocation'] = $location;
@@ -684,6 +691,22 @@ function getSettingsMain()
 				'label' => 'Account DN',
 				'html' => '<span id="accountDN" class="ldapAuth switchAuth">' . $GLOBALS['authBackendHostPrefix'] . 'TestAcct' . $GLOBALS['authBackendHostSuffix'] . '</span>'
 			),
+			array(
+				'type' => 'switch',
+				'name' => 'ldapSSL',
+				'class' => 'ldapAuth switchAuth',
+				'label' => 'Enable LDAP SSL',
+				'value' => $GLOBALS['ldapSSL'],
+				'help' => 'This will enable the use of SSL for LDAP connections'
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'ldapSSL',
+				'class' => 'ldapAuth switchAuth',
+				'label' => 'Enable LDAP TLS',
+				'value' => $GLOBALS['ldapTLS'],
+				'help' => 'This will enable the use of TLS for LDAP connections'
+			),
 			array(
 				'type' => 'button',
 				'name' => 'test-button-ldap',
@@ -707,7 +730,7 @@ function getSettingsMain()
 				'type' => 'input',
 				'name' => 'embyURL',
 				'class' => 'embyAuth switchAuth',
-				'label' => 'Emby URL',
+				'label' => 'Emby/Jellyfin URL',
 				'value' => $GLOBALS['embyURL'],
 				'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
 				'placeholder' => 'http(s)://hostname:port'
@@ -716,7 +739,7 @@ function getSettingsMain()
 				'type' => 'password-alt',
 				'name' => 'embyToken',
 				'class' => 'embyAuth switchAuth',
-				'label' => 'Emby Token',
+				'label' => 'Emby/Jellyin Token',
 				'value' => $GLOBALS['embyToken'],
 				'placeholder' => ''
 			),
@@ -825,6 +848,10 @@ function getSettingsMain()
 						'name' => 'Allow Top Navigation',
 						'value' => 'allow-top-navigation'
 					),
+					array(
+						'name' => 'Allow Downloads',
+						'value' => 'allow-downloads'
+					),
 				)
 			),
 			array(
@@ -1236,6 +1263,30 @@ function getCustomizeAppearance()
 					'label' => 'Show Debug Errors',
 					'value' => $GLOBALS['debugErrors']
 				),
+				array(
+					'type' => 'switch',
+					'name' => 'githubMenuLink',
+					'label' => 'Show GitHub Repo Link',
+					'value' => $GLOBALS['githubMenuLink']
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'organizrSupportMenuLink',
+					'label' => 'Show Organizr Support Link',
+					'value' => $GLOBALS['organizrSupportMenuLink']
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'organizrDocsMenuLink',
+					'label' => 'Show Organizr Docs Link',
+					'value' => $GLOBALS['organizrDocsMenuLink']
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'organizrSignoutMenuLink',
+					'label' => 'Show Organizr Sign out & in Button on Sidebar',
+					'value' => $GLOBALS['organizrSignoutMenuLink']
+				),
 				array(
 					'type' => 'select',
 					'name' => 'unsortedTabs',
@@ -1750,18 +1801,18 @@ function showLogin()
 
 function checkoAuth()
 {
-	return ($GLOBALS['plexoAuth'] && $GLOBALS['authType'] !== 'internal') ? true : false;
+	return ($GLOBALS['plexoAuth'] && $GLOBALS['authBackend'] == 'plex' && $GLOBALS['authType'] !== 'internal') ? true : false;
 }
 
 function checkoAuthOnly()
 {
-	return ($GLOBALS['plexoAuth'] && $GLOBALS['authType'] == 'external') ? true : false;
+	return ($GLOBALS['plexoAuth'] && $GLOBALS['authBackend'] == 'plex' && $GLOBALS['authType'] == 'external') ? true : false;
 }
 
 function showoAuth()
 {
 	$buttons = '';
-	if ($GLOBALS['plexoAuth'] && $GLOBALS['authType'] !== 'internal') {
+	if ($GLOBALS['plexoAuth'] && $GLOBALS['authBackend'] == 'plex' && $GLOBALS['authType'] !== 'internal') {
 		$buttons .= '<a href="javascript:void(0)" onclick="oAuthStart(\'plex\')" class="btn btn-lg btn-block text-uppercase waves-effect waves-light bg-plex text-muted" data-toggle="tooltip" title="" data-original-title="Login with Plex"> <span>Login</span><i aria-hidden="true" class="mdi mdi-plex m-l-5"></i> </a>';
 	}
 	return ($buttons) ? '
@@ -1787,16 +1838,35 @@ function showoAuth()
 
 function getImages()
 {
+	$allIconsPrep = array();
+	$allIcons = array();
+	$ignore = array(".", "..", "._.DS_Store", ".DS_Store", ".pydio_id", "index.html");
 	$dirname = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'tabs' . DIRECTORY_SEPARATOR;
 	$path = 'plugins/images/tabs/';
 	$images = scandir($dirname);
-	$ignore = array(".", "..", "._.DS_Store", ".DS_Store", ".pydio_id");
-	$allIcons = array();
 	foreach ($images as $image) {
 		if (!in_array($image, $ignore)) {
-			$allIcons[] = $path . $image;
+			$allIconsPrep[$image] = array(
+				'path' => $path,
+				'name' => $image
+			);
 		}
 	}
+	$dirname = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'userTabs' . DIRECTORY_SEPARATOR;
+	$path = 'plugins/images/userTabs/';
+	$images = scandir($dirname);
+	foreach ($images as $image) {
+		if (!in_array($image, $ignore)) {
+			$allIconsPrep[$image] = array(
+				'path' => $path,
+				'name' => $image
+			);
+		}
+	}
+	ksort($allIconsPrep);
+	foreach ($allIconsPrep as $item) {
+		$allIcons[] = $item['path'] . $item['name'];
+	}
 	return $allIcons;
 }
 
@@ -1817,7 +1887,7 @@ function editImages()
 	$array = array();
 	$postCheck = array_filter($_POST);
 	$filesCheck = array_filter($_FILES);
-	$approvedPath = 'plugins/images/tabs/';
+	$approvedPath = 'plugins/images/userTabs/';
 	if (!empty($postCheck)) {
 		$removeImage = $approvedPath . pathinfo($_POST['data']['imagePath'], PATHINFO_BASENAME);
 		if ($_POST['data']['action'] == 'deleteImage' && approvedFileExtension($removeImage)) {
@@ -1831,7 +1901,7 @@ function editImages()
 		ini_set('upload_max_filesize', '10M');
 		ini_set('post_max_size', '10M');
 		$tempFile = $_FILES['file']['tmp_name'];
-		$targetPath = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'tabs' . DIRECTORY_SEPARATOR;
+		$targetPath = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'userTabs' . DIRECTORY_SEPARATOR;
 		$targetFile = $targetPath . $_FILES['file']['name'];
 		return (move_uploaded_file($tempFile, $targetFile)) ? true : false;
 	}
@@ -2616,7 +2686,13 @@ function importUserButtons()
 	';
 	$buttons = '';
 	if (!empty($GLOBALS['plexToken'])) {
-		$buttons .= '<button class="btn bg-plex text-muted waves-effect waves-light importUsersButton" onclick="importUsers(\'plex\')" type="button"><span class="btn-label"><i class="mdi mdi-plex"></i></span><span lang="en">Import Plex Users</span></button>';
+		$buttons .= '<button class="btn m-b-20 m-r-20 bg-plex text-muted waves-effect waves-light importUsersButton" onclick="importUsers(\'plex\')" type="button"><span class="btn-label"><i class="mdi mdi-plex"></i></span><span lang="en">Import Plex Users</span></button>';
+	}
+	if (!empty($GLOBALS['embyURL']) && !empty($GLOBALS['embyToken']) && (strpos($GLOBALS['embyURL'], 'jellyfin') !== false)) {
+		$buttons .= '<button class="btn m-b-20 m-r-20 bg-primary text-muted waves-effect waves-light importUsersButton" onclick="importUsers(\'jellyfin\')" type="button"><span class="btn-label"><i class="mdi mdi-fish"></i></span><span lang="en">Import Jellyfin Users</span></button>';
+	}
+	if (!empty($GLOBALS['embyURL']) && !empty($GLOBALS['embyToken']) && (strpos($GLOBALS['embyURL'], 'jellyfin') === false)) {
+		$buttons .= '<button class="btn m-b-20 m-r-20 bg-emby text-muted waves-effect waves-light importUsersButton" onclick="importUsers(\'emby\')" type="button"><span class="btn-label"><i class="mdi mdi-emby"></i></span><span lang="en">Import Jellyfin Users</span></button>';
 	}
 	return ($buttons !== '') ? $buttons : $emptyButtons;
 }

+ 4 - 1
api/functions/sso-functions.php

@@ -15,7 +15,7 @@ function ssoCheck($username, $password, $token = null)
 		$tautulliToken = getTautulliToken($username, $password, $token);
 		if ($tautulliToken) {
 			foreach ($tautulliToken as $key => $value) {
-				coookie('set', 'tautulli_token_' . $value['uuid'], $value['token'], $GLOBALS['rememberMeDays']);
+				coookie('set', 'tautulli_token_' . $value['uuid'], $value['token'], $GLOBALS['rememberMeDays'], true, $value['path']);
 			}
 		}
 	}
@@ -74,8 +74,11 @@ function getTautulliToken($username, $password, $plexToken = null)
 				$options = (localURL($url)) ? array('verify' => false) : array();
 				$response = Requests::post($url . '/auth/signin', $headers, $data, $options);
 				if ($response->success) {
+					$qualifiedURL = qualifyURL($url, true);
+					$path = ($qualifiedURL['path']) ? $qualifiedURL['path'] : '/';
 					$token[$key]['token'] = json_decode($response->body, true)['token'];
 					$token[$key]['uuid'] = json_decode($response->body, true)['uuid'];
+					$token[$key]['path'] = $path;
 					writeLog('success', 'Tautulli Token Function - Grabbed token from: ' . $url, $username);
 				} else {
 					writeLog('error', 'Tautulli Token Function - Error on URL: ' . $url, $username);

+ 1 - 1
api/functions/static-globals.php

@@ -1,7 +1,7 @@
 <?php
 // ===================================
 // Organizr Version
-$GLOBALS['installedVersion'] = '2.0.570';
+$GLOBALS['installedVersion'] = '2.0.650';
 // ===================================
 // Quick php Version check
 $GLOBALS['minimumPHP'] = '7.1.3';

+ 2 - 1
api/functions/token-functions.php

@@ -54,6 +54,7 @@ function createToken($username, $email, $image, $group, $groupID, $key, $days =
 	if (!isset($GLOBALS['dbLocation']) || !isset($GLOBALS['dbName'])) {
 		return false;
 	}
+	$days = ($days > 365) ? 365 : $days;
 	//Quick get user ID
 	try {
 		$database = new Dibi\Connection([
@@ -138,7 +139,7 @@ function validateToken($token, $global = false)
 			} catch (Dibi\Exception $e) {
 				$GLOBALS['organizrUser'] = false;
 			}
-		}else{
+		} else {
 			// Delete cookie & reload page
 			coookie('delete', $GLOBALS['cookieName']);
 			$GLOBALS['organizrUser'] = false;

+ 1 - 1
api/pages/settings-tab-editor-homepage-order.php

@@ -3,7 +3,7 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
 	$pageSettingsTabEditorHomepageOrder = '
 <script>
     $("#homepage-items-sort").sortable({
-	    placeholder:    "sort-placeholder col-md-3 col-xs-12 clearfix",
+	    placeholder:    "sort-placeholder col-md-3 col-xs-12 m-t-10 clearfix",
 	    forcePlaceholderSize: true,
 	    start: function( e, ui ){
 	        ui.item.data( "start-pos", ui.item.index()+1 );

+ 2 - 2
api/pages/settings.php

@@ -8,7 +8,7 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
         sponsorLoad();
         newsLoad();
         checkCommitLoad();
-        [].slice.call(document.querySelectorAll(\'.sttabs\')).forEach(function(el) {
+        [].slice.call(document.querySelectorAll(\'.sttabs-main-settings-div\')).forEach(function(el) {
             new CBPFWTabs(el);
         });
     })();
@@ -30,7 +30,7 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
     <div class="row">
         <!-- Tab style start -->
         <section class="">
-            <div class="sttabs tabs-style-flip">
+            <div class="sttabs sttabs-main-settings-div tabs-style-flip">
                 <nav>
                     <ul>
                         <li onclick="changeSettingsMenu(\'Settings::Tab Editor\')" id="settings-main-tab-editor-anchor"><a href="#settings-main-tab-editor" class="sticon ti-layout-tab-v"><span lang="en">Tab Editor</span></a></li>

+ 26 - 1
css/organizr.css

@@ -1070,9 +1070,15 @@ input.has-success {
 ul.nav.customtab.nav-tabs.nav-low-margin {
     margin: -25px -25px 0px -25px !important;
 }
-i.fa.fa-life-ring.fa-fw {
+#menu-Organizr-Support i {
     color: #C62828;
 }
+#menu-GitHub-Repo i {
+    color: #2cabe3;
+}
+#menu-Organizr-Docs i {
+    color: #707cd2;
+}
 .ping {
     position: relative;
     margin-top: 0;
@@ -4387,3 +4393,22 @@ html {
 .noty_layout [data-name~="mojs-shape"] {
     pointer-events:none
 }
+.bottom-close-splash {
+    position: absolute;
+    width: 130px;
+    right: 20px;
+    bottom: 20px;
+}
+.sort-placeholder {
+    background-size: contain;
+    height: 60px;
+    border-radius: 10px;
+    border: 3px dotted whitesmoke;
+    opacity: .5;
+    font-size: 13px;
+    margin: 1px 0 -9px;
+    text-align: left;
+    background: #1f1f1f;
+    padding-right: 0px !important;
+    padding-left: 0px !important;
+}

Plik diff jest za duży
+ 0 - 0
css/organizr.min.css


+ 3 - 3
index.php

@@ -1,4 +1,4 @@
-<?php include 'api/functions/static-globals.php';?>
+<?php include 'api/functions/static-globals.php'; ?>
 <!DOCTYPE html>
 <html lang="en" ontouchmove>
 
@@ -256,7 +256,7 @@
 <script src="plugins/bower_components/jquery-wizard-master/libs/formvalidation/bootstrap.min.js"></script>
 <script src="js/bowser.min.js"></script>
 <script src="js/jasny-bootstrap.js"></script>
-<script src="js/cbpFWTabs.js"></script>
+<script src="js/cbpFWTabs.js?v=<?php echo $GLOBALS['fileHash']; ?>"></script>
 <script src="js/js.cookie.js"></script>
 <script src="js/jquery-lang.js"></script>
 <script src="js/jquery-ui.min.js"></script>
@@ -287,7 +287,7 @@
 <script src="plugins/bower_components/mousetrap/mousetrap.min.js"></script>
 <script src="plugins/bower_components/bootstrap-treeview-master/dist/bootstrap-treeview.min.js"></script>
 <script src="plugins/bower_components/jquery.easy-pie-chart/dist/jquery.easypiechart.min.js"></script>
-<script src="/js/gauge.min.js"></script>
+<script src="js/gauge.min.js"></script>
 <script src="js/jquery.mousewheel.min.js"></script>
 <script src="js/ua-parser.min.js"></script>
 <script src="js/plyr.js"></script>

+ 9 - 5
js/cbpFWTabs.js

@@ -40,11 +40,15 @@
 		// current index
 		this.current = -1;
 		// show current content item
-        if(this.tabs[0].innerHTML.indexOf('#settings') >= 0){
-            this._show(5);
-        }else{
-            this._show();
-        }
+		try{
+			if(this.tabs[0].innerHTML.indexOf('#settings') >= 0){
+				this._show(5);
+			}else{
+				this._show();
+			}
+		}catch{
+			this._show();
+		}
 		// init events
 		this._initEvents();
 	};

+ 1 - 1
js/custom.js

@@ -1993,7 +1993,7 @@ $(document).on('click', 'li a[aria-controls="Custom data"]', function() {
     var resizeEditor = function(jsonEditor) {
         const aceEditor = jsonEditor;
         const newHeight = aceEditor.getSession().getScreenLength() * (aceEditor.renderer.lineHeight + aceEditor.renderer.scrollBar.getWidth());
-        aceEditor.container.style.height = `${newHeight}px`;
+        aceEditor.container.style.height = newHeight + 'px';
         aceEditor.resize();
     }
 

Plik diff jest za duży
+ 0 - 0
js/custom.min.js


+ 95 - 61
js/functions.js

@@ -204,7 +204,7 @@ function isNumberKey(evt) {
     return true;
 }
 function setTabInfo(tab,action,value){
-    if(tab == 'Organizr-Support'){
+    if(tab == 'Organizr-Support' || tab == 'Organizr-Docs'){
         return false;
     }
     if(tab !== null && action !== null && value !== null){
@@ -2582,20 +2582,23 @@ function userMenu(user){
 }
 function menuExtras(active){
     var supportFrame = buildFrameContainer('Organizr Support','https://organizr.app/support',1);
-    var adminMenu = (activeInfo.user.groupID <= 1) ? buildMenuList('Organizr Support','https://organizr.app/support',1,'fontawesome::life-ring'): '';
+    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'));
 	if(active === true){
-		return `
+		return (activeInfo.settings.menuLink.organizrSignoutMenuLink) ? `
 			<li class="devider"></li>
 			<li id="sign-out"><a class="waves-effect" onclick="logout();"><i class="fa fa-sign-out fa-fw"></i> <span class="hide-menu" lang="en">Logout</span></a></li>
-			<li class="devider"></li>
-			<li id="github"><a href="https://github.com/causefx/organizr" target="_blank" class="waves-effect"><i class="fa fa-github fa-fw text-success"></i> <span class="hide-menu">GitHub</span></a></li>
-		`+adminMenu;
+		` + adminMenu : '' + adminMenu;
 	}else{
-		return `
+		return (activeInfo.settings.menuLink.organizrSignoutMenuLink) ? `
 			<li class="devider"></li>
 			<li id="menu-login"><a class="waves-effect show-login" href="javascript:void(0)"><i class="mdi mdi-login fa-fw"></i> <span class="hide-menu" lang="en">Login/Register</span></a></li>
-		`;
+		` : '';
 	}
 }
 function categoryProcess(arrayItems){
@@ -2795,7 +2798,7 @@ function buildSplashScreen(json){
         <section id="splashScreen" class="lock-screen splash-screen fade in">
             <div class="row p-20 flexbox">`+items+`</div>
             <div class="row p-20 p-t-0 flexbox">
-                <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 col-xl-12 mouse hvr-wobble-bottom" onclick="$('.splash-screen').addClass('hidden').removeClass('in')">
+                <div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 col-xl-12 mouse hvr-wobble-bottom bottom-close-splash" onclick="$('.splash-screen').addClass('hidden').removeClass('in')">
                     <div class="homepage-drag fc-event bg-danger lazyload"  data-src="">
                         <span class="homepage-text">&nbsp; Close Splash</span>
                     </div>
@@ -3302,7 +3305,10 @@ function newsLoad(){
         try {
             var response = JSON.parse(data);
             var items = [];
+            var limit = 5;
+            var count = 0;
             $.each(response, function(i,v) {
+                count++;
                 var newBody = `
                 <h5 class="pull-left">`+moment(v.date).format('LLL')+`</h5>
                 <h5 class="pull-right">`+v.author+`</h5>
@@ -3310,9 +3316,11 @@ function newsLoad(){
                 `+((v.subTitle) ? '<h5>' + v.subTitle + '</h5>' : '' )+`
                 <p>`+v.body+`</p>
                 `;
-                items[i] = {
-                    title:v.title,
-                    body:newBody
+                if(count <= limit){
+                    items[i] = {
+                        title:v.title,
+                        body:newBody
+                    }
                 }
             });
             var body = buildAccordion(items, true);
@@ -3731,7 +3739,7 @@ function marketplaceJSON(type) {
 }
 function allIcons() {
     return $.ajax({
-        url: "/js/icons.json",
+        url: "js/icons.json",
     });
 }
 function organizrConnect(path){
@@ -4816,7 +4824,7 @@ function buildRequest(array){
 			<div class="white-box m-b-0 search-div resultBox-outside">
 				<div class="form-group m-b-0">
 					<div id="request-input-div" class="input-group">
-						<input id="request-input" lang="en" placeholder="Request Show or Movie" type="text" class="form-control inline-focus">
+						<input id="request-input" lang="en" placeholder="Request a Show or Movie" type="text" class="form-control inline-focus">
                         <input id="request-page" type="hidden" class="form-control">
                         <div class="input-group-btn">
                             <button type="button" class="btn waves-effect waves-light btn-info dropdown-toggle" data-toggle="dropdown" aria-expanded="false"><span lang="en">Suggestions</span> <span class="caret"></span></button>
@@ -6094,9 +6102,12 @@ function buildPiholeItem(array){
             <div class="card text-white mb-3 pihole-stat bg-green">
                 <div class="card-body">
                     <div class="inline-block">
-                        <p>Total queries</p>`;
+                        <p class="d-inline mr-1">Total queries</p>`;
         for(var key in data) {
             var e = data[key];
+            if(length > 1 && !combine) {
+                card += `<p class="d-inline text-muted">(`+key+`)</p>`;
+            }
             card += `<h3 data-toggle="tooltip" data-placement="right" title="`+key+`">`+e['dns_queries_today'].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")+`</h3>`;
         };
         card += `
@@ -6114,9 +6125,12 @@ function buildPiholeItem(array){
             <div class="card bg-inverse text-white mb-3 pihole-stat bg-aqua">
                 <div class="card-body">
                     <div class="inline-block">
-                        <p>Queries Blocked</p>`;
+                        <p class="d-inline mr-1">Queries Blocked</p>`;
         for(var key in data) {
             var e = data[key];
+            if(length > 1 && !combine) {
+                card += `<p class="d-inline text-muted">(`+key+`)</p>`;
+            }
             card += `<h3 data-toggle="tooltip" data-placement="right" title="`+key+`">`+e['ads_blocked_today'].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")+`</h3>`;
         };
         card += `
@@ -6134,9 +6148,12 @@ function buildPiholeItem(array){
             <div class="card bg-inverse text-white mb-3 pihole-stat bg-yellow">
                 <div class="card-body">
                     <div class="inline-block">
-                        <p>Percent Blocked</p>`;
+                        <p class="d-inline mr-1">Percent Blocked</p>`;
         for(var key in data) {
             var e = data[key];
+            if(length > 1 && !combine) {
+                card += `<p class="d-inline text-muted">(`+key+`)</p>`;
+            }
             card += `<h3 data-toggle="tooltip" data-placement="right" title="`+key+`">`+e['ads_percentage_today'].toFixed(1)+`%</h3>`
         };
         card += `
@@ -6154,9 +6171,12 @@ function buildPiholeItem(array){
             <div class="card bg-inverse text-white mb-3 pihole-stat bg-red">
                 <div class="card-body">
                     <div class="inline-block">
-                        <p>Domains on Blocklist</p>`;
+                        <p class="d-inline mr-1">Domains on Blocklist</p>`;
         for(var key in data) {
             var e = data[key];
+            if(length > 1 && !combine) {
+                card += `<p class="d-inline text-muted">(`+key+`)</p>`;
+            }
             card += `<h3 data-toggle="tooltip" data-placement="right" title="`+key+`">`+e['domains_being_blocked'].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")+`</h3>`;
         };
         card += `
@@ -6177,15 +6197,6 @@ function buildPiholeItem(array){
         stats += '</div>';
     } else {
         for(var key in array['data']) {
-            if(length > 1) {
-                stats += `
-                <div class="row mb-2">
-                    <div class="col-sm-12">
-                        `+key+`
-                    </div>
-                </div>
-                `;
-            }
             var data = array['data'][key];
             obj = {};
             obj[key] = data;
@@ -6588,13 +6599,13 @@ function buildTautulliItem(array){
             var section_name = null;
             if(type == 'movie'){
                 extraField = 'Movies';
-                section_name = 'Movie Libaries';
+                section_name = 'Movie Libraries';
             }else if(type == 'show'){
                 extraField = 'Shows/Seasons/Episodes';
-                section_name = 'TV Show Libaries';
+                section_name = 'TV Show Libraries';
             }else if(type == 'artist'){
                 extraField = 'Artists/Albums/Tracks';
-                section_name = 'Music Libaries';
+                section_name = 'Music Libraries';
             }
             var cardTitle = '<th><span class="pull-left cardTitle">'+section_name.toUpperCase()+'</span><span class="pull-right cardCountType">'+extraField.toUpperCase()+'</th>';
             var card = `
@@ -7039,7 +7050,7 @@ var html = `
         <div class="white-box text-white p-0">
             <!-- Tabstyle start -->
             <section class="">
-                <div class="sttabs tabs-style-iconbox">
+                <div class="sttabs sttabs-main-weather-health-div tabs-style-iconbox">
                     <nav>
                         <ul>${healthHeader}</ul>
                     </nav>
@@ -7053,7 +7064,7 @@ var html = `
     </div>
     <script>
         (function() {
-            [].slice.call(document.querySelectorAll('.sttabs')).forEach(function(el) {
+            [].slice.call(document.querySelectorAll('.sttabs-main-weather-health-div')).forEach(function(el) {
                 new CBPFWTabs(el);
             });
         })();
@@ -7081,7 +7092,7 @@ function buildPollutant(array){
         <div class="white-box text-white p-0">
             <!-- Tabstyle start -->
             <section class="">
-                <div class="sttabs tabs-style-iconbox">
+                <div class="sttabs sttabs-main-weather-pollutant-div tabs-style-iconbox">
                     <nav>
                         <ul>${pollutantHeader}</ul>
                     </nav>
@@ -7095,7 +7106,7 @@ function buildPollutant(array){
     </div>
     <script>
         (function() {
-            [].slice.call(document.querySelectorAll('.sttabs')).forEach(function(el) {
+            [].slice.call(document.querySelectorAll('.sttabs-main-weather-pollutant-div')).forEach(function(el) {
                 new CBPFWTabs(el);
             });
         })();
@@ -7127,10 +7138,22 @@ function buildMonitorrItem(array){
     var cards = '';
     var options = array['options'];
     var services = array['services'];
+    var tabName = '';
 
     var buildCard = function(name, data) {
-        if(data.status) { var statusColor = 'success'; var imageText = 'fa fa-check-circle text-success' } 
-            else { var statusColor = 'danger animated-3 loop-animation flash'; var imageText = 'fa fa-times-circle text-danger'}
+        if(data.status == true) {
+            var statusColor = 'success'; var imageText = 'fa fa-check-circle text-success'
+        } else if (data.status == 'unresponsive') {
+            var statusColor = 'warning animated-3 loop-animation flash'; var imageText = 'fa fa-times-circle text-warning'
+        } else {
+            var statusColor = 'danger animated-3 loop-animation flash'; var imageText = 'fa fa-times-circle text-danger'
+        }
+        if(typeof data.link !== 'undefined' && data.link.includes('#')) {
+            tabName = data.link.substring(data.link.indexOf('#')+1);
+            monitorrLink = '<a href="javascript:void(0)" onclick="tabActions(event,\''+tabName+'\',1)">';
+        } else if(typeof data.link !== 'undefined') {
+            monitorrLink = '<a href="'+data.link+'" target="_blank">'
+        }
         if(options['compact']) {
             var card = `
             <div class="col-xl-2 col-lg-3 col-md-4 col-sm-6 col-xs-12">
@@ -7140,7 +7163,7 @@ function buildMonitorrItem(array){
                             <div class="left-health bg-`+statusColor+`"></div>
                             <div class="ml-1 w-100">
                                 <i class="`+imageText+` font-20 pull-right mt-3 mb-2"></i>
-                                `; if (typeof data.link !== 'undefined') { card +=`<a href="`+data.link+`" target="_blank">`; }
+                                `; if (typeof data.link !== 'undefined') { card += monitorrLink; }
                                 card += `<h3 class="d-flex no-block align-items-center mt-2 mb-2"><img class="lazyload loginTitle" src="`+data.image+`">&nbsp;`+name+`</h3>
                                 `; if (typeof data.link !== 'undefined') { card +=`</a>`; }
                                 card += `<div class="clearfix"></div>
@@ -7160,7 +7183,7 @@ function buildMonitorrItem(array){
                             <img class="monitorrImage" src="`+data.image+`" alt="service icon">
                         </div>
                         <div class="d-inline-block mt-4 py-2 px-4 badge indicator bg-`+statusColor+`">
-                            <p class="mb-0">`; if(data.status) { card += 'ONLINE' } else { card += 'OFFLINE' } card+=`</p>
+                            <p class="mb-0">`; if(data.status == true) { card += 'ONLINE' } else if(data.status == 'unresponsive') { card += 'UNRESPONSIVE' } else { card += 'OFFLINE' } card+=`</p>
                         </div>
                         `; if (typeof data.link !== 'undefined') { card +=`</a>`; }
                         card += `</div>
@@ -7177,27 +7200,31 @@ function buildMonitorrItem(array){
 }
 function buildMonitorr(array){
     if(array === false){ return ''; }
-    var services = (typeof array.services !== 'undefined') ? Object.keys(array.services).length : false;
-    var html = `
-    <div id="allMonitorr">
-		<div class="el-element-overlay row">`
-    if(array['options']['titleToggle']) {
+    if(array.error != undefined) {
+        console.log('Monitorr error: ' + array.error);
+    } else {
+        var services = (typeof array.services !== 'undefined') ? Object.keys(array.services).length : false;
+        var html = `
+        <div id="allMonitorr">
+            <div class="el-element-overlay row">`
+        if(array['options']['titleToggle']) {
+            html += `
+                <div class="col-md-12">
+                    <h4 class="pull-left homepage-element-title"><span lang="en">`+array['options']['title']+`</span> : </h4><h4 class="pull-left">&nbsp;<span class="label label-info m-l-20 checkbox-circle good-monitorr-services mouse" onclick="homepageMonitorr()">`+services+`</span></h4></h4>
+                    <hr class="hidden-xs ml-2">
+                </div>
+                <div class="clearfix"></div>
+            `;
+        }
         html += `
-            <div class="col-md-12">
-                <h4 class="pull-left homepage-element-title"><span lang="en">`+array['options']['title']+`</span> : </h4><h4 class="pull-left">&nbsp;<span class="label label-info m-l-20 checkbox-circle good-monitorr-services mouse" onclick="homepageMonitorr()">`+services+`</span></h4></h4>
-                <hr class="hidden-xs ml-2">
+                <div class="monitorrCards">
+                    `+buildMonitorrItem(array)+`
+                </div>
             </div>
-            <div class="clearfix"></div>
+        </div>
+        <div class="clearfix"></div>
         `;
     }
-    html += `
-            <div class="monitorrCards">
-                `+buildMonitorrItem(array)+`
-			</div>
-		</div>
-    </div>
-    <div class="clearfix"></div>
-    `;
     return (array) ? html : '';
 }
 function homepageMonitorr(timeout){
@@ -8119,15 +8146,21 @@ function youtubeSearch(searchQuery) {
 function youtubeCheck(title,link){
 	youtubeSearch(title).success(function(data) {
         var response = JSON.parse(data);
-		inlineLoad();
-		var id = response.data.items["0"].id.videoId;
-		var div = `
+        console.log(data)
+		if(response.data){
+			inlineLoad();
+			var id = response.data.items["0"].id.videoId;
+			var div = `
 		<div id="player-`+link+`" data-plyr-provider="youtube" data-plyr-embed-id="`+id+`"></div>
 		<div class="clearfix"></div>
 		`;
-		$('.youtube-div').html(div);
-		$('.'+link).trigger('click');
-		player = new Plyr('#player-'+link);
+			$('.youtube-div').html(div);
+			$('.'+link).trigger('click');
+			player = new Plyr('#player-'+link);
+		}else{
+			messageSingle('API Limit Reached','YouTube API Error',activeInfo.settings.notifications.position,'#FFF','error','5000');
+		}
+
 	}).fail(function(xhr) {
 		console.error("Organizr Function: YouTube Connection Failed");
 	});
@@ -8232,6 +8265,7 @@ function changeAuth(){
         case 'emby_local':
         case 'emby_connect':
         case 'emby_all':
+	    case 'jellyfin':
             $('.switchAuth').parent().parent().parent().hide();
             $('.backendAuth').parent().parent().parent().show();
             $('.embyAuth').parent().parent().parent().show();

+ 10 - 3
js/news.json

@@ -1,23 +1,30 @@
 [
+  {
+    "title": "Plex OAuth Patch",
+    "subTitle": "You server hostname will now be listed on OAuth box",
+    "date": "2020-06-18 16:30",
+    "body": "Plex has now fixed the 3 open CVE's pertaining to the ability for an attacker to phish for an Plex Admins token.  Please follow this link to read more about it.  <a href=\"https:\/\/www.bleepingcomputer.com\/news\/security\/plex-fixes-media-server-bugs-allowing-full-system-takeover\/\" target=\"_blank\" rel=\"noopener noreferrer\">Plex Vulnerabilities Patched<\/a>",
+    "author": "CauseFX"
+  },
   {
     "title": "Tab Redirect Loops and SameSite Cookie Issues",
     "subTitle": "New Browser Restrictions",
     "date": "2020-04-01 19:30",
-    "body": "It seems there are a lot of issues happening with redirect Loops and SameSite cookie issues.  If you are having issues, please read this document.  <a href=\"https:\/\/docs.organizr.app\/books\/troubleshooting\/page\/redirect-looping---samesite-errors\" target=\"_blank\">Organizr SameSite Docs<\/a>",
+    "body": "It seems there are a lot of issues happening with redirect Loops and SameSite cookie issues.  If you are having issues, please read this document.  <a href=\"https:\/\/docs.organizr.app\/books\/troubleshooting\/page\/redirect-looping---samesite-errors\" target=\"_blank\" rel=\"noopener noreferrer\">Organizr SameSite Docs<\/a>",
     "author": "CauseFX"
   },
   {
     "title": "Plex Oauth Issues - RESOLVED!",
     "subTitle": "Let's make them bring back support correctly",
     "date": "2019-07-17 15:20",
-    "body": "It seems like Plex has broken support for us using Oauth.  Currently there is a workaround but we would rather they fix the issue.  Please share your feedback in this thread: <a href=\"https:\/\/forums.plex.tv\/t\/plex-oauth-not-working-with-tautulli-ombi-etc\/433945\" target=\"_blank\">Plex Oauth Discussion<\/a>",
+    "body": "It seems like Plex has broken support for us using Oauth.  Currently there is a workaround but we would rather they fix the issue.  Please share your feedback in this thread: <a href=\"https:\/\/forums.plex.tv\/t\/plex-oauth-not-working-with-tautulli-ombi-etc\/433945\" target=\"_blank\" rel=\"noopener noreferrer\">Plex Oauth Discussion<\/a>",
     "author": "CauseFX"
   },
   {
     "title": "Emby Discontinued Support",
     "subTitle": "Emby API - EmbyConnect Deprecated",
     "date": "2019-05-14 19:15",
-    "body": "Emby has discontinued support for matching users against Emby Connect via API.  Therefore, we will no longer be supporting this method of Authentication for Organizr.  If Emby decides to support this again, I will re-enable it once more.  You can find more information here: <a href=\"https:\/\/github.com\/MediaBrowser\/Emby\/issues\/3553\" target=\"_blank\">Emby API Discussion<\/a>",
+    "body": "Emby has discontinued support for matching users against Emby Connect via API.  Therefore, we will no longer be supporting this method of Authentication for Organizr.  If Emby decides to support this again, I will re-enable it once more.  You can find more information here: <a href=\"https:\/\/github.com\/MediaBrowser\/Emby\/issues\/3553\" target=\"_blank\" rel=\"noopener noreferrer\">Emby API Discussion<\/a>",
     "author": "CauseFX"
   },
   {

+ 21 - 0
js/version.json

@@ -250,5 +250,26 @@
     "new": "Weather Homepage Item|Pi-Hole Homepage Item|Tautulli Homepage Item|Monitorr Homepage Item|Netdata Homepage Item|SpeedTest Homepage Item|HealthChecks Plugin|Bandwidth info to Now Playing|Config files can now contain arrays|Scrape API endpoint|New API Docs WIP located at /api/docs",
     "fixed": "iOS icon issue|Language Bug|Cache Image timing|fix hardcoded plugins directory for root (#1341)|Ombi Posters|Plex Timeouts|Indent cleanup|Homepage Item shortcut in Tab Editor broken (#1368)|Empty login form submitting|Sanitize username going into log (#1359)|Ombi Div for tv items|edit sttabs for reflect more than just settings tabs|Emby HomePage Add-in - Item Details URL Needs updating (#1290)|Fix JQuery error on settings save (#1356)",
     "notes": "Added note about Samesite issues|New Radarr Logo|Please report bugs in GitHub issues page"
+  },
+  "2.0.631": {
+    "date": "2020-06-19 19:15",
+    "title": "Whoa - sorry for the wait",
+    "new": "Added css bottom-close-splash",
+    "fixed": "Netadata fixes|Docker update fixes|Tautulli Fixes|Hardcode fixes for days beyond one year|Typos|NZBHydra tab image|fix stray / in index.php and js/functions.php (#1408)|LDAP with STARTTLS support (#1411)",
+    "notes": "Please report bugs in GitHub issues page"
+  },
+  "2.0.633": {
+    "date": "2020-06-20 02:01",
+    "title": "Small Bugfix",
+    "new": "",
+    "fixed": "Fix heredoc error on php versions under 7.3",
+    "notes": "Please report bugs in GitHub issues page"
+  },
+  "2.0.650": {
+    "date": "2020-07-03 20:55",
+    "title": "Weekly Updates",
+    "new": "Added Jellyfin Auth function to log into Organizr|Re-Enable EmbyConnect backend option|Option to disable logout/login button on sidebar",
+    "fixed": "Plex oAuth login enabled after switching to a different backend (#1416)|losing access to settings (#1418)|Youtube Error catch and added another API Key(#1419)|Update coookie function to include path|Update Tautulli cookie to include path|News.json fixes|homepage sort issue on right side|add uuid on wizard path",
+    "notes": "Please report bugs in GitHub issues page"
   }
 }

BIN
plugins/images/tabs/nzbhydra.png


BIN
plugins/images/tabs/requestrr.png


BIN
plugins/images/tabs/xbackbone.png


+ 10 - 0
plugins/images/userTabs/index.html

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Image folder for User</title>
+</head>
+<body>
+Tab images go here
+</body>
+</html>

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików