Ver Fonte

Merge pull request #1742 from causefx/v2-develop

V2 develop
causefx há 4 anos atrás
pai
commit
2065f690fc

+ 2 - 0
.gitignore

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

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

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

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

@@ -1,140 +1,4 @@
 <?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
-	public $version = '2.1.1030';
+	public $version = '2.1.1110';
 	// ===================================
 	// Quick php Version check
 	public $minimumPHP = '7.3';
@@ -1867,6 +1867,12 @@ class Organizr
 				$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']]]),
 			],
+			'Cron' => [
+				$this->settingsOption('cron-file'),
+				$this->settingsOption('blank'),
+				$this->settingsOption('enable', 'autoUpdateCronEnabled', ['label' => 'Auto-Update Organizr']),
+				$this->settingsOption('cron', 'autoUpdateCronSchedule'),
+			],
 			'Login' => [
 				$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']),
@@ -4578,11 +4584,13 @@ class Organizr
 	{
 		$filesList = false;
 		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;
 	}
@@ -4598,19 +4606,88 @@ class Organizr
 		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)
 	{
 		if (stripos($pluginDetails['repo'], 'github.com') !== false) {
 			$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 {
 			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;
 	}
@@ -4635,9 +4712,9 @@ class Organizr
 		$array = $array[$plugin];
 		// Check Version of Organizr against minimum version needed
 		$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->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;
 		}
 		$files = $this->getPluginFilesFromRepo($plugin, $array);
@@ -4791,6 +4868,10 @@ class Organizr
 				$response = Requests::get($repo, array(), $options);
 				if ($response->success) {
 					$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) {
 				//return false;
@@ -4818,6 +4899,8 @@ class Organizr
 					}
 					return false;
 				} else {
+					$this->setLoggerChannel('Plugins');
+					$this->logger->warning('Getting Marketplace JSON from Github', $this->apiResponseFormatter($response->body));
 					return false;
 				}
 			} 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)
 	{
 		$username = ($array['username']) ?? null;
@@ -6356,18 +6451,18 @@ class Organizr
 		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;
 		}
 		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;
 		} catch (InvalidArgumentException $e) {
 			$this->setResponse(500, $e->getMessage());

+ 3 - 1
api/config/default.php

@@ -618,5 +618,7 @@ return [
 	'logPageSize' => '50',
 	'includeDatabaseQueriesInDebug' => false,
 	'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);
 $iteratorIterator = new RecursiveIteratorIterator($directoryIterator);
 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();
 	}
 }

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

@@ -67,7 +67,7 @@ trait LogFunctions
 		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;
 		if ($file == 'combined-logs') {
@@ -107,11 +107,17 @@ trait LogFunctions
 				$lineGenerator = Bcremer\LineReader\LineReader::readLinesBackwards($file);
 				$lines = iterator_to_array($lineGenerator);
 			}
-			if ($filter) {
+			if ($filter || $trace_id) {
 				$results = [];
 				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;
@@ -123,22 +129,26 @@ trait LogFunctions
 	
 	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()
@@ -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 (isset($this->log)) {
@@ -296,8 +306,9 @@ trait LogFunctions
 				} else {
 					$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;
 			} else {
 				$this->setResponse(404, 'Log not found');

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

@@ -253,7 +253,7 @@ trait NormalFunctions
 		} elseif (isset($_SERVER['REMOTE_ADDR'])) {
 			$ipaddress = $_SERVER['REMOTE_ADDR'];
 		} else {
-			$ipaddress = 'UNKNOWN';
+			$ipaddress = '127.0.0.1';
 		}
 		if (strpos($ipaddress, ',') !== false) {
 			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)
 	{
 		$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;
 	}
 	
+	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)
 	{
 		$bytes = number_format($bytes, 0, '.', '');
@@ -585,6 +607,20 @@ trait NormalFunctions
 		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)
 	{
 		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}',
 				];
 				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':
 				$settingMerge = [
 					'type' => 'input',
@@ -218,6 +272,7 @@ trait OptionsFunction
 					'type' => 'switch',
 					'label' => 'Hide Seeding',
 				];
+				break;
 			case 'hidecompleted':
 				$settingMerge = [
 					'type' => 'switch',

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

@@ -2,9 +2,26 @@
 
 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()
 	{
+		if (!$this->docker) {
+			$this->setResponse(409, 'Your install type is not Docker');
+			return false;
+		}
 		$dockerUpdate = null;
+		ini_set('max_execution_time', 0);
+		set_time_limit(0);
 		chdir('/etc/cont-init.d/');
 		if (file_exists('./30-install')) {
 			$this->setAPIResponse('error', 'Update failed - OrgTools is deprecated - please use organizr/organizr', 500);
@@ -23,6 +40,10 @@ trait UpdateFunctions
 	
 	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';
 		ini_set('max_execution_time', 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')
 	{
 		// 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'];
 			$details = array(
-				"seasonCount" => $child['series']['seasonCount'],
+				"seasonCount" => $child['series']['seasonCount'] ?? isset($child['series']['seasons']) ? count($child['series']['seasons']) : 0,
 				"status" => $child['series']['status'],
 				"topTitle" => $seriesName,
 				"bottomTitle" => $bottomTitle,
-				"overview" => isset($child['overview']) ? $child['overview'] : '',
+				"overview" => $child['overview'] ?? '',
 				"runtime" => $child['series']['runtime'],
 				"image" => $fanart,
 				"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);
 			}
 		}, {
-			"data": "trace_id"
+			"data": "username"
 		}, {
 			data: "context",
 			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-401-enabled' => false,
 	'HEALTHCHECKS-403-enabled' => false,
+	'HEALTHCHECKS-cron-run-enabled' => false,
+	'HEALTHCHECKS-cron-run-schedule' => '*/5 * * * *',
 	'HEALTHCHECKS-Auth-include' => '1',
 	'HEALTHCHECKS-option2-include' => '',
 	'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()
 	{
 		return array(
-			'FYI' => array(
-				array(
+			'Cron' => array(
+				/*array(
 					'type' => 'html',
 					'label' => '',
 					'override' => 12,
 					'html' => '
 						<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>
 						'
-				)
+				),*/
+				$this->settingsOption('cron-file'),
+				$this->settingsOption('blank'),
+				$this->settingsOption('enable', 'HEALTHCHECKS-cron-run-enabled'),
+				$this->settingsOption('cron', 'HEALTHCHECKS-cron-run-schedule')
 			),
 			'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(
 					'type' => 'input',
 					'name' => 'HEALTHCHECKS-PingURL',
@@ -101,23 +99,23 @@ class HealthChecks extends Organizr
 					'override' => 12,
 					'html' => '
 						<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>
 						'
 				)
@@ -264,8 +262,10 @@ class HealthChecks extends Organizr
 				$this->_healthCheckPluginUUID($v['UUID'], $pass);
 			}
 			$this->setAPIResponse('success', null, 200, $allItems);
+			return $allItems;
 		} else {
 			$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
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->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
-$app->get('/log[/{number}]', function ($request, $response, $args) {
+$app->get('/log[/{number}[/{trace_id}]]', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
 	if ($Organizr->checkRoute($request)) {
 		if ($Organizr->qualifyRequest(1, true)) {
 			$args['number'] = $args['number'] ?? 0;
+			$args['trace_id'] = $args['trace_id'] ?? null;
 			$_GET['pageSize'] = $_GET['pageSize'] ?? 1000;
 			$_GET['offset'] = $_GET['offset'] ?? 0;
 			$_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']));

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

@@ -5,6 +5,30 @@
  *     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) {
 	/**
 	 * @OA\Get(
@@ -148,6 +172,30 @@ $app->get('/update/windows', function ($request, $response, $args) {
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->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) {
 	/**

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

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

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

@@ -2,7 +2,7 @@
 namespace Nekonomokochan\PhpJsonLogger;
 
 use Monolog\Formatter\JsonFormatter as BaseJsonFormatter;
-
+use Ramsey\Uuid\Uuid;
 /**
  * Class JsonFormatter
  *
@@ -22,7 +22,8 @@ class JsonFormatter extends BaseJsonFormatter
             'log_level'         => $record['level_name'],
             'message'           => $record['message'],
             'channel'           => $record['channel'],
-            'trace_id'          => $record['extra']['trace_id'],
+	        'username'          => $record['extra']['trace_id'],
+	        'trace_id'          => $this->generateUuid(),
             'file'              => $record['extra']['file'],
             'line'              => $record['extra']['line'],
             'context'           => $record['context'],
@@ -55,4 +56,9 @@ class JsonFormatter extends BaseJsonFormatter
 
         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 $context
      */
-    public function debug($message, array $context = [])
+    public function debug($message, $context = '')
     {
+	    $context = $this->formatParamToArray($context);
         $this->addDebug($message, $context);
     }
 
     /**
      * @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);
     }
 
     /**
      * @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);
     }
 
     /**
      * @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);
     }
 
     /**
      * @param \Throwable $e
-     * @param array $context
+     * @param $context
      */
-    public function error($e, array $context = [])
+    public function error($e, $context = '')
     {
         if ($this->isErrorObject($e) === false) {
             throw new \InvalidArgumentException(
                 $this->generateInvalidArgumentMessage(__METHOD__)
             );
         }
-
+	    $context = $this->formatParamToArray($context);
         $this->addError(get_class($e), $this->formatPhpJsonLoggerErrorsContext($e, $context));
     }
 
     /**
      * @param \Throwable $e
-     * @param array $context
+     * @param $context
      */
-    public function critical($e, array $context = [])
+    public function critical($e, $context = '')
     {
         if ($this->isErrorObject($e) === false) {
             throw new \InvalidArgumentException(
                 $this->generateInvalidArgumentMessage(__METHOD__)
             );
         }
-
+	    $context = $this->formatParamToArray($context);
         $this->addCritical(get_class($e), $this->formatPhpJsonLoggerErrorsContext($e, $context));
     }
 
     /**
      * @param \Throwable $e
-     * @param array $context
+     * @param $context
      */
-    public function alert($e, array $context = [])
+    public function alert($e, $context = '')
     {
         if ($this->isErrorObject($e) === false) {
             throw new \InvalidArgumentException(
                 $this->generateInvalidArgumentMessage(__METHOD__)
             );
         }
-
+	    $context = $this->formatParamToArray($context);
         $this->addAlert(get_class($e), $this->formatPhpJsonLoggerErrorsContext($e, $context));
     }
 
     /**
      * @param \Throwable $e
-     * @param array $context
+     * @param $context
      */
-    public function emergency($e, array $context = [])
+    public function emergency($e, $context = '')
     {
         if ($this->isErrorObject($e) === false) {
             throw new \InvalidArgumentException(
                 $this->generateInvalidArgumentMessage(__METHOD__)
             );
         }
-
+	    $context = $this->formatParamToArray($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;
     }
-}
+	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
         ];
     }
-}
+}

+ 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": {
             "post": {
                 "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}": {
             "get": {
                 "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}": {
             "get": {
                 "tags": [

+ 30 - 2
js/custom.js

@@ -14,6 +14,10 @@ $(document).ready(function () {
         message('Clipboard',e.text,activeInfo.settings.notifications.position,'#FFF','info','5000');
         e.clearSelection();
     });
+	internalClipboard.on('success', function(e) {
+		message('Clipboard',e.text,activeInfo.settings.notifications.position,'#FFF','info','5000');
+		e.clearSelection();
+	});
     "use strict";
     var body = $("body");
 
@@ -1927,11 +1931,35 @@ $(document).on('click', '.ti-shift-left.mouse', function() {
 
 // Log Details
 $(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
 $(document).on("change", ".choose-organizr-log", function () {
 	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);
 });

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
js/custom.min.js


+ 38 - 6
js/functions.js

@@ -1157,10 +1157,27 @@ function buildFormItem(item){
         case 'arrayMultiple':
             return '<span class="text-danger">BuildFormItem Class not setup...';
             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:
 			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'){
 	var activePlugins = '';
 	var inactivePlugins = '';
@@ -1391,6 +1408,7 @@ function loadMarketplace(type){
     });
 }
 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) {
 		try {
 			let response = data.response;
@@ -3787,7 +3805,7 @@ function checkPluginUpdates(){
 			});
 			if(update){
 				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) {
 			organizrCatchError(e,data);
@@ -11063,20 +11081,20 @@ function jsFriendlyJSONStringify (s) {
 }
 function logContext(row){
 	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;
 }
 function formatLogDetails(details){
 	if(!details){
 		return false;
 	}
-	details = JSON.parse(details);
 	let m = moment.tz(details.datetime + 'Z', activeInfo.timezone);
 	details.datetime = moment(m).format('LLL');
 	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-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-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 = '';
@@ -11328,7 +11346,15 @@ function msToTime(s) {
 	if(ms >= '500'){ secs = pad(parseFloat(secs) + 1, 2); }
 	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){
 	if($(selector).length >= 1){
 		$(selector).click();
@@ -11346,10 +11372,16 @@ function shortcut(selectors = ''){
 			selectors = [];
 		}else{
 			switch (selectors){
+				case 'plugin-marketplace':
+					clickSettingsTab();
+					selectors = ['#settings-main-plugins-anchor', '#settings-plugins-marketplace-anchor'];
+					break;
 				case 'custom-cert':
+					clickSettingsTab();
 					selectors = ['#settings-main-system-settings-anchor','#settings-settings-main-anchor','a[href$="Certificate"]'];
 					break;
 				default:
+					clickSettingsTab();
 					selectors = ['#settings-main-system-settings-anchor'];
 
 			}

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

@@ -519,7 +519,7 @@
         "Unifi": "Unifi",
         "Pi-hole": "Pi-hole",
         "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",
         "Show on small screens": "Voir sur les petites écrans",
         "Show on medium screens": "Voir sur les écrans moyennes",
@@ -618,10 +618,10 @@
         "An Error Occurred": "Une erreur s'est produite",
         "Type your message": "Entrer votre message",
         "Bookmark Tabs": "Onglet des favoris",
-        "Bookmark Categories": "Bookmark Categories",
+        "Bookmark Categories": "Catégories Favori",
         "Open Collective": "Open Collective",
         "Github Sponsor": "Sponsor Github",
-        "Backers": "Backers",
+        "Backers": "Supporters",
         "Tab Folder": "Dossier des onglets",
         "Cache Folder": "Dossier du cache",
         "Backup": "Sauvegarde",
@@ -631,27 +631,27 @@
         "Less": "Moins",
         "OpenCollective Sponsor": "Sponsor OpenCollective",
         "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",
         "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",
-        "You": "You",
+        "You": "Toi",
         "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",
-        "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",
         "Bind Password": "Lier le mot de passe",
         "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",
         "Jellyfin SSO URL": "URL SSO 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",
-        "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",
         "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",
@@ -679,23 +679,23 @@
         "Jellyfin": "Jellyfin",
         "Petio": "Petio",
         "Overseerr": "Overseerr",
-        "FYI": "FYI",
-        "https://app.plex.tv/auth#?resetPassword": "https://app.plex.tv/auth#?resetPassword",
-        "Change Password on Plex Website": "Change Password on Plex Website",
+        "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",
         "IP": "IP",
         "Browser": "Navigateur",
-        "Expires": "Expires",
-        "Created": "Created",
+        "Expires": "Expire",
+        "Created": "Créé",
         "Version": "Version",
         "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",
         "Text Color": "Couleur de text",
         "Background Color": "Couleur d'arrière plan",
@@ -717,96 +717,96 @@
         "Colors & Themes": "Couleurs & Thèmes",
         "Options": "Options",
         "Login Page": "Page de connexion",
-        "Top Bar": "Top Bar",
+        "Top Bar": "Barre du haut",
         "Bookmark Settings": "Paramètres de marque-page",
         "HnL Settings": "Paramètres HnL",
         "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 :",
-        "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)",
-        "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",
         "Enabled": "Activé",
         "Internal URL": "URL interne",
         "External URL": "URL externe",
         "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",
-        "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",
         "Services": "Services",
-        "Import Services": "Import Services",
+        "Import Services": "Services Import",
         "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",
         "Emby Settings": "Paramètres Emby",
         "Plex Settings": "Paramètres Plex",
-        "Backend": "Backend",
-        "Templates": "Templates",
+        "Backend": "Back-end",
+        "Templates": "Modèles",
         "Test & Options": "Test & Options",
         "Sender Information": "Information de l'envoyeur",
         "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",
         "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",
-        "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",
         "Date Registered": "Date enregistrée",
         "Group": "Groupe",
@@ -814,46 +814,46 @@
         "Copy to Clipboard": "Copier dans le presse-papier",
         "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",
-        "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",
-        "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",
-        "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",
-        "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",
         ".": "."
     }
 }

Diff do ficheiro suprimidas por serem muito extensas
+ 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

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff