Browse Source

Merge pull request #1710 from causefx/v2-develop

V2 develop
causefx 4 years ago
parent
commit
6605513556
100 changed files with 4185 additions and 2065 deletions
  1. 2 2
      .gitignore
  2. 2 2
      README.md
  3. 584 167
      api/classes/organizr.class.php
  4. 1 1
      api/composer.json
  5. 14 15
      api/composer.lock
  6. 69 12
      api/config/default.php
  7. 0 1
      api/functions.php
  8. 1 1
      api/functions/auth-functions.php
  9. 16 0
      api/functions/config-functions.php
  10. 1 1
      api/functions/homepage-connect-functions.php
  11. 13 2
      api/functions/homepage-functions.php
  12. 37 1
      api/functions/normal-functions.php
  13. 65 11
      api/functions/option-functions.php
  14. 87 3
      api/functions/organizr-functions.php
  15. 4 4
      api/functions/sso-functions.php
  16. 4 4
      api/functions/token-functions.php
  17. 43 0
      api/functions/upgrade-functions.php
  18. 1 7
      api/homepage/calendar.php
  19. 1 7
      api/homepage/couchpotato.php
  20. 5 11
      api/homepage/deluge.php
  21. 14 13
      api/homepage/emby.php
  22. 2 7
      api/homepage/healthchecks.php
  23. 26 76
      api/homepage/html.php
  24. 1 7
      api/homepage/jackett.php
  25. 1 7
      api/homepage/jdownloader.php
  26. 1 7
      api/homepage/jellyfin.php
  27. 3 9
      api/homepage/lidarr.php
  28. 1 7
      api/homepage/monitorr.php
  29. 6 12
      api/homepage/netdata.php
  30. 1 7
      api/homepage/nzbget.php
  31. 1 7
      api/homepage/octoprint.php
  32. 3 9
      api/homepage/ombi.php
  33. 522 0
      api/homepage/overseerr.php
  34. 1 7
      api/homepage/pihole.php
  35. 121 102
      api/homepage/plex.php
  36. 1 7
      api/homepage/qbittorrent.php
  37. 3 9
      api/homepage/radarr.php
  38. 2 8
      api/homepage/rtorrent.php
  39. 1 7
      api/homepage/sabnzbd.php
  40. 1 7
      api/homepage/sickrage.php
  41. 4 10
      api/homepage/sonarr.php
  42. 1 7
      api/homepage/speedtest.php
  43. 2 8
      api/homepage/tautulli.php
  44. 2 8
      api/homepage/trakt.php
  45. 1 7
      api/homepage/transmission.php
  46. 100 121
      api/homepage/unifi.php
  47. 213 0
      api/homepage/utorrent.php
  48. 1 7
      api/homepage/weather.php
  49. 149 0
      api/pages/error.php
  50. 2 2
      api/pages/settings-image-manager.php
  51. 2 2
      api/pages/settings-settings-logs.php
  52. 7 7
      api/pages/settings-tab-editor-categories.php
  53. 13 10
      api/pages/settings-tab-editor-tabs.php
  54. 132 46
      api/pages/settings-user-manage-groups.php
  55. 245 244
      api/pages/settings-user-manage-users.php
  56. 311 345
      api/pages/settings.php
  57. 5 5
      api/plugins/bookmark/plugin.php
  58. 28 7
      api/plugins/bookmark/settings.js
  59. 17 17
      api/plugins/chat/plugin.php
  60. 1 1
      api/plugins/healthChecks/settings.js
  61. 1 1
      api/plugins/invites/plugin.php
  62. 30 1
      api/v2/routes/config.php
  63. 46 0
      api/v2/routes/connectionTester.php
  64. 21 0
      api/v2/routes/database.php
  65. 84 1
      api/v2/routes/homepage.php
  66. 44 0
      api/v2/routes/organizr.php
  67. 4 3
      api/v2/routes/root.php
  68. 21 12
      api/v2/routes/token.php
  69. 5 5
      api/vendor/composer/InstalledVersions.php
  70. 18 4
      api/vendor/composer/autoload_classmap.php
  71. 0 1
      api/vendor/composer/autoload_files.php
  72. 18 5
      api/vendor/composer/autoload_static.php
  73. 17 14
      api/vendor/composer/installed.json
  74. 5 5
      api/vendor/composer/installed.php
  75. 48 0
      api/vendor/dibi/dibi/appveyor.yml
  76. 11 6
      api/vendor/dibi/dibi/composer.json
  77. 7 31
      api/vendor/dibi/dibi/examples/connecting-to-databases.php
  78. BIN
      api/vendor/dibi/dibi/examples/data/sample.s3db
  79. 4 1
      api/vendor/dibi/dibi/examples/database-reflection.php
  80. 4 1
      api/vendor/dibi/dibi/examples/dumping-sql-and-result-set.php
  81. 2 1
      api/vendor/dibi/dibi/examples/fetching-examples.php
  82. 4 1
      api/vendor/dibi/dibi/examples/importing-dump-from-file.php
  83. 4 1
      api/vendor/dibi/dibi/examples/query-language-and-conditions.php
  84. 4 1
      api/vendor/dibi/dibi/examples/query-language-basic-examples.php
  85. 2 1
      api/vendor/dibi/dibi/examples/result-set-data-types.php
  86. 2 4
      api/vendor/dibi/dibi/examples/tracy-and-exceptions.php
  87. 2 4
      api/vendor/dibi/dibi/examples/tracy.php
  88. 4 1
      api/vendor/dibi/dibi/examples/using-datetime.php
  89. 0 33
      api/vendor/dibi/dibi/examples/using-extension-methods.php
  90. 4 1
      api/vendor/dibi/dibi/examples/using-fluent-syntax.php
  91. 4 1
      api/vendor/dibi/dibi/examples/using-limit-and-offset.php
  92. 8 5
      api/vendor/dibi/dibi/examples/using-logger.php
  93. 0 45
      api/vendor/dibi/dibi/examples/using-profiler.php
  94. 4 1
      api/vendor/dibi/dibi/examples/using-substitutions.php
  95. 4 1
      api/vendor/dibi/dibi/examples/using-transactions.php
  96. 588 65
      api/vendor/dibi/dibi/readme.md
  97. 12 11
      api/vendor/dibi/dibi/src/Dibi/Bridges/Nette/DibiExtension22.php
  98. 198 262
      api/vendor/dibi/dibi/src/Dibi/Connection.php
  99. 45 71
      api/vendor/dibi/dibi/src/Dibi/DataSource.php
  100. 8 43
      api/vendor/dibi/dibi/src/Dibi/DateTime.php

+ 2 - 2
.gitignore

@@ -57,7 +57,6 @@ php_errors.log
 # Backup Files
 .pydio_id
 
-.idea/*
 # =========================
 # Organizr files
 # =========================
@@ -96,7 +95,6 @@ logs/
 debug.php
 OrganizrV2/*
 orgv2/*
-test.php
 test/*
 organizrLoginLog.json
 organizrLog.json
@@ -121,6 +119,8 @@ plugins/plugin_files/*
 !plugins/plugin_files/index.html
 plugins/images/userTabs/*
 !plugins/images/userTabs/index.html
+plugins/images/faviconCustom/*
+!plugins/images/faviconCustom/placeFavIconsHere.txt
 api/v2/routes/custom/*
 !api/v2/routes/custom/index.html
 # =========================

+ 2 - 2
README.md

@@ -60,7 +60,7 @@ Do you have quite a bit of services running on your computer or server? Do you h
 
 ![OrganizrFeatReq](https://user-images.githubusercontent.com/16184466/53614286-a9b73480-3b96-11e9-8495-4944b85b1313.png)
 
-[![Feature Requests](http://feathub.com/causefx/Organizr?format=svg)](http://feathub.com/causefx/Organizr)
+[![Feature Requests]](https://vote.organizr.app/)
 
 ![OrganizrDocker](https://user-images.githubusercontent.com/16184466/53667702-fcdcc600-3c2e-11e9-8828-860e531e8096.png)
 
@@ -105,4 +105,4 @@ The optional parameters and GID and UID are described in the [readme](https://gi
 
 ### This project is supported by
 
-<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="200px"></img>
+<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="200px"></img>

File diff suppressed because it is too large
+ 584 - 167
api/classes/organizr.class.php


+ 1 - 1
api/composer.json

@@ -1,6 +1,6 @@
 {
   "require": {
-    "dibi/dibi": "^3.2",
+    "dibi/dibi": "^4.2",
     "lcobucci/jwt": "3.3.1",
     "composer/semver": "^1.4",
     "phpmailer/phpmailer": "^6.2",

+ 14 - 15
api/composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "a9cfe24cec195b73c109643bc90dc450",
+    "content-hash": "db7f63e80bb05dd9f7e63091879075a6",
     "packages": [
         {
             "name": "adldap2/adldap2",
@@ -208,47 +208,46 @@
         },
         {
             "name": "dibi/dibi",
-            "version": "v3.2.4",
+            "version": "v4.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/dg/dibi.git",
-                "reference": "d571460a6f8fa1334a04f7aaa1551bb0f12c2266"
+                "reference": "73e16eb1a322599e8cdf350adcfdbc15eaf16577"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/dg/dibi/zipball/d571460a6f8fa1334a04f7aaa1551bb0f12c2266",
-                "reference": "d571460a6f8fa1334a04f7aaa1551bb0f12c2266",
+                "url": "https://api.github.com/repos/dg/dibi/zipball/73e16eb1a322599e8cdf350adcfdbc15eaf16577",
+                "reference": "73e16eb1a322599e8cdf350adcfdbc15eaf16577",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.4.4"
+                "php": ">=7.2"
             },
             "replace": {
                 "dg/dibi": "*"
             },
             "require-dev": {
-                "nette/tester": "~1.7",
+                "nette/di": "^3.0",
+                "nette/tester": "~2.0",
+                "phpstan/phpstan": "^0.12",
                 "tracy/tracy": "~2.2"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.2-dev"
+                    "dev-master": "4.2-dev"
                 }
             },
             "autoload": {
                 "classmap": [
                     "src/"
-                ],
-                "files": [
-                    "src/loader.php"
                 ]
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
                 "BSD-3-Clause",
-                "GPL-2.0",
-                "GPL-3.0"
+                "GPL-2.0-only",
+                "GPL-3.0-only"
             ],
             "authors": [
                 {
@@ -273,9 +272,9 @@
             ],
             "support": {
                 "issues": "https://github.com/dg/dibi/issues",
-                "source": "https://github.com/dg/dibi/tree/v3.2.4"
+                "source": "https://github.com/dg/dibi/tree/v4.2.3"
             },
-            "time": "2020-03-26T03:05:01+00:00"
+            "time": "2021-07-23T08:49:27+00:00"
         },
         {
             "name": "doctrine/annotations",

+ 69 - 12
api/config/default.php

@@ -1,5 +1,5 @@
 <?php
-return array(
+return [
 	'branch' => 'v2-master',
 	'authType' => 'internal',
 	'authBackend' => '',
@@ -65,6 +65,8 @@ return array(
 	'overseerrToken' => '',
 	'overseerrFallbackUser' => '',
 	'overseerrFallbackPassword' => '',
+	'overseerrDisableCertCheck' => false,
+	'overseerrUseCustomCertificate' => false,
 	'petioURL' => '',
 	'petioToken' => '',
 	'petioFallbackUser' => '',
@@ -126,6 +128,8 @@ return array(
 	'nzbgetSocksAuth' => '999',
 	'nzbgetCombine' => false,
 	'nzbgetRefresh' => '60000',
+	'nzbgetDisableCertCheck' => false,
+	'nzbgetUseCustomCertificate' => true,
 	'transmissionURL' => '',
 	'transmissionUsername' => '',
 	'transmissionPassword' => '',
@@ -158,6 +162,17 @@ return array(
 	'qBittorrentSocksEnabled' => false,
 	'qBittorrentSocksAuth' => '999',
 	'rTorrentURL' => '',
+	'uTorrentCombine' => false,
+	'uTorrentCookie' => '',
+	'uTorrentDisableCertCheck' => false,
+	'uTorrentHideCompleted' => false,
+	'uTorrentHideSeeding' => false,
+	'uTorrentPassword' => '',
+	'uTorrentRefresh' => '60000',
+	'uTorrentToken' => '',
+	'uTorrentURL' => '',
+	'uTorrentUseCustomCertificate' => false,
+	'uTorrentUsername' => '',
 	'rTorrentURLOverride' => '',
 	'rTorrentUsername' => '',
 	'rTorrentPassword' => '',
@@ -180,10 +195,30 @@ return array(
 	'homepageCalendarEnabled' => false,
 	'homepageCalendarAuth' => '4',
 	'calendariCal' => '',
-	'homepageCustomHTMLoneEnabled' => false,
-	'homepageCustomHTMLoneAuth' => '1',
-	'homepageCustomHTMLtwoEnabled' => false,
-	'homepageCustomHTMLtwoAuth' => '1',
+	'homepageCustomHTML01Enabled' => false,
+	'homepageCustomHTML01Auth' => '1',
+	'customHTML01' => '',
+	'homepageCustomHTML02Enabled' => false,
+	'homepageCustomHTML02Auth' => '1',
+	'customHTML02' => '',
+	'homepageCustomHTML03Enabled' => false,
+	'homepageCustomHTML03Auth' => '1',
+	'customHTML03' => '',
+	'homepageCustomHTML04Enabled' => false,
+	'homepageCustomHTML04Auth' => '1',
+	'customHTML04' => '',
+	'homepageCustomHTML05Enabled' => false,
+	'homepageCustomHTML05Auth' => '1',
+	'customHTML05' => '',
+	'homepageCustomHTML06Enabled' => false,
+	'homepageCustomHTML06Auth' => '1',
+	'customHTML06' => '',
+	'homepageCustomHTML07Enabled' => false,
+	'homepageCustomHTML07Auth' => '1',
+	'customHTML07' => '',
+	'homepageCustomHTML08Enabled' => false,
+	'homepageCustomHTML08Auth' => '1',
+	'customHTML08' => '',
 	'homepageDelugeEnabled' => false,
 	'homepageDelugeAuth' => '1',
 	'homepageJdownloaderEnabled' => false,
@@ -214,6 +249,8 @@ return array(
 	'homepageqBittorrentAuth' => '1',
 	'homepagerTorrentEnabled' => false,
 	'homepagerTorrentAuth' => '1',
+	'homepageuTorrentAuth' => '1',
+	'homepageuTorrentEnabled' => false,
 	'homepageNzbgetEnabled' => false,
 	'homepageNzbgetAuth' => '1',
 	'homepagePlexEnabled' => false,
@@ -225,10 +262,16 @@ return array(
 	'homepageOmbiEnabled' => false,
 	'homepageOmbiAuth' => '1',
 	'homepageOmbiRequestAuth' => '1',
+	'homepageOverseerrEnabled' => false,
+	'homepageOverseerrAuth' => '1',
+	'homepageOverseerrRequestAuth' => '1',
 	'homepageJellyfinInstead' => false,
 	'ombiLimitUser' => false,
 	'ombiRefresh' => '600000',
 	'ombiTvDefault' => 'all',
+	'overseerrLimitUser' => false,
+	'overseerrRefresh' => '600000',
+	'overseerrTvDefault' => 'all',
 	'homepageWeatherAndAirEnabled' => false,
 	'homepageWeatherAndAirAuth' => '1',
 	'homepageWeatherAndAirRefresh' => '3600000',
@@ -248,8 +291,8 @@ return array(
 	'homepageHealthChecksShowTags' => false,
 	'healthChecksDisableCertCheck' => false,
 	'healthChecksUseCustomCertificate' => false,
-	'homepageOrdercustomhtml' => '1',
-	'homepageOrdercustomhtmlTwo' => '2',
+	'homepageOrdercustomhtml01' => '1',
+	'homepageOrdercustomhtml02' => '2',
 	'homepageOrdertransmission' => '3',
 	'homepageOrderqBittorrent' => '4',
 	'homepageOrderdeluge' => '5',
@@ -279,6 +322,14 @@ return array(
 	'homepageOrderjellyfinnowplaying' => '29',
 	'homepageOrderjellyfinrecent' => '30',
 	'homepageOrderJackett' => '31',
+	'homepageOrdercustomhtml03' => '32',
+	'homepageOrdercustomhtml04' => '33',
+	'homepageOrdercustomhtml05' => '34',
+	'homepageOrdercustomhtml06' => '35',
+	'homepageOrdercustomhtml07' => '36',
+	'homepageOrdercustomhtml08' => '37',
+	'homepageOrderuTorrent' => '38',
+	'homepageOrderoverseerr' => '39',
 	'homepageShowStreamNames' => false,
 	'homepageShowStreamNamesAuth' => '1',
 	'homepageUseCustomStreamNames' => false,
@@ -317,8 +368,6 @@ return array(
 	'calendarLocale' => 'en',
 	'customCss' => '',
 	'customThemeCss' => '',
-	'customHTMLone' => '',
-	'customHTMLtwo' => '',
 	'mediaSearch' => false,
 	'mediaSearchType' => '',
 	'mediaSearchAuth' => '1',
@@ -357,8 +406,10 @@ return array(
 	'debugAreaAuth' => '1',
 	'commit' => 'n/a',
 	'ombiLimit' => '50',
+	'overseerrLimit' => '50',
 	'localIPFrom' => '',
 	'localIPTo' => '',
+	'localIPList' => '',
 	'sandbox' => 'allow-presentation,allow-forms,allow-same-origin,allow-pointer-lock,allow-scripts,allow-popups,allow-modals,allow-top-navigation,allow-downloads,allow-orientation-lock,allow-popups-to-escape-sandbox,allow-top-navigation-by-user-activation',
 	'description' => 'Organizr - Accept no others',
 	'debugErrors' => false,
@@ -371,6 +422,11 @@ return array(
 	'ombiDefaultFilterApproved' => true,
 	'ombiDefaultFilterUnapproved' => true,
 	'ombiDefaultFilterDenied' => true,
+	'overseerrDefaultFilterAvailable' => true,
+	'overseerrDefaultFilterUnavailable' => true,
+	'overseerrDefaultFilterApproved' => true,
+	'overseerrDefaultFilterUnapproved' => true,
+	'overseerrDefaultFilterDenied' => true,
 	'selfSignedCert' => '',
 	'homepagePlexRecentlyAddedMethod' => 'legacy',
 	'authProxyEnabled' => false,
@@ -528,7 +584,7 @@ return array(
 	'customForgotPassText' => '',
 	'disableRecoverPass' => false,
 	'expandCategoriesByDefault' => false,
-	'ignoredNewsIds' => array(),
+	'ignoredNewsIds' => [],
 	'homepageTraktEnabled' => false,
 	'homepageTraktAuth' => '1',
 	'calendarStartTrakt' => '14',
@@ -542,5 +598,6 @@ return array(
 	'autoExpandNavBar' => true,
 	'defaultSettingsTab' => '5',
 	'blacklisted' => '',
-	'blacklistedMessage' => 'You have been blacklisted from this site.'
-);
+	'blacklistedMessage' => 'You have been blacklisted from this site.',
+	'defaultRequestService' => 'ombi'
+];

+ 0 - 1
api/functions.php

@@ -1,5 +1,4 @@
 <?php
-//error_reporting(E_ALL);
 // Set UTC timeone
 date_default_timezone_set("UTC");
 // Autoload frameworks

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

@@ -482,7 +482,7 @@ trait AuthFunctions
 				if (is_array($json)) {
 					foreach ($json as $key => $value) { // Scan for this user
 						if (isset($value['ConnectUserName']) && isset($value['ConnectLinkType'])) { // Qualify as connect account
-							if (strtolower($value['ConnectUserName']) == $username || strtolower($value['Name']) == $username) {
+							if (strtolower($value['ConnectUserName']) == strtolower($username) || strtolower($value['Name']) == strtolower($username)) {
 								$connectUserName = $value['ConnectUserName'];
 								$this->writeLog('success', 'Emby Connect Auth Function - Found User', $username);
 								break;

+ 16 - 0
api/functions/config-functions.php

@@ -15,6 +15,22 @@ trait ConfigFunctions
 			$this->setAPIResponse('error', $item . ' is not defined or is blank', 404);
 			return false;
 		}
+	}
+	
+	public function getConfigItems()
+	{
+		$configItems = $this->config;
+		/*
+		foreach ($configItems as $configItem => $configItemValue) {
+			// should we keep this to filter more items?
+			if ($configItem == 'organizrHash') {
+				$configItems[$configItem] = '***Secure***';
+			}
+		}
+		*/
+		$configItems['organizrHash'] = '***Secure***';
+		$this->setAPIResponse('success', null, 200, $configItems);
+		return $configItems;
 		
 	}
 }

+ 1 - 1
api/functions/homepage-connect-functions.php

@@ -59,7 +59,7 @@ trait HomepageConnectFunctions
 				$headers = array(
 					"Accept" => "application/json",
 					"Content-Type" => "application/json",
-					"Apikey" => $GLOBALS['ombiToken']
+					"Apikey" => $this->config['ombiToken']
 				);
 				$options = ($this->localURL($url)) ? array('verify' => false) : array();
 				switch ($type) {

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

@@ -2,6 +2,17 @@
 
 trait HomepageFunctions
 {
+	public function homepageCheckKeyPermissions($key, $permissions)
+	{
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
 	public function getHomepageSettingsList()
 	{
 		$methods = get_class_methods($this);
@@ -74,7 +85,7 @@ trait HomepageFunctions
 					if ($settingsType == 'string') {
 						if (empty($this->config[$setting])) {
 							if ($api) {
-								$this->setAPIResponse('error', $setting . 'was not supplied', 422);
+								$this->setAPIResponse('error', $setting . ' was not supplied', 422);
 							}
 							return false;
 						}
@@ -82,7 +93,7 @@ trait HomepageFunctions
 						foreach ($setting as $item) {
 							if (empty($this->config[$item])) {
 								if ($api) {
-									$this->setAPIResponse('error', $item . 'was not supplied', 422);
+									$this->setAPIResponse('error', $item . ' was not supplied', 422);
 								}
 								return false;
 							}

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

@@ -488,6 +488,27 @@ trait NormalFunctions
 		}
 	}
 	
+	public function convertIPToRange($ip)
+	{
+		if (strpos($ip, '/') !== false) {
+			$explodeIP = explode('/', $ip);
+			$prefix = $explodeIP[1];
+			$start_ip = $explodeIP[0];
+			$ip_count = 1 << (32 - $prefix);
+			$start_ip_long = long2ip(ip2long($start_ip));
+			$last_ip_long = long2ip(ip2long($start_ip) + $ip_count - 1);
+		} elseif (substr_count($ip, '.') == 3) {
+			$start_ip_long = long2ip(ip2long($ip));
+			$last_ip_long = long2ip(ip2long($ip));
+		} else {
+			return false;
+		}
+		return [
+			'from' => $start_ip_long,
+			'to' => $last_ip_long
+		];
+	}
+	
 	public function localIPRanges()
 	{
 		$mainArray = array(
@@ -508,7 +529,20 @@ trait NormalFunctions
 				'to' => '127.255.255.255'
 			),
 		);
-		$override = false;
+		if (isset($this->config['localIPList'])) {
+			if ($this->config['localIPList'] !== '') {
+				$ipListing = explode(',', $this->config['localIPList']);
+				if (count($ipListing) > 0) {
+					foreach ($ipListing as $ip) {
+						$ipInfo = $this->convertIPToRange($ip);
+						if ($ipInfo) {
+							array_push($mainArray, $ipInfo);
+						}
+					}
+				}
+			}
+		}
+		/*
 		if ($this->config['localIPFrom']) {
 			$from = trim($this->config['localIPFrom']);
 			$override = true;
@@ -516,6 +550,7 @@ trait NormalFunctions
 		if ($this->config['localIPTo']) {
 			$to = trim($this->config['localIPTo']);
 		}
+		
 		if ($override) {
 			$newArray = array(
 				'from' => $from,
@@ -523,6 +558,7 @@ trait NormalFunctions
 			);
 			array_push($mainArray, $newArray);
 		}
+		*/
 		return $mainArray;
 	}
 	

+ 65 - 11
api/functions/option-functions.php

@@ -20,7 +20,7 @@ trait OptionsFunction
 		$type = strtolower(str_replace('-', '', $type));
 		$setting = [
 			'name' => $name,
-			'value' => $this->config[$name]
+			'value' => $this->config[$name] ?? ''
 		];
 		switch ($type) {
 			case 'enable':
@@ -126,6 +126,30 @@ trait OptionsFunction
 					'settings' => '{tags: true, theme: "default password-alt", selectOnClose: true, closeOnSelect: true}',
 				];
 				break;
+			case 'notice':
+				$settingMerge = [
+					'type' => 'html',
+					'override' => 12,
+					'label' => '',
+					'html' => '
+						<div class="row">
+							<div class="col-lg-12">
+								<div class="panel panel-' . ($extras['notice'] ?? 'info') . '">
+									<div class="panel-heading">
+										<span lang="en">' . ($extras['title'] ?? 'Attention') . '</span>
+									</div>
+									<div class="panel-wrapper collapse in" aria-expanded="true">
+										<div class="panel-body">
+											<span lang="en">' . ($extras['body'] ?? '') . '</span>
+											<span>' . ($extras['bodyHTML'] ?? '') . '</span>
+										</div>
+									</div>
+								</div>
+							</div>
+						</div>
+						'
+				];
+				break;
 			case 'socks':
 				$settingMerge = [
 					'type' => 'html',
@@ -257,7 +281,7 @@ trait OptionsFunction
 					'type' => 'html',
 					'override' => 12,
 					'label' => 'Custom Code',
-					'html' => '<button type="button" class="hidden save' . $name . 'Textarea btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="' . $name . 'Editor" style="height:300px">' . htmlentities($this->config[$name]) . '</div>'
+					'html' => '<div id="' . $name . 'Editor" style="height:300px">' . htmlentities($this->config[$name]) . '</div>'
 				];
 				break;
 			// CALENDAR ITEMS
@@ -599,22 +623,44 @@ trait OptionsFunction
 		);
 	}
 	
-	public function ombiTvOptions()
+	public function requestTvOptions($includeUserOption = false)
 	{
-		return array(
-			array(
+		$options = [
+			[
 				'name' => 'All Seasons',
 				'value' => 'all'
-			),
-			array(
+			],
+			[
 				'name' => 'First Season Only',
 				'value' => 'first'
-			),
-			array(
+			],
+			[
 				'name' => 'Last Season Only',
 				'value' => 'last'
-			),
-		);
+			],
+		];
+		$userOption = [
+			'name' => 'Let User Select',
+			'value' => 'user'
+		];
+		if ($includeUserOption) {
+			array_push($options, $userOption);
+		}
+		return $options;
+	}
+	
+	public function requestServiceOptions()
+	{
+		return [
+			[
+				'name' => 'Ombi',
+				'value' => 'ombi'
+			],
+			[
+				'name' => 'Overseerr',
+				'value' => 'overseerr'
+			]
+		];
 	}
 	
 	public function limitOptions()
@@ -885,6 +931,14 @@ trait OptionsFunction
 				'name' => '6:00p',
 				'value' => 'h:mmt'
 			),
+			array(
+				'name' => '6pm',
+				'value' => 'h(:mm)a'
+			),
+			array(
+				'name' => '6:00pm',
+				'value' => 'h:mma'
+			),
 			array(
 				'name' => '6:00',
 				'value' => 'h:mm'

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

@@ -2,6 +2,36 @@
 
 trait OrganizrFunctions
 {
+	public function docs($path): string
+	{
+		return 'https://organizr.gitbook.io/organizr/' . $path;
+	}
+	
+	public function loadResources($files = [], $rootPath = '')
+	{
+		$scripts = '';
+		if (count($files) > 0) {
+			foreach ($files as $file) {
+				if (strtolower(pathinfo($file, PATHINFO_EXTENSION)) == 'js') {
+					$scripts .= $this->loadJavaResource($file, $rootPath);
+				} elseif (strtolower(pathinfo($file, PATHINFO_EXTENSION)) == 'css') {
+					$scripts .= $this->loadStyleResource($file, $rootPath);
+				}
+			}
+		}
+		return $scripts;
+	}
+	
+	public function loadJavaResource($file = '', $rootPath = '')
+	{
+		return ($file !== '') ? '<script src="' . $rootPath . $file . '?v=' . trim($this->fileHash) . '"></script>' . "\n" : '';
+	}
+	
+	public function loadStyleResource($file = '', $rootPath = '')
+	{
+		return ($file !== '') ? '<link href="' . $rootPath . $file . '?v=' . trim($this->fileHash) . '" rel="stylesheet">' . "\n" : '';
+	}
+	
 	public function loadDefaultJavascriptFiles()
 	{
 		$javaFiles = [
@@ -20,14 +50,14 @@ trait OrganizrFunctions
 		];
 		$scripts = '';
 		foreach ($javaFiles as $file) {
-			$scripts .= '<script src="' . $file . '?v=' . $this->fileHash . '"></script>' . "\n";
+			$scripts .= '<script src="' . $file . '?v=' . trim($this->fileHash) . '"></script>' . "\n";
 		}
 		return $scripts;
 	}
 	
 	public function loadJavascriptFile($file)
 	{
-		return '<script>loadJavascript("' . $file . '?v=' . $this->fileHash . '");' . "</script>\n";
+		return '<script>loadJavascript("' . $file . '?v=' . trim($this->fileHash) . '");' . "</script>\n";
 	}
 	
 	public function embyJoinAPI($array)
@@ -489,7 +519,7 @@ trait OrganizrFunctions
 			ob_end_flush(); // Send the output to the browser
 			die();
 		} else {
-			die("Invalid Request");
+			die($this->showHTML('Invalid Request', 'No image returned'));
 		}
 	}
 	
@@ -734,4 +764,58 @@ trait OrganizrFunctions
 		}
 		return $options;
 	}
+	
+	public function showHTML(string $title = 'Organizr Alert', string $notice = '')
+	{
+		return
+			'<!DOCTYPE html>
+			<html lang="en">
+			<head>
+				<link rel="stylesheet" href="' . $this->getServerPath() . '/css/mvp.css">
+				<meta charset="utf-8">
+				<meta name="description" content="Trakt OAuth">
+				<meta name="viewport" content="width=device-width, initial-scale=1.0">
+				<title>' . $title . '</title>
+			</head>
+
+			<body>
+				<main>
+					<section>
+						<aside>
+							<h3>' . $title . '</h3>
+							<p>' . $notice . '</p>
+						</aside>
+					</section>
+				</main>
+			</body>
+			</html>';
+	}
+	
+	public function buildSettingsMenus($menuItems, $menuName)
+	{
+		$selectMenuItems = '';
+		$unorderedListMenuItems = '';
+		$menuNameLower = strtolower(str_replace(' ', '-', $menuName));
+		foreach ($menuItems as $menuItem) {
+			$anchorShort = str_replace('-anchor', '', $menuItem['anchor']);
+			$active = ($menuItem['active']) ? 'active' : '';
+			$apiPage = ($menuItem['api']) ? 'loadSettingsPage2(\'' . $menuItem['api'] . '\',\'#' . $anchorShort . '\',\'' . $menuItem['name'] . '\');' : '';
+			$onClick = (isset($menuItem['onclick'])) ? $menuItem['onclick'] : '';
+			$selectMenuItems .= '<option value="#' . $menuItem['anchor'] . '" lang="en">' . $menuItem['name'] . '</option>';
+			$unorderedListMenuItems .= '
+				<li onclick="changeSettingsMenu(\'Settings::' . $menuName . '::' . $menuItem['name'] . '\'); ' . $apiPage . $onClick . '" role="presentation" class="' . $active . '">
+					<a id="' . $menuItem['anchor'] . '" href="#' . $anchorShort . '" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="true">
+						<span lang="en">' . $menuItem['name'] . '</span>
+					</a>
+			</li>';
+		}
+		$selectMenu = '<select class="form-control settings-dropdown-box ' . $menuNameLower . '-menu w-100 visible-xs">' . $selectMenuItems . '</select>';
+		$unorderedListMenu = '<ul class="nav customtab2 nav-tabs nav-non-mobile hidden-xs" data-dropdown="' . $menuNameLower . '-menu" role="tablist">' . $unorderedListMenuItems . '</ul>';
+		return $selectMenu . $unorderedListMenu;
+	}
+	
+	public function isJSON($string)
+	{
+		return is_string($string) && is_array(json_decode($string, true)) && (json_last_error() == JSON_ERROR_NONE);
+	}
 }

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

@@ -5,9 +5,9 @@ trait SSOFunctions
 	public function ssoCookies()
 	{
 		$cookies = array(
-			'myPlexAccessToken' => isset($_COOKIE['mpt']) ? $_COOKIE['mpt'] : false,
-			'id_token' => isset($_COOKIE['Auth']) ? $_COOKIE['Auth'] : false,
-			'jellyfin_credentials' => isset($_COOKIE['jellyfin_credentials']) ? $_COOKIE['jellyfin_credentials'] : false,
+			'myPlexAccessToken' => $_COOKIE['mpt'] ?? false,
+			'id_token' => $_COOKIE['Auth'] ?? false,
+			'jellyfin_credentials' => $_COOKIE['jellyfin_credentials'] ?? false,
 		);
 		// Jellyfin cookie
 		foreach (array_keys($_COOKIE) as $k => $v) {
@@ -164,7 +164,7 @@ trait SSOFunctions
 					$headers = array(
 						"Accept" => "application/json",
 						"Content-Type" => "application/x-www-form-urlencoded",
-						"User-Agent" => isset($_SERVER ['HTTP_USER_AGENT']) ? $_SERVER ['HTTP_USER_AGENT'] : null,
+						"User-Agent" => $_SERVER ['HTTP_USER_AGENT'] ?? null,
 						"X-Forwarded-For" => $this->userIP()
 					);
 					$data = array(

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

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

+ 43 - 0
api/functions/upgrade-functions.php

@@ -10,6 +10,8 @@ trait UpgradeFunctions
 				$this->upgradeHomepageTabURL();
 			case '2.1.400':
 				$this->removeOldPluginDirectoriesAndFiles();
+			case '2.1.525':
+				$this->removeOldCustomHTML();
 			default:
 				$this->setAPIResponse('success', 'Ran update function for version: ' . $version, 200);
 				return true;
@@ -77,4 +79,45 @@ trait UpgradeFunctions
 		}
 		return true;
 	}
+	
+	public function checkForConfigKeyAddToArray($keys)
+	{
+		$updateItems = [];
+		foreach ($keys as $new => $old) {
+			if (isset($this->config[$old])) {
+				if ($this->config[$old] !== '') {
+					$updateItemsNew = [$new => $this->config[$old]];
+					$updateItems = array_merge($updateItems, $updateItemsNew);
+				}
+			}
+		}
+		return $updateItems;
+	}
+	
+	public function removeOldCustomHTML()
+	{
+		$backup = $this->backupOrganizr();
+		if ($backup) {
+			$keys = [
+				'homepageCustomHTML01Enabled' => 'homepageCustomHTMLoneEnabled',
+				'homepageCustomHTML01Auth' => 'homepageCustomHTMLoneAuth',
+				'customHTML01' => 'customHTMLone',
+				'homepageCustomHTML02Enabled' => 'homepageCustomHTMLtwoEnabled',
+				'homepageCustomHTML02Auth' => 'homepageCustomHTMLtwoAuth',
+				'customHTML02' => 'customHTMLtwo',
+			];
+			$updateItems = $this->checkForConfigKeyAddToArray($keys);
+			$updateComplete = false;
+			if (!empty($updateItems)) {
+				$updateComplete = $this->updateConfig($updateItems);
+			}
+			if ($updateComplete) {
+				$removeConfigItems = $this->removeConfigItem(['homepagCustomHTMLoneAuth', 'homepagCustomHTMLoneEnabled', 'homepagCustomHTMLtwoAuth', 'homepagCustomHTMLtwoEnabled', 'homepageOrdercustomhtml', 'homepageOrdercustomhtmlTwo', 'homepageCustomHTMLoneEnabled', 'homepageCustomHTMLoneAuth', 'customHTMLone', 'homepageCustomHTMLtwoEnabled', 'homepageCustomHTMLtwoAuth', 'customHTMLtwo']);
+				if ($removeConfigItems) {
+					return true;
+				}
+			}
+		}
+		return false;
+	}
 }

+ 1 - 7
api/homepage/calendar.php

@@ -52,13 +52,7 @@ trait CalendarHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrdercalendar()

+ 1 - 7
api/homepage/couchpotato.php

@@ -59,13 +59,7 @@ trait CouchPotatoHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function getCouchPotatoCalendar()

+ 5 - 11
api/homepage/deluge.php

@@ -66,8 +66,7 @@ trait DelugeHomepageItem
 	
 	public function testConnectionDeluge()
 	{
-		if (empty($this->config['delugeURL'])) {
-			$this->setAPIResponse('error', 'Deluge URL is not defined', 422);
+		if (!$this->homepageItemPermissions($this->delugeHomepagePermissions('main'), true)) {
 			return false;
 		}
 		try {
@@ -99,13 +98,7 @@ trait DelugeHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderdeluge()
@@ -133,7 +126,8 @@ trait DelugeHomepageItem
 			return false;
 		}
 		try {
-			$deluge = new deluge($this->config['delugeURL'], $this->decrypt($this->config['delugePassword']));
+			$options = $this->requestOptions($this->config['delugeURL'], $this->config['delugeRefresh'], $this->config['delugeDisableCertCheck'], $this->config['delugeUseCustomCertificate'], ['organizr_cert' => $this->getCert(), 'custom_cert' => $this->getCustomCert()]);
+			$deluge = new deluge($this->config['delugeURL'], $this->decrypt($this->config['delugePassword']),$options);
 			$torrents = $deluge->getTorrents(null, 'comment, download_payload_rate, eta, hash, is_finished, is_seed, message, name, paused, progress, queue, state, total_size, upload_payload_rate');
 			foreach ($torrents as $key => $value) {
 				$tempStatus = $this->delugeStatus($value->queue, $value->state, $value->progress);
@@ -168,4 +162,4 @@ trait DelugeHomepageItem
 		}
 		return ($state) ? $state : $status;
 	}
-}
+}

+ 14 - 13
api/homepage/emby.php

@@ -57,12 +57,7 @@ trait EmbyHomepageItem
 	
 	public function testConnectionEmby()
 	{
-		if (empty($this->config['embyURL'])) {
-			$this->setAPIResponse('error', 'Emby URL is not defined', 422);
-			return false;
-		}
-		if (empty($this->config['embyToken'])) {
-			$this->setAPIResponse('error', 'Emby Token is not defined', 422);
+		if (!$this->homepageItemPermissions($this->embyHomepagePermissions('test'), true)) {
 			return false;
 		}
 		$url = $this->qualifyURL($this->config['embyURL']);
@@ -86,6 +81,18 @@ trait EmbyHomepageItem
 	public function embyHomepagePermissions($key = null)
 	{
 		$permissions = [
+			'test' => [
+				'enabled' => [
+					'homepageEmbyEnabled',
+				],
+				'auth' => [
+					'homepageEmbyAuth',
+				],
+				'not_empty' => [
+					'embyURL',
+					'embyToken'
+				]
+			],
 			'streams' => [
 				'enabled' => [
 					'homepageEmbyEnabled',
@@ -127,13 +134,7 @@ trait EmbyHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderembynowplaying()

+ 2 - 7
api/homepage/healthchecks.php

@@ -15,6 +15,7 @@ trait HealthChecksHomepageItem
 			return $homepageInformation;
 		}
 		$homepageSettings = [
+			'docs' => $this->docs('features/homepage/healthchecks-homepage-item'),
 			'debug' => true,
 			'settings' => [
 				'Enable' => [
@@ -54,13 +55,7 @@ trait HealthChecksHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderhealthchecks()

+ 26 - 76
api/homepage/html.php

@@ -2,40 +2,17 @@
 
 trait HTMLHomepageItem
 {
-	public function htmlOneSettingsArray($infoOnly = false)
+	public function customHtmlNumber()
 	{
-		$homepageInformation = [
-			'name' => 'CustomHTML-1',
-			'enabled' => strpos('personal,business', $this->config['license']) !== false,
-			'image' => 'plugins/images/tabs/custom1.png',
-			'category' => 'Custom',
-			'settingsArray' => __FUNCTION__
-		];
-		if ($infoOnly) {
-			return $homepageInformation;
-		}
-		$homepageSettings = [
-			'debug' => true,
-			'settings' => [
-				'Enable' => [
-					$this->settingsOption('enable', 'homepageCustomHTMLoneEnabled'),
-					$this->settingsOption('auth', 'homepageCustomHTMLoneAuth'),
-				],
-				'Code' => [
-					$this->settingsOption('pre-code-editor', 'customHTMLone'),
-					$this->settingsOption('code-editor', 'customHTMLone'),
-				]
-			]
-		];
-		return array_merge($homepageInformation, $homepageSettings);
+		return 8;
 	}
 	
-	public function htmlTwoSettingsArray($infoOnly = false)
+	public function customHtmlSettingsArray($infoOnly = false)
 	{
 		$homepageInformation = [
-			'name' => 'CustomHTML-2',
+			'name' => 'CustomHTML',
 			'enabled' => strpos('personal,business', $this->config['license']) !== false,
-			'image' => 'plugins/images/tabs/custom2.png',
+			'image' => 'plugins/images/tabs/HTML5.png',
 			'category' => 'Custom',
 			'settingsArray' => __FUNCTION__
 		];
@@ -44,72 +21,45 @@ trait HTMLHomepageItem
 		}
 		$homepageSettings = [
 			'debug' => true,
-			'settings' => [
-				'Enable' => [
-					$this->settingsOption('enable', 'homepageCustomHTMLtwoEnabled'),
-					$this->settingsOption('auth', 'homepageCustomHTMLtwoAuth'),
-				],
-				'Code' => [
-					$this->settingsOption('pre-code-editor', 'customHTMLtwo'),
-					$this->settingsOption('code-editor', 'customHTMLtwo'),
-				]
-			]
+			'settings' => []
 		];
+		for ($i = 1; $i <= $this->customHtmlNumber(); $i++) {
+			$i = sprintf('%02d', $i);
+			$homepageSettings['settings']['Custom HTML ' . $i] = array(
+				$this->settingsOption('enable', 'homepageCustomHTML' . $i . 'Enabled'),
+				$this->settingsOption('auth', 'homepageCustomHTML' . $i . 'Auth'),
+				$this->settingsOption('pre-code-editor', 'customHTML' . $i),
+				$this->settingsOption('code-editor', 'customHTML' . $i, ['label' => 'Custom HTML Code']),
+			);
+		}
 		return array_merge($homepageInformation, $homepageSettings);
 	}
 	
 	public function htmlHomepagePermissions($key = null)
 	{
-		$permissions = [
-			'one' => [
-				'enabled' => [
-					'homepageCustomHTMLoneEnabled'
-				],
-				'auth' => [
-					'homepageCustomHTMLoneAuth'
-				],
-				'not_empty' => [
-					'customHTMLone'
-				]
-			],
-			'two' => [
+		for ($i = 1; $i <= $this->customHtmlNumber(); $i++) {
+			$i = sprintf('%02d', $i);
+			$permissions[$i] = [
 				'enabled' => [
-					'homepageCustomHTMLtwoEnabled'
+					'homepageCustomHTML' . $i . 'Enabled'
 				],
 				'auth' => [
-					'homepageCustomHTMLtwoAuth'
+					'homepageCustomHTML' . $i . 'Auth'
 				],
 				'not_empty' => [
-					'customHTMLtwo'
+					'customHTML' . $i
 				]
-			]
-		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
-	}
-	
-	public function homepageOrdercustomhtml()
-	{
-		if ($this->homepageItemPermissions($this->htmlHomepagePermissions('one'))) {
-			return '
-				<div id="' . __FUNCTION__ . '">
-					' . $this->config['customHTMLone'] . '
-				</div>
-				';
+			];
 		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
-	public function homepageOrdercustomhtmlTwo()
+	public function homepageOrdercustomhtml($key = '01')
 	{
-		if ($this->homepageItemPermissions($this->htmlHomepagePermissions('two'))) {
+		if ($this->homepageItemPermissions($this->htmlHomepagePermissions($key))) {
 			return '
 				<div id="' . __FUNCTION__ . '">
-					' . $this->config['customHTMLtwo'] . '
+					' . $this->config['customHTML' . $key] . '
 				</div>
 				';
 		}

+ 1 - 7
api/homepage/jackett.php

@@ -51,13 +51,7 @@ trait JackettHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderJackett()

+ 1 - 7
api/homepage/jdownloader.php

@@ -100,13 +100,7 @@ trait JDownloaderHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderjdownloader()

+ 1 - 7
api/homepage/jellyfin.php

@@ -133,13 +133,7 @@ trait JellyfinHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderjellyfinnowplaying()

+ 3 - 9
api/homepage/lidarr.php

@@ -130,13 +130,7 @@ trait LidarrHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function getLidarrQueue()
@@ -173,8 +167,8 @@ trait LidarrHomepageItem
 	
 	public function getLidarrCalendar($startDate = null, $endDate = null)
 	{
-		$startDate = ($startDate) ?? $_GET['start'];
-		$endDate = ($endDate) ?? $_GET['end'];
+		$startDate = ($startDate) ?? $_GET['start'] ?? date('Y-m-d', strtotime('-' . $this->config['calendarStart'] . ' days'));
+		$endDate = ($endDate) ?? $_GET['end'] ?? date('Y-m-d', strtotime('+' . $this->config['calendarEnd'] . ' days'));
 		if (!$this->homepageItemPermissions($this->lidarrHomepagePermissions('calendar'), true)) {
 			return false;
 		}

+ 1 - 7
api/homepage/monitorr.php

@@ -53,13 +53,7 @@ trait MonitorrHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderMonitorr()

+ 6 - 12
api/homepage/netdata.php

@@ -19,8 +19,8 @@ trait NetDataHomepageItem
 						break;
 					case 'disk-write':
 						$data = $this->disk('out', $url);
-						$data['value'] = abs($data['value']);
-						$data['percent'] = abs($data['percent']);
+						$data['value'] = (isset($data['value'])) ? abs($data['value']) : 0;
+						$data['percent'] = (isset($data['percent'])) ? abs($data['percent']) : 0;
 						break;
 					case 'cpu':
 						$data = $this->cpu($url);
@@ -30,8 +30,8 @@ trait NetDataHomepageItem
 						break;
 					case 'net-out':
 						$data = $this->net('sent', $url);
-						$data['value'] = abs($data['value']);
-						$data['percent'] = abs($data['percent']);
+						$data['value'] = (isset($data['value'])) ? abs($data['value']) : 0;
+						$data['percent'] = (isset($data['percent'])) ? abs($data['percent']) : 0;
 						break;
 					case 'ram-used':
 						$data = $this->ram($url);
@@ -79,7 +79,7 @@ trait NetDataHomepageItem
 				array_push($api['data'], $data);
 			}
 		}
-		$api = isset($api) ? $api : false;
+		$api = $api ?? false;
 		$this->setAPIResponse('success', null, 200, $api);
 		return $api;
 		
@@ -209,13 +209,7 @@ trait NetDataHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderNetdata()

+ 1 - 7
api/homepage/nzbget.php

@@ -92,13 +92,7 @@ trait NZBGetHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrdernzbget()

+ 1 - 7
api/homepage/octoprint.php

@@ -52,13 +52,7 @@ trait OctoPrintHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderOctoprint()

+ 3 - 9
api/homepage/ombi.php

@@ -32,7 +32,7 @@ trait OmbiHomepageItem
 				],
 				'Misc Options' => [
 					$this->settingsOption('auth', 'homepageOmbiRequestAuth', ['label' => 'Minimum Group to Request']),
-					$this->settingsOption('select', 'ombiTvDefault', ['label' => 'TV Show Default Request', 'options' => $this->ombiTvOptions()]),
+					$this->settingsOption('select', 'ombiTvDefault', ['label' => 'TV Show Default Request', 'options' => $this->requestTvOptions()]),
 					$this->settingsOption('switch', 'ombiLimitUser', ['label' => 'Limit to User']),
 					$this->settingsOption('limit', 'ombiLimit'),
 					$this->settingsOption('refresh', 'ombiRefresh'),
@@ -100,13 +100,7 @@ trait OmbiHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderombi()
@@ -117,7 +111,7 @@ trait OmbiHomepageItem
 					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Requests...</h2></div>
 					<script>
 						// Ombi Requests
-						homepageRequests("' . $this->config['ombiRefresh'] . '");
+						homepageRequests("ombi", "' . $this->config['ombiRefresh'] . '");
 						// End Ombi Requests
 					</script>
 				</div>

+ 522 - 0
api/homepage/overseerr.php

@@ -0,0 +1,522 @@
+<?php
+
+trait OverseerrHomepageItem
+{
+	
+	public function overseerrSettingsArray($infoOnly = false)
+	{
+		$homepageInformation = [
+			'name' => 'Overseerr',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/overseerr.png',
+			'category' => 'Requests',
+			'settingsArray' => __FUNCTION__
+		];
+		if ($infoOnly) {
+			return $homepageInformation;
+		}
+		$homepageSettings = [
+			'debug' => true,
+			'settings' => [
+				'Enable' => [
+					$this->settingsOption('enable', 'homepageOverseerrEnabled'),
+					$this->settingsOption('auth', 'homepageOverseerrAuth'),
+					$this->settingsOption('notice', '', ['title' => 'Attention', 'body' => 'Since Organizr supports multiple Request Providers, You must now select which service you want to submit requests through']),
+					$this->settingsOption('select', 'defaultRequestService', ['label' => 'Default Request Service', 'options' => $this->requestServiceOptions()]),
+				],
+				'Connection' => [
+					$this->settingsOption('url', 'overseerrURL'),
+					$this->settingsOption('token', 'overseerrToken'),
+					$this->settingsOption('username', 'overseerrFallbackUser', ['label' => 'Overseerr Fallback User', 'help' => 'Organizr will request an Overseerr User Token based off of this user credentials']),
+					$this->settingsOption('password', 'overseerrFallbackPassword', ['label' => 'Overseerr Fallback Password',]),
+					$this->settingsOption('disable-cert-check', 'overseerrDisableCertCheck'),
+					$this->settingsOption('use-custom-certificate', 'overseerrUseCustomCertificate'),
+				],
+				'Misc Options' => [
+					$this->settingsOption('auth', 'homepageOverseerrRequestAuth', ['label' => 'Minimum Group to Request']),
+					$this->settingsOption('select', 'overseerrTvDefault', ['label' => 'TV Show Default Request', 'options' => $this->requestTvOptions(true)]),
+					$this->settingsOption('switch', 'overseerrLimitUser', ['label' => 'Limit to User']),
+					$this->settingsOption('limit', 'overseerrLimit'),
+					$this->settingsOption('refresh', 'overseerrRefresh'),
+				],
+				'Default Filter' => [
+					$this->settingsOption('switch', 'overseerrDefaultFilterAvailable', ['label' => 'Show Available', 'help' => 'Show All Available Overseerr Requests']),
+					$this->settingsOption('switch', 'overseerrDefaultFilterUnavailable', ['label' => 'Show Unavailable', 'help' => 'Show All Unavailable Overseerr Requests']),
+					$this->settingsOption('switch', 'overseerrDefaultFilterApproved', ['label' => 'Show Approved', 'help' => 'Show All Approved Overseerr Requests']),
+					$this->settingsOption('switch', 'overseerrDefaultFilterUnapproved', ['label' => 'Show Unapproved', 'help' => 'Show All Unapproved Overseerr Requests']),
+					$this->settingsOption('switch', 'overseerrDefaultFilterDenied', ['label' => 'Show Denied', 'help' => 'Show All Denied Overseerr Requests']),
+				],
+				'Test Connection' => [
+					$this->settingsOption('blank', null, ['label' => 'Please Save before Testing']),
+					$this->settingsOption('test', 'overseerr'),
+				]
+			]
+		];
+		return array_merge($homepageInformation, $homepageSettings);
+	}
+	
+	public function testConnectionOverseerr()
+	{
+		if (!$this->homepageItemPermissions($this->overseerrHomepagePermissions('test'), true)) {
+			return false;
+		}
+		$headers = array(
+			"Accept" => "application/json",
+			"X-Api-Key" => $this->config['overseerrToken'],
+		);
+		$url = $this->qualifyURL($this->config['overseerrURL']);
+		try {
+			$options = $this->requestOptions($url, null, $this->config['overseerrDisableCertCheck'], $this->config['overseerrUseCustomCertificate']);
+			$test = Requests::get($url . "/api/v1/settings/main", $headers, $options);
+			if ($test->success) {
+				$this->setAPIResponse('success', 'API Connection succeeded', 200);
+				return true;
+			} else {
+				$this->setResponse(401, 'API Connection failed');
+				return false;
+			}
+			
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Overseerr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setResponse(500, $e->getMessage());
+			return false;
+		}
+	}
+	
+	public function overseerrHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageOverseerrEnabled'
+				],
+				'auth' => [
+					'homepageOverseerrAuth'
+				],
+				'not_empty' => [
+					'overseerrURL',
+					'overseerrToken'
+				]
+			],
+			'request' => [
+				'enabled' => [
+					'homepageOverseerrEnabled'
+				],
+				'auth' => [
+					'homepageOverseerrAuth',
+					'homepageOverseerrRequestAuth'
+				],
+				'not_empty' => [
+					'overseerrURL',
+					'overseerrToken'
+				]
+			],
+			'test' => [
+				'not_empty' => [
+					'overseerrURL',
+					'overseerrToken'
+				]
+			]
+		];
+		return $this->homepageCheckKeyPermissions($key, $permissions);
+	}
+	
+	public function homepageOrderoverseerr()
+	{
+		if ($this->homepageItemPermissions($this->overseerrHomepagePermissions('main'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Requests...</h2></div>
+					<script>
+						// Overseerr Requests
+						homepageRequests("overseerr", "' . $this->config['overseerrRefresh'] . '");
+						// End Overseerr Requests
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	
+	public function getOverseerrRequests($limit = 50, $offset = 0)
+	{
+		if (!$this->homepageItemPermissions($this->overseerrHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$api['count'] = [
+			'movie' => 0,
+			'tv' => 0,
+			'limit' => (integer)$limit,
+			'offset' => (integer)$offset
+		];
+		$headers = [
+			"Accept" => "application/json",
+			"X-Api-Key" => $this->config['overseerrToken'],
+		];
+		$requests = [];
+		$url = $this->qualifyURL($this->config['overseerrURL']);
+		try {
+			$options = $this->requestOptions($url, $this->config['overseerrRefresh'], $this->config['overseerrDisableCertCheck'], $this->config['overseerrUseCustomCertificate']);
+			$request = Requests::get($url . "/api/v1/request?take=" . $limit, $headers, $options);
+			if ($request->success) {
+				$requestsData = json_decode($request->body, true);
+				foreach ($requestsData['results'] as $key => $value) {
+					$requester = ($value['requestedBy']['username'] !== '') ? $value['requestedBy']['username'] : $value['requestedBy']['plexUsername'];
+					$requesterEmail = $value['requestedBy']['email'];
+					$proceed = (($this->config['overseerrLimitUser']) && strtolower($this->user['username']) == strtolower($requester)) || (strtolower($requester) == strtolower($this->config['ombiFallbackUser'])) || (!$this->config['ombiLimitUser']) || $this->qualifyRequest(1);
+					if ($proceed) {
+						$requestItem = Requests::get($url . '/api/v1/' . $value['type'] . '/' . $value['media']['tmdbId'], $headers, $options);
+						$requestsItemData = json_decode($requestItem->body, true);
+						if ($requestItem->success) {
+							$api['count'][$value['type']]++;
+							$requests[] = array(
+								'id' => $value['media']['tmdbId'],
+								'title' => ($value['type'] == 'movie') ? $requestsItemData['title'] : $requestsItemData['name'],
+								'overview' => $requestsItemData['overview'],
+								'poster' => (isset($requestsItemData['posterPath']) && $requestsItemData['posterPath'] !== '') ? 'https://image.tmdb.org/t/p/w300/' . $requestsItemData['posterPath'] : 'plugins/images/cache/no-list.png',
+								'background' => (isset($requestsItemData['backdropPath']) && $requestsItemData['backdropPath'] !== '') ? 'https://image.tmdb.org/t/p/w1280/' . $requestsItemData['backdropPath'] : '',
+								'approved' => $value['status'] == 2,
+								'available' => $value['media']['status'] == 5,
+								'denied' => $value['status'] == 3,
+								'deniedReason' => 'n/a',
+								'user' => $requester,
+								'userAlias' => $value['requestedBy']['displayName'],
+								'request_id' => $value['id'],
+								'request_date' => $value['createdAt'],
+								'release_date' => ($value['type'] == 'movie') ? $requestsItemData['releaseDate'] : $requestsItemData['firstAirDate'],
+								'type' => $value['type'],
+								'icon' => 'mdi mdi-' . ($value['type'] == 'movie') ? 'filmstrip' : 'television',
+								'color' => ($value['type'] == 'movie') ? 'palette-Deep-Purple-900 bg white' : 'grayish-blue-bg',
+							);
+						}
+					}
+				}
+				//sort here
+				usort($requests, function ($item1, $item2) {
+					if ($item1['request_date'] == $item2['request_date']) {
+						return 0;
+					}
+					return $item1['request_date'] > $item2['request_date'] ? -1 : 1;
+				});
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Overseerr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setResponse(500, $e->getMessage());
+			return false;
+		}
+		$api['content'] = isset($requests) ? array_slice($requests, $offset, $limit) : false;
+		$this->setResponse(200, null, $api);
+		return $api;
+	}
+	
+	public function getDefaultService($services)
+	{
+		if (empty($services)) {
+			return null;
+		} else {
+			$default = false;
+			foreach ($services as $service) {
+				if ($service['isDefault']) {
+					$default = (int)$service['id'];
+				}
+			}
+			if ($default) {
+				return $services[$default];
+			} else {
+				return $services[0];
+			}
+		}
+	}
+	
+	public function addOverseerrRequest($id, $type, $seasons = null)
+	{
+		$id = ($id) ?? null;
+		$type = ($type) ?? null;
+		if (!$id) {
+			$this->setAPIResponse('error', 'Id was not supplied', 422);
+			return false;
+		}
+		if (!$type) {
+			$this->setAPIResponse('error', 'Type was not supplied', 422);
+			return false;
+		}
+		if (!$this->homepageItemPermissions($this->overseerrHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['overseerrURL']);
+		try {
+			if (!isset($_COOKIE['connect_sid']) && !isset($_COOKIE['connect.sid'])) {
+				$this->setAPIResponse('error', 'User does not have Auth Cookie', 500);
+				return false;
+			}
+			$headers = array(
+				'Accept' => 'application/json',
+				'Content-Type' => 'application/json',
+				'X-Api-Key' => $this->config['overseerrToken']
+			);
+			$cookieJar = new Requests_Cookie_Jar(['connect.sid' => $_COOKIE['connect_sid']]);
+			$optionsUser = $this->requestOptions($url, null, $this->config['overseerrDisableCertCheck'], $this->config['overseerrUseCustomCertificate'], ['cookies' => $cookieJar]);
+			$optionsAPI = $this->requestOptions($url, null, $this->config['overseerrDisableCertCheck'], $this->config['overseerrUseCustomCertificate']);
+			// Check if requested already
+			$searchResponse = Requests::get($url . '/api/v1/request/', $headers, $optionsAPI);
+			if ($searchResponse->success) {
+				$details = json_decode($searchResponse->body, true);
+				if (count($details['results']) > 0) {
+					foreach ($details['results'] as $k => $v) {
+						if ($v['media']['tmdbId'] == $id) {
+							if ($v['media']['status'] == 5) {
+								$this->setAPIResponse('error', 'Request is already available', 409);
+								return false;
+							} else {
+								$this->setAPIResponse('error', 'Request is already requested', 409);
+								return false;
+							}
+						}
+					}
+				}
+			} else {
+				$this->setAPIResponse('error', 'Overseerr Connection Error Occurred', 500);
+				return false;
+			}
+			// Get User info
+			$response = Requests::get($url . '/api/v1/auth/me', [], $optionsUser);
+			if ($response->success) {
+				$userInfo = json_decode($response->body, true);
+			} else {
+				$this->setResponse(500, 'Error getting user information');
+				return false;
+			}
+			switch ($type) {
+				case 'season':
+				case 'tv':
+					if ($this->config['overseerrTvDefault'] == 'user') {
+						if ($seasons) {
+							if (strpos($seasons, ',') !== false) {
+								$seasonsExplode = explode(',', $seasons);
+								foreach ($seasonsExplode as $season) {
+									$seasonsArray[] = (int)$season;
+								}
+								$seasons = $seasonsArray;
+							} else {
+								$seasonsArray[] = (int)$seasons;
+								$seasons = $seasonsArray;
+							}
+						} else {
+							$this->setResponse(500, 'Seasons requested was not supplied');
+							return false;
+						}
+					}
+					$response = Requests::get($url . '/api/v1/tv/' . $id, $headers, $optionsAPI);
+					if ($response->success) {
+						$seriesInfo = json_decode($response->body, true);
+					} else {
+						$this->setResponse(500, 'Error getting series information');
+						return false;
+					}
+					if ($this->config['overseerrTvDefault'] == 'first') {
+						$seasons = [1];
+					} elseif ($this->config['overseerrTvDefault'] == 'last') {
+						$lastSeason = end($seriesInfo['seasons']);
+						$lastSeasonNumber = $lastSeason['seasonNumber'];
+						$seasons = [$lastSeasonNumber];
+					} elseif ($this->config['overseerrTvDefault'] == 'all') {
+						$seasons = [];
+						foreach ($seriesInfo['seasons'] as $season) {
+							if ($season['seasonNumber'] !== 0) {
+								$seasons[] = $season['seasonNumber'];
+							}
+						}
+					}
+					$response = Requests::get($url . '/api/v1/service/sonarr', $headers, $optionsAPI);
+					if ($response->success) {
+						$serviceInfo = $this->getDefaultService(json_decode($response->body, true));
+					} else {
+						$this->setResponse(500, 'Error getting service information');
+						return false;
+					}
+					$add = [
+						'mediaId' => (int)$id,
+						'tvdbId' => $seriesInfo['externalIds']['tvdbId'],
+						'mediaType' => 'tv',
+						'is4k' => $serviceInfo['is4k'],
+						'seasons' => $seasons,
+						'serverId' => (int)$serviceInfo['id'],
+						'profileId' => (int)$serviceInfo['activeProfileId'],
+						'rootFolder' => $serviceInfo['activeDirectory'],
+						'languageProfileId' => (int)$serviceInfo['activeLanguageProfileId'],
+						'userId' => (int)$userInfo['id'],
+						'tags' => []
+					];
+					break;
+				default:
+					$response = Requests::get($url . '/api/v1/service/radarr', $headers, $optionsAPI);
+					if ($response->success) {
+						$serviceInfo = $this->getDefaultService(json_decode($response->body, true));
+					} else {
+						$this->setResponse(500, 'Error getting service information');
+						return false;
+					}
+					$add = [
+						'mediaId' => (int)$id,
+						'mediaType' => 'movie',
+						'is4k' => $serviceInfo['is4k'],
+						'serverId' => (int)$serviceInfo['id'],
+						'profileId' => (int)$serviceInfo['activeProfileId'],
+						'userId' => (int)$userInfo['id'],
+						'tags' => []
+					];
+					break;
+			}
+			$response = Requests::post($url . "/api/v1/request", ['Accept' => 'application/json', 'Content-Type' => 'application/json'], json_encode($add), $optionsUser);
+			if ($response->success) {
+				$this->setAPIResponse('success', 'Overseerr Request submitted', 200);
+				return true;
+			} else {
+				$message = 'Overseerr Error Occurred';
+				if ($this->isJSON($response->body)) {
+					$messageJSON = json_decode($response->body, true);
+					$message = $messageJSON['message'] ?? $message;
+				}
+				$this->setAPIResponse('error', $message, 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Overseerr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function actionOverseerrRequest($id, $type, $action)
+	{
+		$id = ($id) ?? null;
+		$type = ($type) ?? null;
+		$action = ($action) ?? null;
+		if (!$id) {
+			$this->setAPIResponse('error', 'Id was not supplied', 422);
+			return false;
+		}
+		if (!$type) {
+			$this->setAPIResponse('error', 'Type was not supplied', 422);
+			return false;
+		}
+		if (!$action) {
+			$this->setAPIResponse('error', 'Action was not supplied', 422);
+			return false;
+		}
+		if (!$this->homepageItemPermissions($this->overseerrHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['overseerrURL']);
+		$headers = array(
+			"Accept" => "application/json",
+			"Content-Type" => "application/json",
+			'X-Api-Key' => $this->config['overseerrToken']
+		);
+		try {
+			$options = $this->requestOptions($url, null, $this->config['overseerrDisableCertCheck'], $this->config['overseerrUseCustomCertificate']);
+			switch ($action) {
+				case 'approve':
+					$response = Requests::post($url . "/api/v1/request/" . $id . "/approve", $headers, [], $options);
+					$message = 'Overseerr Request has been approved';
+					break;
+				case 'pending':
+					$response = Requests::post($url . '/api/v1/request/' . $id . '/pending', $headers, [], $options);
+					$message = 'Overseerr Request has been approved';
+					break;
+				case 'available':
+					$requestInfoResponse = Requests::get($url . '/api/v1/request/' . $id, $headers, $options);
+					if ($requestInfoResponse->success) {
+						$requestInfo = json_decode($requestInfoResponse->body, true);
+						$mediaId = $requestInfo['media']['id'];
+					} else {
+						$this->setResponse(500, 'Error getting request information');
+						return false;
+					}
+					$response = Requests::post($url . '/api/v1/media/' . $mediaId . '/available', $headers, [], $options);
+					$message = 'Overseerr Request has been marked available';
+					break;
+				case 'unavailable':
+					$requestInfoResponse = Requests::get($url . '/api/v1/request/' . $id, $headers, $options);
+					if ($requestInfoResponse->success) {
+						$requestInfo = json_decode($requestInfoResponse->body, true);
+						$mediaId = $requestInfo['media']['id'];
+					} else {
+						$this->setResponse(500, 'Error getting request information');
+						return false;
+					}
+					$response = Requests::post($url . "/api/v1/media/" . $mediaId . "/pending", $headers, [], $options);
+					$message = 'Overseerr Request has been marked unavailable';
+					break;
+				case 'deny':
+					$response = Requests::post($url . "/api/v1/request/" . $id . "/decline", $headers, [], $options);
+					$message = 'Overseerr Request has been denied';
+					break;
+				case 'delete':
+					$response = Requests::delete($url . "/api/v1/request/" . $id, $headers, $options);
+					$message = 'Overseerr Request has been deleted';
+					break;
+				default:
+					return false;
+			}
+			if ($response->success) {
+				$this->setAPIResponse('success', $message, 200);
+				return true;
+			} else {
+				$message = 'Overseerr Error Occurred';
+				if ($this->isJSON($response->body)) {
+					$messageJSON = json_decode($response->body, true);
+					$message = $messageJSON['message'] ?? $message;
+				}
+				$this->setAPIResponse('error', $message, 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Overseerr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function getOverseerrMetadata($id, $type)
+	{
+		if (!$id) {
+			$this->setAPIResponse('error', 'Id was not supplied', 422);
+			return false;
+		}
+		if (!$type) {
+			$this->setAPIResponse('error', 'Type was not supplied', 422);
+			return false;
+		}
+		if (!$this->homepageItemPermissions($this->overseerrHomepagePermissions('request'), true)) {
+			return false;
+		}
+		try {
+			$url = $this->qualifyURL($this->config['overseerrURL']);
+			$headers = array(
+				'Accept' => 'application/json',
+				'Content-Type' => 'application/json',
+				'X-Api-Key' => $this->config['overseerrToken']
+			);
+			$options = $this->requestOptions($url, null, $this->config['overseerrDisableCertCheck'], $this->config['overseerrUseCustomCertificate']);
+			$response = Requests::get($url . '/api/v1/' . $type . '/' . $id, $headers, $options);
+			if ($response->success) {
+				$metadata = json_decode($response->body, true);
+				$this->setResponse(200, null, $metadata);
+				return $metadata;
+			} else {
+				$this->setResponse(500, 'Error getting series information');
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Overseerr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function overseerrTVDefault($type)
+	{
+		return $type == $this->config['overseerrTvDefault'];
+	}
+}

+ 1 - 7
api/homepage/pihole.php

@@ -95,13 +95,7 @@ trait PiHoleHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderPihole()

+ 121 - 102
api/homepage/plex.php

@@ -2,7 +2,6 @@
 
 trait PlexHomepageItem
 {
-	
 	public function plexSettingsArray($infoOnly = false)
 	{
 		$homepageInformation = [
@@ -24,6 +23,7 @@ trait PlexHomepageItem
 			}
 		}
 		$homepageSettings = [
+			'docs' => $this->docs('features/homepage/plex-homepage-item'),
 			'debug' => true,
 			'settings' => [
 				'Enable' => [
@@ -183,13 +183,7 @@ trait PlexHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderplexnowplaying()
@@ -252,22 +246,27 @@ trait PlexHomepageItem
 		$url = $this->qualifyURL($this->config['plexURL']);
 		$url = $url . "/status/sessions?X-Plex-Token=" . $this->config['plexToken'];
 		$options = $this->requestOptions($url, $this->config['homepageStreamRefresh'], $this->config['plexDisableCertCheck'], $this->config['plexUseCustomCertificate']);
-		$response = Requests::get($url, [], $options);
-		libxml_use_internal_errors(true);
-		if ($response->success) {
-			$items = array();
-			$plex = simplexml_load_string($response->body);
-			foreach ($plex as $child) {
-				if (!in_array($child['type'], $ignore) && !in_array($child['librarySectionID'], $exclude) && isset($child['librarySectionID'])) {
-					$items[] = $this->resolvePlexItem($child);
+		try {
+			$response = Requests::get($url, [], $options);
+			libxml_use_internal_errors(true);
+			if ($response->success) {
+				$items = array();
+				$plex = simplexml_load_string($response->body);
+				foreach ($plex as $child) {
+					if (!in_array($child['type'], $ignore) && !in_array($child['librarySectionID'], $exclude) && isset($child['librarySectionID'])) {
+						$items[] = $this->resolvePlexItem($child);
+					}
 				}
+				$api['content'] = ($resolve) ? $items : $plex;
+				$api['plexID'] = $this->config['plexID'];
+				$api['showNames'] = true;
+				$api['group'] = '1';
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
 			}
-			$api['content'] = ($resolve) ? $items : $plex;
-			$api['plexID'] = $this->config['plexID'];
-			$api['showNames'] = true;
-			$api['group'] = '1';
-			$this->setAPIResponse('success', null, 200, $api);
-			return $api;
+		} catch (Exception $e) {
+			$this->setAPIResponse('error', null, 422, [$e->getMessage()]);
+			return false;
 		}
 	}
 	
@@ -283,35 +282,40 @@ trait PlexHomepageItem
 		$urls['movie'] = $url . "/hubs/home/recentlyAdded?X-Plex-Token=" . $this->config['plexToken'] . "&X-Plex-Container-Start=0&X-Plex-Container-Size=" . $this->config['homepageRecentLimit'] . "&type=1";
 		$urls['tv'] = $url . "/hubs/home/recentlyAdded?X-Plex-Token=" . $this->config['plexToken'] . "&X-Plex-Container-Start=0&X-Plex-Container-Size=" . $this->config['homepageRecentLimit'] . "&type=2";
 		$urls['music'] = $url . "/hubs/home/recentlyAdded?X-Plex-Token=" . $this->config['plexToken'] . "&X-Plex-Container-Start=0&X-Plex-Container-Size=" . $this->config['homepageRecentLimit'] . "&type=8";
-		foreach ($urls as $k => $v) {
-			$options = $this->requestOptions($url, $this->config['homepageRecentRefresh'], $this->config['plexDisableCertCheck'], $this->config['plexUseCustomCertificate']);
-			$response = Requests::get($v, [], $options);
-			libxml_use_internal_errors(true);
-			if ($response->success) {
-				$items = array();
-				$plex = simplexml_load_string($response->body);
-				foreach ($plex as $child) {
-					if (!in_array($child['type'], $ignore) && !in_array($child['librarySectionID'], $exclude) && isset($child['librarySectionID'])) {
-						$items[] = $this->resolvePlexItem($child);
+		try {
+			foreach ($urls as $k => $v) {
+				$options = $this->requestOptions($url, $this->config['homepageRecentRefresh'], $this->config['plexDisableCertCheck'], $this->config['plexUseCustomCertificate']);
+				$response = Requests::get($v, [], $options);
+				libxml_use_internal_errors(true);
+				if ($response->success) {
+					$items = array();
+					$plex = simplexml_load_string($response->body);
+					foreach ($plex as $child) {
+						if (!in_array($child['type'], $ignore) && !in_array($child['librarySectionID'], $exclude) && isset($child['librarySectionID'])) {
+							$items[] = $this->resolvePlexItem($child);
+						}
+					}
+					if (isset($api)) {
+						$api['content'] = array_merge($api['content'], ($resolve) ? $items : $plex);
+					} else {
+						$api['content'] = ($resolve) ? $items : $plex;
 					}
-				}
-				if (isset($api)) {
-					$api['content'] = array_merge($api['content'], ($resolve) ? $items : $plex);
-				} else {
-					$api['content'] = ($resolve) ? $items : $plex;
 				}
 			}
+			if (isset($api['content'])) {
+				usort($api['content'], function ($a, $b) {
+					return $b['addedAt'] <=> $a['addedAt'];
+				});
+			}
+			$api['plexID'] = $this->config['plexID'];
+			$api['showNames'] = true;
+			$api['group'] = '1';
+			$this->setAPIResponse('success', null, 200, $api);
+			return $api;
+		} catch (Exception $e) {
+			$this->setAPIResponse('error', null, 422, [$e->getMessage()]);
+			return false;
 		}
-		if (isset($api['content'])) {
-			usort($api['content'], function ($a, $b) {
-				return $b['addedAt'] <=> $a['addedAt'];
-			});
-		}
-		$api['plexID'] = $this->config['plexID'];
-		$api['showNames'] = true;
-		$api['group'] = '1';
-		$this->setAPIResponse('success', null, 200, $api);
-		return $api;
 	}
 	
 	public function getPlexHomepagePlaylists()
@@ -322,35 +326,40 @@ trait PlexHomepageItem
 		$url = $this->qualifyURL($this->config['plexURL']);
 		$url = $url . "/playlists?X-Plex-Token=" . $this->config['plexToken'];
 		$options = $this->requestOptions($url, null, $this->config['plexDisableCertCheck'], $this->config['plexUseCustomCertificate']);
-		$response = Requests::get($url, [], $options);
-		libxml_use_internal_errors(true);
-		if ($response->success) {
-			$items = array();
-			$plex = simplexml_load_string($response->body);
-			foreach ($plex as $child) {
-				if ($child['playlistType'] == "video" && strpos(strtolower($child['title']), 'private') === false) {
-					$playlistTitleClean = preg_replace("/(\W)+/", "", (string)$child['title']);
-					$playlistURL = $this->qualifyURL($this->config['plexURL']);
-					$playlistURL = $playlistURL . $child['key'] . "?X-Plex-Token=" . $this->config['plexToken'];
-					$options = ($this->localURL($url)) ? array('verify' => false) : array();
-					$playlistResponse = Requests::get($playlistURL, array(), $options);
-					if ($playlistResponse->success) {
-						$playlistResponse = simplexml_load_string($playlistResponse->body);
-						$items[$playlistTitleClean]['title'] = (string)$child['title'];
-						foreach ($playlistResponse->Video as $playlistItem) {
-							$items[$playlistTitleClean][] = $this->resolvePlexItem($playlistItem);
+		try {
+			$response = Requests::get($url, [], $options);
+			libxml_use_internal_errors(true);
+			if ($response->success) {
+				$items = array();
+				$plex = simplexml_load_string($response->body);
+				foreach ($plex as $child) {
+					if ($child['playlistType'] == "video" && strpos(strtolower($child['title']), 'private') === false) {
+						$playlistTitleClean = preg_replace("/(\W)+/", "", (string)$child['title']);
+						$playlistURL = $this->qualifyURL($this->config['plexURL']);
+						$playlistURL = $playlistURL . $child['key'] . "?X-Plex-Token=" . $this->config['plexToken'];
+						$options = ($this->localURL($url)) ? array('verify' => false) : array();
+						$playlistResponse = Requests::get($playlistURL, array(), $options);
+						if ($playlistResponse->success) {
+							$playlistResponse = simplexml_load_string($playlistResponse->body);
+							$items[$playlistTitleClean]['title'] = (string)$child['title'];
+							foreach ($playlistResponse->Video as $playlistItem) {
+								$items[$playlistTitleClean][] = $this->resolvePlexItem($playlistItem);
+							}
 						}
 					}
 				}
+				$api['content'] = $items;
+				$api['plexID'] = $this->config['plexID'];
+				$api['showNames'] = true;
+				$api['group'] = '1';
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
+			} else {
+				$this->setAPIResponse('error', 'Plex API error', 500);
+				return false;
 			}
-			$api['content'] = $items;
-			$api['plexID'] = $this->config['plexID'];
-			$api['showNames'] = true;
-			$api['group'] = '1';
-			$this->setAPIResponse('success', null, 200, $api);
-			return $api;
-		} else {
-			$this->setAPIResponse('error', 'Plex API error', 500);
+		} catch (Exception $e) {
+			$this->setAPIResponse('error', null, 422, [$e->getMessage()]);
 			return false;
 		}
 	}
@@ -370,22 +379,27 @@ trait PlexHomepageItem
 		$url = $this->qualifyURL($this->config['plexURL']);
 		$url = $url . "/library/metadata/" . $key . "?X-Plex-Token=" . $this->config['plexToken'];
 		$options = $this->requestOptions($url, null, $this->config['plexDisableCertCheck'], $this->config['plexUseCustomCertificate']);
-		$response = Requests::get($url, [], $options);
-		libxml_use_internal_errors(true);
-		if ($response->success) {
-			$items = array();
-			$plex = simplexml_load_string($response->body);
-			foreach ($plex as $child) {
-				if (!in_array($child['type'], $ignore) && isset($child['librarySectionID'])) {
-					$items[] = $this->resolvePlexItem($child);
+		try {
+			$response = Requests::get($url, [], $options);
+			libxml_use_internal_errors(true);
+			if ($response->success) {
+				$items = array();
+				$plex = simplexml_load_string($response->body);
+				foreach ($plex as $child) {
+					if (!in_array($child['type'], $ignore) && isset($child['librarySectionID'])) {
+						$items[] = $this->resolvePlexItem($child);
+					}
 				}
+				$api['content'] = ($resolve) ? $items : $plex;
+				$api['plexID'] = $this->config['plexID'];
+				$api['showNames'] = true;
+				$api['group'] = '1';
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
 			}
-			$api['content'] = ($resolve) ? $items : $plex;
-			$api['plexID'] = $this->config['plexID'];
-			$api['showNames'] = true;
-			$api['group'] = '1';
-			$this->setAPIResponse('success', null, 200, $api);
-			return $api;
+		} catch (Exception $e) {
+			$this->setAPIResponse('error', null, 422, [$e->getMessage()]);
+			return false;
 		}
 	}
 	
@@ -405,22 +419,27 @@ trait PlexHomepageItem
 		$url = $this->qualifyURL($this->config['plexURL']);
 		$url = $url . "/search?query=" . rawurlencode($query) . "&X-Plex-Token=" . $this->config['plexToken'];
 		$options = $this->requestOptions($url, null, $this->config['plexDisableCertCheck'], $this->config['plexUseCustomCertificate']);
-		$response = Requests::get($url, [], $options);
-		libxml_use_internal_errors(true);
-		if ($response->success) {
-			$items = array();
-			$plex = simplexml_load_string($response->body);
-			foreach ($plex as $child) {
-				if (!in_array($child['type'], $ignore) && !in_array($child['librarySectionID'], $exclude) && isset($child['librarySectionID'])) {
-					$items[] = $this->resolvePlexItem($child);
+		try {
+			$response = Requests::get($url, [], $options);
+			libxml_use_internal_errors(true);
+			if ($response->success) {
+				$items = array();
+				$plex = simplexml_load_string($response->body);
+				foreach ($plex as $child) {
+					if (!in_array($child['type'], $ignore) && !in_array($child['librarySectionID'], $exclude) && isset($child['librarySectionID'])) {
+						$items[] = $this->resolvePlexItem($child);
+					}
 				}
+				$api['content'] = ($resolve) ? $items : $plex;
+				$api['plexID'] = $this->config['plexID'];
+				$api['showNames'] = true;
+				$api['group'] = '1';
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
 			}
-			$api['content'] = ($resolve) ? $items : $plex;
-			$api['plexID'] = $this->config['plexID'];
-			$api['showNames'] = true;
-			$api['group'] = '1';
-			$this->setAPIResponse('success', null, 200, $api);
-			return $api;
+		} catch (Exception $e) {
+			$this->setAPIResponse('error', null, 422, [$e->getMessage()]);
+			return false;
 		}
 	}
 	
@@ -465,7 +484,7 @@ trait PlexHomepageItem
 			case 'episode':
 				$plexItem['type'] = 'tv';
 				$plexItem['title'] = (string)$item['grandparentTitle'];
-				$plexItem['secondaryTitle'] = (string)$item['parentTitle'];
+				$plexItem['secondaryTitle'] = (string)$item['parentTitle'] . ' - Episode ' . (string)$item['index'];
 				$plexItem['summary'] = (string)$item['title'];
 				$plexItem['ratingKey'] = (string)$item['parentRatingKey'];
 				$plexItem['thumb'] = ($item['parentThumb'] ? (string)$item['parentThumb'] : (string)$item['grandparentThumb']);
@@ -627,8 +646,8 @@ trait PlexHomepageItem
 		$url .= '/api/v2?apikey=' . $this->config['tautulliApikey'];
 		$url .= '&cmd=get_users';
 		$options = $this->requestOptions($url, null, $this->config['tautulliDisableCertCheck'], $this->config['tautulliUseCustomCertificate']);
-		$response = Requests::get($url, [], $options);
 		try {
+			$response = Requests::get($url, [], $options);
 			$response = json_decode($response->body, true);
 			foreach ($response['response']['data'] as $user) {
 				if ($user['user_id'] != 0) {

+ 1 - 7
api/homepage/qbittorrent.php

@@ -117,13 +117,7 @@ trait QBitTorrentHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderqBittorrent()

+ 3 - 9
api/homepage/radarr.php

@@ -140,13 +140,7 @@ trait RadarrHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderRadarrQueue()
@@ -202,8 +196,8 @@ trait RadarrHomepageItem
 	
 	public function getRadarrCalendar($startDate = null, $endDate = null)
 	{
-		$startDate = ($startDate) ?? $_GET['start'];
-		$endDate = ($endDate) ?? $_GET['end'];
+		$startDate = ($startDate) ?? $_GET['start'] ?? date('Y-m-d', strtotime('-' . $this->config['calendarStart'] . ' days'));
+		$endDate = ($endDate) ?? $_GET['end'] ?? date('Y-m-d', strtotime('+' . $this->config['calendarEnd'] . ' days'));
 		if (!$this->homepageItemPermissions($this->radarrHomepagePermissions('calendar'), true)) {
 			return false;
 		}

+ 2 - 8
api/homepage/rtorrent.php

@@ -126,13 +126,7 @@ trait RTorrentHomepageItem
 				'not_empty' => []
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderrTorrent()
@@ -195,7 +189,7 @@ trait RTorrentHomepageItem
 			$extraPath = (strpos($this->config['rTorrentURL'], '.php') !== false) ? '' : '/RPC2';
 			$extraPath = (empty($this->config['rTorrentURLOverride'])) ? $extraPath : '';
 			$url = $digest['scheme'] . '://' . $digest['host'] . $digest['port'] . $digest['path'] . $extraPath;
-			$options = $this->requestOptions($url, $this->config['rTorrentRefresh'], $this->config['rTorrentDisableCertCheck'], $this->config['rtorrentUseCustomCertificate']);
+			$options = $this->requestOptions($url, $this->config['rTorrentRefresh'], $this->config['rTorrentDisableCertCheck'], $this->config['rTorrentUseCustomCertificate']);
 			if ($this->config['rTorrentUsername'] !== '' && $this->decrypt($this->config['rTorrentPassword']) !== '') {
 				$credentials = array('auth' => new Requests_Auth_Digest(array($this->config['rTorrentUsername'], $this->decrypt($this->config['rTorrentPassword']))));
 				$options = array_merge($options, $credentials);

+ 1 - 7
api/homepage/sabnzbd.php

@@ -97,13 +97,7 @@ trait SabNZBdHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrdersabnzbd()

+ 1 - 7
api/homepage/sickrage.php

@@ -108,13 +108,7 @@ trait SickRageHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function getSickRageCalendar($startDate = null, $endDate = null)

+ 4 - 10
api/homepage/sonarr.php

@@ -15,7 +15,7 @@ trait SonarrHomepageItem
 			return $homepageInformation;
 		}
 		$homepageSettings = [
-			'docs' => 'https://docs.organizr.app/books/setup-features/page/sonarr',
+			'docs' => $this->docs('features/homepage/sonarr-homepage-item'),
 			'debug' => true,
 			'settings' => [
 				'About' => [
@@ -141,13 +141,7 @@ trait SonarrHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderSonarrQueue()
@@ -203,8 +197,8 @@ trait SonarrHomepageItem
 	
 	public function getSonarrCalendar($startDate = null, $endDate = null)
 	{
-		$startDate = ($startDate) ?? $_GET['start'];
-		$endDate = ($endDate) ?? $_GET['end'];
+		$startDate = ($startDate) ?? $_GET['start'] ?? date('Y-m-d', strtotime('-' . $this->config['calendarStart'] . ' days'));
+		$endDate = ($endDate) ?? $_GET['end'] ?? date('Y-m-d', strtotime('+' . $this->config['calendarEnd'] . ' days'));
 		if (!$this->homepageItemPermissions($this->sonarrHomepagePermissions('calendar'), true)) {
 			return false;
 		}

+ 1 - 7
api/homepage/speedtest.php

@@ -51,13 +51,7 @@ trait SpeedTestHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderSpeedtest()

+ 2 - 8
api/homepage/tautulli.php

@@ -27,7 +27,7 @@ trait TautulliHomepageItem
 					$this->settingsOption('refresh', 'homepageTautulliRefresh'),
 				],
 				'Connection' => [
-					$this->settingsOption('url', 'tautulliURL'),
+					$this->settingsOption('multiple-url', 'tautulliURL'),
 					$this->settingsOption('api-key', 'tautulliApikey'),
 					$this->settingsOption('disable-cert-check', 'tautulliDisableCertCheck'),
 					$this->settingsOption('use-custom-certificate', 'tautulliUseCustomCertificate'),
@@ -110,13 +110,7 @@ trait TautulliHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrdertautulli()

+ 2 - 8
api/homepage/trakt.php

@@ -30,7 +30,7 @@ trait TraktHomepageItem
 										<p lang="en">In order for this item to be setup, you need to goto the following URL to create a new API app.</p>
 										<p><a href="https://trakt.tv/oauth/applications/new" target="_blank">New API App</a></p>
 										<p lang="en">Enter anything for Name and Description.  You can leave Javascript and Permissions blank.  The only info you have to enter is for Redirect URI.  Enter the following URL:</p>
-										<code>' . $this->getServerPath() . 'api/v2/oauth/trakt</code>
+										<code class="elip hidden-xs">' . $this->getServerPath() . 'api/v2/oauth/trakt</code>
 									</div>
 								</div>
 							</div>'
@@ -77,13 +77,7 @@ trait TraktHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function getTraktCalendar($startDate = null)

+ 1 - 7
api/homepage/transmission.php

@@ -105,13 +105,7 @@ trait TransmissionHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrdertransmission()

+ 100 - 121
api/homepage/unifi.php

@@ -58,15 +58,19 @@ trait UnifiHomepageItem
 					'unifiUsername',
 					'unifiPassword'
 				]
+			],
+			'test' => [
+				'auth' => [
+					'homepageUnifiAuth'
+				],
+				'not_empty' => [
+					'unifiURL',
+					'unifiUsername',
+					'unifiPassword'
+				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderunifi()
@@ -87,45 +91,48 @@ trait UnifiHomepageItem
 	
 	public function getUnifiSiteName()
 	{
-		if (empty($this->config['unifiURL'])) {
-			$this->setAPIResponse('error', 'Unifi URL is not defined', 422);
-			return false;
-		}
-		if (empty($this->config['unifiUsername'])) {
-			$this->setAPIResponse('error', 'Unifi Username is not defined', 422);
+		if (!$this->homepageItemPermissions($this->unifiHomepagePermissions('test'), true)) {
 			return false;
 		}
-		if (empty($this->config['unifiPassword'])) {
-			$this->setAPIResponse('error', 'Unifi Password is not defined', 422);
-			return false;
-		}
-		$url = $this->qualifyURL($this->config['unifiURL']);
 		try {
-			$options = $this->requestOptions($url, $this->config['homepageUnifiRefresh'], $this->config['unifiDisableCertCheck'], $this->config['unifiUseCustomCertificate'], ['follow_redirects' => true]);
-			$data = array(
-				'username' => $this->config['unifiUsername'],
-				'password' => $this->decrypt($this->config['unifiPassword']),
-				'remember' => true,
-				'strict' => true
-			);
-			$response = Requests::post($url . '/api/login', array(), json_encode($data), $options);
-			if ($response->success) {
-				$cookie['unifises'] = ($response->cookies['unifises']->value) ?? false;
-				$cookie['csrf_token'] = ($response->cookies['csrf_token']->value) ?? false;
+			$login = $this->unifiLogin();
+			if ($login) {
+				$url = $this->qualifyURL($this->config['unifiURL']);
+				$unifiOS = $login['unifiOS'];
+				if ($unifiOS) {
+					$this->setResponse(500, 'Unifi OS does not support Multi Site');
+					return false;
+				}
+				$response = Requests::get($url . '/api/self/sites', [], $login['options']);
+				if ($response->success) {
+					$body = json_decode($response->body, true);
+					$this->setAPIResponse('success', null, 200, $body);
+					return $body;
+				} else {
+					$this->setAPIResponse('error', 'Unifi response error3', 409);
+					return false;
+				}
 			} else {
-				$this->setAPIResponse('error', 'Unifi response error - Check Credentials', 409);
 				return false;
 			}
-			$headers = array(
-				'cookie' => 'unifises=' . $cookie['unifises'] . ';' . 'csrf_token=' . $cookie['csrf_token'] . ';'
-			);
-			$response = Requests::get($url . '/api/self/sites', $headers, $options);
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Unifi Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function isUnifiOS()
+	{
+		try {
+			// Is this UnifiOs or Regular
+			$url = $this->qualifyURL($this->config['unifiURL']);
+			$options = $this->requestOptions($url, $this->config['homepageUnifiRefresh'], $this->config['unifiDisableCertCheck'], $this->config['unifiUseCustomCertificate'], ['follow_redirects' => true]);
+			$response = Requests::get($url, [], $options);
 			if ($response->success) {
-				$body = json_decode($response->body, true);
-				$this->setAPIResponse('success', null, 200, $body);
-				return $body;
+				return ($response->headers['x-csrf-token']) ?? false;
 			} else {
-				$this->setAPIResponse('error', 'Unifi response error - Error Occurred', 409);
+				$this->setAPIResponse('error', 'Unifi response error - Check URL', 409);
 				return false;
 			}
 		} catch (Requests_Exception $e) {
@@ -133,24 +140,11 @@ trait UnifiHomepageItem
 			$this->setAPIResponse('error', $e->getMessage(), 500);
 			return false;
 		}
-		
 	}
 	
-	public function testConnectionUnifi()
+	public function unifiLogin()
 	{
-		if (empty($this->config['unifiURL'])) {
-			$this->setAPIResponse('error', 'Unifi URL is not defined', 422);
-			return false;
-		}
-		if (empty($this->config['unifiUsername'])) {
-			$this->setAPIResponse('error', 'Unifi Username is not defined', 422);
-			return false;
-		}
-		if (empty($this->config['unifiPassword'])) {
-			$this->setAPIResponse('error', 'Unifi Password is not defined', 422);
-			return false;
-		}
-		$api['content']['unifi'] = array();
+		$csrfToken = $this->isUnifiOS();
 		$url = $this->qualifyURL($this->config['unifiURL']);
 		$options = $this->requestOptions($url, $this->config['homepageUnifiRefresh'], $this->config['unifiDisableCertCheck'], $this->config['unifiUseCustomCertificate'], ['follow_redirects' => true]);
 		$data = array(
@@ -160,44 +154,55 @@ trait UnifiHomepageItem
 			'strict' => true
 		);
 		try {
-			// Is this UnifiOs or Regular
-			$response = Requests::get($url, [], $options);
-			if ($response->success) {
-				$csrfToken = ($response->headers['x-csrf-token']) ?? false;
-				$data = ($csrfToken) ? $data : json_encode($data);
-			} else {
-				$this->setAPIResponse('error', 'Unifi response error - Check URL', 409);
-				return false;
-			}
+			$data = ($csrfToken) ? $data : json_encode($data);
+			$headers = ($csrfToken) ? ['x-csrf-token' => $csrfToken] : [];
 			$urlLogin = ($csrfToken) ? $url . '/api/auth/login' : $url . '/api/login';
-			$urlStat = ($csrfToken) ? $url . '/proxy/network/api/s/default/stat/health' : $url . '/api/s/' . $this->config['unifiSiteName'] . '/stat/health';
-			$response = Requests::post($urlLogin, [], $data, $options);
+			$response = Requests::post($urlLogin, $headers, $data, $options);
 			if ($response->success) {
-				$cookie['unifises'] = ($response->cookies['unifises']->value) ?? false;
-				$cookie['csrf_token'] = ($response->cookies['csrf_token']->value) ?? false;
-				$cookie['Token'] = ($response->cookies['Token']->value) ?? false;
 				$options['cookies'] = $response->cookies;
-				
+				return [
+					'unifiOS' => $csrfToken,
+					'options' => $options
+				];
 			} else {
 				$this->setAPIResponse('error', 'Unifi response error - Check Credentials', 409);
 				return false;
 			}
-			$headers = array(
-				'cookie' => 'unifises=' . $cookie['unifises'] . ';' . 'csrf_token=' . $cookie['csrf_token'] . ';'
-			);
-			$response = Requests::get($urlStat, $headers, $options);
-			if ($response->success) {
-				$api['content']['unifi'] = json_decode($response->body, true);
-			} else {
-				$this->setAPIResponse('error', 'Unifi response error3', 409);
-				return false;
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Unifi Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function testConnectionUnifi()
+	{
+		if (!$this->homepageItemPermissions($this->unifiHomepagePermissions('test'), true)) {
+			return false;
+		}
+		try {
+			// Is this UnifiOs or Regular
+			$api['content']['unifi'] = array();
+			$login = $this->unifiLogin();
+			if ($login) {
+				$url = $this->qualifyURL($this->config['unifiURL']);
+				$unifiOS = $login['unifiOS'];
+				$headers = ($unifiOS) ? ['x-csrf-token' => $unifiOS] : [];
+				$urlStat = ($unifiOS) ? $url . '/proxy/network/api/s/default/stat/health' : $url . '/api/s/' . $this->config['unifiSiteName'] . '/stat/health';
+				$response = Requests::get($urlStat, $headers, $login['options']);
+				if ($response->success) {
+					$api['content']['unifi'] = json_decode($response->body, true);
+				} else {
+					$this->setAPIResponse('error', 'Unifi response error3', 409);
+					return false;
+				}
 			}
 		} catch (Requests_Exception $e) {
 			$this->writeLog('error', 'Unifi Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
 			$this->setAPIResponse('error', $e->getMessage(), 500);
 			return false;
-		};
-		$api['content']['unifi'] = isset($api['content']['unifi']) ? $api['content']['unifi'] : false;
+		}
+		$api['content']['unifi'] = $api['content']['unifi'] ?? false;
 		$this->setAPIResponse('success', 'API Connection succeeded', 200);
 		return true;
 	}
@@ -207,54 +212,28 @@ trait UnifiHomepageItem
 		if (!$this->homepageItemPermissions($this->unifiHomepagePermissions('main'), true)) {
 			return false;
 		}
-		$api['content']['unifi'] = array();
-		$url = $this->qualifyURL($this->config['unifiURL']);
-		$options = $this->requestOptions($url, $this->config['homepageUnifiRefresh'], $this->config['unifiDisableCertCheck'], $this->config['unifiUseCustomCertificate'], ['follow_redirects' => true]);
-		$data = array(
-			'username' => $this->config['unifiUsername'],
-			'password' => $this->decrypt($this->config['unifiPassword']),
-			'remember' => true,
-			'strict' => true
-		);
 		try {
-			// Is this UnifiOs or Regular
-			$response = Requests::get($url, [], $options);
-			if ($response->success) {
-				$csrfToken = ($response->headers['x-csrf-token']) ?? false;
-				$data = ($csrfToken) ? $data : json_encode($data);
-			} else {
-				$this->setAPIResponse('error', 'Unifi response error - Check URL', 409);
-				return false;
-			}
-			$urlLogin = ($csrfToken) ? $url . '/api/auth/login' : $url . '/api/login';
-			$urlStat = ($csrfToken) ? $url . '/proxy/network/api/s/default/stat/health' : $url . '/api/s/' . $this->config['unifiSiteName'] . '/stat/health';
-			$response = Requests::post($urlLogin, [], $data, $options);
-			if ($response->success) {
-				$cookie['unifises'] = ($response->cookies['unifises']->value) ?? false;
-				$cookie['csrf_token'] = ($response->cookies['csrf_token']->value) ?? false;
-				$cookie['Token'] = ($response->cookies['Token']->value) ?? false;
-				$options['cookies'] = $response->cookies;
-				
-			} else {
-				$this->setAPIResponse('error', 'Unifi response error - Check Credentials', 409);
-				return false;
-			}
-			$headers = array(
-				'cookie' => 'unifises=' . $cookie['unifises'] . ';' . 'csrf_token=' . $cookie['csrf_token'] . ';'
-			);
-			$response = Requests::get($urlStat, $headers, $options);
-			if ($response->success) {
-				$api['content']['unifi'] = json_decode($response->body, true);
-			} else {
-				$this->setAPIResponse('error', 'Unifi response error3', 409);
-				return false;
+			$api['content']['unifi'] = array();
+			$login = $this->unifiLogin();
+			if ($login) {
+				$url = $this->qualifyURL($this->config['unifiURL']);
+				$unifiOS = $login['unifiOS'];
+				$headers = ($unifiOS) ? ['x-csrf-token' => $unifiOS] : [];
+				$urlStat = ($unifiOS) ? $url . '/proxy/network/api/s/default/stat/health' : $url . '/api/s/' . $this->config['unifiSiteName'] . '/stat/health';
+				$response = Requests::get($urlStat, $headers, $login['options']);
+				if ($response->success) {
+					$api['content']['unifi'] = json_decode($response->body, true);
+				} else {
+					$this->setAPIResponse('error', 'Unifi response error3', 409);
+					return false;
+				}
 			}
 		} catch (Requests_Exception $e) {
 			$this->writeLog('error', 'Unifi Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
 			$this->setAPIResponse('error', $e->getMessage(), 500);
 			return false;
-		};
-		$api['content']['unifi'] = isset($api['content']['unifi']) ? $api['content']['unifi'] : false;
+		}
+		$api['content']['unifi'] = $api['content']['unifi'] ?? false;
 		$this->setAPIResponse('success', null, 200, $api);
 		return $api;
 	}

+ 213 - 0
api/homepage/utorrent.php

@@ -0,0 +1,213 @@
+<?php
+
+trait uTorrentHomepageItem
+{
+	public function uTorrentSettingsArray($infoOnly = false)
+	{
+		$homepageInformation = [
+			'name' => 'uTorrent',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/utorrent.png',
+			'category' => 'Downloader',
+			'settingsArray' => __FUNCTION__
+		];
+		if ($infoOnly) {
+			return $homepageInformation;
+		}
+		$homepageSettings = [
+			'debug' => true,
+			'settings' => [
+				'Enable' => [
+					$this->settingsOption('enable', 'homepageuTorrentEnabled'),
+					$this->settingsOption('auth', 'homepageuTorrentAuth'),
+				],
+				'Connection' => [
+					$this->settingsOption('url', 'uTorrentURL'),
+					$this->settingsOption('blank'),
+					$this->settingsOption('username', 'uTorrentUsername'),
+					$this->settingsOption('password', 'uTorrentPassword'),
+					$this->settingsOption('disable-cert-check', 'uTorrentDisableCertCheck'),
+					$this->settingsOption('use-custom-certificate', 'uTorrentUseCustomCertificate'),
+				],
+				'Misc Options' => [
+					$this->settingsOption('hide-seeding', 'uTorrentHideSeeding', ['label' => 'Hide Seeding']),
+					$this->settingsOption('hide-completed', 'uTorrentHideCompleted'),
+					$this->settingsOption('refresh', 'uTorrentRefresh'),
+					$this->settingsOption('combine', 'uTorrentCombine'),
+				],
+				'Test Connection' => [
+					$this->settingsOption('blank', null, ['label' => 'Please Save before Testing']),
+					$this->settingsOption('test', 'utorrent'),
+				]
+			]
+		];
+		return array_merge($homepageInformation, $homepageSettings);
+	}
+	
+	public function uTorrentHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageuTorrentEnabled'
+				],
+				'auth' => [
+					'homepageuTorrentAuth'
+				],
+				'not_empty' => [
+					'uTorrentURL'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function testConnectionuTorrent()
+	{
+		if (empty($this->config['uTorrentURL'])) {
+			$this->setAPIResponse('error', 'uTorrent URL is not defined', 422);
+			return false;
+		}
+		try {
+			
+			$response = $this->getuTorrentToken();
+			
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'uTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function homepageOrderuTorrent()
+	{
+		if ($this->homepageItemPermissions($this->uTorrentHomepagePermissions('main'))) {
+			$loadingBox = ($this->config['uTorrentCombine']) ? '' : '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+			$builder = ($this->config['uTorrentCombine']) ? 'buildDownloaderCombined(\'utorrent\');' : '$("#' . __FUNCTION__ . '").html(buildDownloader("utorrent"));';
+			return '
+                                <div id="' . __FUNCTION__ . '">
+                                        ' . $loadingBox . '
+                                        <script>
+                                // homepageOrderuTorrent
+                                ' . $builder . '
+                                homepageDownloader("utorrent", "' . $this->config['uTorrentRefresh'] . '");
+                                // End homepageOrderuTorrent
+                        </script>
+                                </div>
+                                ';
+		}
+	}
+	
+	public function getuTorrentToken()
+	{
+		try {
+			$tokenUrl = '/gui/token.html';
+			$digest = $this->qualifyURL($this->config['uTorrentURL'], true);
+			$url = $digest['scheme'] . '://' . $digest['host'] . $digest['port'] . $digest['path'] . $tokenUrl;
+			$data = array('username' => $this->config['uTorrentUsername'], 'password' => $this->decrypt($this->config['uTorrentPassword']));
+			$options = $this->requestOptions($url, null, $this->config['uTorrentDisableCertCheck'], $this->config['uTorrentUseCustomCertificate']);
+			if ($this->config['uTorrentUsername'] !== '' && $this->decrypt($this->config['uTorrentPassword']) !== '') {
+				$credentials = array('auth' => new Requests_Auth_Basic(array($this->config['uTorrentUsername'], $this->decrypt($this->config['uTorrentPassword']))));
+				$options = array_merge($options, $credentials);
+			}
+			$response = Requests::post($url, [], $data, $options);
+			$doc = new DOMDocument();
+			$doc->loadHTML($response->body);
+			$id = $doc->getElementById('token');
+			$uTorrentConfig = new stdClass();
+			$uTorrentConfig->uTorrentToken = $id->textContent;
+			$reflection = new ReflectionClass($response->cookies);
+			$cookie = $reflection->getProperty("cookies");
+			$cookie->setAccessible(true);
+			$cookie = $cookie->getValue($response->cookies);
+			if ($cookie['GUID']) {
+				$uTorrentConfig->uTorrentCookie = $cookie['GUID']->value;
+			}
+			if ($uTorrentConfig->uTorrentToken || $uTorrentConfig->uTorrentCookie) {
+				$this->updateConfigItems($uTorrentConfig);
+			}
+			
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'uTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+		
+	}
+	
+	public function getuTorrentHomepageQueue()
+	{
+		if (!$this->homepageItemPermissions($this->uTorrentHomepagePermissions('main'), true)) {
+			return false;
+		}
+		try {
+			if (!$this->config['uTorrentToken'] || !$this->config['uTorrentCookie']) {
+				$this->getuTorrentToken();
+			}
+			$queryUrl = '/gui/?token=' . $this->config['uTorrentToken'] . '&list=1';
+			$digest = $this->qualifyURL($this->config['uTorrentURL'], true);
+			$url = $digest['scheme'] . '://' . $digest['host'] . $digest['port'] . $digest['path'] . $queryUrl;
+			$options = $this->requestOptions($url, null, $this->config['uTorrentDisableCertCheck'], $this->config['uTorrentUseCustomCertificate']);
+			if ($this->config['uTorrentUsername'] !== '' && $this->decrypt($this->config['uTorrentPassword']) !== '') {
+				$credentials = array('auth' => new Requests_Auth_Basic(array($this->config['uTorrentUsername'], $this->decrypt($this->config['uTorrentPassword']))));
+				$options = array_merge($options, $credentials);
+			}
+			$headers = array(
+				'Cookie' => 'GUID=' . $this->config['uTorrentCookie']
+			);
+			$response = Requests::get($url, $headers, $options);
+			$httpResponse = $response->status_code;
+			if ($httpResponse == 400) {
+				$this->writeLog('warn', 'uTorrent Token or Cookie Expired. Generating new session..', 'SYSTEM');
+				$this->getuTorrentToken();
+				$response = Requests::get($url, $headers, $options);
+				$httpResponse = $response->status_code;
+			}
+			if ($httpResponse == 200) {
+				$responseData = json_decode($response->body);
+				$keyArray = (array)$responseData->torrents;
+				//Populate values
+				$valueArray = array();
+				foreach ($keyArray as $keyArr) {
+					preg_match('/(?<Status>(\w+\s+)+)(?<Percentage>\d+.\d+.*)/', $keyArr[21], $matches);
+					$Status = str_replace(' ', '', $matches['Status']);
+					if ($this->config['uTorrentHideSeeding'] && $Status == "Seeding") {
+						// Do Nothing
+					} else if ($this->config['uTorrentHideCompleted'] && $Status == "Finished") {
+						// Do Nothing
+					} else {
+						$value = array(
+							'Name' => $keyArr[2],
+							'Labels' => $keyArr[11],
+							'Percent' => str_replace(' ', '', $matches['Percentage']),
+							'Status' => $Status,
+							'Availability' => $keyArr[4],
+							'Done' => $keyArr[5],
+							'Size' => $keyArr[3],
+							'upSpeed' => $keyArr[8],
+							'downSpeed' => $keyArr[9],
+							'Message' => $keyArr[21],
+						);
+						array_push($valueArray, $value);
+					}
+				}
+				$api['content']['queueItems'] = $valueArray;
+				$api['content'] = $api['content'] ?? false;
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'uTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	
+}

+ 1 - 7
api/homepage/weather.php

@@ -57,13 +57,7 @@ trait WeatherHomepageItem
 				]
 			]
 		];
-		if (array_key_exists($key, $permissions)) {
-			return $permissions[$key];
-		} elseif ($key == 'all') {
-			return $permissions;
-		} else {
-			return [];
-		}
+		return $this->homepageCheckKeyPermissions($key, $permissions);
 	}
 	
 	public function homepageOrderWeatherAndAir()

+ 149 - 0
api/pages/error.php

@@ -0,0 +1,149 @@
+<?php
+$GLOBALS['organizrPages'][] = 'error';
+function get_page_error($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	$nonRoot = isset($_GET['organizr']);
+	$nonRootPath = ($nonRoot) ? $Organizr->getRootPath() : '';
+	$error = $_GET['vars']['var1'] ?? 404;
+	$errorDetails = $Organizr->errorCodes($error);
+	$redirect = $_GET['vars']['var2'] ?? null;
+	if ($redirect) {
+		$Organizr->debug($redirect);
+	}
+	$GLOBALS['responseCode'] = 200;
+	return '
+<!DOCTYPE html>
+<html lang="en">
+<head>
+	<meta charset="utf-8">
+	<meta content="IE=edge" http-equiv="X-UA-Compatible">
+	<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
+	<meta content="' . $Organizr->config['description'] . '" name="description">
+	<meta content="CauseFX" name="author">
+	' . $Organizr->favIcons($nonRootPath) . '
+	<title>Error ' . $Organizr->config['title'] . '</title>
+	' . $Organizr->loadResources(
+			[
+				'bootstrap/dist/css/bootstrap.min.css',
+				'css/animate.css',
+				'plugins/bower_components/overlayScrollbars/OverlayScrollbars.min.css',
+				'css/dark.min.css',
+				'css/organizr.min.css',
+				'js/jquery-2.2.4.min.js',
+				'js/jquery-lang.min.js'
+			], $nonRootPath
+		) . '
+	' . $Organizr->setTheme(null, $nonRootPath) . '
+	<style id="user-appearance"></style>
+	<style id="custom-theme-css"></style>
+	<style id="custom-css"></style>
+	<!--[if lt IE 9]>
+	<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"
+			integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY"
+			crossorigin="anonymous"></script>
+	<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"
+			integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo"
+			crossorigin="anonymous"></script>
+	<![endif]-->
+</head>
+<body class="fix-header">
+<!-- ============================================================== -->
+<!-- Preloader -->
+<!-- ==============================================================
+<div id="preloader" class="preloader">
+	<svg class="circular" viewbox="25 25 50 50">
+		<circle class="path" cx="50" cy="50" fill="none" r="20" stroke-miterlimit="10" stroke-width="10"></circle>
+	</svg>
+</div>-->
+<!-- ============================================================== -->
+<!-- Wrapper -->
+<!-- ============================================================== -->
+<section id="wrapper">
+	<div class="error-box">
+		<div class="error-body text-center">
+			<h1 class="text-danger">' . $error . '</h1>
+			<h2 class="text-uppercase" lang="en">' . $errorDetails['type'] . '</h2>
+			<h3 class="text-uppercase" lang="en">' . $errorDetails['description'] . '</h3>
+			<p class="text-muted m-t-30 m-b-30">Hey there, ' . $Organizr->user['username'] . '.  Looks like you tried accessing something that just ain\'t right!  WTF right?! </p>
+			<a href="' . $nonRootPath . '" class="btn btn-danger btn-rounded waves-effect waves-light m-b-40">Back Home</a>
+		</div>
+	</div>
+</section>
+<script>
+languageList = ' . $Organizr->languagePacks(true) . '
+var langStrings = { "token": {} };
+var lang = new Lang();
+loadLanguageList();
+lang.init({
+	currentLang: (getCookie("organizrLanguage")) ? getCookie("organizrLanguage") : "en",
+	cookie: {
+		name: "organizrLanguage",
+		expiry: 365,
+		path: "/"
+	},
+	allowCookieOverride: true
+});
+
+$.urlParam = function(name){
+	let results = new RegExp("[\?&]" + name + "=([^&#]*)").exec(window.location.href);
+	if (results == null) {
+		return null;
+	} else {
+		return decodeURI(results[1]) || 0;
+	}
+};
+if ($.urlParam("return") !== null && "' . $Organizr->user['groupID'] . '" === "999") {
+	local("set", "uri", $.urlParam("return"));
+}
+function localStorageSupport() {
+	return (("localStorage" in window) && window["localStorage"] !== null)
+}
+function local(type,key,value=null){
+	if (localStorageSupport) {
+		switch (type) {
+			case "set":
+			case "s":
+				localStorage.setItem(key,value);
+				break;
+			case "get":
+			case "g":
+				return localStorage.getItem(key);
+				break;
+			case "remove":
+			case "r":
+				localStorage.removeItem(key);
+				break;
+		}
+	}
+}
+function loadLanguageList(){
+	$.each(languageList, function(i,v) {
+		lang.dynamic(v.code, "' . $nonRootPath . 'js/langpack/"+v.filename);
+	});
+}
+function getCookie(cname) {
+	var name = cname + "=";
+	var decodedCookie = decodeURIComponent(document.cookie);
+	var ca = decodedCookie.split(";");
+	for(var i = 0; i <ca.length; i++) {
+		var c = ca[i];
+		while (c.charAt(0) == " ") {
+			c = c.substring(1);
+		}
+		if (c.indexOf(name) == 0) {
+			return c.substring(name.length, c.length);
+		}
+	}
+	return "";
+}
+</script>
+</body>
+</html>
+';
+}

+ 2 - 2
api/pages/settings-image-manager.php

@@ -28,7 +28,7 @@ function get_page_settings_image_manager($Organizr)
 <div class="panel bg-org panel-info">
     <div class="panel-heading">
 		<span lang="en">View Images</span>
-        <button type="button" class="btn btn-success btn-circle pull-right popup-with-form m-r-5" href="#new-image-form" data-effect="mfp-3d-unfold"><i class="fa fa-upload"></i> </button>
+        <button type="button" class="btn btn-info btn-circle pull-right popup-with-form m-r-5" href="#new-image-form" data-effect="mfp-3d-unfold"><i class="fa fa-upload"></i> </button>
 	</div>
     <div class="panel-wrapper collapse in" aria-expanded="true">
         <div class="panel-body bg-org" >
@@ -46,4 +46,4 @@ function get_page_settings_image_manager($Organizr)
     <div class="clearfix"></div>
 </form>
 ';
-}
+}

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

@@ -36,7 +36,7 @@ function get_page_settings_settings_logs($Organizr)
         <button type="button" class="btn btn-default btn-outline waves-effect bg-org swapLog active" data-name="loginLog" data-path="' . $Organizr->organizrLoginLog . '" lang="en">Login Log</button>
         <button type="button" class="btn btn-default btn-outline waves-effect bg-org swapLog" data-name="orgLog" data-path="' . $Organizr->organizrLog . '" lang="en">Organizr Log</button>
     </div>
-    <button class="btn btn-danger btn-sm waves-effect waves-light pull-right purgeLog" type="button"><span class="btn-label"><i class="fa fa-trash"></i></span>Purge Log</button>
+    <button class="btn btn-danger btn-sm waves-effect waves-light pull-right purgeLog" type="button"><span class="btn-label"><i class="fa fa-trash"></i></span><span lang="en">Purge Log</span></button>
     <div class="clearfix"></div>
     <div class="white-box bg-org logTable loginLogDiv">
         <h3 class="box-title m-b-0" lang="en">Login Logs</h3>
@@ -172,4 +172,4 @@ function get_page_settings_settings_logs($Organizr)
     } );
     </script>
     ';
-}
+}

+ 7 - 7
api/pages/settings-tab-editor-categories.php

@@ -67,22 +67,22 @@ function get_page_settings_tab_editor_categories($Organizr)
 	return '
 <script>
 buildCategoryEditor();
-$( \'#categoryEditorTable\' ).sortable({
-	stop: function () {
-		var inputs = $(\'input.order\');
-		var nbElems = inputs.length;
-		inputs.each(function(idx) {
+
+let el = document.getElementById(\'categoryEditorTable\');
+let sortable = new Sortable(el, {
+	onUpdate: function (evt) {
+		$(\'input.order\').each(function(idx) {
 			$(this).val(idx + 1);
 		});
 		submitCategoryOrder();
-	}
+	},
 });
 ' . $iconSelectors . '
 </script>
 <div class="panel bg-org panel-info">
 	<div class="panel-heading">
 		<span lang="en">Category Editor</span>
-		<button type="button" class="btn btn-success btn-circle pull-right popup-with-form m-r-5" href="#new-category-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
+		<button type="button" class="btn btn-info btn-circle pull-right popup-with-form m-r-5" href="#new-category-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
 	</div>
 	<div class="table-responsive">
 		<form id="submit-categories-form" onsubmit="return false;">

+ 13 - 10
api/pages/settings-tab-editor-tabs.php

@@ -67,34 +67,37 @@ function get_page_settings_tab_editor_tabs($Organizr)
 	return '
 	<script>
 	buildTabEditor();
-	!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);
-	$( \'#tabEditorTable\' ).sortable({
-		stop: function () {
+	let el = document.getElementById(\'tabEditorTable\');
+	let tabSorter = new Sortable(el, {
+		handle: ".sort-tabs-handle",
+		ghostClass: "sortable-ghost",
+		multiDrag: true,
+		selectedClass: "multi-selected",
+		onUpdate: function (evt) {
 			$(\'input.order\').each(function(idx) {
 				$(this).val(idx + 1);
 			});
 			var newTabs = $( "#submit-tabs-form" ).serializeToJSON();
 			newTabsGlobal = newTabs;
 			$(\'.saveTabOrderButton\').removeClass(\'hidden\');
-			//submitTabOrder(newTabs);
-		}
+		},
 	});
-	$( \'#tabEditorTable\' ).disableSelection();
 	' . $iconSelectors . '
 	</script>
 	<div class="panel bg-org panel-info">
 		<div class="panel-heading">
 			<span lang="en">Tab Editor</span>
-			<button type="button" class="btn btn-info btn-circle pull-right popup-with-form m-r-5" href="#new-tab-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
-			<button type="button" class="btn btn-info btn-circle pull-right m-r-5 help-modal" data-modal="tabs"><i class="fa fa-question-circle"></i> </button>
-			<button onclick="submitTabOrder(newTabsGlobal)" class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right animated loop-animation rubberBand m-r-20 saveTabOrderButton hidden" type="button"><span class="btn-label"><i class="fa fa-save"></i></span><span lang="en">Save Tab Order</span></button>
+			<button type="button" data-toggle="tooltip" title="Add New Tab" data-placement="bottom" class="btn btn-info btn-circle pull-right popup-with-form m-r-5" href="#new-tab-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
+			<button type="button" data-toggle="tooltip" title="Help" data-placement="bottom" class="btn btn-info btn-circle pull-right m-r-5 help-modal" data-modal="tabs"><i class="fa fa-question-circle"></i> </button>
+			<button onclick="submitTabOrder(newTabsGlobal)" data-toggle="tooltip" title="Save tab Order" data-placement="bottom" class="btn btn-info btn-circle waves-effect waves-light pull-right animated loop-animation rubberBand m-r-5 saveTabOrderButton hidden" type="button"><i class="fa fa-save"></i></button>
 		</div>
 		<div class="table-responsive">
 			<form id="submit-tabs-form" onsubmit="return false;">
 				<table class="table table-hover manage-u-table">
 					<thead>
 						<tr>
-							<th width="70" class="text-center">#</th>
+							<th width="20" class="text-center"></th>
+							<th width="70" class="text-center"></th>
 							<th lang="en">NAME</th>
 							<th lang="en">CATEGORY</th>
 							<th lang="en">GROUP</th>

+ 132 - 46
api/pages/settings-user-manage-groups.php

@@ -11,59 +11,145 @@ function get_page_settings_user_manage_groups($Organizr)
 	if (!$Organizr->qualifyRequest(1, true)) {
 		return false;
 	}
+	$iconSelectors = '
+		$(".groupIconIconList").select2({
+			ajax: {
+				url: \'api/v2/icon\',
+				data: function (params) {
+					var query = {
+						search: params.term,
+						page: params.page || 1
+					}
+					return query;
+				},
+				processResults: function (data, params) {
+					params.page = params.page || 1;
+					return {
+						results: data.response.data.results,
+						pagination: {
+							more: (params.page * 20) < data.response.data.total
+						}
+					};
+				},
+				//cache: true
+			},
+			placeholder: \'Search for an icon\',
+			templateResult: formatIcon,
+			templateSelection: formatIcon
+		});
+		
+		$(".groupIconImageList").select2({
+			 ajax: {
+				url: \'api/v2/image/select\',
+				data: function (params) {
+					var query = {
+						search: params.term,
+						page: params.page || 1
+					}
+					return query;
+				},
+				processResults: function (data, params) {
+					params.page = params.page || 1;
+					return {
+						results: data.response.data.results,
+						pagination: {
+							more: (params.page * 20) < data.response.data.total
+						}
+					};
+				},
+				//cache: true
+			},
+			placeholder: \'Search for an image\',
+			templateResult: formatImage,
+			templateSelection: formatImage
+		});
+	';
 	return '
 <script>
-    buildGroupManagement();
+	buildGroupManagement();
+	' . $iconSelectors . '
 </script>
 <div class="panel bg-org panel-info">
-    <div class="panel-heading">
-        <span lang="en">MANAGE GROUPS</span>
-        <button type="button" class="btn btn-info btn-circle pull-right popup-with-form" href="#new-group-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
-    </div>
-    <div class="table-responsive">
-        <table class="table table-hover manage-u-table">
-            <thead>
-                <tr>
-                    <th width="70" class="text-center">#</th>
-                    <th lang="en">GROUP NAME</th>
-                    <th lang="en">USERS</th>
-                    <th lang="en">DEFAULT</th>
-                    <th lang="en">EDIT</th>
-                    <th lang="en">DELETE</th>
-                </tr>
-            </thead>
-            <tbody id="manageGroupTable"></tbody>
-        </table>
-    </div>
+	<div class="panel-heading">
+		<span lang="en">MANAGE GROUPS</span>
+		<button type="button" class="btn btn-info btn-circle pull-right popup-with-form" href="#new-group-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
+	</div>
+	<div class="table-responsive">
+		<table class="table table-hover manage-u-table">
+			<thead>
+				<tr>
+					<th width="70" class="text-center">#</th>
+					<th lang="en">GROUP NAME</th>
+					<th lang="en">USERS</th>
+					<th lang="en">DEFAULT</th>
+					<th lang="en">EDIT</th>
+					<th lang="en">DELETE</th>
+				</tr>
+			</thead>
+			<tbody id="manageGroupTable"></tbody>
+		</table>
+	</div>
 </div>
 <form id="new-group-form" class="mfp-hide white-popup-block mfp-with-anim">
-    <h1 lang="en">Add New Group</h1>
-    <fieldset style="border:0;">
-        <div class="form-group">
-            <label class="control-label" for="new-group-form-inputName" lang="en">Group Name</label>
-            <input type="text" class="form-control" id="new-group-form-inputName" name="group" required="" autofocus> </div>
-        <div class="form-group">
-            <label class="control-label" for="new-group-form-inputImage" lang="en">Group Image</label>
-            <input type="text" class="form-control" id="new-group-form-inputImage" name="image" required=""> </div>
-    </fieldset>
-    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none addNewGroup" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Add Group</span></button>
-    <div class="clearfix"></div>
+	<h1 lang="en">Add New Group</h1>
+	<fieldset style="border:0;">
+		<div class="form-group">
+			<label class="control-label" for="new-group-form-inputName" lang="en">Group Name</label>
+			<input type="text" class="form-control" id="new-group-form-inputName" name="group" required="" autofocus> </div>
+			<div class="row">
+				<div class="form-group col-lg-4">
+					<label class="control-label" for="new-group-form-chooseImage" lang="en">Choose Image</label>
+					<select class="form-control groupIconImageList" id="new-group-form-chooseImage" name="chooseImage"><option lang="en">Select or type Image</option></select>
+				</div>
+				<div class="form-group col-lg-4">
+					<label class="control-label" for="new-group-form-chooseIcon" lang="en">Choose Icon</label>
+					<select class="form-control groupIconIconList" id="new-group-form-chooseIcon" name="chooseIcon"><option lang="en">Select or type Icon</option></select>
+				</div>
+				<div class="form-group col-lg-4">
+					<label class="control-label" for="new-group-form-chooseBlackberry" lang="en">Choose Blackberry Theme Icon</label>
+					<button id="new-group-form-chooseBlackberry" class="btn btn-xs btn-primary waves-effect waves-light form-control" onclick="showBlackberryThemes(\'new-group-form-inputImage\');" type="button">
+						<i class="fa fa-search"></i>&nbsp; <span lang="en">Choose</span>
+					</button>
+				</div>
+			</div>
+		<div class="form-group">
+			<label class="control-label" for="new-group-form-inputImage" lang="en">Group Image</label>
+			<input type="text" class="form-control" id="new-group-form-inputImageNew" name="image" required=""> </div>
+	</fieldset>
+	<button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none addNewGroup" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Add Group</span></button>
+	<div class="clearfix"></div>
 </form>
 <form id="edit-group-form" class="mfp-hide white-popup-block mfp-with-anim">
-    <input type="hidden" name="id" value="x">
-    <h1 lang="en">Edit Group</h1>
-    <fieldset style="border:0;">
-        <div class="form-group">
-            <label class="control-label" for="edit-group-form-inputEditGroupName" lang="en">Group Name</label>
-            <input type="text" class="form-control" id="edit-group-form-inputEditGroupName" name="group" required="" autofocus>
-        </div>
-        <div class="form-group">
-            <label class="control-label" for="edit-group-form-inputEditGroupImage" lang="en">Group Image</label>
-            <input type="text" class="form-control" id="edit-group-form-inputEditGroupImage" name="image"  required="">
-        </div>
-    </fieldset>
-    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none editGroup" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Edit Group</span></button>
-    <div class="clearfix"></div>
+	<input type="hidden" name="id" value="x">
+	<h1 lang="en">Edit Group</h1>
+	<fieldset style="border:0;">
+		<div class="form-group">
+			<label class="control-label" for="edit-group-form-inputEditGroupName" lang="en">Group Name</label>
+			<input type="text" class="form-control" id="edit-group-form-inputEditGroupName" name="group" required="" autofocus>
+		</div>
+		<div class="row">
+			<div class="form-group col-lg-4">
+				<label class="control-label" for="edit-group-form-chooseImage" lang="en">Choose Image</label>
+				<select class="form-control groupIconImageList" id="edit-group-form-chooseImage" name="chooseImage"><option lang="en">Select or type Image</option></select>
+			</div>
+			<div class="form-group col-lg-4">
+				<label class="control-label" for="edit-group-form-chooseIcon" lang="en">Choose Icon</label>
+				<select class="form-control groupIconIconList" id="edit-group-form-chooseIcon" name="chooseIcon"><option lang="en">Select or type Icon</option></select>
+			</div>
+			<div class="form-group col-lg-4">
+				<label class="control-label" for="edit-group-form-chooseBlackberry" lang="en">Choose Blackberry Theme Icon</label>
+				<button id="edit-group-form-chooseBlackberry" class="btn btn-xs btn-primary waves-effect waves-light form-control" onclick="showBlackberryThemes(\'edit-group-form-inputImage\');" type="button">
+					<i class="fa fa-search"></i>&nbsp; <span lang="en">Choose</span>
+				</button>
+			</div>
+		</div>
+		<div class="form-group">
+			<label class="control-label" for="edit-group-form-inputImage" lang="en">Group Image</label>
+			<input type="text" class="form-control" id="edit-group-form-inputImage" name="image"  required="">
+		</div>
+	</fieldset>
+	<button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none editGroup" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Edit Group</span></button>
+	<div class="clearfix"></div>
 </form>
 ';
-}
+}

+ 245 - 244
api/pages/settings-user-manage-users.php

@@ -14,182 +14,183 @@ function get_page_settings_user_manage_users($Organizr)
 	return '
 <script>
 	$(document).ready(function($) {
-        
-    }), jsGrid.setDefaults({
-        tableClass: "jsgrid-table table table-striped table-hover"
-    }), jsGrid.setDefaults("text", {
-        _createTextBox: function() {
-            return $("<input>").attr("type", "text").attr("class", "form-control input-md")
-        }
-    }), jsGrid.setDefaults("number", {
-        _createTextBox: function() {
-            return $("<input>").attr("type", "number").attr("class", "form-control input-md")
-        }
-    }), jsGrid.setDefaults("textarea", {
-        _createTextBox: function() {
-            return $("<input>").attr("type", "textarea").attr("class", "form-control")
-        }
-    }), jsGrid.setDefaults("control", {
-        _createGridButton: function(cls, tooltip, clickHandler) {
-            var grid = this._grid;
-            return $("<button>").addClass(this.buttonClass).addClass(cls).attr({
-                type: "button",
-                title: tooltip
-            }).on("click", function(e) {
-                clickHandler(grid, e)
-            })
-        }
-    }), jsGrid.setDefaults("select", {
-        _createSelect: function() {
-            var $result = $("<select>").attr("class", "form-control input-md"),
-                valueField = this.valueField,
-                textField = this.textField,
-                selectedIndex = this.selectedIndex;
-            return $.each(this.items, function(index, item) {
-                var value = valueField ? item[valueField] : index,
-                    text = textField ? item[textField] : item,
-                    $option = $("<option>").attr("value", value).text(text).appendTo($result);
-                $option.prop("selected", selectedIndex === index)
-            }), $result
-        }
-    });
+	
+	}), jsGrid.setDefaults({
+		tableClass: "jsgrid-table table table-striped table-hover"
+	}), jsGrid.setDefaults("text", {
+		_createTextBox: function() {
+			return $("<input>").attr("type", "text").attr("class", "form-control input-md")
+		}
+	}), jsGrid.setDefaults("number", {
+		_createTextBox: function() {
+			return $("<input>").attr("type", "number").attr("class", "form-control input-md")
+		}
+	}), jsGrid.setDefaults("textarea", {
+		_createTextBox: function() {
+			return $("<input>").attr("type", "textarea").attr("class", "form-control")
+		}
+	}), jsGrid.setDefaults("control", {
+		_createGridButton: function(cls, tooltip, clickHandler) {
+			var grid = this._grid;
+			return $("<button>").addClass(this.buttonClass).addClass(cls).attr({
+				type: "button",
+				title: tooltip
+			}).on("click", function(e) {
+				clickHandler(grid, e)
+			})
+		}
+	}), jsGrid.setDefaults("select", {
+		_createSelect: function() {
+			var $result = $("<select>").attr("class", "form-control input-md"),
+				valueField = this.valueField,
+				textField = this.textField,
+				selectedIndex = this.selectedIndex;
+			return $.each(this.items, function(index, item) {
+				var value = valueField ? item[valueField] : index,
+					text = textField ? item[textField] : item,
+					$option = $("<option>").attr("value", value).text(text).appendTo($result);
+				$option.prop("selected", selectedIndex === index)
+			}), $result
+		}
+	});
 	$(function() {
 		pageLength = 10;
 		function onPageSelect(newPageLength) {
-            pageLength = newPageLength;
-            $("#jsGrid-Users").jsGrid("changePageSize", pageLength);
-        }
-        $("#pageLength").on("change", function() {
-            onPageSelect(this.value);
+			pageLength = newPageLength;
+			$("#jsGrid-Users").jsGrid("changePageSize", pageLength);
+		}
+		$("#pageLength").on("change", function() {
+			onPageSelect(this.value);
 		});
-	    $("#jsGrid-Users").jsGrid({
-	        height: "auto",
-	        width: "100%",
-	 		loadIndication: true,
-		    loadIndicationDelay: 50000,
-		    loadMessage: "Please, wait...",
-		    loadShading: true,
-		    noDataContent: "Loading... or Not found",
-		    loadShading: true,
-	        filtering: false,
-	        editing: true,
-	        sorting: true,
-	        paging: true,
-	        autoload: true,
-	        selecting: true,
-	 		confirmDeleting: false,
-	        pageSize: pageLength,
-	        changePageSize: function (pageSize) {
-                var $this = this;
-                let totalUsers = $this.data.length;
-                let totalPages = Math.ceil(totalUsers / pageSize);
-                if($this.pageIndex > totalPages){
-                    $("#jsGrid-Users").jsGrid("openPage", totalPages);
-                }
-                $this.pageSize = pageLength;
-                $this.refresh();
-            },
-	        pageButtonCount: 5,
-        	pagerFormat: "&nbsp;&nbsp; {first} {prev} {pages} {next} {last} &nbsp;&nbsp;",
-	        controller: {
-	            loadData: function() {
-	                let d = $.Deferred();
-	                $.ajax({
-	                    url: "api/v2/users?includeGroups",
-	                    dataType: "json"
-	                }).done(function(response) {
-	                	let groupObj = response.response.data.groups;
-	                	$("#jsGrid-Users").jsGrid("fieldOption", "group_id", "items", groupObj);
-	                    d.resolve(response.response.data.users);
-	                });
-	                return d.promise();
-	            }
-	        },
+		$("#jsGrid-Users").jsGrid({
+			height: "auto",
+			width: "100%",
+			loadIndication: true,
+			loadIndicationDelay: 50000,
+			loadMessage: "Please, wait...",
+			loadShading: true,
+			noDataContent: "Loading... or Not found",
+			loadShading: true,
+			filtering: false,
+			editing: true,
+			sorting: true,
+			paging: true,
+			autoload: true,
+			selecting: true,
+			confirmDeleting: false,
+			pageSize: pageLength,
+			changePageSize: function (pageSize) {
+				var $this = this;
+				let totalUsers = $this.data.length;
+				let totalPages = Math.ceil(totalUsers / pageSize);
+				if($this.pageIndex > totalPages){
+					$("#jsGrid-Users").jsGrid("openPage", totalPages);
+				}
+				$this.pageSize = pageLength;
+				$this.refresh();
+			},
+			pageButtonCount: 5,
+			pagerFormat: "&nbsp;&nbsp; {first} {prev} {pages} {next} {last} &nbsp;&nbsp;",
+			controller: {
+				loadData: function() {
+					let d = $.Deferred();
+					$.ajax({
+						url: "api/v2/users?includeGroups",
+						dataType: "json"
+					}).done(function(response) {
+						let groupObj = response.response.data.groups;
+						$("#jsGrid-Users").jsGrid("fieldOption", "group_id", "items", groupObj);
+						d.resolve(response.response.data.users);
+					});
+					return d.promise();
+				}
+			},
 	 
-	        fields: [
-	        	{ name: "image", title: "Avatar", type: "text", width: 45, css: "text-center hidden-xs", filtering: false, sorting:false, validate: "required",
-	                itemTemplate: function(value) {
-	                    return \'<img alt="user-img" class="img-circle" src="\'+value+\'" width="45">\'; }
-	            },
-	            { name: "username", type: "text", title: "Username", validate: "required", width: 150},
-	            { name: "email", type: "text", title: "Email", validate: "required", width: 200},
-	            { name: "register_date", type: "text", title: "Date Registered",editing: false, css: "hidden-xs",
-	            	itemTemplate: function(value) {
-	                    return moment(value).format(\'ll\') + \' \' + moment(value).format(\'LT\') },
-	            },
-	            { name: "group_id", type: "select", title: "Group", validate: "required",
-	            	items: [],
-				    valueField: "group_id",
-				    textField: "group"
-	            },
-	            { name: "locked", title: "Locked", type: "select", width: 45, validate: "required",
-	            	itemTemplate: function(value) {
-	                    return (value == 0 || value == null || value == "" || value == " ") ? "No" : "Yes"; },
-	                items: [
-	                	{ Name: "No", Id: 0 },
-         				{ Name: "Yes", Id: 1 },
-	                ],
-				    valueField: "Id",
-    				textField: "Name"
-    				
-	            },
-	            { name: "password", type: "text", title: "Password", css: "text-center", filtering: false, sorting:false,
-	                itemTemplate: function(value) {
-	                    return "<i class=\"mdi mdi-account-key\"></i>"; },
-	                
-	            	editTemplate: function(item, value) {
-	            	var $result = jsGrid.fields.text.prototype.editTemplate.apply(this, arguments);
-	            	$result.attr("placeholder", "Enter new password");
-	            	this.editControl[0].value = "";
-	                return $result; },
-	            },
-	            { type: "control", modeSwitchButton: false, editButton: false, title: "Action",
-		             headerTemplate: function() {
-	                    return "Action";
-	                }
-	             }
-	        ],
-		    onItemDeleting: function(args) {
-		        if(args.item.protected) {
-		            args.cancel = true;
-		        }
-		        args.cancel = true;
-		        let id = args.item.id;
-		        swal({
-			        title: window.lang.translate("Delete ")+args.item.username+"?",
-			        icon: "warning",
-			        buttons: {
-			            cancel: window.lang.translate("No"),
-			            confirm: window.lang.translate("Yes"),
-			        },
-			        dangerMode: true,
-			        className: "bg-org",
-			        confirmButtonColor: "#DD6B55"
-			    }).then(function(willDelete) {
-			        if (willDelete) {
-				        organizrAPI2("DELETE","api/v2/users/" + id, null,true).success(function(data) {
-					        $("#jsGrid-Users").jsGrid("render");
-				        	message("User Deleted","",activeInfo.settings.notifications.position,"#FFF","success","5000");
-				        }).fail(function(xhr) {
-					        message("User Deleted Error", xhr.responseJSON.response.message, activeInfo.settings.notifications.position, "#FFF", "error", "10000");
-					        console.error("Organizr Function: API Connection Failed");
-				        });
+			fields: [
+				{ name: "image", title: window.lang.translate("Avatar"), type: "text", width: 45, css: "text-center hidden-xs", filtering: false, sorting:false, validate: "required",
+					itemTemplate: function(value) {
+						return \'<img alt="user-img" class="img-circle" src="\'+value+\'" width="45">\';
+						}
+				},
+				{ name: "username", type: "text", title: window.lang.translate("Username"), validate: "required", width: 150},
+				{ name: "email", type: "text", title: window.lang.translate("Email"), validate: "required", width: 200},
+				{ name: "register_date", type: "text", title: window.lang.translate("Date Registered"),editing: false, css: "hidden-xs",
+					itemTemplate: function(value) {
+						return moment(value).format(\'ll\') + \' \' + moment(value).format(\'LT\') },
+				},
+				{ name: "group_id", type: "select", title: window.lang.translate("Group"), validate: "required",
+					items: [],
+					valueField: "group_id",
+					textField: "group"
+				},
+				{ name: "locked", title: window.lang.translate("Locked"), type: "select", width: 45, validate: "required",
+					itemTemplate: function(value) {
+						return (value == 0 || value == null || value == "" || value == " ") ? "No" : "Yes"; },
+					items: [
+						{ Name: window.lang.translate("No"), Id: 0 },
+						{ Name: window.lang.translate("Yes"), Id: 1 },
+					],
+					valueField: "Id",
+					textField: "Name"
+					
+				},
+				{ name: "password", type: "text", title: window.lang.translate("Password"), css: "text-center", filtering: false, sorting:false,
+					itemTemplate: function(value) {
+						return "<i class=\"mdi mdi-account-key\"></i>"; },
+					
+					editTemplate: function(item, value) {
+					var $result = jsGrid.fields.text.prototype.editTemplate.apply(this, arguments);
+					$result.attr("placeholder", "Enter new password");
+					this.editControl[0].value = "";
+					return $result; },
+				},
+				{ type: "control", modeSwitchButton: false, editButton: false, title: window.lang.translate("Action"),
+					 headerTemplate: function() {
+						return "Action";
+					}
+				 }
+			],
+			onItemDeleting: function(args) {
+				if(args.item.protected) {
+					args.cancel = true;
+				}
+				args.cancel = true;
+				let id = args.item.id;
+				swal({
+					title: window.lang.translate("Delete ")+args.item.username+"?",
+					icon: "warning",
+					buttons: {
+						cancel: window.lang.translate("No"),
+						confirm: window.lang.translate("Yes"),
+					},
+					dangerMode: true,
+					className: "bg-org",
+					confirmButtonColor: "#DD6B55"
+				}).then(function(willDelete) {
+					if (willDelete) {
+						organizrAPI2("DELETE","api/v2/users/" + id, null,true).success(function(data) {
+							$("#jsGrid-Users").jsGrid("render");
+							message("User Deleted","",activeInfo.settings.notifications.position,"#FFF","success","5000");
+						}).fail(function(xhr) {
+							message("User Deleted Error", xhr.responseJSON.response.message, activeInfo.settings.notifications.position, "#FFF", "error", "10000");
+							console.error("Organizr Function: API Connection Failed");
+						});
 					}
 				});
-		    },
-		    onItemUpdating: function(args) {
-		        if(typeof args.item.id == "undefined"){
-		        	args.cancel = true;
-		            alert("Could not get ID");
-		        }
-		        let diff = objDiff(args.previousItem,args.item);
-		        if(typeof diff.password !== "undefined"){
-		            if(diff.password === ""){
-		                delete diff["password"];
-		            }
-		        }
-		        let id = args.item.id;
-		        organizrAPI2("PUT","api/v2/users/" + id, diff,true).success(function(data) {
+			},
+			onItemUpdating: function(args) {
+				if(typeof args.item.id == "undefined"){
+					args.cancel = true;
+					alert("Could not get ID");
+				}
+				let diff = objDiff(args.previousItem,args.item);
+				if(typeof diff.password !== "undefined"){
+					if(diff.password === ""){
+						delete diff["password"];
+					}
+				}
+				let id = args.item.id;
+				organizrAPI2("PUT","api/v2/users/" + id, diff,true).success(function(data) {
 					try {
 						let response = data.response;
 						$("#jsGrid-Users").jsGrid("render");
@@ -204,10 +205,10 @@ function get_page_settings_user_manage_users($Organizr)
 					message("User Error", xhr.responseJSON.response.message, activeInfo.settings.notifications.position,"#FFF","error","10000");
 					console.error("Organizr Function: API Connection Failed");
 				});
-		    },
-		    
-		    onRefreshed: function(){
-		    
+			},
+			
+			onRefreshed: function(){
+			
 				$(".jsgrid-pager").addClass( "pull-right" );
 				$(".jsgrid-pager").find(".jsgrid-pager-page a").addClass( "btn btn-info" );
 				$(".jsgrid-pager").find(".jsgrid-pager-nav-button a").addClass( "btn btn-info" );
@@ -219,88 +220,88 @@ function get_page_settings_user_manage_users($Organizr)
 					}
 				})
 			}
-	    });
-	    
+		});
+		
 	});
 </script>
 <div class="panel bg-org panel-info">
-    <div class="panel-heading">
-        <span lang="en">MANAGE USERS</span>
-        <button type="button" class="btn btn-info btn-circle pull-right popup-with-form" href="#new-user-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
-        <div id="pageDiv" class="hidden-xs">
+	<div class="panel-heading">
+		<span lang="en">MANAGE USERS</span>
+		<button type="button" class="btn btn-info btn-circle pull-right popup-with-form" href="#new-user-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
+		<div id="pageDiv" class="hidden-xs">
 			<div class="item-pager-panel pull-right m-r-10">
-			        <select id="pageLength" class="form-control">
-			            <option>5</option>
-			            <option selected="">10</option>
-			            <option>15</option>
-			            <option>30</option>
-			            <option>60</option>
-			            <option>180</option>
-			        </select>
+					<select id="pageLength" class="form-control">
+						<option>5</option>
+						<option selected="">10</option>
+						<option>15</option>
+						<option>30</option>
+						<option>60</option>
+						<option>180</option>
+					</select>
 			</div>
 		</div>
-    </div>
-    <div id="jsGrid-Users" class=""></div>
+	</div>
+	<div id="jsGrid-Users" class=""></div>
 	<div class="clearfix"></div>
-    <div class="table-responsive hidden">
-        <table class="table table-hover manage-u-table">
-            <thead>
-                <tr>
-                    <th width="70" class="text-center">#</th>
-                    <th lang="en">NAME & EMAIL</th>
-                    <th lang="en">ADDED</th>
-                    <th lang="en">GROUP</th>
-                    <th lang="en">EDIT</th>
-                    <th lang="en">EMAIL</th>
-                    <th lang="en">DELETE</th>
-                </tr>
-            </thead>
-            <tbody id="manageUserTable"></tbody>
-        </table>
-    </div>
+	<div class="table-responsive hidden">
+		<table class="table table-hover manage-u-table">
+			<thead>
+				<tr>
+					<th width="70" class="text-center">#</th>
+					<th lang="en">NAME & EMAIL</th>
+					<th lang="en">ADDED</th>
+					<th lang="en">GROUP</th>
+					<th lang="en">EDIT</th>
+					<th lang="en">EMAIL</th>
+					<th lang="en">DELETE</th>
+				</tr>
+			</thead>
+			<tbody id="manageUserTable"></tbody>
+		</table>
+	</div>
 </div>
 <form id="new-user-form" class="mfp-hide white-popup-block mfp-with-anim">
-    <h1 lang="en">Add New User</h1>
-    <fieldset style="border:0;">
-        <div class="form-group">
-            <label class="control-label" for="new-user-form-inputUsername" lang="en">Username</label>
-            <input type="text" class="form-control" id="new-user-form-inputUsername" name="username" required="" autofocus>
-        </div>
-        <div class="form-group">
-            <label class="control-label" for="new-user-form-inputEmail" lang="en">Email</label>
-            <input type="email" class="form-control" id="new-user-form-inputEmail" name="email"  required="">
-        </div>
-        <div class="form-group">
-            <label class="control-label" for="new-user-form-inputPassword" lang="en">Password</label>
-            <input type="password" class="form-control" id="new-user-form-inputPassword" name="password"  required="">
-        </div>
-    </fieldset>
-    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none addNewUser" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Add User</span></button>
-    <div class="clearfix"></div>
+	<h1 lang="en">Add New User</h1>
+	<fieldset style="border:0;">
+		<div class="form-group">
+			<label class="control-label" for="new-user-form-inputUsername" lang="en">Username</label>
+			<input type="text" class="form-control" id="new-user-form-inputUsername" name="username" required="" autofocus>
+		</div>
+		<div class="form-group">
+			<label class="control-label" for="new-user-form-inputEmail" lang="en">Email</label>
+			<input type="email" class="form-control" id="new-user-form-inputEmail" name="email"  required="">
+		</div>
+		<div class="form-group">
+			<label class="control-label" for="new-user-form-inputPassword" lang="en">Password</label>
+			<input type="password" class="form-control" id="new-user-form-inputPassword" name="password"  required="">
+		</div>
+	</fieldset>
+	<button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none addNewUser" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Add User</span></button>
+	<div class="clearfix"></div>
 </form>
 <form id="edit-user-form" class="mfp-hide white-popup-block mfp-with-anim">
-    <input type="hidden" name="id" value="">
-    <h1 lang="en">Edit User</h1>
-    <fieldset style="border:0;">
-        <div class="form-group">
-            <label class="control-label" for="edit-user-form-inputUsername" lang="en">Username</label>
-            <input type="text" class="form-control" id="edit-user-form-inputUsername" name="username" required="" autofocus>
-        </div>
-        <div class="form-group">
-            <label class="control-label" for="edit-user-form-inputEmail" lang="en">Email</label>
-            <input type="text" class="form-control" id="edit-user-form-inputEmail" name="email" required="" autofocus>
-        </div>
-        <div class="form-group">
-            <label class="control-label" for="edit-user-form-inputPassword" lang="en">Password</label>
-            <input type="password" class="form-control" id="edit-user-form-inputPassword" name="password"  required="">
-        </div>
-        <div class="form-group">
-            <label class="control-label" for="edit-user-form-inputPassword2" lang="en">Password Again</label>
-            <input type="password" class="form-control" id="edit-user-form-inputPassword2" name="password2"  required="">
-        </div>
-    </fieldset>
-    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none editUserAdmin" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Edit User</span></button>
-    <div class="clearfix"></div>
+	<input type="hidden" name="id" value="">
+	<h1 lang="en">Edit User</h1>
+	<fieldset style="border:0;">
+		<div class="form-group">
+			<label class="control-label" for="edit-user-form-inputUsername" lang="en">Username</label>
+			<input type="text" class="form-control" id="edit-user-form-inputUsername" name="username" required="" autofocus>
+		</div>
+		<div class="form-group">
+			<label class="control-label" for="edit-user-form-inputEmail" lang="en">Email</label>
+			<input type="text" class="form-control" id="edit-user-form-inputEmail" name="email" required="" autofocus>
+		</div>
+		<div class="form-group">
+			<label class="control-label" for="edit-user-form-inputPassword" lang="en">Password</label>
+			<input type="password" class="form-control" id="edit-user-form-inputPassword" name="password"  required="">
+		</div>
+		<div class="form-group">
+			<label class="control-label" for="edit-user-form-inputPassword2" lang="en">Password Again</label>
+			<input type="password" class="form-control" id="edit-user-form-inputPassword2" name="password2"  required="">
+		</div>
+	</fieldset>
+	<button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none editUserAdmin" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Edit User</span></button>
+	<div class="clearfix"></div>
 </form>
 ';
 }

+ 311 - 345
api/pages/settings.php

@@ -12,198 +12,164 @@ function get_page_settings($Organizr)
 		return false;
 	}
 	$Organizr->writeLog('success', 'Admin Function -  Accessed Settings Page', $Organizr->user['username']);
-	return $Organizr->pluginFiles('js', true) . '
+	$systemMenus = $Organizr->systemMenuLists();
+	return $Organizr->pluginFiles('js', true) . $Organizr->loadJavascriptFile('js/Sortable.min.js') . '
 <script>
-    (function() {
-        updateCheck();
-        authDebugCheck();
-        sponsorLoad();
-        newsLoad();
-        checkCommitLoad();
-        backersLoad();
-        [].slice.call(document.querySelectorAll(\'.sttabs-main-settings-div\')).forEach(function(el) {
-            new CBPFWTabs(el);
-        });
-    })();
+	(function() {
+		updateCheck();
+		authDebugCheck();
+		sponsorLoad();
+		newsLoad();
+		checkCommitLoad();
+		backersLoad();
+		[].slice.call(document.querySelectorAll(\'.sttabs-main-settings-div\')).forEach(function(el) {
+			new CBPFWTabs(el);
+		});
+	})();
 </script>
 <div class="container-fluid">
-    <div class="row bg-title">
-        <div class="col-lg-3 col-md-4 col-sm-4 col-xs-12">
-            <h4 class="page-title" lang="en">Organizr Settings</h4>
-        </div>
-        <div class="col-lg-9 col-sm-8 col-md-8 col-xs-12">
-            <ol id="settingsBreadcrumb" class="breadcrumb">
-                <li lang="en">Settings</li>
-                <li lang="en">Tab Editor</li>
-            </ol>
-        </div>
-        <!-- /.col-lg-12 -->
-    </div>
-    <!--.row-->
-    <div class="row">
-        <!-- Tab style start -->
-        <section class="">
-            <div class="sttabs sttabs-main-settings-div tabs-style-flip">
-                <nav>
-                    <ul>
-                        <li onclick="changeSettingsMenu(\'Settings::Tab Editor\')" id="settings-main-tab-editor-anchor"><a href="#settings-main-tab-editor" class="sticon ti-layout-tab-v"><span lang="en">Tab Editor</span></a></li>
-                        <li onclick="changeSettingsMenu(\'Settings::Customize\')" id="settings-main-customize-anchor"><a href="#settings-main-customize" class="sticon ti-paint-bucket"><span lang="en">Customize</span></a></li>
-                        <li onclick="changeSettingsMenu(\'Settings::User Management\')" id="settings-main-user-management-anchor"><a href="#settings-main-user-management" class="sticon ti-user"><span lang="en">User Management</span></a></li>
-                        <li onclick="changeSettingsMenu(\'Settings::Image Manager\');loadSettingsPage2(\'api/v2/page/settings_image_manager\',\'#settings-image-manager-view\',\'Image Viewer\');" id="settings-main-image-manager-anchor"><a href="#settings-main-image-manager" class="sticon ti-image"><span lang="en">Image Manager</span></a></li>
-    					<li onclick="changeSettingsMenu(\'Settings::Plugins\');loadSettingsPage2(\'api/v2/page/settings_plugins\',\'#settings-main-plugins\',\'Plugins\');" id="settings-main-plugins-anchor"><a href="#settings-main-plugins" class="sticon ti-plug"><span lang="en">Plugins</span></a></li>
-                        <li onclick="changeSettingsMenu(\'Settings::System Settings\');authDebugCheck();" id="settings-main-system-settings-anchor"><a href="#settings-main-system-settings" class="sticon ti-settings"><span lang="en">System Settings</span></a></li>
-                    </ul>
-                </nav>
-                <div class="content-wrap">
-                    <! -- TAB EDITOR -->
-                    <section id="settings-main-tab-editor">
-                        <ul class="nav customtab2 nav-tabs" role="tablist">
-                            <li onclick="changeSettingsMenu(\'Settings::Tab Editor::Tabs\');loadSettingsPage2(\'api/v2/page/settings_tab_editor_tabs\',\'#settings-tab-editor-tabs\',\'Tab Editor\');" role="presentation" class=""><a id="settings-tab-editor-tabs-anchor" href="#settings-tab-editor-tabs" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-layout-tab-v"></i></span><span class="hidden-xs" lang="en">Tabs</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::Tab Editor::Categories\');loadSettingsPage2(\'api/v2/page/settings_tab_editor_categories\',\'#settings-tab-editor-categories\',\'Category Editor\');" role="presentation" class=""><a id="settings-tab-editor-categories-anchor" href="#settings-tab-editor-categories" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-layout-list-thumb"></i></span><span class="hidden-xs" lang="en">Categories</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::Tab Editor::Homepage Items\');loadSettingsPage2(\'api/v2/page/settings_tab_editor_homepage\',\'#settings-tab-editor-homepage\',\'Homepage Items\');" role="presentation" class=""><a id="settings-tab-editor-homepage-anchor" href="#settings-tab-editor-homepage" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-home"></i></span><span class="hidden-xs" lang="en">Homepage Items</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::Tab Editor::Homepage Order\');loadSettingsPage2(\'api/v2/page/settings_tab_editor_homepage_order\',\'#settings-tab-editor-homepage-order\',\'Homepage Order\');" role="presentation" class=""><a id="settings-tab-editor-homepage-order-anchor" href="#settings-tab-editor-homepage-order" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-exchange-vertical"></i></span><span class="hidden-xs" lang="en">Homepage Order</span></a>
-                            </li>
-                        </ul>
-                        <!-- Tab panes -->
-                        <div class="tab-content">
-                            <div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-tabs">
-                                <h2 lang="en">Loading...</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-categories">
-                                <h2 lang="en">Loading...</h2>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-homepage">
-                                <h2 lang="en">Loading...</h2>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-homepage-order">
-                                <h2 lang="en">Coming Soon...</h2>
-                            </div>
-                        </div>
-                    </section>
-                    <! -- Customize -->
-                    <section id="settings-main-customize">
-                        <ul class="nav customtab2 nav-tabs" role="tablist">
-                            <li onclick="changeSettingsMenu(\'Settings::Customize::Appearance\');loadSettingsPage2(\'api/v2/page/settings_customize_appearance\',\'#settings-customize-appearance\',\'Customize Appearance\');" role="presentation" class=""><a id="settings-customize-appearance-anchor" href="#settings-customize-appearance" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-eye"></i></span><span class="hidden-xs" lang="en">Appearance</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::Customize::Marketplace\');loadMarketplace(\'themes\');" role="presentation" class=""><a id="settings-customize-marketplace-anchor" href="#settings-customize-marketplace" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-shopping-cart-full"></i></span><span class="hidden-xs" lang="en">Marketplace</span></a></li>
-                        </ul>
-                        <!-- Tab panes -->
-                        <div class="tab-content">
-                            <div role="tabpanel" class="tab-pane fade" id="settings-customize-appearance">
-                                <h2 lang="en">Loading...</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-customize-marketplace">
+	<div class="row bg-title">
+		<div class="col-lg-3 col-md-4 col-sm-4 col-xs-12">
+			<h4 class="page-title" lang="en">Organizr Settings</h4>
+		</div>
+		<div class="col-lg-9 col-sm-8 col-md-8 col-xs-12">
+			<ol id="settingsBreadcrumb" class="breadcrumb">
+				<li lang="en">Settings</li>
+				<li lang="en">Tab Editor</li>
+			</ol>
+		</div>
+		<!-- /.col-lg-12 -->
+	</div>
+	<!--.row-->
+	<div class="row">
+		<!-- Tab style start -->
+		<section class="">
+			<div class="sttabs sttabs-main-settings-div tabs-style-flip">
+				<nav>
+					<ul>
+						<li onclick="changeSettingsMenu(\'Settings::Tab Editor\')" id="settings-main-tab-editor-anchor"><a href="#settings-main-tab-editor" class="sticon ti-layout-tab-v"><span lang="en">Tab Editor</span></a></li>
+						<li onclick="changeSettingsMenu(\'Settings::Customize\')" id="settings-main-customize-anchor"><a href="#settings-main-customize" class="sticon ti-paint-bucket"><span lang="en">Customize</span></a></li>
+						<li onclick="changeSettingsMenu(\'Settings::User Management\')" id="settings-main-user-management-anchor"><a href="#settings-main-user-management" class="sticon ti-user"><span lang="en">User Management</span></a></li>
+						<li onclick="changeSettingsMenu(\'Settings::Image Manager\');loadSettingsPage2(\'api/v2/page/settings_image_manager\',\'#settings-image-manager-view\',\'Image Viewer\');" id="settings-main-image-manager-anchor"><a href="#settings-main-image-manager" class="sticon ti-image"><span lang="en">Image Manager</span></a></li>
+						<li onclick="changeSettingsMenu(\'Settings::Plugins\');loadSettingsPage2(\'api/v2/page/settings_plugins\',\'#settings-main-plugins\',\'Plugins\');" id="settings-main-plugins-anchor"><a href="#settings-main-plugins" class="sticon ti-plug"><span lang="en">Plugins</span></a></li>
+						<li onclick="changeSettingsMenu(\'Settings::System Settings\');authDebugCheck();" id="settings-main-system-settings-anchor"><a href="#settings-main-system-settings" class="sticon ti-settings"><span lang="en">System Settings</span></a></li>
+					</ul>
+				</nav>
+				<div class="content-wrap">
+					<! -- TAB EDITOR -->
+					<section id="settings-main-tab-editor">
+						' . $systemMenus['tab_editor'] . '
+						<!-- Tab panes -->
+						<div class="tab-content">
+							<div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-tabs">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-categories">
+								<h2 lang="en">Loading...</h2>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-homepage">
+								<h2 lang="en">Loading...</h2>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-homepage-order">
+								<h2 lang="en">Loading...</h2>
+							</div>
+						</div>
+					</section>
+					<! -- Customize -->
+					<section id="settings-main-customize">
+						' . $systemMenus['customize'] . '
+						<!-- Tab panes -->
+						<div class="tab-content">
+							<div role="tabpanel" class="tab-pane fade" id="settings-customize-appearance">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-customize-marketplace">
 								<div class="panel bg-org panel-info">
 									<div class="panel-heading">
 										<span lang="en">Theme Marketplace</span>
 									</div>
 									<div class="panel-wrapper collapse in" aria-expanded="true">
 										<div class="table-responsive">
-					                        <table class="table table-hover manage-u-table">
-					                            <thead>
-					                                <tr>
-					                                    <th width="70" class="text-center" lang="en">THEME</th>
-					                                    <th></th>
-					                                    <th lang="en">CATEGORY</th>
-					                                    <th lang="en">STATUS</th>
-					                                    <th lang="en" style="text-align:center">INFO</th>
-					                                    <th lang="en" style="text-align:center">INSTALL</th>
-					                                    <th lang="en" style="text-align:center">DELETE</th>
-					                                </tr>
-					                            </thead>
-					                            <tbody id="manageThemeTable"></tbody>
-					                        </table>
-					                    </div>
+											<table class="table table-hover manage-u-table">
+												<thead>
+													<tr>
+														<th width="70" class="text-center" lang="en">THEME</th>
+														<th></th>
+														<th lang="en">CATEGORY</th>
+														<th lang="en">STATUS</th>
+														<th lang="en" style="text-align:center">INFO</th>
+														<th lang="en" style="text-align:center">INSTALL</th>
+														<th lang="en" style="text-align:center">DELETE</th>
+													</tr>
+												</thead>
+												<tbody id="manageThemeTable"></tbody>
+											</table>
+										</div>
 									</div>
 								</div>
 							</div>
-                        </div>
-                    </section>
-                    <! -- USER MANAGEMENT -->
-                    <section id="settings-main-user-management">
-                        <ul class="nav customtab2 nav-tabs" role="tablist">
-                            <li onclick="changeSettingsMenu(\'Settings::User Management::Manage Users\');loadSettingsPage2(\'api/v2/page/settings_user_manage_users\',\'#settings-user-manage-users\',\'User Management\');" role="presentation" class=""><a id="settings-user-manage-users-anchor" href="#settings-user-manage-users" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-id-badge"></i></span><span class="hidden-xs" lang="en">Users</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::User Management::Manage Groups\');loadSettingsPage2(\'api/v2/page/settings_user_manage_groups\',\'#settings-user-manage-groups\',\'Group Management\');" role="presentation" class=""><a id="settings-user-manage-groups-anchor" href="#settings-user-manage-groups" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-briefcase"></i></span><span class="hidden-xs" lang="en">Groups</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::User Management::Import Users\');" role="presentation" class=""><a id="settings-user-import-users-anchor" href="#settings-user-import-users" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-import"></i></span><span class="hidden-xs" lang="en">Import</span></a>
-                            </li>
-                        </ul>
-                        <!-- Tab panes -->
-                        <div class="tab-content">
-                            <div role="tabpanel" class="tab-pane fade" id="settings-user-manage-users">
-                                <h2 lang="en">Loading...</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-user-manage-groups">
-                                <h2 lang="en">Loading...</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-user-import-users">
-                                ' . $Organizr->importUserButtons() . '
-                                <div class="clearfix"></div>
-                            </div>
-                        </div>
-                    </section>
-                    <! -- IMAGE MANAGER -->
-                    <section id="settings-main-image-manager">
-                        <!-- Tab panes -->
-                        <div class="tab-content">
-                            <div role="tabpanel" class="tab-pane fade active in" id="settings-image-manager-view">
-                                <h2 lang="en">Loading...</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                        </div>
-                    </section>
-                    <! -- PLUGINS -->
-                    <section id="settings-main-plugins">
-                        <h2 lang="en">Plugins</h2>
-                    </section>
-                    <! -- SYSTEM SETTINGS -->
-                    <section id="settings-main-system-settings">
-                        <ul class="nav customtab2 nav-tabs" role="tablist">
-                            <li onclick="changeSettingsMenu(\'Settings::System Settings::About\')" role="presentation" class="active"><a id="settings-settings-about-anchor" href="#settings-settings-about" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="true"><span class="visible-xs"><i class="ti-info-alt"></i></span><span class="hidden-xs" lang="en">About</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::System Settings::Main\');loadSettingsPage2(\'api/v2/page/settings_settings_main\',\'#settings-settings-main\',\'Main Settings\');" role="presentation" class=""><a id="settings-settings-main-anchor" href="#settings-settings-main" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-settings"></i></span><span class="hidden-xs" lang="en">Main</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::System Settings::SSO\');loadSettingsPage2(\'api/v2/page/settings_settings_sso\',\'#settings-settings-sso\',\'SSO\');" role="presentation" class=""><a id="settings-settings-sso-anchor" href="#settings-settings-sso" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-key"></i></span><span class="hidden-xs" lang="en">Single Sign-On</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::System Settings::Logs\');loadSettingsPage2(\'api/v2/page/settings_settings_logs\',\'#settings-settings-logs\',\'Log Viewer\');" role="presentation" class=""><a id="settings-settings-logs-anchor" href="#settings-settings-logs" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-receipt"></i></span><span class="hidden-xs" lang="en">Logs</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::System Settings::Updates\')" role="presentation" class=""><a id="update-button" href="#settings-settings-updates" aria-controls="profile" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-package"></i></span> <span class="hidden-xs" lang="en">Updates</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::System Settings::Backup\');loadSettingsPage2(\'api/v2/page/settings_settings_backup\',\'#settings-settings-backup\',\'Backup\');" role="presentation" class=""><a id="settings-settings-backup-anchor" href="#settings-settings-backup" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-export"></i></span><span class="hidden-xs" lang="en">Backup</span></a>
-                            </li>
-                            <li onclick="changeSettingsMenu(\'Settings::System Settings::Donate\')" role="presentation" class=""><a id="settings-settings-donate-anchor" href="#settings-settings-donate" aria-controls="profile" role="tab" data-toggle="tab" aria-expanded="false"><span class="visible-xs"><i class="ti-money"></i></span> <span class="hidden-xs" lang="en">Donate</span></a>
-                            </li>
-                        </ul>
-                        <!-- Tab panes -->
-                        <div class="tab-content">
-                            <div role="tabpanel" class="tab-pane fade" id="settings-settings-main">
-                                <h2 lang="en">Main Settings</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-settings-sso">
-                                <h2 lang="en">Loading...</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-settings-logs">
-                                <h2 lang="en">Loading...</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-settings-backup">
-                                <h2 lang="en">Loading...</h2>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade active in" id="settings-settings-about">
-                            	<div class="row">
-	                                <div class="col-lg-12">
-							            <div class="panel panel-default">
+						</div>
+					</section>
+					<! -- USER MANAGEMENT -->
+					<section id="settings-main-user-management">
+						' . $systemMenus['user_management'] . '
+						<!-- Tab panes -->
+						<div class="tab-content">
+							<div role="tabpanel" class="tab-pane fade" id="settings-user-manage-users">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-user-manage-groups">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-user-import-users">
+								' . $Organizr->importUserButtons() . '
+								<div class="clearfix"></div>
+							</div>
+						</div>
+					</section>
+					<! -- IMAGE MANAGER -->
+					<section id="settings-main-image-manager">
+						<!-- Tab panes -->
+						<div class="tab-content">
+							<div role="tabpanel" class="tab-pane fade active in" id="settings-image-manager-view">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+						</div>
+					</section>
+					<! -- PLUGINS -->
+					<section id="settings-main-plugins">
+						<h2 lang="en">Plugins</h2>
+					</section>
+					<! -- SYSTEM SETTINGS -->
+					<section id="settings-main-system-settings">
+					' . $systemMenus['system_settings'] . '
+						<!-- Tab panes -->
+						<div class="tab-content">
+							<div role="tabpanel" class="tab-pane fade" id="settings-settings-main">
+								<h2 lang="en">Main Settings</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-settings-sso">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-settings-logs">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-settings-backup">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade active in" id="settings-settings-about">
+								<div class="row">
+									<div class="col-lg-12">
+										<div class="panel panel-default">
 											<div class="panel-heading bg-org p-t-10 p-b-10">
 												<span class="pull-left m-t-5">
 													<img class="lazyload loginTitle" data-src="plugins/images/organizr/logo-no-border.png"> &nbsp;
@@ -211,182 +177,182 @@ function get_page_settings($Organizr)
 												</span>
 												<div class="clearfix"></div>
 											</div>
-							                <div class="panel-wrapper p-b-0 collapse in bg-org">
-							        			<div id="organizrNewsPanel"></div>
-							                </div>
-							            </div>
-							        </div>
-    							</div>
-    							<div class="row">
-    								<div class="col-lg-6 col-sm-12 col-md-6">
-    									<div class="panel bg-org">
-    										<div class="p-30">
-    											<div class="row">
-    												<div class="col-xs-12"><img src="plugins/images/organizr/logo-wide.png" alt="organizr" class="img-responsive"></div>
-    											</div>
-    										</div>
-    										<hr class="m-t-10">
-    										<div class="p-20 text-center">
-    											<p lang="en">Below you will find all the links for everything that has to do with Organizr</p>
-    										</div>
-    										<hr>
-    										<ul class="dp-table profile-social-icons">
-    											<li><a href="https://organizr.app" target="_blank"><i class="mdi mdi-web mdi-24px"></i></a></li>
-    											<li><a href="https://reddit.com/r/organizr" target="_blank"><i class="mdi mdi-reddit mdi-24px"></i></a></li>
-    											<li><a href="https://organizr.app/discord" target="_blank"><i class="mdi mdi-discord mdi-24px"></i></a></li>
-    											<li><a href="https://github.com/causefx/organizr" target="_blank"><i class="mdi mdi-github-box mdi-24px"></i></a></li>
-    										</ul>
-    										<hr>
-    										<a href="https://poeditor.com/join/project/T6l68hksTE" target="_blank">
-		                                        <div class="white-box bg-org">
-		                                            <h4 lang="en">Want to help translate?</h4>
-		                                            <p lang="en">Head on over to POEditor and help us translate Organizr into your language</p>
-		                                            <p lang="en">I will try and import new strings every Friday</p>
-		                                        </div>
-		                                    </a>
-    										
-    									</div>
-    								</div>
-                                    <div class="col-lg-6 col-sm-12 col-md-6">
-                                        <div class="white-box bg-org">
-                                            <h3 class="box-title" lang="en">Information</h3>
-                                            <ul class="feeds">
-                                                <li><div class="bg-info"><i class="mdi mdi-webpack mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Organizr Version</span> ' . $Organizr->version . '</li>
-                                                <li><div class="bg-info"><i class="mdi mdi-github-box mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Organizr Branch</span><a href="https://github.com/causefx/Organizr/commits/' . $Organizr->config['branch'] . '" target="_blank"> ' . $Organizr->config['branch'] . '</a></li>
-                                                <li><div class="bg-info"><i class="mdi mdi-database mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Database Location</span> ' . $Organizr->config['dbLocation'] . $Organizr->config['dbName'] . '</li>
-                                                ' . $Organizr->settingsDocker() . $Organizr->settingsPathChecks() . '
-                                                <hr class="m-t-10">
-                                                <li><div class="bg-info"><i class="mdi mdi-language-php mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">PHP Version</span> ' . phpversion() . '</li>
-                                                <li><div class="bg-info"><i class="mdi mdi-package-variant-closed mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Webserver Version</span> ' . $_SERVER['SERVER_SOFTWARE'] . '</li>
-                                                <hr class="m-t-10">
-                                                <li><div class="bg-info"><i class="mdi mdi-account-card-details mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">License</span> ' . ucwords($Organizr->config['license']) . '</li>
-                                            </ul>
-                                        </div>
-                                    </div>
-    							</div>
-    							<div class="row">
-	                                <div class="col-lg-12">
-							            <div class="panel panel-default">
+											<div class="panel-wrapper p-b-0 collapse in bg-org">
+												<div id="organizrNewsPanel"></div>
+											</div>
+										</div>
+									</div>
+								</div>
+								<div class="row">
+									<div class="col-lg-6 col-sm-12 col-md-6">
+										<div class="panel bg-org">
+											<div class="p-30">
+												<div class="row">
+													<div class="col-xs-12"><img src="plugins/images/organizr/logo-wide.png" alt="organizr" class="img-responsive"></div>
+												</div>
+											</div>
+											<hr class="m-t-10">
+											<div class="p-20 text-center">
+												<p lang="en">Below you will find all the links for everything that has to do with Organizr</p>
+											</div>
+											<hr>
+											<ul class="dp-table profile-social-icons">
+												<li><a href="https://organizr.app" target="_blank"><i class="mdi mdi-web mdi-24px"></i></a></li>
+												<li><a href="https://reddit.com/r/organizr" target="_blank"><i class="mdi mdi-reddit mdi-24px"></i></a></li>
+												<li><a href="https://organizr.app/discord" target="_blank"><i class="mdi mdi-discord mdi-24px"></i></a></li>
+												<li><a href="https://github.com/causefx/organizr" target="_blank"><i class="mdi mdi-github-box mdi-24px"></i></a></li>
+											</ul>
+											<hr>
+											<a href="https://poeditor.com/join/project/T6l68hksTE" target="_blank">
+												<div class="white-box bg-org">
+													<h4 lang="en">Want to help translate?</h4>
+													<p lang="en">Head on over to POEditor and help us translate Organizr into your language</p>
+													<p lang="en">I will try and import new strings every Friday</p>
+												</div>
+											</a>
+											
+										</div>
+									</div>
+									<div class="col-lg-6 col-sm-12 col-md-6">
+										<div class="white-box bg-org">
+											<h3 class="box-title" lang="en">Information</h3>
+											<ul class="feeds">
+												<li><div class="bg-info"><i class="mdi mdi-webpack mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Organizr Version</span> ' . $Organizr->version . '</li>
+												<li><div class="bg-info"><i class="mdi mdi-github-box mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Organizr Branch</span><a href="https://github.com/causefx/Organizr/commits/' . $Organizr->config['branch'] . '" target="_blank"> ' . $Organizr->config['branch'] . '</a></li>
+												<li><div class="bg-info"><i class="mdi mdi-database mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Database Location</span> ' . $Organizr->config['dbLocation'] . $Organizr->config['dbName'] . '</li>
+												' . $Organizr->settingsDocker() . $Organizr->settingsPathChecks() . '
+												<hr class="m-t-10">
+												<li><div class="bg-info"><i class="mdi mdi-language-php mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">PHP Version</span> ' . phpversion() . '</li>
+												<li><div class="bg-info"><i class="mdi mdi-package-variant-closed mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Webserver Version</span> ' . $_SERVER['SERVER_SOFTWARE'] . '</li>
+												<hr class="m-t-10">
+												<li><div class="bg-info"><i class="mdi mdi-account-card-details mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">License</span> ' . ucwords($Organizr->config['license']) . '</li>
+											</ul>
+										</div>
+									</div>
+								</div>
+								<div class="row">
+									<div class="col-lg-12">
+										<div class="panel panel-default">
 											<div class="panel-heading bg-org p-t-10 p-b-10">
 												<span class="pull-left m-t-5"><span lang="en">Sponsors</span></span>
 												<div class="clearfix"></div>
 											</div>
-							                <div class="panel-wrapper p-b-0 collapse in bg-org">
-							                	<div id="sponsorList" class="owl-carousel owl-theme sponsor-items"></div>
-							        			<div id="sponsorListModals"></div>
-							                </div>
-							            </div>
-							        </div>
-    							</div>
-    							<div class="row">
-    							    <div class="col-lg-12">
-    							        <div class="panel panel-default">
+											<div class="panel-wrapper p-b-0 collapse in bg-org">
+												<div id="sponsorList" class="owl-carousel owl-theme sponsor-items"></div>
+												<div id="sponsorListModals"></div>
+											</div>
+										</div>
+									</div>
+								</div>
+								<div class="row">
+									<div class="col-lg-12">
+										<div class="panel panel-default">
 											<div class="panel-heading bg-org p-t-10 p-b-10">
 												<span class="pull-left m-t-5"><span lang="en">Backers</span></span>
 												<div class="clearfix"></div>
 											</div>
-							                <div class="panel-wrapper p-b-0 collapse in bg-org">
-							                	<div id="backersList" class="owl-carousel owl-theme backers-items"></div>
-							                </div>
-							            </div>
-                                    </div>
-    							</div>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-settings-donate">
-                                <div class="col-lg-12">
-                                    <div class="white-box bg-org">
-                                        <ul class="nav nav-tabs tabs customtab">
-                                            <li class="tab active">
-                                                <a href="#donate-github" data-toggle="tab" aria-expanded="true"> <span class=""><i class="fa fa-github text-warning"></i></span> <span class="hidden-xs" lang="en">Github Sponsor</span> </a>
-                                            </li>
-                                            <li class="tab">
-                                                <a href="#donate-paypal" data-toggle="tab" aria-expanded="true"> <span class=""><i class="fa fa-paypal text-info"></i></span> <span class="hidden-xs" lang="en">PayPal</span> </a>
-                                            </li>
-                                            <li class="tab">
-                                                <a href="#donate-square" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa mdi mdi-square-inc-cash mdi-18px text-success"></i></span> <span class="hidden-xs" lang="en">Square Cash</span> </a>
-                                            </li>
-                                            <li class="tab">
-                                                <a href="#donate-crypto" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa mdi mdi-coin mdi-18px text-info"></i></span> <span class="hidden-xs" lang="en">Cryptos</span> </a>
-                                            </li>
-                                            <li class="tab">
-                                                <a href="#donate-patreon" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa mdi mdi-account-multiple mdi-18px text-danger"></i></span> <span class="hidden-xs" lang="en">Patreon</span> </a>
-                                            </li>
-                                            <li class="tab">
-                                                <a href="#donate-open-collective" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa fa-circle-o-notch text-primary"></i></span> <span class="hidden-xs" lang="en">Open Collective</span> </a>
-                                            </li>
-                                            <li class="tab">
-                                                <a href="#donate-ads" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa mdi mdi-google mdi-18px text-danger"></i></span> <span class="hidden-xs" lang="en">Google Ads</span> </a>
-                                            </li>
-                                        </ul>
-                                        <div class="tab-content">
-                                        	<div class="tab-pane active" id="donate-github">
-                                                <blockquote>Want to show support on Github?  Sponsor me :)<br/>Please click the button to continue.</blockquote>
-                                                <button onclick="window.open(\'https://github.com/sponsors/causefx\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
-                                            </div>
-                                            <div class="tab-pane" id="donate-paypal">
-                                                <blockquote>I have chosen to go with PayPal Pools so everyone can see how much people have donated.<br/>Please click the button to continue.</blockquote>
-                                                <button onclick="window.open(\'https://paypal.me/pools/c/83JNaMBESR\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
-                                            </div>
-                                            <div class="tab-pane" id="donate-square">
-                                                <blockquote>If you use the Square Cash App, you can donate with that if you like.<br/>Please click the button to continue.</blockquote>
-                                                <button onclick="window.open(\'https://cash.me/$CauseFX\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
-                                            </div>
-                                            <div class="tab-pane" id="donate-crypto">
-                                                <blockquote>Want to donate a small amount of Crypto?.<br/>Please use the QR Code or Wallet ID.</blockquote>
-                                                <div class="col-lg-4 col-xs-12">
-                                                    <div class="lazyload qr-code" data-src="plugins/images/Bitcoin_QR_code.png"></div>
-                                                    <div class="clearfix"></div>
-                                                    <code>18dNtPKgor6pV5DJhYNqFxLJJ2BKugo4K9</code>
-                                                </div>
-                                                <div class="col-lg-4 col-xs-12">
-                                                    <div class="lazyload qr-code" data-src="plugins/images/Litecoin_QR_code.png"></div>
-                                                    <div class="clearfix"></div>
-                                                    <code>LejRxt8huhFGpVrp7TM43VSstrzKGxf8Cj</code>
-                                                </div>
-                                                <div class="col-lg-4 col-xs-12">
-                                                    <div class="lazyload qr-code" data-src="plugins/images/Ethereum_QR_code.png"></div>
-                                                    <div class="clearfix"></div>
-                                                    <code>0x605b678761af62C02Fe0fA86A99053D666dF5d6f</code>
-                                                </div>
-                                                <div class="clearfix"></div>
-                                            </div>
-                                            <div class="tab-pane" id="donate-patreon">
-                                                <blockquote>Need specialized support or just want to support Organizr?  If so head to Patreon...<br/>Please click the button to continue.</blockquote>
-                                                <button onclick="window.open(\'https://www.patreon.com/join/organizr?\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
-                                            </div>
-                                            <div class="tab-pane" id="donate-open-collective">
-                                                <blockquote>Need specialized support or just want to support Organizr?  If so head to Open Collective...<br/>Please click the button to continue.</blockquote>
-                                                <button onclick="window.open(\'https://opencollective.com/organizr\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
-                                            </div>
-                                            <div class="tab-pane" id="donate-ads">
-                                                <blockquote>Money not an option?  No problem.  Show some love to this Google Ad below:</blockquote>
-                                                 <button onclick="window.open(\'https://organizr.app/ads/google.html\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
-                                            </div>
-                                        </div>
-                                    </div>
-                                </div>
-                                <div class="clearfix"></div>
-                            </div>
-                            <div role="tabpanel" class="tab-pane fade" id="settings-settings-updates">
-                                <div id="githubVersions"></div>
-                                <div class="clearfix"></div>
-                            </div>
-                        </div>
-                    </section>
-                </div>
-                <!-- /content -->
-            </div>
-            <!-- /tabs -->
-        </section>
-    </div>
-    <!--./row-->
+											<div class="panel-wrapper p-b-0 collapse in bg-org">
+												<div id="backersList" class="owl-carousel owl-theme backers-items"></div>
+											</div>
+										</div>
+									</div>
+								</div>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-settings-donate">
+								<div class="col-lg-12">
+									<div class="white-box bg-org">
+										<ul class="nav nav-tabs tabs customtab">
+											<li class="tab active">
+												<a href="#donate-github" data-toggle="tab" aria-expanded="true"> <span class=""><i class="fa fa-github text-warning"></i></span> <span class="hidden-xs" lang="en">Github Sponsor</span> </a>
+											</li>
+											<li class="tab">
+												<a href="#donate-paypal" data-toggle="tab" aria-expanded="true"> <span class=""><i class="fa fa-paypal text-info"></i></span> <span class="hidden-xs" lang="en">PayPal</span> </a>
+											</li>
+											<li class="tab">
+												<a href="#donate-square" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa mdi mdi-square-inc-cash mdi-18px text-success"></i></span> <span class="hidden-xs" lang="en">Square Cash</span> </a>
+											</li>
+											<li class="tab">
+												<a href="#donate-crypto" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa mdi mdi-coin mdi-18px text-info"></i></span> <span class="hidden-xs" lang="en">Cryptos</span> </a>
+											</li>
+											<li class="tab">
+												<a href="#donate-patreon" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa mdi mdi-account-multiple mdi-18px text-danger"></i></span> <span class="hidden-xs" lang="en">Patreon</span> </a>
+											</li>
+											<li class="tab">
+												<a href="#donate-open-collective" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa fa-circle-o-notch text-primary"></i></span> <span class="hidden-xs" lang="en">Open Collective</span> </a>
+											</li>
+											<li class="tab">
+												<a href="#donate-ads" data-toggle="tab" aria-expanded="false"> <span class=""><i class="fa mdi mdi-google mdi-18px text-danger"></i></span> <span class="hidden-xs" lang="en">Google Ads</span> </a>
+											</li>
+										</ul>
+										<div class="tab-content">
+											<div class="tab-pane active" id="donate-github">
+												<blockquote lang="en">Want to show support on Github?  Sponsor me :)<br/><span lang="en">Please click the button to continue.</span></blockquote>
+												<button onclick="window.open(\'https://github.com/sponsors/causefx\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
+											</div>
+											<div class="tab-pane" id="donate-paypal">
+												<blockquote lang="en">I have chosen to go with PayPal Pools so everyone can see how much people have donated.<br/><span lang="en">Please click the button to continue.</span></blockquote>
+												<button onclick="window.open(\'https://paypal.me/pools/c/83JNaMBESR\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
+											</div>
+											<div class="tab-pane" id="donate-square">
+												<blockquote lang="en">If you use the Square Cash App, you can donate with that if you like.<br/><span lang="en">Please click the button to continue.</span></blockquote>
+												<button onclick="window.open(\'https://cash.me/$CauseFX\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
+											</div>
+											<div class="tab-pane" id="donate-crypto">
+												<blockquote lang="en">Want to donate a small amount of Crypto?.<br/>Please use the QR Code or Wallet ID.</blockquote>
+												<div class="col-lg-4 col-xs-12">
+													<div class="lazyload qr-code" data-src="plugins/images/Bitcoin_QR_code.png"></div>
+													<div class="clearfix"></div>
+													<code>18dNtPKgor6pV5DJhYNqFxLJJ2BKugo4K9</code>
+												</div>
+												<div class="col-lg-4 col-xs-12">
+													<div class="lazyload qr-code" data-src="plugins/images/Litecoin_QR_code.png"></div>
+													<div class="clearfix"></div>
+													<code>LejRxt8huhFGpVrp7TM43VSstrzKGxf8Cj</code>
+												</div>
+												<div class="col-lg-4 col-xs-12">
+													<div class="lazyload qr-code" data-src="plugins/images/Ethereum_QR_code.png"></div>
+													<div class="clearfix"></div>
+													<code>0x605b678761af62C02Fe0fA86A99053D666dF5d6f</code>
+												</div>
+												<div class="clearfix"></div>
+											</div>
+											<div class="tab-pane" id="donate-patreon">
+												<blockquote lang="en">Need specialized support or just want to support Organizr?  If so head to Patreon...<br/><span lang="en">Please click the button to continue.</span></blockquote>
+												<button onclick="window.open(\'https://www.patreon.com/join/organizr?\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
+											</div>
+											<div class="tab-pane" id="donate-open-collective">
+												<blockquote lang="en">Need specialized support or just want to support Organizr?  If so head to Open Collective...<br/><span lang="en">Please click the button to continue.</span></blockquote>
+												<button onclick="window.open(\'https://opencollective.com/organizr\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
+											</div>
+											<div class="tab-pane" id="donate-ads">
+												<blockquote lang="en">Money not an option?  No problem.  Show some love to this Google Ad below:</blockquote>
+												 <button onclick="window.open(\'https://organizr.app/ads/google.html\', \'_blank\')" class="btn btn-primary btn-rounded waves-effect waves-light" type="button"><span class="btn-label"><i class="fa fa-link"></i></span><span lang="en">Continue To Website</span></button>
+											</div>
+										</div>
+									</div>
+								</div>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-settings-updates">
+								<div id="githubVersions"></div>
+								<div class="clearfix"></div>
+							</div>
+						</div>
+					</section>
+				</div>
+				<!-- /content -->
+			</div>
+			<!-- /tabs -->
+		</section>
+	</div>
+	<!--./row-->
 </div>
 <!-- /.container-fluid -->
 <form id="about-theme-form" class="mfp-hide white-popup-block mfp-with-anim">
-    <h2 id="about-theme-title">Loading...</h2>
-    <div class="clearfix"></div>
-    <div id="about-theme-body" class=""></div>
+	<h2 id="about-theme-title">Loading...</h2>
+	<div class="clearfix"></div>
+	<div id="about-theme-body" class=""></div>
 </form>
 <div id="editHomepageItemDiv"><div id="editHomepageItem" class=""></div></div>
 ';

+ 5 - 5
api/plugins/bookmark/plugin.php

@@ -150,10 +150,10 @@ class Bookmark extends Organizr
 							<div class="panel-wrapper collapse in" aria-expanded="true">
 								<div class="panel-body">
 									<ul class="list-icons">
-										<li><i class="fa fa-chevron-right text-info"></i> Add tab that points to <i>api/v2/plugins/bookmark/page</i> and set it\'s type to <i>Organizr</i>.</li>
-										<li><i class="fa fa-chevron-right text-info"></i> Create Bookmark categories in the new area in <i>Tab Editor</i>.</li>
-										<li><i class="fa fa-chevron-right text-info"></i> Create Bookmark tabs in the new area in <i>Tab Editor</i>.</li>
-										<li><i class="fa fa-chevron-right text-info"></i> Open your custom Bookmark page via menu.</li>
+										<li><i class="fa fa-chevron-right text-info"></i> <span lang="en">Add tab that points to <i>api/v2/plugins/bookmark/page</i> and set it\'s type to <i>Organizr</i>.</span></li>
+										<li><i class="fa fa-chevron-right text-info"></i> <span lang="en">Create Bookmark categories in the new area in <i>Tab Editor</i>.</span></li>
+										<li><i class="fa fa-chevron-right text-info"></i> <span lang="en">Create Bookmark tabs in the new area in <i>Tab Editor</i>.</span></li>
+										<li><i class="fa fa-chevron-right text-info"></i> <span lang="en">Open your custom Bookmark page via menu.</span></li>
 									</ul>
 								</div>
 							</div>
@@ -719,7 +719,7 @@ class Bookmark extends Organizr
 	<div class="panel bg-org panel-info">
 		<div class="panel-heading">
 			<span lang="en">Bookmark Category Editor</span>
-			<button type="button" class="btn btn-success btn-circle pull-right popup-with-form m-r-5" href="#new-bookmark-category-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
+			<button type="button" class="btn btn-info btn-circle pull-right popup-with-form m-r-5" href="#new-bookmark-category-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
 		</div>
 		<div class="table-responsive">
 			<form id="submit-bookmark-categories-form" onsubmit="return false;">

+ 28 - 7
api/plugins/bookmark/settings.js

@@ -51,8 +51,18 @@ function bookmarkLaunch(){
 
 // TAB MANAGEMENT
 function bookmarkTabsLaunch(){
-	var menuList = `<li onclick="changeSettingsMenu('Settings::Tab Editor::Bookmark Tabs');loadSettingsPage2('api/v2/plugins/bookmark/settings_tab_editor_bookmark_tabs','#settings-tab-editor-tabs','Tab Editor');" role="presentation"><a id="settings-tab-editor-tabs-anchor" href="#settings-tab-editor-tabs" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="true"><span class="visible-xs"><i class="ti-layout-tab-v"></i></span><span class="hidden-xs" lang="en">Bookmark Tabs</span></a></li>`;
-	$('#settings-main-tab-editor .nav-tabs').append(menuList);
+	var menuList = `<li class="bookmarkTabsMenu-added" onclick="changeSettingsMenu('Settings::Tab Editor::Bookmark Tabs');loadSettingsPage2('api/v2/plugins/bookmark/settings_tab_editor_bookmark_tabs','#settings-tab-editor-bookmark-tabs','Bookmark Tab Editor');" role="presentation"><a id="settings-tab-editor-bookmark-tabs-anchor" href="#settings-tab-editor-bookmark-tabs" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="true"><span class="visible-xs"><i class="ti-layout-tab-v"></i></span><span class="hidden-xs" lang="en">Bookmark Tabs</span></a></li>`;
+	let menuListAlt = `<option value="#settings-tab-editor-bookmark-tabs-anchor" lang="en">Bookmark Tabs</option>`;
+	let div = `
+	<div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-bookmark-tabs">
+		<h2 lang="en">Loading...</h2>
+	</div>
+	`;
+	if($('.bookmarkCategoryMenu-added').length === 0 ){
+		$('#settings-main-tab-editor .nav-tabs').append(menuList);
+		$('.settings-dropdown-box.tab-editor-menu').append(menuListAlt);
+		$('#settings-main-tab-editor .tab-content').append(div);
+	}
 }
 
 function getColorPickerOptionsWithCallback(func){
@@ -68,7 +78,7 @@ function buildBookmarkTabEditor(){
 			organizrCatchError(e,data);
 		}
 		$('#bookmarkTabEditorTable').html(buildBookmarkTabEditorItem(response.data));
-		
+
 		// initialize color pickers only first time
 		if(!colorPickerInitialized){
 			$("input.bookmark-pick-a-color").ColorPickerSliders({
@@ -77,7 +87,7 @@ function buildBookmarkTabEditor(){
 				hsvpanel: true,
 				previewformat: 'hex',
 				flat: true,
-				onchange: function(container, color){ 
+				onchange: function(container, color){
 					generatePreviewBookmarkNewTab();
 					generatePreviewBookmarkEditTab();
 				}
@@ -473,8 +483,19 @@ $(document).on('input', "#edit-bookmark-tab-form-inputTextColor", generatePrevie
 
 // CATEGORY MANAGEMENT
 function bookmarkCategoriesLaunch(){
-	var menuList = `<li onclick="changeSettingsMenu('Settings::Tab Editor::Bookmark Categories');loadSettingsPage2('api/v2/plugins/bookmark/settings_tab_editor_bookmark_categories','#settings-tab-editor-tabs','Tab Editor');" role="presentation"><a id="settings-tab-editor-tabs-anchor" href="#settings-tab-editor-tabs" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="true"><span class="visible-xs"><i class="ti-layout-tab-v"></i></span><span class="hidden-xs" lang="en">Bookmark Categories</span></a></li>`;
-	$('#settings-main-tab-editor .nav-tabs').append(menuList);
+	var menuList = `<li class="bookmarkCategoryMenu-added" onclick="changeSettingsMenu('Settings::Tab Editor::Bookmark Categories');loadSettingsPage2('api/v2/plugins/bookmark/settings_tab_editor_bookmark_categories','#settings-tab-editor-bookmark-categories','Bookmark Category Editor');" role="presentation"><a id="settings-tab-editor-bookmark-categories-anchor" href="#settings-tab-editor-bookmark-categories" aria-controls="home" role="tab" data-toggle="tab" aria-expanded="true"><span class="visible-xs"><i class="ti-layout-tab-v"></i></span><span class="hidden-xs" lang="en">Bookmark Categories</span></a></li>`;
+	let menuListAlt = `<option value="#settings-tab-editor-bookmark-categories-anchor" lang="en">Bookmark Categories</option>`;
+	let div = `
+	<div role="tabpanel" class="tab-pane fade" id="settings-tab-editor-bookmark-categories">
+		<h2 lang="en">Loading...</h2>
+	</div>
+	`;
+	if($('.bookmarkCategoryMenu-added').length === 0 ){
+		$('#settings-main-tab-editor .nav-tabs').append(menuList);
+		$('.settings-dropdown-box.tab-editor-menu').append(menuListAlt);
+		$('#settings-main-tab-editor .tab-content').append(div);
+	}
+
 }
 
 function buildBookmarkCategoryEditor(){
@@ -653,4 +674,4 @@ function submitBookmarkCategoryOrder(){
 	}).fail(function(xhr) {
 		OrganizrApiError(xhr, 'Update Error');
 	});
-}
+}

+ 17 - 17
api/plugins/chat/plugin.php

@@ -25,25 +25,25 @@ class Chat extends Organizr
 		return array(
 			'custom' => '
 				<div class="row">
-                    <div class="col-lg-12">
-                        <div class="panel panel-info">
-                            <div class="panel-heading">
+					<div class="col-lg-12">
+						<div class="panel panel-info">
+							<div class="panel-heading">
 								<span lang="en">Notice</span>
-                            </div>
-                            <div class="panel-wrapper collapse in" aria-expanded="true">
-                                <div class="panel-body">
+							</div>
+							<div class="panel-wrapper collapse in" aria-expanded="true">
+								<div class="panel-body">
 									<ul class="list-icons">
-                                        <li><i class="fa fa-chevron-right text-danger"></i> <a href="https://dashboard.pusher.com/accounts/sign_up" target="_blank">Signup for Pusher [FREE]</a></li>
-                                        <li><i class="fa fa-chevron-right text-danger"></i> Create an App called whatever you like and choose a cluster (Close to you)</li>
-                                        <li><i class="fa fa-chevron-right text-danger"></i> Frontend (JQuery) - Backend (PHP)</li>
-                                        <li><i class="fa fa-chevron-right text-danger"></i> Click the overview tab on top left</li>
-                                        <li><i class="fa fa-chevron-right text-danger"></i> Copy and paste the 4 values into Organizr</li>
-                                        <li><i class="fa fa-chevron-right text-danger"></i> Save and reload!</li>
-                                    </ul>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
+										<li><i class="fa fa-chevron-right text-danger"></i> <a href="https://dashboard.pusher.com/accounts/sign_up" target="_blank"><span lang="en">Signup for Pusher [FREE]</span></a></li>
+										<li><i class="fa fa-chevron-right text-danger"></i> <span lang="en">Create an App called whatever you like and choose a cluster (Close to you)</span></li>
+										<li><i class="fa fa-chevron-right text-danger"></i> <span lang="en">Frontend (JQuery) - Backend (PHP)</span></li>
+										<li><i class="fa fa-chevron-right text-danger"></i> <span lang="en">Click the overview tab on top left</span></li>
+										<li><i class="fa fa-chevron-right text-danger"></i> <span lang="en">Copy and paste the 4 values into Organizr</span></li>
+										<li><i class="fa fa-chevron-right text-danger"></i> <span lang="en">Save and reload!</span></li>
+									</ul>
+								</div>
+							</div>
+						</div>
+					</div>
 				</div>
 				',
 			'Options' => array(

+ 1 - 1
api/plugins/healthChecks/settings.js

@@ -13,7 +13,7 @@ $(document).on('click', '#HEALTHCHECKS-settings-button', function() {
         $('#HEALTHCHECKS-settings-items').html(buildFormGroup(response.data));
         var elAddButtonStart = $('#HEALTHCHECKS-settings-page [id*="Services"] .row.start');
         var items = $('#HEALTHCHECKS-settings-page [id*="Services"] .row.m-b-40 span');
-        $(elAddButtonStart).after('<div class="row"><button type="button" class="btn btn-info pull-right m-r-20 addNewHCService" ><i class="fa fa-plus"></i> Add New Service</button><button type="button" class="btn btn-primary pull-right m-r-20 importNewHCService" ><i class="fa fa-database"></i> Import Services</button></div>');
+        $(elAddButtonStart).after('<div class="row"><button type="button" class="btn btn-info pull-right m-r-20 addNewHCService" ><i class="fa fa-plus"></i> <span lang="en">Add New Service</span></button><button type="button" class="btn btn-primary pull-right m-r-20 importNewHCService" ><i class="fa fa-database"></i> <span lang="en">Import Services</span></button></div>');
         $.each(items, function(key,val) {
             var el = $(val);
             var text = el.text();

+ 1 - 1
api/plugins/invites/plugin.php

@@ -345,7 +345,7 @@ class Invites extends Organizr
 				array(
 					'type' => 'html',
 					'label' => 'Note',
-					'html' => 'After enabling for the first time, please reload the page - Menu is located under User menu on top right'
+					'html' => '<span lang="en">After enabling for the first time, please reload the page - Menu is located under User menu on top right</span>'
 				)
 			)
 		);

+ 30 - 1
api/v2/routes/config.php

@@ -6,12 +6,41 @@
  * )
  */
 $app->get('/config[/{item}]', function ($request, $response, $args) {
+	/**
+	 * @OA\Get(
+	 *     tags={"config"},
+	 *     path="/api/v2/config",
+	 *     summary="Get Organizr Coniguration Items",
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/success-message"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
+	/**
+	 * @OA\Get(
+	 *     tags={"config"},
+	 *     path="/api/v2/config/{item}",
+	 *     summary="Get Organizr Coniguration Item",
+	 *     @OA\Parameter(name="item",description="The key of the item you want to grab",@OA\Schema(type="string"),in="path",required=true,example="configVersion"),
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/success-message"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
 	if ($Organizr->qualifyRequest(1, true)) {
 		if (isset($args['item'])) {
 			$Organizr->getConfigItem($args['item']);
 		} else {
-			$GLOBALS['api']['response']['data'] = $Organizr->config;
+			$GLOBALS['api']['response']['data'] = $Organizr->getConfigItems();
 		}
 		
 	}

+ 46 - 0
api/v2/routes/connectionTester.php

@@ -346,6 +346,29 @@ $app->post('/test/ombi', function ($request, $response, $args) {
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 	
+});
+$app->post('/test/overseerr', function ($request, $response, $args) {
+	/**
+	 * @OA\Post(
+	 *     security={{ "api_key":{} }},
+	 *     tags={"test connection"},
+	 *     path="/api/v2/test/overseerr",
+	 *     summary="Test connection to Overseerr",
+	 *     @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->testConnectionOverseerr($Organizr->apiData($request));
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+	
 });
 $app->post('/test/nzbget', function ($request, $response, $args) {
 	/**
@@ -369,6 +392,29 @@ $app->post('/test/nzbget', function ($request, $response, $args) {
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 	
+});
+$app->post('/test/utorrent', function ($request, $response, $args) {
+	/**
+	 * @OA\Post(
+	 *     security={{ "api_key":{} }},
+	 *     tags={"test connection"},
+	 *     path="/api/v2/test/utorrent",
+	 *     summary="Test connection to uTorrent",
+	 *     @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="400",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->testConnectionuTorrent($Organizr->apiData($request));
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+	
 });
 $app->post('/test/deluge', function ($request, $response, $args) {
 	/**

+ 21 - 0
api/v2/routes/database.php

@@ -0,0 +1,21 @@
+<?php
+$app->get('/database/journal', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->qualifyRequest(1, true)) {
+		$Organizr->getJournalMode();
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->put('/database/journal/{option}', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->qualifyRequest(1, true)) {
+		$Organizr->setJournalMode($args['option']);
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});

+ 84 - 1
api/v2/routes/homepage.php

@@ -208,6 +208,14 @@ $app->get('/homepage/qbittorrent/queue', function ($request, $response, $args) {
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 });
+$app->get('/homepage/utorrent/queue', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$Organizr->getuTorrentHomepageQueue();
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
 $app->get('/homepage/jdownloader/queue', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
 	$Organizr->getJdownloaderHomepageQueue();
@@ -340,6 +348,81 @@ $app->get('/homepage/healthchecks/{tags}', function ($request, $response, $args)
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 });
+$app->get('/homepage/overseerr/metadata/{type}/{id}', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$Organizr->getOverseerrMetadata($args['id'], $args['type']);
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->get('/homepage/overseerr/requests[/{type}[/{limit}[/{offset}]]]', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$args['limit'] = $args['limit'] ?? $Organizr->config['ombiLimit'];
+	$args['offset'] = $args['offset'] ?? 0;
+	$Organizr->getOverseerrRequests($args['limit'], $args['offset']);
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->post('/homepage/overseerr/requests/{type}/{id}[/{seasons}]', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$args['seasons'] = $args['seasons'] ?? null;
+	$Organizr->addOverseerrRequest($args['id'], $args['type'], $args['seasons']);
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->post('/homepage/overseerr/requests/{type}/{id}/available', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$Organizr->actionOverseerrRequest($args['id'], $args['type'], 'available');
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->post('/homepage/overseerr/requests/{type}/{id}/unavailable', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$Organizr->actionOverseerrRequest($args['id'], $args['type'], 'unavailable');
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->post('/homepage/overseerr/requests/{type}/{id}/pending', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$Organizr->actionOverseerrRequest($args['id'], $args['type'], 'pending');
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->post('/homepage/overseerr/requests/{type}/{id}/approve', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$Organizr->actionOverseerrRequest($args['id'], $args['type'], 'approve');
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->put('/homepage/overseerr/requests/{type}/{id}/deny', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$Organizr->actionOverseerrRequest($args['id'], $args['type'], 'deny');
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->delete('/homepage/overseerr/requests/{type}/{id}', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$Organizr->actionOverseerrRequest($args['id'], $args['type'], 'delete');
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
 $app->get('/homepage/ombi/requests[/{type}[/{limit}[/{offset}]]]', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
 	$args['type'] = $args['type'] ?? 'both';
@@ -425,7 +508,7 @@ $app->get('/homepage/jackett/{query}', function ($request, $response, $args) {
 });
 $app->post('/homepage/jackett/download/', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
-	$postData  = $request->getParsedBody();
+	$postData = $request->getParsedBody();
 	$Organizr->performJackettBackHoleDownload($postData['url']);
 	$response->getBody()->write(jsonE($GLOBALS['api']));
 	return $response

+ 44 - 0
api/v2/routes/organizr.php

@@ -0,0 +1,44 @@
+<?php
+$app->get('/organizr/{page}[/{var1}[/{var2}]]', function ($request, $response, $args) {
+	/**
+	 * @OA\Get(
+	 *     tags={"page"},
+	 *     path="/api/v2/organizr/{page}",
+	 *     summary="Get HTML for Organizr Pages",
+	 *     @OA\Parameter(
+	 *      name="page",
+	 *      description="Page to get",
+	 *      @OA\Schema(
+	 *          type="string"
+	 *      ),
+	 *      in="path",
+	 *      required=true
+	 *      ),
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/get-html"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	$_GET['organizr'] = true;
+	$_GET['vars'] = $args;
+	$page = null;
+	if ($Organizr->checkRoute($request)) {
+		$page = $Organizr->getPage($args['page']);
+	}
+	if ($page) {
+		$response->getBody()->write($page);
+		return $response
+			->withHeader('Content-Type', 'text/html;charset=UTF-8')
+			->withStatus($GLOBALS['responseCode']);
+	} else {
+		$response->getBody()->write(jsonE($GLOBALS['api']));
+		return $response
+			->withHeader('Content-Type', 'application/json;charset=UTF-8')
+			->withStatus($GLOBALS['responseCode']);
+	}
+});

+ 4 - 3
api/v2/routes/root.php

@@ -102,9 +102,9 @@ $app->any('/auth-{group}', function ($request, $response, $args) {
 });
 $app->any('/auth/[{group}[/{type}[/{ips}]]]', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
-	$_GET['group'] = $args['group'];
-	$_GET['type'] = $args['type'];
-	$_GET['ips'] = $args['ips'];
+	$_GET['group'] = $args['group'] ?? 0;
+	$_GET['type'] = $args['type'] ?? 'deny';
+	$_GET['ips'] = $args['ips'] ?? '192.0.0.0';
 	$Organizr->auth();
 	$response->getBody()->write(jsonE($GLOBALS['api']));
 	return $response
@@ -126,6 +126,7 @@ $app->get('/launch', function ($request, $response, $args) {
 	$GLOBALS['api']['response']['data']['appearance'] = $Organizr->loadAppearance();
 	$GLOBALS['api']['response']['data']['status'] = $Organizr->status();
 	$GLOBALS['api']['response']['data']['sso'] = $Organizr->ssoCookies();
+	$GLOBALS['api']['response']['data']['warnings'] = $Organizr->warnings;
 	$response->getBody()->write(jsonE($GLOBALS['api']));
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')

+ 21 - 12
api/v2/routes/token.php

@@ -1,22 +1,31 @@
 <?php
-$app->delete('/token/{id}', function ($request, $response, $args) {
+$app->get('/token/me', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
-	if ($Organizr->qualifyRequest(998, true)) {
-		$Organizr->revokeTokenByIdCurrentUser($args['id']);
+	if ($Organizr->checkRoute($request)) {
+		$GLOBALS['api']['response']['data'] = $Organizr->user;
 	}
 	$response->getBody()->write(jsonE($GLOBALS['api']));
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 });
-
 $app->post('/token/validate', function ($request, $response, $args) {
-        $Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
-        if ($Organizr->qualifyRequest(999, true)) {
-                $GLOBALS['api']['response']['data'] = $Organizr->validateToken($_REQUEST["Token"]);
-        }
-        $response->getBody()->write(jsonE($GLOBALS['api']));
-        return $response
-                ->withHeader('Content-Type', 'application/json;charset=UTF-8')
-                ->withStatus($GLOBALS['responseCode']);
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->qualifyRequest(999, true)) {
+		$GLOBALS['api']['response']['data'] = $Organizr->validateToken($_REQUEST["Token"], true);
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
 });
+$app->delete('/token/{id}', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->qualifyRequest(998, true)) {
+		$Organizr->revokeTokenByIdCurrentUser($args['id']);
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});

+ 5 - 5
api/vendor/composer/InstalledVersions.php

@@ -29,7 +29,7 @@ private static $installed = array (
     'aliases' => 
     array (
     ),
-    'reference' => '7bb47ec76fe2d84d5bb6e4102d47337ceb00fcec',
+    'reference' => 'f571aa755d372fc8205704b3cda8e1585aab0e3a',
     'name' => '__root__',
   ),
   'versions' => 
@@ -41,7 +41,7 @@ private static $installed = array (
       'aliases' => 
       array (
       ),
-      'reference' => '7bb47ec76fe2d84d5bb6e4102d47337ceb00fcec',
+      'reference' => 'f571aa755d372fc8205704b3cda8e1585aab0e3a',
     ),
     'adldap2/adldap2' => 
     array (
@@ -79,12 +79,12 @@ private static $installed = array (
     ),
     'dibi/dibi' => 
     array (
-      'pretty_version' => 'v3.2.4',
-      'version' => '3.2.4.0',
+      'pretty_version' => 'v4.2.3',
+      'version' => '4.2.3.0',
       'aliases' => 
       array (
       ),
-      'reference' => 'd571460a6f8fa1334a04f7aaa1551bb0f12c2266',
+      'reference' => '73e16eb1a322599e8cdf350adcfdbc15eaf16577',
     ),
     'doctrine/annotations' => 
     array (

+ 18 - 4
api/vendor/composer/autoload_classmap.php

@@ -9,26 +9,40 @@ return array(
     'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
     'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
     'Dibi\\Bridges\\Nette\\DibiExtension22' => $vendorDir . '/dibi/dibi/src/Dibi/Bridges/Nette/DibiExtension22.php',
+    'Dibi\\Bridges\\Tracy\\Panel' => $vendorDir . '/dibi/dibi/src/Dibi/Bridges/Tracy/Panel.php',
     'Dibi\\Connection' => $vendorDir . '/dibi/dibi/src/Dibi/Connection.php',
     'Dibi\\ConstraintViolationException' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',
     'Dibi\\DataSource' => $vendorDir . '/dibi/dibi/src/Dibi/DataSource.php',
     'Dibi\\DateTime' => $vendorDir . '/dibi/dibi/src/Dibi/DateTime.php',
     'Dibi\\Driver' => $vendorDir . '/dibi/dibi/src/Dibi/interfaces.php',
     'Dibi\\DriverException' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',
+    'Dibi\\Drivers\\DummyDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/DummyDriver.php',
     'Dibi\\Drivers\\FirebirdDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/FirebirdDriver.php',
-    'Dibi\\Drivers\\MsSqlDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/MsSqlDriver.php',
-    'Dibi\\Drivers\\MsSqlReflector' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/MsSqlReflector.php',
-    'Dibi\\Drivers\\MySqlDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/MySqlDriver.php',
+    'Dibi\\Drivers\\FirebirdReflector' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/FirebirdReflector.php',
+    'Dibi\\Drivers\\FirebirdResult' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/FirebirdResult.php',
     'Dibi\\Drivers\\MySqlReflector' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/MySqlReflector.php',
     'Dibi\\Drivers\\MySqliDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/MySqliDriver.php',
+    'Dibi\\Drivers\\MySqliResult' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/MySqliResult.php',
+    'Dibi\\Drivers\\NoDataResult' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/NoDataResult.php',
     'Dibi\\Drivers\\OdbcDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/OdbcDriver.php',
+    'Dibi\\Drivers\\OdbcReflector' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/OdbcReflector.php',
+    'Dibi\\Drivers\\OdbcResult' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/OdbcResult.php',
     'Dibi\\Drivers\\OracleDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/OracleDriver.php',
+    'Dibi\\Drivers\\OracleReflector' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/OracleReflector.php',
+    'Dibi\\Drivers\\OracleResult' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/OracleResult.php',
     'Dibi\\Drivers\\PdoDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/PdoDriver.php',
+    'Dibi\\Drivers\\PdoResult' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/PdoResult.php',
     'Dibi\\Drivers\\PostgreDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/PostgreDriver.php',
+    'Dibi\\Drivers\\PostgreReflector' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/PostgreReflector.php',
+    'Dibi\\Drivers\\PostgreResult' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/PostgreResult.php',
     'Dibi\\Drivers\\Sqlite3Driver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/Sqlite3Driver.php',
+    'Dibi\\Drivers\\Sqlite3Result' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/Sqlite3Result.php',
+    'Dibi\\Drivers\\SqliteDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/SqliteDriver.php',
     'Dibi\\Drivers\\SqliteReflector' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/SqliteReflector.php',
+    'Dibi\\Drivers\\SqliteResult' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/SqliteResult.php',
     'Dibi\\Drivers\\SqlsrvDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/SqlsrvDriver.php',
     'Dibi\\Drivers\\SqlsrvReflector' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/SqlsrvReflector.php',
+    'Dibi\\Drivers\\SqlsrvResult' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/SqlsrvResult.php',
     'Dibi\\Event' => $vendorDir . '/dibi/dibi/src/Dibi/Event.php',
     'Dibi\\Exception' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',
     'Dibi\\Expression' => $vendorDir . '/dibi/dibi/src/Dibi/Expression.php',
@@ -37,10 +51,10 @@ return array(
     'Dibi\\HashMap' => $vendorDir . '/dibi/dibi/src/Dibi/HashMap.php',
     'Dibi\\HashMapBase' => $vendorDir . '/dibi/dibi/src/Dibi/HashMap.php',
     'Dibi\\Helpers' => $vendorDir . '/dibi/dibi/src/Dibi/Helpers.php',
+    'Dibi\\IConnection' => $vendorDir . '/dibi/dibi/src/Dibi/interfaces.php',
     'Dibi\\IDataSource' => $vendorDir . '/dibi/dibi/src/Dibi/interfaces.php',
     'Dibi\\Literal' => $vendorDir . '/dibi/dibi/src/Dibi/Literal.php',
     'Dibi\\Loggers\\FileLogger' => $vendorDir . '/dibi/dibi/src/Dibi/Loggers/FileLogger.php',
-    'Dibi\\Loggers\\FirePhpLogger' => $vendorDir . '/dibi/dibi/src/Dibi/Loggers/FirePhpLogger.php',
     'Dibi\\NotImplementedException' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',
     'Dibi\\NotNullConstraintViolationException' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',
     'Dibi\\NotSupportedException' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',

+ 0 - 1
api/vendor/composer/autoload_files.php

@@ -21,6 +21,5 @@ return array(
     'bd9634f2d41831496de0d3dfe4c94881' => $vendorDir . '/symfony/polyfill-php56/bootstrap.php',
     'fe62ba7e10580d903cc46d808b5961a4' => $vendorDir . '/tightenco/collect/src/Collect/Support/helpers.php',
     'caf31cc6ec7cf2241cb6f12c226c3846' => $vendorDir . '/tightenco/collect/src/Collect/Support/alias.php',
-    '0097ca414fcb37c7130ac24b05f485f8' => $vendorDir . '/dibi/dibi/src/loader.php',
     '0ccdf99b8f62f02c52cba55802e0c2e7' => $vendorDir . '/zircote/swagger-php/src/functions.php',
 );

+ 18 - 5
api/vendor/composer/autoload_static.php

@@ -22,7 +22,6 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         'bd9634f2d41831496de0d3dfe4c94881' => __DIR__ . '/..' . '/symfony/polyfill-php56/bootstrap.php',
         'fe62ba7e10580d903cc46d808b5961a4' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/helpers.php',
         'caf31cc6ec7cf2241cb6f12c226c3846' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/alias.php',
-        '0097ca414fcb37c7130ac24b05f485f8' => __DIR__ . '/..' . '/dibi/dibi/src/loader.php',
         '0ccdf99b8f62f02c52cba55802e0c2e7' => __DIR__ . '/..' . '/zircote/swagger-php/src/functions.php',
     );
 
@@ -320,26 +319,40 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
         'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
         'Dibi\\Bridges\\Nette\\DibiExtension22' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Bridges/Nette/DibiExtension22.php',
+        'Dibi\\Bridges\\Tracy\\Panel' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Bridges/Tracy/Panel.php',
         'Dibi\\Connection' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Connection.php',
         'Dibi\\ConstraintViolationException' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',
         'Dibi\\DataSource' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/DataSource.php',
         'Dibi\\DateTime' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/DateTime.php',
         'Dibi\\Driver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/interfaces.php',
         'Dibi\\DriverException' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',
+        'Dibi\\Drivers\\DummyDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/DummyDriver.php',
         'Dibi\\Drivers\\FirebirdDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/FirebirdDriver.php',
-        'Dibi\\Drivers\\MsSqlDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/MsSqlDriver.php',
-        'Dibi\\Drivers\\MsSqlReflector' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/MsSqlReflector.php',
-        'Dibi\\Drivers\\MySqlDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/MySqlDriver.php',
+        'Dibi\\Drivers\\FirebirdReflector' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/FirebirdReflector.php',
+        'Dibi\\Drivers\\FirebirdResult' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/FirebirdResult.php',
         'Dibi\\Drivers\\MySqlReflector' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/MySqlReflector.php',
         'Dibi\\Drivers\\MySqliDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/MySqliDriver.php',
+        'Dibi\\Drivers\\MySqliResult' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/MySqliResult.php',
+        'Dibi\\Drivers\\NoDataResult' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/NoDataResult.php',
         'Dibi\\Drivers\\OdbcDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/OdbcDriver.php',
+        'Dibi\\Drivers\\OdbcReflector' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/OdbcReflector.php',
+        'Dibi\\Drivers\\OdbcResult' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/OdbcResult.php',
         'Dibi\\Drivers\\OracleDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/OracleDriver.php',
+        'Dibi\\Drivers\\OracleReflector' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/OracleReflector.php',
+        'Dibi\\Drivers\\OracleResult' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/OracleResult.php',
         'Dibi\\Drivers\\PdoDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/PdoDriver.php',
+        'Dibi\\Drivers\\PdoResult' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/PdoResult.php',
         'Dibi\\Drivers\\PostgreDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/PostgreDriver.php',
+        'Dibi\\Drivers\\PostgreReflector' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/PostgreReflector.php',
+        'Dibi\\Drivers\\PostgreResult' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/PostgreResult.php',
         'Dibi\\Drivers\\Sqlite3Driver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/Sqlite3Driver.php',
+        'Dibi\\Drivers\\Sqlite3Result' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/Sqlite3Result.php',
+        'Dibi\\Drivers\\SqliteDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/SqliteDriver.php',
         'Dibi\\Drivers\\SqliteReflector' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/SqliteReflector.php',
+        'Dibi\\Drivers\\SqliteResult' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/SqliteResult.php',
         'Dibi\\Drivers\\SqlsrvDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/SqlsrvDriver.php',
         'Dibi\\Drivers\\SqlsrvReflector' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/SqlsrvReflector.php',
+        'Dibi\\Drivers\\SqlsrvResult' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/SqlsrvResult.php',
         'Dibi\\Event' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Event.php',
         'Dibi\\Exception' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',
         'Dibi\\Expression' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Expression.php',
@@ -348,10 +361,10 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         'Dibi\\HashMap' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/HashMap.php',
         'Dibi\\HashMapBase' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/HashMap.php',
         'Dibi\\Helpers' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Helpers.php',
+        'Dibi\\IConnection' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/interfaces.php',
         'Dibi\\IDataSource' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/interfaces.php',
         'Dibi\\Literal' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Literal.php',
         'Dibi\\Loggers\\FileLogger' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Loggers/FileLogger.php',
-        'Dibi\\Loggers\\FirePhpLogger' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Loggers/FirePhpLogger.php',
         'Dibi\\NotImplementedException' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',
         'Dibi\\NotNullConstraintViolationException' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',
         'Dibi\\NotSupportedException' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',

+ 17 - 14
api/vendor/composer/installed.json

@@ -211,50 +211,49 @@
         },
         {
             "name": "dibi/dibi",
-            "version": "v3.2.4",
-            "version_normalized": "3.2.4.0",
+            "version": "v4.2.3",
+            "version_normalized": "4.2.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/dg/dibi.git",
-                "reference": "d571460a6f8fa1334a04f7aaa1551bb0f12c2266"
+                "reference": "73e16eb1a322599e8cdf350adcfdbc15eaf16577"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/dg/dibi/zipball/d571460a6f8fa1334a04f7aaa1551bb0f12c2266",
-                "reference": "d571460a6f8fa1334a04f7aaa1551bb0f12c2266",
+                "url": "https://api.github.com/repos/dg/dibi/zipball/73e16eb1a322599e8cdf350adcfdbc15eaf16577",
+                "reference": "73e16eb1a322599e8cdf350adcfdbc15eaf16577",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.4.4"
+                "php": ">=7.2"
             },
             "replace": {
                 "dg/dibi": "*"
             },
             "require-dev": {
-                "nette/tester": "~1.7",
+                "nette/di": "^3.0",
+                "nette/tester": "~2.0",
+                "phpstan/phpstan": "^0.12",
                 "tracy/tracy": "~2.2"
             },
-            "time": "2020-03-26T03:05:01+00:00",
+            "time": "2021-07-23T08:49:27+00:00",
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.2-dev"
+                    "dev-master": "4.2-dev"
                 }
             },
             "installation-source": "dist",
             "autoload": {
                 "classmap": [
                     "src/"
-                ],
-                "files": [
-                    "src/loader.php"
                 ]
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
                 "BSD-3-Clause",
-                "GPL-2.0",
-                "GPL-3.0"
+                "GPL-2.0-only",
+                "GPL-3.0-only"
             ],
             "authors": [
                 {
@@ -277,6 +276,10 @@
                 "sqlite",
                 "sqlsrv"
             ],
+            "support": {
+                "issues": "https://github.com/dg/dibi/issues",
+                "source": "https://github.com/dg/dibi/tree/v4.2.3"
+            },
             "install-path": "../dibi/dibi"
         },
         {

+ 5 - 5
api/vendor/composer/installed.php

@@ -6,7 +6,7 @@
     'aliases' => 
     array (
     ),
-    'reference' => '7bb47ec76fe2d84d5bb6e4102d47337ceb00fcec',
+    'reference' => 'f571aa755d372fc8205704b3cda8e1585aab0e3a',
     'name' => '__root__',
   ),
   'versions' => 
@@ -18,7 +18,7 @@
       'aliases' => 
       array (
       ),
-      'reference' => '7bb47ec76fe2d84d5bb6e4102d47337ceb00fcec',
+      'reference' => 'f571aa755d372fc8205704b3cda8e1585aab0e3a',
     ),
     'adldap2/adldap2' => 
     array (
@@ -56,12 +56,12 @@
     ),
     'dibi/dibi' => 
     array (
-      'pretty_version' => 'v3.2.4',
-      'version' => '3.2.4.0',
+      'pretty_version' => 'v4.2.3',
+      'version' => '4.2.3.0',
       'aliases' => 
       array (
       ),
-      'reference' => 'd571460a6f8fa1334a04f7aaa1551bb0f12c2266',
+      'reference' => '73e16eb1a322599e8cdf350adcfdbc15eaf16577',
     ),
     'doctrine/annotations' => 
     array (

+ 48 - 0
api/vendor/dibi/dibi/appveyor.yml

@@ -0,0 +1,48 @@
+build: off
+cache:
+    - c:\php7 -> appveyor.yml
+    - '%LOCALAPPDATA%\Composer\files -> appveyor.yml'
+
+clone_folder: c:\projects\dibi
+
+services:
+    - mssql2012sp1
+#    - mssql2014
+    - mysql
+
+init:
+    - SET PATH=c:\php7;%PATH%
+    - SET ANSICON=121x90 (121x90)
+
+install:
+    # Install PHP 7.2
+    - IF EXIST c:\php7 (SET PHP=0) ELSE (SET PHP=1)
+    - IF %PHP%==1 mkdir c:\php7
+    - IF %PHP%==1 cd c:\php7
+    - IF %PHP%==1 curl https://windows.php.net/downloads/releases/archives/php-7.2.18-Win32-VC15-x64.zip --output php.zip
+    - IF %PHP%==1 7z x php.zip >nul
+    - IF %PHP%==1 echo extension_dir=ext >> php.ini
+    - IF %PHP%==1 echo extension=php_openssl.dll >> php.ini
+    - IF %PHP%==1 curl https://github.com/microsoft/msphpsql/releases/download/v5.8.0/Windows-7.2.zip -L --output sqlsrv.zip
+    - IF %PHP%==1 7z x sqlsrv.zip >nul
+    - IF %PHP%==1 copy Windows-7.2\x64\php_sqlsrv_72_ts.dll ext\php_sqlsrv_ts.dll
+    - IF %PHP%==1 del /Q *.zip
+
+    # Install Microsoft Access Database Engine x64
+    - IF %PHP%==1 curl https://download.microsoft.com/download/2/4/3/24375141-E08D-4803-AB0E-10F2E3A07AAA/AccessDatabaseEngine_X64.exe --output AccessDatabaseEngine_X64.exe
+    - cmd /c start /wait AccessDatabaseEngine_X64.exe /passive
+
+    # Install Nette Tester
+    - cd c:\projects\dibi
+    - appveyor DownloadFile https://getcomposer.org/composer.phar
+    - php composer.phar install --prefer-dist --no-interaction --no-progress
+
+    # Create databases.ini
+    - copy tests\databases.appveyor.ini tests\databases.ini
+
+test_script:
+    - vendor\bin\tester tests -s -p c:\php7\php -c tests\php-win.ini
+
+on_failure:
+    # Print *.actual content
+    - for /r %%x in (*.actual) do ( type "%%x" )

+ 11 - 6
api/vendor/dibi/dibi/composer.json

@@ -3,7 +3,7 @@
 	"description": "Dibi is Database Abstraction Library for PHP",
 	"keywords": ["database", "dbal", "mysql", "postgresql", "sqlite", "mssql", "sqlsrv", "oracle", "access", "pdo", "odbc"],
 	"homepage": "https://dibiphp.com",
-	"license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"],
+	"license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"],
 	"authors": [
 		{
 			"name": "David Grudl",
@@ -11,22 +11,27 @@
 		}
 	],
 	"require": {
-		"php": ">=5.4.4"
+		"php": ">=7.2"
 	},
 	"require-dev": {
 		"tracy/tracy": "~2.2",
-		"nette/tester": "~1.7"
+		"nette/tester": "~2.0",
+		"nette/di": "^3.0",
+		"phpstan/phpstan": "^0.12"
 	},
 	"replace": {
 		"dg/dibi": "*"
 	},
 	"autoload": {
-		"classmap": ["src/"],
-		"files": ["src/loader.php"]
+		"classmap": ["src/"]
+	},
+	"scripts": {
+		"phpstan": "phpstan analyse",
+		"tester": "tester tests -s"
 	},
 	"extra": {
 		"branch-alias": {
-			"dev-master": "3.2-dev"
+			"dev-master": "4.2-dev"
 		}
 	}
 }

+ 7 - 31
api/vendor/dibi/dibi/examples/connecting-to-databases.php

@@ -1,3 +1,6 @@
+<?php
+declare(strict_types=1);
+?>
 <!DOCTYPE html><link rel="stylesheet" href="data/style.css">
 
 <h1>Connecting to Databases | Dibi</h1>
@@ -13,7 +16,7 @@ if (@!include __DIR__ . '/../vendor/autoload.php') {
 echo '<p>Connecting to Sqlite: ';
 try {
 	dibi::connect([
-		'driver' => 'sqlite3',
+		'driver' => 'sqlite',
 		'database' => 'data/sample.s3db',
 	]);
 	echo 'OK';
@@ -27,7 +30,7 @@ echo "</p>\n";
 echo '<p>Connecting to Sqlite: ';
 try {
 	$connection = new Dibi\Connection([
-		'driver' => 'sqlite3',
+		'driver' => 'sqlite',
 		'database' => 'data/sample.s3db',
 	]);
 	echo 'OK';
@@ -37,18 +40,7 @@ try {
 echo "</p>\n";
 
 
-// connects to MySQL using DSN
-echo '<p>Connecting to MySQL: ';
-try {
-	dibi::connect('driver=mysql&host=localhost&username=root&password=xxx&database=test&charset=cp1250');
-	echo 'OK';
-} catch (Dibi\Exception $e) {
-	echo get_class($e), ': ', $e->getMessage(), "\n";
-}
-echo "</p>\n";
-
-
-// connects to MySQLi using array
+// connects to MySQLi
 echo '<p>Connecting to MySQLi: ';
 try {
 	dibi::connect([
@@ -76,7 +68,7 @@ try {
 		'driver' => 'odbc',
 		'username' => 'root',
 		'password' => '***',
-		'dsn' => 'Driver={Microsoft Access Driver (*.mdb)};Dbq=' . __DIR__ . '/data/sample.mdb',
+		'dsn' => 'Driver={Microsoft Access Driver (*.mdb, *.accdb)};Dbq=' . __DIR__ . '/data/sample.mdb',
 	]);
 	echo 'OK';
 } catch (Dibi\Exception $e) {
@@ -114,22 +106,6 @@ try {
 echo "</p>\n";
 
 
-// connects to MS SQL
-echo '<p>Connecting to MS SQL: ';
-try {
-	dibi::connect([
-		'driver' => 'mssql',
-		'host' => 'localhost',
-		'username' => 'root',
-		'password' => 'xxx',
-	]);
-	echo 'OK';
-} catch (Dibi\Exception $e) {
-	echo get_class($e), ': ', $e->getMessage(), "\n";
-}
-echo "</p>\n";
-
-
 // connects to SQLSRV
 echo '<p>Connecting to Microsoft SQL Server: ';
 try {

BIN
api/vendor/dibi/dibi/examples/data/sample.s3db


+ 4 - 1
api/vendor/dibi/dibi/examples/database-reflection.php

@@ -1,3 +1,6 @@
+<?php
+declare(strict_types=1);
+?>
 <!DOCTYPE html><link rel="stylesheet" href="data/style.css">
 
 <h1>Database Reflection | Dibi</h1>
@@ -10,7 +13,7 @@ if (@!include __DIR__ . '/../vendor/autoload.php') {
 
 
 $dibi = new Dibi\Connection([
-	'driver' => 'sqlite3',
+	'driver' => 'sqlite',
 	'database' => 'data/sample.s3db',
 ]);
 

+ 4 - 1
api/vendor/dibi/dibi/examples/dumping-sql-and-result-set.php

@@ -1,3 +1,6 @@
+<?php
+declare(strict_types=1);
+?>
 <!DOCTYPE html><link rel="stylesheet" href="data/style.css">
 
 <h1>Dumping SQL and Result Set | Dibi</h1>
@@ -10,7 +13,7 @@ if (@!include __DIR__ . '/../vendor/autoload.php') {
 
 
 $dibi = new Dibi\Connection([
-	'driver' => 'sqlite3',
+	'driver' => 'sqlite',
 	'database' => 'data/sample.s3db',
 ]);
 

+ 2 - 1
api/vendor/dibi/dibi/examples/fetching-examples.php

@@ -1,4 +1,5 @@
 <?php
+declare(strict_types=1);
 
 if (@!include __DIR__ . '/../vendor/autoload.php') {
 	die('Install dependencies using `composer install --dev`');
@@ -14,7 +15,7 @@ Tracy\Debugger::enable();
 <?php
 
 $dibi = new Dibi\Connection([
-	'driver' => 'sqlite3',
+	'driver' => 'sqlite',
 	'database' => 'data/sample.s3db',
 ]);
 

+ 4 - 1
api/vendor/dibi/dibi/examples/importing-dump-from-file.php

@@ -1,3 +1,6 @@
+<?php
+declare(strict_types=1);
+?>
 <!DOCTYPE html><link rel="stylesheet" href="data/style.css">
 
 <h1>Importing SQL Dump from File | Dibi</h1>
@@ -10,7 +13,7 @@ if (@!include __DIR__ . '/../vendor/autoload.php') {
 
 
 $dibi = new Dibi\Connection([
-	'driver' => 'sqlite3',
+	'driver' => 'sqlite',
 	'database' => 'data/sample.s3db',
 ]);
 

+ 4 - 1
api/vendor/dibi/dibi/examples/query-language-and-conditions.php

@@ -1,3 +1,6 @@
+<?php
+declare(strict_types=1);
+?>
 <!DOCTYPE html><link rel="stylesheet" href="data/style.css">
 
 <h1>Query Language & Conditions | Dibi</h1>
@@ -10,7 +13,7 @@ if (@!include __DIR__ . '/../vendor/autoload.php') {
 
 
 $dibi = new Dibi\Connection([
-	'driver' => 'sqlite3',
+	'driver' => 'sqlite',
 	'database' => 'data/sample.s3db',
 ]);
 

+ 4 - 1
api/vendor/dibi/dibi/examples/query-language-basic-examples.php

@@ -1,3 +1,6 @@
+<?php
+declare(strict_types=1);
+?>
 <!DOCTYPE html><link rel="stylesheet" href="data/style.css">
 
 <h1>Query Language Basic Examples | Dibi</h1>
@@ -12,7 +15,7 @@ date_default_timezone_set('Europe/Prague');
 
 
 $dibi = new Dibi\Connection([
-	'driver' => 'sqlite3',
+	'driver' => 'sqlite',
 	'database' => 'data/sample.s3db',
 ]);
 

+ 2 - 1
api/vendor/dibi/dibi/examples/result-set-data-types.php

@@ -1,4 +1,5 @@
 <?php
+declare(strict_types=1);
 
 use Dibi\Type;
 
@@ -18,7 +19,7 @@ date_default_timezone_set('Europe/Prague');
 <?php
 
 $dibi = new Dibi\Connection([
-	'driver' => 'sqlite3',
+	'driver' => 'sqlite',
 	'database' => 'data/sample.s3db',
 ]);
 

+ 2 - 4
api/vendor/dibi/dibi/examples/tracy-and-exceptions.php

@@ -1,4 +1,5 @@
 <?php
+declare(strict_types=1);
 
 if (@!include __DIR__ . '/../vendor/autoload.php') {
 	die('Install dependencies using `composer install --dev`');
@@ -10,11 +11,8 @@ Tracy\Debugger::enable();
 
 
 $dibi = new Dibi\Connection([
-	'driver' => 'sqlite3',
+	'driver' => 'sqlite',
 	'database' => 'data/sample.s3db',
-	'profiler' => [
-		'run' => true,
-	],
 ]);
 
 

+ 2 - 4
api/vendor/dibi/dibi/examples/tracy.php

@@ -1,4 +1,5 @@
 <?php
+declare(strict_types=1);
 
 if (@!include __DIR__ . '/../vendor/autoload.php') {
 	die('Install dependencies using `composer install --dev`');
@@ -10,11 +11,8 @@ Tracy\Debugger::enable();
 
 
 $dibi = new Dibi\Connection([
-	'driver' => 'sqlite3',
+	'driver' => 'sqlite',
 	'database' => 'data/sample.s3db',
-	'profiler' => [
-		'run' => true,
-	],
 ]);
 
 

+ 4 - 1
api/vendor/dibi/dibi/examples/using-datetime.php

@@ -1,3 +1,6 @@
+<?php
+declare(strict_types=1);
+?>
 <!DOCTYPE html><link rel="stylesheet" href="data/style.css">
 
 <h1>Using DateTime | Dibi</h1>
@@ -13,7 +16,7 @@ date_default_timezone_set('Europe/Prague');
 
 // CHANGE TO REAL PARAMETERS!
 $dibi = new Dibi\Connection([
-	'driver' => 'sqlite3',
+	'driver' => 'sqlite',
 	'database' => 'data/sample.s3db',
 	'formatDate' => "'Y-m-d'",
 	'formatDateTime' => "'Y-m-d H-i-s'",

+ 0 - 33
api/vendor/dibi/dibi/examples/using-extension-methods.php

@@ -1,33 +0,0 @@
-<?php
-
-if (@!include __DIR__ . '/../vendor/autoload.php') {
-	die('Install dependencies using `composer install --dev`');
-}
-
-Tracy\Debugger::enable();
-
-?>
-<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
-
-<h1>Using Extension Methods | Dibi</h1>
-
-<?php
-
-$dibi = new Dibi\Connection([
-	'driver' => 'sqlite3',
-	'database' => 'data/sample.s3db',
-]);
-
-
-// using the "prototype" to add custom method to class Dibi\Result
-Dibi\Result::extensionMethod('fetchShuffle', function (Dibi\Result $obj) {
-	$all = $obj->fetchAll();
-	shuffle($all);
-	return $all;
-});
-
-
-// fetch complete result set shuffled
-$res = $dibi->query('SELECT * FROM [customers]');
-$all = $res->fetchShuffle();
-Tracy\Dumper::dump($all);

+ 4 - 1
api/vendor/dibi/dibi/examples/using-fluent-syntax.php

@@ -1,3 +1,6 @@
+<?php
+declare(strict_types=1);
+?>
 <!DOCTYPE html><link rel="stylesheet" href="data/style.css">
 
 <h1>Using Fluent Syntax | Dibi</h1>
@@ -12,7 +15,7 @@ date_default_timezone_set('Europe/Prague');
 
 
 $dibi = new Dibi\Connection([
-	'driver' => 'sqlite3',
+	'driver' => 'sqlite',
 	'database' => 'data/sample.s3db',
 ]);
 

+ 4 - 1
api/vendor/dibi/dibi/examples/using-limit-and-offset.php

@@ -1,3 +1,6 @@
+<?php
+declare(strict_types=1);
+?>
 <!DOCTYPE html><link rel="stylesheet" href="data/style.css">
 
 <h1>Using Limit & Offset | Dibi</h1>
@@ -10,7 +13,7 @@ if (@!include __DIR__ . '/../vendor/autoload.php') {
 
 
 $dibi = new Dibi\Connection([
-	'driver' => 'sqlite3',
+	'driver' => 'sqlite',
 	'database' => 'data/sample.s3db',
 ]);
 

+ 8 - 5
api/vendor/dibi/dibi/examples/using-logger.php

@@ -1,3 +1,6 @@
+<?php
+declare(strict_types=1);
+?>
 <!DOCTYPE html><link rel="stylesheet" href="data/style.css">
 
 <h1>Using Logger | Dibi</h1>
@@ -12,12 +15,12 @@ date_default_timezone_set('Europe/Prague');
 
 
 $dibi = new Dibi\Connection([
-	'driver' => 'sqlite3',
+	'driver' => 'sqlite',
 	'database' => 'data/sample.s3db',
 	// enable query logging to this file
 	'profiler' => [
-		'run' => true,
-		'file' => 'data/log.sql',
+		'file' => 'log/log.sql',
+		'errorsOnly' => false,
 	],
 ]);
 
@@ -34,6 +37,6 @@ try {
 
 
 // outputs a log file
-echo '<h2>File data/log.sql:</h2>';
+echo '<h2>File log/log.sql:</h2>';
 
-echo '<pre>', file_get_contents('data/log.sql'), '</pre>';
+echo '<pre>', file_get_contents('log/log.sql'), '</pre>';

+ 0 - 45
api/vendor/dibi/dibi/examples/using-profiler.php

@@ -1,45 +0,0 @@
-<?php ob_start() // needed by FirePHP ?>
-
-<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
-
-<h1>Using Profiler | Dibi</h1>
-
-<?php
-
-if (@!include __DIR__ . '/../vendor/autoload.php') {
-	die('Install packages using `composer install`');
-}
-
-
-$dibi = new Dibi\Connection([
-	'driver' => 'sqlite3',
-	'database' => 'data/sample.s3db',
-	'profiler' => [
-		'run' => true,
-	],
-]);
-
-
-// execute some queries...
-for ($i = 0; $i < 20; $i++) {
-	$res = $dibi->query('SELECT * FROM [customers] WHERE [customer_id] < ?', $i);
-}
-
-// display output
-?>
-<p>Last query: <strong><?php echo dibi::$sql; ?></strong></p>
-
-<p>Number of queries: <strong><?php echo dibi::$numOfQueries; ?></strong></p>
-
-<p>Elapsed time for last query: <strong><?php echo sprintf('%0.3f', dibi::$elapsedTime * 1000); ?> ms</strong></p>
-
-<p>Total elapsed time: <strong><?php echo sprintf('%0.3f', dibi::$totalTime * 1000); ?> ms</strong></p>
-
-<br>
-
-<p>Dibi can log to your Firebug Console. You first need to install the Firefox, Firebug and FirePHP extensions. You can install them from here:</p>
-
-<ul>
-	<li>Firebug: https://addons.mozilla.org/en-US/firefox/addon/1843
-	<li>FirePHP: http://www.firephp.org/
-</ul>

+ 4 - 1
api/vendor/dibi/dibi/examples/using-substitutions.php

@@ -1,3 +1,6 @@
+<?php
+declare(strict_types=1);
+?>
 <!DOCTYPE html><link rel="stylesheet" href="data/style.css">
 
 <h1>Using Substitutions | Dibi</h1>
@@ -10,7 +13,7 @@ if (@!include __DIR__ . '/../vendor/autoload.php') {
 
 
 $dibi = new Dibi\Connection([
-	'driver' => 'sqlite3',
+	'driver' => 'sqlite',
 	'database' => 'data/sample.s3db',
 ]);
 

+ 4 - 1
api/vendor/dibi/dibi/examples/using-transactions.php

@@ -1,3 +1,6 @@
+<?php
+declare(strict_types=1);
+?>
 <!DOCTYPE html><link rel="stylesheet" href="data/style.css">
 
 <h1>Using Transactions | Dibi</h1>
@@ -10,7 +13,7 @@ if (@!include __DIR__ . '/../vendor/autoload.php') {
 
 
 $dibi = new Dibi\Connection([
-	'driver' => 'sqlite3',
+	'driver' => 'sqlite',
 	'database' => 'data/sample.s3db',
 ]);
 

+ 588 - 65
api/vendor/dibi/dibi/readme.md

@@ -2,7 +2,7 @@
 =========================================================
 
 [![Downloads this Month](https://img.shields.io/packagist/dm/dibi/dibi.svg)](https://packagist.org/packages/dibi/dibi)
-[![Build Status](https://travis-ci.org/dg/dibi.svg?branch=master)](https://travis-ci.org/dg/dibi)
+[![Tests](https://github.com/dg/dibi/workflows/Tests/badge.svg?branch=master)](https://github.com/dg/dibi/actions)
 [![Build Status Windows](https://ci.appveyor.com/api/projects/status/github/dg/dibi?branch=master&svg=true)](https://ci.appveyor.com/project/dg/dibi/branch/master)
 [![Latest Stable Version](https://poser.pugx.org/dibi/dibi/v/stable)](https://github.com/dg/dibi/releases)
 [![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/dg/dibi/blob/master/license.md)
@@ -14,19 +14,27 @@ Introduction
 Database access functions in PHP are not standardised. This library
 hides the differences between them, and above all, it gives you a very handy interface.
 
-If you like Dibi, **[please make a donation now](https://nette.org/make-donation?to=dibi)**. Thank you!
+
+Support Me
+----------
+
+Do you like Dibi? Are you looking forward to the new features?
+
+[![Buy me a coffee](https://files.nette.org/icons/donation-3.svg)](https://github.com/sponsors/dg)
+
+Thank you!
 
 
 Installation
 ------------
 
-The recommended way to install Dibi is via Composer (alternatively you can [download package](https://github.com/dg/dibi/releases)):
+Install Dibi via Composer:
 
 ```bash
 composer require dibi/dibi
 ```
 
-The Dibi 3.x requires PHP version 5.4.4 and supports PHP up to 7.2.
+The Dibi 4.2 requires PHP version 7.2 and supports PHP up to 8.0.
 
 
 Usage
@@ -35,107 +43,622 @@ Usage
 Refer to the `examples` directory for examples. Dibi documentation is
 available on the [homepage](https://dibiphp.com).
 
-Connect to database:
+
+### Connecting to database
+
+The database connection is represented by the object `Dibi\Connection`:
+
+```php
+$database = new Dibi\Connection([
+	'driver'   => 'mysqli',
+	'host'     => 'localhost',
+	'username' => 'root',
+	'password' => '***',
+	'database' => 'table',
+]);
+
+$result = $database->query('SELECT * FROM users');
+```
+
+Alternatively, you can use the `dibi` static register, which maintains a connection object in a globally available storage and calls all the functions above it:
 
 ```php
-$dibi = new Dibi\Connection([
-    'driver'   => 'mysqli',
-    'host'     => 'localhost',
-    'username' => 'root',
-    'password' => '***',
+dibi::connect([
+	'driver'   => 'mysqli',
+	'host'     => 'localhost',
+	'username' => 'root',
+	'password' => '***',
+	'database' => 'test',
+	'charset'  => 'utf8',
 ]);
 
+$result = dibi::query('SELECT * FROM users');
+```
+
+In the event of a connection error, it throws `Dibi\Exception`.
+
+
+
+### Queries
+
+We query the database queries by the method `query()` which returns `Dibi\Result`. Rows are objects `Dibi\Row`.
+
+You can try all the examples [online at the playground](https://repl.it/@DavidGrudl/dibi-playground).
+
+```php
+$result = $database->query('SELECT * FROM users');
+
+foreach ($result as $row) {
+	echo $row->id;
+	echo $row->name;
+}
+
+// array of all rows
+$all = $result->fetchAll();
+
+// array of all rows, key is 'id'
+$all = $result->fetchAssoc('id');
+
+// associative pairs id => name
+$pairs = $result->fetchPairs('id', 'name');
+
+// the number of rows of the result, if known, or number of affected rows
+$count = $result->getRowCount();
+```
+
+Method fetchAssoc() can return a more complex associative array.
+
+You can easily add parameters to the query, note the question mark:
+
+```php
+$result = $database->query('SELECT * FROM users WHERE name = ? AND active = ?', $name, $active);
+
+// or
+$result = $database->query('SELECT * FROM users WHERE name = ?', $name, 'AND active = ?', $active););
+
+$ids = [10, 20, 30];
+$result = $database->query('SELECT * FROM users WHERE id IN (?)', $ids);
+```
+
+**WARNING: Never concatenate parameters to SQL. It would create a [SQL injection](https://en.wikipedia.org/wiki/SQL_injection)** vulnerability.
+```
+$result = $database->query('SELECT * FROM users WHERE id = ' . $id); // BAD!!!
+```
+
+Instead of a question mark, so-called modifiers can be used.
+
+```php
+$result = $database->query('SELECT * FROM users WHERE name = %s', $name);
+```
+
+In case of failure `query()` throws `Dibi\Exception`, or one of the descendants:
+
+- `ConstraintViolationException` - violation of a table constraint
+- `ForeignKeyConstraintViolationException` - invalid foreign key
+- `NotNullConstraintViolationException` - violation of the NOT NULL condition
+- `UniqueConstraintViolationException` - collides unique index
+
+You can use also shortcuts:
+
+```php
+// returns associative pairs id => name, shortcut for query(...)->fetchPairs()
+$pairs = $database->fetchPairs('SELECT id, name FROM users');
+
+// returns array of all rows, shortcut for query(...)->fetchAll()
+$rows = $database->fetchAll('SELECT * FROM users');
+
+// returns row, shortcut for query(...)->fetch()
+$row = $database->fetch('SELECT * FROM users WHERE id = ?', $id);
+
+// returns field, shortcut for query(...)->fetchSingle()
+$name = $database->fetchSingle('SELECT name FROM users WHERE id = ?', $id);
+```
+
+
+### Modifiers
+
+In addition to the `?` wildcard char, we can also use modifiers:
+
+| modifier | description
+|----------|-----
+| %s | string
+| %sN | string, but '' translates as NULL
+| %bin | binary data
+| %b | boolean
+| %i | integer
+| %iN | integer, but 0 is translates as NULL
+| %f | float
+| %d | date (accepts DateTime, string or UNIX timestamp)
+| %dt | datetime (accepts DateTime, string or UNIX timestamp)
+| %n | identifier, ie the name of the table or column
+| %N | identifier, treats period as a common character, ie alias or a database name (`%n AS %N` or `DROP DATABASE %N`)
+| %SQL | SQL - directly inserts into SQL (the alternative is Dibi\Literal)
+| %ex | SQL expression or array of expressions
+| %lmt | special - adds LIMIT to the query
+| %ofs | special - adds OFFSET to the query
+
+Example:
+
+```php
+$result = $database->query('SELECT * FROM users WHERE name = %s', $name);
+```
+
+If $name is null, the NULL is inserted into the SQL statement.
+
+If the variable is an array, the modifier is applied to all of its elements and they are inserted into SQL separated by commas:
+
+```php
+$ids = [10, '20', 30];
+$result = $database->query('SELECT * FROM users WHERE id IN (%i)', $ids);
+// SELECT * FROM users WHERE id IN (10, 20, 30)
+```
 
-// or static way; in all other examples use dibi:: instead of $dibi->
-dibi::connect($options);
+The modifier `%n` is used if the table or column name is a variable. (Beware, do not allow the user to manipulate the content of such a variable):
+
+```php
+$table = 'blog.users';
+$column = 'name';
+$result = $database->query('SELECT * FROM %n WHERE %n = ?', $table, $column, $value);
+// SELECT * FROM `blog`.`users` WHERE `name` = 'Jim'
 ```
 
-SELECT, INSERT, UPDATE
+Three special modifiers are available for LIKE:
+
+| modifier | description
+|----------|-----
+| `%like~` | the expression starts with a string
+| `%~like` | the expression ends with a string
+| `%~like~` | the expression contains a string
+| `%like` | the expression matches a string
+
+Search for names beginning with a string:
 
 ```php
-$dibi->query('SELECT * FROM users WHERE id = ?', $id);
+$result = $database->query('SELECT * FROM table WHERE name LIKE %like~', $query);
+```
+
 
+### Modifiers for arrays
+
+The parameter entered in the SQL query can also be an array. These modifiers determine how to compile the SQL statement:
+
+| modifier |   | result
+|----------|---|-----
+| %and   |        | `key1 = value1 AND key2 = value2 AND ...`
+| %or    |        | `key1 = value1 OR key2 = value2 OR ...`
+| %a     | assoc  | `key1 = value1, key2 = value2, ...`
+| %l %in | list   | `(val1, val2, ...)`
+| %v     | values | `(key1, key2, ...) VALUES (value1, value2, ...)`
+| %m     | multi  | `(key1, key2, ...) VALUES (value1, value2, ...), (value1, value2, ...), ...`
+| %by    | ordering | `key1 ASC, key2 DESC ...`
+| %n     | names  | `key1, key2 AS alias, ...`
+
+Example:
+
+```php
 $arr = [
-    'name' => 'John',
-    'is_admin'  => true,
+	'a' => 'hello',
+	'b'  => true,
 ];
-$dibi->query('INSERT INTO users', $arr);
-// INSERT INTO users (`name`, `is_admin`) VALUES ('John', 1)
 
-$dibi->query('UPDATE users SET', $arr, 'WHERE `id`=?', $x);
-// UPDATE users SET `name`='John', `is_admin`=1 WHERE `id` = 123
+$database->query('INSERT INTO table %v', $arr);
+// INSERT INTO `table` (`a`, `b`) VALUES ('hello', 1)
+
+$database->query('UPDATE `table` SET %a', $arr);
+// UPDATE `table` SET `a`='hello', `b`=1
+```
+
+In the WHERE clause modifiers `%and` nebo `%or` can be used:
 
-$dibi->query('UPDATE users SET', [
-	'title' => array('SHA1(?)', 'tajneheslo'),
+```php
+$result = $database->query('SELECT * FROM users WHERE %and', [
+	'name' => $name,
+	'year' => $year,
 ]);
-// UPDATE users SET 'title' = SHA1('tajneheslo')
+// SELECT * FROM users WHERE `name` = 'Jim' AND `year` = 1978
 ```
 
-Getting results
+The modifier `%by` is used to sort, the keys show the columns, and the boolean value will determine whether to sort in ascending order:
 
 ```php
-$result = $dibi->query('SELECT * FROM users');
+$result = $database->query('SELECT id FROM author ORDER BY %by', [
+	'id' => true, // ascending
+	'name' => false, // descending
+]);
+// SELECT id FROM author ORDER BY `id`, `name` DESC
+```
 
-$value = $result->fetchSingle(); // single value
-$all = $result->fetchAll(); // all rows
-$assoc = $result->fetchAssoc('id'); // all rows as associative array
-$pairs = $result->fetchPairs('customerID', 'name'); // all rows as key => value pairs
 
-// iterating
-foreach ($result as $n => $row) {
-    print_r($row);
-}
+### Insert, Update & Delete
+
+We insert the data into an SQL query as an associative array. Modifiers and wildcards `?` are not required in these cases.
+
+```php
+$database->query('INSERT INTO users', [
+	'name' => $name,
+	'year' => $year,
+]);
+// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978)
+
+$id = $database->getInsertId(); // returns the auto-increment of the inserted record
+
+$id = $database->getInsertId($sequence); // or sequence value
+```
+
+Multiple INSERT:
+
+```php
+$database->query('INSERT INTO users', [
+	'name' => 'Jim',
+	'year' => 1978,
+], [
+	'name' => 'Jack',
+	'year' => 1987,
+]);
+// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978), ('Jack', 1987)
+```
+
+Deleting:
+
+```php
+$database->query('DELETE FROM users WHERE id = ?', $id);
+
+// returns the number of deleted rows
+$affectedRows = $database->getAffectedRows();
+```
+
+Update:
+
+```php
+$database->query('UPDATE users SET', [
+	'name' => $name,
+	'year' => $year,
+], 'WHERE id = ?', $id);
+// UPDATE users SET `name` = 'Jim', `year` = 1978 WHERE id = 123
+
+// returns the number of updated rows
+$affectedRows = $database->getAffectedRows();
+```
+
+Insert an entry or update if it already exists:
+
+```php
+$database->query('INSERT INTO users', [
+	'id' => $id,
+	'name' => $name,
+	'year' => $year,
+], 'ON DUPLICATE KEY UPDATE %a', [ // here the modifier %a must be used
+	'name' => $name,
+	'year' => $year,
+]);
+// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978)
+//   ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978
+```
+
+
+### Transaction
+
+There are three methods for dealing with transactions:
+
+```php
+$database->beginTransaction();
+
+$database->commit();
+
+$database->rollback();
+```
+
+
+### Testing
+
+In order to play with Dibi a little, there is a `test()` method that you pass parameters like to `query()`, but instead of executing the SQL statement, it is echoed on the screen.
+
+The query results can be echoed as a table using `$result->dump()`.
+
+These variables are also available:
+
+```php
+dibi::$sql; // the latest SQL query
+dibi::$elapsedTime; // its duration in sec
+dibi::$numOfQueries;
+dibi::$totalTime;
+```
+
+
+### Complex queries
+
+The parameter may also be an object `DateTime`.
+
+```php
+$result = $database->query('SELECT * FROM users WHERE created < ?', new DateTime);
+
+$database->query('INSERT INTO users', [
+	'created' => new DateTime,
+]);
+```
+
+Or SQL literal:
+
+```php
+$database->query('UPDATE table SET', [
+	'date' => $database->literal('NOW()'),
+]);
+// UPDATE table SET `date` = NOW()
+```
+
+Or an expression in which you can use `?` or modifiers:
+
+```php
+$database->query('UPDATE `table` SET', [
+	'title' => $database::expression('SHA1(?)', 'secret'),
+]);
+// UPDATE `table` SET `title` = SHA1('secret')
+```
+
+When updating, modifiers can be placed directly in the keys:
+
+```php
+$database->query('UPDATE table SET', [
+	'date%SQL' => 'NOW()', // %SQL means SQL ;)
+]);
+// UPDATE table SET `date` = NOW()
+```
+
+In conditions (ie, for `%and` and `%or` modifiers), it is not necessary to specify the keys:
+
+```php
+$result = $database->query('SELECT * FROM `table` WHERE %and', [
+	'number > 10',
+	'number < 100',
+]);
+// SELECT * FROM `table` WHERE (number > 10) AND (number < 100)
+```
+
+Modifiers or wildcards can also be used in expressions:
+
+```php
+$result = $database->query('SELECT * FROM `table` WHERE %and', [
+	['number > ?', 10],  // or $database::expression('number > ?', 10)
+	['number < ?', 100],
+	['%or', [
+		'left' => 1,
+		'top' => 2,
+	]],
+]);
+// SELECT * FROM `table` WHERE (number > 10) AND (number < 100) AND (`left` = 1 OR `top` = 2)
 ```
 
-Modifiers for arrays:
+The `%ex` modifier inserts all items of the array into SQL:
 
 ```php
-$dibi->query('SELECT * FROM users WHERE %and', [
-	array('number > ?', 10),
-	array('number < ?', 100),
+$result = $database->query('SELECT * FROM `table` WHERE %ex', [
+	$database::expression('left = ?', 1),
+	'AND',
+	'top IS NULL',
 ]);
-// SELECT * FROM users WHERE (number > 10) AND (number < 100)
+// SELECT * FROM `table` WHERE left = 1 AND top IS NULL
+```
+
+
+### Conditions in the SQL
+
+Conditional SQL commands are controlled by three modifiers `%if`, `%else`, and `%end`. The `%if` must be at the end of the string representing SQL and is followed by the variable:
+
+```php
+$user = ???
+
+$result = $database->query('
+	SELECT *
+	FROM table
+	%if', isset($user), 'WHERE user=%s', $user, '%end
+	ORDER BY name
+');
+```
+
+The condition can be supplemented by the section `%else`:
+
+```php
+$result = $database->query('
+	SELECT *
+	FROM %if', $cond, 'one_table %else second_table
+');
+```
+
+Conditions can nest together.
+
+
+### Identifiers and strings in SQL
+
+SQL itself goes through processing to meet the conventions of the database. The identifiers (names of tables and columns) can be entered into square brackets or backticks, strings are quoted with single or double quotation marks, but the server always sends what the database asks for. Example:
+
+```php
+$database->query("UPDATE `table` SET [status]='I''m fine'");
+// MySQL: UPDATE `table` SET `status`='I\'m fine'
+// ODBC:  UPDATE [table] SET [status]='I''m fine'
+```
+
+The quotation marks are duplicated inside the string in SQL.
+
+
+
+### Result as associative array
+
+Example: returns results as an associative field, where the key will be the value of the `id` field:
+
+```php
+$assoc = $result->fetchAssoc('id');
+```
+
+The greatest power of `fetchAssoc()` is reflected in a SQL query joining several tables with different types of joins. The database will make a flat table, fetchAssoc returns the shape.
+
+Example: Let's take a customer and order table (N:M binding) and query:
+
+```php
+$result = $database->query('
+  SELECT customer_id, customers.name, order_id, orders.number, ...
+  FROM customers
+  INNER JOIN orders USING (customer_id)
+  WHERE ...
+');
 ```
 
-<table>
-<tr><td> %and </td><td>  </td><td> `[key]=val AND [key2]="val2" AND ...` </td></tr>
-<tr><td> %or </td><td>  </td><td> `[key]=val OR [key2]="val2" OR ...` </td></tr>
-<tr><td> %a </td><td> assoc </td><td> `[key]=val, [key2]="val2", ...` </td></tr>
-<tr><td> %l %in </td><td> list </td><td> `(val, "val2", ...)` </td></tr>
-<tr><td> %v </td><td> values </td><td> `([key], [key2], ...) VALUES (val, "val2", ...)` </td></tr>
-<tr><td> %m </td><td> multivalues </td><td> `([key], [key2], ...) VALUES (val, "val2", ...), (val, "val2", ...), ...` </td></tr>
-<tr><td> %by </td><td> ordering </td><td> `[key] ASC, [key2] DESC ...` </td></tr>
-<tr><td> %n </td><td> identifiers </td><td> `[key], [key2] AS alias, ...` </td></tr>
-<tr><td> other  </td><td> - </td><td> `val, val2, ...` </td></tr>
-</table>
+And we'd like to get a nested associative array by Customer ID and then Order ID:
 
+```php
+$all = $result->fetchAssoc('customer_id|order_id');
+
+// we will iterate like this:
+foreach ($all as $customerId => $orders) {
+   foreach ($orders as $orderId => $order) {
+	   ...
+   }
+}
+```
+
+An associative descriptor has a similar syntax as when you type the array by assigning it to PHP. Thus `'customer_id|order_id'` represents the assignment series `$all[$customerId][$orderId] = $row;` sequentially for all rows.
+
+Sometimes it would be useful to associate by the customer's name instead of his ID:
+
+```php
+$all = $result->fetchAssoc('name|order_id');
+
+// the elements then proceeds like this:
+$order = $all['Arnold Rimmer'][$orderId];
+```
+
+But what if there are more customers with the same name? The table should be in the form of:
+
+```php
+$row = $all['Arnold Rimmer'][0][$orderId];
+$row = $all['Arnold Rimmer'][1][$orderId];
+...
+```
+
+So we can distinguish between multiple possible Rimmers using an array. The associative descriptor has a format similar to the assignment, with the sequence array representing `[]`:
+
+```php
+$all = $result->fetchAssoc('name[]order_id');
+
+// we get all the Arnolds in the results
+foreach ($all['Arnold Rimmer'] as $arnoldOrders) {
+   foreach ($arnoldOrders as $orderId => $order) {
+	   ...
+   }
+}
+```
+
+Returning to the example with the `customer_id|order_id` descriptor, we will try to list the orders of each customer:
+
+```php
+$all = $result->fetchAssoc('customer_id|order_id');
+
+foreach ($all as $customerId => $orders) {
+   echo "Customer $customerId":
+
+   foreach ($orders as $orderId => $order) {
+	   echo "ID number: $order->number";
+	   // customer name is in $order->name
+   }
+}
+```
+
+It would be a nice to echo customer name too. But we would have to look for it in the `$orders` array. So let's adjust the results to such a shape:
+
+```php
+$all[$customerId]->name = 'John Doe';
+$all[$customerId]->order_id[$orderId] = $row;
+$all[$customerId]->order_id[$orderId2] = $row2;
+```
 
-Modifiers for LIKE
+So, between `$clientId` and `$orderId`, we will also insert an intermediate item. This time not the numbered indexes as we used to distinguish between individual Rimmers, but a database row. The solution is very similar, just remember that the row symbolizes the arrow:
 
 ```php
-$dibi->query("SELECT * FROM table WHERE name LIKE %like~", $query);
+$all = $result->fetchAssoc('customer_id->order_id');
+
+foreach ($all as $customerId => $row) {
+   echo "Customer $row->name":
+
+   foreach ($row->order_id as $orderId => $order) {
+	   echo "ID number: $order->number";
+   }
+}
+```
+
+
+
+### Prefixes & substitutions
+
+Table and column names can contain variable parts. You will first define:
+
+```php
+// create new substitution :blog:  ==>  wp_
+$database->substitute('blog', 'wp_');
+```
+
+and then use it in SQL. Note that in SQL they are quoted by the colon:
+
+```php
+$database->query("UPDATE [:blog:items] SET [text]='Hello World'");
+// UPDATE `wp_items` SET `text`='Hello World'
 ```
 
-<table>
-<tr><td> %like~	</td><td> begins with </td></tr>
-<tr><td> %~like	</td><td> ends with </td></tr>
-<tr><td> %~like~ </td><td> contains </td></tr>
-</table>
 
-DateTime:
+### Field data types
+
+Dibi automatically detects the types of query columns and converts fields them to native PHP types. We can also specify the type manually. You can find the possible types in the `Dibi\Type` class.
 
 ```php
-$dibi->query('UPDATE users SET', [
-    'time' => new DateTime,
+$result->setType('id', Dibi\Type::INTEGER); // id will be integer
+$row = $result->fetch();
+
+is_int($row->id) // true
+```
+
+
+### Logger
+
+Dibi has a built-in logger that lets you track all SQL statements executed and measure the length of their duration. Activating the logger:
+
+```php
+$database->connect([
+	'driver'   => 'sqlite',
+	'database' => 'sample.sdb',
+	'profiler' => [
+		'file' => 'file.log',
+	],
 ]);
-// UPDATE users SET ('2008-01-01 01:08:10')
 ```
 
-Testing:
+A more versatile profiler is a Tracy panel that is activated when connected to Nette.
+
+
+
+### Connect to [Nette](https://nette.org)
+
+In the configuration file, we will register the DI extensions and add the `dibi` section to create the required objects and also the database panel in the [Tracy](https://tracy.nette.org) debugger bar.
+
+```neon
+extensions:
+	dibi: Dibi\Bridges\Nette\DibiExtension22
+
+dibi:
+	host: localhost
+	username: root
+	password: ***
+	database: foo
+	lazy: true
+```
+
+Then the object of connection can be [obtained as a service from the container DI](https://doc.nette.org/di-usage), eg:
 
 ```php
-echo dibi::$sql; // last SQL query
-echo dibi::$elapsedTime;
-echo dibi::$numOfQueries;
-echo dibi::$totalTime;
+class Model
+{
+	private $database;
+
+	public function __construct(Dibi\Connection $database)
+	{
+		$this->database = $database;
+	}
+}
 ```

+ 12 - 11
api/vendor/dibi/dibi/src/Dibi/Bridges/Nette/DibiExtension22.php

@@ -5,10 +5,13 @@
  * Copyright (c) 2005 David Grudl (https://davidgrudl.com)
  */
 
+declare(strict_types=1);
+
 namespace Dibi\Bridges\Nette;
 
 use Dibi;
 use Nette;
+use Tracy;
 
 
 /**
@@ -16,11 +19,11 @@ use Nette;
  */
 class DibiExtension22 extends Nette\DI\CompilerExtension
 {
-	/** @var bool */
+	/** @var bool|null */
 	private $debugMode;
 
 
-	public function __construct($debugMode = null)
+	public function __construct(bool $debugMode = null)
 	{
 		$this->debugMode = $debugMode;
 	}
@@ -35,9 +38,7 @@ class DibiExtension22 extends Nette\DI\CompilerExtension
 			$this->debugMode = $container->parameters['debugMode'];
 		}
 
-		$useProfiler = isset($config['profiler'])
-			? $config['profiler']
-			: class_exists('Tracy\Debugger') && $this->debugMode;
+		$useProfiler = $config['profiler'] ?? (class_exists(Tracy\Debugger::class) && $this->debugMode);
 
 		unset($config['profiler']);
 
@@ -50,19 +51,19 @@ class DibiExtension22 extends Nette\DI\CompilerExtension
 		}
 
 		$connection = $container->addDefinition($this->prefix('connection'))
-			->setFactory('Dibi\Connection', [$config])
-			->setAutowired(isset($config['autowired']) ? $config['autowired'] : true);
+			->setFactory(Dibi\Connection::class, [$config])
+			->setAutowired($config['autowired'] ?? true);
 
-		if (class_exists('Tracy\Debugger')) {
+		if (class_exists(Tracy\Debugger::class)) {
 			$connection->addSetup(
 				[new Nette\DI\Statement('Tracy\Debugger::getBlueScreen'), 'addPanel'],
-				[['Dibi\Bridges\Tracy\Panel', 'renderException']]
+				[[Dibi\Bridges\Tracy\Panel::class, 'renderException']]
 			);
 		}
 		if ($useProfiler) {
 			$panel = $container->addDefinition($this->prefix('panel'))
-				->setFactory('Dibi\Bridges\Tracy\Panel', [
-					isset($config['explain']) ? $config['explain'] : true,
+				->setFactory(Dibi\Bridges\Tracy\Panel::class, [
+					$config['explain'] ?? true,
 					isset($config['filter']) && $config['filter'] === false ? Dibi\Event::ALL : Dibi\Event::QUERY,
 				]);
 			$connection->addSetup([$panel, 'register'], [$connection]);

+ 198 - 262
api/vendor/dibi/dibi/src/Dibi/Connection.php

@@ -5,6 +5,8 @@
  * Copyright (c) 2005 David Grudl (https://davidgrudl.com)
  */
 
+declare(strict_types=1);
+
 namespace Dibi;
 
 use Traversable;
@@ -16,109 +18,92 @@ use Traversable;
  * @property-read int $affectedRows
  * @property-read int $insertId
  */
-class Connection
+class Connection implements IConnection
 {
 	use Strict;
 
 	/** @var array of function (Event $event); Occurs after query is executed */
-	public $onEvent;
+	public $onEvent = [];
 
 	/** @var array  Current connection configuration */
 	private $config;
 
-	/** @var Driver */
+	/** @var string[]  resultset formats */
+	private $formats;
+
+	/** @var Driver|null */
 	private $driver;
 
 	/** @var Translator|null */
 	private $translator;
 
-	/** @var bool  Is connected? */
-	private $connected = false;
-
 	/** @var HashMap Substitutes for identifiers */
 	private $substitutes;
 
+	private $transactionDepth = 0;
+
 
 	/**
 	 * Connection options: (see driver-specific options too)
 	 *   - lazy (bool) => if true, connection will be established only when required
 	 *   - result (array) => result set options
-	 *       - formatDateTime => date-time format (if empty, DateTime objects will be returned)
-	 *   - profiler (array or bool)
+	 *       - normalize => normalizes result fields (default: true)
+	 *       - formatDateTime => date-time format
+	 *           empty for decoding as Dibi\DateTime (default)
+	 *           "..." formatted according to given format, see https://www.php.net/manual/en/datetime.format.php
+	 *           "native" for leaving value as is
+	 *       - formatTimeInterval => time-interval format
+	 *           empty for decoding as DateInterval (default)
+	 *           "..." formatted according to given format, see https://www.php.net/manual/en/dateinterval.format.php
+	 *           "native" for leaving value as is
+	 *       - formatJson => json format
+	 *           "array" for decoding json as an array (default)
+	 *           "object" for decoding json as \stdClass
+	 *           "native" for leaving value as is
+	 *   - profiler (array)
 	 *       - run (bool) => enable profiler?
 	 *       - file => file to log
+	 *       - errorsOnly (bool) => log only errors
 	 *   - substitutes (array) => map of driver specific substitutes (under development)
-	 * @param  mixed   connection parameters
-	 * @param  string  connection name
+	 *   - onConnect (array) => list of SQL queries to execute (by Connection::query()) after connection is established
 	 * @throws Exception
 	 */
-	public function __construct($config, $name = null)
+	public function __construct(array $config, string $name = null)
 	{
-		if (is_string($config)) {
-			parse_str($config, $config);
-
-		} elseif ($config instanceof Traversable) {
-			$tmp = [];
-			foreach ($config as $key => $val) {
-				$tmp[$key] = $val instanceof Traversable ? iterator_to_array($val) : $val;
-			}
-			$config = $tmp;
-
-		} elseif (!is_array($config)) {
-			throw new \InvalidArgumentException('Configuration must be array, string or object.');
-		}
-
 		Helpers::alias($config, 'username', 'user');
 		Helpers::alias($config, 'password', 'pass');
 		Helpers::alias($config, 'host', 'hostname');
 		Helpers::alias($config, 'result|formatDate', 'resultDate');
 		Helpers::alias($config, 'result|formatDateTime', 'resultDateTime');
-
-		if (!isset($config['driver'])) {
-			$config['driver'] = \dibi::$defaultDriver;
-		}
-
-		if ($config['driver'] instanceof Driver) {
-			$this->driver = $config['driver'];
-			$config['driver'] = get_class($this->driver);
-		} elseif (is_subclass_of($config['driver'], 'Dibi\Driver')) {
-			$this->driver = new $config['driver'];
-		} else {
-			$class = preg_replace(['#\W#', '#sql#'], ['_', 'Sql'], ucfirst(strtolower($config['driver'])));
-			$class = "Dibi\\Drivers\\{$class}Driver";
-			if (!class_exists($class)) {
-				throw new Exception("Unable to create instance of dibi driver '$class'.");
-			}
-			$this->driver = new $class;
-		}
-
+		$config['driver'] = $config['driver'] ?? 'mysqli';
 		$config['name'] = $name;
 		$this->config = $config;
 
-		// profiler
-		$profilerCfg = &$config['profiler'];
-		if (is_scalar($profilerCfg)) {
-			$profilerCfg = ['run' => (bool) $profilerCfg];
-		}
-		if (!empty($profilerCfg['run'])) {
-			$filter = isset($profilerCfg['filter']) ? $profilerCfg['filter'] : Event::QUERY;
-
-			if (isset($profilerCfg['file'])) {
-				$this->onEvent[] = [new Loggers\FileLogger($profilerCfg['file'], $filter), 'logEvent'];
-			}
+		$this->formats = [
+			Type::DATE => $this->config['result']['formatDate'],
+			Type::DATETIME => $this->config['result']['formatDateTime'],
+			Type::JSON => $this->config['result']['formatJson'] ?? 'array',
+			Type::TIME_INTERVAL => $this->config['result']['formatTimeInterval'] ?? null,
+		];
 
-			if (Loggers\FirePhpLogger::isAvailable()) {
-				$this->onEvent[] = [new Loggers\FirePhpLogger($filter), 'logEvent'];
-			}
+		// profiler
+		if (isset($config['profiler']['file']) && (!isset($config['profiler']['run']) || $config['profiler']['run'])) {
+			$filter = $config['profiler']['filter'] ?? Event::QUERY;
+			$errorsOnly = $config['profiler']['errorsOnly'] ?? false;
+			$this->onEvent[] = [new Loggers\FileLogger($config['profiler']['file'], $filter, $errorsOnly), 'logEvent'];
 		}
 
-		$this->substitutes = new HashMap(function ($expr) { return ":$expr:"; });
+		$this->substitutes = new HashMap(function (string $expr) { return ":$expr:"; });
 		if (!empty($config['substitutes'])) {
 			foreach ($config['substitutes'] as $key => $value) {
 				$this->substitutes->$key = $value;
 			}
 		}
 
+		if (isset($config['onConnect']) && !is_array($config['onConnect'])) {
+			throw new \InvalidArgumentException("Configuration option 'onConnect' must be array.");
+		}
+
 		if (empty($config['lazy'])) {
 			$this->connect();
 		}
@@ -127,11 +112,10 @@ class Connection
 
 	/**
 	 * Automatically frees the resources allocated for this result set.
-	 * @return void
 	 */
 	public function __destruct()
 	{
-		if ($this->connected && $this->driver->getResource()) {
+		if ($this->driver && $this->driver->getResource()) {
 			$this->disconnect();
 		}
 	}
@@ -139,19 +123,40 @@ class Connection
 
 	/**
 	 * Connects to a database.
-	 * @return void
 	 */
-	final public function connect()
+	final public function connect(): void
 	{
+		if ($this->config['driver'] instanceof Driver) {
+			$this->driver = $this->config['driver'];
+			$this->translator = new Translator($this);
+			return;
+
+		} elseif (is_subclass_of($this->config['driver'], Driver::class)) {
+			$class = $this->config['driver'];
+
+		} else {
+			$class = preg_replace(['#\W#', '#sql#'], ['_', 'Sql'], ucfirst(strtolower($this->config['driver'])));
+			$class = "Dibi\\Drivers\\{$class}Driver";
+			if (!class_exists($class)) {
+				throw new Exception("Unable to create instance of Dibi driver '$class'.");
+			}
+		}
+
 		$event = $this->onEvent ? new Event($this, Event::CONNECT) : null;
 		try {
-			$this->driver->connect($this->config);
-			$this->connected = true;
+			$this->driver = new $class($this->config);
+			$this->translator = new Translator($this);
+
 			if ($event) {
 				$this->onEvent($event->done());
 			}
+			if (isset($this->config['onConnect'])) {
+				foreach ($this->config['onConnect'] as $sql) {
+					$this->query($sql);
+				}
+			}
 
-		} catch (Exception $e) {
+		} catch (DriverException $e) {
 			if ($event) {
 				$this->onEvent($event->done($e));
 			}
@@ -162,61 +167,44 @@ class Connection
 
 	/**
 	 * Disconnects from a database.
-	 * @return void
 	 */
-	final public function disconnect()
+	final public function disconnect(): void
 	{
-		$this->driver->disconnect();
-		$this->connected = false;
+		if ($this->driver) {
+			$this->driver->disconnect();
+			$this->driver = $this->translator = null;
+		}
 	}
 
 
 	/**
 	 * Returns true when connection was established.
-	 * @return bool
 	 */
-	final public function isConnected()
+	final public function isConnected(): bool
 	{
-		return $this->connected;
+		return (bool) $this->driver;
 	}
 
 
 	/**
 	 * Returns configuration variable. If no $key is passed, returns the entire array.
 	 * @see self::__construct
-	 * @param  string
-	 * @param  mixed  default value to use if key not found
 	 * @return mixed
 	 */
-	final public function getConfig($key = null, $default = null)
+	final public function getConfig(string $key = null, $default = null)
 	{
-		if ($key === null) {
-			return $this->config;
-
-		} elseif (isset($this->config[$key])) {
-			return $this->config[$key];
-
-		} else {
-			return $default;
-		}
-	}
-
-
-	/** @deprecated */
-	public static function alias(&$config, $key, $alias)
-	{
-		trigger_error(__METHOD__ . '() is deprecated, use Helpers::alias().', E_USER_DEPRECATED);
-		Helpers::alias($config, $key, $alias);
+		return $key === null
+			? $this->config
+			: ($this->config[$key] ?? $default);
 	}
 
 
 	/**
 	 * Returns the driver and connects to a database in lazy mode.
-	 * @return Driver
 	 */
-	final public function getDriver()
+	final public function getDriver(): Driver
 	{
-		if (!$this->connected) {
+		if (!$this->driver) {
 			$this->connect();
 		}
 		return $this->driver;
@@ -225,40 +213,37 @@ class Connection
 
 	/**
 	 * Generates (translates) and executes SQL query.
-	 * @param  array|mixed      one or more arguments
-	 * @return Result|int   result set or number of affected rows
+	 * @param  mixed  ...$args
 	 * @throws Exception
 	 */
-	final public function query($args)
+	final public function query(...$args): Result
 	{
-		$args = func_get_args();
-		return $this->nativeQuery($this->translateArgs($args));
+		return $this->nativeQuery($this->translate(...$args));
 	}
 
 
 	/**
 	 * Generates SQL query.
-	 * @param  array|mixed      one or more arguments
-	 * @return string
+	 * @param  mixed  ...$args
 	 * @throws Exception
 	 */
-	final public function translate($args)
+	final public function translate(...$args): string
 	{
-		$args = func_get_args();
-		return $this->translateArgs($args);
+		if (!$this->driver) {
+			$this->connect();
+		}
+		return (clone $this->translator)->translate($args);
 	}
 
 
 	/**
 	 * Generates and prints SQL query.
-	 * @param  array|mixed  one or more arguments
-	 * @return bool
+	 * @param  mixed  ...$args
 	 */
-	final public function test($args)
+	final public function test(...$args): bool
 	{
-		$args = func_get_args();
 		try {
-			Helpers::dump($this->translateArgs($args));
+			Helpers::dump($this->translate(...$args));
 			return true;
 
 		} catch (Exception $e) {
@@ -274,44 +259,22 @@ class Connection
 
 	/**
 	 * Generates (translates) and returns SQL query as DataSource.
-	 * @param  array|mixed      one or more arguments
-	 * @return DataSource
+	 * @param  mixed  ...$args
 	 * @throws Exception
 	 */
-	final public function dataSource($args)
-	{
-		$args = func_get_args();
-		return new DataSource($this->translateArgs($args), $this);
-	}
-
-
-	/**
-	 * Generates SQL query.
-	 * @param  array
-	 * @return string
-	 */
-	protected function translateArgs($args)
+	final public function dataSource(...$args): DataSource
 	{
-		if (!$this->connected) {
-			$this->connect();
-		}
-		if (!$this->translator) {
-			$this->translator = new Translator($this);
-		}
-		$translator = clone $this->translator;
-		return $translator->translate($args);
+		return new DataSource($this->translate(...$args), $this);
 	}
 
 
 	/**
 	 * Executes the SQL query.
-	 * @param  string           SQL statement.
-	 * @return Result|int   result set or number of affected rows
 	 * @throws Exception
 	 */
-	final public function nativeQuery($sql)
+	final public function nativeQuery(string $sql): Result
 	{
-		if (!$this->connected) {
+		if (!$this->driver) {
 			$this->connect();
 		}
 
@@ -320,19 +283,14 @@ class Connection
 		try {
 			$res = $this->driver->query($sql);
 
-		} catch (Exception $e) {
+		} catch (DriverException $e) {
 			if ($event) {
 				$this->onEvent($event->done($e));
 			}
 			throw $e;
 		}
 
-		if ($res) {
-			$res = $this->createResultSet($res);
-		} else {
-			$res = $this->driver->getAffectedRows();
-		}
-
+		$res = $this->createResultSet($res ?: new Drivers\NoDataResult(max(0, $this->driver->getAffectedRows())));
 		if ($event) {
 			$this->onEvent($event->done($res));
 		}
@@ -342,69 +300,48 @@ class Connection
 
 	/**
 	 * Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
-	 * @return int  number of rows
 	 * @throws Exception
 	 */
-	public function getAffectedRows()
+	public function getAffectedRows(): int
 	{
-		if (!$this->connected) {
+		if (!$this->driver) {
 			$this->connect();
 		}
 		$rows = $this->driver->getAffectedRows();
-		if (!is_int($rows) || $rows < 0) {
+		if ($rows === null || $rows < 0) {
 			throw new Exception('Cannot retrieve number of affected rows.');
 		}
 		return $rows;
 	}
 
 
-	/**
-	 * @deprecated
-	 */
-	public function affectedRows()
-	{
-		trigger_error(__METHOD__ . '() is deprecated, use getAffectedRows()', E_USER_DEPRECATED);
-		return $this->getAffectedRows();
-	}
-
-
 	/**
 	 * Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
-	 * @param  string     optional sequence name
-	 * @return int
 	 * @throws Exception
 	 */
-	public function getInsertId($sequence = null)
+	public function getInsertId(string $sequence = null): int
 	{
-		if (!$this->connected) {
+		if (!$this->driver) {
 			$this->connect();
 		}
 		$id = $this->driver->getInsertId($sequence);
-		if ($id < 1) {
+		if ($id === null) {
 			throw new Exception('Cannot retrieve last generated ID.');
 		}
-		return Helpers::intVal($id);
-	}
-
-
-	/**
-	 * @deprecated
-	 */
-	public function insertId($sequence = null)
-	{
-		trigger_error(__METHOD__ . '() is deprecated, use getInsertId()', E_USER_DEPRECATED);
-		return $this->getInsertId($sequence);
+		return $id;
 	}
 
 
 	/**
 	 * Begins a transaction (if supported).
-	 * @param  string  optional savepoint name
-	 * @return void
 	 */
-	public function begin($savepoint = null)
+	public function begin(string $savepoint = null): void
 	{
-		if (!$this->connected) {
+		if ($this->transactionDepth !== 0) {
+			throw new \LogicException(__METHOD__ . '() call is forbidden inside a transaction() callback');
+		}
+
+		if (!$this->driver) {
 			$this->connect();
 		}
 		$event = $this->onEvent ? new Event($this, Event::BEGIN, $savepoint) : null;
@@ -414,7 +351,7 @@ class Connection
 				$this->onEvent($event->done());
 			}
 
-		} catch (Exception $e) {
+		} catch (DriverException $e) {
 			if ($event) {
 				$this->onEvent($event->done($e));
 			}
@@ -425,12 +362,14 @@ class Connection
 
 	/**
 	 * Commits statements in a transaction.
-	 * @param  string  optional savepoint name
-	 * @return void
 	 */
-	public function commit($savepoint = null)
+	public function commit(string $savepoint = null): void
 	{
-		if (!$this->connected) {
+		if ($this->transactionDepth !== 0) {
+			throw new \LogicException(__METHOD__ . '() call is forbidden inside a transaction() callback');
+		}
+
+		if (!$this->driver) {
 			$this->connect();
 		}
 		$event = $this->onEvent ? new Event($this, Event::COMMIT, $savepoint) : null;
@@ -440,7 +379,7 @@ class Connection
 				$this->onEvent($event->done());
 			}
 
-		} catch (Exception $e) {
+		} catch (DriverException $e) {
 			if ($event) {
 				$this->onEvent($event->done($e));
 			}
@@ -451,12 +390,14 @@ class Connection
 
 	/**
 	 * Rollback changes in a transaction.
-	 * @param  string  optional savepoint name
-	 * @return void
 	 */
-	public function rollback($savepoint = null)
+	public function rollback(string $savepoint = null): void
 	{
-		if (!$this->connected) {
+		if ($this->transactionDepth !== 0) {
+			throw new \LogicException(__METHOD__ . '() call is forbidden inside a transaction() callback');
+		}
+
+		if (!$this->driver) {
 			$this->connect();
 		}
 		$event = $this->onEvent ? new Event($this, Event::ROLLBACK, $savepoint) : null;
@@ -466,7 +407,7 @@ class Connection
 				$this->onEvent($event->done());
 			}
 
-		} catch (Exception $e) {
+		} catch (DriverException $e) {
 			if ($event) {
 				$this->onEvent($event->done($e));
 			}
@@ -475,78 +416,80 @@ class Connection
 	}
 
 
+	/**
+	 * @return mixed
+	 */
+	public function transaction(callable $callback)
+	{
+		if ($this->transactionDepth === 0) {
+			$this->begin();
+		}
+
+		$this->transactionDepth++;
+		try {
+			$res = $callback($this);
+		} catch (\Throwable $e) {
+			$this->transactionDepth--;
+			if ($this->transactionDepth === 0) {
+				$this->rollback();
+			}
+			throw $e;
+		}
+
+		$this->transactionDepth--;
+		if ($this->transactionDepth === 0) {
+			$this->commit();
+		}
+
+		return $res;
+	}
+
+
 	/**
 	 * Result set factory.
-	 * @param  ResultDriver
-	 * @return Result
 	 */
-	public function createResultSet(ResultDriver $resultDriver)
+	public function createResultSet(ResultDriver $resultDriver): Result
 	{
-		$res = new Result($resultDriver);
-		return $res->setFormat(Type::DATE, $this->config['result']['formatDate'])
-			->setFormat(Type::DATETIME, $this->config['result']['formatDateTime']);
+		return (new Result($resultDriver, $this->config['result']['normalize'] ?? true))
+			->setFormats($this->formats);
 	}
 
 
 	/********************* fluent SQL builders ****************d*g**/
 
 
-	/**
-	 * @return Fluent
-	 */
-	public function command()
+	public function command(): Fluent
 	{
 		return new Fluent($this);
 	}
 
 
-	/**
-	 * @param  mixed    column name
-	 * @return Fluent
-	 */
-	public function select($args)
+	public function select(...$args): Fluent
 	{
-		$args = func_get_args();
-		return $this->command()->__call('select', $args);
+		return $this->command()->select(...$args);
 	}
 
 
 	/**
-	 * @param  string   table
-	 * @param  array
-	 * @return Fluent
+	 * @param  string|string[]  $table
 	 */
-	public function update($table, $args)
+	public function update($table, iterable $args): Fluent
 	{
-		if (!(is_array($args) || $args instanceof Traversable)) {
-			throw new \InvalidArgumentException('Arguments must be array or Traversable.');
-		}
 		return $this->command()->update('%n', $table)->set($args);
 	}
 
 
-	/**
-	 * @param  string   table
-	 * @param  array
-	 * @return Fluent
-	 */
-	public function insert($table, $args)
+	public function insert(string $table, iterable $args): Fluent
 	{
 		if ($args instanceof Traversable) {
 			$args = iterator_to_array($args);
-		} elseif (!is_array($args)) {
-			throw new \InvalidArgumentException('Arguments must be array or Traversable.');
 		}
 		return $this->command()->insert()
 			->into('%n', $table, '(%n)', array_keys($args))->values('%l', $args);
 	}
 
 
-	/**
-	 * @param  string   table
-	 * @return Fluent
-	 */
-	public function delete($table)
+	public function delete(string $table): Fluent
 	{
 		return $this->command()->delete()->from('%n', $table);
 	}
@@ -557,9 +500,8 @@ class Connection
 
 	/**
 	 * Returns substitution hashmap.
-	 * @return HashMap
 	 */
-	public function getSubstitutes()
+	public function getSubstitutes(): HashMap
 	{
 		return $this->substitutes;
 	}
@@ -567,13 +509,12 @@ class Connection
 
 	/**
 	 * Provides substitution.
-	 * @return string
 	 */
-	public function substitute($value)
+	public function substitute(string $value): string
 	{
 		return strpos($value, ':') === false
 			? $value
-			: preg_replace_callback('#:([^:\s]*):#', function ($m) { return $this->substitutes->{$m[1]}; }, $value);
+			: preg_replace_callback('#:([^:\s]*):#', function (array $m) { return $this->substitutes->{$m[1]}; }, $value);
 	}
 
 
@@ -582,75 +523,71 @@ class Connection
 
 	/**
 	 * Executes SQL query and fetch result - shortcut for query() & fetch().
-	 * @param  array|mixed    one or more arguments
-	 * @return Row|false
+	 * @param  mixed  ...$args
 	 * @throws Exception
 	 */
-	public function fetch($args)
+	public function fetch(...$args): ?Row
 	{
-		$args = func_get_args();
 		return $this->query($args)->fetch();
 	}
 
 
 	/**
 	 * Executes SQL query and fetch results - shortcut for query() & fetchAll().
-	 * @param  array|mixed    one or more arguments
+	 * @param  mixed  ...$args
 	 * @return Row[]|array[]
 	 * @throws Exception
 	 */
-	public function fetchAll($args)
+	public function fetchAll(...$args): array
 	{
-		$args = func_get_args();
 		return $this->query($args)->fetchAll();
 	}
 
 
 	/**
 	 * Executes SQL query and fetch first column - shortcut for query() & fetchSingle().
-	 * @param  array|mixed    one or more arguments
+	 * @param  mixed  ...$args
 	 * @return mixed
 	 * @throws Exception
 	 */
-	public function fetchSingle($args)
+	public function fetchSingle(...$args)
 	{
-		$args = func_get_args();
 		return $this->query($args)->fetchSingle();
 	}
 
 
 	/**
 	 * Executes SQL query and fetch pairs - shortcut for query() & fetchPairs().
-	 * @param  array|mixed    one or more arguments
-	 * @return array
+	 * @param  mixed  ...$args
 	 * @throws Exception
 	 */
-	public function fetchPairs($args)
+	public function fetchPairs(...$args): array
 	{
-		$args = func_get_args();
 		return $this->query($args)->fetchPairs();
 	}
 
 
-	/**
-	 * @return Literal
-	 */
-	public static function literal($value)
+	public static function literal(string $value): Literal
 	{
 		return new Literal($value);
 	}
 
 
+	public static function expression(...$args): Expression
+	{
+		return new Expression(...$args);
+	}
+
+
 	/********************* misc ****************d*g**/
 
 
 	/**
 	 * Import SQL dump from file.
-	 * @param  string  filename
-	 * @param  callable  function (int $count, ?float $percent): void
+	 * @param  callable  $onProgress  function (int $count, ?float $percent): void
 	 * @return int  count of sql commands
 	 */
-	public function loadFile($file, callable $onProgress = null)
+	public function loadFile(string $file, callable $onProgress = null): int
 	{
 		return Helpers::loadFromFile($this, $file, $onProgress);
 	}
@@ -658,14 +595,13 @@ class Connection
 
 	/**
 	 * Gets a information about the current database.
-	 * @return Reflection\Database
 	 */
-	public function getDatabaseInfo()
+	public function getDatabaseInfo(): Reflection\Database
 	{
-		if (!$this->connected) {
+		if (!$this->driver) {
 			$this->connect();
 		}
-		return new Reflection\Database($this->driver->getReflector(), isset($this->config['database']) ? $this->config['database'] : null);
+		return new Reflection\Database($this->driver->getReflector(), $this->config['database'] ?? null);
 	}
 
 
@@ -674,7 +610,7 @@ class Connection
 	 */
 	public function __wakeup()
 	{
-		throw new NotSupportedException('You cannot serialize or unserialize ' . get_class($this) . ' instances.');
+		throw new NotSupportedException('You cannot serialize or unserialize ' . static::class . ' instances.');
 	}
 
 
@@ -683,14 +619,14 @@ class Connection
 	 */
 	public function __sleep()
 	{
-		throw new NotSupportedException('You cannot serialize or unserialize ' . get_class($this) . ' instances.');
+		throw new NotSupportedException('You cannot serialize or unserialize ' . static::class . ' instances.');
 	}
 
 
-	protected function onEvent($arg)
+	protected function onEvent($arg): void
 	{
-		foreach ($this->onEvent ?: [] as $handler) {
-			call_user_func($handler, $arg);
+		foreach ($this->onEvent as $handler) {
+			$handler($arg);
 		}
 	}
 }

+ 45 - 71
api/vendor/dibi/dibi/src/Dibi/DataSource.php

@@ -5,6 +5,8 @@
  * Copyright (c) 2005 David Grudl (https://davidgrudl.com)
  */
 
+declare(strict_types=1);
+
 namespace Dibi;
 
 
@@ -47,27 +49,23 @@ class DataSource implements IDataSource
 
 
 	/**
-	 * @param  string  SQL command or table or view name, as data source
-	 * @param  Connection  connection
+	 * @param  string  $sql  command or table or view name, as data source
 	 */
-	public function __construct($sql, Connection $connection)
+	public function __construct(string $sql, Connection $connection)
 	{
-		if (strpbrk($sql, " \t\r\n") === false) {
-			$this->sql = $connection->getDriver()->escapeIdentifier($sql); // table name
-		} else {
-			$this->sql = '(' . $sql . ') t'; // SQL command
-		}
+		$this->sql = strpbrk($sql, " \t\r\n") === false
+			? $connection->getDriver()->escapeIdentifier($sql) // table name
+			: '(' . $sql . ') t'; // SQL command
 		$this->connection = $connection;
 	}
 
 
 	/**
 	 * Selects columns to query.
-	 * @param  string|array  column name or array of column names
-	 * @param  string        column alias
-	 * @return self
+	 * @param  string|array  $col  column name or array of column names
+	 * @param  string  $as        column alias
 	 */
-	public function select($col, $as = null)
+	public function select($col, string $as = null): self
 	{
 		if (is_array($col)) {
 			$this->cols = $col;
@@ -81,17 +79,12 @@ class DataSource implements IDataSource
 
 	/**
 	 * Adds conditions to query.
-	 * @param  mixed  conditions
-	 * @return self
 	 */
-	public function where($cond)
+	public function where($cond): self
 	{
-		if (is_array($cond)) {
-			// TODO: not consistent with select and orderBy
-			$this->conds[] = $cond;
-		} else {
-			$this->conds[] = func_get_args();
-		}
+		$this->conds[] = is_array($cond)
+			? $cond // TODO: not consistent with select and orderBy
+			: func_get_args();
 		$this->result = $this->count = null;
 		return $this;
 	}
@@ -99,16 +92,14 @@ class DataSource implements IDataSource
 
 	/**
 	 * Selects columns to order by.
-	 * @param  string|array  column name or array of column names
-	 * @param  string        sorting direction
-	 * @return self
+	 * @param  string|array  $row  column name or array of column names
 	 */
-	public function orderBy($row, $sorting = 'ASC')
+	public function orderBy($row, string $direction = 'ASC'): self
 	{
 		if (is_array($row)) {
 			$this->sorting = $row;
 		} else {
-			$this->sorting[$row] = $sorting;
+			$this->sorting[$row] = $direction;
 		}
 		$this->result = null;
 		return $this;
@@ -117,11 +108,8 @@ class DataSource implements IDataSource
 
 	/**
 	 * Limits number of rows.
-	 * @param  int|null limit
-	 * @param  int offset
-	 * @return self
 	 */
-	public function applyLimit($limit, $offset = null)
+	public function applyLimit(int $limit, int $offset = null): self
 	{
 		$this->limit = $limit;
 		$this->offset = $offset;
@@ -130,10 +118,7 @@ class DataSource implements IDataSource
 	}
 
 
-	/**
-	 * @return Connection
-	 */
-	final public function getConnection()
+	final public function getConnection(): Connection
 	{
 		return $this->connection;
 	}
@@ -144,9 +129,8 @@ class DataSource implements IDataSource
 
 	/**
 	 * Returns (and queries) Result.
-	 * @return Result
 	 */
-	public function getResult()
+	public function getResult(): Result
 	{
 		if ($this->result === null) {
 			$this->result = $this->connection->nativeQuery($this->__toString());
@@ -155,10 +139,7 @@ class DataSource implements IDataSource
 	}
 
 
-	/**
-	 * @return ResultIterator
-	 */
-	public function getIterator()
+	public function getIterator(): ResultIterator
 	{
 		return $this->getResult()->getIterator();
 	}
@@ -166,9 +147,8 @@ class DataSource implements IDataSource
 
 	/**
 	 * Generates, executes SQL query and fetches the single row.
-	 * @return Row|false
 	 */
-	public function fetch()
+	public function fetch(): ?Row
 	{
 		return $this->getResult()->fetch();
 	}
@@ -176,7 +156,7 @@ class DataSource implements IDataSource
 
 	/**
 	 * Like fetch(), but returns only first field.
-	 * @return mixed  value on success, false if no next record
+	 * @return mixed  value on success, null if no next record
 	 */
 	public function fetchSingle()
 	{
@@ -186,9 +166,8 @@ class DataSource implements IDataSource
 
 	/**
 	 * Fetches all records from table.
-	 * @return array
 	 */
-	public function fetchAll()
+	public function fetchAll(): array
 	{
 		return $this->getResult()->fetchAll();
 	}
@@ -196,10 +175,8 @@ class DataSource implements IDataSource
 
 	/**
 	 * Fetches all records from table and returns associative tree.
-	 * @param  string  associative descriptor
-	 * @return array
 	 */
-	public function fetchAssoc($assoc)
+	public function fetchAssoc(string $assoc): array
 	{
 		return $this->getResult()->fetchAssoc($assoc);
 	}
@@ -207,11 +184,8 @@ class DataSource implements IDataSource
 
 	/**
 	 * Fetches all records from table like $key => $value pairs.
-	 * @param  string  associative key
-	 * @param  string  value
-	 * @return array
 	 */
-	public function fetchPairs($key = null, $value = null)
+	public function fetchPairs(string $key = null, string $value = null): array
 	{
 		return $this->getResult()->fetchPairs($key, $value);
 	}
@@ -219,9 +193,8 @@ class DataSource implements IDataSource
 
 	/**
 	 * Discards the internal cache.
-	 * @return void
 	 */
-	public function release()
+	public function release(): void
 	{
 		$this->result = $this->count = $this->totalCount = null;
 	}
@@ -232,9 +205,8 @@ class DataSource implements IDataSource
 
 	/**
 	 * Returns this data source wrapped in Fluent object.
-	 * @return Fluent
 	 */
-	public function toFluent()
+	public function toFluent(): Fluent
 	{
 		return $this->connection->select('*')->from('(%SQL) t', $this->__toString());
 	}
@@ -242,9 +214,8 @@ class DataSource implements IDataSource
 
 	/**
 	 * Returns this data source wrapped in DataSource object.
-	 * @return DataSource
 	 */
-	public function toDataSource()
+	public function toDataSource(): self
 	{
 		return new self($this->__toString(), $this->connection);
 	}
@@ -252,19 +223,24 @@ class DataSource implements IDataSource
 
 	/**
 	 * Returns SQL query.
-	 * @return string
 	 */
-	public function __toString()
+	public function __toString(): string
 	{
 		try {
-			return $this->connection->translate('
-SELECT %n', (empty($this->cols) ? '*' : $this->cols), '
-FROM %SQL', $this->sql, '
-%ex', $this->conds ? ['WHERE %and', $this->conds] : null, '
-%ex', $this->sorting ? ['ORDER BY %by', $this->sorting] : null, '
-%ofs %lmt', $this->offset, $this->limit
+			return $this->connection->translate(
+				"\nSELECT %n",
+				(empty($this->cols) ? '*' : $this->cols),
+				"\nFROM %SQL",
+				$this->sql,
+				"\n%ex",
+				$this->conds ? ['WHERE %and', $this->conds] : null,
+				"\n%ex",
+				$this->sorting ? ['ORDER BY %by', $this->sorting] : null,
+				"\n%ofs %lmt",
+				$this->offset,
+				$this->limit
 			);
-		} catch (\Exception $e) {
+		} catch (\Throwable $e) {
 			trigger_error($e->getMessage(), E_USER_ERROR);
 			return '';
 		}
@@ -276,9 +252,8 @@ FROM %SQL', $this->sql, '
 
 	/**
 	 * Returns the number of rows in a given data source.
-	 * @return int
 	 */
-	public function count()
+	public function count(): int
 	{
 		if ($this->count === null) {
 			$this->count = $this->conds || $this->offset || $this->limit
@@ -293,9 +268,8 @@ FROM %SQL', $this->sql, '
 
 	/**
 	 * Returns the number of rows in a given data source.
-	 * @return int
 	 */
-	public function getTotalCount()
+	public function getTotalCount(): int
 	{
 		if ($this->totalCount === null) {
 			$this->totalCount = Helpers::intVal($this->connection->nativeQuery(

+ 8 - 43
api/vendor/dibi/dibi/src/Dibi/DateTime.php

@@ -5,70 +5,35 @@
  * Copyright (c) 2005 David Grudl (https://davidgrudl.com)
  */
 
+declare(strict_types=1);
+
 namespace Dibi;
 
 
 /**
  * DateTime.
  */
-class DateTime extends \DateTime
+class DateTime extends \DateTimeImmutable
 {
 	use Strict;
 
 	/**
-	 * @param  string|int
+	 * @param  string|int  $time
 	 */
 	public function __construct($time = 'now', \DateTimeZone $timezone = null)
 	{
+		$timezone = $timezone ?: new \DateTimeZone(date_default_timezone_get());
 		if (is_numeric($time)) {
-			parent::__construct('@' . $time);
-			$this->setTimeZone($timezone ?: new \DateTimeZone(date_default_timezone_get()));
-		} elseif ($timezone === null) {
-			parent::__construct($time);
+			$tmp = (new self('@' . $time))->setTimezone($timezone);
+			parent::__construct($tmp->format('Y-m-d H:i:s.u'), $tmp->getTimezone());
 		} else {
 			parent::__construct($time, $timezone);
 		}
 	}
 
 
-	public function modifyClone($modify = '')
-	{
-		$dolly = clone $this;
-		return $modify ? $dolly->modify($modify) : $dolly;
-	}
-
-
-	public function setTimestamp($timestamp)
-	{
-		$zone = $this->getTimezone();
-		$this->__construct('@' . $timestamp);
-		return $this->setTimeZone($zone);
-	}
-
-
-	public function getTimestamp()
-	{
-		$ts = $this->format('U');
-		return is_float($tmp = $ts * 1) ? $ts : $tmp;
-	}
-
-
-	public function __toString()
+	public function __toString(): string
 	{
 		return $this->format('Y-m-d H:i:s.u');
 	}
-
-
-	public function __wakeup()
-	{
-		if (isset($this->fix, $this->fix[1])) {
-			$this->__construct($this->fix[0], new \DateTimeZone($this->fix[1]));
-			unset($this->fix);
-		} elseif (isset($this->fix)) {
-			$this->__construct($this->fix[0]);
-			unset($this->fix);
-		} else {
-			parent::__wakeup();
-		}
-	}
 }

Some files were not shown because too many files changed in this diff