Ver Fonte

Merge pull request #1952 from causefx/v2-develop

V2 develop
causefx há 2 anos atrás
pai
commit
529f85119a

+ 149 - 97
api/classes/organizr.class.php

@@ -64,10 +64,11 @@ class Organizr
 	use UnifiHomepageItem;
 	use WeatherHomepageItem;
 	use uTorrentHomepageItem;
+	use UptimeKumaHomepageItem;
 
 	// ===================================
 	// Organizr Version
-	public $version = '2.1.2430';
+	public $version = '2.1.2460';
 	// ===================================
 	// Quick php Version check
 	public $minimumPHP = '7.4';
@@ -1453,7 +1454,7 @@ class Organizr
 		$status = [];
 		$dependenciesActive = [];
 		$dependenciesInactive = [];
-		$extensions = ['PDO_SQLITE', 'PDO', 'SQLITE3', 'zip', 'cURL', 'openssl', 'simplexml', 'json', 'session', 'filter'];
+		$extensions = ['PDO_SQLITE', 'PDO', 'SQLITE3', 'zip', 'cURL', 'openssl', 'simplexml', 'json', 'session', 'filter', 'fileinfo', 'mbstring'];
 		$functions = ['hash', 'fopen', 'fsockopen', 'fwrite', 'fclose', 'readfile'];
 		foreach ($extensions as $check) {
 			if (extension_loaded($check)) {
@@ -1501,7 +1502,7 @@ class Organizr
 		$status = array();
 		$dependenciesActive = array();
 		$dependenciesInactive = array();
-		$extensions = array('PDO_SQLITE', 'PDO', 'SQLITE3', 'zip', 'cURL', 'openssl', 'simplexml', 'json', 'session', 'filter');
+		$extensions = array('PDO_SQLITE', 'PDO', 'SQLITE3', 'zip', 'cURL', 'openssl', 'simplexml', 'json', 'session', 'filter', 'fileinfo', 'mbstring');
 		$functions = array('hash', 'fopen', 'fsockopen', 'fwrite', 'fclose', 'readfile');
 		foreach ($extensions as $check) {
 			if (extension_loaded($check)) {
@@ -2045,32 +2046,34 @@ class Organizr
 	public function uploadImage()
 	{
 		$filesCheck = array_filter($_FILES);
-		if (!empty($filesCheck)) {
-			if (strpos($_FILES['file']['type'], 'image/') === false) {
-				$this->setResponse(403, 'File Type not approved', $_FILES['file']['type']);
-				return false;
-			}
-			if (!$this->approvedFileType($_FILES['file']['tmp_name'])) {
-				$this->setResponse(403, 'File Type not approved', $_FILES['file']['tmp_name']);
-				return false;
-			}
-			if ($this->approvedFileExtension($_FILES['file']['name'])) {
-				ini_set('upload_max_filesize', '10M');
-				ini_set('post_max_size', '10M');
-				$tempFile = $_FILES['file']['tmp_name'];
-				$targetPath = $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'userTabs' . DIRECTORY_SEPARATOR;
-				$this->makeDir($targetPath);
-				$targetFile = $targetPath . $this->sanitizeUserString($_FILES['file']['name']);
-				$this->setAPIResponse(null, pathinfo($_FILES['file']['name'], PATHINFO_BASENAME) . ' has been uploaded', null);
-				return move_uploaded_file($tempFile, $targetFile);
-			} else {
-				$this->setResponse(403, 'File Extension not approved');
-				return false;
-			}
-		} else {
+		if (empty($filesCheck)) {
 			$this->setResponse(500, 'No File was uploaded');
 			return false;
 		}
+		if ($_FILES['file']['tmp_name'] == '') {
+			$this->setResponse(500, 'File upload error');
+			return false;
+		}
+		if (strpos($_FILES['file']['type'], 'image/') === false) {
+			$this->setResponse(403, 'File Type not image', $_FILES['file']['type']);
+			return false;
+		}
+		if (!$this->approvedFileType($_FILES['file']['tmp_name'])) {
+			$this->setResponse(403, 'File Type not approved', $_FILES['file']['tmp_name']);
+			return false;
+		}
+		if (!$this->approvedFileExtension($_FILES['file']['name'])) {
+			$this->setResponse(403, 'File Extension not approved');
+			return false;
+		}
+		ini_set('upload_max_filesize', '10M');
+		ini_set('post_max_size', '10M');
+		$tempFile = $_FILES['file']['tmp_name'];
+		$targetPath = $this->root . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'userTabs' . DIRECTORY_SEPARATOR;
+		$this->makeDir($targetPath);
+		$targetFile = $targetPath . $this->sanitizeUserString($_FILES['file']['name']);
+		$this->setAPIResponse(null, pathinfo($_FILES['file']['name'], PATHINFO_BASENAME) . ' has been uploaded', null);
+		return move_uploaded_file($tempFile, $targetFile);
 	}
 
 	public function formatPingHost($host)
@@ -2197,6 +2200,7 @@ class Organizr
 			],
 			'Options' => [
 				$this->settingsOption('switch', 'alternateHomepageHeaders', ['label' => 'Alternate Homepage Titles']),
+				$this->settingsOption('switch', 'disableHomepageModals', ['label' => 'Disable Homepage Saved Modal', 'help' => 'Disable the modal when saving homepage config settings']),
 				$this->settingsOption('switch', 'debugErrors', ['label' => 'Show Debug Errors']),
 				$this->settingsOption('switch', 'easterEggs', ['label' => 'Show Easter Eggs']),
 				$this->settingsOption('input', 'gaTrackingID', ['label' => 'Google Analytics Tracking ID', 'placeholder' => 'e.g. UA-XXXXXXXXX-X']),
@@ -2351,9 +2355,16 @@ class Organizr
 	{
 		$certificateStatus = $this->hasCustomCert() ? '<span lang="en">Custom Certificate Loaded</span><br />Located at <span>' . $this->getCustomCert() . '</span>' : '<span lang="en">Custom Certificate not found - please upload below</span>';
 		$settings = [
+			'Socks' => [
+				$this->settingsOption('switch', 'socksDebug', ['label' => 'Enable Debug Log', 'help' => 'Enable the option to have socks output to logs']),
+				$this->settingsOption('number', 'maxSocksDebugSize', ['label' => 'Max Debug Data Rows', 'help' => 'Max amount of rows in debug log', 'attr' => 'min="1"'])
+			],
 			'Settings Page' => [
 				$this->settingsOption('select', 'defaultSettingsTab', ['label' => 'Default Settings Tab', 'options' => $this->getSettingsTabs(), 'help' => 'Choose which Settings Tab to be default when opening settings page']),
 			],
+			'Other' => [
+				$this->settingsOption('switch', 'checkForUpdate', ['label' => 'Check For Update', 'help' => 'Check for update on Organizr load']),
+			],
 			'Github' => [
 				$this->settingsOption('select', 'branch', ['label' => 'Branch', 'value' => $this->config['branch'], 'options' => $this->getBranches(), 'disabled' => $this->docker, 'help' => ($this->docker) ? 'Since you are using the Official Docker image, Change the image to change the branch' : 'Choose which branch to download from']),
 				$this->settingsOption('button', 'force-install-branch', ['label' => 'Force Install Branch', 'class' => 'updateNow', 'icon' => 'fa fa-download', 'text' => 'Retrieve', 'attr' => ($this->docker) ? 'title="You can just restart your docker to update"' : '', 'help' => ($this->docker) ? 'Since you are using the official Docker image, you can just restart your Docker container to update Organizr' : 'This will re-download all of the source files for Organizr']),
@@ -4081,6 +4092,26 @@ class Organizr
 		return $this->processQueries($response);
 	}
 
+	public function revokeTokensByUserId($userId = null)
+	{
+		if (!$userId) {
+			$this->setAPIResponse('error', 'User Id was not supplied', 422);
+			return false;
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'DELETE FROM tokens WHERE user_id = ?',
+					$userId,
+				)
+			),
+		];
+		$this->setAPIResponse('success', 'User Tokens revoked', 204);
+		$this->setLoggerChannel('User Management')->info('Revoked all tokens for deleted user', ['id' => $userId]);
+		return $this->processQueries($response);
+	}
+
 	public function updateUserPassword($password, $id)
 	{
 		$response = [
@@ -4410,6 +4441,8 @@ class Organizr
 				'collapseSideMenuOnClick' => $this->config['allowCollapsableSideMenu'] && $this->config['collapseSideMenuOnClick'],
 				'authProxyOverrideLogout' => $this->config['authProxyOverrideLogout'],
 				'authProxyLogoutURL' => $this->config['authProxyLogoutURL'],
+				'disableHomepageModals' => $this->config['disableHomepageModals'],
+				'checkForUpdate' => $this->config['checkForUpdate']
 			],
 			'menuLink' => [
 				'githubMenuLink' => $this->config['githubMenuLink'],
@@ -4669,6 +4702,13 @@ class Organizr
 						$class .= ' faded';
 					}
 					break;
+				case 'homepageOrderUptimeKuma':
+					$class = 'bg-info';
+					$image = 'plugins/images/tabs/kuma.png';
+					if (!$this->config['homepageUptimeKumaEnabled']) {
+						$class .= ' faded';
+					}
+					break;
 				case 'homepageOrderWeatherAndAir':
 					$class = 'bg-success';
 					$image = 'plugins/images/tabs/wind.png';
@@ -6702,15 +6742,15 @@ class Organizr
 			$this->setAPIResponse('error', 'Cannot delete your own user', 409);
 			return false;
 		}
-		if ($userInfo) {
-			$this->setLoggerChannel('User Management');
-			$this->logger->info('Deleted User [' . $userInfo['username'] . ']');
-			$this->setAPIResponse('success', 'User deleted', 204);
-			return $this->processQueries($response);
-		} else {
+		if (!$userInfo) {
 			$this->setAPIResponse('error', 'id not found', 404);
 			return false;
 		}
+		$this->setLoggerChannel('User Management');
+		$this->logger->info('Deleted User [' . $userInfo['username'] . ']');
+		$this->revokeTokensByUserId($id);
+		$this->setAPIResponse('success', 'User deleted', 204);
+		return $this->processQueries($response);
 	}
 
 	public function addUser($array)
@@ -7529,73 +7569,85 @@ class Organizr
 				$this->setAPIResponse('error', 'Multiple endpoint accessed but multiple URLs not found in field, please use /api/v2/socks endpoint', 409);
 			}
 		}
-		if (!$error) {
-			if ($multiple) {
-				$instance = $multiple - 1;
-				$pre = explode('/api/v2/multiple/socks/', $requestObject->getUri()->getPath());
-				$pre[1] = $this->replace_first('/' . $multiple . '/', '/', $pre[1]);
-				// sent url twice since we arent using tokens
-				$list = $this->csvHomepageUrlToken($this->config[$url], $this->config[$url]);
-				$appURL = $list[$instance]['url'];
-			} else {
-				$pre = explode('/api/v2/socks/', $requestObject->getUri()->getPath());
-				$appURL = $this->config[$url];
-			}
-			$endpoint = explode('/', $pre[1]);
-			$new = urldecode(preg_replace('/' . $endpoint[0] . '/', '', $pre[1], 1));
-			$getParams = ($_GET) ? '?' . http_build_query($_GET) : '';
-			$url = $this->qualifyURL($appURL) . $new . $getParams;
-			$url = $this->cleanPath($url);
-			$options = ($this->localURL($appURL)) ? array('verify' => false, 'timeout' => 120) : array('timeout' => 120);
-			$headers = [];
-			$apiData = $this->apiData($requestObject, false);
-			if ($header) {
-				if ($requestObject->hasHeader($header)) {
-					$headerKey = $requestObject->getHeaderLine($header);
-					$headers[$header] = $headerKey;
-				}
-			}
-			if ($requestObject->hasHeader('Content-Type')) {
-				$headerKey = $requestObject->getHeaderLine('Content-Type');
-				$headers['Content-Type'] = $headerKey;
-			}
-			$debugInformation = [
-				'type' => $requestObject->getMethod(),
-				'headerType' => $requestObject->getHeaderLine('Content-Type'),
-				'header' => $header,
-				'headers' => $headers,
-				'url' => $url,
-				'options' => $options,
-				'data' => $apiData,
-			];
-			$this->setLoggerChannel('Socks');
-			$this->logger->debug('Sending Socks request', $debugInformation);
-			try {
-				switch ($requestObject->getMethod()) {
-					case 'GET':
-						$call = Requests::get($url, $headers, $options);
-						break;
-					case 'POST':
-						$call = Requests::post($url, $headers, $apiData, $options);
-						break;
-					case 'DELETE':
-						$call = Requests::delete($url, $headers, $options);
-						break;
-					case 'PUT':
-						$call = Requests::put($url, $headers, $apiData, $options);
-						break;
-					default:
-						$call = Requests::get($url, $headers, $options);
+
+		if ($error) {
+			return null;
+		}
+
+		if ($multiple) {
+			$instance = $multiple - 1;
+			$pre = explode('/api/v2/multiple/socks/', $requestObject->getUri()->getPath());
+			$pre[1] = $this->replace_first('/' . $multiple . '/', '/', $pre[1]);
+			// sent url twice since we arent using tokens
+			$list = $this->csvHomepageUrlToken($this->config[$url], $this->config[$url]);
+			$appURL = $list[$instance]['url'];
+		} else {
+			$pre = explode('/api/v2/socks/', $requestObject->getUri()->getPath());
+			$appURL = $this->config[$url];
+		}
+		$endpoint = explode('/', $pre[1]);
+		$new = urldecode(preg_replace('/' . $endpoint[0] . '/', '', $pre[1], 1));
+		$getParams = ($_GET) ? '?' . http_build_query($_GET) : '';
+		$url = $this->qualifyURL($appURL) . $new . $getParams;
+		$url = $this->cleanPath($url);
+		$options = ($this->localURL($appURL)) ? ['verify' => false, 'timeout' => 120] : ['timeout' => 120];
+		$headers = [];
+		$apiData = $this->apiData($requestObject, false);
+		if ($header) {
+			if ($requestObject->hasHeader($header)) {
+				$headerKey = $requestObject->getHeaderLine($header);
+				$headers[$header] = $headerKey;
+			}
+		}
+		if ($requestObject->hasHeader('Content-Type')) {
+			$headerKey = $requestObject->getHeaderLine('Content-Type');
+			$headers['Content-Type'] = $headerKey;
+		}
+		$debugInformation = [
+			'type' => $requestObject->getMethod(),
+			'headerType' => $requestObject->getHeaderLine('Content-Type'),
+			'header' => $header,
+			'headers' => $headers,
+			'url' => $url,
+			'options' => $options,
+			'data' => $apiData,
+		];
+		$this->setLoggerChannel('Socks')->debug('Sending Socks request', $debugInformation);
+		try {
+			switch ($requestObject->getMethod()) {
+				case 'GET':
+					$call = Requests::get($url, $headers, $options);
+					break;
+				case 'POST':
+					$call = Requests::post($url, $headers, $apiData, $options);
+					break;
+				case 'DELETE':
+					$call = Requests::delete($url, $headers, $options);
+					break;
+				case 'PUT':
+					$call = Requests::put($url, $headers, $apiData, $options);
+					break;
+				default:
+					$call = Requests::get($url, $headers, $options);
+			}
+			if ($this->config['socksDebug']) {
+				if ($this->json_validator($call->body)) {
+					$logData = json_decode($call->body, true);
+					$size = (!is_numeric($this->config['maxSocksDebugSize']) || $this->config['maxSocksDebugSize'] == 0) ? 1 : $this->config['maxSocksDebugSize'];
+					if (count($logData) > $size) {
+						$logData = 'Count too large to output';
+					}
+				} else {
+					$logData = $call->body;
 				}
-				$this->logger->debug('Socks Response', $this->json_validator($call->body) ? json_decode($call->body, true) : $call->body);
-				return $call->body;
-			} catch (Requests_Exception $e) {
-				$this->setResponse(500, $e->getMessage());
-				$this->setLoggerChannel('Socks');
-				$this->logger->critical($e, $debugInformation);
-				return null;
+			} else {
+				$logData = 'Debug not enabled';
 			}
-		} else {
+			$this->setLoggerChannel('Socks')->debug('Socks Response', ['body' => $logData, 'debug' => $debugInformation]);
+			return $call->body;
+		} catch (Requests_Exception $e) {
+			$this->setResponse(500, $e->getMessage());
+			$this->setLoggerChannel('Socks')->critical($e, $debugInformation);
 			return null;
 		}
 	}

+ 15 - 1
api/config/default.php

@@ -376,6 +376,7 @@ return [
 	'homepageOrderDonate' => '41',
 	'homepageOrderAdguard' => '42',
 	'homepageOrderProwlarr' => '43',
+	'homepageOrderUptimeKuma' => '44',
 	'homepageShowStreamNames' => false,
 	'homepageShowStreamNamesAuth' => '1',
 	'homepageShowStreamNamesWithoutIp' => false,
@@ -694,5 +695,18 @@ return [
 	'slackLogWebhook' => '',
 	'slackLogWebHookChannel' => '',
 	'matchUserAgents' => false,
-	'matchUserIP' => false
+	'matchUserIP' => false,
+	'disableHomepageModals' => false,
+	'homepageUptimeKumaEnabled' => false,
+	'homepageUptimeKumaAuth' => '1',
+	'uptimeKumaURL' => '',
+	'uptimeKumaToken' => '',
+	'homepageUptimeKumaRefresh' => '60000',
+	'homepageUptimeKumaHeader' => 'Uptime Kuma',
+	'homepageUptimeKumaHeaderToggle' => true,
+	'homepageUptimeKumaCompact' => true,
+	'homepageUptimeKumaShowLatency' => true,
+	'checkForUpdate' => true,
+	'socksDebug' => false,
+	'maxSocksDebugSize' => 100
 ];

+ 88 - 0
api/functions/UptimeKumaMetrics.php

@@ -0,0 +1,88 @@
+<?php
+
+class UptimeKumaMetrics
+{
+    protected string $raw;
+    private array $monitors = [];
+
+    public function __construct(string $raw)
+    {
+        $this->raw = $raw;
+    }
+
+    public function process(): self
+    {
+        $processed = explode(PHP_EOL, $this->raw);
+
+        $monitors = array_filter($processed, function (string $item) {
+            return str_starts_with($item, 'monitor_status');
+        });
+        // TODO: parse the latencies and add them on to the info card
+        $latencies = array_filter($processed, function (string $item) {
+            return str_starts_with($item, 'monitor_response_time');
+        });
+        
+        $monitors = array_map(function (string $item) {
+            return $this->parseMonitorStatus($item);
+        }, $monitors);
+        $this->addLatencyToMonitors($monitors, $latencies);
+        $this->monitors = array_values(array_filter($monitors));
+
+        return $this;
+    }
+
+    public function getMonitors(): array
+    {
+        return $this->monitors;
+    }
+
+    private function parseMonitorStatus(string $status): ?array
+    {  
+        if (substr($status, -1) === '2') {
+            return null;
+		}
+
+		$up = (substr($status, -1)) == '0' ? false : true;
+		$status = substr($status, 15);
+		$status = substr($status, 0, -4);
+		$status = explode(',', $status);
+		$data = [
+			'name' => $this->getStringBetweenQuotes($status[0]),
+			'url' => $this->getStringBetweenQuotes($status[2]),
+			'type' => $this->getStringBetweenQuotes($status[1]),
+			'status' => $up,
+		];
+
+		return $data;
+    }
+
+    private function addLatencyToMonitors(array &$monitors, array $latencies)
+    {
+        $latencies = $this->getLatenciesByName($latencies);
+        foreach ($monitors as &$monitor) {
+            $monitor['latency'] = $latencies[$monitor['name']] ?? null;
+        }
+    }
+
+    private function getLatenciesByName(array $latencies): array
+    {
+        $l = [];
+
+        foreach ($latencies as $latency) {
+            if (preg_match('/monitor_name="(.*)",monitor_type.* ([0-9]{1,})$/', $latency, $match)) {
+                $l[$match[1]] = (int) $match[2];
+            }
+            continue;
+        }
+
+        return $l;
+    }
+
+    private function getStringBetweenQuotes(string $input): string
+	{
+		if (preg_match('/"(.*?)"/', $input, $match) == 1) {
+			return $match[1];
+		}
+		return '';
+	} 
+}

+ 17 - 20
api/homepage/sonarr.php

@@ -239,7 +239,7 @@ trait SonarrHomepageItem
 	public function formatSonarrCalendar($array, $number)
 	{
 		$array = json_decode($array, true);
-		$gotCalendar = array();
+		$gotCalendar = [];
 		$i = 0;
 		foreach ($array as $child) {
 			$i++;
@@ -250,12 +250,11 @@ trait SonarrHomepageItem
 			if (!isset($episodeID)) {
 				$episodeID = "";
 			}
-			//$episodeName = htmlentities($child['title'], ENT_QUOTES);
 			$episodeAirDate = $child['airDateUtc'];
 			$episodeAirDate = strtotime($episodeAirDate);
 			$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
 			if (new DateTime() < new DateTime($episodeAirDate)) {
-				$unaired = true;
+				$unAired = true;
 			}
 			if ($child['episodeNumber'] == "1") {
 				$episodePremier = "true";
@@ -267,32 +266,30 @@ trait SonarrHomepageItem
 				$child['airDateUtc'] = gmdate('Y-m-d\TH:i:s\Z', strtotime($date->format(DateTime::ATOM)));
 			}
 			$downloaded = $child['hasFile'];
-			if ($downloaded == "0" && isset($unaired) && $episodePremier == "true") {
+			if ($downloaded == "0" && isset($unAired) && $episodePremier == "true") {
 				$downloaded = "text-primary animated flash";
-			} elseif ($downloaded == "0" && isset($unaired) && $monitored == "0") {
+			} elseif ($downloaded == "0" && isset($unAired) && $monitored == "0") {
 				$downloaded = "text-dark";
-			} elseif ($downloaded == "0" && isset($unaired)) {
+			} elseif ($downloaded == "0" && isset($unAired)) {
 				$downloaded = "text-info";
 			} elseif ($downloaded == "1") {
 				$downloaded = "text-success";
 			} else {
 				$downloaded = "text-danger";
 			}
-			$fanart = "/plugins/images/homepage/no-np.png";
+			$fanArt = "/plugins/images/homepage/no-np.png";
 			foreach ($child['series']['images'] as $image) {
-				if ($image['coverType'] == "fanart") {
-					$fanart = $image['url'];
+				if ($image['coverType'] == "fanart" && (isset($image['url']) && $image['url'] !== '')) {
+					$fanArt = $image['url'];
 				}
 			}
-			if ($fanart !== "/plugins/images/homepage/no-np.png" || (strpos($fanart, '://') === false)) {
+			if ($fanArt !== "/plugins/images/homepage/no-np.png" || (strpos($fanArt, '://') === false)) {
 				$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
-				$imageURL = $fanart;
-				$cacheFile = $cacheDirectory . $seriesID . '.jpg';
-				$fanart = 'data/cache/' . $seriesID . '.jpg';
-				if (!file_exists($cacheFile)) {
+				$imageURL = $fanArt;
+				$fanArt = 'data/cache/' . $seriesID . '.jpg';
+				if (!file_exists($cacheDirectory . $seriesID . '.jpg')) {
 					$this->cacheImage($imageURL, $seriesID);
 					unset($imageURL);
-					unset($cacheFile);
 				}
 			}
 			$bottomTitle = 'S' . sprintf("%02d", $child['seasonNumber']) . 'E' . sprintf("%02d", $child['episodeNumber']) . ' - ' . $child['title'];
@@ -305,14 +302,14 @@ trait SonarrHomepageItem
 				$href = $href . '/series/' . preg_replace('/[^A-Za-z0-9 -]/', '', str_replace('&', 'and', preg_replace('/[[:space:]]+/', '-', $seriesName)));
 				$href = str_replace("//series/", "/series/", $href);
 			}
-			$details = array(
+			$details = [
 				"seasonCount" => $child['series']['seasonCount'] ?? isset($child['series']['seasons']) ? count($child['series']['seasons']) : 0,
 				"status" => $child['series']['status'],
 				"topTitle" => $seriesName,
 				"bottomTitle" => $bottomTitle,
 				"overview" => $child['overview'] ?? '',
 				"runtime" => $child['series']['runtime'],
-				"image" => $fanart,
+				"image" => $fanArt,
 				"ratings" => $child['series']['ratings']['value'],
 				"videoQuality" => $child["hasFile"] && isset($child['episodeFile']['quality']['quality']['name']) ? $child['episodeFile']['quality']['quality']['name'] : "unknown",
 				"audioChannels" => $child["hasFile"] && isset($child['episodeFile']['mediaInfo']) ? $child['episodeFile']['mediaInfo']['audioChannels'] : "unknown",
@@ -324,8 +321,8 @@ trait SonarrHomepageItem
 				"icon" => "/plugins/images/tabs/sonarr.png",
 				"frame" => $this->config['sonarrFrameTarget'],
 				"showLink" => $this->config['sonarrIcon']
-			);
-			array_push($gotCalendar, array(
+			];
+			$gotCalendar[] = [
 				"id" => "Sonarr-" . $number . "-" . $i,
 				"title" => $seriesName,
 				"start" => $child['airDateUtc'],
@@ -335,7 +332,7 @@ trait SonarrHomepageItem
 				"downloadFilter" => $downloaded,
 				"bgColor" => str_replace('text', 'bg', $downloaded),
 				"details" => $details
-			));
+			];
 		}
 		if ($i != 0) {
 			return $gotCalendar;

+ 127 - 0
api/homepage/uptime_kuma.php

@@ -0,0 +1,127 @@
+<?php
+
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\GuzzleException;
+
+trait UptimeKumaHomepageItem
+{
+	private static Client $kumaClient;
+
+	public function uptimeKumaSettingsArray($infoOnly = false)
+	{
+		$homepageInformation = [
+			'name' => 'UptimeKuma',
+			'enabled' => true,
+			'image' => 'plugins/images/tabs/kuma.png',
+			'category' => 'Monitor',
+			'settingsArray' => __FUNCTION__
+		];
+		if ($infoOnly) {
+			return $homepageInformation;
+		}
+		$homepageSettings = [
+			'debug' => true,
+			'settings' => [
+				'Enable' => [
+					$this->settingsOption('enable', 'homepageUptimeKumaEnabled'),
+					$this->settingsOption('auth', 'homepageUptimeKumaAuth'),
+				],
+				'Connection' => [
+					$this->settingsOption('url', 'uptimeKumaURL', ['help' => 'URL for Uptime Kuma e.g. http://kuma:3001 (no trailing slash)', 'placeholder' => 'http://kuma:3001']),
+					$this->settingsOption('token', 'uptimeKumaToken'),
+				],
+				'Options' => [
+					$this->settingsOption('refresh', 'homepageUptimeKumaRefresh'),
+					$this->settingsOption('title', 'homepageUptimeKumaHeader'),
+					$this->settingsOption('toggle-title', 'homepageUptimeKumaHeaderToggle'),
+					$this->settingsOption('switch', 'homepageUptimeKumaCompact', ['label' => 'Compact view', 'help' => 'Toggles the compact view of this homepage module']),
+					$this->settingsOption('switch', 'homepageUptimeKumaShowLatency', ['label' => 'Show monitor latency']),
+				],
+			]
+		];
+		return array_merge($homepageInformation, $homepageSettings);
+	}
+
+	public function uptimeKumaHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageUptimeKumaEnabled'
+				],
+				'auth' => [
+					'homepageUptimeKumaAuth'
+				],
+				'not_empty' => [
+					'uptimeKumaURL',
+					'uptimeKumaToken',
+				]
+			]
+		];
+		return $this->homepageCheckKeyPermissions($key, $permissions);
+	}
+
+	public function homepageOrderUptimeKuma()
+	{
+		if ($this->homepageItemPermissions($this->uptimeKumaHomepagePermissions('main'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Uptime Kuma...</h2></div>
+					<script>
+						// Uptime Kuma
+						homepageUptimeKuma("' . $this->config['homepageUptimeKumaRefresh'] . '");
+						// End Uptime Kuma
+					</script>
+				</div>
+				';
+		}
+	}
+
+	public function getUptimeKumaHomepageData()
+	{
+		if (!$this->homepageItemPermissions($this->uptimeKumaHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$api = [];
+		$url = $this->qualifyURL($this->config['uptimeKumaURL']);
+		try {
+			$metrics = (new UptimeKumaMetrics(
+				$this->getKumaClient($url, $this->config['uptimeKumaToken'])
+					->get('/metrics')
+					->getBody()
+					->getContents()
+			))->process();
+
+			$api = [
+				'data' => $metrics->getMonitors(),
+				'options' => [
+					'title' => $this->config['homepageUptimeKumaHeader'],
+					'titleToggle' => $this->config['homepageUptimeKumaHeaderToggle'],
+					'compact' => $this->config['homepageUptimeKumaCompact'],
+					'showLatency' => $this->config['homepageUptimeKumaShowLatency'],
+				]
+			];
+		} catch (GuzzleException $e) {
+			$this->setLoggerChannel('UptimeKuma')->error($e);
+			$this->setAPIResponse('error', $e->getMessage(), 401);
+			return false;
+		};
+		$api = isset($api) ? $api : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+
+	private function getKumaClient(string $url, string $token): Client
+	{
+		if (!isset(static::$kumaClient)) {
+			static::$kumaClient = new Client([
+				'base_uri' => $url,
+				'headers' => [
+					'Authorization' => sprintf("Basic %s", base64_encode(':'.$token)),
+				],
+			]);
+		}
+
+		return static::$kumaClient;
+	}
+}

+ 31 - 28
api/pages/settings-image-manager.php

@@ -14,40 +14,43 @@ function get_page_settings_image_manager($Organizr)
 	return '
 <script>
 	buildImageManagerView();
-    var myDropzone = new Dropzone("#new-image-form", {
-      url: "api/v2/image",
-      headers:{ "formKey": local("g","formKey") },
-      init: function() {
-        this.on("complete", function(file) {
-            if(file["status"] === "success"){
-                buildImageManagerView();
-            }else{
-                let response = JSON.parse(file.xhr.responseText);
-            	message("Upload Error", response.response.message,activeInfo.settings.notifications.position,"#FFF","error","5000");
-            }
-        });
-      }
-    });
+	const myDropzone = new Dropzone("#new-image-form", {
+		url: "api/v2/image",
+		headers:{ "formKey": local("g","formKey") },
+		init: function() {
+		this.on("complete", function(file) {
+			if(file["status"] === "success"){
+				buildImageManagerView();
+			}else{
+				let response = JSON.parse(file.xhr.responseText);
+				message("Upload Error", response.response.message,activeInfo.settings.notifications.position,"#FFF","error","5000");
+			}
+		});
+		this.on("error", function(file, response) {
+			$(file.previewElement).find(".dz-error-message").text(response.response.message);
+		});
+	  }
+	});
 </script>
 <div class="panel bg-org panel-info">
-    <div class="panel-heading">
+	<div class="panel-heading">
 		<span lang="en">View Images</span>
-        <button type="button" class="btn btn-info btn-circle pull-right popup-with-form m-r-5" href="#new-image-form" data-effect="mfp-3d-unfold"><i class="fa fa-upload"></i> </button>
+		<button type="button" class="btn btn-info btn-circle pull-right popup-with-form m-r-5" href="#new-image-form" data-effect="mfp-3d-unfold"><i class="fa fa-upload"></i> </button>
+	</div>
+	<div class="panel-wrapper collapse in" aria-expanded="true">
+		<div class="panel-body bg-org" >
+			<div id="gallery-content">
+				<div id="gallery-content-center" class="settings-image-manager-list"></div>
+			</div>
+		</div>
 	</div>
-    <div class="panel-wrapper collapse in" aria-expanded="true">
-        <div class="panel-body bg-org" >
-        	<div id="gallery-content">
-                <div id="gallery-content-center" class="settings-image-manager-list"></div>
-            </div>
-        </div>
-    </div>
 </div>
 <form action="#" id="new-image-form" class="mfp-hide white-popup-block mfp-with-anim dropzone" enctype="multipart/form-data">
-    <h1 lang="en">Upload Image</h1>
-    <div class="fallback">
-        <input name="file" type="file" multiple />
-    </div>
-    <div class="clearfix"></div>
+	<h1 lang="en">Upload Image</h1>
+	<div class="fallback">
+		<input name="file" type="file" multiple />
+	</div>
+	<div class="clearfix"></div>
 </form>
 ';
 }

+ 8 - 0
api/v2/routes/homepage.php

@@ -308,6 +308,14 @@ $app->get('/homepage/monitorr/data', function ($request, $response, $args) {
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 });
+$app->get('/homepage/kuma/data', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$Organizr->getUptimeKumaHomepageData();
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
 $app->get('/homepage/speedtest/data', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
 	$Organizr->getSpeedtestHomepageData();

+ 118 - 2
js/functions.js

@@ -3643,7 +3643,7 @@ function submitSettingsForm(form, homepageItem = false){
 				organizrCatchError(e,data);
 			}
 			if(callbacks){ callbacks.fire(); }
-			if(homepageItem) {
+			if(homepageItem && !activeInfo.settings.misc.disableHomepageModals) {
 				let html = `
 		        <div class="panel panel-default">
                     <div class="panel-heading">${response.message}</div>
@@ -9087,6 +9087,122 @@ function homepageMonitorr(timeout){
     timeouts[timeoutTitle] = setTimeout(function(){ homepageMonitorr(timeout); }, timeout);
     delete timeout;
 }
+function buildUptimeKumaItem(array){
+    var cards = '';
+    var options = array['options'];
+    var services = array['data'];
+    var tabName = '';
+
+    var buildCard = function(name, data) {
+        if(data.status == true) {
+            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'
+        }
+        tabName = data.name;
+        kumaLink = '<a href="javascript:void(0)" onclick="tabActions(event,\''+tabName+'\',1)">';
+        if(options['compact']) {
+            var card = `
+            <div class="col-xl-2 col-lg-3 col-md-4 col-sm-6 col-xs-12">
+                <div class="card bg-inverse text-white mb-3 monitorr-card">
+                    <div class="card-body bg-org-alt pt-1 pb-1">
+                        <div class="d-flex no-block align-items-center">
+                            <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.url !== 'undefined') { card += kumaLink; }
+                                card += `<h3 class="d-flex no-block align-items-center mt-2 mb-2"><img class="lazyload loginTitle">&nbsp;`+data.name;
+                                if (data.latency != null && options.showLatency) {
+                                    card += `<span class="ml-3 font-12 align-self-center text-dark">`+data.latency+`ms</span></h3>`
+                                }
+                                card += `</h3>`
+                                if (typeof data.url !== 'undefined') { card +=`</a>`; }
+                                card += `<div class="clearfix"></div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>`;
+        } else {
+            var card = `
+            <div class="col-lg-2 col-md-3 col-sm-4 col-xs-6">
+                <div class="card bg-inverse text-white mb-3 monitorr-card">
+                    <div class="card-body bg-org-alt text-center">
+                        `; if (typeof data.url !== 'undefined') { card +=`<a href="`+data.url+`" target="_blank">`; }
+                        card += `<div class="d-block">
+                            <h3 class="mt-0 mb-2">`+data.name+`</h3>`
+                            
+                        if (data.latency != null && options.showLatency) {
+                            card += `<p class="text-dark mb-0">`+data.latency+`ms</p>`
+                        }
+
+                        card += `</div>
+                        <div class="d-inline-block mt-4 py-2 px-4 badge indicator bg-`+statusColor+`">
+                            <p class="mb-0">`; if(data.status == true) { card += 'ONLINE' } else { card += 'OFFLINE' } card+=`</p>
+                        </div>
+                        `; if (typeof data.url !== 'undefined') { card +=`</a>`; }
+                        card += `</div>
+                </div>
+            </div>
+            `;
+        }
+        return card;
+    }
+    for(var key in services) {
+        cards += buildCard(key, services[key]);
+    };
+    return cards;
+}
+function buildUptimeKuma(array){
+    if(array === false){ return ''; }
+    if(array.error != undefined) {
+	    organizrConsole('Uptime Kuma Function',array.error, 'error');
+    } else {
+        var html = `
+        <div id="allUptimeKuma">
+            <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>
+                    <hr class="hidden-xs ml-2">
+                </div>
+                <div class="clearfix"></div>
+            `;
+        }
+        html += `
+                <div class="uptimeKumaCards">
+                    `+buildUptimeKumaItem(array)+`
+                </div>
+            </div>
+        </div>
+        <div class="clearfix"></div>
+        `;
+    }
+    return (array) ? html : '';
+}
+function homepageUptimeKuma(timeout){
+    var timeout = (typeof timeout !== 'undefined') ? timeout : activeInfo.settings.homepage.refresh.homepageUptimeKumaRefresh;
+    organizrAPI2('GET','api/v2/homepage/kuma/data').success(function(data) {
+        try {
+            let response = data.response;
+	        document.getElementById('homepageOrderUptimeKuma').innerHTML = '';
+	        if(response.data !== null){
+		        buildUptimeKuma(response.data)
+		        $('#homepageOrderUptimeKuma').html(buildUptimeKuma(response.data));
+	        }
+        }catch(e) {
+            console.log(e)
+	        organizrCatchError(e,data);
+        }
+    }).fail(function(xhr) {
+	    OrganizrApiError(xhr);
+    });
+    let timeoutTitle = 'UptimeKuma-Homepage';
+    if(typeof timeouts[timeoutTitle] !== 'undefined'){ clearTimeout(timeouts[timeoutTitle]); }
+    timeouts[timeoutTitle] = setTimeout(function(){ homepageUptimeKuma(timeout); }, timeout);
+    delete timeout;
+}
 function homepageSpeedtest(timeout){
     var timeout = (typeof timeout !== 'undefined') ? timeout : activeInfo.settings.homepage.refresh.homepageSpeedtestRefresh;
     organizrAPI2('GET','api/v2/homepage/speedtest/data').success(function(data) {
@@ -12118,7 +12234,7 @@ function OrganizrApiError(xhr, secondaryMessage = null){
 	return false;
 }
 function checkForUpdates(){
-	if(activeInfo.user.loggedin && activeInfo.user.groupID <= 1){
+	if(activeInfo.user.loggedin && activeInfo.user.groupID <= 1 && activeInfo.settings.misc.checkForUpdate){
 		updateCheck();
 		checkCommitLoad();
 		checkPluginUpdates();

+ 7 - 0
js/version.json

@@ -628,5 +628,12 @@
     "new": "added check for gitlock file on docker update|added check for response to Overseerr test|Added check for response to the testConnectionOverseerr method",
     "fixed": "Fix JDownloader API authentification|fix Overseerr homepage item does not read the username properly (#1915)|Truly Fix JDownloader API authentification",
     "notes": "or MyJD-API \u003E= 1.0.4|Requires FeedCrawler \u003E= 16.0.4|Updated the following languages: [Arabic (Saudi Arabia)]|update shebang for portability (#1899) (#1908) (#1906) (#1881)"
+  },
+  "2.1.2460": {
+    "date": "2023-11-21 19:44",
+    "title": "Long Lost Update",
+    "new": "added autobrr tab image|added base config|added check for file upload to see if upload was successful before mime check|added check on sonarr if url is not set (#1943)|added config item disableHomepageModals|added extension checks for mbstring and fileinfo|added function revokeTokensByUserId|added js to render homepage item|added kuma data route|added latency to monitors|added Navidrome logo|added settings page for kuma|Added setting to toggle compact mode|added socksDebug and maxSocksDebugSize config values|added TODO comment|added toggle for displaying latency|Added uptime kuma homepage item",
+    "fixed": "fixed A removed user can still use organizr until they logout (#1925)|fix homepage item name to remove space|set limit to log debugger for socks output - fixes Radarr etc",
+    "notes": "cleanup image error|formatting|Move autobrr image to correct dir|move parsing logic into a class|pop latency in the monitors array|turn off the update available notification (#1938)|updated socks to reflect big requests|update kuma function to get and parse prometheus metrics"
   }
 }

BIN
plugins/images/tabs/autobrr.png


BIN
plugins/images/tabs/kuma.png


BIN
plugins/images/tabs/navidrome.png