ソースを参照

Merge pull request #1742 from causefx/v2-develop

V2 develop
causefx 4 年 前
コミット
2065f690fc

+ 2 - 0
.gitignore

@@ -72,6 +72,8 @@ test.php
 users.db
 users.db
 speedtest.db
 speedtest.db
 chatpack.db
 chatpack.db
+api.json
+Cron.txt
 Docker.txt
 Docker.txt
 Github.txt
 Github.txt
 Demo.txt
 Demo.txt

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

@@ -1,5 +1,6 @@
 <?php
 <?php
 
 
+use Nekonomokochan\PhpJsonLogger\Logger;
 use Nekonomokochan\PhpJsonLogger\LoggerBuilder;
 use Nekonomokochan\PhpJsonLogger\LoggerBuilder;
 
 
 class OrganizrLogger extends LoggerBuilder
 class OrganizrLogger extends LoggerBuilder
@@ -22,13 +23,13 @@ class OrganizrLogger extends LoggerBuilder
 		$this->isReady = $readyStatus;
 		$this->isReady = $readyStatus;
 	}
 	}
 	
 	
-	public function build(): OrganizrLoggerExt
+	public function build(): Logger
 	{
 	{
 		if (!$this->isReady) {
 		if (!$this->isReady) {
 			$this->setChannel('Organizr');
 			$this->setChannel('Organizr');
 			$this->setLogLevel(self::DEBUG);
 			$this->setLogLevel(self::DEBUG);
 			$this->setMaxFiles(1);
 			$this->setMaxFiles(1);
 		}
 		}
-		return new OrganizrLoggerExt($this);
+		return new Logger($this);
 	}
 	}
 }
 }

+ 3 - 139
api/classes/loggerExt.class.php

@@ -1,140 +1,4 @@
 <?php
 <?php
-
-use Nekonomokochan\PhpJsonLogger\Logger;
-
-class OrganizrLoggerExt extends Logger
-{
-	/**
-	 * @param $message
-	 * @param $context
-	 */
-	public function debug($message, $context = '')
-	{
-		$context = $this->formatParamToArray($context);
-		$this->addDebug($message, $context);
-	}
-	
-	/**
-	 * @param $message
-	 * @param $context
-	 */
-	public function info($message, $context = '')
-	{
-		$context = $this->formatParamToArray($context);
-		$this->addInfo($message, $context);
-	}
-	
-	/**
-	 * @param $message
-	 * @param $context
-	 */
-	public function notice($message, $context = '')
-	{
-		$context = $this->formatParamToArray($context);
-		$this->addNotice($message, $context);
-	}
-	
-	/**
-	 * @param $message
-	 * @param $context
-	 */
-	public function warning($message, $context = '')
-	{
-		$context = $this->formatParamToArray($context);
-		$this->addWarning($message, $context);
-	}
-	
-	/**
-	 * @param \Throwable $e
-	 * @param            $context
-	 */
-	public function error($e, $context = '')
-	{
-		$context = $this->formatParamToArray($context);
-		if ($this->isErrorObject($e) === false) {
-			throw new \InvalidArgumentException(
-				$this->generateInvalidArgumentMessage(__METHOD__)
-			);
-		}
-		$this->addError(get_class($e), $this->formatPhpJsonLoggerErrorsContext($e, $context));
-	}
-	
-	/**
-	 * @param \Throwable $e
-	 * @param            $context
-	 */
-	public function critical($e, $context = '')
-	{
-		$context = $this->formatParamToArray($context);
-		if ($this->isErrorObject($e) === false) {
-			throw new \InvalidArgumentException(
-				$this->generateInvalidArgumentMessage(__METHOD__)
-			);
-		}
-		$this->addCritical(get_class($e), $this->formatPhpJsonLoggerErrorsContext($e, $context));
-	}
-	
-	/**
-	 * @param \Throwable $e
-	 * @param            $context
-	 */
-	public function alert($e, $context = '')
-	{
-		$context = $this->formatParamToArray($context);
-		if ($this->isErrorObject($e) === false) {
-			throw new \InvalidArgumentException(
-				$this->generateInvalidArgumentMessage(__METHOD__)
-			);
-		}
-		$this->addAlert(get_class($e), $this->formatPhpJsonLoggerErrorsContext($e, $context));
-	}
-	
-	/**
-	 * @param \Throwable $e
-	 * @param            $context
-	 */
-	public function emergency($e, $context = '')
-	{
-		$context = $this->formatParamToArray($context);
-		if ($this->isErrorObject($e) === false) {
-			throw new \InvalidArgumentException(
-				$this->generateInvalidArgumentMessage(__METHOD__)
-			);
-		}
-		$this->addEmergency(get_class($e), $this->formatPhpJsonLoggerErrorsContext($e, $context));
-	}
-	
-	/**
-	 * @param $value
-	 * @return bool
-	 */
-	private function isErrorObject($value): bool
-	{
-		if ($value instanceof \Exception || $value instanceof \Error) {
-			return true;
-		}
-		return false;
-	}
-	
-	/**
-	 * @param $value
-	 * @return array
-	 */
-	private function formatParamToArray($value): array
-	{
-		if (is_array($value)) {
-			return $value;
-		} else {
-			return (empty($value)) ? [] : ['context' => $value];
-		}
-	}
-	
-	/**
-	 * @param string $method
-	 * @return string
-	 */
-	private function generateInvalidArgumentMessage(string $method): string
-	{
-		return 'Please give the exception class to the ' . $method;
-	}
-}
+/*
+ * deprecated
+ */

+ 116 - 21
api/classes/organizr.class.php

@@ -63,7 +63,7 @@ class Organizr
 	
 	
 	// ===================================
 	// ===================================
 	// Organizr Version
 	// Organizr Version
-	public $version = '2.1.1030';
+	public $version = '2.1.1110';
 	// ===================================
 	// ===================================
 	// Quick php Version check
 	// Quick php Version check
 	public $minimumPHP = '7.3';
 	public $minimumPHP = '7.3';
@@ -1867,6 +1867,12 @@ class Organizr
 				$this->settingsOption('select', 'logLiveUpdateRefresh', ['label' => 'Live Update Refresh', 'options' => $this->timeOptions()]),
 				$this->settingsOption('select', 'logLiveUpdateRefresh', ['label' => 'Live Update Refresh', 'options' => $this->timeOptions()]),
 				$this->settingsOption('select', 'logPageSize', ['label' => 'Log Page Size', 'options' => [['name' => '10 Items', 'value' => '10'], ['name' => '25 Items', 'value' => '25'], ['name' => '50 Items', 'value' => '50'], ['name' => '100 Items', 'value' => '100']]]),
 				$this->settingsOption('select', 'logPageSize', ['label' => 'Log Page Size', 'options' => [['name' => '10 Items', 'value' => '10'], ['name' => '25 Items', 'value' => '25'], ['name' => '50 Items', 'value' => '50'], ['name' => '100 Items', 'value' => '100']]]),
 			],
 			],
+			'Cron' => [
+				$this->settingsOption('cron-file'),
+				$this->settingsOption('blank'),
+				$this->settingsOption('enable', 'autoUpdateCronEnabled', ['label' => 'Auto-Update Organizr']),
+				$this->settingsOption('cron', 'autoUpdateCronSchedule'),
+			],
 			'Login' => [
 			'Login' => [
 				$this->settingsOption('password', 'registrationPassword', ['label' => 'Registration Password', 'help' => 'Sets the password for the Registration form on the login screen']),
 				$this->settingsOption('password', 'registrationPassword', ['label' => 'Registration Password', 'help' => 'Sets the password for the Registration form on the login screen']),
 				$this->settingsOption('switch', 'hideRegistration', ['label' => 'Hide Registration', 'help' => 'Enable this to hide the Registration button on the login screen']),
 				$this->settingsOption('switch', 'hideRegistration', ['label' => 'Hide Registration', 'help' => 'Enable this to hide the Registration button on the login screen']),
@@ -4578,11 +4584,13 @@ class Organizr
 	{
 	{
 		$filesList = false;
 		$filesList = false;
 		foreach ($files as $k => $v) {
 		foreach ($files as $k => $v) {
-			$filesList[] = array(
-				'fileName' => $v['name'],
-				'path' => DIRECTORY_SEPARATOR . 'api' . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR,
-				'githubPath' => $v['download_url']
-			);
+			if ($v['type'] !== 'dir') {
+				$filesList[] = array(
+					'fileName' => $v['name'],
+					'path' => DIRECTORY_SEPARATOR . 'api' . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR . str_replace($v['name'], '', $v['path']),
+					'githubPath' => $v['download_url']
+				);
+			}
 		}
 		}
 		return $filesList;
 		return $filesList;
 	}
 	}
@@ -4598,19 +4606,88 @@ class Organizr
 		return false;
 		return false;
 	}
 	}
 	
 	
+	public function getBranchFromGithub($repo)
+	{
+		$url = 'https://api.github.com/repos/' . $repo;
+		$options = array('verify' => false);
+		$response = Requests::get($url, array(), $options);
+		try {
+			if ($response->success) {
+				$github = json_decode($response->body, true);
+				return $github['default_branch'] ?? null;
+			} else {
+				$this->setLoggerChannel('Plugins');
+				$this->logger->warning('Plugin failed to get branch from Github', $this->apiResponseFormatter($response->body));
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->logger->error($e);
+			$this->setAPIResponse('error', $e->getMessage(), 401);
+			return false;
+		}
+	}
+	
+	public function getFilesFromGithub($repo, $branch)
+	{
+		if (!$repo || !$branch) {
+			return false;
+		}
+		$url = 'https://api.github.com/repos/' . $repo . '/git/trees/' . $branch . '?recursive=1';
+		$options = array('verify' => false);
+		$response = Requests::get($url, array(), $options);
+		try {
+			if ($response->success) {
+				$github = json_decode($response->body, true);
+				return is_array($github) ? $github : null;
+			} else {
+				$this->setLoggerChannel('Plugins');
+				$this->logger->warning('Plugin failed to get branch from Github', $this->apiResponseFormatter($response->body));
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->logger->error($e);
+			$this->setAPIResponse('error', $e->getMessage(), 401);
+			return false;
+		}
+	}
+	
+	public function formatFilesFromGithub($files, $repo, $branch, $folder)
+	{
+		if (!$files || !$repo || !$branch || !$folder) {
+			return false;
+		}
+		if (isset($files['tree'])) {
+			$fileList = [];
+			foreach ($files['tree'] as $k => $v) {
+				if ($v['type'] !== 'tree') {
+					$fileInfo = pathinfo($v['path']);
+					$v['name'] = $fileInfo['basename'];
+					$v['download_url'] = 'https://raw.githubusercontent.com/' . $repo . '/' . $branch . '/' . $v['path'];
+					if ($folder == 'root') {
+						$fileList[] = $v;
+					} else {
+						if (stripos($v['path'], $folder) !== false) {
+							$v['path'] = (substr($v['path'], 0, strlen($folder)) == $folder) ? substr($v['path'], (strlen($folder) + 1)) : $v['path'];
+							$fileList[] = $v;
+						}
+					}
+				}
+			}
+			return $fileList;
+		}
+		return false;
+	}
+	
 	public function getPluginFilesFromRepo($plugin, $pluginDetails)
 	public function getPluginFilesFromRepo($plugin, $pluginDetails)
 	{
 	{
 		if (stripos($pluginDetails['repo'], 'github.com') !== false) {
 		if (stripos($pluginDetails['repo'], 'github.com') !== false) {
 			$repo = explode('https://github.com/', $pluginDetails['repo']);
 			$repo = explode('https://github.com/', $pluginDetails['repo']);
-			$folder = $pluginDetails['github_folder'] !== 'root' ? $pluginDetails['github_folder'] : '';
-			$url = 'https://api.github.com/repos/' . $repo[1] . '/contents/' . $folder;
 		} else {
 		} else {
 			return false;
 			return false;
 		}
 		}
-		$options = array('verify' => false);
-		$response = Requests::get($url, array(), $options);
-		if ($response->success) {
-			return json_decode($response->body, true);
+		$branch = $this->getBranchFromGithub($repo[1]);
+		if ($branch) {
+			return $this->formatFilesFromGithub($this->getFilesFromGithub($repo[1], $branch), $repo[1], $branch, $pluginDetails['github_folder']);
 		}
 		}
 		return false;
 		return false;
 	}
 	}
@@ -4635,9 +4712,9 @@ class Organizr
 		$array = $array[$plugin];
 		$array = $array[$plugin];
 		// Check Version of Organizr against minimum version needed
 		// Check Version of Organizr against minimum version needed
 		$compare = new Composer\Semver\Comparator;
 		$compare = new Composer\Semver\Comparator;
-		if (!$compare->lessThan($array['minimum_organizr_version'], $this->version)) {
+		if ($compare->lessThan($this->version, $array['minimum_organizr_version'])) {
 			$this->logger->warning('Minimum Organizr version needed: ' . $array['minimum_organizr_version']);
 			$this->logger->warning('Minimum Organizr version needed: ' . $array['minimum_organizr_version']);
-			$this->setResponse(500, 'Minimum Organizr version needed: ' . $array['minimum_organizr_version']);
+			$this->setResponse(500, 'Minimum Organizr version needed: ' . $array['minimum_organizr_version'] . ' | Current Version: ' . $this->version);
 			return true;
 			return true;
 		}
 		}
 		$files = $this->getPluginFilesFromRepo($plugin, $array);
 		$files = $this->getPluginFilesFromRepo($plugin, $array);
@@ -4791,6 +4868,10 @@ class Organizr
 				$response = Requests::get($repo, array(), $options);
 				$response = Requests::get($repo, array(), $options);
 				if ($response->success) {
 				if ($response->success) {
 					$plugins = array_merge($plugins, json_decode($response->body, true));
 					$plugins = array_merge($plugins, json_decode($response->body, true));
+				} else {
+					$this->setLoggerChannel('Plugins');
+					$this->logger->warning('Getting Marketplace items from Github', $this->apiResponseFormatter($response->body));
+					return false;
 				}
 				}
 			} catch (Requests_Exception $e) {
 			} catch (Requests_Exception $e) {
 				//return false;
 				//return false;
@@ -4818,6 +4899,8 @@ class Organizr
 					}
 					}
 					return false;
 					return false;
 				} else {
 				} else {
+					$this->setLoggerChannel('Plugins');
+					$this->logger->warning('Getting Marketplace JSON from Github', $this->apiResponseFormatter($response->body));
 					return false;
 					return false;
 				}
 				}
 			} catch (Requests_Exception $e) {
 			} catch (Requests_Exception $e) {
@@ -5742,6 +5825,18 @@ class Organizr
 		}
 		}
 	}
 	}
 	
 	
+	public function createCronFile()
+	{
+		$file = $this->root . DIRECTORY_SEPARATOR . 'Cron.txt';
+		file_put_contents($file, time());
+	}
+	
+	public function checkCronFile()
+	{
+		$file = $this->root . DIRECTORY_SEPARATOR . 'Cron.txt';
+		return file_exists($file) && time() - 120 < filemtime($file);
+	}
+	
 	public function plexJoinAPI($array)
 	public function plexJoinAPI($array)
 	{
 	{
 		$username = ($array['username']) ?? null;
 		$username = ($array['username']) ?? null;
@@ -6356,18 +6451,18 @@ class Organizr
 		return $query;
 		return $query;
 	}
 	}
 	
 	
-	public function testCronFrequency($frequency = null)
+	public function testCronSchedule($schedule = null)
 	{
 	{
-		if (is_array($frequency)) {
-			$frequency = str_replace('_', ' ', array_keys($frequency)[0]);
+		if (is_array($schedule)) {
+			$schedule = str_replace('_', ' ', array_keys($schedule)[0]);
 		}
 		}
-		if (!$frequency) {
-			$this->setResponse(409, 'Frequency was not supplied');
+		if (!$schedule) {
+			$this->setResponse(409, 'Schedule was not supplied');
 			return false;
 			return false;
 		}
 		}
 		try {
 		try {
-			$frequency = new Cron\CronExpression($frequency);
-			$this->setResponse(200, 'Frequency was validated');
+			$schedule = new Cron\CronExpression($schedule);
+			$this->setResponse(200, 'Schedule was validated');
 			return true;
 			return true;
 		} catch (InvalidArgumentException $e) {
 		} catch (InvalidArgumentException $e) {
 			$this->setResponse(500, $e->getMessage());
 			$this->setResponse(500, $e->getMessage());

+ 3 - 1
api/config/default.php

@@ -618,5 +618,7 @@ return [
 	'logPageSize' => '50',
 	'logPageSize' => '50',
 	'includeDatabaseQueriesInDebug' => false,
 	'includeDatabaseQueriesInDebug' => false,
 	'externalPluginMarketplaceRepos' => '',
 	'externalPluginMarketplaceRepos' => '',
-	'checkForPluginUpdate' => true
+	'checkForPluginUpdate' => true,
+	'autoUpdateCronEnabled' => false,
+	'autoUpdateCronSchedule' => '@weekly'
 ];
 ];

+ 1 - 1
api/functions.php

@@ -26,7 +26,7 @@ $folder = __DIR__ . DIRECTORY_SEPARATOR . 'plugins';
 $directoryIterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS);
 $directoryIterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS);
 $iteratorIterator = new RecursiveIteratorIterator($directoryIterator);
 $iteratorIterator = new RecursiveIteratorIterator($directoryIterator);
 foreach ($iteratorIterator as $info) {
 foreach ($iteratorIterator as $info) {
-	if ($info->getFilename() == 'plugin.php' || $info->getFilename() == 'page.php') {
+	if ($info->getFilename() == 'plugin.php' || strpos($info->getFilename(), 'page.php') !== false || $info->getFilename() == 'cron.php') {
 		require_once $info->getPathname();
 		require_once $info->getPathname();
 	}
 	}
 }
 }

+ 33 - 22
api/functions/log-functions.php

@@ -67,7 +67,7 @@ trait LogFunctions
 		return false;
 		return false;
 	}
 	}
 	
 	
-	public function readLog($file, $pageSize = 10, $offset = 0, $filter = 'NONE')
+	public function readLog($file, $pageSize = 10, $offset = 0, $filter = 'NONE', $trace_id = null)
 	{
 	{
 		$combinedLogs = false;
 		$combinedLogs = false;
 		if ($file == 'combined-logs') {
 		if ($file == 'combined-logs') {
@@ -107,11 +107,17 @@ trait LogFunctions
 				$lineGenerator = Bcremer\LineReader\LineReader::readLinesBackwards($file);
 				$lineGenerator = Bcremer\LineReader\LineReader::readLinesBackwards($file);
 				$lines = iterator_to_array($lineGenerator);
 				$lines = iterator_to_array($lineGenerator);
 			}
 			}
-			if ($filter) {
+			if ($filter || $trace_id) {
 				$results = [];
 				$results = [];
 				foreach ($lines as $line) {
 				foreach ($lines as $line) {
-					if (stripos($line, '"' . $filter . '"') !== false) {
-						$results[] = $line;
+					if ($filter) {
+						if (stripos($line, '"' . $filter . '"') !== false) {
+							$results[] = $line;
+						}
+					} elseif ($trace_id) {
+						if (stripos($line, '"' . $trace_id . '"') !== false) {
+							$results = $line;
+						}
 					}
 					}
 				}
 				}
 				$lines = $results;
 				$lines = $results;
@@ -123,22 +129,26 @@ trait LogFunctions
 	
 	
 	public function formatLogResults($lines, $pageSize, $offset)
 	public function formatLogResults($lines, $pageSize, $offset)
 	{
 	{
-		$totalLines = count($lines);
-		$totalPages = $totalLines / $pageSize;
-		$results = array_slice($lines, $offset, $pageSize);
-		$lines = [];
-		foreach ($results as $line) {
-			$lines[] = json_decode($line, true);
+		if (is_array($lines)) {
+			$totalLines = count($lines);
+			$totalPages = $totalLines / $pageSize;
+			$results = array_slice($lines, $offset, $pageSize);
+			$lines = [];
+			foreach ($results as $line) {
+				$lines[] = json_decode($line, true);
+			}
+			return [
+				'pageInfo' => [
+					'results' => $totalLines,
+					'totalPages' => ceil($totalPages),
+					'pageSize' => $pageSize,
+					'page' => $offset >= $totalPages ? -1 : ceil($offset / $pageSize) + 1
+				],
+				'results' => $lines
+			];
+		} else {
+			return json_decode($lines, true);
 		}
 		}
-		return [
-			'pageInfo' => [
-				'results' => $totalLines,
-				'totalPages' => ceil($totalPages),
-				'pageSize' => $pageSize,
-				'page' => $offset >= $totalPages ? -1 : ceil($offset / $pageSize) + 1
-			],
-			'results' => $lines
-		];
 	}
 	}
 	
 	
 	public function getLatestLogFile()
 	public function getLatestLogFile()
@@ -282,7 +292,7 @@ trait LogFunctions
 		}
 		}
 	}
 	}
 	
 	
-	public function getLog($pageSize = 10, $offset = 0, $filter = 'NONE', $number = 0)
+	public function getLog($pageSize = 10, $offset = 0, $filter = 'NONE', $number = 0, $trace_id = null)
 	{
 	{
 		if ($this->log) {
 		if ($this->log) {
 			if (isset($this->log)) {
 			if (isset($this->log)) {
@@ -296,8 +306,9 @@ trait LogFunctions
 				} else {
 				} else {
 					$log = $this->getLatestLogFile();
 					$log = $this->getLatestLogFile();
 				}
 				}
-				$readLog = $this->readLog($log, 1000, 0, $filter);
-				$this->setResponse(200, 'Results for log: ' . $log, $readLog);
+				$readLog = $this->readLog($log, $pageSize, $offset, $filter, $trace_id);
+				$msg = ($trace_id) ? 'Results for trace_id: ' . $trace_id : 'Results for log: ' . $log;
+				$this->setResponse(200, $msg, $readLog);
 				return $readLog;
 				return $readLog;
 			} else {
 			} else {
 				$this->setResponse(404, 'Log not found');
 				$this->setResponse(404, 'Log not found');

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

@@ -253,7 +253,7 @@ trait NormalFunctions
 		} elseif (isset($_SERVER['REMOTE_ADDR'])) {
 		} elseif (isset($_SERVER['REMOTE_ADDR'])) {
 			$ipaddress = $_SERVER['REMOTE_ADDR'];
 			$ipaddress = $_SERVER['REMOTE_ADDR'];
 		} else {
 		} else {
-			$ipaddress = 'UNKNOWN';
+			$ipaddress = '127.0.0.1';
 		}
 		}
 		if (strpos($ipaddress, ',') !== false) {
 		if (strpos($ipaddress, ',') !== false) {
 			list($first, $last) = explode(",", $ipaddress);
 			list($first, $last) = explode(",", $ipaddress);
@@ -264,6 +264,14 @@ trait NormalFunctions
 		}
 		}
 	}
 	}
 	
 	
+	public function serverIP()
+	{
+		if (array_key_exists('SERVER_ADDR', $_SERVER)) {
+			return $_SERVER['SERVER_ADDR'];
+		}
+		return '127.0.0.1';
+	}
+	
 	public function parseDomain($value, $force = false)
 	public function parseDomain($value, $force = false)
 	{
 	{
 		$badDomains = array('ddns.net', 'ddnsking.com', '3utilities.com', 'bounceme.net', 'freedynamicdns.net', 'freedynamicdns.org', 'gotdns.ch', 'hopto.org', 'myddns.me', 'myds.me', 'myftp.biz', 'myftp.org', 'myvnc.com', 'noip.com', 'onthewifi.com', 'redirectme.net', 'serveblog.net', 'servecounterstrike.com', 'serveftp.com', 'servegame.com', 'servehalflife.com', 'servehttp.com', 'serveirc.com', 'serveminecraft.net', 'servemp3.com', 'servepics.com', 'servequake.com', 'sytes.net', 'viewdns.net', 'webhop.me', 'zapto.org');
 		$badDomains = array('ddns.net', 'ddnsking.com', '3utilities.com', 'bounceme.net', 'freedynamicdns.net', 'freedynamicdns.org', 'gotdns.ch', 'hopto.org', 'myddns.me', 'myds.me', 'myftp.biz', 'myftp.org', 'myvnc.com', 'noip.com', 'onthewifi.com', 'redirectme.net', 'serveblog.net', 'servecounterstrike.com', 'serveftp.com', 'servegame.com', 'servehalflife.com', 'servehttp.com', 'serveirc.com', 'serveminecraft.net', 'servemp3.com', 'servepics.com', 'servequake.com', 'sytes.net', 'viewdns.net', 'webhop.me', 'zapto.org');
@@ -577,6 +585,20 @@ trait NormalFunctions
 		return $isLocal;
 		return $isLocal;
 	}
 	}
 	
 	
+	public function isLocalOrServer()
+	{
+		$isLocalOrServer = false;
+		$isLocal = $this->isLocal();
+		if (!$isLocal) {
+			if ($this->userIP() == $this->serverIP()) {
+				$isLocalOrServer = true;
+			}
+		} else {
+			$isLocalOrServer = true;
+		}
+		return $isLocalOrServer;
+	}
+	
 	public function human_filesize($bytes, $dec = 2)
 	public function human_filesize($bytes, $dec = 2)
 	{
 	{
 		$bytes = number_format($bytes, 0, '.', '');
 		$bytes = number_format($bytes, 0, '.', '');
@@ -585,6 +607,20 @@ trait NormalFunctions
 		return sprintf("%.{$dec}f %s", $bytes / (1024 ** $factor), $size[$factor]);
 		return sprintf("%.{$dec}f %s", $bytes / (1024 ** $factor), $size[$factor]);
 	}
 	}
 	
 	
+	public function apiResponseFormatter($response)
+	{
+		if (is_array($response)) {
+			return $response;
+		}
+		if (empty($response) || $response == '') {
+			return ['api_response' => 'No data'];
+		}
+		if ($this->json_validator($response)) {
+			return json_decode($response, true);
+		}
+		return ['api_response' => 'No data'];
+	}
+	
 	public function json_validator($data = null)
 	public function json_validator($data = null)
 	{
 	{
 		if (!empty($data)) {
 		if (!empty($data)) {

+ 55 - 0
api/functions/option-functions.php

@@ -92,6 +92,60 @@ trait OptionsFunction
 					'settings' => '{tags: true, selectOnClose: true, closeOnSelect: true, allowClear: true}',
 					'settings' => '{tags: true, selectOnClose: true, closeOnSelect: true, allowClear: true}',
 				];
 				];
 				break;
 				break;
+			case 'cron':
+				$settingMerge = [
+					'type' => 'cron',
+					'label' => 'Cron Schedule',
+					'help' => 'You may use either Cron format or - @hourly, @daily, @monthly',
+					'placeholder' => '* * * * *'
+				];
+				break;
+			case 'cronfile':
+				$path = $this->root . DIRECTORY_SEPARATOR . 'cron.php';
+				$server = $this->serverIP();
+				$installInstruction = ($this->docker) ?
+					'<p lang="en">No action needed.  Organizr\'s docker image comes with the Cron job built-in</p>' :
+					'<p lang="en">Setup a Cron job so it\'s call will originate from either the server\'s IP address or a local IP address.  Please use the following information to set up the Cron Job correctly.</p>
+					<h5>Cron Information</h5>
+					<ul class="list-icons">
+						<li><i class="fa fa-caret-right text-info"></i> <b lang="en">Schedule</b> <small>* * * * *</small></li>
+						<li><i class="fa fa-caret-right text-info"></i> <b lang="en">File Path</b> <small>' . $path . '</small></li>
+					</ul>
+					<h5>Command Examples</h5>
+					<ul class="list-icons">
+						<li><i class="ti-angle-right"></i> * * * * * /path/to/php ' . $path . '</li>
+						<li><i class="ti-angle-right"></i> * * * * * curl -XGET -sL  "http://' . $server . '/cron.php"</li>
+					</ul>
+					';
+				$settingMerge = [
+					'type' => 'html',
+					'override' => 12,
+					'label' => '',
+					'html' => '
+						<div class="row">
+							<div class="col-lg-12">
+								<div class="panel panel-info">
+									<div class="panel-heading">
+										<span lang="en">Organizr Enable Cron Instructions</span>
+									</div>
+									<div class="panel-wrapper collapse in" aria-expanded="true">
+										<div class="panel-body">
+											<h3 lang="en">Instructions for your install type</h3>
+											<span>' . $installInstruction . '</span>
+											<button type="button" onclick="checkCronFile();" class="btn btn-outline btn-info btn-lg btn-block" lang="en">Check Cron Status</button>
+											<div class="m-t-15 hidden cron-results-container">
+												<div class="well">
+													<pre class="cron-results"></pre>
+												</div>
+											</div>
+										</div>
+									</div>
+								</div>
+							</div>
+						</div>
+						'
+				];
+				break;
 			case 'username':
 			case 'username':
 				$settingMerge = [
 				$settingMerge = [
 					'type' => 'input',
 					'type' => 'input',
@@ -218,6 +272,7 @@ trait OptionsFunction
 					'type' => 'switch',
 					'type' => 'switch',
 					'label' => 'Hide Seeding',
 					'label' => 'Hide Seeding',
 				];
 				];
+				break;
 			case 'hidecompleted':
 			case 'hidecompleted':
 				$settingMerge = [
 				$settingMerge = [
 					'type' => 'switch',
 					'type' => 'switch',

+ 42 - 0
api/functions/update-functions.php

@@ -2,9 +2,26 @@
 
 
 trait UpdateFunctions
 trait UpdateFunctions
 {
 {
+	public function updateOrganizr()
+	{
+		if ($this->docker) {
+			return $this->dockerUpdate();
+		} elseif ($this->getOS() == 'win') {
+			return $this->windowsUpdate();
+		} else {
+			return $this->linuxUpdate();
+		}
+	}
+	
 	public function dockerUpdate()
 	public function dockerUpdate()
 	{
 	{
+		if (!$this->docker) {
+			$this->setResponse(409, 'Your install type is not Docker');
+			return false;
+		}
 		$dockerUpdate = null;
 		$dockerUpdate = null;
+		ini_set('max_execution_time', 0);
+		set_time_limit(0);
 		chdir('/etc/cont-init.d/');
 		chdir('/etc/cont-init.d/');
 		if (file_exists('./30-install')) {
 		if (file_exists('./30-install')) {
 			$this->setAPIResponse('error', 'Update failed - OrgTools is deprecated - please use organizr/organizr', 500);
 			$this->setAPIResponse('error', 'Update failed - OrgTools is deprecated - please use organizr/organizr', 500);
@@ -23,6 +40,10 @@ trait UpdateFunctions
 	
 	
 	public function windowsUpdate()
 	public function windowsUpdate()
 	{
 	{
+		if ($this->docker || $this->getOS() !== 'win') {
+			$this->setResponse(409, 'Your install type is not Windows');
+			return false;
+		}
 		$branch = ($this->config['branch'] == 'v2-master') ? '-m' : '-d';
 		$branch = ($this->config['branch'] == 'v2-master') ? '-m' : '-d';
 		ini_set('max_execution_time', 0);
 		ini_set('max_execution_time', 0);
 		set_time_limit(0);
 		set_time_limit(0);
@@ -38,6 +59,27 @@ trait UpdateFunctions
 		}
 		}
 	}
 	}
 	
 	
+	public function linuxUpdate()
+	{
+		if ($this->docker || $this->getOS() == 'win') {
+			$this->setResponse(409, 'Your install type is not Linux');
+			return false;
+		}
+		$branch = $this->config['branch'];
+		ini_set('max_execution_time', 0);
+		set_time_limit(0);
+		$logFile = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'log.txt';
+		$script = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'linux-update.sh ' . $branch . ' > ' . $logFile . ' 2>&1';
+		$update = shell_exec($script);
+		if ($update) {
+			$this->setAPIResponse('success', $update, 200);
+			return true;
+		} else {
+			$this->setAPIResponse('success', 'Update Complete - check log.txt for output', 200);
+			return false;
+		}
+	}
+	
 	public function upgradeInstall($branch = 'v2-master', $stage = '1')
 	public function upgradeInstall($branch = 'v2-master', $stage = '1')
 	{
 	{
 		// may kill this function in place for php script to run elsewhere
 		// may kill this function in place for php script to run elsewhere

+ 2 - 2
api/homepage/sonarr.php

@@ -290,11 +290,11 @@ trait SonarrHomepageItem
 			}
 			}
 			$bottomTitle = 'S' . sprintf("%02d", $child['seasonNumber']) . 'E' . sprintf("%02d", $child['episodeNumber']) . ' - ' . $child['title'];
 			$bottomTitle = 'S' . sprintf("%02d", $child['seasonNumber']) . 'E' . sprintf("%02d", $child['episodeNumber']) . ' - ' . $child['title'];
 			$details = array(
 			$details = array(
-				"seasonCount" => $child['series']['seasonCount'],
+				"seasonCount" => $child['series']['seasonCount'] ?? isset($child['series']['seasons']) ? count($child['series']['seasons']) : 0,
 				"status" => $child['series']['status'],
 				"status" => $child['series']['status'],
 				"topTitle" => $seriesName,
 				"topTitle" => $seriesName,
 				"bottomTitle" => $bottomTitle,
 				"bottomTitle" => $bottomTitle,
-				"overview" => isset($child['overview']) ? $child['overview'] : '',
+				"overview" => $child['overview'] ?? '',
 				"runtime" => $child['series']['runtime'],
 				"runtime" => $child['series']['runtime'],
 				"image" => $fanart,
 				"image" => $fanart,
 				"ratings" => $child['series']['ratings']['value'],
 				"ratings" => $child['series']['ratings']['value'],

+ 1 - 1
api/pages/settings-settings-logs.php

@@ -85,7 +85,7 @@ function get_page_settings_settings_logs($Organizr)
 				return ipInfoSpan(data);
 				return ipInfoSpan(data);
 			}
 			}
 		}, {
 		}, {
-			"data": "trace_id"
+			"data": "username"
 		}, {
 		}, {
 			data: "context",
 			data: "context",
 			render: function(data, type, row) {
 			render: function(data, type, row) {

+ 36 - 0
api/plugins/healthChecks/advancedCron.php

@@ -0,0 +1,36 @@
+<?php
+/*
+ * Simple Cron job
+ */
+/* COMMENTED OUT AS THIS IS AN EXAMPLE
+// Initiate Class
+$plugin = new HealthChecks();
+// Set logger to CRON Channel
+$plugin->setLoggerChannel('CRON');
+// Check to see if plugin cron job is enabled and check if schedule is set in config value
+if ($plugin->config['HEALTHCHECKS-cron-run-enabled'] && $plugin->config['HEALTHCHECKS-cron-run-schedule'] !== '') {
+	$plugin->logger->debug('Starting cron job for function: HealthChecks run', ['cronJob' => 'HealthChecks']);
+	$plugin->logger->debug('Validating cron job schedule', ['schedule' => $plugin->config['HEALTHCHECKS-cron-run-schedule']]);
+	// Validate if schedule is in correct cron format
+	try {
+		$schedule = new Cron\CronExpression($plugin->config['HEALTHCHECKS-cron-run-schedule']);
+		$plugin->logger->debug('Cron schedule has passed validation', ['schedule' => $plugin->config['HEALTHCHECKS-cron-run-schedule']]);
+	} catch (InvalidArgumentException $e) {
+		$plugin->logger->critical($e->getMessage());
+	}
+	// Setup job for cron
+	$scheduler->call(
+		function ($plugin) {
+			$plugin->logger->debug('Starting cron job for function: HealthChecks run');
+			return $plugin->_healthCheckPluginRun();
+		}, [$plugin])
+		->then(function ($output) use ($plugin) {
+			$plugin->logger->debug('Completed cron job', [
+				'output' => $output,
+			]);
+		})
+		->at($plugin->config['HEALTHCHECKS-cron-run-schedule']);
+} else {
+	$plugin->logger->debug('Cron job is not enabled or is set up incorrectly', ['cronJob' => 'HealthChecks']);
+}
+*/

+ 2 - 0
api/plugins/healthChecks/config.php

@@ -3,6 +3,8 @@ return array(
 	'HEALTHCHECKS-enabled' => false,
 	'HEALTHCHECKS-enabled' => false,
 	'HEALTHCHECKS-401-enabled' => false,
 	'HEALTHCHECKS-401-enabled' => false,
 	'HEALTHCHECKS-403-enabled' => false,
 	'HEALTHCHECKS-403-enabled' => false,
+	'HEALTHCHECKS-cron-run-enabled' => false,
+	'HEALTHCHECKS-cron-run-schedule' => '*/5 * * * *',
 	'HEALTHCHECKS-Auth-include' => '1',
 	'HEALTHCHECKS-Auth-include' => '1',
 	'HEALTHCHECKS-option2-include' => '',
 	'HEALTHCHECKS-option2-include' => '',
 	'HEALTHCHECKS-all-items' => '',
 	'HEALTHCHECKS-all-items' => '',

+ 10 - 0
api/plugins/healthChecks/cron.php

@@ -0,0 +1,10 @@
+<?php
+/*
+ * Simple Cron job
+ */
+$GLOBALS['cron'][] = [
+	'class' => 'HealthChecks', // Class name of plugin (case-sensitive)
+	'enabled' => 'HEALTHCHECKS-cron-run-enabled', // Config item for job enable
+	'schedule' => 'HEALTHCHECKS-cron-run-schedule', // Config item for job schedule
+	'function' => '_healthCheckPluginRun', // Function to run during job
+];

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

@@ -21,44 +21,42 @@ class HealthChecks extends Organizr
 	public function _healthCheckPluginGetSettings()
 	public function _healthCheckPluginGetSettings()
 	{
 	{
 		return array(
 		return array(
-			'FYI' => array(
-				array(
+			'Cron' => array(
+				/*array(
 					'type' => 'html',
 					'type' => 'html',
 					'label' => '',
 					'label' => '',
 					'override' => 12,
 					'override' => 12,
 					'html' => '
 					'html' => '
 						<div class="row">
 						<div class="row">
-						    <div class="col-lg-12">
-						        <div class="panel panel-info">
-						            <div class="panel-heading">
-						                <span lang="en">ATTENTION</span>
-						            </div>
-						            <div class="panel-wrapper collapse in" aria-expanded="true">
-						                <div class="panel-body">
-						                	<h4 lang="en">Once this plugin is setup, you will need to setup a CRON job</h4>
-						                    <br/>
-						                    <span>
-						                    	<h4><b lang="en">CRON Job URL</b></h4>
-						                    	<code>' . $this->getServerPath() . 'api/v2/plugins/healthchecks/run</code><br/>
-						                    	<h5><b lang="en">Frequency</b></h5>
-						                    	<span lang="en">As often as you like - i.e. every 1 minute</span>
-						                    </span>
-						                </div>
-						            </div>
-						        </div>
-						    </div>
+							<div class="col-lg-12">
+								<div class="panel panel-info">
+									<div class="panel-heading">
+										<span lang="en">ATTENTION</span>
+									</div>
+									<div class="panel-wrapper collapse in" aria-expanded="true">
+										<div class="panel-body">
+											<h4 lang="en">Once this plugin is setup, you will need to setup a CRON job</h4>
+											<br/>
+											<span>
+												<h4><b lang="en">CRON Job URL</b></h4>
+												<code>' . $this->getServerPath() . 'api/v2/plugins/healthchecks/run</code><br/>
+												<h5><b lang="en">Schedule</b></h5>
+												<span lang="en">As often as you like - i.e. every 1 minute</span>
+											</span>
+										</div>
+									</div>
+								</div>
+							</div>
 						</div>
 						</div>
 						'
 						'
-				)
+				),*/
+				$this->settingsOption('cron-file'),
+				$this->settingsOption('blank'),
+				$this->settingsOption('enable', 'HEALTHCHECKS-cron-run-enabled'),
+				$this->settingsOption('cron', 'HEALTHCHECKS-cron-run-schedule')
 			),
 			),
 			'Options' => array(
 			'Options' => array(
-				array(
-					'type' => 'select',
-					'name' => 'HEALTHCHECKS-Auth-include',
-					'label' => 'Minimum Authentication',
-					'value' => $this->config['HEALTHCHECKS-Auth-include'],
-					'options' => $this->groupSelect()
-				),
+				$this->settingsOption('auth', 'HEALTHCHECKS-Auth-include'),
 				array(
 				array(
 					'type' => 'input',
 					'type' => 'input',
 					'name' => 'HEALTHCHECKS-PingURL',
 					'name' => 'HEALTHCHECKS-PingURL',
@@ -101,23 +99,23 @@ class HealthChecks extends Organizr
 					'override' => 12,
 					'override' => 12,
 					'html' => '
 					'html' => '
 						<div class="row">
 						<div class="row">
-						    <div class="col-lg-12">
-						        <div class="panel panel-danger">
-						            <div class="panel-heading">
-						                <span lang="en">ATTENTION</span>
-						            </div>
-						            <div class="panel-wrapper collapse in" aria-expanded="true">
-						                <div class="panel-body">
-						                	<h4 lang="en">Please use a Full Access Token</h4>
-						                    <br/>
-						                    <div>
-						                    	<p lang="en">Do not use a Read-Only Token as that will not give a correct UUID for sending the results to HealthChecks.io</p>
-						                    	<p lang="en">Make sure to save before using the import button on Services tab</p>
-						                    </div>
-						                </div>
-						            </div>
-						        </div>
-						    </div>
+							<div class="col-lg-12">
+								<div class="panel panel-danger">
+									<div class="panel-heading">
+										<span lang="en">ATTENTION</span>
+									</div>
+									<div class="panel-wrapper collapse in" aria-expanded="true">
+										<div class="panel-body">
+											<h4 lang="en">Please use a Full Access Token</h4>
+											<br/>
+											<div>
+												<p lang="en">Do not use a Read-Only Token as that will not give a correct UUID for sending the results to HealthChecks.io</p>
+												<p lang="en">Make sure to save before using the import button on Services tab</p>
+											</div>
+										</div>
+									</div>
+								</div>
+							</div>
 						</div>
 						</div>
 						'
 						'
 				)
 				)
@@ -264,8 +262,10 @@ class HealthChecks extends Organizr
 				$this->_healthCheckPluginUUID($v['UUID'], $pass);
 				$this->_healthCheckPluginUUID($v['UUID'], $pass);
 			}
 			}
 			$this->setAPIResponse('success', null, 200, $allItems);
 			$this->setAPIResponse('success', null, 200, $allItems);
+			return $allItems;
 		} else {
 		} else {
 			$this->setAPIResponse('error', 'User does not have access', 401);
 			$this->setAPIResponse('error', 'User does not have access', 401);
 		}
 		}
+		return false;
 	}
 	}
 }
 }

+ 49 - 1
api/v2/routes/connectionTester.php

@@ -575,5 +575,53 @@ $app->post('/test/tautulli', function ($request, $response, $args) {
 	return $response
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 		->withStatus($GLOBALS['responseCode']);
-	
+});
+$app->post('/test/cron', function ($request, $response, $args) {
+	/**
+	 * @OA\Post(
+	 *     security={{ "api_key":{} }},
+	 *     tags={"test connection"},
+	 *     path="/api/v2/test/cron",
+	 *     summary="Test cron schedule",
+	 *     @OA\Response(response="200",description="Success",@OA\JsonContent(ref="#/components/schemas/success-message")),
+	 *     @OA\Response(response="401",description="Unauthorized",@OA\JsonContent(ref="#/components/schemas/unauthorized-message")),
+	 *     @OA\Response(response="422",description="Error",@OA\JsonContent(ref="#/components/schemas/error-message")),
+	 *     @OA\Response(response="500",description="Error",@OA\JsonContent(ref="#/components/schemas/error-message")),
+	 * )
+	 */
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->qualifyRequest(1, true)) {
+		$Organizr->testCronSchedule($Organizr->apiData($request));
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->get('/test/cron', function ($request, $response, $args) {
+	/**
+	 * @OA\Get(
+	 *     security={{ "api_key":{} }},
+	 *     tags={"test connection"},
+	 *     path="/api/v2/test/cron",
+	 *     summary="Test if cron is setup correctly",
+	 *     @OA\Response(response="200",description="Success",@OA\JsonContent(ref="#/components/schemas/success-message")),
+	 *     @OA\Response(response="401",description="Unauthorized",@OA\JsonContent(ref="#/components/schemas/unauthorized-message")),
+	 *     @OA\Response(response="422",description="Error",@OA\JsonContent(ref="#/components/schemas/error-message")),
+	 *     @OA\Response(response="500",description="Error",@OA\JsonContent(ref="#/components/schemas/error-message")),
+	 * )
+	 */
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->qualifyRequest(1, true)) {
+		$file = $Organizr->checkCronFile();
+		if ($file) {
+			$Organizr->setResponse(200, 'Cron file is setup');
+		} else {
+			$Organizr->setResponse(500, 'Cron file is not setup correctly');
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
 });
 });

+ 3 - 2
api/v2/routes/log.php

@@ -1,13 +1,14 @@
 <?php
 <?php
-$app->get('/log[/{number}]', function ($request, $response, $args) {
+$app->get('/log[/{number}[/{trace_id}]]', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
 	if ($Organizr->checkRoute($request)) {
 	if ($Organizr->checkRoute($request)) {
 		if ($Organizr->qualifyRequest(1, true)) {
 		if ($Organizr->qualifyRequest(1, true)) {
 			$args['number'] = $args['number'] ?? 0;
 			$args['number'] = $args['number'] ?? 0;
+			$args['trace_id'] = $args['trace_id'] ?? null;
 			$_GET['pageSize'] = $_GET['pageSize'] ?? 1000;
 			$_GET['pageSize'] = $_GET['pageSize'] ?? 1000;
 			$_GET['offset'] = $_GET['offset'] ?? 0;
 			$_GET['offset'] = $_GET['offset'] ?? 0;
 			$_GET['filter'] = $_GET['filter'] ?? 'NONE';
 			$_GET['filter'] = $_GET['filter'] ?? 'NONE';
-			$Organizr->getLog($_GET['pageSize'], $_GET['offset'], $_GET['filter'], $args['number']);
+			$Organizr->getLog($_GET['pageSize'], $_GET['offset'], $_GET['filter'], $args['number'], $args['trace_id']);
 		}
 		}
 	}
 	}
 	$response->getBody()->write(jsonE($GLOBALS['api']));
 	$response->getBody()->write(jsonE($GLOBALS['api']));

+ 48 - 0
api/v2/routes/update.php

@@ -5,6 +5,30 @@
  *     description="Organizr Update"
  *     description="Organizr Update"
  * )
  * )
  */
  */
+$app->get('/update', function ($request, $response, $args) {
+	/**
+	 * @OA\Get(
+	 *     security={{ "api_key":{} }},
+	 *     tags={"update"},
+	 *     path="/api/v2/update",
+	 *     summary="Update Organizr install using update script",
+	 *     @OA\Response(response="200",description="Success",@OA\JsonContent(ref="#/components/schemas/success-message")),
+	 *     @OA\Response(response="401",description="Unauthorized",@OA\JsonContent(ref="#/components/schemas/unauthorized-message")),
+	 *     @OA\Response(response="404",description="Error",@OA\JsonContent(ref="#/components/schemas/error-message")),
+	 *     @OA\Response(response="422",description="Error",@OA\JsonContent(ref="#/components/schemas/error-message")),
+	 *     @OA\Response(response="500",description="Error",@OA\JsonContent(ref="#/components/schemas/error-message")),
+	 * )
+	 */
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->qualifyRequest(1, true)) {
+		$Organizr->updateOrganizr();
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+	
+});
 $app->get('/update/download/{branch}', function ($request, $response, $args) {
 $app->get('/update/download/{branch}', function ($request, $response, $args) {
 	/**
 	/**
 	 * @OA\Get(
 	 * @OA\Get(
@@ -148,6 +172,30 @@ $app->get('/update/windows', function ($request, $response, $args) {
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 		->withStatus($GLOBALS['responseCode']);
 	
 	
+});
+$app->get('/update/linux', function ($request, $response, $args) {
+	/**
+	 * @OA\Get(
+	 *     security={{ "api_key":{} }},
+	 *     tags={"update"},
+	 *     path="/api/v2/update/linux",
+	 *     summary="Update Organizr install using Linux script",
+	 *     @OA\Response(response="200",description="Success",@OA\JsonContent(ref="#/components/schemas/success-message")),
+	 *     @OA\Response(response="401",description="Unauthorized",@OA\JsonContent(ref="#/components/schemas/unauthorized-message")),
+	 *     @OA\Response(response="404",description="Error",@OA\JsonContent(ref="#/components/schemas/error-message")),
+	 *     @OA\Response(response="422",description="Error",@OA\JsonContent(ref="#/components/schemas/error-message")),
+	 *     @OA\Response(response="500",description="Error",@OA\JsonContent(ref="#/components/schemas/error-message")),
+	 * )
+	 */
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->qualifyRequest(1, true)) {
+		$Organizr->linuxUpdate();
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+	
 });
 });
 $app->get('/update/migrate/{version}', function ($request, $response, $args) {
 $app->get('/update/migrate/{version}', function ($request, $response, $args) {
 	/**
 	/**

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

@@ -71,6 +71,9 @@ class Sonarr
 	    if ( $this->type == 'lidarr' ) {
 	    if ( $this->type == 'lidarr' ) {
 		    $uriData['includeArtist'] = 'true';
 		    $uriData['includeArtist'] = 'true';
 	    }
 	    }
+	    if ( $this->type == 'sonarr' ) {
+		    $uriData['includeSeries'] = 'true';
+	    }
 	    $response = [
 	    $response = [
             'uri' => 'calendar',
             'uri' => 'calendar',
             'type' => 'get',
             'type' => 'get',
@@ -658,7 +661,7 @@ class Sonarr
 		    $compare = new Comparator;
 		    $compare = new Comparator;
 		    switch ($this->type){
 		    switch ($this->type){
 			    case 'sonarr':
 			    case 'sonarr':
-				    $versionCheck = '';
+				    $versionCheck = 'v3/';
 				    break;
 				    break;
 			    case 'radarr':
 			    case 'radarr':
 			    	$versionCheck =  'v3/';
 			    	$versionCheck =  'v3/';

+ 9 - 3
api/vendor/nekonomokochan/php-json-logger/src/PhpJsonLogger/JsonFormatter.php

@@ -2,7 +2,7 @@
 namespace Nekonomokochan\PhpJsonLogger;
 namespace Nekonomokochan\PhpJsonLogger;
 
 
 use Monolog\Formatter\JsonFormatter as BaseJsonFormatter;
 use Monolog\Formatter\JsonFormatter as BaseJsonFormatter;
-
+use Ramsey\Uuid\Uuid;
 /**
 /**
  * Class JsonFormatter
  * Class JsonFormatter
  *
  *
@@ -22,7 +22,8 @@ class JsonFormatter extends BaseJsonFormatter
             'log_level'         => $record['level_name'],
             'log_level'         => $record['level_name'],
             'message'           => $record['message'],
             'message'           => $record['message'],
             'channel'           => $record['channel'],
             'channel'           => $record['channel'],
-            'trace_id'          => $record['extra']['trace_id'],
+	        'username'          => $record['extra']['trace_id'],
+	        'trace_id'          => $this->generateUuid(),
             'file'              => $record['extra']['file'],
             'file'              => $record['extra']['file'],
             'line'              => $record['extra']['line'],
             'line'              => $record['extra']['line'],
             'context'           => $record['context'],
             'context'           => $record['context'],
@@ -55,4 +56,9 @@ class JsonFormatter extends BaseJsonFormatter
 
 
         return ($time - $createdTime) * 1000;
         return ($time - $createdTime) * 1000;
     }
     }
-}
+    
+	private function generateUuid(): string
+	{
+		return Uuid::uuid4()->toString();
+	}
+}

+ 32 - 20
api/vendor/nekonomokochan/php-json-logger/src/PhpJsonLogger/Logger.php

@@ -69,95 +69,99 @@ class Logger extends \Monolog\Logger
      * @param $message
      * @param $message
      * @param $context
      * @param $context
      */
      */
-    public function debug($message, array $context = [])
+    public function debug($message, $context = '')
     {
     {
+	    $context = $this->formatParamToArray($context);
         $this->addDebug($message, $context);
         $this->addDebug($message, $context);
     }
     }
 
 
     /**
     /**
      * @param $message
      * @param $message
-     * @param array $context
+     * @param $context
      */
      */
-    public function info($message, array $context = [])
+    public function info($message, $context = '')
     {
     {
+	    $context = $this->formatParamToArray($context);
         $this->addInfo($message, $context);
         $this->addInfo($message, $context);
     }
     }
 
 
     /**
     /**
      * @param $message
      * @param $message
-     * @param array $context
+     * @param $context
      */
      */
-    public function notice($message, array $context = [])
+    public function notice($message, $context = '')
     {
     {
+	    $context = $this->formatParamToArray($context);
         $this->addNotice($message, $context);
         $this->addNotice($message, $context);
     }
     }
 
 
     /**
     /**
      * @param $message
      * @param $message
-     * @param array $context
+     * @param $context
      */
      */
-    public function warning($message, array $context = [])
+    public function warning($message, $context = '')
     {
     {
+	    $context = $this->formatParamToArray($context);
         $this->addWarning($message, $context);
         $this->addWarning($message, $context);
     }
     }
 
 
     /**
     /**
      * @param \Throwable $e
      * @param \Throwable $e
-     * @param array $context
+     * @param $context
      */
      */
-    public function error($e, array $context = [])
+    public function error($e, $context = '')
     {
     {
         if ($this->isErrorObject($e) === false) {
         if ($this->isErrorObject($e) === false) {
             throw new \InvalidArgumentException(
             throw new \InvalidArgumentException(
                 $this->generateInvalidArgumentMessage(__METHOD__)
                 $this->generateInvalidArgumentMessage(__METHOD__)
             );
             );
         }
         }
-
+	    $context = $this->formatParamToArray($context);
         $this->addError(get_class($e), $this->formatPhpJsonLoggerErrorsContext($e, $context));
         $this->addError(get_class($e), $this->formatPhpJsonLoggerErrorsContext($e, $context));
     }
     }
 
 
     /**
     /**
      * @param \Throwable $e
      * @param \Throwable $e
-     * @param array $context
+     * @param $context
      */
      */
-    public function critical($e, array $context = [])
+    public function critical($e, $context = '')
     {
     {
         if ($this->isErrorObject($e) === false) {
         if ($this->isErrorObject($e) === false) {
             throw new \InvalidArgumentException(
             throw new \InvalidArgumentException(
                 $this->generateInvalidArgumentMessage(__METHOD__)
                 $this->generateInvalidArgumentMessage(__METHOD__)
             );
             );
         }
         }
-
+	    $context = $this->formatParamToArray($context);
         $this->addCritical(get_class($e), $this->formatPhpJsonLoggerErrorsContext($e, $context));
         $this->addCritical(get_class($e), $this->formatPhpJsonLoggerErrorsContext($e, $context));
     }
     }
 
 
     /**
     /**
      * @param \Throwable $e
      * @param \Throwable $e
-     * @param array $context
+     * @param $context
      */
      */
-    public function alert($e, array $context = [])
+    public function alert($e, $context = '')
     {
     {
         if ($this->isErrorObject($e) === false) {
         if ($this->isErrorObject($e) === false) {
             throw new \InvalidArgumentException(
             throw new \InvalidArgumentException(
                 $this->generateInvalidArgumentMessage(__METHOD__)
                 $this->generateInvalidArgumentMessage(__METHOD__)
             );
             );
         }
         }
-
+	    $context = $this->formatParamToArray($context);
         $this->addAlert(get_class($e), $this->formatPhpJsonLoggerErrorsContext($e, $context));
         $this->addAlert(get_class($e), $this->formatPhpJsonLoggerErrorsContext($e, $context));
     }
     }
 
 
     /**
     /**
      * @param \Throwable $e
      * @param \Throwable $e
-     * @param array $context
+     * @param $context
      */
      */
-    public function emergency($e, array $context = [])
+    public function emergency($e, $context = '')
     {
     {
         if ($this->isErrorObject($e) === false) {
         if ($this->isErrorObject($e) === false) {
             throw new \InvalidArgumentException(
             throw new \InvalidArgumentException(
                 $this->generateInvalidArgumentMessage(__METHOD__)
                 $this->generateInvalidArgumentMessage(__METHOD__)
             );
             );
         }
         }
-
+	    $context = $this->formatParamToArray($context);
         $this->addEmergency(get_class($e), $this->formatPhpJsonLoggerErrorsContext($e, $context));
         $this->addEmergency(get_class($e), $this->formatPhpJsonLoggerErrorsContext($e, $context));
     }
     }
 
 
@@ -232,4 +236,12 @@ class Logger extends \Monolog\Logger
     {
     {
         return 'Please give the exception class to the ' . $method;
         return 'Please give the exception class to the ' . $method;
     }
     }
-}
+	private function formatParamToArray($value): array
+	{
+		if (is_array($value)) {
+			return $value;
+		} else {
+			return (empty($value)) ? [] : ['data' => $value];
+		}
+	}
+}

+ 1 - 1
api/vendor/nekonomokochan/php-json-logger/src/PhpJsonLogger/MonologCreator.php

@@ -85,4 +85,4 @@ trait MonologCreator
             'processors' => $processors
             'processors' => $processors
         ];
         ];
     }
     }
-}
+}

+ 121 - 0
cron.php

@@ -0,0 +1,121 @@
+<?php
+require_once 'api/functions.php';
+$Organizr = new Organizr();
+$Organizr->setLoggerChannel('Cron');
+if ($Organizr->isLocalOrServer() && $Organizr->hasDB()) {
+	// Set user as Organizr API
+	$_GET['apikey'] = $Organizr->config['organizrAPI'];
+	// Create a new scheduler
+	$scheduler = new GO\Scheduler();
+	// Clear any pre-existing jobs if any
+	$scheduler->clearJobs();
+	$Organizr->logger->debug('Cron process starting');
+	// Auto-update Cron
+	if ($Organizr->config['autoUpdateCronEnabled'] && $Organizr->config['autoUpdateCronSchedule']) {
+		try {
+			$schedule = new Cron\CronExpression($Organizr->config['autoUpdateCronSchedule']);
+			$Organizr->logger->debug('Cron schedule has passed validation', ['schedule' => $Organizr->config['autoUpdateCronSchedule']]);
+			$scheduler->call(
+				function () use ($Organizr) {
+					$Organizr->logger->debug('Running cron job', ['function' => 'Auto-update']);
+					return $Organizr->updateOrganizr();
+				})
+				->then(function ($output) use ($Organizr) {
+					$Organizr->logger->debug('Completed cron job', [
+						'output' => $output,
+					]);
+				})
+				->at($Organizr->config['autoUpdateCronSchedule']);
+		} catch (InvalidArgumentException $e) {
+			$Organizr->logger->warning('Cron schedule has failed validation', ['schedule' => $Organizr->config['autoUpdateCronSchedule']]);
+			$Organizr->logger->error($e);
+		} catch (Exception $e) {
+			$Organizr->logger->error($e);
+		}
+	}
+	// End Auto-update Cron
+	// Add plugin cron
+	$Organizr->logger->debug('Checking if any plugins have cron jobs');
+	foreach ($GLOBALS['cron'] as $cronJob) {
+		if (isset($cronJob['enabled']) && isset($cronJob['class']) && isset($cronJob['function']) && isset($cronJob['schedule'])) {
+			$Organizr->logger->debug('Starting cron job for function: ' . $cronJob['function'], ['cronJob' => $cronJob]);
+			if ($Organizr->config[$cronJob['enabled']]) {
+				$Organizr->logger->debug('Checking if cron job class exists', ['cronJob' => $cronJob]);
+				if (class_exists($cronJob['class'])) {
+					$Organizr->logger->debug('Class exists', ['cronJob' => $cronJob]);
+					$Organizr->logger->debug('Validating cron job schedule', ['schedule' => $cronJob['schedule']]);
+					try {
+						$schedule = new Cron\CronExpression($Organizr->config[$cronJob['schedule']]);
+						$Organizr->logger->debug('Cron schedule has passed validation', ['schedule' => $Organizr->config[$cronJob['schedule']]]);
+						$plugin = new $cronJob['class']();
+						$function = $cronJob['function'];
+						$Organizr->logger->debug('Checking if cron job method exists', ['cronJob' => $cronJob]);
+						if (method_exists($plugin, $function)) {
+							$Organizr->logger->debug('Method exists', ['cronJob' => $cronJob]);
+							$scheduler->call(
+								function ($plugin, $function) use ($Organizr) {
+									$Organizr->logger->debug('Running cron job', ['function' => $function]);
+									return $plugin->$function();
+								}, [$plugin, $function])
+								->then(function ($output) use ($Organizr) {
+									$Organizr->logger->debug('Completed cron job', [
+										'output' => $output,
+									]);
+								})
+								->at($Organizr->config[$cronJob['schedule']]);
+						} else {
+							$Organizr->warning('Method error', ['cronJob' => $cronJob['class']]);
+						}
+					} catch (InvalidArgumentException $e) {
+						$Organizr->logger->warning('Cron schedule has failed validation', ['schedule' => $Organizr->config[$cronJob['schedule']]]);
+						$Organizr->logger->error($e);
+						break;
+					} catch (Exception $e) {
+						$Organizr->logger->error($e);
+						break;
+					}
+				} else {
+					$Organizr->warning('Class error', ['cronJob' => $cronJob['class']]);
+				}
+			} else {
+				$Organizr->logger->debug('Cron job is not enabled', ['cronJob' => $cronJob]);
+			}
+		} else {
+			$Organizr->logger->warning('Cron job was setup incorrectly', ['cronJob' => $cronJob]);
+		}
+	}
+	$Organizr->logger->debug('Finished processing plugin cron jobs');
+	/*
+	 * Include plugin advanced cron
+	 */
+	$Organizr->logger->debug('Checking if any Plugins have advanced cron jobs');
+	try {
+		$directoryIterator = new RecursiveDirectoryIterator($Organizr->root . DIRECTORY_SEPARATOR . 'api' . DIRECTORY_SEPARATOR . 'plugins', FilesystemIterator::SKIP_DOTS);
+		$iteratorIterator = new RecursiveIteratorIterator($directoryIterator);
+		foreach ($iteratorIterator as $info) {
+			if ($info->getFilename() == 'advancedCron.php') {
+				require_once $info->getPathname();
+			}
+		}
+	} catch (UnexpectedValueException $e) {
+		$Organizr->logger->error($e);
+	}
+	$Organizr->logger->debug('Finished processing advanced plugin cron jobs');
+	// Run cron jobs
+	$scheduler->run();
+	// Debug stuff
+	//$Organizr->prettyPrint($scheduler->getVerboseOutput());
+	//$Organizr->prettyPrint($scheduler->getFailedJobs());
+	$Organizr->logger->debug('Cron process completion', ['verbose' => $scheduler->getVerboseOutput()]);
+	if (!empty($scheduler->getFailedJobs())) {
+		$Organizr->logger->warning('Cron jobs have failed', ['jobs' => $scheduler->getFailedJobs()]);
+	}
+	// End Run and set file with time
+	$Organizr->createCronFile();
+} else {
+	if ($Organizr->hasDB()) {
+		$Organizr->logger->warning('Unauthorized user tried to access cron file');
+		die($Organizr->showHTML('Unauthorized', 'Go-on.... Git!!!'));
+	}
+	die('Unauthorized');
+}

+ 238 - 0
docs/api.json

@@ -2593,6 +2593,114 @@
                 ]
                 ]
             }
             }
         },
         },
+        "/api/v2/test/cron": {
+            "get": {
+                "tags": [
+                    "test connection"
+                ],
+                "summary": "Test if cron is setup correctly",
+                "responses": {
+                    "200": {
+                        "description": "Success",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/success-message"
+                                }
+                            }
+                        }
+                    },
+                    "401": {
+                        "description": "Unauthorized",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/unauthorized-message"
+                                }
+                            }
+                        }
+                    },
+                    "422": {
+                        "description": "Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/error-message"
+                                }
+                            }
+                        }
+                    },
+                    "500": {
+                        "description": "Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/error-message"
+                                }
+                            }
+                        }
+                    }
+                },
+                "security": [
+                    {
+                        "api_key": []
+                    }
+                ]
+            },
+            "post": {
+                "tags": [
+                    "test connection"
+                ],
+                "summary": "Test cron schedule",
+                "responses": {
+                    "200": {
+                        "description": "Success",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/success-message"
+                                }
+                            }
+                        }
+                    },
+                    "401": {
+                        "description": "Unauthorized",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/unauthorized-message"
+                                }
+                            }
+                        }
+                    },
+                    "422": {
+                        "description": "Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/error-message"
+                                }
+                            }
+                        }
+                    },
+                    "500": {
+                        "description": "Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/error-message"
+                                }
+                            }
+                        }
+                    }
+                },
+                "security": [
+                    {
+                        "api_key": []
+                    }
+                ]
+            }
+        },
         "/api/v2/emby/register": {
         "/api/v2/emby/register": {
             "post": {
             "post": {
                 "tags": [
                 "tags": [
@@ -2847,6 +2955,71 @@
                 }
                 }
             }
             }
         },
         },
+        "/api/v2/update": {
+            "get": {
+                "tags": [
+                    "update"
+                ],
+                "summary": "Update Organizr install using update script",
+                "responses": {
+                    "200": {
+                        "description": "Success",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/success-message"
+                                }
+                            }
+                        }
+                    },
+                    "401": {
+                        "description": "Unauthorized",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/unauthorized-message"
+                                }
+                            }
+                        }
+                    },
+                    "404": {
+                        "description": "Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/error-message"
+                                }
+                            }
+                        }
+                    },
+                    "422": {
+                        "description": "Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/error-message"
+                                }
+                            }
+                        }
+                    },
+                    "500": {
+                        "description": "Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/error-message"
+                                }
+                            }
+                        }
+                    }
+                },
+                "security": [
+                    {
+                        "api_key": []
+                    }
+                ]
+            }
+        },
         "/api/v2/update/download/{branch}": {
         "/api/v2/update/download/{branch}": {
             "get": {
             "get": {
                 "tags": [
                 "tags": [
@@ -3237,6 +3410,71 @@
                 ]
                 ]
             }
             }
         },
         },
+        "/api/v2/update/linux": {
+            "get": {
+                "tags": [
+                    "update"
+                ],
+                "summary": "Update Organizr install using Linux script",
+                "responses": {
+                    "200": {
+                        "description": "Success",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/success-message"
+                                }
+                            }
+                        }
+                    },
+                    "401": {
+                        "description": "Unauthorized",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/unauthorized-message"
+                                }
+                            }
+                        }
+                    },
+                    "404": {
+                        "description": "Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/error-message"
+                                }
+                            }
+                        }
+                    },
+                    "422": {
+                        "description": "Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/error-message"
+                                }
+                            }
+                        }
+                    },
+                    "500": {
+                        "description": "Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/error-message"
+                                }
+                            }
+                        }
+                    }
+                },
+                "security": [
+                    {
+                        "api_key": []
+                    }
+                ]
+            }
+        },
         "/api/v2/update/migrate/{version}": {
         "/api/v2/update/migrate/{version}": {
             "get": {
             "get": {
                 "tags": [
                 "tags": [

+ 30 - 2
js/custom.js

@@ -14,6 +14,10 @@ $(document).ready(function () {
         message('Clipboard',e.text,activeInfo.settings.notifications.position,'#FFF','info','5000');
         message('Clipboard',e.text,activeInfo.settings.notifications.position,'#FFF','info','5000');
         e.clearSelection();
         e.clearSelection();
     });
     });
+	internalClipboard.on('success', function(e) {
+		message('Clipboard',e.text,activeInfo.settings.notifications.position,'#FFF','info','5000');
+		e.clearSelection();
+	});
     "use strict";
     "use strict";
     var body = $("body");
     var body = $("body");
 
 
@@ -1927,11 +1931,35 @@ $(document).on('click', '.ti-shift-left.mouse', function() {
 
 
 // Log Details
 // Log Details
 $(document).on('click', '.log-details', function() {
 $(document).on('click', '.log-details', function() {
-	let details = $(this).attr('data-details');
-	formatLogDetails(details);
+	let trace = $(this).attr('data-trace');
+	let activateClipboard = $(this).attr('data-clipboard');
+	let el = $(this);
+	el.find('i').toggleClass('fa fa-lg fa-spin mdi-reload');
+	organizrAPI2('GET','api/v2/log/all/'+trace).success(function(data) {
+		try {
+			let response = data.response;
+			if(activateClipboard){
+				clipboard(true,JSON.stringify(response.data));
+			}else{
+				formatLogDetails(response.data);
+			}
+		}catch(e) {
+			organizrCatchError(e,data);
+		}
+		el.find('i').toggleClass('fa fa-lg fa-spin mdi-reload');
+	}).fail(function(xhr) {
+		OrganizrApiError(xhr, 'API Error');
+		el.find('i').toggleClass('fa fa-lg fa-spin mdi-reload');
+	})
 });
 });
 
 
 // Choose Log choose-organizr-log
 // Choose Log choose-organizr-log
 $(document).on("change", ".choose-organizr-log", function () {
 $(document).on("change", ".choose-organizr-log", function () {
 	organizrLogTable.ajax.url($(this).val()).load();
 	organizrLogTable.ajax.url($(this).val()).load();
+});
+
+// Test cron
+$(document).on('click', '.test-cron', function() {
+	let cron = $(this).parent().parent().find('input').val();
+	testAPIConnection('cron',cron);
 });
 });

ファイルの差分が大きいため隠しています
+ 0 - 0
js/custom.min.js


+ 38 - 6
js/functions.js

@@ -1157,10 +1157,27 @@ function buildFormItem(item){
         case 'arrayMultiple':
         case 'arrayMultiple':
             return '<span class="text-danger">BuildFormItem Class not setup...';
             return '<span class="text-danger">BuildFormItem Class not setup...';
             break;
             break;
+		case 'cron':
+			return `${smallLabel}<div class="input-group"><input data-changed="false" class="form-control ${extraClass}" ${placeholder} ${value} ${id} ${name} ${disabled} ${type} ${label} ${attr} autocomplete="new-password"><span class="input-group-btn"><button class="btn btn-info test-cron" type="button"><i class="fa fa-flask"></i></button></span></div>`;
+			break;
 		default:
 		default:
 			return '<span class="text-danger">BuildFormItem Class not setup...';
 			return '<span class="text-danger">BuildFormItem Class not setup...';
 	}
 	}
 }
 }
+function checkCronFile(){
+	$('.cron-results-container').removeClass('hidden');
+	organizrAPI2('GET','api/v2/test/cron').success(function(data) {
+		try {
+			$('.cron-results').text('Cron file is setup correctly');
+		}catch(e) {
+			$('.cron-results').text('Unknown error');
+			organizrCatchError(e,data);
+		}
+	}).fail(function(xhr) {
+		$('.cron-results').text('Cron file is not setup or is setup incorrectly');
+		OrganizrApiError(xhr);
+	});
+}
 function buildPluginsItem(array, type = 'enabled'){
 function buildPluginsItem(array, type = 'enabled'){
 	var activePlugins = '';
 	var activePlugins = '';
 	var inactivePlugins = '';
 	var inactivePlugins = '';
@@ -1391,6 +1408,7 @@ function loadMarketplace(type){
     });
     });
 }
 }
 function loadPluginMarketplace(){
 function loadPluginMarketplace(){
+	$('#managePluginTable').html('<td class="text-center" colspan="12"><i class="fa fa-spin fa-spinner"></i></td>');
 	organizrAPI2('GET','api/v2/plugins/marketplace').success(function(data) {
 	organizrAPI2('GET','api/v2/plugins/marketplace').success(function(data) {
 		try {
 		try {
 			let response = data.response;
 			let response = data.response;
@@ -3787,7 +3805,7 @@ function checkPluginUpdates(){
 			});
 			});
 			if(update){
 			if(update){
 				pluginsNeedingUpdate = '[' + pluginsNeedingUpdate.join(', ') + ']';
 				pluginsNeedingUpdate = '[' + pluginsNeedingUpdate.join(', ') + ']';
-				messageSingle(window.lang.translate('Update Available'), 'The following plugin(s) need updates: ' + pluginsNeedingUpdate, activeInfo.settings.notifications.position, '#FFF', 'update', '600000');
+				messageSingle(window.lang.translate('Update Available'), '<a href="javascript:void(0)" onclick="shortcut(\'plugin-marketplace\');"><span lang="en">The following plugin(s) need updates</span></a>: ' + pluginsNeedingUpdate, activeInfo.settings.notifications.position, '#FFF', 'update', '600000');
 			}
 			}
 		}catch(e) {
 		}catch(e) {
 			organizrCatchError(e,data);
 			organizrCatchError(e,data);
@@ -11063,20 +11081,20 @@ function jsFriendlyJSONStringify (s) {
 }
 }
 function logContext(row){
 function logContext(row){
 	let buttons = '';
 	let buttons = '';
-	buttons += (Object.keys(row).length > 0) ? '<button data-toggle="tooltip" title="" data-original-title="View Details" class="btn btn-xs btn-primary waves-effect waves-light log-details m-r-5" data-details=\''+jsFriendlyJSONStringify(row)+'\'><i class="mdi mdi-file-find"></i></button>' : '';
-	buttons += (Object.keys(row).length > 0) ? '<button data-toggle="tooltip" title="" data-original-title="Copy Log" class="btn btn-xs btn-info waves-effect waves-light clipboard m-r-5" data-clipboard-text=\''+jsFriendlyJSONStringify(row)+'\'><i class="mdi mdi-content-copy"></i></button>' : '';
+	buttons += (Object.keys(row).length > 0) ? '<button data-toggle="tooltip" title="" data-original-title="View Details" class="btn btn-xs btn-primary waves-effect waves-light log-details m-r-5" data-trace="'+row.trace_id+'"><i class="mdi mdi-file-find"></i></button>' : '';
+	buttons += (Object.keys(row).length > 0) ? '<button data-toggle="tooltip" title="" data-original-title="Copy Log" class="btn btn-xs btn-info waves-effect waves-light log-details m-r-5" data-trace="'+row.trace_id+'" data-clipboard="true"><i class="mdi mdi-content-copy"></i></button>' : '';
 	return buttons;
 	return buttons;
 }
 }
 function formatLogDetails(details){
 function formatLogDetails(details){
 	if(!details){
 	if(!details){
 		return false;
 		return false;
 	}
 	}
-	details = JSON.parse(details);
 	let m = moment.tz(details.datetime + 'Z', activeInfo.timezone);
 	let m = moment.tz(details.datetime + 'Z', activeInfo.timezone);
 	details.datetime = moment(m).format('LLL');
 	details.datetime = moment(m).format('LLL');
 	let items = '';
 	let items = '';
 	items += `<li><div class="bg-inverse"><i class="mdi mdi-calendar-text text-white"></i></div> ${details.datetime}<span class="text-muted" lang="en">Date</span></li>`;
 	items += `<li><div class="bg-inverse"><i class="mdi mdi-calendar-text text-white"></i></div> ${details.datetime}<span class="text-muted" lang="en">Date</span></li>`;
-	items += `<li><div class="bg-inverse"><i class="mdi mdi-account-box-outline text-white"></i></div> ${details.trace_id}<span class="text-muted" lang="en">User</span></li>`;
+	items += `<li><div class="bg-warning"><i class="mdi mdi-robot text-white"></i></div> ${details.trace_id}<span class="text-muted" lang="en">Trace ID</span></li>`;
+	items += `<li><div class="bg-primary"><i class="mdi mdi-account-box-outline text-white"></i></div> ${details.username}<span class="text-muted" lang="en">User</span></li>`;
 	items += `<li><div class="bg-info"><i class="mdi mdi-function text-white"></i></div> ${details.channel}<span class="text-muted" lang="en">Function</span></li>`;
 	items += `<li><div class="bg-info"><i class="mdi mdi-function text-white"></i></div> ${details.channel}<span class="text-muted" lang="en">Function</span></li>`;
 	items += `<li><div class="bg-plex"><i class="mdi mdi-language-php text-white"></i></div> ${details.file}<code>#L${details.line}</code><span class="text-muted" lang="en">File</span></li>`;
 	items += `<li><div class="bg-plex"><i class="mdi mdi-language-php text-white"></i></div> ${details.file}<code>#L${details.line}</code><span class="text-muted" lang="en">File</span></li>`;
 	let items2 = '';
 	let items2 = '';
@@ -11328,7 +11346,15 @@ function msToTime(s) {
 	if(ms >= '500'){ secs = pad(parseFloat(secs) + 1, 2); }
 	if(ms >= '500'){ secs = pad(parseFloat(secs) + 1, 2); }
 	return hours+mins+secs;
 	return hours+mins+secs;
 }
 }
-
+function clickSettingsTab(){
+	let tabs = $('.allTabsList');
+	$.each(tabs, function(i,v) {
+		let tab = $(v);
+		if(tab.attr('data-url') == 'api/v2/page/settings'){
+			tab.find('a').trigger('click');
+		}
+	});
+}
 function clickMenuItem(selector){
 function clickMenuItem(selector){
 	if($(selector).length >= 1){
 	if($(selector).length >= 1){
 		$(selector).click();
 		$(selector).click();
@@ -11346,10 +11372,16 @@ function shortcut(selectors = ''){
 			selectors = [];
 			selectors = [];
 		}else{
 		}else{
 			switch (selectors){
 			switch (selectors){
+				case 'plugin-marketplace':
+					clickSettingsTab();
+					selectors = ['#settings-main-plugins-anchor', '#settings-plugins-marketplace-anchor'];
+					break;
 				case 'custom-cert':
 				case 'custom-cert':
+					clickSettingsTab();
 					selectors = ['#settings-main-system-settings-anchor','#settings-settings-main-anchor','a[href$="Certificate"]'];
 					selectors = ['#settings-main-system-settings-anchor','#settings-settings-main-anchor','a[href$="Certificate"]'];
 					break;
 					break;
 				default:
 				default:
+					clickSettingsTab();
 					selectors = ['#settings-main-system-settings-anchor'];
 					selectors = ['#settings-main-system-settings-anchor'];
 
 
 			}
 			}

+ 141 - 141
js/langpack/fr[French].json

@@ -519,7 +519,7 @@
         "Unifi": "Unifi",
         "Unifi": "Unifi",
         "Pi-hole": "Pi-hole",
         "Pi-hole": "Pi-hole",
         "Check For Updates": "Vérifier mise à jour",
         "Check For Updates": "Vérifier mise à jour",
-        "I will try and import new strings every Friday": "Je vais essayer d'importer de nouvelles chaînes tous les vendredis",
+        "I will try and import new strings every Friday": "Je vais essayer d'importer de nouvelles traductions tous les vendredis",
         "Custom definitions": "Définitions personnalisées",
         "Custom definitions": "Définitions personnalisées",
         "Show on small screens": "Voir sur les petites écrans",
         "Show on small screens": "Voir sur les petites écrans",
         "Show on medium screens": "Voir sur les écrans moyennes",
         "Show on medium screens": "Voir sur les écrans moyennes",
@@ -618,10 +618,10 @@
         "An Error Occurred": "Une erreur s'est produite",
         "An Error Occurred": "Une erreur s'est produite",
         "Type your message": "Entrer votre message",
         "Type your message": "Entrer votre message",
         "Bookmark Tabs": "Onglet des favoris",
         "Bookmark Tabs": "Onglet des favoris",
-        "Bookmark Categories": "Bookmark Categories",
+        "Bookmark Categories": "Catégories Favori",
         "Open Collective": "Open Collective",
         "Open Collective": "Open Collective",
         "Github Sponsor": "Sponsor Github",
         "Github Sponsor": "Sponsor Github",
-        "Backers": "Backers",
+        "Backers": "Supporters",
         "Tab Folder": "Dossier des onglets",
         "Tab Folder": "Dossier des onglets",
         "Cache Folder": "Dossier du cache",
         "Cache Folder": "Dossier du cache",
         "Backup": "Sauvegarde",
         "Backup": "Sauvegarde",
@@ -631,27 +631,27 @@
         "Less": "Moins",
         "Less": "Moins",
         "OpenCollective Sponsor": "Sponsor OpenCollective",
         "OpenCollective Sponsor": "Sponsor OpenCollective",
         "Patreon Sponsor": "Sponsor Patreon",
         "Patreon Sponsor": "Sponsor Patreon",
-        "New Organizr API v2": "New Organizr API v2",
-        "Develop Branch Users - Please switch to Master for mean time": "Develop Branch Users - Please switch to Master for mean time",
+        "New Organizr API v2": "API Organizr v2",
+        "Develop Branch Users - Please switch to Master for mean time": "Branche Utilisateurs Develop - Veuillez switcher sur Master pendant un moment",
         "API V2 TESTING almost complete": "API V2 TESTING presque complet",
         "API V2 TESTING almost complete": "API V2 TESTING presque complet",
         "Important Messages - Each message can now be ignored using ignore button": "Messages importants - Chaque message peut maintenant être ignoré en utilisant ce bouton",
         "Important Messages - Each message can now be ignored using ignore button": "Messages importants - Chaque message peut maintenant être ignoré en utilisant ce bouton",
         "Minimum PHP Version change": "Changement de la version minimum de PHP",
         "Minimum PHP Version change": "Changement de la version minimum de PHP",
-        "You": "You",
+        "You": "Toi",
         "Drop Certificate file here to upload": "Déposer le fichier de certificat ici pour le téléverser",
         "Drop Certificate file here to upload": "Déposer le fichier de certificat ici pour le téléverser",
-        "Custom Certificate Loaded": "Custom Certificate Loaded",
-        "By default, Organizr uses certificates from https://curl.se/docs/caextract.html": "By default, Organizr uses certificates from https://curl.se/docs/caextract.html",
-        "If you would like to use your own certificate, please upload it below.  You will then need to enable each homepage item to use it.": "If you would like to use your own certificate, please upload it below.  You will then need to enable each homepage item to use it.",
+        "Custom Certificate Loaded": "Charger un certificat personnalisé",
+        "By default, Organizr uses certificates from https://curl.se/docs/caextract.html": "Par défaut, Organizr utilise les certificats depuis https://curl.se/docs/caextract.html",
+        "If you would like to use your own certificate, please upload it below.  You will then need to enable each homepage item to use it.": "Si vous voulez utiliser votre propre certificat, merci de l'uploader ici. Vous aurez alors besoin de l'activer sur chaque élément de la page d'accueil pour l'utiliser.",
         "i.e. X-Forwarded-Email": "i.e. X-Forwarded-Email",
         "i.e. X-Forwarded-Email": "i.e. X-Forwarded-Email",
-        "Auth Proxy Header Name for Email": "Auth Proxy Header Name for Email",
-        "Custom Recover Password Text": "Custom Recover Password Text",
-        "Disable Recover Password": "Disable Recover Password",
-        "Blacklisted Error Message": "Blacklisted Error Message",
-        "Blacklisted IP's": "Blacklisted IP's",
-        "http(s)://domain": "http(s)://domain",
-        "Traefik Domain for Return Override": "Traefik Domain for Return Override",
-        "Jellyfin Token": "Jellyfin Token",
-        "Jellyfin URL": "Jellyfin URL",
-        "Enable LDAP TLS": "Enable LDAP TLS",
+        "Auth Proxy Header Name for Email": "Nom de l'en-tête Auth Proxy pour les mails",
+        "Custom Recover Password Text": "Note personnalisée pour la récupération du mot de passe",
+        "Disable Recover Password": "Désactiver la récupération de mot de passe",
+        "Blacklisted Error Message": "Message d'erreur sur la liste noire",
+        "Blacklisted IP's": "IP blacklistées",
+        "http(s)://domain": "http(s)://domaine",
+        "Traefik Domain for Return Override": "Domaine Traefik pour le retour prioritaire",
+        "Jellyfin Token": "Token Jellyfin",
+        "Jellyfin URL": "URL Jellyfin",
+        "Enable LDAP TLS": "Activer LDAP TLS",
         "Enable LDAP SSL": "Activer le LDAP SSL",
         "Enable LDAP SSL": "Activer le LDAP SSL",
         "Bind Password": "Lier le mot de passe",
         "Bind Password": "Lier le mot de passe",
         "http(s) | ftp(s) | ldap(s)://hostname:port": "http(s) | ftp(s) | ldap(s)://hostname:port",
         "http(s) | ftp(s) | ldap(s)://hostname:port": "http(s) | ftp(s) | ldap(s)://hostname:port",
@@ -665,13 +665,13 @@
         "http(s)://domain.com": "http(s)://domain.com",
         "http(s)://domain.com": "http(s)://domain.com",
         "Jellyfin SSO URL": "URL SSO de Jellyfin",
         "Jellyfin SSO URL": "URL SSO de Jellyfin",
         "Jellyfin API URL": "URL de l'API de Jellyfin",
         "Jellyfin API URL": "URL de l'API de Jellyfin",
-        "Ombi Fallback Password": "Ombi Fallback Password",
-        "Ombi Fallback User": "Ombi Fallback User",
-        "Petio Fallback Password": "Petio Fallback Password",
-        "Petio Fallback User": "Petio Fallback User",
+        "Ombi Fallback Password": "Mot de passe de secours pour Ombi",
+        "Ombi Fallback User": "Utilisateur de secours pour Ombi",
+        "Petio Fallback Password": "Mot de passe de secours pour Petio",
+        "Petio Fallback User": "Utilisateur de secours pour Petio",
         "Petio URL": "URL de Petio",
         "Petio URL": "URL de Petio",
-        "Overseerr Fallback Password": "Overseerr Fallback Password",
-        "Overseerr Fallback User": "Overseerr Fallback User",
+        "Overseerr Fallback Password": "Mot de passe de secours pour Overseerr",
+        "Overseerr Fallback User": "Utilisateur de secours pour Overseerr",
         "Overseerr URL": "URL d'Overseerr",
         "Overseerr URL": "URL d'Overseerr",
         "Multiple URL's": "URLs multiples",
         "Multiple URL's": "URLs multiples",
         "Using multiple SSO application will cause your Cookie Header item to increase.  If you haven't increased it by now, please follow this guide": "Utiliser plusieurs application en SSO va provoquez une augmentation de l'en-tête des cookies. Si vous avez déjà augmenté l'entête de vos cookies, suivez ce guide",
         "Using multiple SSO application will cause your Cookie Header item to increase.  If you haven't increased it by now, please follow this guide": "Utiliser plusieurs application en SSO va provoquez une augmentation de l'en-tête des cookies. Si vous avez déjà augmenté l'entête de vos cookies, suivez ce guide",
@@ -679,23 +679,23 @@
         "Jellyfin": "Jellyfin",
         "Jellyfin": "Jellyfin",
         "Petio": "Petio",
         "Petio": "Petio",
         "Overseerr": "Overseerr",
         "Overseerr": "Overseerr",
-        "FYI": "FYI",
-        "https://app.plex.tv/auth#?resetPassword": "https://app.plex.tv/auth#?resetPassword",
-        "Change Password on Plex Website": "Change Password on Plex Website",
+        "FYI": "Pour votre information",
+        "https://app.plex.tv/auth#?resetPassword": "\nhttps://app.plex.tv/auth#?resetPassword\n",
+        "Change Password on Plex Website": "Changer le mot de passer sur le site web de Plex",
         "Action": "Action",
         "Action": "Action",
         "IP": "IP",
         "IP": "IP",
         "Browser": "Navigateur",
         "Browser": "Navigateur",
-        "Expires": "Expires",
-        "Created": "Created",
+        "Expires": "Expire",
+        "Created": "Créé",
         "Version": "Version",
         "Version": "Version",
         "Files": "Fichiers",
         "Files": "Fichiers",
-        "Backup Organizr": "Backup Organizr",
-        "Create Backup": "Create Backup",
-        "Select or type Image": "Select or type Image",
-        "Choose": "Choose",
-        "Choose Blackberry Theme Icon": "Choose Blackberry Theme Icon",
-        "Save Tab Order": "Save Tab Order",
-        "Drag Homepage Items to Order Them": "Drag Homepage Items to Order Them",
+        "Backup Organizr": "Sauvegarde Organizr",
+        "Create Backup": "Créer une sauvegarde",
+        "Select or type Image": "Sélectionnez ou saisissez une image",
+        "Choose": "Choisir",
+        "Choose Blackberry Theme Icon": "Choisir le thème Icône Blackberry",
+        "Save Tab Order": "Sauvegarder l'ordre des onglets",
+        "Drag Homepage Items to Order Them": "Glisser les éléments de la page d'accueil pour les mettre dans l'ordre",
         "Preview": "Aperçu",
         "Preview": "Aperçu",
         "Text Color": "Couleur de text",
         "Text Color": "Couleur de text",
         "Background Color": "Couleur d'arrière plan",
         "Background Color": "Couleur d'arrière plan",
@@ -717,96 +717,96 @@
         "Colors & Themes": "Couleurs & Thèmes",
         "Colors & Themes": "Couleurs & Thèmes",
         "Options": "Options",
         "Options": "Options",
         "Login Page": "Page de connexion",
         "Login Page": "Page de connexion",
-        "Top Bar": "Top Bar",
+        "Top Bar": "Barre du haut",
         "Bookmark Settings": "Paramètres de marque-page",
         "Bookmark Settings": "Paramètres de marque-page",
         "HnL Settings": "Paramètres HnL",
         "HnL Settings": "Paramètres HnL",
         "Not Installed": "Non installé",
         "Not Installed": "Non installé",
         "Money not an option?  No problem.  Show some love to this Google Ad below:": "L'argent n'est pas une option ? Aucun problème. Montrez de l'amour sur cet Google Ad ci-dessous :",
         "Money not an option?  No problem.  Show some love to this Google Ad below:": "L'argent n'est pas une option ? Aucun problème. Montrez de l'amour sur cet Google Ad ci-dessous :",
-        "Please click the button to continue.": "Please click the button to continue.",
-        "Need specialized support or just want to support Organizr?  If so head to Open Collective...": "Need specialized support or just want to support Organizr?  If so head to Open Collective...",
-        "Need specialized support or just want to support Organizr?  If so head to Patreon...": "Need specialized support or just want to support Organizr?  If so head to Patreon...",
-        "Want to donate a small amount of Crypto?.": "Want to donate a small amount of Crypto?.",
-        "Please use the QR Code or Wallet ID.": "Please use the QR Code or Wallet ID.",
-        "If you use the Square Cash App, you can donate with that if you like.": "If you use the Square Cash App, you can donate with that if you like.",
-        "I have chosen to go with PayPal Pools so everyone can see how much people have donated.": "I have chosen to go with PayPal Pools so everyone can see how much people have donated.",
-        "Want to show support on Github?  Sponsor me :)": "Want to show support on Github?  Sponsor me :)",
-        "If messages get stuck sending, please turn this option off.": "If messages get stuck sending, please turn this option off.",
-        "Save and reload!": "Save and reload!",
-        "Copy and paste the 4 values into Organizr": "Copy and paste the 4 values into Organizr",
-        "Click the overview tab on top left": "Click the overview tab on top left",
+        "Please click the button to continue.": "Merci de cliquer sur le bouton pour continuer",
+        "Need specialized support or just want to support Organizr?  If so head to Open Collective...": "Besoin d'un support spécifique ou uniquement le support d'Organizr ? Si c'est le cas, rendez-vous sur Open Collective...",
+        "Need specialized support or just want to support Organizr?  If so head to Patreon...": "Besoin d'un support spécifique ou uniquement le support d'Organizr ? Si c'est le cas, rendez-vous sur Patreon...",
+        "Want to donate a small amount of Crypto?.": "Vous voulez donner une petite somme en Crypto ?",
+        "Please use the QR Code or Wallet ID.": "Merci d'utiliser le QR Code ou le Wallet ID",
+        "If you use the Square Cash App, you can donate with that if you like.": "Si vous utilisez l'app Square Cash, vous pouvez faire un don avec si vous le souhaitez.",
+        "I have chosen to go with PayPal Pools so everyone can see how much people have donated.": "J'ai choisi d'utiliser PayPal Pools pour que tout le monde puisse voir combien de personnes ont fait un don.",
+        "Want to show support on Github?  Sponsor me :)": "Vous voulez montrer votre soutien sur Github ? Sponsorisez moi :)",
+        "If messages get stuck sending, please turn this option off.": "Si les messages sont bloqués lors de l'envoi, veuillez désactiver cette option.",
+        "Save and reload!": "Sauvegarder et relancer !",
+        "Copy and paste the 4 values into Organizr": "Copier et coller les 4 valeurs dans Organizr",
+        "Click the overview tab on top left": "Cliquez en haut à gauche pour visualiser l'onglet",
         "Frontend (JQuery) - Backend (PHP)": "Frontend (JQuery) - Backend (PHP)",
         "Frontend (JQuery) - Backend (PHP)": "Frontend (JQuery) - Backend (PHP)",
-        "Create an App called whatever you like and choose a cluster (Close to you)": "Create an App called whatever you like and choose a cluster (Close to you)",
-        "Signup for Pusher [FREE]": "Signup for Pusher [FREE]",
+        "Create an App called whatever you like and choose a cluster (Close to you)": "Créez une application appelée comme vous le souhaitez et choisissez un cluster (à proximité)",
+        "Signup for Pusher [FREE]": "Inscription pour Pusher [GRATUIT]",
         "Connection": "Connexion",
         "Connection": "Connexion",
         "Enabled": "Activé",
         "Enabled": "Activé",
         "Internal URL": "URL interne",
         "Internal URL": "URL interne",
         "External URL": "URL externe",
         "External URL": "URL externe",
         "UUID": "UUID",
         "UUID": "UUID",
-        "Service Name": "Service Name",
-        "Make sure to save before using the import button on Services tab": "Make sure to save before using the import button on Services tab",
-        "Do not use a Read-Only Token as that will not give a correct UUID for sending the results to HealthChecks.io": "Do not use a Read-Only Token as that will not give a correct UUID for sending the results to HealthChecks.io",
-        "Please use a Full Access Token": "Please use a Full Access Token",
-        "URL for HealthChecks API": "URL for HealthChecks API",
-        "403 Error as Success": "403 Error as Success",
-        "401 Error as Success": "401 Error as Success",
-        "HealthChecks Ping URL": "HealthChecks Ping URL",
-        "URL for HealthChecks Ping": "URL for HealthChecks Ping",
+        "Service Name": "Nom du service",
+        "Make sure to save before using the import button on Services tab": "S'assurer d'avoir sauvegarder avant d'appuyer sur le bouton import sur l'onglet Services",
+        "Do not use a Read-Only Token as that will not give a correct UUID for sending the results to HealthChecks.io": "N'utilisez pas de Token en lecture seule car cela ne donnera pas un UUID correct pour l'envoi des résultats à HealthChecks.io",
+        "Please use a Full Access Token": "Merci d'utiliser le Full Access Token",
+        "URL for HealthChecks API": "URL pour l'API HealthChecks",
+        "403 Error as Success": "403 Erreur comme réussite",
+        "401 Error as Success": "401 Erreur comme réussite",
+        "HealthChecks Ping URL": "URL Ping HealthChecks",
+        "URL for HealthChecks Ping": "URL du Ping HealthChecks",
         "As often as you like - i.e. every 1 minute": "Aussi souvent que vous le souhaitez - i.e toute les 1 minute",
         "As often as you like - i.e. every 1 minute": "Aussi souvent que vous le souhaitez - i.e toute les 1 minute",
-        "Frequency": "Frequency",
-        "CRON Job URL": "CRON Job URL",
+        "Frequency": "Fréquence",
+        "CRON Job URL": "URL Job CRON",
         "Once this plugin is setup, you will need to setup a CRON job": "Une fois ce plugin paramétré, vous devrez mettre en place un CRON job",
         "Once this plugin is setup, you will need to setup a CRON job": "Une fois ce plugin paramétré, vous devrez mettre en place un CRON job",
         "Services": "Services",
         "Services": "Services",
-        "Import Services": "Import Services",
+        "Import Services": "Services Import",
         "Add New Service": "Ajouter un nouveau service",
         "Add New Service": "Ajouter un nouveau service",
         "After enabling for the first time, please reload the page - Menu is located under User menu on top right": "Après avoir activé pour la première fois, rechargez la page - Le menu se trouve sous le menu Utilisateur en haut à droite",
         "After enabling for the first time, please reload the page - Menu is located under User menu on top right": "Après avoir activé pour la première fois, rechargez la page - Le menu se trouve sous le menu Utilisateur en haut à droite",
         "Emby Settings": "Paramètres Emby",
         "Emby Settings": "Paramètres Emby",
         "Plex Settings": "Paramètres Plex",
         "Plex Settings": "Paramètres Plex",
-        "Backend": "Backend",
-        "Templates": "Templates",
+        "Backend": "Back-end",
+        "Templates": "Modèles",
         "Test & Options": "Test & Options",
         "Test & Options": "Test & Options",
         "Sender Information": "Information de l'envoyeur",
         "Sender Information": "Information de l'envoyeur",
         "Host": "Hôte",
         "Host": "Hôte",
-        "Open your custom Bookmark page via menu.": "Open your custom Bookmark page via menu.",
-        "Create Bookmark tabs in the new area in": "Create Bookmark tabs in the new area in",
-        "Create Bookmark categories in the new area in": "Create Bookmark categories in the new area in",
+        "Open your custom Bookmark page via menu.": "Ouvrir votre page de Favori personnalisé via le menu.",
+        "Create Bookmark tabs in the new area in": "Créer des onglets Favori dans le nouvel espace dans",
+        "Create Bookmark categories in the new area in": "Créer des catégories Favori dans le nouvel espace dans",
         "Add tab that points to": "Ajouter un nouvel onglet qui pointe vers",
         "Add tab that points to": "Ajouter un nouvel onglet qui pointe vers",
         "and set it's type to": "et défini son type à",
         "and set it's type to": "et défini son type à",
-        "Checking for bookmark default category...": "Checking for bookmark default category...",
-        "Checking for Bookmark tab...": "Checking for Bookmark tab...",
-        "Automatic Setup Tasks": "Automatic Setup Tasks",
-        "Located at": "Located at",
-        "Custom Certificate Status": "Custom Certificate Status",
-        "Will play a sound if the server goes down and will play sound if comes back up.": "Will play a sound if the server goes down and will play sound if comes back up.",
-        "Please choose a unique value for added security": "Please choose a unique value for added security",
-        "IPv4 only at the moment - This must be set to work, will accept subnet or IP address": "IPv4 only at the moment - This must be set to work, will accept subnet or IP address",
-        "Enable option to set Auth Proxy Header Login": "Enable option to set Auth Proxy Header Login",
-        "Text or HTML for recovery password section": "Text or HTML for recovery password section",
-        "Disables recover password area": "Disables recover password area",
-        "Enables the local address forward if on local address and accessed from WAN Domain": "Enables the local address forward if on local address and accessed from WAN Domain",
+        "Checking for bookmark default category...": "En train de vérifier la catégorie Favori par défaut...",
+        "Checking for Bookmark tab...": "En train de vérifier l'onglet Favori...",
+        "Automatic Setup Tasks": "Tâches d'installation Automatique",
+        "Located at": "Situé à",
+        "Custom Certificate Status": "Statut du certificat personnalisé",
+        "Will play a sound if the server goes down and will play sound if comes back up.": "Jouera un son si le serveur tombe en panne et jouera un son s'il revient.",
+        "Please choose a unique value for added security": "Veuillez choisir une valeur unique pour plus de sécurité",
+        "IPv4 only at the moment - This must be set to work, will accept subnet or IP address": "IPv4 uniquement pour le moment - Cela doit être configuré pour fonctionner, sera accepté sous-réseau ou adresse IP",
+        "Enable option to set Auth Proxy Header Login": "Activer l'option pour définir Auth Proxy Header Login",
+        "Text or HTML for recovery password section": "Texte ou HTML pour la partie mot de passe de récupération",
+        "Disables recover password area": "Zone de récupération de mot de passe désactivée",
+        "Enables the local address forward if on local address and accessed from WAN Domain": "Active la redirection de l'adresse local si elle est accessible depuis le domaine WAN",
         "Full local address of organizr install - i.e. http://home.local or http://192.168.0.100": "Adresse locale complète de l'installation d'Organizr - i.e. http://home.local ou http://192.168.0.100",
         "Full local address of organizr install - i.e. http://home.local or http://192.168.0.100": "Adresse locale complète de l'installation d'Organizr - i.e. http://home.local ou http://192.168.0.100",
-        "Enter domain if you wish to be forwarded to a local address - Local Address filled out on next item": "Enter domain if you wish to be forwarded to a local address - Local Address filled out on next item",
-        "IPv4 only at the moment - This will set your login as local if your IP falls within the From and To": "IPv4 only at the moment - This will set your login as local if your IP falls within the From and To",
-        "Default status of Remember Me button on login screen": "Default status of Remember Me button on login screen",
-        "Number of days cookies and tokens will be valid for": "Number of days cookies and tokens will be valid for",
-        "Enable this to hide the Registration button on the login screen": "Enable this to hide the Registration button on the login screen",
-        "Sets the password for the Registration form on the login screen": "Sets the password for the Registration form on the login screen",
-        "WARNING! This will block anyone with these IP's": "WARNING! This will block anyone with these IP's",
-        "WARNING! This can potentially mess up your iFrames": "WARNING! This can potentially mess up your iFrames",
-        "Please use a FQDN on this URL Override": "Please use a FQDN on this URL Override",
-        "This will enable the webserver to forward errors so traefik will accept them": "This will enable the webserver to forward errors so traefik will accept them",
-        "Please make sure to use local IP address and port - You also may use local dns name too.": "Please make sure to use local IP address and port - You also may use local dns name too.",
-        "Remember! Please save before using the test button!": "Remember! Please save before using the test button!",
-        "This will enable the use of TLS for LDAP connections": "This will enable the use of TLS for LDAP connections",
-        "This will enable the use of SSL for LDAP connections": "This will enable the use of SSL for LDAP connections",
-        "Enabling this will bypass external 2FA security if user is on local Subnet": "Enabling this will bypass external 2FA security if user is on local Subnet",
-        "Enabling this will only allow Friends that have shares to the Machine ID entered above to login, Having this disabled will allow all Friends on your Friends list to login": "Enabling this will only allow Friends that have shares to the Machine ID entered above to login, Having this disabled will allow all Friends on your Friends list to login",
-        "Since you are using the official Docker image, you can just restart your Docker container to update Organizr": "Since you are using the official Docker image, you can just restart your Docker container to update Organizr",
-        "Since you are using the Official Docker image, Change the image to change the branch": "Since you are using the Official Docker image, Change the image to change the branch",
-        "Choose which Settings Tab to be default when opening settings page": "Choose which Settings Tab to be default when opening settings page",
-        "Please make sure to use the same (sub)domain to access Jellyfin as Organizr's": "Please make sure to use the same (sub)domain to access Jellyfin as Organizr's",
-        "Please make sure to use the local address to the API": "Please make sure to use the local address to the API",
-        "DO NOT SET THIS TO YOUR ADMIN ACCOUNT. We recommend you create a local account as a \"catch all\" for when Organizr is unable to perform SSO.  Organizr will request a User Token based off of this user credentials": "DO NOT SET THIS TO YOUR ADMIN ACCOUNT. We recommend you create a local account as a \"catch all\" for when Organizr is unable to perform SSO.  Organizr will request a User Token based off of this user credentials",
-        "Purge Log": "Purge Log",
+        "Enter domain if you wish to be forwarded to a local address - Local Address filled out on next item": "Entrez le domaine si vous souhaitez être redirigé vers une adresse locale - l'adresse locale a renseigner sur l'élément suivant",
+        "IPv4 only at the moment - This will set your login as local if your IP falls within the From and To": "IPv4 uniquement pour le moment - Cela définira votre connexion comme locale si votre IP se situe dans les champs De et À",
+        "Default status of Remember Me button on login screen": "Statut par défaut du bouton Se souvenir de moi sur l'écran de connexion",
+        "Number of days cookies and tokens will be valid for": "Nombre de jours de validité des cookies et des tokens",
+        "Enable this to hide the Registration button on the login screen": "Activez cette option pour masquer le bouton Inscription sur l'écran de connexion",
+        "Sets the password for the Registration form on the login screen": "Définit le mot de passe pour le formulaire d'inscription sur l'écran de connexion",
+        "WARNING! This will block anyone with these IP's": "ATTENTION! Cela bloquera toute personne avec ces IP",
+        "WARNING! This can potentially mess up your iFrames": "ATTENTION! Cela peut potentiellement endommager vos iFrames",
+        "Please use a FQDN on this URL Override": "Veuillez utiliser un FQDN sur cette URL Override",
+        "This will enable the webserver to forward errors so traefik will accept them": "Cela permettra au serveur Web de transmettre les erreurs afin que traefik les accepte",
+        "Please make sure to use local IP address and port - You also may use local dns name too.": "Assurez-vous d'utiliser l'adresse IP et le port locaux - Vous pouvez également utiliser le nom DNS local.",
+        "Remember! Please save before using the test button!": "Attention ! Veuillez enregistrer avant d'utiliser le bouton de test !",
+        "This will enable the use of TLS for LDAP connections": "Ceci activera l'utilisation du TLS pour les connexions LDAP",
+        "This will enable the use of SSL for LDAP connections": "Ceci activera l'utilisation du SSL pour les connexions LDAP",
+        "Enabling this will bypass external 2FA security if user is on local Subnet": "L'activation de cela contournera la sécurité 2FA si l'utilisateur est dans le sous-réseau local",
+        "Enabling this will only allow Friends that have shares to the Machine ID entered above to login, Having this disabled will allow all Friends on your Friends list to login": "L'activation de cette option permettra, uniquement à vos amis qui ont des partages avec l'ID de machine saisi ci-dessus, de se connecter. La désactivation de cette option permettra à tous les amis de votre liste d'amis de se connecter.",
+        "Since you are using the official Docker image, you can just restart your Docker container to update Organizr": "Puisque vous utilisez l'image Docker officielle, vous pouvez simplement redémarrer votre conteneur Docker pour mettre à jour Organizr",
+        "Since you are using the Official Docker image, Change the image to change the branch": "Puisque vous utilisez l'image Docker officielle, changez d'image pour changer de branche",
+        "Choose which Settings Tab to be default when opening settings page": "Choisissez l'onglet Paramètres à utiliser par défaut lors de l'ouverture de la page des paramètres",
+        "Please make sure to use the same (sub)domain to access Jellyfin as Organizr's": "Veuillez vous assurer d'utiliser le même (sous-)domaine pour accéder à Jellyfin que celui d'Organizr",
+        "Please make sure to use the local address to the API": "Bien s'assurer d'utiliser l'adresse locale pour l'API",
+        "DO NOT SET THIS TO YOUR ADMIN ACCOUNT. We recommend you create a local account as a \"catch all\" for when Organizr is unable to perform SSO.  Organizr will request a User Token based off of this user credentials": "NE PAS METTRE CECI SUR VOTRE COMPTE ADMIN. Nous vous recommandons de créer un compte local en tant que \"attrape-tout\" lorsque Organizr est incapable d'effectuer l'authentification unique. Organizr demandera un jeton d'utilisateur basé sur les informations d'identification de cet utilisateur",
+        "Purge Log": "Supprimer le log",
         "Avatar": "Avatar",
         "Avatar": "Avatar",
         "Date Registered": "Date enregistrée",
         "Date Registered": "Date enregistrée",
         "Group": "Groupe",
         "Group": "Groupe",
@@ -814,46 +814,46 @@
         "Copy to Clipboard": "Copier dans le presse-papier",
         "Copy to Clipboard": "Copier dans le presse-papier",
         "Choose action:": "Choisir une action :",
         "Choose action:": "Choisir une action :",
         "You may enter multiple URL's using the CSV format.  i.e. link#1,link#2,link#3": "Vous pouvez entrer plusieurs URLs en utilisant le format CSV. i.e. link#1,link#2,link#3",
         "You may enter multiple URL's using the CSV format.  i.e. link#1,link#2,link#3": "Vous pouvez entrer plusieurs URLs en utilisant le format CSV. i.e. link#1,link#2,link#3",
-        "Used to set the description for SEO meta tags": "Used to set the description for SEO meta tags",
+        "Used to set the description for SEO meta tags": "Utilisé pour définir la description des méta tags SEO",
         "Also sets the title of your site": "Défini également le titre de votre site",
         "Also sets the title of your site": "Défini également le titre de votre site",
-        "Up to date": "Up to date",
-        "Loading Pihole...": "Loading Pihole...",
-        "Loading Unifi...": "Loading Unifi...",
-        "Loading Weather...": "Loading Weather...",
-        "Loading Tautulli...": "Loading Tautulli...",
-        "Loading Health Checks...": "Loading Health Checks...",
-        "Health Checks": "Health Checks",
+        "Up to date": "A jour",
+        "Loading Pihole...": "Chargement de Pihole...",
+        "Loading Unifi...": "Chargement de Unifi...",
+        "Loading Weather...": "Chargement de la météo...",
+        "Loading Tautulli...": "Chargement de Tautulli...",
+        "Loading Health Checks...": "Chargement du Bilan de Santé",
+        "Health Checks": "Bilan de Santé",
         "UniFi": "UniFi",
         "UniFi": "UniFi",
-        "Connection Error to rTorrent": "Connection Error to rTorrent",
-        "Request a Show or Movie": "Request a Show or Movie",
-        "Set": "Set",
-        "Set WAL Mode": "Set WAL Mode",
-        "Set DELETE Mode (Default)": "Set DELETE Mode (Default)",
-        "Journal Mode Status": "Journal Mode Status",
-        "This feature is experimental - You may face unexpected database is locked errors in logs": "This feature is experimental - You may face unexpected database is locked errors in logs",
+        "Connection Error to rTorrent": "Erreur de connexion à rTorrent",
+        "Request a Show or Movie": "Requête pour une série ou un film",
+        "Set": "Définir",
+        "Set WAL Mode": "Définir le mode WAL",
+        "Set DELETE Mode (Default)": "Définir le mode EFFACER (défaut)",
+        "Journal Mode Status": "Statut Mode Journal",
+        "This feature is experimental - You may face unexpected database is locked errors in logs": "Cette fonctionnalité est expérimentale - Vous pouvez rencontrer des erreurs inattendues de base de données verrouillées dans les logs",
         "Warning": "Attention",
         "Warning": "Attention",
-        "Tab Help": "Tab Help",
-        "Toggle this tab to loaded in the background on page load": "Toggle this tab to loaded in the background on page load",
-        "Preload": "Preload",
-        "Enable Organizr to ping the status of the local URL of this tab": "Enable Organizr to ping the status of the local URL of this tab",
-        "Toggle this to add the tab to the Splash Page on page load": "Toggle this to add the tab to the Splash Page on page load",
-        "Splash": "Splash",
-        "Either mark a tab as active or inactive": "Either mark a tab as active or inactive",
-        "You can choose one tab to be the first opened tab on page load": "You can choose one tab to be the first opened tab on page load",
-        "Default": "Default",
-        "Internal is for Organizr pages": "Internal is for Organizr pages",
-        "iFrame is for all others": "iFrame is for all others",
-        "New Window is for items to open in a new window": "New Window is for items to open in a new window",
-        "The lowest Group that will have access to this tab": "The lowest Group that will have access to this tab",
-        "Each Tab is assigned a Category, the default is unsorted.  You may create new categories on the Category settings tab": "Each Tab is assigned a Category, the default is unsorted.  You may create new categories on the Category settings tab",
-        "Category": "Category",
-        "The text that will be displayed for that certain tab": "The text that will be displayed for that certain tab",
-        "Please Save before Testing. Note that using a blank password might not work correctly.": "Please Save before Testing. Note that using a blank password might not work correctly.",
-        "Use Custom Certificate": "Use Custom Certificate",
-        "Note that using a blank password might not work correctly.": "Note that using a blank password might not work correctly.",
-        "Database Password": "Database Password",
-        "Database Username": "Database Username",
-        "Database Host": "Database Host",
+        "Tab Help": "Aide pour les onglets",
+        "Toggle this tab to loaded in the background on page load": "Basculez cet onglet pour qu'il soit chargé en arrière-plan lors du chargement de la page",
+        "Preload": "Précharger",
+        "Enable Organizr to ping the status of the local URL of this tab": "Activer Organizr pour envoyer un ping sur l'état de l'URL locale de cet onglet",
+        "Toggle this to add the tab to the Splash Page on page load": "Basculez ceci pour ajouter l'onglet à la Page de démarrage lors du chargement de la page",
+        "Splash": "Démarrage",
+        "Either mark a tab as active or inactive": "Marquer un onglet comme actif ou inactif",
+        "You can choose one tab to be the first opened tab on page load": "Vous pouvez choisir un onglet qui sera le premier onglet d'ouvert lors du chargement de la page",
+        "Default": "Défaut",
+        "Internal is for Organizr pages": "Interne est pour les pages Organizr",
+        "iFrame is for all others": "IFrame pour tous les autres",
+        "New Window is for items to open in a new window": "Nouvelle Fenêtre permet aux éléments de s'ouvrir dans une nouvelle fenêtre",
+        "The lowest Group that will have access to this tab": "Le groupe le plus bas aura accès à cet onglet",
+        "Each Tab is assigned a Category, the default is unsorted.  You may create new categories on the Category settings tab": "Chaque onglet se voit attribuer une Catégorie, la valeur par défaut est non triée. Vous pouvez créer de nouvelles catégories dans l'onglet paramètres de Catégorie",
+        "Category": "Catégorie",
+        "The text that will be displayed for that certain tab": "Le texte qui sera affiché pour certains onglets",
+        "Please Save before Testing. Note that using a blank password might not work correctly.": "Merci de sauvegarder afin de tester. Attention, un mot de passe vide pourrait ne pas marcher correctement.",
+        "Use Custom Certificate": "Utiliser un certificat personnalisé",
+        "Note that using a blank password might not work correctly.": "Attention, un mot de passe vide pourrait ne pas marcher correctement.",
+        "Database Password": "Mot de passe de la base de données",
+        "Database Username": "Nom d'utilisateur de la base de données",
+        "Database Host": "Hôte de la base de données",
         ".": "."
         ".": "."
     }
     }
 }
 }

ファイルの差分が大きいため隠しています
+ 4 - 0
js/version.json


+ 30 - 0
scripts/linux-update.sh

@@ -0,0 +1,30 @@
+#!/bin/bash
+if [ -z "$1" ]
+  then
+  echo 'No branch setup.. using v2-master'
+  BRANCH="v2-master"
+elif [ "$1" == "v2-develop" ] || [ "$1" == "develop" ] || [ "$1" == "dev" ]
+  then
+  BRANCH="v2-develop"
+elif [ "$1" == "v2-master" ] || [ "$1" == "master" ]
+  then
+  BRANCH="v2-master"
+else
+  echo "$1 is not a valid branch, exiting"
+  exit 1
+fi
+SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
+UPGRADEPATH=$SCRIPTPATH"/upgrade"
+UPGRADEFILE=$SCRIPTPATH"/upgrade/upgrade.zip"
+FOLDER=$UPGRADEPATH"/Organizr-"${BRANCH#v}
+URL=https://github.com/causefx/Organizr/archive/${BRANCH}.zip
+mkdir -p $UPGRADEPATH                                                  && \
+curl -sSL ${URL} > $UPGRADEFILE                                        && \
+unzip $UPGRADEFILE -d $UPGRADEPATH                                     && \
+cd $FOLDER                                                             && \
+cp -r ./ $SCRIPTPATH/../                                               && \
+cd $SCRIPTPATH                                                         && \
+rm $UPGRADEFILE                                                        && \
+rm -rf $FOLDER                                                         && \
+rm -rf $UPGRADEPATH                                                    && \
+exit 0

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません