Forráskód Böngészése

Merge branch 'v2-develop' into patch-1

causefx 5 éve
szülő
commit
41f27edab4
100 módosított fájl, 22355 hozzáadás és 17299 törlés
  1. 5 0
      .gitignore
  2. 65 51
      README.md
  3. 16 0
      api/classes/custom.class.php
  4. 572 0
      api/classes/deluge.class.php
  5. 5890 0
      api/classes/organizr.class.php
  6. 328 0
      api/classes/ping.class.php
  7. 4 1
      api/composer.json
  8. 788 77
      api/composer.lock
  9. 52 6
      api/config/default.php
  10. 7 44
      api/functions.php
  11. 97 67
      api/functions/2fa-functions.php
  12. 3 1358
      api/functions/api-functions.php
  13. 429 334
      api/functions/auth-functions.php
  14. 112 44
      api/functions/backup-functions.php
  15. 2 141
      api/functions/config-functions.php
  16. 1 12
      api/functions/custom-class.php
  17. 1 571
      api/functions/deluge.class.php
  18. 71 3237
      api/functions/homepage-connect-functions.php
  19. 89 3179
      api/functions/homepage-functions.php
  20. 4 69
      api/functions/log-functions.php
  21. 1 421
      api/functions/netdata-functions.php
  22. 540 439
      api/functions/normal-functions.php
  23. 707 252
      api/functions/option-functions.php
  24. 543 2708
      api/functions/organizr-functions.php
  25. 1 327
      api/functions/ping.class.php
  26. 2 77
      api/functions/plugin-functions.php
  27. 96 80
      api/functions/sso-functions.php
  28. 1 143
      api/functions/static-globals.php
  29. 1 80
      api/functions/theme-functions.php
  30. 47 170
      api/functions/token-functions.php
  31. 105 192
      api/functions/update-functions.php
  32. 45 49
      api/functions/upgrade-functions.php
  33. 184 0
      api/homepage/calendar.php
  34. 233 0
      api/homepage/couchpotato.php
  35. 216 0
      api/homepage/deluge.php
  36. 629 0
      api/homepage/emby.php
  37. 152 0
      api/homepage/healthchecks.php
  38. 145 0
      api/homepage/html.php
  39. 295 0
      api/homepage/ical.php
  40. 119 0
      api/homepage/jackett.php
  41. 200 0
      api/homepage/jdownloader.php
  42. 630 0
      api/homepage/jellyfin.php
  43. 370 0
      api/homepage/lidarr.php
  44. 32 0
      api/homepage/misc.php
  45. 208 0
      api/homepage/monitorr.php
  46. 547 0
      api/homepage/netdata.php
  47. 186 0
      api/homepage/nzbget.php
  48. 140 0
      api/homepage/octoprint.php
  49. 541 0
      api/homepage/ombi.php
  50. 185 0
      api/homepage/pihole.php
  51. 760 0
      api/homepage/plex.php
  52. 267 0
      api/homepage/qbittorrent.php
  53. 444 0
      api/homepage/radarr.php
  54. 372 0
      api/homepage/rtorrent.php
  55. 227 0
      api/homepage/sabnzbd.php
  56. 496 0
      api/homepage/sickrage.php
  57. 443 0
      api/homepage/sonarr.php
  58. 150 0
      api/homepage/speedtest.php
  59. 322 0
      api/homepage/tautulli.php
  60. 248 0
      api/homepage/transmission.php
  61. 311 0
      api/homepage/unifi.php
  62. 233 0
      api/homepage/weather.php
  63. 7 1906
      api/index.php
  64. 39 10
      api/pages/custom/index.html
  65. 9 3
      api/pages/dependencies.php
  66. 19 9
      api/pages/homepage.php
  67. 12 4
      api/pages/lockscreen.php
  68. 16 8
      api/pages/login.php
  69. 15 2
      api/pages/settings-customize-appearance.php
  70. 18 3
      api/pages/settings-image-manager.php
  71. 13 2
      api/pages/settings-plugins.php
  72. 55 0
      api/pages/settings-settings-backup.php
  73. 51 8
      api/pages/settings-settings-logs.php
  74. 15 2
      api/pages/settings-settings-main.php
  75. 15 2
      api/pages/settings-settings-sso.php
  76. 18 6
      api/pages/settings-tab-editor-categories.php
  77. 14 3
      api/pages/settings-tab-editor-homepage-order.php
  78. 17 4
      api/pages/settings-tab-editor-homepage.php
  79. 89 37
      api/pages/settings-tab-editor-tabs.php
  80. 21 3
      api/pages/settings-template.php
  81. 18 9
      api/pages/settings-user-manage-groups.php
  82. 233 4
      api/pages/settings-user-manage-users.php
  83. 70 31
      api/pages/settings.php
  84. 19 31
      api/pages/wizard.php
  85. 191 56
      api/plugins/api/chat.php
  86. 127 56
      api/plugins/api/healthChecks.php
  87. 379 39
      api/plugins/api/invites.php
  88. 135 67
      api/plugins/api/php-mailer.php
  89. 32 18
      api/plugins/api/speedTest.php
  90. 122 115
      api/plugins/chat.php
  91. 1 0
      api/plugins/config/chat.php
  92. 2 2
      api/plugins/config/php-mailer.php
  93. 107 100
      api/plugins/healthChecks.php
  94. 431 341
      api/plugins/invites.php
  95. 7 14
      api/plugins/js/chat.js
  96. 3 24
      api/plugins/js/healthChecks.js
  97. 33 111
      api/plugins/js/invites.js
  98. 23 82
      api/plugins/js/php-mailer.js
  99. 3 58
      api/plugins/js/speedTest.js
  100. 66 0
      api/plugins/misc/emailTemplates/dark.php

+ 5 - 0
.gitignore

@@ -75,6 +75,7 @@ chatpack.db
 Docker.txt
 Github.txt
 Demo.txt
+Dev.txt
 config/cacert.pem
 config/config.php
 config/config*.bak.php
@@ -83,6 +84,7 @@ config/users
 config/users*.db
 config/users*.bak.db
 config/tmp/*
+docs/api.json
 images/cache/*
 backups/*
 backups/
@@ -117,6 +119,8 @@ plugins/plugin_files/*
 !plugins/plugin_files/index.html
 plugins/images/userTabs/*
 !plugins/images/userTabs/index.html
+api/v2/routes/custom/*
+!api/v2/routes/custom/index.html
 # =========================
 # Plugin files
 # =========================
@@ -144,6 +148,7 @@ api/plugins/*
 !api/plugins/misc/emailTemplates/default.php
 !api/plugins/misc/emailTemplates/gray.php
 !api/plugins/misc/emailTemplates/light.php
+!api/plugins/misc/emailTemplates/dark.php
 !api/plugins/misc/emailTemplates/plehex.php
 !api/plugins/misc/speedTest/empty.php
 !api/plugins/misc/speedTest/garbage.php

+ 65 - 51
README.md

@@ -1,4 +1,4 @@
-<p align="center"><img src="https://github.com/causefx/Organizr/raw/v2-develop/plugins/images/organizr/logo-wide.png"></p>
+![OrganizrHeader](https://github.com/causefx/Organizr/raw/v2-develop/plugins/images/organizr/logo-wide.png)
 
 [![Percentage of issues still open](http://isitmaintained.com/badge/open/causefx/Organizr.svg)](http://isitmaintained.com/project/causefx/Organizr "Percentage of issues still open")
 [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/causefx/Organizr.svg)](http://isitmaintained.com/project/causefx/Organizr "Average time to resolve an issue")
@@ -6,89 +6,103 @@
 [![GitHub forks](https://img.shields.io/github/forks/causefx/Organizr.svg)](https://github.com/causefx/Organizr/network)
 [![Docker pulls](https://img.shields.io/docker/pulls/organizr/organizr.svg)](https://hub.docker.com/r/organizr/organizr)
 [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/causefx)
-[![Beerpay](https://beerpay.io/causefx/Organizr/badge.svg?style=beer-square)](https://beerpay.io/causefx/Organizr)  [![Beerpay](https://beerpay.io/causefx/Organizr/make-wish.svg?style=flat-square)](https://beerpay.io/causefx/Organizr?focus=wish)
+[![Beerpay](https://beerpay.io/causefx/Organizr/badge.svg?style=beer-square)](https://beerpay.io/causefx/Organizr)
+[![Beerpay](https://beerpay.io/causefx/Organizr/make-wish.svg?style=flat-square)](https://beerpay.io/causefx/Organizr?focus=wish)
 
-<img src="https://user-images.githubusercontent.com/16184466/53614282-a91e9e00-3b96-11e9-9b3e-d249775ecaa1.png">
+![OrganizrAbout](https://user-images.githubusercontent.com/16184466/53614282-a91e9e00-3b96-11e9-9b3e-d249775ecaa1.png)
 
-Do you have quite a bit of services running on your computer or server?  Do you have a lot of bookmarks or have to memorize a bunch of ip's and ports?  Well, Organizr is here to help with that.  Organizr allows you to setup "Tabs" that will be loaded all in one webpage.  You can then work on your server with ease.  Want to give users access to some Tabs?  No problem, just enable user support and have them make an account.  Want guests to be able to visit too?  Enable Guest support for those tabs.
+Do you have quite a bit of services running on your computer or server? Do you have a lot of bookmarks or have to memorize a bunch of ip's and ports? Well, Organizr is here to help with that. Organizr allows you to setup "Tabs" that will be loaded all in one webpage. You can then work on your server with ease. Want to give users access to some Tabs? No problem, just enable user support and have them make an account. Want guests to be able to visit too? Enable Guest support for those tabs.
 
-<img src="https://user-images.githubusercontent.com/16184466/53614285-a9b73480-3b96-11e9-835e-9fadd045582b.png">
+![OrganizrInfo](https://user-images.githubusercontent.com/16184466/53614285-a9b73480-3b96-11e9-835e-9fadd045582b.png)
 
-- PHP 7.1.3+
+- PHP 7.2+
 - [Official Site](https://organizr.app) - Will be refreshed soon!
 - [Official Discord](https://organizr.app/discord)
-- [See Wiki](https://github.com/causefx/Organizr/wiki) - Will be updated soon!
+
+- [See Wiki](https://docs.organizr.app/) - Will be updated soon!
 - [Docker](https://hub.docker.com/r/organizr/organizr)
 
-<img src="https://user-images.githubusercontent.com/16184466/53614284-a9b73480-3b96-11e9-9bea-d7a30b294267.png">
+![OrganizrGallery](https://user-images.githubusercontent.com/16184466/53614284-a9b73480-3b96-11e9-9bea-d7a30b294267.png)
 
-<img src="https://user-images.githubusercontent.com/16184466/53615855-35cc5a80-3b9d-11e9-882b-f09f3eb18173.png" width="23%"></img> <img src="https://user-images.githubusercontent.com/16184466/53615856-35cc5a80-3b9d-11e9-8428-1f2ae05da2c9.png" width="23%"></img> <img src="https://user-images.githubusercontent.com/16184466/53615857-35cc5a80-3b9d-11e9-82bf-91987c529e72.png" width="23%"></img> <img src="https://user-images.githubusercontent.com/16184466/53615858-35cc5a80-3b9d-11e9-8149-01a7fcd9160a.png" width="23%"></img> 
+<img src="https://user-images.githubusercontent.com/16184466/53615855-35cc5a80-3b9d-11e9-882b-f09f3eb18173.png" width="23%"></img>
+<img src="https://user-images.githubusercontent.com/16184466/53615856-35cc5a80-3b9d-11e9-8428-1f2ae05da2c9.png" width="23%"></img>
+<img src="https://user-images.githubusercontent.com/16184466/53615857-35cc5a80-3b9d-11e9-82bf-91987c529e72.png" width="23%"></img>
+<img src="https://user-images.githubusercontent.com/16184466/53615858-35cc5a80-3b9d-11e9-8149-01a7fcd9160a.png" width="23%"></img>
 
-[![Organizr Overview](https://img.youtube.com/vi/LZL4smFB6wU/0.jpg)](https://www.youtube.com/watch?v=LZL4smFB6wU)
+[![OrganizrOverview](https://img.youtube.com/vi/LZL4smFB6wU/0.jpg)](https://www.youtube.com/watch?v=LZL4smFB6wU)
 
-<img src="https://user-images.githubusercontent.com/16184466/53614283-a9b73480-3b96-11e9-90ef-6e752e067884.png">
+![OrganizrFeat](https://user-images.githubusercontent.com/16184466/53614283-a9b73480-3b96-11e9-90ef-6e752e067884.png)
 
-- Login with Plex/Emby/LDAP or sFTP credentials
+- 'Forgot Password' support [receive an email with your new password, prerequisites: mail server setup]
+- Additional language support
 - Custom tabs for your services
+- Customise the top bar by adding your own site logo or site name
+- Enable or disable iFrame for your tabs
+- Fail2ban support ([see wiki](https://docs.organizr.app/books/setup-features/page/fail2ban-integration))
 - Fullscreen Support
-- Pin/Unpin sidebar
+- Gravatar Support
+- Keyboard shortcut support (Check help tab in settings)
+- Login with Plex/Emby/LDAP or sFTP credentials
 - Mobile support
+- Multiple login support
+- Nginx Auth_Request support ([see wiki](https://docs.organizr.app/books/setup-features/page/serverauth))
+- Organizr login log viewer
+- Personalise any theme: Customise the look and feel of Organizr with access to the colour palette
+- Pin/Unpin sidebar
+- Protect new user account creation with registration password
+- Quick access tabs (access your tabs quickly e.g. www.example.com/#Sonarr)
 - Set default page on launch
+- Theme-able
+- Unlimited User Groups
 - Upload new icons with ease
-- Enable or disable iFrame for your tabs
 - User management support: Create, delete and promote users from the user management console
-- Unlimited User Groups
-- Theme-able
-- Personalise any theme: Customise the look and feel of Organizr with access to the colour palette
-- Organizr login log viewer 
-- Fail2ban support (see wiki)
-- Nginx Auth_Request support
-- Protect new user account creation with registration password
-- 'Forgot Password' support [receive an email with your new password, prerequisites: mail server setup]
-- Multiple login support
-- Keyboard shortcut support (Check help tab in settings)
-- Gravatar Support
-- Customise the top bar by adding your own site logo or site name
-- Additional language support
-- Quick access tabs [access your tabs quickly e.g. www.example.com/#Sonarr]
 - Many more...
 
-<img src="https://user-images.githubusercontent.com/16184466/53614286-a9b73480-3b96-11e9-8495-4944b85b1313.png">
+![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)
 
-<img src="https://user-images.githubusercontent.com/16184466/53667702-fcdcc600-3c2e-11e9-8828-860e531e8096.png">
+![OrganizrDocker](https://user-images.githubusercontent.com/16184466/53667702-fcdcc600-3c2e-11e9-8828-860e531e8096.png)
+
+[![Repository](https://img.shields.io/github/stars/organizr/docker-organizr?color=402885&style=for-the-badge&logo=github&logoColor=41add3&)](https://github.com/Organizr/docker-organizr)
+[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/organizr/docker-organizr/Build%20Container?color=402885&style=for-the-badge&logo=github&logoColor=41add3)](https://github.com/organizr/docker-organizr/actions?query=workflow%3A%22Build+Container%22)
+[![Docker Pulls](https://img.shields.io/docker/pulls/organizr/organizr?color=402885&style=for-the-badge&logo=docker&logoColor=41add3)](https://hub.docker.com/r/organizr/organizr/)
 
 ##### Usage
-```
+
+```bash
 docker create \
   --name=organizr \
   -v <path to data>:/config \
   -e PGID=<gid> -e PUID=<uid>  \
   -p 80:80 \
-  organizrtools/organizr-v2
+  -e fpm="false" `#optional` \
+  -e branch="v2-master" `#optional` \
+  organizr/organizr
 ```
+
 ##### Parameters
+
 The parameters are split into two halves, separated by a colon, the left hand side representing the host and the right the container side. For example with a port -p external:internal - what this shows is the port mapping from internal to external of the container. So `-p 8080:80` would expose port 80 from inside the container to be accessible from the host's IP on port 8080 and `http://192.168.x.x:8080` would show you what's running INSIDE the container on port 80.
 
-* `-p 80` - The port(s)
-* `-v /config` - Mapping the config files for Organizr
-* `-e PGID` Used for GroupID - see below for explanation
-* `-e PUID` Used for UserID - see below for explanation
+- `-p 80` - The port(s)
+- `-v /config` - Mapping the config files for Organizr
+- `-e PGID` Used for GroupID - see below for link
+- `-e PUID` Used for UserID - see below for link
+
+The optional parameters and GID and UID are described in the [readme](https://github.com/Organizr/docker-organizr#parameters) for the container.
 
 ##### Info
-* Shell access whilst the container is running: `docker exec -it organizr /bin/bash`
-* To monitor the logs of the container in realtime: `docker logs -f organizr`
-* Container version number: `docker inspect -f '{{ index .Config.Labels "build_version" }}' organizr`
-* Image version number: `docker inspect -f '{{ index .Config.Labels "build_version" }}' organizrtools/docker-organizr-v2`
-
-<img src="https://user-images.githubusercontent.com/16184466/53614287-a9b73480-3b96-11e9-9c8e-e32b4ae20c0d.png">
-
-<p align="center"><a href="https://www.browserstack.com"><img src="https://avatars2.githubusercontent.com/u/1119453?s=200&v=4g"></a></p>
-<p align="center"><a href="https://www.browserstack.com">BrowserStack</a> for allowing us to use their platform for testing</p>
-<p>This project is supported by:</p>
-<p>
-  <a href="https://www.digitalocean.com/">
-    <img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
-  </a>
-</p>
+
+- Shell access whilst the container is running: `docker exec -it organizr /bin/bash`
+- To monitor the logs of the container in realtime: `docker logs -f organizr`
+
+![OrganizrSponsor](https://user-images.githubusercontent.com/16184466/53614287-a9b73480-3b96-11e9-9c8e-e32b4ae20c0d.png)
+
+### BrowserStack for allowing us to use their platform for testing
+
+[![BrowserStack](https://avatars2.githubusercontent.com/u/1119453?s=200&v=4g)](https://www.browserstack.com)
+
+### This project is supported by
+
+<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="200px"></img>

+ 16 - 0
api/classes/custom.class.php

@@ -0,0 +1,16 @@
+<?php
+
+class Requests_Auth_Digest extends Requests_Auth_Basic
+{
+	
+	/**
+	 * Set cURL parameters before the data is sent
+	 *
+	 * @param resource $handle cURL resource
+	 */
+	public function curl_before_send(&$handle)
+	{
+		curl_setopt($handle, CURLOPT_USERPWD, $this->getAuthString());
+		curl_setopt($handle, CURLOPT_HTTPAUTH, CURLAUTH_ANY); //CURLAUTH_ANY work with Wowza RESTful
+	}
+}

+ 572 - 0
api/classes/deluge.class.php

@@ -0,0 +1,572 @@
+<?php
+
+class deluge
+{
+	private $ch;
+	private $url;
+	private $request_id;
+	
+	public function __construct($host, $password)
+	{
+		$this->url = $host . (substr($host, -1) == "/" ? "" : "/") . "json";
+		$this->request_id = 0;
+		$this->ch = curl_init($this->url);
+		$curl_options = array(
+			CURLOPT_RETURNTRANSFER => true,
+			CURLOPT_HTTPHEADER => array("Accept: application/json", "Content-Type: application/json"),
+			CURLOPT_ENCODING => "",
+			CURLOPT_COOKIEJAR => "",
+			CURLOPT_CONNECTTIMEOUT => 10,
+			CURLOPT_TIMEOUT => 10,
+			CURLOPT_CAINFO => getCert(),
+			CURLOPT_SSL_VERIFYHOST => localURL($host) ? 0 : 2,
+			CURLOPT_SSL_VERIFYPEER => localURL($host) ? 0 : 2,
+			//CURLOPT_SSL_VERIFYPEER => false, THIS IS INSECURE!! However, deluge appears not to send intermediate certificates, so it can be necessary. Use with caution!
+		);
+		curl_setopt_array($this->ch, $curl_options);
+		//Log in and get cookies
+		$result = $this->makeRequest("auth.login", array($password));
+		if (gettype($result) == 'boolean' && $result !== true) {
+			throw new Exception("Login failed");
+		} elseif (gettype($result) == 'string') {
+			throw new Exception($result);
+		}
+	}
+	
+	/////////////////////////////
+	//
+	//webapi functions (https://github.com/idlesign/deluge-webapi)
+	//
+	/////////////////////////////
+	//ids is an array of hashes, or null for all
+	//params is an array of params:
+	//active_time, all_time_download, comment, compact, distributed_copies, download_payload_rate, eta, file_priorities, file_progress, files, hash, is_auto_managed, is_finished, is_seed, max_connections, max_download_speed, max_upload_slots, max_upload_speed, message, move_completed, move_completed_path, move_on_completed, move_on_completed_path, name, next_announce, num_files, num_peers, num_pieces, num_seeds, paused, peers, piece_length, prioritize_first_last, private, progress, queue, ratio, remove_at_ratio, save_path, seed_rank, seeding_time, seeds_peers_ratio, state, stop_at_ratio, stop_ratio, time_added, total_done, total_payload_download, total_payload_upload, total_peers, total_seeds, total_size, total_uploaded, total_wanted, tracker, tracker_host, tracker_status, trackers, upload_payload_rate
+	//or an empty array for all (or null, which returns comment, hash, name, save_path)
+	public function getTorrents($ids, $params)
+	{
+		//if (isset($this->makeRequest("webapi.get_torrents", array($ids, $params))->torrents)) {
+		return $this->makeRequest("webapi.get_torrents", array($ids, $params))->torrents;
+		//} else {
+		//throw new Exception("Login failed");
+		//}
+	}
+	
+	//metainfo is base64 encoded torrent data or a magnet url
+	//returns a torrent hash or null if the torrent wasn't aded
+	//params are torrent addition parameters like download_location?
+	public function addTorrent($metaInfo, $params)
+	{
+		return $this->makeRequest("webapi.add_torrent", array($metaInfo, $params));
+	}
+	
+	/*implemented in core
+	public function removeTorrent($hash, $removeData) {
+		return $this->makeRequest("webapi.remove_torrent", array($hash, $removeData));
+	}*/
+	public function getWebAPIVersion()
+	{
+		return $this->makeRequest("webapi.get_api_version", array());
+	}
+	
+	/////////////////////////////
+	//
+	//core functions
+	//
+	//parsed from https://web.archive.org/web/20150423162855/http://deluge-torrent.org:80/docs/master/core/rpc.html
+	//
+	/////////////////////////////
+	//Adds a torrent file to the session.
+	//Parameters:
+	//filename (string) – the filename of the torrent
+	//filedump (string) – a base64 encoded string of the torrent file contents
+	//options (dict) – the options to apply to the torrent on add
+	public function addTorrentFile($filename, $filedump, $options)
+	{
+		return $this->makeRequest("core.add_torrent_file", array($filename, $filedump, $options));
+	}
+	
+	//Adds a torrent from a magnet link.
+	//Parameters:
+	//uri (string) – the magnet link
+	//options (dict) – the options to apply to the torrent on add
+	public function addTorrentMagnet($uri, $options)
+	{
+		return $this->makeRequest("core.add_torrent_magnet", array($uri, $options));
+	}
+	
+	//Adds a torrent from a url. Deluge will attempt to fetch the torrentfrom url prior to adding it to the session.
+	//Parameters:
+	//url (string) – the url pointing to the torrent file
+	//options (dict) – the options to apply to the torrent on add
+	//headers (dict) – any optional headers to send
+	public function addTorrentUrl($url, $options, $headers)
+	{
+		return $this->makeRequest("core.add_torrent_url", array($url, $options, $headers));
+	}
+	
+	public function connectPeer($torrentId, $ip, $port)
+	{
+		return $this->makeRequest("core.connect_peer", array($torrentId, $ip, $port));
+	}
+	
+	public function createTorrent($path, $tracker, $pieceLength, $comment, $target, $webseeds, $private, $createdBy, $trackers, $addToSession)
+	{
+		return $this->makeRequest("core.create_torrent", array($path, $tracker, $pieceLength, $comment, $target, $webseeds, $private, $createdBy, $trackers, $addToSession));
+	}
+	
+	public function disablePlugin($plugin)
+	{
+		return $this->makeRequest("core.disable_plugin", array($plugin));
+	}
+	
+	public function enablePlugin($plugin)
+	{
+		return $this->makeRequest("core.enable_plugin", array($plugin));
+	}
+	
+	public function forceReannounce($torrentIds)
+	{
+		return $this->makeRequest("core.force_reannounce", array($torrentIds));
+	}
+	
+	//Forces a data recheck on torrent_ids
+	public function forceRecheck($torrentIds)
+	{
+		return $this->makeRequest("core.force_recheck", array($torrentIds));
+	}
+	
+	//Returns a list of plugins available in the core
+	public function getAvailablePlugins()
+	{
+		return $this->makeRequest("core.get_available_plugins", array());
+	}
+	
+	//Returns a dictionary of the session’s cache status.
+	public function getCacheStatus()
+	{
+		return $this->makeRequest("core.get_cache_status", array());
+	}
+	
+	//Get all the preferences as a dictionary
+	public function getConfig()
+	{
+		return $this->makeRequest("core.get_config", array());
+	}
+	
+	//Get the config value for key
+	public function getConfigValue($key)
+	{
+		return $this->makeRequest("core.get_config_value", array($key));
+	}
+	
+	//Get the config values for the entered keys
+	public function getConfigValues($keys)
+	{
+		return $this->makeRequest("core.get_config_values", array($keys));
+	}
+	
+	//Returns a list of enabled plugins in the core
+	public function getEnabledPlugins()
+	{
+		return $this->makeRequest("core.get_enabled_plugins", array());
+	}
+	
+	//returns {field: [(value,count)] }for use in sidebar(s)
+	public function getFilterTree($showZeroHits, $hideCat)
+	{
+		return $this->makeRequest("core.get_filter_tree", array($showZeroHits, $hideCat));
+	}
+	
+	//Returns the number of free bytes at path
+	public function getFreeSpace($path)
+	{
+		return $this->makeRequest("core.get_free_space", array($path));
+	}
+	
+	//Returns the libtorrent version.
+	public function getLibtorrentVersion()
+	{
+		return $this->makeRequest("core.get_libtorrent_version", array());
+	}
+	
+	//Returns the active listen port
+	public function getListenPort()
+	{
+		return $this->makeRequest("core.get_listen_port", array());
+	}
+	
+	//Returns the current number of connections
+	public function getNumConnections()
+	{
+		return $this->makeRequest("core.get_num_connections", array());
+	}
+	
+	public function getPathSize($path)
+	{
+		return $this->makeRequest("core.get_path_size", array($path));
+	}
+	
+	//Returns a list of torrent_ids in the session.
+	public function getSessionState()
+	{
+		return $this->makeRequest("core.get_session_state", array());
+	}
+	
+	//Gets the session status values for ‘keys’, these keys are takingfrom libtorrent’s session status.
+	public function getSessionStatus($keys)
+	{
+		return $this->makeRequest("core.get_session_status", array($keys));
+	}
+	
+	public function getTorrentStatus($torrentId, $keys, $diff)
+	{
+		return $this->makeRequest("core.get_torrent_status", array($torrentId, $keys, $diff));
+	}
+	
+	//returns all torrents , optionally filtered by filter_dict.
+	public function getTorrentsStatus($filterDict, $keys, $diff)
+	{
+		return $this->makeRequest("core.get_torrents_status", array($filterDict, $keys, $diff));
+	}
+	
+	public function glob($path)
+	{
+		return $this->makeRequest("core.glob", array($path));
+	}
+	
+	public function moveStorage($torrentIds, $dest)
+	{
+		return $this->makeRequest("core.move_storage", array($torrentIds, $dest));
+	}
+	
+	//Pause all torrents in the session
+	public function pauseAllTorrents()
+	{
+		return $this->makeRequest("core.pause_all_torrents", array());
+	}
+	
+	public function pauseTorrent($torrentIds)
+	{
+		return $this->makeRequest("core.pause_torrent", array($torrentIds));
+	}
+	
+	public function queueBottom($torrentIds)
+	{
+		return $this->makeRequest("core.queue_bottom", array($torrentIds));
+	}
+	
+	public function queueDown($torrentIds)
+	{
+		return $this->makeRequest("core.queue_down", array($torrentIds));
+	}
+	
+	public function queueTop($torrentIds)
+	{
+		return $this->makeRequest("core.queue_top", array($torrentIds));
+	}
+	
+	public function queueUp($torrentIds)
+	{
+		return $this->makeRequest("core.queue_up", array($torrentIds));
+	}
+	
+	//Removes a torrent from the session.
+	//Parameters:
+	//torrentId (string) – the torrentId of the torrent to remove
+	//removeData (boolean) – if True, remove the data associated with this torrent
+	public function removeTorrent($torrentId, $removeData)
+	{
+		return $this->makeRequest("core.remove_torrent", array($torrentId, $removeData));
+	}
+	
+	//Rename files in torrent_id.  Since this is an asynchronous operation bylibtorrent, watch for the TorrentFileRenamedEvent to know when thefiles have been renamed.
+	//Parameters:
+	//torrentId (string) – the torrentId to rename files
+	//filenames (((index, filename), ...)) – a list of index, filename pairs
+	public function renameFiles($torrentId, $filenames)
+	{
+		return $this->makeRequest("core.rename_files", array($torrentId, $filenames));
+	}
+	
+	//Renames the ‘folder’ to ‘new_folder’ in ‘torrent_id’.  Watch for theTorrentFolderRenamedEvent which is emitted when the folder has beenrenamed successfully.
+	//Parameters:
+	//torrentId (string) – the torrent to rename folder in
+	//folder (string) – the folder to rename
+	//newFolder (string) – the new folder name
+	public function renameFolder($torrentId, $folder, $newFolder)
+	{
+		return $this->makeRequest("core.rename_folder", array($torrentId, $folder, $newFolder));
+	}
+	
+	//Rescans the plugin folders for new plugins
+	public function rescanPlugins()
+	{
+		return $this->makeRequest("core.rescan_plugins", array());
+	}
+	
+	//Resume all torrents in the session
+	public function resumeAllTorrents()
+	{
+		return $this->makeRequest("core.resume_all_torrents", array());
+	}
+	
+	public function resumeTorrent($torrentIds)
+	{
+		return $this->makeRequest("core.resume_torrent", array($torrentIds));
+	}
+	
+	//Set the config with values from dictionary
+	public function setConfig($config)
+	{
+		return $this->makeRequest("core.set_config", array($config));
+	}
+	
+	//Sets the auto managed flag for queueing purposes
+	public function setTorrentAutoManaged($torrentId, $value)
+	{
+		return $this->makeRequest("core.set_torrent_auto_managed", array($torrentId, $value));
+	}
+	
+	//Sets a torrents file priorities
+	public function setTorrentFilePriorities($torrentId, $priorities)
+	{
+		return $this->makeRequest("core.set_torrent_file_priorities", array($torrentId, $priorities));
+	}
+	
+	//Sets a torrents max number of connections
+	public function setTorrentMaxConnections($torrentId, $value)
+	{
+		return $this->makeRequest("core.set_torrent_max_connections", array($torrentId, $value));
+	}
+	
+	//Sets a torrents max download speed
+	public function setTorrentMaxDownloadSpeed($torrentId, $value)
+	{
+		return $this->makeRequest("core.set_torrent_max_download_speed", array($torrentId, $value));
+	}
+	
+	//Sets a torrents max number of upload slots
+	public function setTorrentMaxUploadSlots($torrentId, $value)
+	{
+		return $this->makeRequest("core.set_torrent_max_upload_slots", array($torrentId, $value));
+	}
+	
+	//Sets a torrents max upload speed
+	public function setTorrentMaxUploadSpeed($torrentId, $value)
+	{
+		return $this->makeRequest("core.set_torrent_max_upload_speed", array($torrentId, $value));
+	}
+	
+	//Sets the torrent to be moved when completed
+	public function setTorrentMoveCompleted($torrentId, $value)
+	{
+		return $this->makeRequest("core.set_torrent_move_completed", array($torrentId, $value));
+	}
+	
+	//Sets the path for the torrent to be moved when completed
+	public function setTorrentMoveCompletedPath($torrentId, $value)
+	{
+		return $this->makeRequest("core.set_torrent_move_completed_path", array($torrentId, $value));
+	}
+	
+	//Sets the torrent options for torrent_ids
+	public function setTorrentOptions($torrentIds, $options)
+	{
+		return $this->makeRequest("core.set_torrent_options", array($torrentIds, $options));
+	}
+	
+	//Sets a higher priority to the first and last pieces
+	public function setTorrentPrioritizeFirstLast($torrentId, $value)
+	{
+		return $this->makeRequest("core.set_torrent_prioritize_first_last", array($torrentId, $value));
+	}
+	
+	//Sets the torrent to be removed at ‘stop_ratio’
+	public function setTorrentRemoveAtRatio($torrentId, $value)
+	{
+		return $this->makeRequest("core.set_torrent_remove_at_ratio", array($torrentId, $value));
+	}
+	
+	//Sets the torrent to stop at ‘stop_ratio’
+	public function setTorrentStopAtRatio($torrentId, $value)
+	{
+		return $this->makeRequest("core.set_torrent_stop_at_ratio", array($torrentId, $value));
+	}
+	
+	//Sets the ratio when to stop a torrent if ‘stop_at_ratio’ is set
+	public function setTorrentStopRatio($torrentId, $value)
+	{
+		return $this->makeRequest("core.set_torrent_stop_ratio", array($torrentId, $value));
+	}
+	
+	//Sets a torrents tracker list.  trackers will be [{“url”, “tier”}]
+	public function setTorrentTrackers($torrentId, $trackers)
+	{
+		return $this->makeRequest("core.set_torrent_trackers", array($torrentId, $trackers));
+	}
+	
+	//Checks if the active port is open
+	public function testListenPort()
+	{
+		return $this->makeRequest("core.test_listen_port", array());
+	}
+	
+	public function uploadPlugin($filename, $filedump)
+	{
+		return $this->makeRequest("core.upload_plugin", array($filename, $filedump));
+	}
+	
+	//Returns a list of the exported methods.
+	public function getMethodList()
+	{
+		return $this->makeRequest("daemon.get_method_list", array());
+	}
+	
+	//Returns some info from the daemon.
+	public function info()
+	{
+		return $this->makeRequest("daemon.info", array());
+	}
+	
+	public function shutdown(...$params)
+	{
+		return $this->makeRequest("daemon.shutdown", $params);
+	}
+	
+	/////////////////////////////
+	//
+	//web ui functions
+	//
+	//parsed from https://web.archive.org/web/20150423143401/http://deluge-torrent.org:80/docs/master/modules/ui/web/json_api.html#module-deluge.ui.web.json_api
+	//
+	/////////////////////////////
+	//Parameters:
+	//host (string) – the hostname
+	//port (int) – the port
+	//username (string) – the username to login as
+	//password (string) – the password to login with
+	public function addHost($host, $port, $username, $password)
+	{
+		return $this->makeRequest("web.add_host", array($host, $port, $username, $password));
+	}
+	
+	//Usage
+	public function addTorrents($torrents)
+	{
+		return $this->makeRequest("web.add_torrents", array($torrents));
+	}
+	
+	public function connect($hostId)
+	{
+		return $this->makeRequest("web.connect", array($hostId));
+	}
+	
+	public function connected()
+	{
+		return $this->makeRequest("web.connected", array());
+	}
+	
+	public function deregisterEventListener($event)
+	{
+		return $this->makeRequest("web.deregister_event_listener", array($event));
+	}
+	
+	public function disconnect()
+	{
+		return $this->makeRequest("web.disconnect", array());
+	}
+	
+	public function downloadTorrentFromUrl($url, $cookie)
+	{
+		return $this->makeRequest("web.download_torrent_from_url", array($url, $cookie));
+	}
+	
+	/* in core
+	public function getConfig() {
+		return $this->makeRequest("web.get_config", array());
+	}*/
+	public function getEvents()
+	{
+		return $this->makeRequest("web.get_events", array());
+	}
+	
+	public function getHost($hostId)
+	{
+		return $this->makeRequest("web.get_host", array($hostId));
+	}
+	
+	public function getHostStatus($hostId)
+	{
+		return $this->makeRequest("web.get_host_status", array($hostId));
+	}
+	
+	public function getHosts()
+	{
+		return $this->makeRequest("web.get_hosts", array());
+	}
+	
+	public function getTorrentFiles($torrentId)
+	{
+		return $this->makeRequest("web.get_torrent_files", array($torrentId));
+	}
+	
+	public function getTorrentInfo($filename)
+	{
+		return $this->makeRequest("web.get_torrent_info", array($filename));
+	}
+	
+	public function registerEventListener($event)
+	{
+		return $this->makeRequest("web.register_event_listener", array($event));
+	}
+	
+	public function removeHost($connectionId)
+	{
+		return $this->makeRequest("web.remove_host", array($connectionId));
+	}
+	
+	/*in core
+	public function setConfig($config) {
+		return $this->makeRequest("web.set_config", array($config));
+	}*/
+	public function startDaemon($port)
+	{
+		return $this->makeRequest("web.start_daemon", array($port));
+	}
+	
+	public function stopDaemon($hostId)
+	{
+		return $this->makeRequest("web.stop_daemon", array($hostId));
+	}
+	
+	//Parameters:
+	//keys (list) – the information about the torrents to gather
+	//filterDict (dictionary) – the filters to apply when selecting torrents.
+	public function updateUi($keys, $filterDict)
+	{
+		return $this->makeRequest("web.update_ui", array($keys, $filterDict));
+	}
+	
+	private function makeRequest($method, $params)
+	{
+		$post_data = array("id" => $this->request_id, "method" => $method, "params" => $params);
+		curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($post_data));
+		$result = curl_exec($this->ch);
+		if ($result === false) {
+			throw new Exception("Could not log in due to curl error (no. " . curl_errno($this->ch) . "): " . curl_error($this->ch));
+		}
+		$http_code = curl_getinfo($this->ch, CURLINFO_HTTP_CODE);
+		if ($http_code != 200) {
+			throw new Exception("Request for method $method returned http code $http_code");
+		}
+		$result = json_decode($result);
+		if (!is_null($result->error)) {
+			throw new Exception("Method request returned an error (no. " . $result->error->code . "): " . $result->error->message);
+		}
+		if ($result->id != $this->request_id) {
+			throw new Exception("Response id did not match request id");
+		}
+		$this->request_id++;
+		return $result->result;
+	}
+}

+ 5890 - 0
api/classes/organizr.class.php

@@ -0,0 +1,5890 @@
+<?php
+
+use Dibi\Connection;
+
+class Organizr
+{
+	// Use Custom Functions From Traits;
+	use TwoFAFunctions;
+	use ApiFunctions;
+	use AuthFunctions;
+	use BackupFunctions;
+	use ConfigFunctions;
+	use HomepageConnectFunctions;
+	use HomepageFunctions;
+	use LogFunctions;
+	use NetDataFunctions;
+	use NormalFunctions;
+	use OptionsFunction;
+	use OrganizrFunctions;
+	use PluginFunctions;
+	use StaticFunctions;
+	use SSOFunctions;
+	use TokenFunctions;
+	use UpdateFunctions;
+	use UpgradeFunctions;
+	
+	// Use homepage item functions
+	use CalendarHomepageItem;
+	use CouchPotatoHomepageItem;
+	use DelugeHomepageItem;
+	use EmbyHomepageItem;
+	use HealthChecksHomepageItem;
+	use HTMLHomepageItem;
+	use ICalHomepageItem;
+	use JackettHomepageItem;
+	use JDownloaderHomepageItem;
+	use JellyfinHomepageItem;
+	use LidarrHomepageItem;
+	use MiscHomepageItem;
+	use MonitorrHomepageItem;
+	use NetDataHomepageItem;
+	use NZBGetHomepageItem;
+	use OctoPrintHomepageItem;
+	use OmbiHomepageItem;
+	use PiHoleHomepageItem;
+	use PlexHomepageItem;
+	use QBitTorrentHomepageItem;
+	use RadarrHomepageItem;
+	use RTorrentHomepageItem;
+	use SabNZBdHomepageItem;
+	use SickRageHomepageItem;
+	use SonarrHomepageItem;
+	use SpeedTestHomepageItem;
+	use TautulliHomepageItem;
+	use TransmissionHomepageItem;
+	use UnifiHomepageItem;
+	use WeatherHomepageItem;
+	
+	// ===================================
+	// Organizr Version
+	public $version = '2.1.0';
+	// ===================================
+	// Quick php Version check
+	public $minimumPHP = '7.2';
+	// ===================================
+	protected $db;
+	protected $otherDb;
+	public $config;
+	public $user;
+	public $userConfigPath;
+	public $defaultConfigPath;
+	public $currentTime;
+	public $docker;
+	public $dev;
+	public $demo;
+	public $commit;
+	public $fileHash;
+	public $cookieName;
+	public $organizrLog;
+	public $organizrLoginLog;
+	public $timeExecution;
+	public $root;
+	public $paths;
+	public $updating;
+	public $groupOptions;
+	
+	public function __construct($updating = false)
+	{
+		// First Check PHP Version
+		$this->checkPHP();
+		// Constructed from Updater?
+		$this->updating = $updating;
+		// Set Project Root directory
+		$this->root = dirname(__DIR__, 2);
+		// Set Start Execution Time
+		$this->timeExecution = $this->timeExecution();
+		// Set location path to user config path
+		$this->userConfigPath = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.php';
+		// Set location path to default config path
+		$this->defaultConfigPath = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'default.php';
+		// Set current time
+		$this->currentTime = gmdate("Y-m-d\TH:i:s\Z");
+		// Set variable if install is for official docker
+		$this->docker = (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Docker.txt'));
+		// Set variable if install is for develop
+		$this->dev = (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Dev.txt'));
+		// Set variable if install is for demo
+		$this->demo = (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Demo.txt'));
+		// Set variable if install has commit hash
+		$this->commit = ($this->docker && !$this->dev) ? file_get_contents(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Github.txt') : null;
+		// Set variable to be used as hash for files
+		$this->fileHash = ($this->commit) ?? $this->version;
+		// Load Config file
+		$this->config = $this->config();
+		// Set organizr Log file location
+		$this->organizrLog = ($this->hasDB()) ? $this->config['dbLocation'] . 'organizrLog.json' : false;
+		// Set organizr Login Log file location
+		$this->organizrLoginLog = ($this->hasDB()) ? $this->config['dbLocation'] . 'organizrLoginLog.json' : false;
+		// Set Paths
+		$this->paths = array(
+			'Root Folder' => dirname(__DIR__, 2) . DIRECTORY_SEPARATOR,
+			'Cache Folder' => dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR,
+			'Tab Folder' => dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'userTabs' . DIRECTORY_SEPARATOR,
+			'API Folder' => dirname(__DIR__, 1) . DIRECTORY_SEPARATOR,
+			'DB Folder' => ($this->hasDB()) ? $this->config['dbLocation'] : false
+		);
+		// Connect to DB
+		$this->connectDB();
+		// Check DB Writable
+		$this->checkWritableDB();
+		// Set cookie name for Organizr Instance
+		$this->cookieName = ($this->hasDB()) ? $this->config['uuid'] !== '' ? 'organizr_token_' . $this->config['uuid'] : 'organizr_token_temp' : 'organizr_token_temp';
+		// Get token form cookie and validate
+		$this->user = $this->hasCookie() ? $this->validateToken($_COOKIE[$this->cookieName]) ?? $this->guestUser() : $this->guestUser();
+		// might just run this at index
+		$this->upgradeCheck();
+		// Is Page load Organizr OAuth?
+		$this->checkForOrganizrOAuth();
+	}
+	
+	protected function connectDB()
+	{
+		if ($this->hasDB()) {
+			try {
+				$this->db = new Connection([
+					'driver' => 'sqlite3',
+					'database' => $this->config['dbLocation'] . $this->config['dbName'],
+				]);
+			} catch (Dibi\Exception $e) {
+				$this->db = null;
+			}
+		} else {
+			$this->db = null;
+		}
+	}
+	
+	public function connectOtherDB($file = null)
+	{
+		$file = $file ?? $this->config['dbLocation'] . 'tempMigration.db';
+		try {
+			$this->otherDb = new Connection([
+				'driver' => 'sqlite3',
+				'database' => $file,
+			]);
+		} catch (Dibi\Exception $e) {
+			$this->otherDb = null;
+		}
+	}
+	
+	public function checkForOrganizrOAuth()
+	{
+		// Oauth?
+		if ($this->config['authProxyEnabled'] && $this->config['authProxyHeaderName'] !== '' && $this->config['authProxyWhitelist'] !== '') {
+			if (isset(getallheaders()[$this->config['authProxyHeaderName']])) {
+				$this->coookieSeconds('set', 'organizrOAuth', 'true', 20000, false);
+			}
+		}
+	}
+	
+	public function auth()
+	{
+		if ($this->hasDB()) {
+			$whitelist = isset($_GET['whitelist']) ? $_GET['whitelist'] : false;
+			$blacklist = isset($_GET['blacklist']) ? $_GET['blacklist'] : false;
+			$group = 0;
+			$groupParam = ($_GET['group']) ?? 0;
+			$redirect = false;
+			if (isset($groupParam)) {
+				if (is_numeric($groupParam)) {
+					$group = (int)$groupParam;
+				} else {
+					$group = $this->getTabGroupByTabName($groupParam);
+				}
+			}
+			$currentIP = $this->userIP();
+			$unlocked = ($this->user['locked'] == '1') ? false : true;
+			if (isset($this->user)) {
+				$currentUser = $this->user['username'];
+				$currentGroup = $this->user['groupID'];
+				$currentEmail = $this->user['email'];
+			} else {
+				$currentUser = 'Guest';
+				$currentGroup = $this->getUserLevel();
+				$currentEmail = 'guest@guest.com';
+			}
+			$userInfo = "User: $currentUser | Group: $currentGroup | IP: $currentIP | Requesting Access to Group $group | Result: ";
+			if ($whitelist) {
+				if (in_array($currentIP, $this->arrayIP($whitelist))) {
+					$this->setAPIResponse('success', 'User is whitelisted', 200);
+				}
+			}
+			if ($blacklist) {
+				if (in_array($currentIP, $this->arrayIP($blacklist))) {
+					$this->setAPIResponse('error', $userInfo . ' User is blacklisted', 401);
+				}
+			}
+			if ($group !== null) {
+				if ((isset($_SERVER['HTTP_X_FORWARDED_SERVER']) && $_SERVER['HTTP_X_FORWARDED_SERVER'] == 'traefik') || $this->config['traefikAuthEnable']) {
+					$redirect = 'Location: ' . $this->getServerPath();
+				}
+				if ($this->qualifyRequest($group) && $unlocked) {
+					header("X-Organizr-User: $currentUser");
+					header("X-Organizr-Email: $currentEmail");
+					$this->setAPIResponse('success', $userInfo . ' User is Authorized', 200);
+				} else {
+					if (!$redirect) {
+						$this->setAPIResponse('error', $userInfo . ' User is not Authorized or User is locked', 401);
+					} else {
+						exit(http_response_code(401) . header($redirect));
+					}
+				}
+			} else {
+				$this->setAPIResponse('error', 'Missing info', 401);
+			}
+		}
+		return true;
+	}
+	
+	public function setAPIResponse($result = null, $message = null, $responseCode = null, $data = null)
+	{
+		if ($result) {
+			$GLOBALS['api']['response']['result'] = $result;
+		}
+		if ($message) {
+			$GLOBALS['api']['response']['message'] = $message;
+		}
+		if ($responseCode) {
+			$GLOBALS['responseCode'] = $responseCode;
+		}
+		if ($data) {
+			$GLOBALS['api']['response']['data'] = $data;
+		}
+	}
+	
+	public function checkRoute($request)
+	{
+		$route = $request->getUri()->getPath();
+		$method = $request->getMethod();
+		$data = $this->apiData($request);
+		if (!in_array($route, $GLOBALS['bypass'])) {
+			if ($this->isApprovedRequest($method, $data) === false) {
+				$this->setAPIResponse('error', 'Not authorized', 401);
+				$this->writeLog('success', 'Killed Attack From [' . (isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : 'No Referer') . ']', $this->user['username']);
+				return false;
+			}
+		}
+		return true;
+	}
+	
+	public function apiData($request)
+	{
+		switch ($request->getMethod()) {
+			case 'POST':
+				if ($request->getHeaderLine('Content-Type') == 'application/json') {
+					return json_decode(file_get_contents('php://input', 'r'), true);
+				} else {
+					return $request->getParsedBody();
+				}
+			default:
+				if ($request->getHeaderLine('Content-Type') == 'application/json') {
+					return json_decode(file_get_contents('php://input', 'r'), true);
+				} else {
+					return null;
+				}
+		}
+	}
+	
+	public function getPlugins()
+	{
+		if ($this->hasDB()) {
+			$pluginList = [];
+			foreach ($GLOBALS['plugins'] as $plugin) {
+				foreach ($plugin as $key => $value) {
+					if (strpos($value['license'], $this->config['license']) !== false) {
+						$plugin[$key]['enabled'] = $this->config[$value['configPrefix'] . '-enabled'];
+						$pluginList[$key] = $plugin[$key];
+					}
+				}
+			}
+			return $pluginList;
+		}
+		return false;
+	}
+	
+	public function refreshCookieName()
+	{
+		$this->cookieName = $this->config['uuid'] !== '' ? 'organizr_token_' . $this->config['uuid'] : 'organizr_token_temp';
+	}
+	
+	public function favIcons()
+	{
+		$favicon = '
+	<link rel="apple-touch-icon" sizes="180x180" href="plugins/images/favicon/apple-touch-icon.png">
+	<link rel="icon" type="image/png" sizes="32x32" href="plugins/images/favicon/favicon-32x32.png">
+	<link rel="icon" type="image/png" sizes="16x16" href="plugins/images/favicon/favicon-16x16.png">
+	<link rel="manifest" href="plugins/images/favicon/site.webmanifest">
+	<link rel="mask-icon" href="plugins/images/favicon/safari-pinned-tab.svg" color="#5bbad5">
+	<link rel="shortcut icon" href="plugins/images/favicon/favicon.ico">
+	<meta name="msapplication-TileColor" content="#da532c">
+	<meta name="msapplication-TileImage" content="plugins/images/favicon/mstile-144x144.png">
+	<meta name="msapplication-config" content="plugins/images/favicon/browserconfig.xml">
+	<meta name="theme-color" content="#ffffff">
+	';
+		return ($this->config['favIcon'] == '') ? $favicon : $this->config['favIcon'];
+	}
+	
+	public function pluginGlobalList()
+	{
+		$pluginSearch = '-enabled';
+		$pluginInclude = '-include';
+		$plugins = array_filter($this->config, function ($k) use ($pluginSearch) {
+			return stripos($k, $pluginSearch) !== false;
+		}, ARRAY_FILTER_USE_KEY);
+		$plugins['includes'] = array_filter($this->config, function ($k) use ($pluginInclude) {
+			return stripos($k, $pluginInclude) !== false;
+		}, ARRAY_FILTER_USE_KEY);
+		return $plugins;
+	}
+	
+	public function googleTracking()
+	{
+		if ($this->config['gaTrackingID'] !== '') {
+			return '
+				<script async src="https://www.googletagmanager.com/gtag/js?id=' . $this->config['gaTrackingID'] . '"></script>
+    			<script>
+				    window.dataLayer = window.dataLayer || [];
+				    function gtag(){dataLayer.push(arguments);}
+				    gtag("js", new Date());
+				    gtag("config","' . $this->config['gaTrackingID'] . '");
+    			</script>
+			';
+		}
+		return null;
+	}
+	
+	public function matchBrackets($text, $brackets = 's')
+	{
+		switch ($brackets) {
+			case 's':
+			case 'square':
+				$pattern = '#\[(.*?)\]#';
+				break;
+			case 'c':
+			case 'curly':
+				$pattern = '#\((.*?)\)#';
+				break;
+			default:
+				return null;
+		}
+		preg_match($pattern, $text, $match);
+		return $match[1];
+	}
+	
+	public function languagePacks($encode = false)
+	{
+		$files = array();
+		foreach (glob(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . 'langpack' . DIRECTORY_SEPARATOR . "*.json") as $filename) {
+			if (strpos(basename($filename), '[') !== false) {
+				$explode = explode('[', basename($filename));
+				$files[] = array(
+					'filename' => basename($filename),
+					'code' => $explode[0],
+					'language' => $this->matchBrackets(basename($filename))
+				);
+			}
+		}
+		usort($files, function ($a, $b) {
+			return $a['language'] <=> $b['language'];
+		});
+		return ($encode) ? json_encode($files) : $files;
+	}
+	
+	public function pluginFiles($type)
+	{
+		$files = '';
+		switch ($type) {
+			case 'js':
+				foreach (glob(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . "*.js") as $filename) {
+					$files .= '<script src="api/plugins/js/' . basename($filename) . '?v=' . $this->fileHash . '" defer="true"></script>';
+				}
+				break;
+			case 'css':
+				foreach (glob(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'css' . DIRECTORY_SEPARATOR . "*.css") as $filename) {
+					$files .= '<link href="api/plugins/css/' . basename($filename) . '?v=' . $this->fileHash . '" rel="stylesheet">';
+				}
+				break;
+			default:
+				break;
+		}
+		return $files;
+	}
+	
+	public function formKey($script = true)
+	{
+		if (isset($this->config['organizrHash'])) {
+			if ($this->config['organizrHash'] !== '') {
+				$hash = password_hash(substr($this->config['organizrHash'], 2, 10), PASSWORD_BCRYPT);
+				return ($script) ? '<script>local("s","formKey","' . $hash . '");</script>' : $hash;
+			}
+		}
+	}
+	
+	private function checkPHP()
+	{
+		if (!(version_compare(PHP_VERSION, $this->minimumPHP) >= 0)) {
+			die('Organizr needs PHP Version: ' . $this->minimumPHP . '<br/> You have PHP Version: ' . PHP_VERSION);
+		}
+	}
+	
+	private function checkWritableDB()
+	{
+		if ($this->hasDB()) {
+			$db = is_writable($this->config['dbLocation'] . $this->config['dbName']);
+			if (!$db) {
+				die('Organizr DB is not writable!!!  Please fix...');
+			}
+		}
+	}
+	
+	public function upgradeCheck()
+	{
+		if ($this->hasDB()) {
+			$tempLock = $this->config['dbLocation'] . 'DBLOCK.txt';
+			$updateComplete = $this->config['dbLocation'] . 'completed.txt';
+			$cleanup = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'upgrade' . DIRECTORY_SEPARATOR;
+			if (file_exists($updateComplete)) {
+				@unlink($updateComplete);
+				@$this->rrmdir($cleanup);
+			}
+			if (file_exists($tempLock)) {
+				die('upgrading');
+			}
+			$updateDB = false;
+			$updateSuccess = true;
+			$compare = new Composer\Semver\Comparator;
+			$oldVer = $this->config['configVersion'];
+			// Upgrade check start for version below
+			$versionCheck = '2.0.0-beta-200';
+			if ($compare->lessThan($oldVer, $versionCheck)) {
+				$updateDB = true;
+				$oldVer = $versionCheck;
+			}
+			// End Upgrade check start for version above
+			// Upgrade check start for version below
+			$versionCheck = '2.0.0-beta-500';
+			if ($compare->lessThan($oldVer, $versionCheck)) {
+				$updateDB = true;
+				$oldVer = $versionCheck;
+			}
+			// End Upgrade check start for version above
+			// Upgrade check start for version below
+			$versionCheck = '2.0.0-beta-800';
+			if ($compare->lessThan($oldVer, $versionCheck)) {
+				$updateDB = true;
+				$oldVer = $versionCheck;
+			}
+			// End Upgrade check start for version above
+			// Upgrade check start for version below
+			$versionCheck = '2.1.0';
+			if ($compare->lessThan($oldVer, $versionCheck)) {
+				$updateDB = false;
+				$oldVer = $versionCheck;
+				$this->upgradeToVersion($versionCheck);
+			}
+			// End Upgrade check start for version above
+			if ($updateDB == true) {
+				//return 'Upgraded Needed - Current Version '.$oldVer.' - New Version: '.$versionCheck;
+				// Upgrade database to latest version
+				$updateSuccess = $this->updateDB($oldVer);
+			}
+			// Update config.php version if different to the installed version
+			if ($updateSuccess && $this->version !== $this->config['configVersion']) {
+				$this->updateConfig(array('apply_CONFIG_VERSION' => $this->version));
+			}
+			if ($updateSuccess == false) {
+				die('Database update failed - Please manually check logs and fix - Then reload this page');
+			}
+			return true;
+		}
+	}
+	
+	public function updateDB($oldVerNum = false)
+	{
+		$tempLock = $this->config['dbLocation'] . 'DBLOCK.txt';
+		if (!file_exists($tempLock)) {
+			touch($tempLock);
+			$migrationDB = 'tempMigration.db';
+			$pathDigest = pathinfo($this->config['dbLocation'] . $this->config['dbName']);
+			if (file_exists($this->config['dbLocation'] . $migrationDB)) {
+				unlink($this->config['dbLocation'] . $migrationDB);
+			}
+			// Create Temp DB First
+			$this->connectOtherDB();
+			$backupDB = $pathDigest['dirname'] . '/' . $pathDigest['filename'] . '[' . date('Y-m-d_H-i-s') . ']' . ($oldVerNum ? '[' . $oldVerNum . ']' : '') . '.bak.db';
+			copy($this->config['dbLocation'] . $this->config['dbName'], $backupDB);
+			$success = $this->createDB($this->config['dbLocation'], true);
+			if ($success) {
+				$response = [
+					array(
+						'function' => 'fetchAll',
+						'query' => array(
+							'SELECT name FROM sqlite_master WHERE type="table"'
+						)
+					),
+				];
+				$tables = $this->processQueries($response);
+				foreach ($tables as $table) {
+					$response = [
+						array(
+							'function' => 'fetchAll',
+							'query' => array(
+								'SELECT * FROM ' . $table['name']
+							)
+						),
+					];
+					$data = $this->processQueries($response);
+					$this->writeLog('success', 'Update Function -  Grabbed Table data for Table: ' . $table['name'], 'Database');
+					foreach ($data as $row) {
+						$response = [
+							array(
+								'function' => 'query',
+								'query' => array(
+									'INSERT into ' . $table['name'],
+									$row
+								)
+							),
+						];
+						$this->processQueries($response, true);
+					}
+					$this->writeLog('success', 'Update Function -  Wrote Table data for Table: ' . $table['name'], 'Database');
+				}
+				$this->writeLog('success', 'Update Function -  All Table data converted - Starting Movement', 'Database');
+				$this->db->disconnect();
+				$this->otherDb->disconnect();
+				// Remove Current Database
+				if (file_exists($this->config['dbLocation'] . $migrationDB)) {
+					$oldFileSize = filesize($this->config['dbLocation'] . $this->config['dbName']);
+					$newFileSize = filesize($this->config['dbLocation'] . $migrationDB);
+					if ($newFileSize > 0) {
+						$this->writeLog('success', 'Update Function -  Table Size of new DB ok..', 'Database');
+						@unlink($this->config['dbLocation'] . $this->config['dbName']);
+						copy($this->config['dbLocation'] . $migrationDB, $this->config['dbLocation'] . $this->config['dbName']);
+						@unlink($this->config['dbLocation'] . $migrationDB);
+						$this->writeLog('success', 'Update Function -  Migrated Old Info to new Database', 'Database');
+						@unlink($tempLock);
+						return true;
+					} else {
+						$this->writeLog('error', 'Update Function -  Filesize is zero', 'Database');
+					}
+				} else {
+					$this->writeLog('error', 'Update Function -  Migration DB does not exist', 'Database');
+				}
+				@unlink($tempLock);
+				return false;
+				
+			} else {
+				$this->writeLog('error', 'Update Function -  Could not create migration DB', 'Database');
+			}
+			@unlink($tempLock);
+			return false;
+		}
+		return false;
+	}
+	
+	// Create config file in the return syntax
+	public function createConfig($array, $path = null, $nest = 0)
+	{
+		$path = ($path) ? $path : $this->userConfigPath;
+		// Define Initial Value
+		$output = array();
+		// Sort Items
+		ksort($array);
+		// Update the current config version
+		if (!$nest) {
+			// Inject Current Version
+			$output[] = "\t'configVersion' => '" . (isset($array['apply_CONFIG_VERSION']) ? $array['apply_CONFIG_VERSION'] : $this->version) . "'";
+		}
+		unset($array['configVersion']);
+		unset($array['apply_CONFIG_VERSION']);
+		// Process Settings
+		foreach ($array as $k => $v) {
+			$allowCommit = true;
+			$item = '';
+			switch (gettype($v)) {
+				case 'boolean':
+					$item = ($v ? 'true' : 'false');
+					break;
+				case 'integer':
+				case 'double':
+				case 'NULL':
+					$item = $v;
+					break;
+				case 'string':
+					$item = "'" . str_replace(array('\\', "'"), array('\\\\', "\'"), $v) . "'";
+					break;
+				case 'array':
+					$item = $this->createConfig($v, false, $nest + 1);
+					break;
+				default:
+					$allowCommit = false;
+			}
+			if ($allowCommit) {
+				$output[] = str_repeat("\t", $nest + 1) . "'$k' => $item";
+			}
+		}
+		// Build output
+		$output = (!$nest ? "<?php\nreturn " : '') . "array(\n" . implode(",\n", $output) . "\n" . str_repeat("\t", $nest) . ')' . (!$nest ? ';' : '');
+		if (!$nest && $path) {
+			$pathDigest = pathinfo($path);
+			@mkdir($pathDigest['dirname'], 0770, true);
+			if (file_exists($path)) {
+				rename($path, $pathDigest['dirname'] . '/' . $pathDigest['filename'] . '.bak.php');
+			}
+			$file = fopen($path, 'w');
+			fwrite($file, $output);
+			fclose($file);
+			if (file_exists($path)) {
+				return true;
+			}
+			return false;
+		} else {
+			return $output;
+		}
+	}
+	
+	// Commit new values to the configuration
+	public function updateConfig($new, $current = false)
+	{
+		// Get config if not supplied
+		if ($current === false) {
+			//$current = $this->loadConfig();
+			$current = $this->config;
+		} elseif (is_string($current) && is_file($current)) {
+			$current = $this->loadConfig($current);
+		}
+		// Inject Parts
+		foreach ($new as $k => $v) {
+			$current[$k] = $v;
+		}
+		// Return Create
+		return $this->createConfig($current);
+	}
+	
+	public function loadConfig($path = null)
+	{
+		$path = ($path) ? $path : $this->userConfigPath;
+		if (!is_file($path)) {
+			return null;
+		} else {
+			return (array)call_user_func(function () use ($path) {
+				return include($path);
+			});
+		}
+	}
+	
+	public function fillDefaultConfig($array)
+	{
+		$path = $this->defaultConfigPath;
+		if (is_string($path)) {
+			$loadedDefaults = $this->loadConfig($path);
+		} else {
+			$loadedDefaults = $path;
+		}
+		// Include all plugin config files
+		foreach (glob(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . "*.php") as $filename) {
+			$loadedDefaults = array_merge($loadedDefaults, $this->loadConfig($filename));
+		}
+		return (is_array($loadedDefaults) ? $this->fillDefaultConfig_recurse($array, $loadedDefaults) : false);
+	}
+	
+	public function fillDefaultConfig_recurse($current, $defaults)
+	{
+		foreach ($defaults as $k => $v) {
+			if (!isset($current[$k])) {
+				$current[$k] = $v;
+			} elseif (is_array($current[$k]) && is_array($v)) {
+				$current[$k] = $this->fillDefaultConfig_recurse($current[$k], $v);
+			}
+		}
+		return $current;
+	}
+	
+	public function config()
+	{
+		// Load config or default
+		if (file_exists($this->userConfigPath)) {
+			$config = $this->fillDefaultConfig($this->loadConfig($this->userConfigPath));
+		} else {
+			$config = $this->fillDefaultConfig($this->loadConfig($this->defaultConfigPath));
+		}
+		return $config;
+	}
+	
+	public function combineConfig($array)
+	{
+		$this->config = array_merge($this->config, $array);
+		return $this->config;
+	}
+	
+	public function status()
+	{
+		$status = array();
+		$dependenciesActive = array();
+		$dependenciesInactive = array();
+		$extensions = array("PDO_SQLITE", "PDO", "SQLITE3", "zip", "cURL", "openssl", "simplexml", "json", "session", "filter");
+		$functions = array("hash", "fopen", "fsockopen", "fwrite", "fclose", "readfile");
+		foreach ($extensions as $check) {
+			if (extension_loaded($check)) {
+				array_push($dependenciesActive, $check);
+			} else {
+				array_push($dependenciesInactive, $check);
+			}
+		}
+		foreach ($functions as $check) {
+			if (function_exists($check)) {
+				array_push($dependenciesActive, $check);
+			} else {
+				array_push($dependenciesInactive, $check);
+			}
+		}
+		if (!file_exists($this->userConfigPath)) {
+			$status['status'] = "wizard";//wizard - ok for test
+		}
+		if (count($dependenciesInactive) > 0 || !is_writable(dirname(__DIR__, 2)) || !(version_compare(PHP_VERSION, $this->minimumPHP) >= 0)) {
+			$status['status'] = "dependencies";
+		}
+		$status['status'] = ($status['status']) ?? "ok";
+		$status['writable'] = is_writable(dirname(__DIR__, 2)) ? 'yes' : 'no';
+		$status['minVersion'] = (version_compare(PHP_VERSION, $this->minimumPHP) >= 0) ? 'yes' : 'no';
+		$status['dependenciesActive'] = $dependenciesActive;
+		$status['dependenciesInactive'] = $dependenciesInactive;
+		$status['version'] = $this->version;
+		$status['os'] = $this->getOS();
+		$status['php'] = phpversion();
+		$status['userConfigPath'] = $this->userConfigPath;
+		return $status;
+	}
+	
+	public function hasDB()
+	{
+		return (file_exists($this->userConfigPath)) ?? false;
+	}
+	
+	public function hasCookie()
+	{
+		return ($_COOKIE[$this->cookieName]) ?? false;
+	}
+	
+	public function getGuest()
+	{
+		$guest = array(
+			'group' => 'Guest',
+			'group_id' => 999,
+			'image' => 'plugins/images/groups/guest.png'
+		);
+		$response = [
+			array(
+				'function' => 'fetch',
+				'query' => 'SELECT * FROM groups WHERE `group_id` = 999'
+			),
+		];
+		return $this->hasDB() ? $this->processQueries($response) : $guest;
+		
+	}
+	
+	public function getSchema()
+	{
+		$response = [
+			array(
+				'function' => 'fetchAll',
+				'query' => 'SELECT name, sql FROM sqlite_master WHERE type=\'table\' ORDER BY name'
+			),
+		];
+		return $this->hasDB() ? $this->processQueries($response) : 'Database not setup yet';
+	}
+	
+	public function guestUser()
+	{
+		if ($this->hasDB()) {
+			if ($this->getUserLevel() !== 999) {
+				$guest = array(
+					"token" => null,
+					"tokenDate" => null,
+					"tokenExpire" => null,
+					"username" => "Organizr API",
+					"uid" => $this->guestHash(0, 5),
+					"group" => 'Admin',
+					"groupID" => 0,
+					"email" => null,
+					//"groupImage"=>getGuest()['image'],
+					"image" => $this->getGuest()['image'],
+					"userID" => null,
+					"loggedin" => false,
+					"locked" => false,
+					"tokenList" => null,
+					"authService" => null
+				);
+			}
+		}
+		$guest = $guest ?? array(
+				"token" => null,
+				"tokenDate" => null,
+				"tokenExpire" => null,
+				"username" => "Guest",
+				"uid" => $this->guestHash(0, 5),
+				"group" => $this->getGuest()['group'],
+				"groupID" => $this->getGuest()['group_id'],
+				"email" => null,
+				//"groupImage"=>getGuest()['image'],
+				"image" => $this->getGuest()['image'],
+				"userID" => null,
+				"loggedin" => false,
+				"locked" => false,
+				"tokenList" => null,
+				"authService" => null
+			);
+		return $guest;
+	}
+	
+	public function getAllUserTokens($id)
+	{
+		
+		$response = [
+			array(
+				'function' => 'fetchAll',
+				'query' => array(
+					'SELECT * FROM `tokens` WHERE user_id = ? AND expires > ?',
+					[$id],
+					[$this->currentTime]
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getUserById($id)
+	{
+		$response = [
+			array(
+				'function' => 'fetch',
+				'query' => array(
+					'SELECT * FROM users WHERE id = ?',
+					$id
+				)
+			)
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getUserByEmail($email)
+	{
+		$response = [
+			array(
+				'function' => 'fetch',
+				'query' => array(
+					'SELECT * FROM users WHERE email = ? COLLATE NOCASE',
+					$email
+				)
+			)
+		];
+		return $this->processQueries($response);
+	}
+	
+	protected function invalidToken()
+	{
+		$this->coookie('delete', $this->cookieName);
+		$this->user = null;
+	}
+	
+	public function validateToken($token)
+	{
+		// Validate script
+		$userInfo = $this->jwtParse($token);
+		$validated = $userInfo ? true : false;
+		if ($validated == true) {
+			$allTokens = $this->getAllUserTokens($userInfo['userID']);
+			$user = $this->getUserById($userInfo['userID']);
+			$tokenCheck = ($this->searchArray($allTokens, 'token', $token) !== false);
+			if (!$tokenCheck) {
+				$this->invalidToken();
+				return false;
+			} else {
+				return array(
+					"token" => $token,
+					"tokenDate" => $userInfo['tokenDate'],
+					"tokenExpire" => $userInfo['tokenExpire'],
+					"username" => $user['username'],
+					"uid" => $this->guestHash(0, 5),
+					"group" => $user['group'],
+					"groupID" => $user['group_id'],
+					"email" => $user['email'],
+					"image" => $user['image'],
+					"userID" => $user['id'],
+					"loggedin" => true,
+					"locked" => $user['locked'],
+					"tokenList" => $allTokens,
+					"authService" => explode('::', $user['auth_service'])[0]
+				);
+			}
+		} else {
+			$this->invalidToken();
+		}
+		return false;
+	}
+	
+	public function defaultUserGroup()
+	{
+		$response = [
+			array(
+				'function' => 'fetch',
+				'query' => 'SELECT * FROM groups WHERE `default` = 1'
+			)
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getAllTabs()
+	{
+		$response = [
+			array(
+				'function' => 'fetchAll',
+				'query' => 'SELECT * FROM tabs ORDER BY `order` ASC',
+				'key' => 'tabs'
+			),
+			array(
+				'function' => 'fetchAll',
+				'query' => 'SELECT * FROM categories ORDER BY `order` ASC',
+				'key' => 'categories'
+			),
+			array(
+				'function' => 'fetchAll',
+				'query' => 'SELECT * FROM groups ORDER BY `group_id` ASC',
+				'key' => 'groups'
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getUsers()
+	{
+		$response = [
+			array(
+				'function' => 'fetchAll',
+				'query' => 'SELECT * FROM users'
+			),
+			array(
+				'function' => 'fetchAll',
+				'query' => 'SELECT * FROM groups ORDER BY group_id ASC'
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function usernameTaken($username, $email, $id = null)
+	{
+		if ($id) {
+			$response = [
+				array(
+					'function' => 'fetch',
+					'query' => array(
+						'SELECT * FROM users WHERE `id` != ? AND (username = ? COLLATE NOCASE or email = ? COLLATE NOCASE)',
+						$id,
+						$username,
+						$email
+					)
+				),
+			];
+		} else {
+			$response = [
+				array(
+					'function' => 'fetch',
+					'query' => array(
+						'SELECT * FROM users WHERE username = ? COLLATE NOCASE or email = ? COLLATE NOCASE',
+						[$username],
+						[$email]
+					)
+				),
+			];
+		}
+		return $this->processQueries($response);
+	}
+	
+	public function cleanPageName($page)
+	{
+		return ($page) ? strtolower(str_replace(array('%20', ' ', '-', '_'), '_', $page)) : '';
+	}
+	
+	public function cleanClassName($name)
+	{
+		return ($name) ? (str_replace(array('%20', ' ', '-', '_'), '-', $name)) : '';
+	}
+	
+	public function reverseCleanClassName($name)
+	{
+		return ($name) ? (str_replace(array('%20', '-', '_'), ' ', $name)) : '';
+	}
+	
+	public function getPageList()
+	{
+		return $GLOBALS['organizrPages'];
+	}
+	
+	public function getPage($page)
+	{
+		if (!$page) {
+			$this->setAPIResponse('error', 'Page not setup', 409);
+			return null;
+		}
+		$pageFunction = 'get_page_' . $this->cleanPageName($page);
+		if (function_exists($pageFunction)) {
+			return $pageFunction($this);
+		} else {
+			$this->setAPIResponse('error', 'Page not setup', 409);
+			return null;
+		}
+	}
+	
+	public function getUserLevel()
+	{
+		// Grab token
+		$requesterToken = isset($this->getallheaders()['Token']) ? $this->getallheaders()['Token'] : (isset($_GET['apikey']) ? $_GET['apikey'] : false);
+		$apiKey = ($this->config['organizrAPI']) ?? null;
+		// Check token or API key
+		// If API key, return 0 for admin
+		if (strlen($requesterToken) == 20 && $requesterToken == $apiKey) {
+			//DO API CHECK
+			return 0;
+		} elseif (isset($this->user)) {
+			return $this->user['groupID'];
+		}
+		// All else fails?  return guest id
+		return 999;
+	}
+	
+	public function qualifyRequest($accessLevelNeeded, $api = false)
+	{
+		if ($this->getUserLevel() <= $accessLevelNeeded && $this->getUserLevel() !== null) {
+			return true;
+		} else {
+			if ($api) {
+				$this->setAPIResponse('error', 'Not Authorized', 401);
+			}
+			return false;
+		}
+	}
+	
+	public function getImages()
+	{
+		$allIconsPrep = array();
+		$allIcons = array();
+		$ignore = array(".", "..", "._.DS_Store", ".DS_Store", ".pydio_id", "index.html");
+		$dirname = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'tabs' . DIRECTORY_SEPARATOR;
+		$path = 'plugins/images/tabs/';
+		$images = scandir($dirname);
+		foreach ($images as $image) {
+			if (!in_array($image, $ignore)) {
+				$allIconsPrep[$image] = array(
+					'path' => $path,
+					'name' => $image
+				);
+			}
+		}
+		$dirname = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'userTabs' . DIRECTORY_SEPARATOR;
+		$path = 'plugins/images/userTabs/';
+		$images = scandir($dirname);
+		foreach ($images as $image) {
+			if (!in_array($image, $ignore)) {
+				$allIconsPrep[$image] = array(
+					'path' => $path,
+					'name' => $image
+				);
+			}
+		}
+		uksort($allIconsPrep, 'strcasecmp');
+		foreach ($allIconsPrep as $item) {
+			$allIcons[] = $item['path'] . $item['name'];
+		}
+		return $allIcons;
+	}
+	
+	public function getImagesSelect()
+	{
+		$term = $_GET['search'] ?? null;
+		$page = $_GET['page'] ?? 1;
+		$limit = $_GET['limit'] ?? 20;
+		$offset = ($page * $limit) - $limit;
+		$goodIcons['results'] = [];
+		$goodIcons['limit'] = $limit;
+		$goodIcons['page'] = $page;
+		$goodIcons['term'] = $term;
+		$imageListing = $this->getImages();
+		$newImageListing = [];
+		foreach ($imageListing as $image) {
+			$newImageListing[] = [
+				'id' => $image,
+				'text' => basename($image)
+			];
+		}
+		foreach ($newImageListing as $k => $v) {
+			if (stripos($v['text'], $term) !== false || !$term) {
+				$goodIcons['results'][] = $v;
+			}
+		}
+		$total = count($goodIcons['results']);
+		$goodIcons['total'] = $total;
+		$goodIcons['results'] = array_slice($goodIcons['results'], $offset, $limit);
+		$goodIcons['pagination']['more'] = $page < (ceil($total / $limit));
+		return $goodIcons;
+	}
+	
+	public function removeImage($image = null)
+	{
+		if (!$image) {
+			$this->setAPIResponse('error', 'No image supplied', 422);
+			return false;
+		}
+		$approvedPath = 'plugins/images/userTabs/';
+		$removeImage = $approvedPath . pathinfo($image, PATHINFO_BASENAME);
+		if ($this->approvedFileExtension($removeImage)) {
+			if (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . $removeImage)) {
+				$this->writeLog('success', 'Image Manager Function -  Deleted Image [' . pathinfo($image, PATHINFO_BASENAME) . ']', $this->user['username']);
+				$this->setAPIResponse(null, pathinfo($image, PATHINFO_BASENAME) . ' has been deleted', null);
+				return (unlink(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . $removeImage));
+			} else {
+				$this->setAPIResponse('error', $removeImage . ' does not exist', 404);
+				return false;
+			}
+		} else {
+			$this->setAPIResponse('error', $removeImage . ' is not approved to be deleted', 409);
+			return false;
+		}
+	}
+	
+	public function uploadImage()
+	{
+		$filesCheck = array_filter($_FILES);
+		if (!empty($filesCheck) && $this->approvedFileExtension($_FILES['file']['name']) && strpos($_FILES['file']['type'], 'image/') !== false) {
+			ini_set('upload_max_filesize', '10M');
+			ini_set('post_max_size', '10M');
+			$tempFile = $_FILES['file']['tmp_name'];
+			$targetPath = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'userTabs' . DIRECTORY_SEPARATOR;
+			$targetFile = $targetPath . $_FILES['file']['name'];
+			$this->setAPIResponse(null, pathinfo($_FILES['file']['name'], PATHINFO_BASENAME) . ' has been uploaded', null);
+			return move_uploaded_file($tempFile, $targetFile);
+		}
+	}
+	
+	public function ping($pings)
+	{
+		if ($this->qualifyRequest($this->config['pingAuth'], true)) {
+			if (!$pings['list']) {
+				$this->setAPIResponse('error', 'No ping hostname/IP\'s entered', 409);
+				return null;
+			}
+			$pings = $pings['list'];
+			$type = gettype($pings);
+			$ping = new Ping("");
+			$ping->setTtl(128);
+			$ping->setTimeout(2);
+			switch ($type) {
+				case "array":
+					$results = [];
+					foreach ($pings as $k => $v) {
+						if (strpos($v, ':') !== false) {
+							$domain = explode(':', $v)[0];
+							$port = explode(':', $v)[1];
+							$ping->setHost($domain);
+							$ping->setPort($port);
+							$latency = $ping->ping('fsockopen');
+						} else {
+							$ping->setHost($v);
+							$latency = $ping->ping();
+						}
+						if ($latency || $latency === 0) {
+							$results[$v] = $latency;
+						} else {
+							$results[$v] = false;
+						}
+					}
+					break;
+				case "string":
+					if (strpos($pings, ':') !== false) {
+						$domain = explode(':', $pings)[0];
+						$port = explode(':', $pings)[1];
+						$ping->setHost($domain);
+						$ping->setPort($port);
+						$latency = $ping->ping('fsockopen');
+					} else {
+						$ping->setHost($pings);
+						$latency = $ping->ping();
+					}
+					if ($latency || $latency === 0) {
+						$results = $latency;
+					} else {
+						$results = null;
+					}
+					break;
+			}
+			return ($results) ?? null;
+		}
+		return null;
+	}
+	
+	public function getCustomizeAppearance()
+	{
+		return array(
+			'Top Bar' => array(
+				array(
+					'type' => 'input',
+					'name' => 'logo',
+					'label' => 'Logo',
+					'value' => $this->config['logo']
+				),
+				array(
+					'type' => 'input',
+					'name' => 'title',
+					'label' => 'Title',
+					'value' => $this->config['title']
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'useLogo',
+					'label' => 'Use Logo instead of Title',
+					'value' => $this->config['useLogo'],
+					'help' => 'Also sets the title of your site'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'description',
+					'label' => 'Meta Description',
+					'value' => $this->config['description'],
+					'help' => 'Used to set the description for SEO meta tags'
+				),
+			),
+			'Login Page' => array(
+				array(
+					'type' => 'input',
+					'name' => 'loginLogo',
+					'label' => 'Login Logo',
+					'value' => $this->config['loginLogo'],
+				),
+				array(
+					'type' => 'input',
+					'name' => 'loginWallpaper',
+					'label' => 'Login Wallpaper',
+					'value' => $this->config['loginWallpaper'],
+					'help' => 'You may enter multiple URL\'s using the CSV format.  i.e. link#1,link#2,link#3'
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'useLogoLogin',
+					'label' => 'Use Logo instead of Title on Login Page',
+					'value' => $this->config['useLogoLogin']
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'minimalLoginScreen',
+					'label' => 'Minimal Login Screen',
+					'value' => $this->config['minimalLoginScreen']
+				)
+			),
+			'Options' => array(
+				array(
+					'type' => 'switch',
+					'name' => 'alternateHomepageHeaders',
+					'label' => 'Alternate Homepage Titles',
+					'value' => $this->config['alternateHomepageHeaders']
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'debugErrors',
+					'label' => 'Show Debug Errors',
+					'value' => $this->config['debugErrors']
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'githubMenuLink',
+					'label' => 'Show GitHub Repo Link',
+					'value' => $this->config['githubMenuLink']
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'organizrSupportMenuLink',
+					'label' => 'Show Organizr Support Link',
+					'value' => $this->config['organizrSupportMenuLink']
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'organizrDocsMenuLink',
+					'label' => 'Show Organizr Docs Link',
+					'value' => $this->config['organizrDocsMenuLink']
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'organizrSignoutMenuLink',
+					'label' => 'Show Organizr Sign out & in Button on Sidebar',
+					'value' => $this->config['organizrSignoutMenuLink']
+				),
+				array(
+					'type' => 'select',
+					'name' => 'unsortedTabs',
+					'label' => 'Unsorted Tab Placement',
+					'value' => $this->config['unsortedTabs'],
+					'options' => array(
+						array(
+							'name' => 'Top',
+							'value' => 'top'
+						),
+						array(
+							'name' => 'Bottom',
+							'value' => 'bottom'
+						)
+					)
+				),
+				array(
+					'type' => 'input',
+					'name' => 'gaTrackingID',
+					'label' => 'Google Analytics Tracking ID',
+					'placeholder' => 'e.g. UA-XXXXXXXXX-X',
+					'value' => $this->config['gaTrackingID']
+				)
+			),
+			'Colors & Themes' => array(
+				array(
+					'type' => 'html',
+					'override' => 12,
+					'label' => 'Custom CSS [Can replace colors from above]',
+					'html' => '
+					<div class="row">
+					    <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">
+					                    <span lang="en">The value of #987654 is just a placeholder, you can change to any value you like.</span>
+					                    <span lang="en">To revert back to default, save with no value defined in the relevant field.</span>
+					                </div>
+					            </div>
+					        </div>
+					    </div>
+					</div>
+					',
+				),
+				array(
+					'type' => 'blank',
+					'label' => ''
+				),
+				array(
+					'type' => 'input',
+					'name' => 'headerColor',
+					'label' => 'Nav Bar Color',
+					'value' => $this->config['headerColor'],
+					'class' => 'pick-a-color',
+					'attr' => 'data-original="' . $this->config['headerColor'] . '"'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'headerTextColor',
+					'label' => 'Nav Bar Text Color',
+					'value' => $this->config['headerTextColor'],
+					'class' => 'pick-a-color',
+					'attr' => 'data-original="' . $this->config['headerTextColor'] . '"'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'sidebarColor',
+					'label' => 'Side Bar Color',
+					'value' => $this->config['sidebarColor'],
+					'class' => 'pick-a-color',
+					'attr' => 'data-original="' . $this->config['sidebarColor'] . '"'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'sidebarTextColor',
+					'label' => 'Side Bar Text Color',
+					'value' => $this->config['sidebarTextColor'],
+					'class' => 'pick-a-color',
+					'attr' => 'data-original="' . $this->config['sidebarTextColor'] . '"'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'accentColor',
+					'label' => 'Accent Color',
+					'value' => $this->config['accentColor'],
+					'class' => 'pick-a-color',
+					'attr' => 'data-original="' . $this->config['accentColor'] . '"'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'accentTextColor',
+					'label' => 'Accent Text Color',
+					'value' => $this->config['accentTextColor'],
+					'class' => 'pick-a-color',
+					'attr' => 'data-original="' . $this->config['accentTextColor'] . '"'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'buttonColor',
+					'label' => 'Button Color',
+					'value' => $this->config['buttonColor'],
+					'class' => 'pick-a-color',
+					'attr' => 'data-original="' . $this->config['buttonColor'] . '"'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'buttonTextColor',
+					'label' => 'Button Text Color',
+					'value' => $this->config['buttonTextColor'],
+					'class' => 'pick-a-color',
+					'attr' => 'data-original="' . $this->config['buttonTextColor'] . '"'
+				),
+				array(
+					'type' => 'select',
+					'name' => 'theme',
+					'label' => 'Theme',
+					'class' => 'themeChanger',
+					'value' => $this->config['theme'],
+					'options' => $this->getThemes()
+				),
+				array(
+					'type' => 'select',
+					'name' => 'style',
+					'label' => 'Style',
+					'class' => 'styleChanger',
+					'value' => $this->config['style'],
+					'options' => array(
+						array(
+							'name' => 'Light',
+							'value' => 'light'
+						),
+						array(
+							'name' => 'Dark',
+							'value' => 'dark'
+						),
+						array(
+							'name' => 'Horizontal',
+							'value' => 'horizontal'
+						)
+					)
+				)
+			),
+			'Notifications' => array(
+				array(
+					'type' => 'select',
+					'name' => 'notificationBackbone',
+					'class' => 'notifyChanger',
+					'label' => 'Type',
+					'value' => $this->config['notificationBackbone'],
+					'options' => $this->notificationTypesOptions()
+				),
+				array(
+					'type' => 'select',
+					'name' => 'notificationPosition',
+					'class' => 'notifyPositionChanger',
+					'label' => 'Position',
+					'value' => $this->config['notificationPosition'],
+					'options' => $this->notificationPositionsOptions()
+				),
+				array(
+					'type' => 'html',
+					'label' => 'Test Message',
+					'html' => '
+					<div class="btn-group m-r-10 dropup">
+						<button aria-expanded="false" data-toggle="dropdown" class="btn btn-info btn-outline dropdown-toggle waves-effect waves-light" type="button">
+							<i class="fa fa-comment m-r-5"></i>
+							<span>Test </span>
+						</button>
+						<ul role="menu" class="dropdown-menu">
+							<li><a onclick="message(\'Test Message\',\'This is a success Message\',activeInfo.settings.notifications.position,\'#FFF\',\'success\',\'5000\');">Success</a></li>
+							<li><a onclick="message(\'Test Message\',\'This is a info Message\',activeInfo.settings.notifications.position,\'#FFF\',\'info\',\'5000\');">Info</a></li>
+							<li><a onclick="message(\'Test Message\',\'This is a warning Message\',activeInfo.settings.notifications.position,\'#FFF\',\'warning\',\'5000\');">Warning</a></li>
+							<li><a onclick="message(\'Test Message\',\'This is a error Message\',activeInfo.settings.notifications.position,\'#FFF\',\'error\',\'5000\');">Error</a></li>
+						</ul>
+					</div>
+					'
+				)
+			),
+			'FavIcon' => array(
+				array(
+					'type' => 'textbox',
+					'name' => 'favIcon',
+					'class' => '',
+					'label' => 'Fav Icon Code',
+					'value' => $this->config['favIcon'],
+					'placeholder' => 'Paste Contents from https://realfavicongenerator.net/',
+					'attr' => 'rows="10"',
+				),
+				array(
+					'type' => 'html',
+					'label' => 'Instructions',
+					'html' => '
+					<div class="panel panel-default">
+						<div class="panel-heading">
+							<a href="https://realfavicongenerator.net/" target="_blank"><span class="label label-info m-l-5">Visit FavIcon Site</span></a>
+						</div>
+						<div class="panel-wrapper collapse in">
+							<div class="panel-body">
+								<ul class="list-icons">
+									<li lang="en"><i class="fa fa-caret-right text-info"></i> Click [Select your Favicon picture]</li>
+									<li lang="en"><i class="fa fa-caret-right text-info"></i> Choose your image to use</li>
+									<li lang="en"><i class="fa fa-caret-right text-info"></i> Edit settings to your liking</li>
+									<li lang="en"><i class="fa fa-caret-right text-info"></i> At bottom of page on [Favicon Generator Options] under [Path] choose [I cannot or I do not want to place favicon files at the root of my web site.]</li>
+									<li lang="en"><i class="fa fa-caret-right text-info"></i> Enter this path <code>plugins/images/faviconCustom</code></li>
+									<li lang="en"><i class="fa fa-caret-right text-info"></i> Click [Generate your Favicons and HTML code]</li>
+									<li lang="en"><i class="fa fa-caret-right text-info"></i> Download and unzip file and place in <code>plugins/images/faviconCustom</code></li>
+									<li lang="en"><i class="fa fa-caret-right text-info"></i> Copy code and paste inside left box</li>
+								</ul>
+							</div>
+						</div>
+					</div>
+					'
+				),
+			),
+			'Custom CSS' => array(
+				array(
+					'type' => 'html',
+					'override' => 12,
+					'label' => 'Custom CSS [Can replace colors from above]',
+					'html' => '<button type="button" class="hidden saveCss btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customCSSEditor" style="height:300px">' . htmlentities($this->config['customCss']) . '</div>'
+				),
+				array(
+					'type' => 'textbox',
+					'name' => 'customCss',
+					'class' => 'hidden cssTextarea',
+					'label' => '',
+					'value' => $this->config['customCss'],
+					'placeholder' => 'No &lt;style&gt; tags needed',
+					'attr' => 'rows="10"',
+				),
+			),
+			'Theme CSS' => array(
+				array(
+					'type' => 'html',
+					'override' => 12,
+					'label' => 'Theme CSS [Can replace colors from above]',
+					'html' => '<button type="button" class="hidden saveCssTheme btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customThemeCSSEditor" style="height:300px">' . htmlentities($this->config['customThemeCss']) . '</div>'
+				),
+				array(
+					'type' => 'textbox',
+					'name' => 'customThemeCss',
+					'class' => 'hidden cssThemeTextarea',
+					'label' => '',
+					'value' => $this->config['customThemeCss'],
+					'placeholder' => 'No &lt;style&gt; tags needed',
+					'attr' => 'rows="10"',
+				),
+			),
+			'Custom Javascript' => array(
+				array(
+					'type' => 'html',
+					'override' => 12,
+					'label' => 'Custom Javascript',
+					'html' => '<button type="button" class="hidden saveJava btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customJavaEditor" style="height:300px">' . htmlentities($this->config['customJava']) . '</div>'
+				),
+				array(
+					'type' => 'textbox',
+					'name' => 'customJava',
+					'class' => 'hidden javaTextarea',
+					'label' => '',
+					'value' => $this->config['customJava'],
+					'placeholder' => 'No &lt;script&gt; tags needed',
+					'attr' => 'rows="10"',
+				),
+			),
+			'Theme Javascript' => array(
+				array(
+					'type' => 'html',
+					'override' => 12,
+					'label' => 'Theme Javascript',
+					'html' => '<button type="button" class="hidden saveJavaTheme btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customThemeJavaEditor" style="height:300px">' . htmlentities($this->config['customThemeJava']) . '</div>'
+				),
+				array(
+					'type' => 'textbox',
+					'name' => 'customThemeJava',
+					'class' => 'hidden javaThemeTextarea',
+					'label' => '',
+					'value' => $this->config['customThemeJava'],
+					'placeholder' => 'No &lt;script&gt; tags needed',
+					'attr' => 'rows="10"',
+				),
+			),
+		);
+	}
+	
+	public function loadAppearance()
+	{
+		$appearance['logo'] = $this->config['logo'];
+		$appearance['title'] = $this->config['title'];
+		$appearance['useLogo'] = $this->config['useLogo'];
+		$appearance['useLogoLogin'] = $this->config['useLogoLogin'];
+		$appearance['headerColor'] = $this->config['headerColor'];
+		$appearance['headerTextColor'] = $this->config['headerTextColor'];
+		$appearance['sidebarColor'] = $this->config['sidebarColor'];
+		$appearance['headerTextColor'] = $this->config['headerTextColor'];
+		$appearance['sidebarTextColor'] = $this->config['sidebarTextColor'];
+		$appearance['accentColor'] = $this->config['accentColor'];
+		$appearance['accentTextColor'] = $this->config['accentTextColor'];
+		$appearance['buttonColor'] = $this->config['buttonColor'];
+		$appearance['buttonTextColor'] = $this->config['buttonTextColor'];
+		$appearance['buttonTextHoverColor'] = $this->config['buttonTextHoverColor'];
+		$appearance['buttonHoverColor'] = $this->config['buttonHoverColor'];
+		$appearance['loginWallpaper'] = $this->config['loginWallpaper'];
+		$appearance['loginLogo'] = $this->config['loginLogo'];
+		$appearance['customCss'] = $this->config['customCss'];
+		$appearance['customThemeCss'] = $this->config['customThemeCss'];
+		$appearance['customJava'] = $this->config['customJava'];
+		$appearance['customThemeJava'] = $this->config['customThemeJava'];
+		return $appearance;
+	}
+	
+	public function getSettingsMain()
+	{
+		return array(
+			'Github' => array(
+				array(
+					'type' => 'select',
+					'name' => 'branch',
+					'label' => 'Branch',
+					'value' => $this->config['branch'],
+					'options' => $this->getBranches(),
+					'disabled' => $this->docker,
+					'help' => ($this->docker) ? 'Since you are using the Official Docker image, Change the image to change the branch' : 'Choose which branch to download from'
+				),
+				array(
+					'type' => 'button',
+					'name' => 'force-install-branch',
+					'label' => 'Force Install Branch',
+					'class' => 'updateNow',
+					'icon' => 'fa fa-download',
+					'text' => 'Retrieve',
+					'attr' => ($this->docker) ? 'title="You can just restart your docker to update"' : '',
+					'help' => ($this->docker) ? 'Since you are using the official Docker image, you can just restart your Docker container to update Organizr' : 'This will re-download all of the source files for Organizr'
+				)
+			),
+			'API' => array(
+				array(
+					'type' => 'password-alt',
+					'name' => 'organizrAPI',
+					'label' => 'Organizr API',
+					'value' => $this->config['organizrAPI']
+				),
+				array(
+					'type' => 'button',
+					'label' => 'Generate New API Key',
+					'class' => 'newAPIKey',
+					'icon' => 'fa fa-refresh',
+					'text' => 'Generate'
+				)
+			),
+			'Authentication' => array(
+				array(
+					'type' => 'select',
+					'name' => 'authType',
+					'id' => 'authSelect',
+					'label' => 'Authentication Type',
+					'value' => $this->config['authType'],
+					'options' => $this->getAuthTypes()
+				),
+				array(
+					'type' => 'select',
+					'name' => 'authBackend',
+					'id' => 'authBackendSelect',
+					'label' => 'Authentication Backend',
+					'class' => 'backendAuth switchAuth',
+					'value' => $this->config['authBackend'],
+					'options' => $this->getAuthBackends()
+				),
+				array(
+					'type' => 'password-alt',
+					'name' => 'plexToken',
+					'class' => 'plexAuth switchAuth',
+					'label' => 'Plex Token',
+					'value' => $this->config['plexToken'],
+					'placeholder' => 'Use Get Token Button'
+				),
+				array(
+					'type' => 'button',
+					'label' => 'Get Plex Token',
+					'class' => 'getPlexTokenAuth plexAuth switchAuth',
+					'icon' => 'fa fa-ticket',
+					'text' => 'Retrieve',
+					'attr' => 'onclick="showPlexTokenForm(\'#settings-main-form [name=plexToken]\')"'
+				),
+				array(
+					'type' => 'password-alt',
+					'name' => 'plexID',
+					'class' => 'plexAuth switchAuth',
+					'label' => 'Plex Machine',
+					'value' => $this->config['plexID'],
+					'placeholder' => 'Use Get Plex Machine Button'
+				),
+				array(
+					'type' => 'button',
+					'label' => 'Get Plex Machine',
+					'class' => 'getPlexMachineAuth plexAuth switchAuth',
+					'icon' => 'fa fa-id-badge',
+					'text' => 'Retrieve',
+					'attr' => 'onclick="showPlexMachineForm(\'#settings-main-form [name=plexID]\')"'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'plexAdmin',
+					'label' => 'Plex Admin Username',
+					'class' => 'plexAuth switchAuth',
+					'value' => $this->config['plexAdmin'],
+					'placeholder' => 'Admin username for Plex'
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'plexoAuth',
+					'label' => 'Enable Plex oAuth',
+					'class' => 'plexAuth switchAuth',
+					'value' => $this->config['plexoAuth']
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'plexStrictFriends',
+					'label' => 'Strict Plex Friends ',
+					'class' => 'plexAuth switchAuth',
+					'value' => $this->config['plexStrictFriends'],
+					'help' => 'Enabling this will only allow Friends that have shares to the Machine ID entered above to login, Having this disabled will allow all Friends on your Friends list to login'
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'ignoreTFALocal',
+					'label' => 'Ignore External 2FA on Local Subnet',
+					'value' => $this->config['ignoreTFALocal'],
+					'help' => 'Enabling this will bypass external 2FA security if user is on local Subnet'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'authBackendHost',
+					'class' => 'ldapAuth ftpAuth switchAuth',
+					'label' => 'Host Address',
+					'value' => $this->config['authBackendHost'],
+					'placeholder' => 'http{s) | ftp(s) | ldap(s)://hostname:port'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'authBaseDN',
+					'class' => 'ldapAuth switchAuth',
+					'label' => 'Host Base DN',
+					'value' => $this->config['authBaseDN'],
+					'placeholder' => 'cn=%s,dc=sub,dc=domain,dc=com'
+				),
+				array(
+					'type' => 'select',
+					'name' => 'ldapType',
+					'id' => 'ldapType',
+					'label' => 'LDAP Backend Type',
+					'class' => 'ldapAuth switchAuth',
+					'value' => $this->config['ldapType'],
+					'options' => $this->getLDAPOptions()
+				),
+				array(
+					'type' => 'input',
+					'name' => 'authBackendHostPrefix',
+					'class' => 'ldapAuth switchAuth',
+					'label' => 'Account Prefix',
+					'id' => 'authBackendHostPrefix-input',
+					'value' => $this->config['authBackendHostPrefix'],
+					'placeholder' => 'Account prefix - i.e. Controller\ from Controller\Username for AD - uid= for OpenLDAP'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'authBackendHostSuffix',
+					'class' => 'ldapAuth switchAuth',
+					'label' => 'Account Suffix',
+					'id' => 'authBackendHostSuffix-input',
+					'value' => $this->config['authBackendHostSuffix'],
+					'placeholder' => 'Account suffix - start with comma - ,ou=people,dc=domain,dc=tld'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'ldapBindUsername',
+					'class' => 'ldapAuth switchAuth',
+					'label' => 'Bind Username',
+					'value' => $this->config['ldapBindUsername'],
+					'placeholder' => ''
+				),
+				array(
+					'type' => 'password',
+					'name' => 'ldapBindPassword',
+					'class' => 'ldapAuth switchAuth',
+					'label' => 'Password',
+					'value' => $this->config['ldapBindPassword']
+				),
+				array(
+					'type' => 'html',
+					'class' => 'ldapAuth switchAuth',
+					'label' => 'Account DN',
+					'html' => '<span id="accountDN" class="ldapAuth switchAuth">' . $this->config['authBackendHostPrefix'] . 'TestAcct' . $this->config['authBackendHostSuffix'] . '</span>'
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'ldapSSL',
+					'class' => 'ldapAuth switchAuth',
+					'label' => 'Enable LDAP SSL',
+					'value' => $this->config['ldapSSL'],
+					'help' => 'This will enable the use of SSL for LDAP connections'
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'ldapSSL',
+					'class' => 'ldapAuth switchAuth',
+					'label' => 'Enable LDAP TLS',
+					'value' => $this->config['ldapTLS'],
+					'help' => 'This will enable the use of TLS for LDAP connections'
+				),
+				array(
+					'type' => 'button',
+					'name' => 'test-button-ldap',
+					'label' => 'Test Connection',
+					'icon' => 'fa fa-flask',
+					'class' => 'ldapAuth switchAuth',
+					'text' => 'Test Connection',
+					'attr' => 'onclick="testAPIConnection(\'ldap\')"',
+					'help' => 'Remember! Please save before using the test button!'
+				),
+				array(
+					'type' => 'button',
+					'name' => 'test-button-ldap-login',
+					'label' => 'Test Login',
+					'icon' => 'fa fa-flask',
+					'class' => 'ldapAuth switchAuth',
+					'text' => 'Test Login',
+					'attr' => 'onclick="showLDAPLoginTest()"'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'embyURL',
+					'class' => 'embyAuth switchAuth',
+					'label' => 'Emby URL',
+					'value' => $this->config['embyURL'],
+					'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+					'placeholder' => 'http(s)://hostname:port'
+				),
+				array(
+					'type' => 'password-alt',
+					'name' => 'embyToken',
+					'class' => 'embyAuth switchAuth',
+					'label' => 'Emby Token',
+					'value' => $this->config['embyToken'],
+					'placeholder' => ''
+				),
+				array(
+					'type' => 'input',
+					'name' => 'jellyfinURL',
+					'class' => 'jellyfinAuth switchAuth',
+					'label' => 'Jellyfin URL',
+					'value' => $this->config['jellyfinURL'],
+					'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+					'placeholder' => 'http(s)://hostname:port'
+				),
+				array(
+					'type' => 'password-alt',
+					'name' => 'jellyfinToken',
+					'class' => 'jellyfinAuth switchAuth',
+					'label' => 'Jellyfin Token',
+					'value' => $this->config['jellyfinToken'],
+					'placeholder' => ''
+				),
+			),
+			'Security' => array(
+				array(
+					'type' => 'number',
+					'name' => 'loginAttempts',
+					'label' => 'Max Login Attempts',
+					'value' => $this->config['loginAttempts'],
+					'placeholder' => ''
+				),
+				array(
+					'type' => 'select',
+					'name' => 'loginLockout',
+					'label' => 'Login Lockout Seconds',
+					'value' => $this->config['loginLockout'],
+					'options' => $this->timeOptions()
+				),
+				array(
+					'type' => 'number',
+					'name' => 'lockoutTimeout',
+					'label' => 'Inactivity Timer [Minutes]',
+					'value' => $this->config['lockoutTimeout'],
+					'placeholder' => ''
+				),
+				array(
+					'type' => 'select',
+					'name' => 'lockoutMinAuth',
+					'label' => 'Lockout Groups From',
+					'value' => $this->config['lockoutMinAuth'],
+					'options' => $this->groupSelect()
+				),
+				array(
+					'type' => 'select',
+					'name' => 'lockoutMaxAuth',
+					'label' => 'Lockout Groups To',
+					'value' => $this->config['lockoutMaxAuth'],
+					'options' => $this->groupSelect()
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'lockoutSystem',
+					'label' => 'Inactivity Lock',
+					'value' => $this->config['lockoutSystem']
+				),
+				array(
+					'type' => 'select',
+					'name' => 'debugAreaAuth',
+					'label' => 'Minimum Authentication for Debug Area',
+					'value' => $this->config['debugAreaAuth'],
+					'options' => $this->groupSelect()
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'authDebug',
+					'label' => 'Nginx Auth Debug',
+					'help' => 'Important! Do not keep this enabled for too long as this opens up Authentication while testing.',
+					'value' => $this->config['authDebug'],
+					'class' => 'authDebug'
+				),
+				array(
+					'type' => 'select2',
+					'class' => 'select2-multiple',
+					'id' => 'sandbox-select',
+					'name' => 'sandbox',
+					'label' => 'iFrame Sandbox',
+					'value' => $this->config['sandbox'],
+					'help' => 'WARNING! This can potentially mess up your iFrames',
+					'options' => array(
+						array(
+							'name' => 'Allow Presentation',
+							'value' => 'allow-presentation'
+						),
+						array(
+							'name' => 'Allow Forms',
+							'value' => 'allow-forms'
+						),
+						array(
+							'name' => 'Allow Same Origin',
+							'value' => 'allow-same-origin'
+						),
+						array(
+							'name' => 'Allow Pointer Lock',
+							'value' => 'allow-pointer-lock'
+						),
+						array(
+							'name' => 'Allow Scripts',
+							'value' => 'allow-scripts'
+						), array(
+							'name' => 'Allow Popups',
+							'value' => 'allow-popups'
+						),
+						array(
+							'name' => 'Allow Modals',
+							'value' => 'allow-modals'
+						),
+						array(
+							'name' => 'Allow Top Navigation',
+							'value' => 'allow-top-navigation'
+						),
+						array(
+							'name' => 'Allow Downloads',
+							'value' => 'allow-downloads'
+						),
+					)
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'traefikAuthEnable',
+					'label' => 'Enable Traefik Auth Redirect',
+					'help' => 'This will enable the webserver to forward errors so traefik will accept them',
+					'value' => $this->config['traefikAuthEnable']
+				),
+			),
+			'Performance' => array(
+				array(
+					'type' => 'switch',
+					'name' => 'performanceDisableIconDropdown',
+					'label' => 'Disable Icon Dropdown',
+					'help' => 'Disable select dropdown boxes on new and edit tab forms',
+					'value' => $this->config['performanceDisableIconDropdown'],
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'performanceDisableImageDropdown',
+					'label' => 'Disable Image Dropdown',
+					'help' => 'Disable select dropdown boxes on new and edit tab forms',
+					'value' => $this->config['performanceDisableImageDropdown'],
+				),
+			),
+			'Login' => array(
+				array(
+					'type' => 'password-alt',
+					'name' => 'registrationPassword',
+					'label' => 'Registration Password',
+					'help' => 'Sets the password for the Registration form on the login screen',
+					'value' => $this->config['registrationPassword'],
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'hideRegistration',
+					'label' => 'Hide Registration',
+					'help' => 'Enable this to hide the Registration button on the login screen',
+					'value' => $this->config['hideRegistration'],
+				),
+				array(
+					'type' => 'number',
+					'name' => 'rememberMeDays',
+					'label' => 'Remember Me Length',
+					'help' => 'Number of days cookies and tokens will be valid for',
+					'value' => $this->config['rememberMeDays'],
+					'placeholder' => '',
+					'attr' => 'min="1"'
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'rememberMe',
+					'label' => 'Remember Me',
+					'help' => 'Default status of Remember Me button on login screen',
+					'value' => $this->config['rememberMe'],
+				),
+				array(
+					'type' => 'input',
+					'name' => 'localIPFrom',
+					'label' => 'Override Local IP From',
+					'value' => $this->config['localIPFrom'],
+					'placeholder' => 'i.e. 123.123.123.123',
+					'help' => 'IPv4 only at the moment - This will set your login as local if your IP falls within the From and To'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'localIPTo',
+					'label' => 'Override Local IP To',
+					'value' => $this->config['localIPTo'],
+					'placeholder' => 'i.e. 123.123.123.123',
+					'help' => 'IPv4 only at the moment - This will set your login as local if your IP falls within the From and To'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'wanDomain',
+					'label' => 'WAN Domain',
+					'value' => $this->config['wanDomain'],
+					'placeholder' => 'only domain and tld - i.e. domain.com',
+					'help' => 'Enter domain if you wish to be forwarded to a local address - Local Address filled out on next item'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'localAddress',
+					'label' => 'Local Address',
+					'value' => $this->config['localAddress'],
+					'placeholder' => 'http://home.local',
+					'help' => 'Full local address of organizr install - i.e. http://home.local or http://192.168.0.100'
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'enableLocalAddressForward',
+					'label' => 'Enable Local Address Forward',
+					'help' => 'Enables the local address forward if on local address and accessed from WAN Domain',
+					'value' => $this->config['enableLocalAddressForward'],
+				),
+			),
+			'Auth Proxy' => array(
+				array(
+					'type' => 'switch',
+					'name' => 'authProxyEnabled',
+					'label' => 'Auth Proxy',
+					'help' => 'Enable option to set Auth Proxy Header Login',
+					'value' => $this->config['authProxyEnabled'],
+				),
+				array(
+					'type' => 'input',
+					'name' => 'authProxyHeaderName',
+					'label' => 'Auth Proxy Header Name',
+					'value' => $this->config['authProxyHeaderName'],
+					'placeholder' => 'i.e. X-Forwarded-User',
+					'help' => 'Please choose a unique value for added security'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'authProxyWhitelist',
+					'label' => 'Auth Proxy Whitelist',
+					'value' => $this->config['authProxyWhitelist'],
+					'placeholder' => 'i.e. 10.0.0.0/24 or 10.0.0.20',
+					'help' => 'IPv4 only at the moment - This must be set to work, will accept subnet or IP address'
+				),
+			),
+			'Ping' => array(
+				array(
+					'type' => 'select',
+					'name' => 'pingAuth',
+					'label' => 'Minimum Authentication',
+					'value' => $this->config['pingAuth'],
+					'options' => $this->groupSelect()
+				),
+				array(
+					'type' => 'select',
+					'name' => 'pingAuthMessage',
+					'label' => 'Minimum Authentication for Message and Sound',
+					'value' => $this->config['pingAuthMessage'],
+					'options' => $this->groupSelect()
+				),
+				array(
+					'type' => 'select',
+					'name' => 'pingOnlineSound',
+					'label' => 'Online Sound',
+					'value' => $this->config['pingOnlineSound'],
+					'options' => $this->getSounds()
+				),
+				array(
+					'type' => 'select',
+					'name' => 'pingOfflineSound',
+					'label' => 'Offline Sound',
+					'value' => $this->config['pingOfflineSound'],
+					'options' => $this->getSounds()
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'pingMs',
+					'label' => 'Show Ping Time',
+					'value' => $this->config['pingMs']
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'statusSounds',
+					'label' => 'Enable Notify Sounds',
+					'value' => $this->config['statusSounds'],
+					'help' => 'Will play a sound if the server goes down and will play sound if comes back up.',
+				),
+				array(
+					'type' => 'select',
+					'name' => 'pingAuthMs',
+					'label' => 'Minimum Authentication for Time Display',
+					'value' => $this->config['pingAuthMs'],
+					'options' => $this->groupSelect()
+				),
+				array(
+					'type' => 'select',
+					'name' => 'adminPingRefresh',
+					'label' => 'Admin Refresh Seconds',
+					'value' => $this->config['adminPingRefresh'],
+					'options' => $this->timeOptions()
+				),
+				array(
+					'type' => 'select',
+					'name' => 'otherPingRefresh',
+					'label' => 'Everyone Refresh Seconds',
+					'value' => $this->config['otherPingRefresh'],
+					'options' => $this->timeOptions()
+				),
+			)
+		);
+	}
+	
+	public function getSettingsSSO()
+	{
+		return array(
+			'FYI' => array(
+				array(
+					'type' => 'html',
+					'label' => 'Important Information',
+					'override' => 12,
+					'html' => '
+				<div class="row">
+						    <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">
+						                    <span lang="en">This is not the same as database authentication - i.e. Plex Authentication | Emby Authentication | FTP Authentication<br/>Click Main on the sub-menu above.</span>
+						                </div>
+						            </div>
+						        </div>
+						    </div>
+						</div>
+				'
+				)
+			),
+			'Plex' => array(
+				array(
+					'type' => 'password-alt',
+					'name' => 'plexToken',
+					'label' => 'Plex Token',
+					'value' => $this->config['plexToken'],
+					'placeholder' => 'Use Get Token Button'
+				),
+				array(
+					'type' => 'button',
+					'label' => 'Get Plex Token',
+					'icon' => 'fa fa-ticket',
+					'text' => 'Retrieve',
+					'attr' => 'onclick="showPlexTokenForm(\'#sso-form [name=plexToken]\')"'
+				),
+				array(
+					'type' => 'password-alt',
+					'name' => 'plexID',
+					'label' => 'Plex Machine',
+					'value' => $this->config['plexID'],
+					'placeholder' => 'Use Get Plex Machine Button'
+				),
+				array(
+					'type' => 'button',
+					'label' => 'Get Plex Machine',
+					'icon' => 'fa fa-id-badge',
+					'text' => 'Retrieve',
+					'attr' => 'onclick="showPlexMachineForm(\'#sso-form [name=plexID]\')"'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'plexAdmin',
+					'label' => 'Admin Username',
+					'value' => $this->config['plexAdmin'],
+					'placeholder' => 'Admin username for Plex'
+				),
+				array(
+					'type' => 'blank',
+					'label' => ''
+				),
+				array(
+					'type' => 'html',
+					'label' => 'Plex Note',
+					'html' => '<span lang="en">Please make sure both Token and Machine are filled in</span>'
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'ssoPlex',
+					'label' => 'Enable',
+					'value' => $this->config['ssoPlex']
+				)
+			),
+			'Tautulli' => array(
+				array(
+					'type' => 'input',
+					'name' => 'tautulliURL',
+					'label' => 'Tautulli URL',
+					'value' => $this->config['tautulliURL'],
+					'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+					'placeholder' => 'http(s)://hostname:port'
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'ssoTautulli',
+					'label' => 'Enable',
+					'value' => $this->config['ssoTautulli']
+				)
+			),
+			'Ombi' => array(
+				array(
+					'type' => 'input',
+					'name' => 'ombiURL',
+					'label' => 'Ombi URL',
+					'value' => $this->config['ombiURL'],
+					'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+					'placeholder' => 'http(s)://hostname:port'
+				),
+				array(
+					'type' => 'password-alt',
+					'name' => 'ombiToken',
+					'label' => 'Token',
+					'value' => $this->config['ombiToken']
+				),
+				array(
+					'type' => 'input',
+					'name' => 'ombiFallbackUser',
+					'label' => 'Ombi Fallback User',
+					'value' => $this->config['ombiFallbackUser'],
+					'help' => 'Organizr will request an Ombi User Token based off of this user credentials'
+				),
+				array(
+					'type' => 'password-alt',
+					'name' => 'ombiFallbackPassword',
+					'label' => 'Ombi Fallback Password',
+					'value' => $this->config['ombiFallbackPassword']
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'ssoOmbi',
+					'label' => 'Enable',
+					'value' => $this->config['ssoOmbi']
+				)
+			)
+		);
+	}
+	
+	public function updateConfigMultiple($array)
+	{
+		return ($this->updateConfig($array)) ? true : false;
+	}
+	
+	public function updateConfigItems($array)
+	{
+		if (!count($array)) {
+			$this->setAPIResponse('error', 'No data submitted', 409);
+			return false;
+		}
+		$newItem = array();
+		foreach ($array as $k => $v) {
+			$v = $v ?? '';
+			switch ($v) {
+				case 'true':
+					$v = (bool)true;
+					break;
+				case 'false':
+					$v = (bool)false;
+					break;
+			}
+			// Hash
+			if ((stripos($k, 'password') !== false)) {
+				if (!$this->isEncrypted($v)) {
+					if ($v !== '') {
+						$v = $this->encrypt($v);
+					}
+				}
+			}
+			if (strtolower($k) !== 'formkey') {
+				$newItem[$k] = $v;
+			}
+		}
+		$this->setAPIResponse('success', 'Config items updated', 200);
+		return ($this->updateConfig($newItem)) ? true : false;
+	}
+	
+	public function updateConfigItem($array)
+	{
+		$array['value'] = $array['value'] ?? '';
+		switch ($array['value']) {
+			case 'true':
+				$array['value'] = (bool)true;
+				break;
+			case 'false':
+				$array['value'] = (bool)false;
+				break;
+		}
+		// Hash
+		if ($array['type'] == 'password') {
+			$array['value'] = $this->encrypt($array['value']);
+		}
+		$newItem = array(
+			$array['name'] => $array['value']
+		);
+		return ($this->updateConfig($newItem)) ? true : false;
+	}
+	
+	public function testWizardPath($array)
+	{
+		if ($this->hasDB()) {
+			$this->setAPIResponse('error', 'Endpoint disabled as database already exists', 401);
+			return false;
+		}
+		$path = $array['path'] ?? null;
+		if (file_exists($path)) {
+			if (is_writable($path)) {
+				$this->setAPIResponse('success', 'Path exists and is writable', 200);
+				return true;
+			}
+		} else {
+			if (is_writable(dirname($path, 1))) {
+				if (mkdir($path, 0760, true)) {
+					$this->setAPIResponse('success', 'Path is writable - Creating now', 200);
+					return true;
+				}
+			}
+		}
+		$this->setAPIResponse('error', 'Path is not writable', 401);
+		return false;
+	}
+	
+	public function wizardConfig($array)
+	{
+		$dbName = $array['dbName'] ?? null;
+		$path = $array['dbPath'] ?? null;
+		$license = $array['license'] ?? null;
+		$hashKey = $array['hashKey'] ?? null;
+		$api = $array['api'] ?? null;
+		$registrationPassword = $array['registrationPassword'] ?? null;
+		$username = $array['username'] ?? null;
+		$password = $array['password'] ?? null;
+		$email = $array['email'] ?? null;
+		$validation = array(
+			'dbName' => $dbName,
+			'dbPath' => $path,
+			'license' => $license,
+			'hashKey' => $hashKey,
+			'api' => $api,
+			'registrationPassword' => $registrationPassword,
+			'username' => $username,
+			'password' => $password,
+			'email' => $email,
+		);
+		foreach ($validation as $k => $v) {
+			if ($v == null) {
+				$this->setAPIResponse('error', '[' . $k . '] cannot be empty', 422);
+				return false;
+			}
+		}
+		$path = $this->cleanDirectory($path);
+		if (file_exists($path)) {
+			if (!is_writable($path)) {
+				$this->setAPIResponse('error', '[' . $path . ']  is not writable', 422);
+				return false;
+			}
+		} else {
+			if (is_writable(dirname($path, 1))) {
+				if (!mkdir($path, 0760, true)) {
+					$this->setAPIResponse('error', '[' . $path . ']  is not writable', 422);
+					return false;
+				}
+			} else {
+				$this->setAPIResponse('error', '[' . $path . ']  is not writable', 422);
+				return false;
+			}
+		}
+		$dbName = $this->dbExtension($dbName);
+		$configVersion = $this->version;
+		$configArray = array(
+			'dbName' => $dbName,
+			'dbLocation' => $path,
+			'license' => $license,
+			'organizrHash' => $hashKey,
+			'organizrAPI' => $api,
+			'registrationPassword' => $registrationPassword,
+			'uuid' => $this->gen_uuid()
+		);
+		// Create Config
+		if ($this->createConfig($configArray)) {
+			$this->config = $this->config();
+			$this->refreshCookieName();
+			$this->connectDB();
+			// Call DB Create
+			if ($this->createDB($path)) {
+				// Add in first user
+				if ($this->createFirstAdmin($username, $password, $email)) {
+					if ($this->createToken($username, $email, 1)) {
+						return true;
+					} else {
+						$this->setAPIResponse('error', 'error creating token', 500);
+					}
+				} else {
+					$this->setAPIResponse('error', 'error creating admin', 500);
+				}
+			} else {
+				$this->setAPIResponse('error', 'error creating database', 500);
+			}
+			
+		} else {
+			$this->setAPIResponse('error', 'error creating config', 500);
+		}
+		return false;
+	}
+	
+	public function createDB($path, $migration = false)
+	{
+		
+		if (!file_exists($path)) {
+			mkdir($path, 0777, true);
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => 'CREATE TABLE `users` (
+				`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
+				`username`	TEXT UNIQUE,
+				`password`	TEXT,
+				`email`	TEXT,
+				`plex_token`	TEXT,
+				`group`	TEXT,
+				`group_id`	INTEGER,
+				`locked`	INTEGER,
+				`image`	TEXT,
+				`register_date`	DATE,
+				`auth_service`	TEXT DEFAULT \'internal\'
+				);'
+			),
+			array(
+				'function' => 'query',
+				'query' => 'CREATE TABLE `chatroom` (
+			        `id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
+			        `username`	TEXT,
+			        `gravatar`	TEXT,
+			        `uid`	TEXT,
+			        `date` DATE,
+			        `ip` TEXT,
+			        `message` TEXT
+			    );'
+			),
+			array(
+				'function' => 'query',
+				'query' => 'CREATE TABLE `tokens` (
+			        `id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
+			        `token`	TEXT UNIQUE,
+			        `user_id`	INTEGER,
+			        `browser`	TEXT,
+			        `ip`	TEXT,
+			        `created` DATE,
+			        `expires` DATE
+			    );'
+			),
+			array(
+				'function' => 'query',
+				'query' => 'CREATE TABLE `groups` (
+			        `id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
+			        `group`	TEXT UNIQUE,
+			        `group_id`	INTEGER,
+			        `image`	TEXT,
+			        `default` INTEGER
+			    );'
+			),
+			array(
+				'function' => 'query',
+				'query' => 'CREATE TABLE `categories` (
+			        `id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
+			        `order`	INTEGER,
+			        `category`	TEXT UNIQUE,
+			        `category_id`	INTEGER,
+			        `image`	TEXT,
+			        `default` INTEGER
+			    );'
+			),
+			array(
+				'function' => 'query',
+				'query' => 'CREATE TABLE `tabs` (
+			        `id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
+			        `order`	INTEGER,
+			        `category_id`	INTEGER,
+			        `name`	TEXT,
+			        `url`	TEXT,
+			        `url_local`	TEXT,
+			        `default`	INTEGER,
+			        `enabled`	INTEGER,
+			        `group_id`	INTEGER,
+			        `image`	TEXT,
+			        `type`	INTEGER,
+			        `splash`	INTEGER,
+			        `ping`		INTEGER,
+			        `ping_url`	TEXT,
+			        `timeout`	INTEGER,
+			        `timeout_ms`	INTEGER,
+			        `preload`	INTEGER
+			    );'
+			),
+			array(
+				'function' => 'query',
+				'query' => 'CREATE TABLE `options` (
+			        `id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
+			        `name`	TEXT UNIQUE,
+			        `value`	TEXT
+			    );'
+			),
+			array(
+				'function' => 'query',
+				'query' => 'CREATE TABLE `invites` (
+			        `id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
+			        `code`	TEXT UNIQUE,
+			        `date`	TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+			        `email`	TEXT,
+			        `username`	TEXT,
+			        `dateused`	TIMESTAMP,
+			        `usedby`	TEXT,
+			        `ip`	TEXT,
+			        `valid`	TEXT,
+			        `type` TEXT
+			    );'
+			),
+		];
+		return $this->processQueries($response, $migration);
+	}
+	
+	public function createFirstAdmin($username, $password, $email)
+	{
+		
+		$userInfo = [
+			'username' => $username,
+			'password' => password_hash($password, PASSWORD_BCRYPT),
+			'email' => $email,
+			'group' => 'Admin',
+			'group_id' => 0,
+			'image' => $this->gravatar($email),
+			'register_date' => $this->currentTime,
+		];
+		$groupInfo0 = [
+			'group' => 'Admin',
+			'group_id' => 0,
+			'default' => false,
+			'image' => 'plugins/images/groups/admin.png',
+		];
+		$groupInfo1 = [
+			'group' => 'Co-Admin',
+			'group_id' => 1,
+			'default' => false,
+			'image' => 'plugins/images/groups/coadmin.png',
+		];
+		$groupInfo2 = [
+			'group' => 'Super User',
+			'group_id' => 2,
+			'default' => false,
+			'image' => 'plugins/images/groups/superuser.png',
+		];
+		$groupInfo3 = [
+			'group' => 'Power User',
+			'group_id' => 3,
+			'default' => false,
+			'image' => 'plugins/images/groups/poweruser.png',
+		];
+		$groupInfo4 = [
+			'group' => 'User',
+			'group_id' => 4,
+			'default' => true,
+			'image' => 'plugins/images/groups/user.png',
+		];
+		$groupInfoGuest = [
+			'group' => 'Guest',
+			'group_id' => 999,
+			'default' => false,
+			'image' => 'plugins/images/groups/guest.png',
+		];
+		$settingsInfo = [
+			'order' => 1,
+			'category_id' => 0,
+			'name' => 'Settings',
+			'url' => 'api/v2/page/settings',
+			'default' => false,
+			'enabled' => true,
+			'group_id' => 1,
+			'image' => 'fontawesome::cog',
+			'type' => 0
+		];
+		$homepageInfo = [
+			'order' => 2,
+			'category_id' => 0,
+			'name' => 'Homepage',
+			'url' => 'api/v2/page/homepage',
+			'default' => false,
+			'enabled' => false,
+			'group_id' => 4,
+			'image' => 'fontawesome::home',
+			'type' => 0
+		];
+		$unsortedInfo = [
+			'order' => 1,
+			'category' => 'Unsorted',
+			'category_id' => 0,
+			'image' => 'plugins/images/categories/unsorted.png',
+			'default' => true
+		];
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [users]',
+					$userInfo
+				)
+			),
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [groups]',
+					$groupInfo0
+				)
+			),
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [groups]',
+					$groupInfo1
+				)
+			),
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [groups]',
+					$groupInfo2
+				)
+			),
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [groups]',
+					$groupInfo3
+				)
+			),
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [groups]',
+					$groupInfo4
+				)
+			),
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [groups]',
+					$groupInfoGuest
+				)
+			),
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [tabs]',
+					$settingsInfo
+				)
+			),
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [tabs]',
+					$homepageInfo
+				)
+			),
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [categories]',
+					$unsortedInfo
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getUserByUsernameAndEmail($username, $email)
+	{
+		$response = [
+			array(
+				'function' => 'fetch',
+				'query' => array(
+					'SELECT * FROM users WHERE username = ? COLLATE NOCASE OR email = ? COLLATE NOCASE',
+					[$username],
+					[$email]
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function createToken($username, $email, $days = 1)
+	{
+		$days = ($days > 365) ? 365 : $days;
+		//Quick get user ID
+		$result = $this->getUserByUsernameAndEmail($username, $email);
+		// Create JWT
+		// Set key
+		// SHA256 Encryption
+		$signer = new Lcobucci\JWT\Signer\Hmac\Sha256();
+		// Start Builder
+		$jwttoken = (new Lcobucci\JWT\Builder())->issuedBy('Organizr')// Configures the issuer (iss claim)
+		->permittedFor('Organizr')// Configures the audience (aud claim)
+		->identifiedBy('4f1g23a12aa', true)// Configures the id (jti claim), replicating as a header item
+		->issuedAt(time())// Configures the time that the token was issue (iat claim)
+		->expiresAt(time() + (86400 * $days))// Configures the expiration time of the token (exp claim)
+		->withClaim('username', $result['username'])// Configures a new claim, called "username"
+		->withClaim('group', $result['group'])// Configures a new claim, called "group"
+		->withClaim('groupID', $result['group_id'])// Configures a new claim, called "groupID"
+		->withClaim('email', $result['email'])// Configures a new claim, called "email"
+		->withClaim('image', $result['image'])// Configures a new claim, called "image"
+		->withClaim('userID', $result['id'])// Configures a new claim, called "image"
+		->sign($signer, $this->config['organizrHash'])// creates a signature using "testing" as key
+		->getToken(); // Retrieves the generated token
+		$jwttoken->getHeaders(); // Retrieves the token headers
+		$jwttoken->getClaims(); // Retrieves the token claims
+		$this->coookie('set', $this->cookieName, $jwttoken, $days);
+		// Add token to DB
+		$addToken = [
+			'token' => (string)$jwttoken,
+			'user_id' => $result['id'],
+			'created' => $this->currentTime,
+			'browser' => isset($_SERVER ['HTTP_USER_AGENT']) ? $_SERVER ['HTTP_USER_AGENT'] : null,
+			'ip' => $this->userIP(),
+			'expires' => gmdate("Y-m-d\TH:i:s\Z", time() + (86400 * $days))
+		];
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [tokens]',
+					$addToken
+				)
+			),
+		];
+		$token = $this->processQueries($response);
+		return $jwttoken;
+		
+	}
+	
+	public function login($array)
+	{
+		// Grab username, Password & other optional items from api call
+		$username = $array['username'] ?? null;
+		$password = $array['password'] ?? null;
+		$oAuth = $array['oAuth'] ?? null;
+		$oAuthType = $array['oAuthType'] ?? null;
+		$remember = $array['remember'] ?? null;
+		$tfaCode = $array['tfaCode'] ?? null;
+		$loginAttempts = $array['loginAttempts'] ?? null;
+		$output = $array['output'] ?? null;
+		$username = (strpos($this->config['authBackend'], 'emby') !== false) ? $username : strtolower($username);
+		$days = (isset($remember)) ? $this->config['rememberMeDays'] : 1;
+		// Set  other variables
+		$function = 'plugin_auth_' . $this->config['authBackend'];
+		$authSuccess = false;
+		$authProxy = false;
+		// Check Login attempts and kill if over limit
+		if ($loginAttempts > $this->config['loginAttempts'] || isset($_COOKIE['lockout'])) {
+			$this->coookieSeconds('set', 'lockout', $this->config['loginLockout'], $this->config['loginLockout']);
+			$this->setAPIResponse('error', 'User is locked out', 403);
+			return false;
+		}
+		// Check if Auth Proxy is enabled
+		if ($this->config['authProxyEnabled'] && $this->config['authProxyHeaderName'] !== '' && $this->config['authProxyWhitelist'] !== '') {
+			if (isset($this->getallheaders()[$this->config['authProxyHeaderName']])) {
+				$usernameHeader = isset($this->getallheaders()[$this->config['authProxyHeaderName']]) ? $this->getallheaders()[$this->config['authProxyHeaderName']] : $username;
+				$this->writeLog('success', 'Auth Proxy Function - Starting Verification for IP: ' . $this->userIP() . ' for request on: ' . $_SERVER['REMOTE_ADDR'] . ' against IP/Subnet: ' . $this->config['authProxyWhitelist'], $usernameHeader);
+				$whitelistRange = $this->analyzeIP($this->config['authProxyWhitelist']);
+				$authProxy = $this->authProxyRangeCheck($whitelistRange['from'], $whitelistRange['to']);
+				$username = ($authProxy) ? $usernameHeader : $username;
+				if ($authProxy) {
+					$this->writeLog('success', 'Auth Proxy Function - IP: ' . $this->userIP() . ' has been verified', $usernameHeader);
+				} else {
+					$this->writeLog('error', 'Auth Proxy Function - IP: ' . $this->userIP() . ' has failed verification', $usernameHeader);
+				}
+			}
+		}
+		// Check if Login method was an oAuth login
+		if (!$oAuth) {
+			$result = $this->getUserByUsernameAndEmail($username, $username);
+			$result['password'] = $result['password'] ?? '';
+			// Switch AuthType - internal - external - both
+			switch ($this->config['authType']) {
+				case 'external':
+					if (method_exists($this, $function)) {
+						$authSuccess = $this->$function($username, $password);
+					}
+					break;
+				/** @noinspection PhpMissingBreakStatementInspection */
+				case 'both':
+					if (method_exists($this, $function)) {
+						$authSuccess = $this->$function($username, $password);
+					}
+				// no break
+				default: // Internal
+					if (!$authSuccess) {
+						// perform the internal authentication step
+						if (password_verify($password, $result['password'])) {
+							$authSuccess = true;
+						}
+					}
+			}
+			$authSuccess = ($authProxy) ? true : $authSuccess;
+		} else {
+			// Has oAuth Token!
+			switch ($oAuthType) {
+				case 'plex':
+					if ($this->config['plexoAuth']) {
+						$tokenInfo = $this->checkPlexToken($oAuth);
+						if ($tokenInfo) {
+							$authSuccess = array(
+								'username' => $tokenInfo['user']['username'],
+								'email' => $tokenInfo['user']['email'],
+								'image' => $tokenInfo['user']['thumb'],
+								'token' => $tokenInfo['user']['authToken']
+							);
+							$this->coookie('set', 'oAuth', 'true', $this->config['rememberMeDays']);
+							$authSuccess = ((!empty($this->config['plexAdmin']) && strtolower($this->config['plexAdmin']) == strtolower($tokenInfo['user']['username'])) || (!empty($this->config['plexAdmin']) && strtolower($this->config['plexAdmin']) == strtolower($tokenInfo['user']['email'])) || $this->checkPlexUser($tokenInfo['user']['username'])) ? $authSuccess : false;
+						}
+					} else {
+						$this->setAPIResponse('error', 'Plex oAuth is not setup', 422);
+						return false;
+					}
+					break;
+				default:
+					return ($output) ? 'No oAuthType defined' : 'error';
+			}
+			$result = ($authSuccess) ? $this->getUserByUsernameAndEmail($authSuccess['username'], $authSuccess['email']) : '';
+		}
+		if ($authSuccess) {
+			// Make sure user exists in database
+			$userExists = false;
+			$passwordMatches = $oAuth || $authProxy;
+			$token = (is_array($authSuccess) && isset($authSuccess['token']) ? $authSuccess['token'] : '');
+			if (isset($result['username'])) {
+				$userExists = true;
+				$username = $result['username'];
+				if ($passwordMatches == false) {
+					$passwordMatches = password_verify($password, $result['password']);
+				}
+			}
+			if ($userExists) {
+				//does org password need to be updated
+				if (!$passwordMatches) {
+					$this->updateUserPassword($password, $result['id']);
+					$this->writeLog('success', 'Login Function - User Password updated from backend', $username);
+				}
+				if ($token !== '') {
+					if ($token !== $result['plex_token']) {
+						$this->updateUserPlexToken($token, $result['id']);
+						$this->writeLog('success', 'Login Function - User Plex Token updated from backend', $username);
+					}
+				}
+				// 2FA might go here
+				if ($result['auth_service'] !== 'internal' && strpos($result['auth_service'], '::') !== false) {
+					$tfaProceed = true;
+					// Add check for local or not
+					if ($this->config['ignoreTFALocal'] !== false) {
+						$tfaProceed = ($this->isLocal()) ? false : true;
+					}
+					if ($tfaProceed) {
+						$TFA = explode('::', $result['auth_service']);
+						// Is code with login info?
+						if ($tfaCode == '') {
+							$this->setAPIResponse('warning', '2FA Code Needed', 422);
+							return false;
+						} else {
+							if (!$this->verify2FA($TFA[1], $tfaCode, $TFA[0])) {
+								$this->writeLoginLog($username, 'error');
+								$this->writeLog('error', 'Login Function - Wrong 2FA', $username);
+								$this->setAPIResponse('error', 'Wrong 2FA', 422);
+								return false;
+							}
+						}
+					}
+				}
+				// End 2FA
+				// authentication passed - 1) mark active and update token
+				$createToken = $this->createToken($result['username'], $result['email'], $days);
+				if ($createToken) {
+					$this->writeLoginLog($username, 'success');
+					$this->writeLog('success', 'Login Function - A User has logged in', $username);
+					$ssoUser = ((empty($result['email'])) ? $result['username'] : (strpos($result['email'], 'placeholder') !== false)) ? $result['username'] : $result['email'];
+					$this->ssoCheck($ssoUser, $password, $token); //need to work on this
+					return ($output) ? array('name' => $this->cookieName, 'token' => (string)$createToken) : true;
+				} else {
+					$this->setAPIResponse('error', 'Token creation error', 500);
+					return false;
+				}
+			} else {
+				// Create User
+				return $this->authRegister((is_array($authSuccess) && isset($authSuccess['username']) ? $authSuccess['username'] : $username), $password, (is_array($authSuccess) && isset($authSuccess['email']) ? $authSuccess['email'] : ''), $token);
+			}
+		} else {
+			// authentication failed
+			$this->writeLoginLog($username, 'error');
+			$this->writeLog('error', 'Login Function - Wrong Password', $username);
+			if ($loginAttempts >= $this->config['loginAttempts']) {
+				$this->coookieSeconds('set', 'lockout', $this->config['loginLockout'], $this->config['loginLockout']);
+				$this->setAPIResponse('error', 'User is locked out', 403);
+				return false;
+			} else {
+				$this->setAPIResponse('error', 'User credentials incorrect', 401);
+				return false;
+			}
+		}
+	}
+	
+	public function logout()
+	{
+		$this->coookie('delete', $this->cookieName);
+		$this->coookie('delete', 'mpt');
+		$this->coookie('delete', 'Auth');
+		$this->coookie('delete', 'oAuth');
+		$this->clearTautulliTokens();
+		$this->revokeTokenCurrentUser($this->user['token']);
+		$this->user = null;
+		return true;
+	}
+	
+	public function recover($array)
+	{
+		$email = $array['email'] ?? null;
+		if (!$email) {
+			$this->setAPIResponse('error', 'Email was not supplied', 422);
+			return false;
+		}
+		$newPassword = $this->randString(10);
+		$isUser = $this->getUserByEmail($email);
+		if ($isUser) {
+			$this->updateUserPassword($newPassword, $isUser['id']);
+			$this->setAPIResponse('success', 'User password has been reset', 200);
+			$this->writeLog('success', 'User Management Function - User: ' . $isUser['username'] . '\'s password was reset', $isUser['username']);
+			if ($this->config['PHPMAILER-enabled']) {
+				$PhpMailer = new PhpMailer();
+				$emailTemplate = array(
+					'type' => 'reset',
+					'body' => $this->config['PHPMAILER-emailTemplateReset'],
+					'subject' => $this->config['PHPMAILER-emailTemplateResetSubject'],
+					'user' => $isUser['username'],
+					'password' => $newPassword,
+					'inviteCode' => null,
+				);
+				$emailTemplate = $PhpMailer->_phpMailerPluginEmailTemplate($emailTemplate);
+				$sendEmail = array(
+					'to' => $email,
+					'user' => $isUser['username'],
+					'subject' => $emailTemplate['subject'],
+					'body' => $PhpMailer->_phpMailerPluginBuildEmail($emailTemplate),
+				);
+				$PhpMailer->_phpMailerPluginSendEmail($sendEmail);
+				$this->setAPIResponse('success', 'User password has been reset and email has been sent', 200);
+			}
+			return true;
+		} else {
+			$this->setAPIResponse('error', 'User not found', 404);
+			return false;
+		}
+	}
+	
+	public function register($array)
+	{
+		$email = $array['email'] ?? null;
+		$username = $array['username'] ?? null;
+		$password = $array['password'] ?? null;
+		$registrationPassword = $array['registrationPassword'] ?? null;
+		if (!$email) {
+			$this->setAPIResponse('error', 'Email was not supplied', 422);
+			return false;
+		}
+		if (!$username) {
+			$this->setAPIResponse('error', 'Username was not supplied', 422);
+			return false;
+		}
+		if (!$password) {
+			$this->setAPIResponse('error', 'Password was not supplied', 422);
+			return false;
+		}
+		if (!$registrationPassword) {
+			$this->setAPIResponse('error', 'Registration Password was not supplied', 422);
+			return false;
+		}
+		if ($registrationPassword == $this->decrypt($this->config['registrationPassword'])) {
+			$this->writeLog('success', 'Registration Function - Registration Password Verified', $username);
+			if ($this->createUser($username, $password, $email)) {
+				$this->writeLog('success', 'Registration Function - A User has registered', $username);
+				if ($this->createToken($username, $email, $this->config['rememberMeDays'])) {
+					$this->writeLoginLog($username, 'success');
+					$this->writeLog('success', 'Login Function - A User has logged in', $username);
+					return true;
+				}
+			} else {
+				return false;
+			}
+		} else {
+			$this->writeLog('warning', 'Registration Function - Wrong Password', $username);
+			$this->setAPIResponse('error', 'Registration Password was incorrect', 401);
+			return false;
+		}
+	}
+	
+	public function authRegister($username, $password, $email, $token = null)
+	{
+		if ($this->config['authBackend'] !== '') {
+			$this->ombiImport($this->config['authBackend']);
+		}
+		$this->ssoCheck($username, $password, $token);
+		if ($token && (!$password || $password == '')) {
+			$password = $this->random_ascii_string(10);
+		}
+		if ($this->createUser($username, $password, $email)) {
+			$this->writeLog('success', 'Registration Function - A User has registered', $username);
+			if ($this->config['PHPMAILER-enabled'] && $email !== '') {
+				$PhpMailer = new PhpMailer();
+				$emailTemplate = array(
+					'type' => 'registration',
+					'body' => $this->config['PHPMAILER-emailTemplateRegisterUser'],
+					'subject' => $this->config['PHPMAILER-emailTemplateRegisterUserSubject'],
+					'user' => $username,
+					'password' => null,
+					'inviteCode' => null,
+				);
+				$emailTemplate = $PhpMailer->_phpMailerPluginEmailTemplate($emailTemplate);
+				$sendEmail = array(
+					'to' => $email,
+					'user' => $username,
+					'subject' => $emailTemplate['subject'],
+					'body' => $PhpMailer->_phpMailerPluginBuildEmail($emailTemplate),
+				);
+				$PhpMailer->_phpMailerPluginSendEmail($sendEmail);
+			}
+			if ($this->createToken($username, $email, $this->gravatar($email), $this->config['rememberMeDays'])) {
+				$this->writeLoginLog($username, 'success');
+				$this->writeLog('success', 'Login Function - A User has logged in', $username);
+				return true;
+			} else {
+				return false;
+			}
+		} else {
+			$this->writeLog('error', 'Registration Function - An error occurred', $username);
+			return false;
+		}
+	}
+	
+	public function revokeTokenCurrentUser($token = null)
+	{
+		if ($token) {
+			$response = [
+				array(
+					'function' => 'query',
+					'query' => array(
+						'DELETE FROM tokens WHERE token = ?',
+						[$token]
+					)
+				),
+			];
+		} else {
+			$response = [
+				array(
+					'function' => 'query',
+					'query' => array(
+						'DELETE FROM tokens WHERE user_id = ?',
+						[$this->user['userID']]
+					)
+				),
+			];
+		}
+		return $this->processQueries($response);
+	}
+	
+	public function revokeToken($token = null)
+	{
+		if (!$token) {
+			$this->setAPIResponse('error', 'Token was not supplied', 422);
+			return false;
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'DELETE FROM tokens WHERE token = ?',
+					[$token]
+				)
+			),
+		];
+		$this->setAPIResponse('success', 'Token revoked', 204);
+		return $this->processQueries($response);
+	}
+	
+	public function revokeTokenByIdCurrentUser($id = null)
+	{
+		if (!$id) {
+			$this->setAPIResponse('error', 'Id was not supplied', 422);
+			return false;
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'DELETE FROM tokens WHERE id = ? AND user_id = ?',
+					$id,
+					$this->user['userID']
+				)
+			),
+		];
+		$this->setAPIResponse('success', 'Token revoked', 204);
+		return $this->processQueries($response);
+	}
+	
+	public function updateUserPassword($password, $id)
+	{
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE users SET',
+					['password' => password_hash($password, PASSWORD_BCRYPT)],
+					'WHERE id = ?',
+					$id
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function updateUserPlexToken($token, $id)
+	{
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE users SET',
+					['plex_token' => $token],
+					'WHERE id = ?',
+					$id
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getUserTabsAndCategories($type = null)
+	{
+		if (!$this->hasDB()) {
+			return false;
+		}
+		$sort = ($this->config['unsortedTabs'] == 'top') ? 'DESC' : 'ASC';
+		$response = [
+			array(
+				'function' => 'fetchAll',
+				'query' => array(
+					'SELECT * FROM tabs WHERE `group_id` >= ? AND `enabled` = 1 ORDER BY `order` ' . $sort,
+					$this->user['groupID']
+				),
+				'key' => 'tabs'
+			),
+			array(
+				'function' => 'fetchAll',
+				'query' => array(
+					'SELECT * FROM categories ORDER BY `order` ASC',
+				),
+				'key' => 'categories'
+			),
+		];
+		$queries = $this->processQueries($response);
+		$all['tabs'] = $queries['tabs'];
+		foreach ($queries['tabs'] as $k => $v) {
+			$v['access_url'] = (!empty($v['url_local']) && ($v['url_local'] !== null) && ($v['url_local'] !== 'null') && $this->isLocal() && $v['type'] !== 0) ? $v['url_local'] : $v['url'];
+		}
+		$count = array_map(function ($element) {
+			return $element['category_id'];
+		}, $queries['tabs']);
+		$count = (array_count_values($count));
+		foreach ($queries['categories'] as $k => $v) {
+			$v['count'] = isset($count[$v['category_id']]) ? $count[$v['category_id']] : 0;
+		}
+		$all['categories'] = $queries['categories'];
+		switch ($type) {
+			case 'categories':
+				return $all['categories'];
+			case 'tabs':
+				return $all['tabs'];
+			default:
+				return $all;
+		}
+	}
+	
+	public function refreshList()
+	{
+		$searchTerm = "Refresh";
+		return array_filter($this->config, function ($k) use ($searchTerm) {
+			return stripos($k, $searchTerm) !== false;
+		}, ARRAY_FILTER_USE_KEY);
+	}
+	
+	public function homepageOrderList()
+	{
+		$searchTerm = "homepageOrder";
+		$order = array_filter($this->config, function ($k) use ($searchTerm) {
+			return stripos($k, $searchTerm) !== false;
+		}, ARRAY_FILTER_USE_KEY);
+		asort($order);
+		return $order;
+	}
+	
+	public function tautulliList()
+	{
+		$searchTerm = "tautulli_token";
+		return array_filter($this->config, function ($k) use ($searchTerm) {
+			return stripos($k, $searchTerm) !== false;
+		}, ARRAY_FILTER_USE_KEY);
+	}
+	
+	public function checkPlexAdminFilled()
+	{
+		if ($this->config['plexAdmin'] == '') {
+			return false;
+		} else {
+			if ((strpos($this->config['plexAdmin'], '@') !== false)) {
+				return 'email';
+			} else {
+				return 'username';
+			}
+		}
+	}
+	
+	public function organizrSpecialSettings()
+	{
+		return array(
+			'homepage' => array(
+				'refresh' => $this->refreshList(),
+				'order' => $this->homepageOrderList(),
+				'search' => array(
+					'enabled' => $this->qualifyRequest($this->config['mediaSearchAuth']) && $this->config['mediaSearch'] == true && $this->config['plexToken'],
+					'type' => $this->config['mediaSearchType'],
+				),
+				'ombi' => array(
+					'enabled' => $this->qualifyRequest($this->config['homepageOmbiAuth']) && $this->qualifyRequest($this->config['homepageOmbiRequestAuth']) && $this->config['homepageOmbiEnabled'] == true && $this->config['ssoOmbi'] && isset($_COOKIE['Auth']),
+					'authView' => $this->qualifyRequest($this->config['homepageOmbiAuth']),
+					'authRequest' => $this->qualifyRequest($this->config['homepageOmbiRequestAuth']),
+					'sso' => ($this->config['ssoOmbi']) ? true : false,
+					'cookie' => isset($_COOKIE['Auth']),
+					'alias' => ($this->config['ombiAlias']) ? true : false,
+					'ombiDefaultFilterAvailable' => $this->config['ombiDefaultFilterAvailable'] ? true : false,
+					'ombiDefaultFilterUnavailable' => $this->config['ombiDefaultFilterUnavailable'] ? true : false,
+					'ombiDefaultFilterApproved' => $this->config['ombiDefaultFilterApproved'] ? true : false,
+					'ombiDefaultFilterUnapproved' => $this->config['ombiDefaultFilterUnapproved'] ? true : false,
+					'ombiDefaultFilterDenied' => $this->config['ombiDefaultFilterDenied'] ? true : false
+				),
+				'options' => array(
+					'alternateHomepageHeaders' => $this->config['alternateHomepageHeaders'],
+					'healthChecksTags' => $this->config['healthChecksTags'],
+					'titles' => array(
+						'tautulli' => $this->config['tautulliHeader']
+					)
+				),
+				'media' => array(
+					'jellyfin' => $this->config['homepageJellyfinInstead']
+				)
+			),
+			'sso' => array(
+				'misc' => array(
+					'oAuthLogin' => isset($_COOKIE['oAuth']),
+					'rememberMe' => $this->config['rememberMe'],
+					'rememberMeDays' => $this->config['rememberMeDays']
+				),
+				'plex' => array(
+					'enabled' => ($this->config['ssoPlex']) ? true : false,
+					'cookie' => isset($_COOKIE['mpt']),
+					'machineID' => strlen($this->config['plexID']) == 40,
+					'token' => $this->config['plexToken'] !== '',
+					'plexAdmin' => $this->checkPlexAdminFilled(),
+					'strict' => ($this->config['plexStrictFriends']) ? true : false,
+					'oAuthEnabled' => ($this->config['plexoAuth']) ? true : false,
+					'backend' => $this->config['authBackend'] == 'plex',
+				),
+				'ombi' => array(
+					'enabled' => ($this->config['ssoOmbi']) ? true : false,
+					'cookie' => isset($_COOKIE['Auth']),
+					'url' => ($this->config['ombiURL'] !== '') ? $this->config['ombiURL'] : false,
+					'api' => $this->config['ombiToken'] !== '',
+				),
+				'tautulli' => array(
+					'enabled' => ($this->config['ssoTautulli']) ? true : false,
+					'cookie' => !empty($this->tautulliList()),
+					'url' => ($this->config['tautulliURL'] !== '') ? $this->config['tautulliURL'] : false,
+				),
+			),
+			'ping' => array(
+				'onlineSound' => $this->config['pingOnlineSound'],
+				'offlineSound' => $this->config['pingOfflineSound'],
+				'statusSounds' => $this->config['statusSounds'],
+				'auth' => $this->config['pingAuth'],
+				'authMessage' => $this->config['pingAuthMessage'],
+				'authMs' => $this->config['pingAuthMs'],
+				'ms' => $this->config['pingMs'],
+				'adminRefresh' => $this->config['adminPingRefresh'],
+				'everyoneRefresh' => $this->config['otherPingRefresh'],
+			),
+			'notifications' => array(
+				'backbone' => $this->config['notificationBackbone'],
+				'position' => $this->config['notificationPosition']
+			),
+			'lockout' => array(
+				'enabled' => $this->config['lockoutSystem'],
+				'timer' => $this->config['lockoutTimeout'],
+				'minGroup' => $this->config['lockoutMinAuth'],
+				'maxGroup' => $this->config['lockoutMaxAuth']
+			),
+			'user' => array(
+				'agent' => isset($_SERVER ['HTTP_USER_AGENT']) ? $_SERVER ['HTTP_USER_AGENT'] : null,
+				'oAuthLogin' => isset($_COOKIE['oAuth']),
+				'local' => $this->isLocal(),
+				'ip' => $this->userIP()
+			),
+			'login' => array(
+				'rememberMe' => $this->config['rememberMe'],
+				'rememberMeDays' => $this->config['rememberMeDays'],
+				'wanDomain' => $this->config['wanDomain'],
+				'localAddress' => $this->config['localAddress'],
+				'enableLocalAddressForward' => $this->config['enableLocalAddressForward'],
+			),
+			'misc' => array(
+				'installedPlugins' => $this->qualifyRequest(1) ? $this->config['installedPlugins'] : '',
+				'installedThemes' => $this->qualifyRequest(1) ? $this->config['installedThemes'] : '',
+				'return' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : false,
+				'authDebug' => $this->config['authDebug'],
+				'minimalLoginScreen' => $this->config['minimalLoginScreen'],
+				'unsortedTabs' => $this->config['unsortedTabs'],
+				'authType' => $this->config['authType'],
+				'authBackend' => $this->config['authBackend'],
+				'newMessageSound' => (isset($this->config['CHAT-newMessageSound-include'])) ? $this->config['CHAT-newMessageSound-include'] : '',
+				'uuid' => ($this->config['uuid']) ?? null,
+				'docker' => $this->qualifyRequest(1) ? $this->docker : '',
+				'githubCommit' => $this->qualifyRequest(1) ? $this->commit : '',
+				'schema' => $this->qualifyRequest(1) ? $this->getSchema() : '',
+				'debugArea' => $this->qualifyRequest($this->config['debugAreaAuth']),
+				'debugErrors' => $this->config['debugErrors'],
+				'sandbox' => $this->config['sandbox'],
+			),
+			'menuLink' => array(
+				'githubMenuLink' => $this->config['githubMenuLink'],
+				'organizrSupportMenuLink' => $this->config['organizrSupportMenuLink'],
+				'organizrDocsMenuLink' => $this->config['organizrDocsMenuLink'],
+				'organizrSignoutMenuLink' => $this->config['organizrSignoutMenuLink']
+			)
+		);
+	}
+	
+	public function getLog($log, $reverse = true)
+	{
+		switch ($log) {
+			case 'login':
+			case 'loginLog':
+			case 'loginlog':
+				$file = $this->organizrLoginLog;
+				$parent = 'auth';
+				break;
+			case 'org':
+			case 'organizr':
+			case 'organizrLog':
+			case 'orglog':
+				$file = $this->organizrLog;
+				$parent = 'log_items';
+				break;
+			default:
+				$this->setAPIResponse('error', 'Log not defined', 404);
+				return null;
+		}
+		if (!file_exists($file)) {
+			$this->setAPIResponse('error', 'Log does not exist', 404);
+			return null;
+		}
+		$getLog = str_replace("\r\ndate", "date", file_get_contents($file));
+		$gotLog = json_decode($getLog, true);
+		return ($reverse) ? array_reverse($gotLog[$parent]) : $gotLog[$parent];
+	}
+	
+	public function purgeLog($log)
+	{
+		
+		switch ($log) {
+			case 'login':
+			case 'loginLog':
+			case 'loginlog':
+				$file = $this->organizrLoginLog;
+				break;
+			case 'org':
+			case 'organizr':
+			case 'organizrLog':
+			case 'orgLog':
+			case 'orglog':
+				$file = $this->organizrLog;
+				break;
+			default:
+				$this->setAPIResponse('error', 'Log not defined', 404);
+				return null;
+		}
+		if (file_exists($file)) {
+			if (unlink($file)) {
+				$this->writeLog('success', 'Log Management Function - Log: ' . $log . ' has been purged/deleted', 'SYSTEM');
+				$this->setAPIResponse(null, 'Log purged');
+				return true;
+			} else {
+				$this->writeLog('error', 'Log Management Function - Log: ' . $log . ' - Error Occurred', 'SYSTEM');
+				$this->setAPIResponse('error', 'Log could not be purged', 500);
+				return false;
+			}
+		} else {
+			$this->setAPIResponse('error', 'Log does not exist', 404);
+			return false;
+		}
+		
+	}
+	
+	public function checkLog($path)
+	{
+		if (file_exists($path)) {
+			if (filesize($path) > 500000) {
+				rename($path, $path . '[' . date('Y-m-d') . '].json');
+				return false;
+			}
+			return true;
+		} else {
+			return false;
+		}
+	}
+	
+	public function writeLoginLog($username, $authType)
+	{
+		$username = htmlspecialchars($username, ENT_QUOTES);
+		if ($this->checkLog($this->organizrLoginLog)) {
+			$getLog = str_replace("\r\ndate", "date", file_get_contents($this->organizrLoginLog));
+			$gotLog = json_decode($getLog, true);
+		}
+		$logEntryFirst = array('logType' => 'login_log', 'auth' => array(array('date' => date("Y-m-d H:i:s"), 'utc_date' => $this->currentTime, 'username' => $username, 'ip' => $this->userIP(), 'auth_type' => $authType)));
+		$logEntry = array('date' => date("Y-m-d H:i:s"), 'utc_date' => $this->currentTime, 'username' => $username, 'ip' => $this->userIP(), 'auth_type' => $authType);
+		if (isset($gotLog)) {
+			array_push($gotLog["auth"], $logEntry);
+			$writeFailLog = str_replace("date", "\r\ndate", json_encode($gotLog));
+		} else {
+			$writeFailLog = str_replace("date", "\r\ndate", json_encode($logEntryFirst));
+		}
+		file_put_contents($this->organizrLoginLog, $writeFailLog);
+	}
+	
+	public function writeLog($type = 'error', $message, $username = null)
+	{
+		$this->timeExecution = $this->timeExecution($this->timeExecution);
+		$message = $message . ' [Execution Time: ' . $this->formatSeconds($this->timeExecution) . ']';
+		$username = ($username) ? htmlspecialchars($username, ENT_QUOTES) : $this->user['username'];
+		if ($this->checkLog($this->organizrLog)) {
+			$getLog = str_replace("\r\ndate", "date", file_get_contents($this->organizrLog));
+			$gotLog = json_decode($getLog, true);
+		}
+		$logEntryFirst = array('logType' => 'organizr_log', 'log_items' => array(array('date' => date("Y-m-d H:i:s"), 'utc_date' => $this->currentTime, 'type' => $type, 'username' => $username, 'ip' => $this->userIP(), 'message' => $message)));
+		$logEntry = array('date' => date("Y-m-d H:i:s"), 'utc_date' => $this->currentTime, 'type' => $type, 'username' => $username, 'ip' => $this->userIP(), 'message' => $message);
+		if (isset($gotLog)) {
+			array_push($gotLog["log_items"], $logEntry);
+			$writeFailLog = str_replace("date", "\r\ndate", json_encode($gotLog));
+		} else {
+			$writeFailLog = str_replace("date", "\r\ndate", json_encode($logEntryFirst));
+		}
+		file_put_contents($this->organizrLog, $writeFailLog);
+	}
+	
+	public function isApprovedRequest($method, $data)
+	{
+		$requesterToken = isset($this->getallheaders()['Token']) ? $this->getallheaders()['Token'] : (isset($_GET['apikey']) ? $_GET['apikey'] : false);
+		$apiKey = ($this->config['organizrAPI']) ?? null;
+		if (isset($data['formKey'])) {
+			$formKey = $data['formKey'];
+		} elseif (isset($this->getallheaders()['Formkey'])) {
+			$formKey = $this->getallheaders()['Formkey'];
+		} elseif (isset($this->getallheaders()['formkey'])) {
+			$formKey = $this->getallheaders()['formkey'];
+		} elseif (isset($this->getallheaders()['formKey'])) {
+			$formKey = $this->getallheaders()['formKey'];
+		} elseif (isset($this->getallheaders()['FormKey'])) {
+			$formKey = $this->getallheaders()['FormKey'];
+		} else {
+			$formKey = false;
+		}
+		// Check token or API key
+		// If API key, return 0 for admin
+		if (strlen($requesterToken) == 20 && $requesterToken == $apiKey) {
+			//DO API CHECK
+			return true;
+		} elseif ($method == 'POST') {
+			if ($this->checkFormKey($formKey)) {
+				return true;
+			} else {
+				$this->writeLog('error', 'API ERROR: Unable to authenticate Form Key: ' . $formKey, $this->user['username']);
+				return false;
+			}
+		} else {
+			return true;
+		}
+		return false;
+	}
+	
+	public function checkFormKey($formKey = '')
+	{
+		return password_verify(substr($this->config['organizrHash'], 2, 10), $formKey);
+	}
+	
+	public function buildHomepage()
+	{
+		$homepageOrder = $this->homepageOrderList();
+		$homepageBuilt = '';
+		foreach ($homepageOrder as $key => $value) {
+			//new way
+			if (method_exists($this, $key)) {
+				$homepageBuilt .= $this->$key();
+			} else {
+				$homepageBuilt .= '<div id="' . $key . '"></div>';
+			}
+			//old way
+			//$homepageBuilt .= $this->buildHomepageItem($key);
+		}
+		return $homepageBuilt;
+	}
+	
+	public function buildHomepageSettings()
+	{
+		$homepageOrder = $this->homepageOrderList();
+		$homepageList = '<h4>Drag Homepage Items to Order Them</h4><div id="homepage-items-sort" class="external-events">';
+		$inputList = '<form id="homepage-values" class="row">';
+		foreach ($homepageOrder as $key => $val) {
+			switch ($key) {
+				case 'homepageOrdercustomhtml':
+					$class = 'bg-info';
+					$image = 'plugins/images/tabs/custom1.png';
+					if (!$this->config['homepageCustomHTMLoneEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrdercustomhtmlTwo':
+					$class = 'bg-info';
+					$image = 'plugins/images/tabs/custom2.png';
+					if (!$this->config['homepageCustomHTMLtwoEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrdertransmission':
+					$class = 'bg-transmission';
+					$image = 'plugins/images/tabs/transmission.png';
+					if (!$this->config['homepageTransmissionEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrdernzbget':
+					$class = 'bg-nzbget';
+					$image = 'plugins/images/tabs/nzbget.png';
+					if (!$this->config['homepageNzbgetEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderjdownloader':
+					$class = 'bg-sab';
+					$image = 'plugins/images/tabs/jdownloader.png';
+					if (!$this->config['homepageJdownloaderEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrdersabnzbd':
+					$class = 'bg-sab';
+					$image = 'plugins/images/tabs/sabnzbd.png';
+					if (!$this->config['homepageSabnzbdEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderdeluge':
+					$class = 'bg-deluge';
+					$image = 'plugins/images/tabs/deluge.png';
+					if (!$this->config['homepageDelugeEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderqBittorrent':
+					$class = 'bg-qbit';
+					$image = 'plugins/images/tabs/qBittorrent.png';
+					if (!$this->config['homepageqBittorrentEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderrTorrent':
+					$class = 'bg-qbit';
+					$image = 'plugins/images/tabs/rTorrent.png';
+					if (!$this->config['homepagerTorrentEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderplexnowplaying':
+				case 'homepageOrderplexrecent':
+				case 'homepageOrderplexplaylist':
+					$class = 'bg-plex';
+					$image = 'plugins/images/tabs/plex.png';
+					if (!$this->config['homepagePlexEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderembynowplaying':
+				case 'homepageOrderembyrecent':
+					$class = 'bg-emby';
+					$image = 'plugins/images/tabs/emby.png';
+					if (!$this->config['homepageEmbyEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderjellyfinnowplaying':
+				case 'homepageOrderjellyfinrecent':
+					$class = 'bg-jellyfin';
+					$image = 'plugins/images/tabs/jellyfin.png';
+					if (!$this->config['homepageJellyfinEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderombi':
+					$class = 'bg-inverse';
+					$image = 'plugins/images/tabs/ombi.png';
+					if (!$this->config['homepageOmbiEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrdercalendar':
+					$class = 'bg-primary';
+					$image = 'plugins/images/tabs/calendar.png';
+					if (!$this->config['homepageSonarrEnabled'] && !$this->config['homepageRadarrEnabled'] && !$this->config['homepageSickrageEnabled'] && !$this->config['homepageCouchpotatoEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderdownloader':
+					$class = 'bg-inverse';
+					$image = 'plugins/images/tabs/downloader.png';
+					if (!$this->config['jdownloaderCombine'] && !$this->config['sabnzbdCombine'] && !$this->config['nzbgetCombine'] && !$this->config['rTorrentCombine'] && !$this->config['delugeCombine'] && !$this->config['transmissionCombine'] && !$this->config['qBittorrentCombine']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderhealthchecks':
+					$class = 'bg-healthchecks';
+					$image = 'plugins/images/tabs/healthchecks.png';
+					if (!$this->config['homepageHealthChecksEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderunifi':
+					$class = 'bg-info';
+					$image = 'plugins/images/tabs/ubnt.png';
+					if (!$this->config['homepageUnifiEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrdertautulli':
+					$class = 'bg-info';
+					$image = 'plugins/images/tabs/tautulli.png';
+					if (!$this->config['homepageTautulliEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderPihole':
+					$class = 'bg-info';
+					$image = 'plugins/images/tabs/pihole.png';
+					if (!$this->config['homepagePiholeEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderMonitorr':
+					$class = 'bg-info';
+					$image = 'plugins/images/tabs/monitorr.png';
+					if (!$this->config['homepageMonitorrEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderWeatherAndAir':
+					$class = 'bg-success';
+					$image = 'plugins/images/tabs/wind.png';
+					if (!$this->config['homepageWeatherAndAirEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderSpeedtest':
+					$class = 'bg-success';
+					$image = 'plugins/images/tabs/speedtest-icon.png';
+					if (!$this->config['homepageSpeedtestEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderNetdata':
+					$class = 'bg-success';
+					$image = 'plugins/images/tabs/netdata.png';
+					if (!$this->config['homepageNetdataEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderOctoprint':
+					$class = 'bg-success';
+					$image = 'plugins/images/tabs/octoprint.png';
+					if (!$this->config['homepageOctoprintEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderSonarrQueue':
+					$class = 'bg-sonarr';
+					$image = 'plugins/images/tabs/sonarr.png';
+					if (!$this->config['homepageSonarrQueueEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderRadarrQueue':
+					$class = 'bg-radarr';
+					$image = 'plugins/images/tabs/radarr.png';
+					if (!$this->config['homepageRadarrQueueEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				case 'homepageOrderJackett':
+					$class = 'bg-inverse';
+					$image = 'plugins/images/tabs/jackett.png';
+					if (!$this->config['homepageJackettEnabled']) {
+						$class .= ' faded';
+					}
+					break;
+				default:
+					$class = 'blue-bg';
+					$image = '';
+					break;
+			}
+			$homepageList .= '
+		<div class="col-md-3 col-xs-12 sort-homepage m-t-10 hvr-grow clearfix">
+			<div class="homepage-drag fc-event ' . $class . ' lazyload"  data-src="' . $image . '">
+				<span class="ordinal-position text-uppercase badge bg-org homepage-number" data-link="' . $key . '" style="float:left;width: 30px;">' . $val . '</span>
+				<span class="homepage-text">&nbsp; ' . strtoupper(substr($key, 13)) . '</span>
+
+			</div>
+		</div>
+		';
+			$inputList .= '<input type="hidden" name="' . $key . '">';
+		}
+		$homepageList .= '</div>';
+		$inputList .= '</form>';
+		return $homepageList . $inputList;
+	}
+	
+	public function setGroupOptionsVariable()
+	{
+		$this->groupOptions = $this->groupSelect();
+	}
+	
+	public function getSettingsHomepageItem($item)
+	{
+		$items = $this->getSettingsHomepage();
+		foreach ($items as $k => $v) {
+			if ($v['name'] === $item) {
+				return $v;
+			}
+		}
+		$this->setAPIResponse('error', 'Homepage item was not found', 404);
+		return null;
+	}
+	
+	public function getSettingsHomepage()
+	{
+		$this->setGroupOptionsVariable();
+		return $this->getHomepageSettingsCombined();
+	}
+	
+	public function isTabNameTaken($name, $id = null)
+	{
+		if ($id) {
+			$response = [
+				array(
+					'function' => 'fetchAll',
+					'query' => array(
+						'SELECT * FROM tabs WHERE `name` LIKE ? AND `id` != ?',
+						$name,
+						$id
+					)
+				),
+			];
+		} else {
+			$response = [
+				array(
+					'function' => 'fetchAll',
+					'query' => array(
+						'SELECT * FROM tabs WHERE `name` LIKE ?',
+						$name
+					)
+				),
+			];
+		}
+		return $this->processQueries($response);
+	}
+	
+	public function isCategoryNameTaken($name, $id = null)
+	{
+		if ($id) {
+			$response = [
+				array(
+					'function' => 'fetchAll',
+					'query' => array(
+						'SELECT * FROM categories WHERE `category` LIKE ? AND `id` != ?',
+						$name,
+						$id
+					)
+				),
+			];
+		} else {
+			$response = [
+				array(
+					'function' => 'fetchAll',
+					'query' => array(
+						'SELECT * FROM categories WHERE `category` LIKE ?',
+						$name
+					)
+				),
+			];
+		}
+		return $this->processQueries($response);
+	}
+	
+	public function isGroupNameTaken($name, $id = null)
+	{
+		if ($id) {
+			$response = [
+				array(
+					'function' => 'fetchAll',
+					'query' => array(
+						'SELECT * FROM groups WHERE `group` LIKE ? AND `id` != ?',
+						$name,
+						$id
+					)
+				),
+			];
+		} else {
+			$response = [
+				array(
+					'function' => 'fetchAll',
+					'query' => array(
+						'SELECT * FROM groups WHERE `group` LIKE ?',
+						$name
+					)
+				),
+			];
+		}
+		return $this->processQueries($response);
+	}
+	
+	public function getTableColumns($table)
+	{
+		$response = [
+			array(
+				'function' => 'fetchAll',
+				'query' => array(
+					'PRAGMA table_info(?)',
+					$table
+				)
+			),
+		];
+		return $this->processQueries($response);
+		
+	}
+	
+	public function getTableColumnsFormatted($table)
+	{
+		$columns = $this->getTableColumns($table);
+		if ($columns) {
+			$columnsFormatted = [];
+			foreach ($columns as $k => $v) {
+				$columnsFormatted[$v['name']] = $v;
+			}
+			return $columnsFormatted;
+		} else {
+			return false;
+		}
+	}
+	
+	public function getTabById($id)
+	{
+		$response = [
+			array(
+				'function' => 'fetch',
+				'query' => array(
+					'SELECT * FROM tabs WHERE `id` = ?',
+					$id
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getTabGroupByTabName($tab)
+	{
+		$response = [
+			array(
+				'function' => 'fetch',
+				'query' => array(
+					'SELECT group_id FROM tabs WHERE name LIKE %~like~',
+					$tab
+				)
+			),
+		];
+		$query = $this->processQueries($response);
+		return $query ? $query['group_id'] : 0;
+	}
+	
+	public function getCategoryById($id)
+	{
+		$response = [
+			array(
+				'function' => 'fetch',
+				'query' => array(
+					'SELECT * FROM categories WHERE `id` = ?',
+					$id
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getGroupUserCountById($id)
+	{
+		$response = [
+			array(
+				'function' => 'fetchSingle',
+				'query' => array(
+					'SELECT count(username) AS count FROM groups INNER JOIN users ON users.group_id = groups.group_id AND groups.id = ?',
+					$id
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getGroupById($id)
+	{
+		$response = [
+			array(
+				'function' => 'fetch',
+				'query' => array(
+					'SELECT * FROM groups WHERE `id` = ?',
+					$id
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getGroupByGroupId($id)
+	{
+		$response = [
+			array(
+				'function' => 'fetch',
+				'query' => array(
+					'SELECT * FROM groups WHERE `group_id` = ?',
+					$id
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getDefaultGroup()
+	{
+		$response = [
+			array(
+				'function' => 'fetch',
+				'query' => array(
+					'SELECT * FROM groups WHERE `default` = 1'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getDefaultGroupId()
+	{
+		$response = [
+			array(
+				'function' => 'fetchSingle',
+				'query' => array(
+					'SELECT `group_id` FROM groups WHERE `default` = 1'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getDefaultCategory()
+	{
+		$response = [
+			array(
+				'function' => 'fetch',
+				'query' => array(
+					'SELECT * FROM categories WHERE `default` = 1'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getDefaultCategoryId()
+	{
+		$response = [
+			array(
+				'function' => 'fetchSingle',
+				'query' => array(
+					'SELECT `category_id` FROM categories WHERE `default` = 1'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getNextTabOrder()
+	{
+		$response = [
+			array(
+				'function' => 'fetchSingle',
+				'query' => array(
+					'SELECT `order` from tabs ORDER BY `order` DESC'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getNextCategoryOrder()
+	{
+		$response = [
+			array(
+				'function' => 'fetchSingle',
+				'query' => array(
+					'SELECT `order` from categories ORDER BY `order` DESC'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getNextGroupOrder()
+	{
+		$response = [
+			array(
+				'function' => 'fetchSingle',
+				'query' => array(
+					'SELECT `group_id` from groups WHERE `group_id` != "999" ORDER BY `group_id` DESC'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function getNextCategoryId()
+	{
+		$response = [
+			array(
+				'function' => 'fetchSingle',
+				'query' => array(
+					'SELECT `category_id` from categories ORDER BY `category_id` DESC'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function clearTabDefault()
+	{
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE tabs SET `default` = 0'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function clearCategoryDefault()
+	{
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE categories SET `default` = 0'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function clearGroupDefault()
+	{
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE groups SET `default` = 0'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function checkKeys($tabInfo, $newData)
+	{
+		foreach ($newData as $k => $v) {
+			if (!array_key_exists($k, $tabInfo)) {
+				unset($newData[$k]);
+			}
+		}
+		return $newData;
+	}
+	
+	public function deleteTab($id)
+	{
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'DELETE FROM tabs WHERE id = ?',
+					$id
+				)
+			),
+		];
+		$tabInfo = $this->getTabById($id);
+		if ($tabInfo) {
+			$this->writeLog('success', 'Tab Delete Function -  Deleted Tab [' . $tabInfo['name'] . ']', $this->user['username']);
+			$this->setAPIResponse('success', 'Tab deleted', 204);
+			return $this->processQueries($response);
+		} else {
+			$this->setAPIResponse('error', 'id not found', 404);
+			return false;
+		}
+	}
+	
+	public function addTab($array)
+	{
+		if (!$array) {
+			$this->setAPIResponse('error', 'no data was sent', 422);
+			return null;
+		}
+		$array = $this->checkKeys($this->getTableColumnsFormatted('tabs'), $array);
+		$array['group_id'] = ($array['group_id']) ?? $this->getDefaultGroupId();
+		$array['category_id'] = ($array['category_id']) ?? $this->getDefaultCategoryId();
+		$array['enabled'] = ($array['enabled']) ?? 0;
+		$array['default'] = ($array['default']) ?? 0;
+		$array['type'] = ($array['type']) ?? 1;
+		$array['order'] = ($array['order']) ?? $this->getNextTabOrder() + 1;
+		if (array_key_exists('name', $array)) {
+			if ($this->isTabNameTaken($array['name'])) {
+				$this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409);
+				return false;
+			}
+		} else {
+			$this->setAPIResponse('error', 'Tab name was not supplied', 422);
+			return false;
+		}
+		if (!array_key_exists('url', $array) && !array_key_exists('url_local', $array)) {
+			$this->setAPIResponse('error', 'Tab url or url_local was not supplied', 422);
+			return false;
+		}
+		if (!array_key_exists('image', $array)) {
+			$this->setAPIResponse('error', 'Tab image was not supplied', 422);
+			return false;
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [tabs]',
+					$array
+				)
+			),
+		];
+		$this->setAPIResponse(null, 'Tab added');
+		$this->writeLog('success', 'Tab Editor Function -  Added Tab for [' . $array['name'] . ']', $this->user['username']);
+		return $this->processQueries($response);
+	}
+	
+	public function updateTab($id, $array)
+	{
+		if (!$id || $id == '') {
+			$this->setAPIResponse('error', 'id was not set', 422);
+			return null;
+		}
+		if (!$array) {
+			$this->setAPIResponse('error', 'no data was sent', 422);
+			return null;
+		}
+		$tabInfo = $this->getTabById($id);
+		if ($tabInfo) {
+			$array = $this->checkKeys($tabInfo, $array);
+		} else {
+			$this->setAPIResponse('error', 'No tab info found', 404);
+			return false;
+		}
+		if (array_key_exists('name', $array)) {
+			if ($this->isTabNameTaken($array['name'], $id)) {
+				$this->setAPIResponse('error', 'Tab name: ' . $array['name'] . ' is already taken', 409);
+				return false;
+			}
+		}
+		if (array_key_exists('default', $array)) {
+			if ($array['default']) {
+				$this->clearTabDefault();
+			}
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE tabs SET',
+					$array,
+					'WHERE id = ?',
+					$id
+				)
+			),
+		];
+		$this->setAPIResponse(null, 'Tab info updated');
+		$this->writeLog('success', 'Tab Editor Function -  Edited Tab Info for [' . $tabInfo['name'] . ']', $this->user['username']);
+		return $this->processQueries($response);
+	}
+	
+	public function updateTabOrder($array)
+	{
+		if (count($array) >= 1) {
+			foreach ($array as $tab) {
+				if (count($tab) !== 2) {
+					$this->setAPIResponse('error', 'data is malformed', 422);
+					break;
+				}
+				$id = $tab['id'] ?? null;
+				$order = $tab['order'] ?? null;
+				if ($id && $order) {
+					$response = [
+						array(
+							'function' => 'query',
+							'query' => array(
+								'UPDATE tabs set `order` = ? WHERE `id` = ?',
+								$order,
+								$id
+							)
+						),
+					];
+					$this->processQueries($response);
+					$this->setAPIResponse(null, 'Tab Order updated');
+				} else {
+					$this->setAPIResponse('error', 'data is malformed', 422);
+				}
+			}
+		} else {
+			$this->setAPIResponse('error', 'data is empty or not in array', 422);
+			return false;
+		}
+	}
+	
+	public function addCategory($array)
+	{
+		if (!$array) {
+			$this->setAPIResponse('error', 'no data was sent', 422);
+			return null;
+		}
+		$array = $this->checkKeys($this->getTableColumnsFormatted('categories'), $array);
+		$array['default'] = ($array['default']) ?? 0;
+		$array['order'] = ($array['order']) ?? $this->getNextCategoryOrder() + 1;
+		$array['category_id'] = ($array['category_id']) ?? $this->getNextCategoryId() + 1;
+		if (array_key_exists('category', $array)) {
+			if ($this->isCategoryNameTaken($array['category'])) {
+				$this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409);
+				return false;
+			}
+		} else {
+			$this->setAPIResponse('error', 'Category name was not supplied', 422);
+			return false;
+		}
+		if (!array_key_exists('image', $array)) {
+			$this->setAPIResponse('error', 'Category image was not supplied', 422);
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [categories]',
+					$array
+				)
+			),
+		];
+		$this->setAPIResponse(null, 'Category added');
+		$this->writeLog('success', 'Category Editor Function -  Added Category for [' . $array['category'] . ']', $this->user['username']);
+		return $this->processQueries($response);
+	}
+	
+	public function updateCategory($id, $array)
+	{
+		if (!$id || $id == '') {
+			$this->setAPIResponse('error', 'id was not set', 422);
+			return null;
+		}
+		if (!$array) {
+			$this->setAPIResponse('error', 'no data was sent', 422);
+			return null;
+		}
+		$categoryInfo = $this->getCategoryById($id);
+		if ($categoryInfo) {
+			$array = $this->checkKeys($categoryInfo, $array);
+		} else {
+			$this->setAPIResponse('error', 'No category info found', 404);
+			return false;
+		}
+		if (array_key_exists('category', $array)) {
+			if ($this->isCategoryNameTaken($array['category'], $id)) {
+				$this->setAPIResponse('error', 'Category name: ' . $array['category'] . ' is already taken', 409);
+				return false;
+			}
+		}
+		if (array_key_exists('default', $array)) {
+			if ($array['default']) {
+				$this->clearCategoryDefault();
+			}
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE categories SET',
+					$array,
+					'WHERE id = ?',
+					$id
+				)
+			),
+		];
+		$this->setAPIResponse(null, 'Category info updated');
+		$this->writeLog('success', 'Category Editor Function -  Edited Category Info for [' . $categoryInfo['category'] . ']', $this->user['username']);
+		return $this->processQueries($response);
+	}
+	
+	public function updateCategoryOrder($array)
+	{
+		if (count($array) >= 1) {
+			foreach ($array as $category) {
+				if (count($category) !== 2) {
+					$this->setAPIResponse('error', 'data is malformed', 422);
+					break;
+				}
+				$id = $category['id'] ?? null;
+				$order = $category['order'] ?? null;
+				if ($id && $order) {
+					$response = [
+						array(
+							'function' => 'query',
+							'query' => array(
+								'UPDATE categories set `order` = ? WHERE `id` = ?',
+								$order,
+								$id
+							)
+						),
+					];
+					$this->processQueries($response);
+					$this->setAPIResponse(null, 'Category Order updated');
+				} else {
+					$this->setAPIResponse('error', 'data is malformed', 422);
+				}
+			}
+		} else {
+			$this->setAPIResponse('error', 'data is empty or not in array', 422);
+			return false;
+		}
+	}
+	
+	public function deleteCategory($id)
+	{
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'DELETE FROM categories WHERE id = ?',
+					$id
+				)
+			),
+		];
+		$categoryInfo = $this->getCategoryById($id);
+		if ($categoryInfo) {
+			$this->writeLog('success', 'Category Delete Function -  Deleted Category [' . $categoryInfo['category'] . ']', $this->user['username']);
+			$this->setAPIResponse('success', 'Category deleted', 204);
+			return $this->processQueries($response);
+		} else {
+			$this->setAPIResponse('error', 'id not found', 404);
+			return false;
+		}
+	}
+	
+	public function marketplaceFileListFormat($files, $folder, $type)
+	{
+		foreach ($files as $k => $v) {
+			$splitFiles = explode('|', $v);
+			$prePath = (strlen($k) !== 1) ? $k . '/' : $k;
+			foreach ($splitFiles as $file) {
+				$filesList[] = array(
+					'fileName' => $file,
+					'path' => $prePath,
+					'githubPath' => 'https://raw.githubusercontent.com/causefx/Organizr/v2-' . $type . '/' . $folder . $prePath . $file
+				);
+			}
+		}
+		return $filesList;
+		
+	}
+	
+	public function removeTheme($theme)
+	{
+		$theme = $this->reverseCleanClassName($theme);
+		$array = $this->getThemesGithub();
+		$arrayLower = array_change_key_case($array);
+		if (!$array) {
+			$this->setAPIResponse('error', 'Could not access theme marketplace', 409);
+			return false;
+		}
+		if (!$arrayLower[$theme]) {
+			$this->setAPIResponse('error', 'Theme does not exist in marketplace', 404);
+			return false;
+		} else {
+			$key = array_search($theme, array_keys($arrayLower));
+			$theme = array_keys($array)[$key];
+		}
+		$array = $array[$theme];
+		$downloadList = $this->marketplaceFileListFormat($array['files'], $array['github_folder'], 'themes');
+		if (!$downloadList) {
+			$this->setAPIResponse('error', 'Could not get download list for theme', 409);
+			return false;
+		}
+		$name = $theme;
+		$version = $array['version'];
+		$installedThemesNew = '';
+		foreach ($downloadList as $k => $v) {
+			$file = array(
+				'from' => $v['githubPath'],
+				'to' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->root . $v['path'] . $v['fileName']),
+				'path' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->root . $v['path'])
+			);
+			if (!$this->rrmdir($file['to'])) {
+				$this->writeLog('error', 'Theme Function -  Remove File Failed  for: ' . $v['githubPath'], $this->user['username']);
+				return false;
+			}
+		}
+		if ($this->config['installedThemes'] !== '') {
+			$installedThemes = explode('|', $this->config['installedThemes']);
+			foreach ($installedThemes as $k => $v) {
+				$themes = explode(':', $v);
+				$installedThemesList[$themes[0]] = $themes[1];
+			}
+			if (isset($installedThemesList[$name])) {
+				foreach ($installedThemesList as $k => $v) {
+					if ($k !== $name) {
+						if ($installedThemesNew == '') {
+							$installedThemesNew .= $k . ':' . $v;
+						} else {
+							$installedThemesNew .= '|' . $k . ':' . $v;
+						}
+					}
+				}
+			}
+		}
+		$this->updateConfig(array('installedThemes' => $installedThemesNew));
+		$this->setAPIResponse('success', 'Theme removed', 200, $installedThemesNew);
+		return true;
+	}
+	
+	public function installTheme($theme)
+	{
+		$theme = $this->reverseCleanClassName($theme);
+		$array = $this->getThemesGithub();
+		$arrayLower = array_change_key_case($array);
+		if (!$array) {
+			$this->setAPIResponse('error', 'Could not access theme marketplace', 409);
+			return false;
+		}
+		if (!$arrayLower[$theme]) {
+			$this->setAPIResponse('error', 'Theme does not exist in marketplace', 404);
+			return false;
+		} else {
+			$key = array_search($theme, array_keys($arrayLower));
+			$theme = array_keys($array)[$key];
+		}
+		$array = $array[$theme];
+		$downloadList = $this->marketplaceFileListFormat($array['files'], $array['github_folder'], 'themes');
+		if (!$downloadList) {
+			$this->setAPIResponse('error', 'Could not get download list for theme', 409);
+			return false;
+		}
+		$name = $theme;
+		$version = $array['version'];
+		foreach ($downloadList as $k => $v) {
+			$file = array(
+				'from' => $v['githubPath'],
+				'to' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->root . $v['path'] . $v['fileName']),
+				'path' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->root . $v['path'])
+			);
+			if (!$this->downloadFileToPath($file['from'], $file['to'], $file['path'])) {
+				$this->writeLog('error', 'Theme Function -  Downloaded File Failed  for: ' . $v['githubPath'], $this->user['username']);
+				$this->setAPIResponse('error', 'Theme download failed', 500);
+				return false;
+			}
+		}
+		if ($this->config['installedThemes'] !== '') {
+			$installedThemes = explode('|', $this->config['installedThemes']);
+			foreach ($installedThemes as $k => $v) {
+				$themes = explode(':', $v);
+				$installedThemesList[$themes[0]] = $themes[1];
+			}
+			if (isset($installedThemesList[$name])) {
+				$installedThemesList[$name] = $version;
+				$installedThemesNew = '';
+				foreach ($installedThemesList as $k => $v) {
+					if ($installedThemesNew == '') {
+						$installedThemesNew .= $k . ':' . $v;
+					} else {
+						$installedThemesNew .= '|' . $k . ':' . $v;
+					}
+				}
+			} else {
+				$installedThemesNew = $this->config['installedThemes'] . '|' . $name . ':' . $version;
+			}
+		} else {
+			$installedThemesNew = $name . ':' . $version;
+		}
+		$this->updateConfig(array('installedThemes' => $installedThemesNew));
+		$this->setAPIResponse('success', 'Theme installed', 200, $installedThemesNew);
+		return true;
+	}
+	
+	public function removePlugin($plugin)
+	{
+		$plugin = $this->reverseCleanClassName($plugin);
+		$array = $this->getPluginsGithub();
+		$arrayLower = array_change_key_case($array);
+		if (!$array) {
+			$this->setAPIResponse('error', 'Could not access plugin marketplace', 409);
+			return false;
+		}
+		if (!$arrayLower[$plugin]) {
+			$this->setAPIResponse('error', 'Plugin does not exist in marketplace', 404);
+			return false;
+		} else {
+			$key = array_search($plugin, array_keys($arrayLower));
+			$plugin = array_keys($array)[$key];
+		}
+		$array = $array[$plugin];
+		$downloadList = $this->marketplaceFileListFormat($array['files'], $array['github_folder'], 'plugins');
+		if (!$downloadList) {
+			$this->setAPIResponse('error', 'Could not get download list for plugin', 409);
+			return false;
+		}
+		$name = $plugin;
+		$version = $array['version'];
+		$installedPluginsNew = '';
+		foreach ($downloadList as $k => $v) {
+			$file = array(
+				'from' => $v['githubPath'],
+				'to' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->root . $v['path'] . $v['fileName']),
+				'path' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->root . $v['path'])
+			);
+			if (!$this->rrmdir($file['to'])) {
+				$this->writeLog('error', 'Plugin Function -  Remove File Failed  for: ' . $v['githubPath'], $this->user['username']);
+				return false;
+			}
+		}
+		if ($this->config['installedPlugins'] !== '') {
+			$installedPlugins = explode('|', $this->config['installedPlugins']);
+			foreach ($installedPlugins as $k => $v) {
+				$plugins = explode(':', $v);
+				$installedPluginsList[$plugins[0]] = $plugins[1];
+			}
+			if (isset($installedPluginsList[$name])) {
+				foreach ($installedPluginsList as $k => $v) {
+					if ($k !== $name) {
+						if ($installedPluginsNew == '') {
+							$installedPluginsNew .= $k . ':' . $v;
+						} else {
+							$installedPluginsNew .= '|' . $k . ':' . $v;
+						}
+					}
+				}
+			}
+		}
+		$this->updateConfig(array('installedPlugins' => $installedPluginsNew));
+		$this->setAPIResponse('success', 'Plugin removed', 200, $installedPluginsNew);
+		return true;
+	}
+	
+	public function installPlugin($plugin)
+	{
+		$plugin = $this->reverseCleanClassName($plugin);
+		$array = $this->getPluginsGithub();
+		$arrayLower = array_change_key_case($array);
+		if (!$array) {
+			$this->setAPIResponse('error', 'Could not access plugin marketplace', 409);
+			return false;
+		}
+		if (!$arrayLower[$plugin]) {
+			$this->setAPIResponse('error', 'Plugin does not exist in marketplace', 404);
+			return false;
+		} else {
+			$key = array_search($plugin, array_keys($arrayLower));
+			$plugin = array_keys($array)[$key];
+		}
+		$array = $array[$plugin];
+		$downloadList = $this->marketplaceFileListFormat($array['files'], $array['github_folder'], 'plugins');
+		if (!$downloadList) {
+			$this->setAPIResponse('error', 'Could not get download list for plugin', 409);
+			return false;
+		}
+		$name = $plugin;
+		$version = $array['version'];
+		foreach ($downloadList as $k => $v) {
+			$file = array(
+				'from' => $v['githubPath'],
+				'to' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->root . $v['path'] . $v['fileName']),
+				'path' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->root . $v['path'])
+			);
+			if (!$this->downloadFileToPath($file['from'], $file['to'], $file['path'])) {
+				$this->writeLog('error', 'Plugin Function -  Downloaded File Failed  for: ' . $v['githubPath'], $this->user['username']);
+				$this->setAPIResponse('error', 'Plugin download failed', 500);
+				return false;
+			}
+		}
+		if ($this->config['installedPlugins'] !== '') {
+			$installedPlugins = explode('|', $this->config['installedPlugins']);
+			foreach ($installedPlugins as $k => $v) {
+				$plugins = explode(':', $v);
+				$installedPluginsList[$plugins[0]] = $plugins[1];
+			}
+			if (isset($installedPluginsList[$name])) {
+				$installedPluginsList[$name] = $version;
+				$installedPluginsNew = '';
+				foreach ($installedPluginsList as $k => $v) {
+					if ($installedPluginsNew == '') {
+						$installedPluginsNew .= $k . ':' . $v;
+					} else {
+						$installedPluginsNew .= '|' . $k . ':' . $v;
+					}
+				}
+			} else {
+				$installedPluginsNew = $this->config['installedPlugins'] . '|' . $name . ':' . $version;
+			}
+		} else {
+			$installedPluginsNew = $name . ':' . $version;
+		}
+		$this->updateConfig(array('installedPlugins' => $installedPluginsNew));
+		$this->setAPIResponse('success', 'Plugin installed', 200, $installedPluginsNew);
+		return true;
+	}
+	
+	public function getThemesGithub()
+	{
+		$url = 'https://raw.githubusercontent.com/causefx/Organizr/v2-themes/themes.json';
+		$options = (localURL($url)) ? array('verify' => false) : array();
+		$response = Requests::get($url, array(), $options);
+		if ($response->success) {
+			return json_decode($response->body, true);
+		}
+		return false;
+	}
+	
+	public function getPluginsGithub()
+	{
+		$url = 'https://raw.githubusercontent.com/causefx/Organizr/v2-plugins/plugins.json';
+		$options = (localURL($url)) ? array('verify' => false) : array();
+		$response = Requests::get($url, array(), $options);
+		if ($response->success) {
+			return json_decode($response->body, true);
+		}
+		return false;
+	}
+	
+	public function getOpenCollectiveBackers()
+	{
+		$url = 'https://opencollective.com/organizr/members/users.json?limit=100&offset=0';
+		$options = (localURL($url)) ? array('verify' => false) : array();
+		$response = Requests::get($url, array(), $options);
+		if ($response->success) {
+			$api = json_decode($response->body, true);
+			$this->setAPIResponse('success', '', 200, $api);
+			return $api;
+		}
+		$this->setAPIResponse('error', 'Error connecting to Open Collective', 409);
+		return false;
+	}
+	
+	public function guestHash($start, $end)
+	{
+		$ip = $_SERVER['REMOTE_ADDR'];
+		$ip = md5($ip);
+		return substr($ip, $start, $end);
+	}
+	
+	public function rrmdir($dir)
+	{
+		ini_set('max_execution_time', 0);
+		set_time_limit(0);
+		if (is_dir($dir)) {
+			$files = scandir($dir);
+			foreach ($files as $file) {
+				if ($file != "." && $file != "..") {
+					$this->rrmdir("$dir/$file");
+				}
+			}
+			rmdir($dir);
+		} elseif (file_exists($dir)) {
+			unlink($dir);
+		}
+		return true;
+	}
+	
+	public function rcopy($src, $dst)
+	{
+		ini_set('max_execution_time', 0);
+		set_time_limit(0);
+		$src = $this->cleanPath($src);
+		$dst = $this->cleanPath($dst);
+		if (is_dir($src)) {
+			if (!file_exists($dst)) : mkdir($dst);
+			endif;
+			$files = scandir($src);
+			foreach ($files as $file) {
+				if ($file != "." && $file != "..") {
+					$this->rcopy("$src/$file", "$dst/$file");
+				}
+			}
+		} elseif (file_exists($src)) {
+			copy($src, $dst);
+		}
+		return true;
+	}
+	
+	public function unzipFile($zipFile)
+	{
+		ini_set('max_execution_time', 0);
+		set_time_limit(0);
+		$zip = new ZipArchive;
+		$extractPath = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . "upgrade/";
+		if ($zip->open($extractPath . $zipFile) != "true") {
+			$this->writeLog("error", "organizr could not unzip upgrade.zip");
+		} else {
+			$this->writeLog("success", "organizr unzipped upgrade.zip");
+		}
+		/* Extract Zip File */
+		$zip->extractTo($extractPath);
+		$zip->close();
+		return true;
+	}
+	
+	public function downloadFile($url, $path)
+	{
+		ini_set('max_execution_time', 0);
+		set_time_limit(0);
+		$folderPath = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . "upgrade" . DIRECTORY_SEPARATOR;
+		if (!file_exists($folderPath)) {
+			if (@!mkdir($folderPath)) {
+				$this->writeLog('error', 'Update Function -  Folder Creation failed', $this->user['username']);
+				return false;
+			}
+		}
+		$newfname = $folderPath . $path;
+		$context = stream_context_create(
+			array(
+				'ssl' => array(
+					'verify_peer' => true,
+					'cafile' => $this->getCert()
+				)
+			)
+		);
+		$file = fopen($url, 'rb', false, $context);
+		if ($file) {
+			$newf = fopen($newfname, 'wb');
+			if ($newf) {
+				while (!feof($file)) {
+					fwrite($newf, fread($file, 1024 * 8), 1024 * 8);
+				}
+			}
+		} else {
+			$this->writeLog("error", "organizr could not download $url");
+			return false;
+		}
+		if ($file) {
+			fclose($file);
+			$this->writeLog("success", "organizr finished downloading the github zip file");
+		} else {
+			$this->writeLog("error", "organizr could not download the github zip file");
+			return false;
+		}
+		if ($newf) {
+			fclose($newf);
+			$this->writeLog("success", "organizr created upgrade zip file from github zip file");
+		} else {
+			$this->writeLog("error", "organizr could not create upgrade zip file from github zip file");
+			return false;
+		}
+		return true;
+	}
+	
+	public function downloadFileToPath($from, $to, $path)
+	{
+		ini_set('max_execution_time', 0);
+		set_time_limit(0);
+		if (@!mkdir($path, 0777, true)) {
+			$this->writeLog("error", "organizr could not create folder or folder already exists", 'SYSTEM');
+		}
+		$file = fopen($from, 'rb');
+		if ($file) {
+			$newf = fopen($to, 'wb');
+			if ($newf) {
+				while (!feof($file)) {
+					fwrite($newf, fread($file, 1024 * 8), 1024 * 8);
+				}
+			}
+		} else {
+			$this->writeLog("error", "organizr could not download file", 'SYSTEM');
+		}
+		if ($file) {
+			fclose($file);
+			$this->writeLog("success", "organizr finished downloading the file", 'SYSTEM');
+		} else {
+			$this->writeLog("error", "organizr could not download the file", 'SYSTEM');
+		}
+		if ($newf) {
+			fclose($newf);
+			$this->writeLog("success", "organizr saved/moved the file", 'SYSTEM');
+		} else {
+			$this->writeLog("error", "organizr could not saved/moved the file", 'SYSTEM');
+		}
+		return true;
+	}
+	
+	public function getAllUsers($includeGroups = false)
+	{
+		$response = [
+			array(
+				'function' => 'fetchAll',
+				'query' => array(
+					'SELECT * FROM users'
+				),
+				'key' => 'users'
+			),
+		];
+		$groups = array(
+			'function' => 'fetchAll',
+			'query' => array(
+				'SELECT * FROM groups ORDER BY group_id ASC'
+			),
+			'key' => 'groups'
+		);
+		$addGroups = (isset($_GET['includeGroups']) || $includeGroups) ?? false;
+		if ($addGroups) {
+			array_push($response, $groups);
+		}
+		return $this->processQueries($response);
+	}
+	
+	public function getAllGroups()
+	{
+		$response = [
+			array(
+				'function' => 'fetchAll',
+				'query' => array(
+					'SELECT * FROM groups ORDER BY group_id ASC'
+				),
+				'key' => 'groups'
+			),
+		];
+		$users = array(
+			'function' => 'fetchAll',
+			'query' => array(
+				'SELECT * FROM users'
+			),
+			'key' => 'users'
+		);
+		$addUsers = (isset($_GET['includeUsers'])) ?? false;
+		if ($addUsers) {
+			array_push($response, $users);
+		}
+		return $this->processQueries($response);
+	}
+	
+	public function importUsers($array)
+	{
+		$imported = 0;
+		foreach ($array as $user) {
+			$password = $this->random_ascii_string(30);
+			if ($user['username'] !== '' && $user['email'] !== '' && $password !== '') {
+				$newUser = $this->createUser($user['username'], $password, $user['email']);
+				if (!$newUser) {
+					$this->writeLog('error', 'Import Function - Error', $user['username']);
+				} else {
+					$imported++;
+				}
+			}
+		}
+		$this->setAPIResponse('success', 'Imported ' . $imported . ' users', 200);
+		return true;
+	}
+	
+	public function importUsersType($type)
+	{
+		if ($type !== '') {
+			switch ($type) {
+				case 'plex':
+					return $this->importUsers($this->allPlexUsers(true));
+				case 'jellyfin':
+					return $this->importUsers($this->allJellyfinUsers(true));
+				case 'emby':
+					return $this->importUsers($this->allEmbyUsers(true));
+				default:
+					return false;
+			}
+		}
+		return false;
+	}
+	
+	public function allPlexUsers($newOnly = false, $friendsOnly = false)
+	{
+		try {
+			if (!empty($this->config['plexToken'])) {
+				$url = 'https://plex.tv/api/users';
+				$headers = array(
+					'X-Plex-Token' => $this->config['plexToken'],
+				);
+				$response = Requests::get($url, $headers);
+				if ($response->success) {
+					libxml_use_internal_errors(true);
+					$userXML = simplexml_load_string($response->body);
+					if (is_array($userXML) || is_object($userXML)) {
+						$results = array();
+						foreach ($userXML as $child) {
+							if (((string)$child['restricted'] == '0')) {
+								if ($newOnly) {
+									$taken = $this->usernameTaken((string)$child['username'], (string)$child['email']);
+									if (!$taken) {
+										$results[] = array(
+											'username' => (string)$child['username'],
+											'email' => (string)$child['email'],
+											'id' => (string)$child['id'],
+										);
+									}
+								} elseif ($friendsOnly) {
+									$machineMatches = false;
+									foreach ($child->Server as $server) {
+										if ((string)$server['machineIdentifier'] == $this->config['plexID']) {
+											$machineMatches = true;
+										}
+									}
+									if ($machineMatches) {
+										$results[] = array(
+											'username' => (string)$child['username'],
+											'email' => (string)$child['email'],
+											'id' => (string)$child['id'],
+										);
+									}
+								} else {
+									$results[] = array(
+										'username' => (string)$child['username'],
+										'email' => (string)$child['email'],
+										'id' => (string)$child['id'],
+									);
+								}
+								
+							}
+						}
+						return $results;
+					}
+				}
+			}
+			return false;
+		} catch (Requests_Exception $e) {
+			$this->writeLog('success', 'Plex Import User Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		}
+		return false;
+	}
+	
+	public function allJellyfinUsers($newOnly = false)
+	{
+		try {
+			if (!empty($this->config['jellyfinURL']) && !empty($this->config['jellyfinToken'])) {
+				$url = $this->qualifyURL($this->config['jellyfinURL']) . '/Users?api_key=' . $this->config['jellyfinToken'];
+				$headers = array();
+				$response = Requests::get($url, $headers);
+				if ($response->success) {
+					$users = json_decode($response->body, true);
+					if (is_array($users) || is_object($users)) {
+						$results = array();
+						foreach ($users as $child) {
+							// Jellyfin doesn't list emails for some reason
+							$email = $this->random_ascii_string(10) . '@placeholder.eml';
+							if ($newOnly) {
+								$taken = $this->usernameTaken((string)$child['Name'], $email);
+								if (!$taken) {
+									$results[] = array(
+										'username' => (string)$child['Name'],
+										'email' => $email
+									);
+								}
+							} else {
+								$results[] = array(
+									'username' => (string)$child['Name'],
+									'email' => $email,
+								);
+							}
+						}
+						return $results;
+					}
+				}
+			}
+			return false;
+		} catch (Requests_Exception $e) {
+			$this->writeLog('success', 'Jellyfin Import User Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		}
+		return false;
+	}
+	
+	public function allEmbyUsers($newOnly = false)
+	{
+		try {
+			if (!empty($this->config['embyURL']) && !empty($this->config['embyToken'])) {
+				$url = $this->qualifyURL($this->config['embyURL']) . '/Users?api_key=' . $this->config['embyToken'];
+				$headers = array();
+				$response = Requests::get($url, $headers);
+				if ($response->success) {
+					$users = json_decode($response->body, true);
+					if (is_array($users) || is_object($users)) {
+						$results = array();
+						foreach ($users as $child) {
+							// Emby doesn't list emails for some reason
+							$email = $this->random_ascii_string(10) . '@placeholder.eml';
+							if ($newOnly) {
+								$taken = $this->usernameTaken((string)$child['Name'], $email);
+								if (!$taken) {
+									$results[] = array(
+										'username' => (string)$child['Name'],
+										'email' => $email
+									);
+								}
+							} else {
+								$results[] = array(
+									'username' => (string)$child['Name'],
+									'email' => $email,
+								);
+							}
+						}
+						return $results;
+					}
+				}
+			}
+			return false;
+		} catch (Requests_Exception $e) {
+			$this->writeLog('success', 'Emby Import User Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		}
+		return false;
+	}
+	
+	public function updateUser($id, $array)
+	{
+		if (!$id) {
+			$this->setAPIResponse('error', 'Id was not supplied', 422);
+			return false;
+		}
+		if ($id !== $this->user['userID']) {
+			if (!$this->qualifyRequest('1', true)) {
+				return false;
+			}
+		}
+		$user = $this->getUserById($id);
+		if ($user) {
+			$array = $this->checkKeys($user, $array);
+		} else {
+			$this->setAPIResponse('error', 'User was not found', 404);
+			return false;
+		}
+		if ($user['group_id'] == 0 && $this->user['groupID'] !== 0) {
+			$this->setAPIResponse('error', 'Cannot update admin unless you are admin', 401);
+			return false;
+		}
+		if (array_key_exists('username', $array)) {
+			if ($array['username'] == '') {
+				$this->setAPIResponse('error', 'Username was set but empty', 409);
+				return false;
+			}
+			if ($this->usernameTaken($array['username'], $array['username'], $id)) {
+				$this->setAPIResponse('error', 'Username: ' . $array['username'] . ' is already taken', 409);
+				return false;
+			}
+		}
+		if (array_key_exists('email', $array)) {
+			if ($array['email'] == '') {
+				$this->setAPIResponse('error', 'Email was set but empty', 409);
+				return false;
+			}
+			if ($this->usernameTaken($array['email'], $array['email'], $id)) {
+				$this->setAPIResponse('error', 'Email: ' . $array['email'] . ' is already taken', 409);
+				return false;
+			}
+		}
+		if (array_key_exists('group_id', $array)) {
+			if ($array['group_id'] == '') {
+				$this->setAPIResponse('error', 'group_id was set but empty', 409);
+				return false;
+			}
+			if (!$this->qualifyRequest('1', false)) {
+				$this->setAPIResponse('error', 'Cannot change your own group_id', 401);
+				return false;
+			}
+			if (($id == $this->user['userID']) && $this->user['groupID'] == 0) {
+				$array['group_id'] = 0;
+			}
+			if (($id == $this->user['userID']) && ($array['group_id'] == 0 && $this->user['groupID'] !== 0)) {
+				$this->setAPIResponse('error', 'Only admins can make others admins', 401);
+				return false;
+			}
+			$array['group'] = $this->getGroupByGroupId($array['group_id'])['group'];
+			if (!$array['group']) {
+				$this->setAPIResponse('error', 'group_id does not exist', 404);
+				return false;
+			}
+		}
+		if (array_key_exists('locked', $array)) {
+			//$this->setAPIResponse('error', 'Cannot use endpoint to unlock or lock user - please use /users/{id}/lock', 409);
+			//return false;
+		}
+		if (array_key_exists('password', $array)) {
+			if ($array['password'] == '') {
+				$this->setAPIResponse('error', 'Password was set but empty', 409);
+				return false;
+			}
+			$array['password'] = password_hash($array['password'], PASSWORD_BCRYPT);
+		}
+		if (array_key_exists('register_date', $array)) {
+			$this->setAPIResponse('error', 'Cannot update register date', 409);
+			return false;
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE users SET',
+					$array,
+					'WHERE id = ?',
+					$id
+				)
+			),
+		];
+		$this->setAPIResponse(null, 'User info updated');
+		$this->writeLog('success', 'User Editor Function -  Updated User Info for [' . $user['username'] . ']', $this->user['username']);
+		return $this->processQueries($response);
+	}
+	
+	public function deleteUser($id)
+	{
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'DELETE FROM users WHERE id = ?',
+					$id
+				)
+			),
+		];
+		$userInfo = $this->getUserById($id);
+		if ($id == $this->user['userID']) {
+			$this->setAPIResponse('error', 'Cannot delete your own user', 409);
+			return false;
+		}
+		if ($userInfo) {
+			$this->writeLog('success', 'User Delete Function -  Deleted User [' . $userInfo['username'] . ']', $this->user['username']);
+			$this->setAPIResponse('success', 'User deleted', 204);
+			return $this->processQueries($response);
+		} else {
+			$this->setAPIResponse('error', 'id not found', 404);
+			return false;
+		}
+	}
+	
+	public function addUser($array)
+	{
+		$username = $array['username'] ?? null;
+		$password = $array['password'] ?? null;
+		$email = $array['email'] ?? null;
+		if (!$username) {
+			$this->setAPIResponse('error', 'Username was not supplied', 409);
+			return false;
+		}
+		if (!$password) {
+			$this->setAPIResponse('error', 'Password was not supplied', 409);
+			return false;
+		}
+		if ($this->createUser($username, $password, $email)) {
+			$this->writeLog('success', 'Create User Function - Account created for [' . $username . ']', $this->user['username']);
+			return true;
+		} else {
+			$this->writeLog('error', 'Create User Function - An error occurred', $this->user['username']);
+			return false;
+		}
+	}
+	
+	public function createUser($username, $password, $email = null)
+	{
+		$username = $username ?? null;
+		$password = $password ?? null;
+		$email = ($email) ? $email : $this->random_ascii_string(10) . '@placeholder.eml';
+		if (!$username) {
+			$this->setAPIResponse('error', 'Username was set but empty', 409);
+			return false;
+		}
+		if (!$password) {
+			$this->setAPIResponse('error', 'Password was set but empty', 409);
+			return false;
+		}
+		if ($this->usernameTaken($username, $email)) {
+			$this->setAPIResponse('error', 'Username: ' . $username . ' or Email: ' . $email . ' is already taken', 409);
+			return false;
+		}
+		$defaults = $this->getDefaultGroup();
+		$userInfo = [
+			'username' => $username,
+			'password' => password_hash($password, PASSWORD_BCRYPT),
+			'email' => $email,
+			'group' => $defaults['group'],
+			'group_id' => $defaults['group_id'],
+			'image' => $this->gravatar($email),
+			'register_date' => $this->currentTime,
+		];
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [users]',
+					$userInfo
+				)
+			),
+		];
+		$this->setAPIResponse('success', 'User created', 200);
+		return $this->processQueries($response);
+	}
+	
+	public function updateGroup($id, $array)
+	{
+		if (!$id || $id == '') {
+			$this->setAPIResponse('error', 'id was not set', 422);
+			return null;
+		}
+		if (!$array) {
+			$this->setAPIResponse('error', 'no data was sent', 422);
+			return null;
+		}
+		$groupInfo = $this->getGroupById($id);
+		if ($groupInfo) {
+			$array = $this->checkKeys($groupInfo, $array);
+		} else {
+			$this->setAPIResponse('error', 'No category info found', 404);
+			return false;
+		}
+		if (array_key_exists('group_id', $array)) {
+			$this->setAPIResponse('error', 'Cannot change group_id', 409);
+			return false;
+			
+		}
+		if (array_key_exists('group', $array)) {
+			if ($array['group'] == '') {
+				$this->setAPIResponse('error', 'Group was set but empty', 409);
+				return false;
+			}
+			if ($this->isGroupNameTaken($array['group'], $id)) {
+				$this->setAPIResponse('error', 'Group name: ' . $array['group'] . ' is already taken', 409);
+				return false;
+			}
+		}
+		if (array_key_exists('image', $array)) {
+			if ($array['image'] == '') {
+				$this->setAPIResponse('error', 'Image was set but empty', 409);
+				return false;
+			}
+		}
+		if (array_key_exists('default', $array)) {
+			if ($groupInfo['group_id'] == 0 || $groupInfo['group_id'] == 999) {
+				$this->setAPIResponse('error', 'Setting ' . $groupInfo['group'] . ' as default group is not allowed', 409);
+				return false;
+			}
+			if ($array['default']) {
+				$this->clearGroupDefault();
+				$array['default'] = 1;
+			}
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE groups SET',
+					$array,
+					'WHERE id = ?',
+					$id
+				)
+			),
+		];
+		$this->setAPIResponse(null, 'Group info updated');
+		$this->writeLog('success', 'Group Editor Function -  Edited Group Info for [' . $groupInfo['group'] . ']', $this->user['username']);
+		return $this->processQueries($response);
+	}
+	
+	public function deleteGroup($id)
+	{
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'DELETE FROM groups WHERE id = ?',
+					$id
+				)
+			),
+		];
+		$groupInfo = $this->getGroupById($id);
+		if ($groupInfo['group_id'] == 0 || $groupInfo['group_id'] == 999) {
+			$this->setAPIResponse('error', 'Cannot delete group: ' . $groupInfo['group'] . ' as it is not allowed', 409);
+			return false;
+		}
+		if ($this->getGroupUserCountById($id) >= 1) {
+			$this->setAPIResponse('error', 'Cannot delete group as group still has users assigned to it', 409);
+			return false;
+		}
+		if ($groupInfo) {
+			$this->writeLog('success', 'Group Delete Function -  Deleted Group [' . $groupInfo['group'] . ']', $this->user['username']);
+			$this->setAPIResponse('success', 'Group deleted', 204);
+			return $this->processQueries($response);
+		} else {
+			$this->setAPIResponse('error', 'id not found', 404);
+			return false;
+		}
+	}
+	
+	public function addGroup($array)
+	{
+		if (!$array) {
+			$this->setAPIResponse('error', 'no data was sent', 422);
+			return null;
+		}
+		$array = $this->checkKeys($this->getTableColumnsFormatted('groups'), $array);
+		$array['default'] = ($array['default']) ?? 0;
+		$array['group_id'] = $this->getNextGroupOrder() + 1;
+		if (array_key_exists('group', $array)) {
+			if ($this->isGroupNameTaken($array['group'])) {
+				$this->setAPIResponse('error', 'Group name: ' . $array['group'] . ' is already taken', 409);
+				return false;
+			}
+		} else {
+			$this->setAPIResponse('error', 'Group name was not supplied', 422);
+			return false;
+		}
+		if (array_key_exists('image', $array)) {
+			if ($array['image'] == '') {
+				$this->setAPIResponse('error', 'Group image cannot be empty', 422);
+				return false;
+			}
+		} else {
+			$this->setAPIResponse('error', 'Group image was not supplied', 422);
+			return false;
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [groups]',
+					$array
+				)
+			),
+		];
+		$this->setAPIResponse(null, 'Tab added');
+		$this->writeLog('success', 'Tab Editor Function -  Added Tab for [' . $array['name'] . ']', $this->user['username']);
+		return $this->processQueries($response);
+	}
+	
+	public function userList($type = null)
+	{
+		switch ($type) {
+			case 'plex':
+				if (!empty($this->config['plexToken']) && !empty($this->config['plexID'])) {
+					$url = 'https://plex.tv/api/servers/' . $this->config['plexID'] . '/shared_servers';
+					try {
+						$headers = array(
+							"Accept" => "application/json",
+							"X-Plex-Token" => $this->config['plexToken']
+						);
+						$response = Requests::get($url, $headers, array());
+						libxml_use_internal_errors(true);
+						if ($response->success) {
+							$libraryList = array();
+							$plex = simplexml_load_string($response->body);
+							foreach ($plex->SharedServer as $child) {
+								if (!empty($child['username'])) {
+									$username = (string)strtolower($child['username']);
+									$email = (string)strtolower($child['email']);
+									$libraryList['users'][$username] = (string)$child['id'];
+									$libraryList['emails'][$email] = (string)$child['id'];
+									$libraryList['both'][$username] = $email;
+								}
+							}
+							$libraryList = array_change_key_case($libraryList, CASE_LOWER);
+							return $libraryList;
+						}
+					} catch (Requests_Exception $e) {
+						$this->writeLog('error', 'Plex Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+					}
+				}
+				break;
+			default:
+				# code...
+				break;
+		}
+		return false;
+	}
+	
+	
+	public function encrypt($password, $key = null)
+	{
+		$key = ($key) ? $key : ((isset($this->config['organizrHash'])) ? $this->config['organizrHash'] : null);
+		return openssl_encrypt($password, 'AES-256-CBC', $key, 0, $this->fillString($key, 16));
+	}
+	
+	public function decrypt($password, $key = null)
+	{
+		if (empty($password)) {
+			return '';
+		}
+		$key = ($key) ? $key : ((isset($this->config['organizrHash'])) ? $this->config['organizrHash'] : null);
+		return openssl_decrypt($password, 'AES-256-CBC', $key, 0, $this->fillString($key, 16));
+	}
+	
+	public function getCert()
+	{
+		$url = 'http://curl.haxx.se/ca/cacert.pem';
+		$file = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'functions' . DIRECTORY_SEPARATOR . 'cert' . DIRECTORY_SEPARATOR . 'cacert.pem';
+		$file2 = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'functions' . DIRECTORY_SEPARATOR . 'cert' . DIRECTORY_SEPARATOR . 'cacert-initial.pem';
+		$useCert = (file_exists($file)) ? $file : $file2;
+		if ($this->config['selfSignedCert'] !== '') {
+			if (file_exists($this->config['selfSignedCert'])) {
+				return $this->config['selfSignedCert'];
+			}
+		}
+		$context = stream_context_create(
+			array(
+				'ssl' => array(
+					'verify_peer' => true,
+					'cafile' => $useCert
+				)
+			)
+		);
+		if (!file_exists($file)) {
+			file_put_contents($file, fopen($url, 'r', false, $context));
+		} elseif (file_exists($file) && time() - 2592000 > filemtime($file)) {
+			file_put_contents($file, fopen($url, 'r', false, $context));
+		}
+		return $file;
+	}
+	
+	public function plexJoinAPI($array)
+	{
+		$username = ($array['username']) ?? null;
+		$email = ($array['email']) ?? null;
+		$password = ($array['password']) ?? null;
+		if (!$username) {
+			$this->setAPIResponse('error', 'Username not supplied', 409);
+			return false;
+		}
+		if (!$email) {
+			$this->setAPIResponse('error', 'Email not supplied', 409);
+			return false;
+		}
+		if (!$password) {
+			$this->setAPIResponse('error', 'Password not supplied', 409);
+			return false;
+		}
+		return $this->plexJoin($username, $email, $password);
+	}
+	
+	public function plexJoin($username, $email, $password)
+	{
+		
+		try {
+			$url = 'https://plex.tv/api/v2/users';
+			$headers = array(
+				'Accept' => 'application/json',
+				'Content-Type' => 'application/x-www-form-urlencoded',
+				'X-Plex-Product' => 'Organizr',
+				'X-Plex-Version' => '2.0',
+				'X-Plex-Client-Identifier' => $this->config['uuid'],
+			);
+			$data = array(
+				'email' => $email,
+				'username' => $username,
+				'password' => $password,
+			);
+			$response = Requests::post($url, $headers, $data, array());
+			$json = json_decode($response->body, true);
+			$errors = !empty($json['errors']);
+			$success = !empty($json['user']);
+			//Use This for later
+			$errorMessage = "";
+			if ($errors) {
+				foreach ($json['errors'] as $error) {
+					if (isset($error['message']) && isset($error['field'])) {
+						$errorMessage .= "[Plex.tv Error: " . $error['message'] . " for field: (" . $error['field'] . ")]";
+					}
+				}
+			}
+			$msg = (!empty($success) && empty($errors)) ? 'User has joined Plex' : $errorMessage;
+			$status = (!empty($success) && empty($errors)) ? 'success' : 'error';
+			$code = (!empty($success) && empty($errors)) ? 200 : 422;
+			$this->setAPIResponse($status, $msg, $code);
+			return (!empty($success) && empty($errors));
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Plex.TV Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', 'An Error Occurred', 409);
+			return false;
+		}
+		return false;
+	}
+	
+	public function lockCurrentUser()
+	{
+		if ($this->user['userID'] == '999') {
+			$this->setAPIResponse('error', 'Locking not allowed on Guest users', 409);
+			return false;
+		}
+		return $this->lockUser($this->user['userID']);
+	}
+	
+	public function lockUser($id)
+	{
+		
+		$user = $this->getUserById($id);
+		if (!$user) {
+			$this->setAPIResponse('error', 'User not found', 404);
+			return false;
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE users SET',
+					['locked' => '1'],
+					'WHERE id = ?',
+					$id
+				)
+			),
+		];
+		$this->writeLog('success', 'User Lockout Function - User: ' . $user['username'] . ' account locked', $this->user['username']);
+		$this->setAPIResponse('success', 'User account locked', 200);
+		return $this->processQueries($response);
+	}
+	
+	public function unlockCurrentUser($array)
+	{
+		if ($array['password'] == '') {
+			$this->setAPIResponse('error', 'Password Not Set', 422);
+			return false;
+		}
+		$user = $this->getUserById($this->user['userID']);
+		if (!password_verify($array['password'], $user['password'])) {
+			$this->setAPIResponse('error', 'Password Incorrect', 401);
+			return false;
+		}
+		return $this->unlockUser($this->user['userID']);
+	}
+	
+	public function unlockUser($id)
+	{
+		$user = $this->getUserById($id);
+		if (!$user) {
+			$this->setAPIResponse('error', 'User not found', 404);
+			return false;
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE users SET',
+					['locked' => '0'],
+					'WHERE id = ?',
+					$id
+				)
+			),
+		];
+		$this->writeLog('success', 'User Lockout Function - User: ' . $user['username'] . ' account unlocked', $this->user['username']);
+		$this->setAPIResponse('success', 'User account unlocked', 200);
+		return $this->processQueries($response);
+	}
+	
+	public function youtubeSearch($query)
+	{
+		if (!$query) {
+			$this->setAPIResponse('error', 'No query supplied', 422);
+			return false;
+		}
+		$keys = array(
+			'AIzaSyBsdt8nLJRMTwOq5PY5A5GLZ2q7scgn01w',
+			'AIzaSyD-8SHutB60GCcSM8q_Fle38rJUV7ujd8k',
+			'AIzaSyBzOpVBT6VII-b-8gWD0MOEosGg4hyhCsQ',
+			'AIzaSyBKnRe1P8fpfBHgooJpmT0WOsrdUtZ4cpk'
+		);
+		$randomKeyIndex = array_rand($keys);
+		$key = $keys[$randomKeyIndex];
+		$apikey = ($this->config['youtubeAPI'] !== '') ? $this->config['youtubeAPI'] : $key;
+		$results = false;
+		$url = "https://www.googleapis.com/youtube/v3/search?part=snippet&q=$query+official+trailer&part=snippet&maxResults=1&type=video&videoDuration=short&key=$apikey";
+		$response = Requests::get($url);
+		if ($response->success) {
+			$results = json_decode($response->body, true);
+			$this->setAPIResponse('success', null, 200, $results);
+			return $results;
+		} else {
+			$this->setAPIResponse('error', 'Bad response from YouTube', 500);
+			return false;
+		}
+	}
+	
+	public function scrapePage($array)
+	{
+		try {
+			$url = $array['url'] ?? false;
+			$type = $array['type'] ?? false;
+			if (!$url) {
+				$this->setAPIResponse('error', 'URL was not supplied', 422);
+				return false;
+			}
+			$url = $this->qualifyURL($url);
+			$data = array(
+				'full_url' => $url,
+				'drill_url' => $this->qualifyURL($url, true)
+			);
+			$options = array('verify' => false);
+			$response = Requests::get($url, array(), $options);
+			$data['response_code'] = $response->status_code;
+			if ($response->success) {
+				$data['result'] = 'Success';
+				switch ($type) {
+					case 'html':
+						$data['data'] = html_entity_decode($response->body);
+						break;
+					case 'json':
+						$data['data'] = json_decode($response->body);
+						break;
+					default:
+						$data['data'] = $response->body;
+				}
+				$this->setAPIResponse('success', null, 200, $data);
+				return $data;
+			} else {
+				$this->setAPIResponse('error', 'Error getting successful response', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function socks($url, $enabled, $auth, $requestObject, $header = null)
+	{
+		$error = false;
+		if (!$this->config[$enabled]) {
+			$error = true;
+			$this->setAPIResponse('error', 'SOCKS module is not enabled', 409);
+		}
+		if (!$this->qualifyRequest($this->config[$auth], true)) {
+			$error = true;
+		}
+		if (!$error) {
+			$pre = explode('/api/v2/socks/', $requestObject->getUri()->getPath());
+			$endpoint = explode('/', $pre[1]);
+			$new = str_ireplace($endpoint[0], '', $pre[1]);
+			$getParams = ($_GET) ? '?' . http_build_query($_GET) : '';
+			$url = $this->qualifyURL($this->config[$url]) . $new . $getParams;
+			$url = $this->cleanPath($url);
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$headers = [];
+			if ($header) {
+				if ($requestObject->hasHeader($header)) {
+					$headerKey = $requestObject->getHeaderLine($header);
+					$headers[$header] = $headerKey;
+				}
+			}
+			switch ($requestObject->getMethod()) {
+				case 'GET':
+					$call = Requests::get($url, $headers, $options);
+					break;
+				case 'POST':
+					$call = Requests::post($url, $headers, $this->apiData($requestObject), $options);
+					break;
+				case 'DELETE':
+					$call = Requests::delete($url, $headers, $options);
+					break;
+				case 'PUT':
+					$call = Requests::put($url, $headers, $this->apiData($requestObject), $options);
+					break;
+				default:
+					$call = Requests::get($url, $headers, $options);
+			}
+			return $call->body;
+		} else {
+			return null;
+		}
+	}
+	
+	public function getPlexServers()
+	{
+		if ($this->config['plexToken'] == '') {
+			$this->setAPIResponse('error', 'Plex Token cannot be empty', 422);
+			return false;
+		}
+		$ownedOnly = isset($_GET['owned']) ?? false;
+		$url = $this->qualifyURL('https://plex.tv/pms/servers');
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		$headers = [
+			'X-Plex-Product' => 'Organizr',
+			'X-Plex-Version' => '2.0',
+			'X-Plex-Client-Identifier' => '01010101-10101010',
+			'X-Plex-Token' => $this->config['plexToken'],
+		];
+		$response = Requests::get($url, $headers, $options);
+		libxml_use_internal_errors(true);
+		if ($response->success) {
+			$items = array();
+			$plex = simplexml_load_string($response->body);
+			foreach ($plex as $server) {
+				if ($ownedOnly) {
+					if ($server['owned'] == 1) {
+						$items[] = array(
+							'name' => (string)$server['name'],
+							'address' => (string)$server['address'],
+							'machineIdentifier' => (string)$server['machineIdentifier'],
+							'owned' => (float)$server['owned'],
+						);
+					}
+				} else {
+					$items[] = array(
+						'name' => (string)$server['name'],
+						'address' => (string)$server['address'],
+						'machineIdentifier' => (string)$server['machineIdentifier'],
+						'owned' => (float)$server['owned'],
+					);
+				}
+				
+			}
+			$this->setAPIResponse('success', null, 200, $items);
+			return $items;
+		}
+		
+	}
+	
+	public function getIcons()
+	{
+		$term = $_GET['search'] ?? null;
+		$page = $_GET['page'] ?? 1;
+		$limit = $_GET['limit'] ?? 20;
+		$offset = ($page * $limit) - $limit;
+		$goodIcons['results'] = [];
+		$goodIcons['limit'] = $limit;
+		$goodIcons['page'] = $page;
+		$goodIcons['term'] = $term;
+		$allIcons = file_get_contents($this->root . '/js/icons.json');
+		$iconListing = json_decode($allIcons, true);
+		foreach ($iconListing as $setKey => $set) {
+			foreach ($set['children'] as $k => $v) {
+				if (stripos($v['text'], $term) !== false || !$term) {
+					$goodIcons['results'][] = $v;
+				}
+			}
+		}
+		$total = count($goodIcons['results']);
+		$goodIcons['total'] = $total;
+		$goodIcons['results'] = array_slice($goodIcons['results'], $offset, $limit);
+		$goodIcons['pagination']['more'] = $page < (ceil($total / $limit));
+		return $goodIcons;
+	}
+	
+	protected function processQueries(array $request, $migration = false)
+	{
+		$results = array();
+		$firstKey = '';
+		try {
+			foreach ($request as $k => $v) {
+				
+				$query = ($migration) ? $this->otherDb->query($v['query']) : $this->db->query($v['query']);
+				$keyName = (isset($v['key'])) ? $v['key'] : $k;
+				$firstKey = (isset($v['key']) && $k == 0) ? $v['key'] : $k;
+				switch ($v['function']) {
+					case 'fetchAll':
+						$results[$keyName] = $query->fetchAll();
+						break;
+					case 'fetch':
+						$results[$keyName] = $query->fetch();
+						break;
+					case 'getAffectedRows':
+						$results[$keyName] = $query->getAffectedRows();
+						break;
+					case 'getRowCount':
+						$results[$keyName] = $query->getRowCount();
+						break;
+					case 'fetchSingle':
+						$results[$keyName] = $query->fetchSingle();
+						break;
+					case 'query':
+						$results[$keyName] = $query;
+						break;
+					default:
+						return false;
+				}
+			}
+			
+		} catch (Exception $e) {
+			return $e;
+		}
+		return count($request) > 1 ? $results : $results[$firstKey];
+	}
+	
+}

+ 328 - 0
api/classes/ping.class.php

@@ -0,0 +1,328 @@
+<?php
+
+class Ping
+{
+	
+	private $host;
+	private $ttl;
+	private $timeout;
+	private $port = 80;
+	private $data = 'Ping';
+	private $commandOutput;
+	
+	/**
+	 * Called when the Ping object is created.
+	 *
+	 * @param string $host
+	 *   The host to be pinged.
+	 * @param int $ttl
+	 *   Time-to-live (TTL) (You may get a 'Time to live exceeded' error if this
+	 *   value is set too low. The TTL value indicates the scope or range in which
+	 *   a packet may be forwarded. By convention:
+	 *     - 0 = same host
+	 *     - 1 = same subnet
+	 *     - 32 = same site
+	 *     - 64 = same region
+	 *     - 128 = same continent
+	 *     - 255 = unrestricted
+	 * @param int $timeout
+	 *   Timeout (in seconds) used for ping and fsockopen().
+	 * @throws \Exception if the host is not set.
+	 */
+	public function __construct($host, $ttl = 255, $timeout = 10)
+	{
+		if (!isset($host)) {
+			throw new \Exception("Error: Host name not supplied.");
+		}
+		$this->host = $host;
+		$this->ttl = $ttl;
+		$this->timeout = $timeout;
+	}
+	
+	/**
+	 * Set the ttl (in hops).
+	 *
+	 * @param int $ttl
+	 *   TTL in hops.
+	 */
+	public function setTtl($ttl)
+	{
+		$this->ttl = $ttl;
+	}
+	
+	/**
+	 * Get the ttl.
+	 *
+	 * @return int
+	 *   The current ttl for Ping.
+	 */
+	public function getTtl()
+	{
+		return $this->ttl;
+	}
+	
+	/**
+	 * Set the timeout.
+	 *
+	 * @param int $timeout
+	 *   Time to wait in seconds.
+	 */
+	public function setTimeout($timeout)
+	{
+		$this->timeout = $timeout;
+	}
+	
+	/**
+	 * Get the timeout.
+	 *
+	 * @return int
+	 *   Current timeout for Ping.
+	 */
+	public function getTimeout()
+	{
+		return $this->timeout;
+	}
+	
+	/**
+	 * Set the host.
+	 *
+	 * @param string $host
+	 *   Host name or IP address.
+	 */
+	public function setHost($host)
+	{
+		$this->host = $host;
+	}
+	
+	/**
+	 * Get the host.
+	 *
+	 * @return string
+	 *   The current hostname for Ping.
+	 */
+	public function getHost()
+	{
+		return $this->host;
+	}
+	
+	/**
+	 * Set the port (only used for fsockopen method).
+	 *
+	 * Since regular pings use ICMP and don't need to worry about the concept of
+	 * 'ports', this is only used for the fsockopen method, which pings servers by
+	 * checking port 80 (by default).
+	 *
+	 * @param int $port
+	 *   Port to use for fsockopen ping (defaults to 80 if not set).
+	 */
+	public function setPort($port)
+	{
+		$this->port = $port;
+	}
+	
+	/**
+	 * Get the port (only used for fsockopen method).
+	 *
+	 * @return int
+	 *   The port used by fsockopen pings.
+	 */
+	public function getPort()
+	{
+		return $this->port;
+	}
+	
+	/**
+	 * Return the command output when method=exec.
+	 * @return string
+	 */
+	public function getCommandOutput()
+	{
+		return $this->commandOutput;
+	}
+	
+	/**
+	 * Matches an IP on command output and returns.
+	 * @return string
+	 */
+	public function getIpAddress()
+	{
+		$out = array();
+		if (preg_match('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', $this->commandOutput, $out)) {
+			return $out[0];
+		}
+		return null;
+	}
+	
+	/**
+	 * Ping a host.
+	 *
+	 * @param string $method
+	 *   Method to use when pinging:
+	 *     - exec (default): Pings through the system ping command. Fast and
+	 *       robust, but a security risk if you pass through user-submitted data.
+	 *     - fsockopen: Pings a server on port 80.
+	 *     - socket: Creates a RAW network socket. Only usable in some
+	 *       environments, as creating a SOCK_RAW socket requires root privileges.
+	 *
+	 * @throws InvalidArgumentException if $method is not supported.
+	 *
+	 * @return mixed
+	 *   Latency as integer, in ms, if host is reachable or FALSE if host is down.
+	 */
+	public function ping($method = 'exec')
+	{
+		$latency = false;
+		switch ($method) {
+			case 'exec':
+				$latency = $this->pingExec();
+				break;
+			case 'fsockopen':
+				$latency = $this->pingFsockopen();
+				break;
+			case 'socket':
+				$latency = $this->pingSocket();
+				break;
+			default:
+				throw new \InvalidArgumentException('Unsupported ping method.');
+		}
+		// Return the latency.
+		return $latency;
+	}
+	
+	/**
+	 * The exec method uses the possibly insecure exec() function, which passes
+	 * the input to the system. This is potentially VERY dangerous if you pass in
+	 * any user-submitted data. Be SURE you sanitize your inputs!
+	 *
+	 * @return int
+	 *   Latency, in ms.
+	 */
+	private function pingExec()
+	{
+		$latency = false;
+		$ttl = escapeshellcmd($this->ttl);
+		$timeout = escapeshellcmd($this->timeout);
+		$host = escapeshellcmd($this->host);
+		// Exec string for Windows-based systems.
+		if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+			// -n = number of pings; -i = ttl; -w = timeout (in milliseconds).
+			$exec_string = 'ping -n 1 -i ' . $ttl . ' -w ' . ($timeout * 1000) . ' ' . $host;
+		} // Exec string for Darwin based systems (OS X).
+		else if (strtoupper(PHP_OS) === 'DARWIN') {
+			// -n = numeric output; -c = number of pings; -m = ttl; -t = timeout.
+			$exec_string = 'ping -n -c 1 -m ' . $ttl . ' -t ' . $timeout . ' ' . $host;
+		} // Exec string for other UNIX-based systems (Linux).
+		else {
+			// -n = numeric output; -c = number of pings; -t = ttl; -W = timeout
+			$exec_string = 'ping -n -c 1 -t ' . $ttl . ' -W ' . $timeout . ' ' . $host . ' 2>&1';
+		}
+		exec($exec_string, $output, $return);
+		// Strip empty lines and reorder the indexes from 0 (to make results more
+		// uniform across OS versions).
+		$this->commandOutput = implode($output, '');
+		$output = array_values(array_filter($output));
+		// If the result line in the output is not empty, parse it.
+		if (!empty($output[1])) {
+			// Search for a 'time' value in the result line.
+			$response = preg_match("/time(?:=|<)(?<time>[\.0-9]+)(?:|\s)ms/", $output[1], $matches);
+			// If there's a result and it's greater than 0, return the latency.
+			if ($response > 0 && isset($matches['time'])) {
+				$latency = round($matches['time'], 2);
+			}
+		}
+		return $latency;
+	}
+	
+	/**
+	 * The fsockopen method simply tries to reach the host on a port. This method
+	 * is often the fastest, but not necessarily the most reliable. Even if a host
+	 * doesn't respond, fsockopen may still make a connection.
+	 *
+	 * @return int
+	 *   Latency, in ms.
+	 */
+	private function pingFsockopen()
+	{
+		$start = microtime(true);
+		// fsockopen prints a bunch of errors if a host is unreachable. Hide those
+		// irrelevant errors and deal with the results instead.
+		$fp = @fsockopen($this->host, $this->port, $errno, $errstr, $this->timeout);
+		if (!$fp) {
+			$latency = false;
+		} else {
+			$latency = microtime(true) - $start;
+			$latency = round($latency * 1000, 2);
+		}
+		return $latency;
+	}
+	
+	/**
+	 * The socket method uses raw network packet data to try sending an ICMP ping
+	 * packet to a server, then measures the response time. Using this method
+	 * requires the script to be run with root privileges, though, so this method
+	 * only works reliably on Windows systems and on Linux servers where the
+	 * script is not being run as a web user.
+	 *
+	 * @return int
+	 *   Latency, in ms.
+	 */
+	private function pingSocket()
+	{
+		// Create a package.
+		$type = "\x08";
+		$code = "\x00";
+		$checksum = "\x00\x00";
+		$identifier = "\x00\x00";
+		$seq_number = "\x00\x00";
+		$package = $type . $code . $checksum . $identifier . $seq_number . $this->data;
+		// Calculate the checksum.
+		$checksum = $this->calculateChecksum($package);
+		// Finalize the package.
+		$package = $type . $code . $checksum . $identifier . $seq_number . $this->data;
+		// Create a socket, connect to server, then read socket and calculate.
+		if ($socket = socket_create(AF_INET, SOCK_RAW, 1)) {
+			socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array(
+				'sec' => 10,
+				'usec' => 0,
+			));
+			// Prevent errors from being printed when host is unreachable.
+			@socket_connect($socket, $this->host, null);
+			$start = microtime(true);
+			// Send the package.
+			@socket_send($socket, $package, strlen($package), 0);
+			if (socket_read($socket, 255) !== false) {
+				$latency = microtime(true) - $start;
+				$latency = round($latency * 1000, 2);
+			} else {
+				$latency = false;
+			}
+		} else {
+			$latency = false;
+		}
+		// Close the socket.
+		socket_close($socket);
+		return $latency;
+	}
+	
+	/**
+	 * Calculate a checksum.
+	 *
+	 * @param string $data
+	 *   Data for which checksum will be calculated.
+	 *
+	 * @return string
+	 *   Binary string checksum of $data.
+	 */
+	private function calculateChecksum($data)
+	{
+		if (strlen($data) % 2) {
+			$data .= "\x00";
+		}
+		$bit = unpack('n*', $data);
+		$sum = array_sum($bit);
+		while ($sum >> 16) {
+			$sum = ($sum >> 16) + ($sum & 0xffff);
+		}
+		return pack('n*', ~$sum);
+	}
+}

+ 4 - 1
api/composer.json

@@ -11,6 +11,9 @@
     "pusher/pusher-php-server": "^4.0",
     "pragmarx/google2fa": "^3.0",
     "psr/log": "^1.1",
-    "adldap2/adldap2": "^10.0"
+    "adldap2/adldap2": "^10.0",
+    "slim/slim": "4.0",
+    "slim/psr7": "^1.1",
+    "zircote/swagger-php": "^3.0"
   }
 }

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 788 - 77
api/composer.lock


+ 52 - 6
api/config/default.php

@@ -42,21 +42,33 @@ return array(
 	'embyTabName' => '',
 	'embyURL' => '',
 	'embyToken' => '',
+	'jellyfinTabURL' => '',
+	'jellyfinTabName' => '',
+	'jellyfinURL' => '',
+	'jellyfinToken' => '',
 	'plexID' => '',
 	'tautulliURL' => '',
 	'ombiURL' => '',
 	'ombiToken' => '',
 	'ombiAlias' => false,
+	'ombiFallbackUser' => '',
+	'ombiFallbackPassword' => '',
 	'ssoPlex' => false,
 	'ssoOmbi' => false,
 	'ssoTautulli' => false,
 	'sonarrURL' => '',
 	'sonarrUnmonitored' => false,
 	'sonarrToken' => '',
+	'sonarrSocksEnabled' => false,
+	'sonarrSocksAuth' => '999',
 	'lidarrURL' => '',
 	'lidarrToken' => '',
+	'lidarrSocksEnabled' => false,
+	'lidarrSocksAuth' => '999',
 	'radarrURL' => '',
 	'radarrToken' => '',
+	'radarrSocksEnabled' => false,
+	'radarrSocksAuth' => '999',
 	'couchpotatoURL' => '',
 	'couchpotatoToken' => '',
 	'sickrageURL' => '',
@@ -100,13 +112,18 @@ return array(
 	'rTorrentReverseSorting' => false,
 	'rTorrentCombine' => false,
 	'rTorrentDisableCertCheck' => false,
+	'rTorrentLimit' => '200',
+	'homepageJackettEnabled' => false,
+	'homepageJackettAuth' => '1',
+	'jackettURL' => '',
+	'jackettToken' => '',
 	'homepageCalendarEnabled' => false,
 	'homepageCalendarAuth' => '4',
 	'calendariCal' => '',
-	'homepagCustomHTMLoneEnabled' => false,
-	'homepagCustomHTMLoneAuth' => '1',
-	'homepagCustomHTMLtwoEnabled' => false,
-	'homepagCustomHTMLtwoAuth' => '1',
+	'homepageCustomHTMLoneEnabled' => false,
+	'homepageCustomHTMLoneAuth' => '1',
+	'homepageCustomHTMLtwoEnabled' => false,
+	'homepageCustomHTMLtwoAuth' => '1',
 	'homepageDelugeEnabled' => false,
 	'homepageDelugeAuth' => '1',
 	'homepageJdownloaderEnabled' => false,
@@ -115,6 +132,10 @@ return array(
 	'homepageSabnzbdAuth' => '1',
 	'homepageSonarrEnabled' => false,
 	'homepageSonarrAuth' => '1',
+	'homepageSonarrQueueEnabled' => false,
+	'homepageSonarrQueueAuth' => '1',
+	'homepageSonarrQueueCombine' => false,
+	'homepageSonarrQueueRefresh' => '60000',
 	'homepageLidarrEnabled' => false,
 	'homepageLidarrAuth' => '1',
 	'homepageCouchpotatoEnabled' => false,
@@ -123,6 +144,10 @@ return array(
 	'homepageSickrageAuth' => '1',
 	'homepageRadarrEnabled' => false,
 	'homepageRadarrAuth' => '1',
+	'homepageRadarrQueueEnabled' => false,
+	'homepageRadarrQueueAuth' => '1',
+	'homepageRadarrQueueCombine' => false,
+	'homepageRadarrQueueRefresh' => '60000',
 	'homepageTransmissionEnabled' => false,
 	'homepageTransmissionAuth' => '1',
 	'homepageqBittorrentEnabled' => false,
@@ -135,9 +160,12 @@ return array(
 	'homepagePlexAuth' => '1',
 	'homepageEmbyEnabled' => false,
 	'homepageEmbyAuth' => '1',
+	'homepageJellyfinEnabled' => false,
+	'homepageJellyfinAuth' => '1',
 	'homepageOmbiEnabled' => false,
 	'homepageOmbiAuth' => '1',
 	'homepageOmbiRequestAuth' => '1',
+	'homepageJellyfinInstead' => false,
 	'ombiLimitUser' => false,
 	'ombiRefresh' => '600000',
 	'ombiTvDefault' => 'all',
@@ -181,6 +209,12 @@ return array(
 	'homepageOrderWeatherAndAir' => '23',
 	'homepageOrderSpeedtest' => '24',
 	'homepageOrderNetdata' => '25',
+	'homepageOrderSonarrQueue' => '26',
+	'homepageOrderRadarrQueue' => '27',
+	'homepageOrderOctoprint' => '28',
+	'homepageOrderjellyfinnowplaying' => '29',
+	'homepageOrderjellyfinrecent' => '30',
+	'homepageOrderJackett' => '31',
 	'homepageShowStreamNames' => false,
 	'homepageShowStreamNamesAuth' => '1',
 	'homepageStreamRefresh' => '60000',
@@ -198,6 +232,10 @@ return array(
 	'homepageEmbyStreamsAuth' => '1',
 	'homepageEmbyRecent' => false,
 	'homepageEmbyRecentAuth' => '1',
+	'homepageJellyfinStreams' => false,
+	'homepageJellyStreamsAuth' => '1',
+	'homepageJellyfinRecent' => false,
+	'homepageJellyfinRecentAuth' => '1',
 	'calendarDefault' => 'month',
 	'calendarFirstDay' => '1',
 	'calendarStart' => '14',
@@ -205,6 +243,7 @@ return array(
 	'calendarRefresh' => '60000',
 	'calendarTimeFormat' => 'h(:mm)t',
 	'calendarLimit' => '1000',
+	'calendarLocale' => 'en',
 	'customCss' => '',
 	'customThemeCss' => '',
 	'customHTMLone' => '',
@@ -313,7 +352,7 @@ return array(
 	'monitorrCompact' => false,
 	'homepageSpeedtestEnabled' => false,
 	'homepageSpeedtestAuth' => '1',
-	'homepageSpeedtestRefresh' => '1800000', // 30 mins
+	'homepageSpeedtestRefresh' => '1800000',
 	'speedtestURL' => '',
 	'speedtestHeaderToggle' => true,
 	'speedtestHeader' => 'Speedtest',
@@ -385,8 +424,15 @@ return array(
 	'netdata6Enabled' => false,
 	'netdata7Enabled' => false,
 	'netdataCustom' => '{
-    
+
 	}',
+	'homepageOctoprintEnabled' => false,
+	'homepageOctoprintAuth' => '1',
+	'homepageOctoprintRefresh' => 10000,
+	'octoprintURL' => '',
+	'octoprintToken' => '',
+	'octoprintHeaderToggle' => true,
+	'octoprintHeader' => 'Octoprint',
 	'githubMenuLink' => true,
 	'organizrSupportMenuLink' => true,
 	'organizrDocsMenuLink' => true,

+ 7 - 44
api/functions.php

@@ -1,56 +1,19 @@
 <?php
+//error_reporting(E_ALL);
 // Set UTC timeone
 date_default_timezone_set("UTC");
 // Autoload frameworks
 require_once(__DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php');
-// Include all function files
+// Include all function and class files
 foreach (glob(__DIR__ . DIRECTORY_SEPARATOR . 'functions' . DIRECTORY_SEPARATOR . '*.php') as $filename) {
 	require_once $filename;
 }
-// Set Root Directory
-$GLOBALS['root'] = dirname(__DIR__, 1);
-$GLOBALS['uuid'] = '';
-$GLOBALS['rememberMeDays'] = '99';
-$GLOBALS['timeExecution'] = timeExecution();
-// Add in default and custom settings
-configLazy();
-// Define Logs and files after db location is set
-if (isset($GLOBALS['dbLocation'])) {
-	$GLOBALS['organizrLog'] = $GLOBALS['dbLocation'] . 'organizrLog.json';
-	$GLOBALS['organizrLoginLog'] = $GLOBALS['dbLocation'] . 'organizrLoginLog.json';
-	$GLOBALS['paths'] = array(
-		'Root Folder' => dirname(__DIR__, 2) . DIRECTORY_SEPARATOR,
-		'API Folder' => dirname(__DIR__, 1) . DIRECTORY_SEPARATOR,
-		'DB Folder' => $GLOBALS['dbLocation']
-	);
-	if (($GLOBALS['uuid'] == '')) {
-		$uuid = gen_uuid();
-		$GLOBALS['uuid'] = $uuid;
-		updateConfig(array('uuid' => $uuid));
-	}
-	if ($GLOBALS['docker']) {
-		$getBranch = file_get_contents(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'Docker.txt');
-		$getBranch = (empty($getBranch)) ? 'v2-master' : trim($getBranch);
-		$GLOBALS['branch'] = $getBranch;
-		if (!isset($GLOBALS['commit']) || $GLOBALS['commit'] == 'n/a') {
-			$GLOBALS['commit'] = $GLOBALS['quickCommit'];
-		}
-	}
-	// Oauth?
-	if ($GLOBALS['authProxyEnabled'] && $GLOBALS['authProxyHeaderName'] !== '' && $GLOBALS['authProxyWhitelist'] !== '') {
-		if (isset(getallheaders()[$GLOBALS['authProxyHeaderName']])) {
-			coookieSeconds('set', 'organizrOAuth', 'true', 20000, false);
-		}
-	}
-	//Upgrade Check
-	upgradeCheck();
+foreach (glob(__DIR__ . DIRECTORY_SEPARATOR . 'homepage' . DIRECTORY_SEPARATOR . '*.php') as $filename) {
+	require_once $filename;
+}
+foreach (glob(__DIR__ . DIRECTORY_SEPARATOR . 'classes' . DIRECTORY_SEPARATOR . '*.php') as $filename) {
+	require_once $filename;
 }
-// Reset RememberMe if zero
-$GLOBALS['rememberMeDays'] = ($GLOBALS['rememberMeDays'] == '0') ? '99' : $GLOBALS['rememberMeDays'];
-// Cookie name
-$GLOBALS['cookieName'] = $GLOBALS['uuid'] !== '' ? 'organizr_token_' . $GLOBALS['uuid'] : 'organizr_token_temp';
-// Validate Token if set and set guest if not - sets GLOBALS
-getOrganizrUserToken();
 // Include all pages files
 foreach (glob(__DIR__ . DIRECTORY_SEPARATOR . 'pages' . DIRECTORY_SEPARATOR . "*.php") as $filename) {
 	require_once $filename;

+ 97 - 67
api/functions/2fa-functions.php

@@ -1,78 +1,108 @@
 <?php
-function create2FA($type)
+
+trait TwoFAFunctions
 {
-	$result['type'] = $type;
-	switch ($type) {
-		case 'google':
-			try {
+	public function create2FA($type)
+	{
+		$result['type'] = $type;
+		switch ($type) {
+			case 'google':
+				try {
+					$google2fa = new PragmaRX\Google2FA\Google2FA();
+					$google2fa->setAllowInsecureCallToGoogleApis(true);
+					$result['secret'] = $google2fa->generateSecretKey();
+					$result['url'] = $google2fa->getQRCodeGoogleUrl(
+						$this->config['title'],
+						$this->user['username'],
+						$result['secret']
+					);
+				} catch (PragmaRX\Google2FA\Exceptions\InsecureCallException $e) {
+					$this->setAPIResponse('error', $e->getMessage(), 500);
+					return null;
+				}
+				break;
+			default:
+				$this->setAPIResponse('error', $type . ' is not an available to be setup', 404);
+				return null;
+		}
+		$this->setAPIResponse('success', '2FA code created - awaiting verification', 200);
+		return $result;
+	}
+
+	public function verify2FA($secret, $code, $type)
+	{
+		if (!$secret || $secret == '') {
+			$this->setAPIResponse('error', 'Secret was not supplied or left blank', 422);
+			return false;
+		}
+		if (!$code || $code == '') {
+			$this->setAPIResponse('error', 'Code was not supplied or left blank', 422);
+			return false;
+		}
+		if (!$type || $type == '') {
+			$this->setAPIResponse('error', 'Type was not supplied or left blank', 422);
+			return false;
+		}
+		switch ($type) {
+			case 'google':
 				$google2fa = new PragmaRX\Google2FA\Google2FA();
-				$google2fa->setAllowInsecureCallToGoogleApis(true);
-				$result['secret'] = $google2fa->generateSecretKey();
-				$result['url'] = $google2fa->getQRCodeGoogleUrl(
-					$GLOBALS['title'],
-					$GLOBALS['organizrUser']['username'],
-					$result['secret']
-				);
-			} catch (PragmaRX\Google2FA\Exceptions\InsecureCallException $e) {
+				$google2fa->setWindow(5);
+				$valid = $google2fa->verifyKey($secret, $code);
+				break;
+			default:
+				$this->setAPIResponse('error', $type . ' is not an available to be setup', 404);
 				return false;
-			}
-			break;
-		default:
+		}
+		if ($valid) {
+			$this->setAPIResponse('success', 'Verification code verified', 200);
+			return true;
+		} else {
+			$this->setAPIResponse('success', 'Verification code invalid', 401);
 			return false;
+		}
 	}
-	return $result;
-}
-
-function save2FA($secret, $type)
-{
-	try {
-		$connect = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-		]);
-		$connect->query('
-            UPDATE users SET', [
-			'auth_service' => $type . '::' . $secret
-		], '
-            WHERE id=?', $GLOBALS['organizrUser']['userID']);
-		writeLog('success', 'User Management Function - User added 2FA', $GLOBALS['organizrUser']['username']);
-		return true;
-	} catch (Dibi\Exception $e) {
-		writeLog('error', 'User Management Function - Error Adding User 2FA', $GLOBALS['organizrUser']['username']);
-		return false;
-	}
-}
 
-function verify2FA($secret, $code, $type)
-{
-	switch ($type) {
-		case 'google':
-			$google2fa = new PragmaRX\Google2FA\Google2FA();
-			$google2fa->setWindow(5);
-			$valid = $google2fa->verifyKey($secret, $code);
-			break;
-		default:
+	public function save2FA($secret, $type)
+	{
+		if (!$secret || $secret == '') {
+			$this->setAPIResponse('error', 'Secret was not supplied or left blank', 422);
 			return false;
+		}
+		if (!$type || $type == '') {
+			$this->setAPIResponse('error', 'Type was not supplied or left blank', 422);
+			return false;
+		}
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE users SET',
+					['auth_service' => $type . '::' . $secret],
+					'WHERE id = ?',
+					$this->user['userID']
+				)
+			),
+		];
+		$this->writeLog('success', 'User Management Function - User added 2FA', $this->user['username']);
+		$this->setAPIResponse('success', '2FA Added', 200);
+		return $this->processQueries($response);
 	}
-	return ($valid) ? true : false;
-}
 
-function remove2FA()
-{
-	try {
-		$connect = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-		]);
-		$connect->query('
-            UPDATE users SET', [
-			'auth_service' => 'internal'
-		], '
-            WHERE id=?', $GLOBALS['organizrUser']['userID']);
-		writeLog('success', 'User Management Function - User removed 2FA', $GLOBALS['organizrUser']['username']);
-		return true;
-	} catch (Dibi\Exception $e) {
-		writeLog('error', 'User Management Function - Error Removing User 2FA', $GLOBALS['organizrUser']['username']);
-		return false;
+	public function remove2FA()
+	{
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE users SET',
+					['auth_service' => 'internal'],
+					'WHERE id = ?',
+					$this->user['userID']
+				)
+			),
+		];
+		$this->writeLog('success', 'User Management Function - User removed 2FA', $this->user['username']);
+		$this->setAPIResponse('success', '2FA deleted', 204);
+		return $this->processQueries($response);
 	}
-}
+}

+ 3 - 1358
api/functions/api-functions.php

@@ -1,1361 +1,6 @@
-<?php /** @noinspection SqlResolve */
-/** @noinspection SqlResolve */
-/** @noinspection SqlResolve */
-/** @noinspection SqlResolve */
-/** @noinspection SyntaxError */
-function apiLogin()
-{
-	$array = array(
-		'data' => array(
-			array(
-				'name' => 'username',
-				'value' => (isset($_POST['username'])) ? $_POST['username'] : false
-			),
-			array(
-				'name' => 'password',
-				'value' => (isset($_POST['password'])) ? $_POST['password'] : false
-			),
-			array(
-				'name' => 'remember',
-				'value' => (isset($_POST['remember'])) ? true : false
-			),
-			array(
-				'name' => 'oAuth',
-				'value' => (isset($_POST['oAuth'])) ? $_POST['oAuth'] : false
-			),
-			array(
-				'name' => 'oAuthType',
-				'value' => (isset($_POST['oAuthType'])) ? $_POST['oAuthType'] : false
-			),
-			array(
-				'name' => 'tfaCode',
-				'value' => (isset($_POST['tfaCode'])) ? $_POST['tfaCode'] : false
-			),
-			array(
-				'name' => 'loginAttempts',
-				'value' => (isset($_POST['loginAttempts'])) ? $_POST['loginAttempts'] : false
-			),
-			array(
-				'name' => 'output',
-				'value' => true
-			),
-		)
-	);
-	foreach ($array['data'] as $items) {
-		foreach ($items as $key => $value) {
-			if ($key == 'name') {
-				$newKey = $value;
-			}
-			if ($key == 'value') {
-				$newValue = $value;
-			}
-			if (isset($newKey) && isset($newValue)) {
-				$$newKey = $newValue;
-			}
-		}
-	}
-	return login($array);
-}
-
-function login($array)
-{
-	// Grab username and Password from login form
-	$username = $password = $oAuth = $oAuthType = '';
-	foreach ($array['data'] as $items) {
-		foreach ($items as $key => $value) {
-			if ($key == 'name') {
-				$newKey = $value;
-			}
-			if ($key == 'value') {
-				$newValue = $value;
-			}
-			if (isset($newKey) && isset($newValue)) {
-				$$newKey = $newValue;
-			}
-		}
-	}
-	$username = (strpos($GLOBALS['authBackend'], 'emby') !== false) ? $username : strtolower($username);
-	$days = (isset($remember)) ? $GLOBALS['rememberMeDays'] : 1;
-	$oAuth = (isset($oAuth)) ? $oAuth : false;
-	$output = (isset($output)) ? $output : false;
-	$loginAttempts = (isset($loginAttempts)) ? $loginAttempts : false;
-	if ($loginAttempts > $GLOBALS['loginAttempts'] || isset($_COOKIE['lockout'])) {
-		coookieSeconds('set', 'lockout', $GLOBALS['loginLockout'], $GLOBALS['loginLockout']);
-		return 'lockout';
-	}
-	try {
-		$database = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-		]);
-		$authSuccess = false;
-		$authProxy = false;
-		if ($GLOBALS['authProxyEnabled'] && $GLOBALS['authProxyHeaderName'] !== '' && $GLOBALS['authProxyWhitelist'] !== '') {
-			if (isset(getallheaders()[$GLOBALS['authProxyHeaderName']])) {
-				$usernameHeader = isset(getallheaders()[$GLOBALS['authProxyHeaderName']]) ? getallheaders()[$GLOBALS['authProxyHeaderName']] : $username;
-				writeLog('success', 'Auth Proxy Function - Starting Verification for IP: ' . userIP() . ' for request on: ' . $_SERVER['REMOTE_ADDR'] . ' against IP/Subnet: ' . $GLOBALS['authProxyWhitelist'], $usernameHeader);
-				$whitelistRange = analyzeIP($GLOBALS['authProxyWhitelist']);
-				$from = $whitelistRange['from'];
-				$to = $whitelistRange['to'];
-				$authProxy = authProxyRangeCheck($from, $to);
-				$username = ($authProxy) ? $usernameHeader : $username;
-				if ($authProxy) {
-					writeLog('success', 'Auth Proxy Function - IP: ' . userIP() . ' has been verified', $usernameHeader);
-				} else {
-					writeLog('error', 'Auth Proxy Function - IP: ' . userIP() . ' has failed verification', $usernameHeader);
-				}
-			}
-		}
-		$function = 'plugin_auth_' . $GLOBALS['authBackend'];
-		if (!$oAuth) {
-			$result = $database->fetch('SELECT * FROM users WHERE username = ? COLLATE NOCASE OR email = ? COLLATE NOCASE', $username, $username);
-			$result['password'] = $result['password'] ?? '';
-			switch ($GLOBALS['authType']) {
-				case 'external':
-					if (function_exists($function)) {
-						$authSuccess = $function($username, $password);
-					}
-					break;
-				/** @noinspection PhpMissingBreakStatementInspection */
-				case 'both':
-					if (function_exists($function)) {
-						$authSuccess = $function($username, $password);
-					}
-				// no break
-				default: // Internal
-					if (!$authSuccess) {
-						// perform the internal authentication step
-						if (password_verify($password, $result['password'])) {
-							$authSuccess = true;
-						}
-					}
-			}
-			$authSuccess = ($authProxy) ? true : $authSuccess;
-		} else {
-			// Has oAuth Token!
-			switch ($oAuthType) {
-				case 'plex':
-					if ($GLOBALS['plexoAuth']) {
-						$tokenInfo = checkPlexToken($oAuth);
-						if ($tokenInfo) {
-							$authSuccess = array(
-								'username' => $tokenInfo['user']['username'],
-								'email' => $tokenInfo['user']['email'],
-								'image' => $tokenInfo['user']['thumb'],
-								'token' => $tokenInfo['user']['authToken']
-							);
-							coookie('set', 'oAuth', 'true', $GLOBALS['rememberMeDays']);
-							$authSuccess = ((!empty($GLOBALS['plexAdmin']) && strtolower($GLOBALS['plexAdmin']) == strtolower($tokenInfo['user']['username'])) || (!empty($GLOBALS['plexAdmin']) && strtolower($GLOBALS['plexAdmin']) == strtolower($tokenInfo['user']['email'])) || checkPlexUser($tokenInfo['user']['username'])) ? $authSuccess : false;
-						}
-					}
-					break;
-				default:
-					return ($output) ? 'No oAuthType defined' : 'error';
-					break;
-			}
-			$result = ($authSuccess) ? $database->fetch('SELECT * FROM users WHERE username = ? COLLATE NOCASE OR email = ? COLLATE NOCASE', $authSuccess['username'], $authSuccess['email']) : '';
-		}
-		if ($authSuccess) {
-			// Make sure user exists in database
-			$userExists = false;
-			$passwordMatches = ($oAuth || $authProxy) ? true : false;
-			$token = (is_array($authSuccess) && isset($authSuccess['token']) ? $authSuccess['token'] : '');
-			if ($result['username']) {
-				$userExists = true;
-				$username = $result['username'];
-				if ($passwordMatches == false) {
-					$passwordMatches = (password_verify($password, $result['password'])) ? true : false;
-				}
-			}
-			if ($userExists) {
-				//does org password need to be updated
-				if (!$passwordMatches) {
-					$database->query('
-                    	UPDATE users SET', [
-						'password' => password_hash($password, PASSWORD_BCRYPT)
-					], '
-                    	WHERE id=?', $result['id']);
-					writeLog('success', 'Login Function - User Password updated from backend', $username);
-				}
-				if ($token !== '') {
-					if ($token !== $result['plex_token']) {
-						$database->query('
-	                        UPDATE users SET', [
-							'plex_token' => $token
-						], '
-	                        WHERE id=?', $result['id']);
-						writeLog('success', 'Login Function - User Plex Token updated from backend', $username);
-					}
-				}
-				// 2FA might go here
-				if ($result['auth_service'] !== 'internal' && strpos($result['auth_service'], '::') !== false) {
-					$tfaProceed = true;
-					// Add check for local or not
-					if ($GLOBALS['ignoreTFALocal'] !== false) {
-						$tfaProceed = (isLocal()) ? false : true;
-					}
-					if ($tfaProceed) {
-						$TFA = explode('::', $result['auth_service']);
-						// Is code with login info?
-						if ($tfaCode == '') {
-							return '2FA';
-						} else {
-							if (!verify2FA($TFA[1], $tfaCode, $TFA[0])) {
-								writeLoginLog($username, 'error');
-								writeLog('error', 'Login Function - Wrong 2FA', $username);
-								return '2FA-incorrect';
-							}
-						}
-					}
-				}
-				// End 2FA
-				// authentication passed - 1) mark active and update token
-				$createToken = createToken($result['username'], $result['email'], $result['image'], $result['group'], $result['group_id'], $GLOBALS['organizrHash'], $days);
-				if ($createToken) {
-					writeLoginLog($username, 'success');
-					writeLog('success', 'Login Function - A User has logged in', $username);
-					$ssoUser = ((empty($result['email'])) ? $result['username'] : (strpos($result['email'], 'placeholder') !== false)) ? $result['username'] : $result['email'];
-					ssoCheck($ssoUser, $password, $token); //need to work on this
-					return ($output) ? array('name' => $GLOBALS['cookieName'], 'token' => (string)$createToken) : true;
-				} else {
-					return 'Token Creation Error';
-				}
-			} else {
-				// Create User
-				//ssoCheck($username, $password, $token);
-				return authRegister((is_array($authSuccess) && isset($authSuccess['username']) ? $authSuccess['username'] : $username), $password, defaultUserGroup(), (is_array($authSuccess) && isset($authSuccess['email']) ? $authSuccess['email'] : ''), $token);
-			}
-		} else {
-			// authentication failed
-			writeLoginLog($username, 'error');
-			writeLog('error', 'Login Function - Wrong Password', $username);
-			if ($loginAttempts >= $GLOBALS['loginAttempts']) {
-				coookieSeconds('set', 'lockout', $GLOBALS['loginLockout'], $GLOBALS['loginLockout']);
-				return 'lockout';
-			} else {
-				return 'mismatch';
-			}
-		}
-	} catch (Dibi\Exception $e) {
-		return $e;
-	}
-}
-
-function createDB($path, $filename)
-{
-	try {
-		if (!file_exists($path)) {
-			mkdir($path, 0777, true);
-		}
-		$createDB = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $path . $filename,
-		]);
-		// Create Users
-		$createDB->query('CREATE TABLE `users` (
-    		`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
-    		`username`	TEXT UNIQUE,
-    		`password`	TEXT,
-    		`email`	TEXT,
-    		`plex_token`	TEXT,
-            `group`	TEXT,
-            `group_id`	INTEGER,
-            `locked`	INTEGER,
-    		`image`	TEXT,
-            `register_date` DATE,
-    		`auth_service`	TEXT DEFAULT \'internal\'
-    	);');
-		// Create Tokens
-		$createDB->query('CREATE TABLE `chatroom` (
-    		`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
-    		`username`	TEXT,
-    		`gravatar`	TEXT,
-    		`uid`	TEXT,
-            `date` DATE,
-            `ip` TEXT,
-            `message` TEXT
-    	);');
-		$createDB->query('CREATE TABLE `tokens` (
-    		`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
-    		`token`	TEXT UNIQUE,
-    		`user_id`	INTEGER,
-    		`browser`	TEXT,
-    		`ip`	TEXT,
-            `created` DATE,
-            `expires` DATE
-    	);');
-		$createDB->query('CREATE TABLE `groups` (
-    		`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
-    		`group`	TEXT UNIQUE,
-            `group_id`	INTEGER,
-    		`image`	TEXT,
-            `default` INTEGER
-    	);');
-		$createDB->query('CREATE TABLE `categories` (
-    		`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
-            `order`	INTEGER,
-    		`category`	TEXT UNIQUE,
-            `category_id`	INTEGER,
-    		`image`	TEXT,
-            `default` INTEGER
-    	);');
-		// Create Tabs
-		$createDB->query('CREATE TABLE `tabs` (
-    		`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
-    		`order`	INTEGER,
-    		`category_id`	INTEGER,
-    		`name`	TEXT,
-            `url`	TEXT,
-    		`url_local`	TEXT,
-    		`default`	INTEGER,
-    		`enabled`	INTEGER,
-    		`group_id`	INTEGER,
-    		`image`	TEXT,
-    		`type`	INTEGER,
-    		`splash`	INTEGER,
-    		`ping`		INTEGER,
-    		`ping_url`	TEXT,
-    		`timeout`	INTEGER,
-    		`timeout_ms`	INTEGER,
-    		`preload`	INTEGER
-    	);');
-		// Create Options
-		$createDB->query('CREATE TABLE `options` (
-    		`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
-    		`name`	TEXT UNIQUE,
-    		`value`	TEXT
-    	);');
-		// Create Invites
-		$createDB->query('CREATE TABLE `invites` (
-    		`id`	INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
-    		`code`	TEXT UNIQUE,
-    		`date`	TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-    		`email`	TEXT,
-    		`username`	TEXT,
-    		`dateused`	TIMESTAMP,
-    		`usedby`	TEXT,
-    		`ip`	TEXT,
-    		`valid`	TEXT,
-            `type` TEXT
-    	);');
-		return true;
-	} catch (Dibi\Exception $e) {
-		return false;
-	}
-}
-
-// Upgrade Database
-function updateDB($oldVerNum = false)
-{
-	$tempLock = $GLOBALS['dbLocation'] . 'DBLOCK.txt';
-	if (!file_exists($tempLock)) {
-		touch($tempLock);
-		// Create Temp DB First
-		$migrationDB = 'tempMigration.db';
-		$pathDigest = pathinfo($GLOBALS['dbLocation'] . $GLOBALS['dbName']);
-		if (file_exists($GLOBALS['dbLocation'] . $migrationDB)) {
-			unlink($GLOBALS['dbLocation'] . $migrationDB);
-		}
-		$backupDB = $pathDigest['dirname'] . '/' . $pathDigest['filename'] . '[' . date('Y-m-d_H-i-s') . ']' . ($oldVerNum ? '[' . $oldVerNum . ']' : '') . '.bak.db';
-		copy($GLOBALS['dbLocation'] . $GLOBALS['dbName'], $backupDB);
-		$success = createDB($GLOBALS['dbLocation'], $migrationDB);
-		if ($success) {
-			try {
-				$connectOldDB = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $backupDB,
-				]);
-				$connectNewDB = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $migrationDB,
-				]);
-				$tables = $connectOldDB->fetchAll('SELECT name FROM sqlite_master WHERE type="table"');
-				foreach ($tables as $table) {
-					$data = $connectOldDB->fetchAll('SELECT * FROM ' . $table['name']);
-					writeLog('success', 'Update Function -  Grabbed Table data for Table: ' . $table['name'], 'Database');
-					foreach ($data as $row) {
-						$connectNewDB->query('INSERT into ' . $table['name'], $row);
-					}
-					writeLog('success', 'Update Function -  Wrote Table data for Table: ' . $table['name'], 'Database');
-				}
-				writeLog('success', 'Update Function -  All Table data converted - Starting Movement', 'Database');
-				$connectOldDB->disconnect();
-				$connectNewDB->disconnect();
-				// Remove Current Database
-				if (file_exists($GLOBALS['dbLocation'] . $migrationDB)) {
-					$oldFileSize = filesize($GLOBALS['dbLocation'] . $GLOBALS['dbName']);
-					$newFileSize = filesize($GLOBALS['dbLocation'] . $migrationDB);
-					if ($newFileSize > 0) {
-						writeLog('success', 'Update Function -  Table Size of new DB ok..', 'Database');
-						@unlink($GLOBALS['dbLocation'] . $GLOBALS['dbName']);
-						copy($GLOBALS['dbLocation'] . $migrationDB, $GLOBALS['dbLocation'] . $GLOBALS['dbName']);
-						@unlink($GLOBALS['dbLocation'] . $migrationDB);
-						writeLog('success', 'Update Function -  Migrated Old Info to new Database', 'Database');
-						@unlink($tempLock);
-						return true;
-					}
-				}
-				@unlink($tempLock);
-				return false;
-			} catch (Dibi\Exception $e) {
-				writeLog('error', 'Update Function -  Error [' . $e . ']', 'Database');
-				@unlink($tempLock);
-				return false;
-			}
-		}
-		@unlink($tempLock);
-		return false;
-	}
-	return false;
-}
-
-function createFirstAdmin($path, $filename, $username, $password, $email)
-{
-	try {
-		$createDB = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $path . $filename,
-		]);
-		$userInfo = [
-			'username' => $username,
-			'password' => password_hash($password, PASSWORD_BCRYPT),
-			'email' => $email,
-			'group' => 'Admin',
-			'group_id' => 0,
-			'image' => gravatar($email),
-			'register_date' => $GLOBALS['currentTime'],
-		];
-		$groupInfo0 = [
-			'group' => 'Admin',
-			'group_id' => 0,
-			'default' => false,
-			'image' => 'plugins/images/groups/admin.png',
-		];
-		$groupInfo1 = [
-			'group' => 'Co-Admin',
-			'group_id' => 1,
-			'default' => false,
-			'image' => 'plugins/images/groups/coadmin.png',
-		];
-		$groupInfo2 = [
-			'group' => 'Super User',
-			'group_id' => 2,
-			'default' => false,
-			'image' => 'plugins/images/groups/superuser.png',
-		];
-		$groupInfo3 = [
-			'group' => 'Power User',
-			'group_id' => 3,
-			'default' => false,
-			'image' => 'plugins/images/groups/poweruser.png',
-		];
-		$groupInfo4 = [
-			'group' => 'User',
-			'group_id' => 4,
-			'default' => true,
-			'image' => 'plugins/images/groups/user.png',
-		];
-		$groupInfoGuest = [
-			'group' => 'Guest',
-			'group_id' => 999,
-			'default' => false,
-			'image' => 'plugins/images/groups/guest.png',
-		];
-		$settingsInfo = [
-			'order' => 1,
-			'category_id' => 0,
-			'name' => 'Settings',
-			'url' => 'api/?v1/settings/page',
-			'default' => false,
-			'enabled' => true,
-			'group_id' => 1,
-			'image' => 'fontawesome::cog',
-			'type' => 0
-		];
-		$homepageInfo = [
-			'order' => 2,
-			'category_id' => 0,
-			'name' => 'Homepage',
-			'url' => 'api/?v1/homepage/page',
-			'default' => false,
-			'enabled' => false,
-			'group_id' => 4,
-			'image' => 'fontawesome::home',
-			'type' => 0
-		];
-		$unsortedInfo = [
-			'order' => 1,
-			'category' => 'Unsorted',
-			'category_id' => 0,
-			'image' => 'plugins/images/categories/unsorted.png',
-			'default' => true
-		];
-		$createDB->query('INSERT INTO [users]', $userInfo);
-		$createDB->query('INSERT INTO [groups]', $groupInfo0);
-		$createDB->query('INSERT INTO [groups]', $groupInfo1);
-		$createDB->query('INSERT INTO [groups]', $groupInfo2);
-		$createDB->query('INSERT INTO [groups]', $groupInfo3);
-		$createDB->query('INSERT INTO [groups]', $groupInfo4);
-		$createDB->query('INSERT INTO [groups]', $groupInfoGuest);
-		$createDB->query('INSERT INTO [tabs]', $settingsInfo);
-		$createDB->query('INSERT INTO [tabs]', $homepageInfo);
-		$createDB->query('INSERT INTO [categories]', $unsortedInfo);
-		return true;
-	} catch (Dibi\Exception $e) {
-		writeLog('error', 'Wizard Function -  Error [' . $e . ']', 'Wizard');
-		return false;
-	}
-}
-
-function defaultUserGroup()
-{
-	try {
-		$connect = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-		]);
-		$all = $connect->fetch('SELECT * FROM groups WHERE `default` = 1');
-		return $all;
-	} catch (Dibi\Exception $e) {
-		return false;
-	}
-}
-
-function defaultTabCategory()
-{
-	try {
-		$connect = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-		]);
-		$all = $connect->fetch('SELECT * FROM categories WHERE `default` = 1');
-		return $all;
-	} catch (Dibi\Exception $e) {
-		return false;
-	}
-}
-
-function getGuest()
-{
-	if (isset($GLOBALS['dbLocation'])) {
-		try {
-			$connect = new Dibi\Connection([
-				'driver' => 'sqlite3',
-				'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-			]);
-			$all = $connect->fetch('SELECT * FROM groups WHERE `group_id` = 999');
-			return $all;
-		} catch (Dibi\Exception $e) {
-			return false;
-		}
-	} else {
-		return array(
-			'group' => 'Guest',
-			'group_id' => 999,
-			'image' => 'plugins/images/groups/guest.png'
-		);
-	}
-}
-
-function adminEditGroup($array)
-{
-	switch ($array['data']['action']) {
-		case 'changeDefaultGroup':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('UPDATE groups SET `default` = 0');
-				$connect->query('
-                	UPDATE groups SET', [
-					'default' => 1
-				], '
-                	WHERE id=?', $array['data']['id']);
-				writeLog('success', 'Group Management Function -  Changed Default Group from [' . $array['data']['oldGroupName'] . '] to [' . $array['data']['newGroupName'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		case 'deleteUserGroup':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('DELETE FROM groups WHERE id = ?', $array['data']['id']);
-				writeLog('success', 'Group Management Function -  Deleted Group [' . $array['data']['groupName'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		case 'addUserGroup':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$newGroup = [
-					'group' => $array['data']['newGroupName'],
-					'group_id' => $array['data']['newGroupID'],
-					'default' => false,
-					'image' => $array['data']['newGroupImage'],
-				];
-				$connect->query('INSERT INTO [groups]', $newGroup);
-				writeLog('success', 'Group Management Function -  Added Group [' . $array['data']['newGroupName'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		case 'editUserGroup':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('
-                	UPDATE groups SET', [
-					'group' => $array['data']['groupName'],
-					'image' => $array['data']['groupImage'],
-				], '
-                	WHERE id=?', $array['data']['id']);
-				writeLog('success', 'Group Management Function -  Edited Group Info for [' . $array['data']['oldGroupName'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		default:
-			return false;
-			break;
-	}
-}
-
-function adminEditUser($array)
-{
-	switch ($array['data']['action']) {
-		case 'changeGroup':
-			if ($array['data']['newGroupID'] == 0) {
-				return false;
-			}
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('
-                	UPDATE users SET', [
-					'group' => $array['data']['newGroupName'],
-					'group_id' => $array['data']['newGroupID'],
-				], '
-                	WHERE id=?', $array['data']['id']);
-				writeLog('success', 'User Management Function - User: ' . $array['data']['username'] . '\'s group was changed from [' . $array['data']['oldGroup'] . '] to [' . $array['data']['newGroupName'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				writeLog('error', 'User Management Function - Error - User: ' . $array['data']['username'] . '\'s group was changed from [' . $array['data']['oldGroup'] . '] to [' . $array['data']['newGroupName'] . ']', $GLOBALS['organizrUser']['username']);
-				return false;
-			}
-			break;
-		case 'editUser':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				if (!usernameTakenExcept($array['data']['username'], $array['data']['email'], $array['data']['id'])) {
-					$connect->query('
-                        UPDATE users SET', [
-						'username' => $array['data']['username'],
-						'email' => $array['data']['email'],
-						'image' => gravatar($array['data']['email']),
-					], '
-                        WHERE id=?', $array['data']['id']);
-					if (!empty($array['data']['password'])) {
-						$connect->query('
-                            UPDATE users SET', [
-							'password' => password_hash($array['data']['password'], PASSWORD_BCRYPT)
-						], '
-                            WHERE id=?', $array['data']['id']);
-					}
-					writeLog('success', 'User Management Function - User: ' . $array['data']['username'] . '\'s info was changed', $GLOBALS['organizrUser']['username']);
-					return true;
-				} else {
-					return false;
-				}
-			} catch (Dibi\Exception $e) {
-				writeLog('error', 'User Management Function - Error - User: ' . $array['data']['username'] . '\'s group was changed from [' . $array['data']['oldGroup'] . '] to [' . $array['data']['newGroupName'] . ']', $GLOBALS['organizrUser']['username']);
-				return false;
-			}
-			break;
-		case 'addNewUser':
-			$defaults = defaultUserGroup();
-			if (createUser($array['data']['username'], $array['data']['password'], $defaults, $array['data']['email'])) {
-				writeLog('success', 'Create User Function - Account created for [' . $array['data']['username'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} else {
-				writeLog('error', 'Registration Function - An error occurred', $GLOBALS['organizrUser']['username']);
-				return 'username taken';
-			}
-			break;
-		case 'deleteUser':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('DELETE FROM users WHERE id = ?', $array['data']['id']);
-				writeLog('success', 'User Management Function -  Deleted User [' . $array['data']['username'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		default:
-			return false;
-			break;
-	}
-}
-
-function editTabs($array)
-{
-	switch ($array['data']['action']) {
-		case 'changeGroup':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('
-                	UPDATE tabs SET', [
-					'group_id' => $array['data']['newGroupID'],
-				], '
-                	WHERE id=?', $array['data']['id']);
-				writeLog('success', 'Tab Editor Function - Tab: ' . $array['data']['tab'] . '\'s group was changed to [' . $array['data']['newGroupName'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		case 'changeCategory':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('
-                        UPDATE tabs SET', [
-					'category_id' => $array['data']['newCategoryID'],
-				], '
-                        WHERE id=?', $array['data']['id']);
-				writeLog('success', 'Tab Editor Function - Tab: ' . $array['data']['tab'] . '\'s category was changed to [' . $array['data']['newCategoryName'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		case 'changeType':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('
-                        UPDATE tabs SET', [
-					'type' => $array['data']['newTypeID'],
-				], '
-                        WHERE id=?', $array['data']['id']);
-				writeLog('success', 'Tab Editor Function - Tab: ' . $array['data']['tab'] . '\'s type was changed to [' . $array['data']['newTypeName'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		case 'changeEnabled':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('
-                        UPDATE tabs SET', [
-					'enabled' => $array['data']['tabEnabled'],
-				], '
-                        WHERE id=?', $array['data']['id']);
-				writeLog('success', 'Tab Editor Function - Tab: ' . $array['data']['tab'] . '\'s enable status was changed to [' . $array['data']['tabEnabledWord'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		case 'changeSplash':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('
-                        UPDATE tabs SET', [
-					'splash' => $array['data']['tabSplash'],
-				], '
-                        WHERE id=?', $array['data']['id']);
-				writeLog('success', 'Tab Editor Function - Tab: ' . $array['data']['tab'] . '\'s splash status was changed to [' . $array['data']['tabSplashWord'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		case 'changePing':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('
-                        UPDATE tabs SET', [
-					'ping' => $array['data']['tabPing'],
-				], '
-                        WHERE id=?', $array['data']['id']);
-				writeLog('success', 'Tab Editor Function - Tab: ' . $array['data']['tab'] . '\'s ping status was changed to [' . $array['data']['tabPingWord'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		case 'changePreload':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('
-                        UPDATE tabs SET', [
-					'preload' => $array['data']['tabPreload'],
-				], '
-                        WHERE id=?', $array['data']['id']);
-				writeLog('success', 'Tab Editor Function - Tab: ' . $array['data']['tab'] . '\'s preload status was changed to [' . $array['data']['tabPreloadWord'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		case 'changeDefault':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('UPDATE tabs SET `default` = 0');
-				$connect->query('
-                    UPDATE tabs SET', [
-					'default' => 1
-				], '
-                    WHERE id=?', $array['data']['id']);
-				writeLog('success', 'Tab Editor Function -  Changed Default Tab to [' . $array['data']['tab'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		case 'deleteTab':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('DELETE FROM tabs WHERE id = ?', $array['data']['id']);
-				writeLog('success', 'Tab Editor Function -  Deleted Tab [' . $array['data']['tab'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		case 'editTab':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('
-                    UPDATE tabs SET', [
-					'name' => $array['data']['tabName'],
-					'url' => $array['data']['tabURL'],
-					'url_local' => $array['data']['tabLocalURL'],
-					'ping_url' => $array['data']['pingURL'],
-					'image' => $array['data']['tabImage'],
-					'timeout' => $array['data']['tabActionType'],
-					'timeout_ms' => $array['data']['tabActionTime'],
-				], '
-                    WHERE id=?', $array['data']['id']);
-				writeLog('success', 'Tab Editor Function -  Edited Tab Info for [' . $array['data']['tabName'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-		case 'changeOrder':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				foreach ($array['data']['tabs']['tab'] as $key => $value) {
-					if ($value['order'] != $value['originalOrder']) {
-						$connect->query('
-                            UPDATE tabs SET', [
-							'order' => $value['order'],
-						], '
-                            WHERE id=?', $value['id']);
-						writeLog('success', 'Tab Editor Function - ' . $value['name'] . ' Order Changed From ' . $value['order'] . ' to ' . $value['originalOrder'], $GLOBALS['organizrUser']['username']);
-					}
-				}
-				writeLog('success', 'Tab Editor Function - Tab Order Changed', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		case 'addNewTab':
-			try {
-				$default = defaultTabCategory()['category_id'];
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$newTab = [
-					'order' => $array['data']['tabOrder'],
-					'category_id' => $default,
-					'name' => $array['data']['tabName'],
-					'url' => $array['data']['tabURL'],
-					'url_local' => $array['data']['tabLocalURL'],
-					'ping_url' => $array['data']['pingURL'],
-					'default' => $array['data']['tabDefault'],
-					'enabled' => 1,
-					'group_id' => $array['data']['tabGroupID'],
-					'image' => $array['data']['tabImage'],
-					'type' => $array['data']['tabType'],
-					'timeout' => $array['data']['tabActionType'],
-					'timeout_ms' => $array['data']['tabActionTime'],
-				];
-				$connect->query('INSERT INTO [tabs]', $newTab);
-				writeLog('success', 'Tab Editor Function - Created Tab for: ' . $array['data']['tabName'], $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		default:
-			return false;
-			break;
-	}
-}
-
-function editCategories($array)
-{
-	switch ($array['data']['action']) {
-		case 'changeDefault':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('UPDATE categories SET `default` = 0');
-				$connect->query('
-                	UPDATE categories SET', [
-					'default' => 1
-				], '
-                	WHERE id=?', $array['data']['id']);
-				writeLog('success', 'Category Editor Function -  Changed Default Category from [' . $array['data']['oldCategoryName'] . '] to [' . $array['data']['newCategoryName'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		case 'deleteCategory':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('DELETE FROM categories WHERE id = ?', $array['data']['id']);
-				writeLog('success', 'Category Editor Function -  Deleted Category [' . $array['data']['category'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		case 'addNewCategory':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$newCategory = [
-					'category' => $array['data']['categoryName'],
-					'order' => $array['data']['categoryOrder'],
-					'category_id' => $array['data']['categoryID'],
-					'default' => false,
-					'image' => $array['data']['categoryImage'],
-				];
-				$connect->query('INSERT INTO [categories]', $newCategory);
-				writeLog('success', 'Category Editor Function -  Added Category [' . $array['data']['categoryName'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return $e;
-			}
-			break;
-		case 'editCategory':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$connect->query('
-                	UPDATE categories SET', [
-					'category' => $array['data']['name'],
-					'image' => $array['data']['image'],
-				], '
-                	WHERE id=?', $array['data']['id']);
-				writeLog('success', 'Category Editor Function -  Edited Category Info for [' . $array['data']['name'] . ']', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		case 'changeOrder':
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				foreach ($array['data']['categories']['category'] as $key => $value) {
-					if ($value['order'] != $value['originalOrder']) {
-						$connect->query('
-                            UPDATE categories SET', [
-							'order' => $value['order'],
-						], '
-                            WHERE id=?', $value['id']);
-						writeLog('success', 'Category Editor Function - ' . $value['name'] . ' Order Changed From ' . $value['order'] . ' to ' . $value['originalOrder'], $GLOBALS['organizrUser']['username']);
-					}
-				}
-				writeLog('success', 'Category Editor Function - Category Order Changed', $GLOBALS['organizrUser']['username']);
-				return true;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		default:
-			return false;
-			break;
-	}
-}
-
-function allUsers()
-{
-	try {
-		$connect = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-		]);
-		$users = $connect->fetchAll('SELECT * FROM users');
-		$groups = $connect->fetchAll('SELECT * FROM groups ORDER BY group_id ASC');
-		foreach ($users as $k => $v) {
-			// clear password from array
-			unset($users[$k]['password']);
-		}
-		$all['users'] = $users;
-		$all['groups'] = $groups;
-		return $all;
-	} catch (Dibi\Exception $e) {
-		return false;
-	}
-}
+<?php
 
-function usernameTaken($username, $email)
+trait ApiFunctions
 {
-	try {
-		$connect = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-		]);
-		$all = $connect->fetch('SELECT * FROM users WHERE username = ? COLLATE NOCASE OR email = ? COLLATE NOCASE', $username, $email);
-		return ($all) ? true : false;
-	} catch (Dibi\Exception $e) {
-		return false;
-	}
+	// Nothing
 }
-
-function usernameTakenExcept($username, $email, $id)
-{
-	try {
-		$connect = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-		]);
-		$all = $connect->fetch('SELECT * FROM users WHERE id IS NOT ? AND username = ? COLLATE NOCASE OR id IS NOT ? AND email = ? COLLATE NOCASE', $id, $username, $id, $email);
-		return ($all) ? true : false;
-	} catch (Dibi\Exception $e) {
-		return false;
-	}
-}
-
-function createUser($username, $password, $defaults, $email = null)
-{
-	$email = ($email) ? $email : random_ascii_string(10) . '@placeholder.eml';
-	try {
-		if (!usernameTaken($username, $email)) {
-			$createDB = new Dibi\Connection([
-				'driver' => 'sqlite3',
-				'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-			]);
-			$userInfo = [
-				'username' => $username,
-				'password' => password_hash($password, PASSWORD_BCRYPT),
-				'email' => $email,
-				'group' => $defaults['group'],
-				'group_id' => $defaults['group_id'],
-				'image' => gravatar($email),
-				'register_date' => $GLOBALS['currentTime'],
-			];
-			$createDB->query('INSERT INTO [users]', $userInfo);
-			return true;
-		} else {
-			return false;
-		}
-	} catch (Dibi\Exception $e) {
-		return false;
-	}
-}
-
-function importUsers($array)
-{
-	$imported = 0;
-	$defaults = defaultUserGroup();
-	foreach ($array as $user) {
-		$password = random_ascii_string(30);
-		if ($user['username'] !== '' && $user['email'] !== '' && $password !== '' && $defaults !== '') {
-			$newUser = createUser($user['username'], $password, $defaults, $user['email']);
-			if (!$newUser) {
-				writeLog('error', 'Import Function - Error', $user['username']);
-			} else {
-				$imported++;
-			}
-		}
-	}
-	return $imported;
-}
-
-function importUsersType($array)
-{
-	$type = $array['data']['type'];
-	if ($type !== '') {
-		switch ($type) {
-			case 'plex':
-				return importUsers(allPlexUsers(true));
-				break;
-			default:
-				return false;
-		}
-	}
-	return false;
-}
-
-function allTabs()
-{
-	if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
-		try {
-			$connect = new Dibi\Connection([
-				'driver' => 'sqlite3',
-				'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-			]);
-			$all['tabs'] = $connect->fetchAll('SELECT * FROM tabs ORDER BY `order` ASC');
-			$all['categories'] = $connect->fetchAll('SELECT * FROM categories ORDER BY `order` ASC');
-			$all['groups'] = $connect->fetchAll('SELECT * FROM groups ORDER BY `group_id` ASC');
-			return $all;
-		} catch (Dibi\Exception $e) {
-			return false;
-		}
-	}
-	return false;
-}
-
-function allGroups()
-{
-	if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
-		try {
-			$connect = new Dibi\Connection([
-				'driver' => 'sqlite3',
-				'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-			]);
-			$all = $connect->fetchAll('SELECT * FROM groups ORDER BY `group_id` ASC');
-			return $all;
-		} catch (Dibi\Exception $e) {
-			return false;
-		}
-	}
-	return false;
-}
-
-function loadTabs($type = null)
-{
-	if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php') && $type) {
-		try {
-			$connect = new Dibi\Connection([
-				'driver' => 'sqlite3',
-				'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-			]);
-			$sort = ($GLOBALS['unsortedTabs'] == 'top') ? 'DESC' : 'ASC';
-			$tabs = $connect->fetchAll('SELECT * FROM tabs WHERE `group_id` >= ? AND `enabled` = 1 ORDER BY `order` ' . $sort, $GLOBALS['organizrUser']['groupID']);
-			$categories = $connect->fetchAll('SELECT * FROM categories ORDER BY `order` ASC');
-			$all['tabs'] = $tabs;
-			foreach ($tabs as $k => $v) {
-				$v['access_url'] = (!empty($v['url_local']) && ($v['url_local'] !== null) && ($v['url_local'] !== 'null') && isLocal() && $v['type'] !== 0) ? $v['url_local'] : $v['url'];
-			}
-			$count = array_map(function ($element) {
-				return $element['category_id'];
-			}, $tabs);
-			$count = (array_count_values($count));
-			foreach ($categories as $k => $v) {
-				$v['count'] = isset($count[$v['category_id']]) ? $count[$v['category_id']] : 0;
-			}
-			$all['categories'] = $categories;
-			switch ($type) {
-				case 'categories':
-					return $all['categories'];
-				case 'tabs':
-					return $all['tabs'];
-				default:
-					return $all;
-			}
-		} catch (Dibi\Exception $e) {
-			return false;
-		}
-	}
-	return false;
-}
-
-function getActiveTokens()
-{
-	try {
-		$connect = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-		]);
-		$all = $connect->fetchAll('SELECT * FROM `tokens` WHERE `user_id` = ? AND `expires` > ?', $GLOBALS['organizrUser']['userID'], $GLOBALS['currentTime']);
-		return $all;
-	} catch (Dibi\Exception $e) {
-		return false;
-	}
-}
-
-function revokeToken($array)
-{
-	if ($array['data']['token']) {
-		try {
-			$connect = new Dibi\Connection([
-				'driver' => 'sqlite3',
-				'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-			]);
-			$connect->query('DELETE FROM tokens WHERE user_id = ? AND token = ?', $GLOBALS['organizrUser']['userID'], $array['data']['token']);
-			return true;
-		} catch (Dibi\Exception $e) {
-			return false;
-		}
-	}
-}
-
-function getSchema()
-{
-	if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
-		try {
-			$connect = new Dibi\Connection([
-				'driver' => 'sqlite3',
-				'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-			]);
-			$result = $connect->fetchAll(' SELECT name, sql FROM sqlite_master WHERE type=\'table\' ORDER BY name');
-			return $result;
-		} catch (Dibi\Exception $e) {
-			return false;
-		}
-	} else {
-		return 'DB not set yet...';
-	}
-}
-
-function youtubeSearch($query)
-{
-	if (!$query) {
-		return 'no query provided!';
-	}
-	$keys = array('AIzaSyBsdt8nLJRMTwOq5PY5A5GLZ2q7scgn01w', 'AIzaSyD-8SHutB60GCcSM8q_Fle38rJUV7ujd8k', 'AIzaSyBzOpVBT6VII-b-8gWD0MOEosGg4hyhCsQ', 'AIzaSyBKnRe1P8fpfBHgooJpmT0WOsrdUtZ4cpk');
-	$randomKeyIndex = array_rand($keys);
-	$key = $keys[$randomKeyIndex];
-	$apikey = ($GLOBALS['youtubeAPI'] !== '') ? $GLOBALS['youtubeAPI'] : $key;
-	$results = false;
-	$url = "https://www.googleapis.com/youtube/v3/search?part=snippet&q=$query+official+trailer&part=snippet&maxResults=1&type=video&videoDuration=short&key=$apikey";
-	$response = Requests::get($url);
-	if ($response->success) {
-		$results = json_decode($response->body, true);
-	}
-	return ($results) ? $results : false;
-}
-
-function scrapePage($array)
-{
-	try {
-		$url = $array['data']['url'] ?? false;
-		$type = $array['data']['type'] ?? false;
-		if (!$url) return array(
-			'result' => 'Error',
-			'data' => 'No URL'
-		);
-		$url = qualifyURL($url);
-		$data = array(
-			'full_url' => $url,
-			'drill_url' => qualifyURL($url, true)
-		);
-		$options = array('verify' => false);
-		$response = Requests::get($url, array(), $options);
-		$data['response_code'] = $response->status_code;
-		if ($response->success) {
-			$data['result'] = 'Success';
-			switch ($type) {
-				case 'html':
-					$data['data'] = html_entity_decode($response->body);
-					break;
-				case 'json':
-					$data['data'] = json_decode($response->body);
-					break;
-				default:
-					$data['data'] = $response->body;
-			}
-			return $data;
-		}
-	} catch (Requests_Exception $e) {
-		return array(
-			'result' => 'Error',
-			'data' => $e->getMessage()
-		);
-	};
-	return array('result' => 'Error');
-}
-
-function searchCityForCoordinates($array)
-{
-	try {
-		$query = $array['data']['query'] ?? false;
-		$url = qualifyURL('https://api.mapbox.com/geocoding/v5/mapbox.places/' . urlencode($query) . '.json?access_token=pk.eyJ1IjoiY2F1c2VmeCIsImEiOiJjazhyeGxqeXgwMWd2M2ZydWQ4YmdjdGlzIn0.R50iYuMewh1CnUZ7sFPdHA&limit=5&fuzzyMatch=true');
-		$options = array('verify' => false);
-		$response = Requests::get($url, array(), $options);
-		if ($response->success) {
-			return json_decode($response->body);
-		}
-	} catch (Requests_Exception $e) {
-		return array(
-			'result' => 'Error',
-			'data' => $e->getMessage()
-		);
-	};
-	return array('result' => 'Error');
-}

+ 429 - 334
api/functions/auth-functions.php

@@ -1,203 +1,285 @@
 <?php
-function authRegister($username, $password, $defaults, $email, $token = null)
+
+trait AuthFunctions
 {
-	if ($GLOBALS['authBackend'] !== '') {
-		ombiImport($GLOBALS['authBackend']);
-	}
-	ssoCheck($username, $password, $token);
-	if (createUser($username, $password, $defaults, $email)) {
-		writeLog('success', 'Registration Function - A User has registered', $username);
-		if ($GLOBALS['PHPMAILER-enabled'] && $email !== '') {
-			$emailTemplate = array(
-				'type' => 'registration',
-				'body' => $GLOBALS['PHPMAILER-emailTemplateRegisterUser'],
-				'subject' => $GLOBALS['PHPMAILER-emailTemplateRegisterUserSubject'],
-				'user' => $username,
-				'password' => null,
-				'inviteCode' => null,
-			);
-			$emailTemplate = phpmEmailTemplate($emailTemplate);
-			$sendEmail = array(
-				'to' => $email,
-				'user' => $username,
-				'subject' => $emailTemplate['subject'],
-				'body' => phpmBuildEmail($emailTemplate),
-			);
-			phpmSendEmail($sendEmail);
-		}
-		if (createToken($username, $email, gravatar($email), $defaults['group'], $defaults['group_id'], $GLOBALS['organizrHash'], $GLOBALS['rememberMeDays'])) {
-			writeLoginLog($username, 'success');
-			writeLog('success', 'Login Function - A User has logged in', $username);
-			return true;
+	public function testConnectionLdap()
+	{
+		if (!empty($this->config['authBaseDN']) && !empty($this->config['authBackendHost'])) {
+			$ad = new \Adldap\Adldap();
+			// Create a configuration array.
+			$ldapServers = explode(',', $this->config['authBackendHost']);
+			$i = 0;
+			foreach ($ldapServers as $key => $value) {
+				// Calculate parts
+				$digest = parse_url(trim($value));
+				$scheme = strtolower((isset($digest['scheme']) ? $digest['scheme'] : 'ldap'));
+				$host = (isset($digest['host']) ? $digest['host'] : (isset($digest['path']) ? $digest['path'] : ''));
+				$port = (isset($digest['port']) ? $digest['port'] : (strtolower($scheme) == 'ldap' ? 389 : 636));
+				// Reassign
+				$ldapHosts[] = $host;
+				if ($i == 0) {
+					$ldapPort = $port;
+				}
+				$i++;
+			}
+			$config = [
+				// Mandatory Configuration Options
+				'hosts' => $ldapHosts,
+				'base_dn' => $this->config['authBaseDN'],
+				'username' => (empty($this->config['ldapBindUsername'])) ? null : $this->config['ldapBindUsername'],
+				'password' => (empty($this->config['ldapBindPassword'])) ? null : $this->decrypt($this->config['ldapBindPassword']),
+				// Optional Configuration Options
+				'schema' => (($this->config['ldapType'] == '1') ? Adldap\Schemas\ActiveDirectory::class : (($this->config['ldapType'] == '2') ? Adldap\Schemas\OpenLDAP::class : Adldap\Schemas\FreeIPA::class)),
+				'account_prefix' => '',
+				'account_suffix' => '',
+				'port' => $ldapPort,
+				'follow_referrals' => false,
+				'use_ssl' => $this->config['ldapSSL'],
+				'use_tls' => $this->config['ldapTLS'],
+				'version' => 3,
+				'timeout' => 5,
+				// Custom LDAP Options
+				'custom_options' => [
+					// See: http://php.net/ldap_set_option
+					//LDAP_OPT_X_TLS_REQUIRE_CERT => LDAP_OPT_X_TLS_HARD
+				]
+			];
+			// Add a connection provider to Adldap.
+			$ad->addProvider($config);
+			try {
+				// If a successful connection is made to your server, the provider will be returned.
+				$provider = $ad->connect();
+			} catch (\Adldap\Auth\BindException $e) {
+				$detailedError = $e->getDetailedError();
+				$this->writeLog('error', 'LDAP Function - Error: ' . $detailedError->getErrorMessage(), 'SYSTEM');
+				$this->setAPIResponse('error', $detailedError->getErrorMessage(), 409);
+				return $detailedError->getErrorMessage();
+				// There was an issue binding / connecting to the server.
+			}
+			if ($provider) {
+				$this->setAPIResponse('success', 'LDAP connection successful', 200);
+				return true;
+			} else {
+				$this->setAPIResponse('error', 'Could not connect', 500);
+				return false;
+			}
+			return ($provider) ? true : false;
+		} else {
+			$this->setAPIResponse('error', 'authBaseDN and/or BackendHost not supplied', 422);
+			return false;
 		}
-	} else {
-		writeLog('error', 'Registration Function - An error occurred', $username);
-		return 'username taken';
 	}
-	return false;
-}
-
-function checkPlexToken($token = '')
-{
-	try {
-		if (($token !== '')) {
-			$url = 'https://plex.tv/users/account.json';
-			$headers = array(
-				'X-Plex-Token' => $token,
-				'Content-Type' => 'application/json',
-				'Accept' => 'application/json'
-			);
-			$response = Requests::get($url, $headers);
-			if ($response->success) {
-				return json_decode($response->body, true);
+	
+	public function testConnectionLdapLogin($array)
+	{
+		$username = $array['username'] ?? null;
+		$password = $array['password'] ?? null;
+		if (empty($username) || empty($password)) {
+			$this->setAPIResponse('error', 'Username and/or Password not supplied', 422);
+			return false;
+		}
+		if (!empty($this->config['authBaseDN']) && !empty($this->config['authBackendHost'])) {
+			$ad = new \Adldap\Adldap();
+			// Create a configuration array.
+			$ldapServers = explode(',', $this->config['authBackendHost']);
+			$i = 0;
+			foreach ($ldapServers as $key => $value) {
+				// Calculate parts
+				$digest = parse_url(trim($value));
+				$scheme = strtolower((isset($digest['scheme']) ? $digest['scheme'] : 'ldap'));
+				$host = (isset($digest['host']) ? $digest['host'] : (isset($digest['path']) ? $digest['path'] : ''));
+				$port = (isset($digest['port']) ? $digest['port'] : (strtolower($scheme) == 'ldap' ? 389 : 636));
+				// Reassign
+				$ldapHosts[] = $host;
+				$ldapServersNew[$key] = $scheme . '://' . $host . ':' . $port; // May use this later
+				if ($i == 0) {
+					$ldapPort = $port;
+				}
+				$i++;
+			}
+			$config = [
+				// Mandatory Configuration Options
+				'hosts' => $ldapHosts,
+				'base_dn' => $this->config['authBaseDN'],
+				'username' => (empty($this->config['ldapBindUsername'])) ? null : $this->config['ldapBindUsername'],
+				'password' => (empty($this->config['ldapBindPassword'])) ? null : $this->decrypt($this->config['ldapBindPassword']),
+				// Optional Configuration Options
+				'schema' => (($this->config['ldapType'] == '1') ? Adldap\Schemas\ActiveDirectory::class : (($this->config['ldapType'] == '2') ? Adldap\Schemas\OpenLDAP::class : Adldap\Schemas\FreeIPA::class)),
+				'account_prefix' => (empty($this->config['authBackendHostPrefix'])) ? null : $this->config['authBackendHostPrefix'],
+				'account_suffix' => (empty($this->config['authBackendHostSuffix'])) ? null : $this->config['authBackendHostSuffix'],
+				'port' => $ldapPort,
+				'follow_referrals' => false,
+				'use_ssl' => $this->config['ldapSSL'],
+				'use_tls' => $this->config['ldapTLS'],
+				'version' => 3,
+				'timeout' => 5,
+				// Custom LDAP Options
+				'custom_options' => [
+					// See: http://php.net/ldap_set_option
+					//LDAP_OPT_X_TLS_REQUIRE_CERT => LDAP_OPT_X_TLS_HARD
+				]
+			];
+			// Add a connection provider to Adldap.
+			$ad->addProvider($config);
+			try {
+				// If a successful connection is made to your server, the provider will be returned.
+				$provider = $ad->connect();
+				//prettyPrint($provider);
+				if ($provider->auth()->attempt($username, $password, true)) {
+					// Passed.
+					$user = $provider->search()->find($username);
+					//return $user->getFirstAttribute('cn');
+					//return $user->getGroups(['cn']);
+					//return $user;
+					//return $user->getUserPrincipalName();
+					//return $user->getGroups(['cn']);
+					$this->setAPIResponse('success', 'LDAP connection successful', 200);
+					return true;
+				} else {
+					// Failed.
+					$this->setAPIResponse('error', 'Username/Password Failed to authenticate', 401);
+					return false;
+				}
+			} catch (\Adldap\Auth\BindException $e) {
+				$detailedError = $e->getDetailedError();
+				$this->writeLog('error', 'LDAP Function - Error: ' . $detailedError->getErrorMessage(), $username);
+				$this->setAPIResponse('error', $detailedError->getErrorMessage(), 500);
+				return $detailedError->getErrorMessage();
+				// There was an issue binding / connecting to the server.
+			} catch (Adldap\Auth\UsernameRequiredException $e) {
+				$detailedError = $e->getDetailedError();
+				$this->writeLog('error', 'LDAP Function - Error: ' . $detailedError->getErrorMessage(), $username);
+				$this->setAPIResponse('error', $detailedError->getErrorMessage(), 422);
+				return $detailedError->getErrorMessage();
+				// The user didn't supply a username.
+			} catch (Adldap\Auth\PasswordRequiredException $e) {
+				$detailedError = $e->getDetailedError();
+				$this->writeLog('error', 'LDAP Function - Error: ' . $detailedError->getErrorMessage(), $username);
+				$this->setAPIResponse('error', $detailedError->getErrorMessage(), 422);
+				return $detailedError->getErrorMessage();
+				// The user didn't supply a password.
 			}
 		} else {
+			$this->setAPIResponse('error', 'authBaseDN and/or BackendHost not supplied', 422);
 			return false;
 		}
-		
-	} catch (Requests_Exception $e) {
-		writeLog('success', 'Plex Token Check Function - Error: ' . $e->getMessage(), SYSTEM);
 	}
-	return false;
-}
-
-function checkPlexUser($username)
-{
-	try {
-		if (!empty($GLOBALS['plexToken'])) {
-			$url = 'https://plex.tv/api/users';
-			$headers = array(
-				'X-Plex-Token' => $GLOBALS['plexToken'],
-			);
-			$response = Requests::get($url, $headers);
-			if ($response->success) {
-				libxml_use_internal_errors(true);
-				$userXML = simplexml_load_string($response->body);
-				if (is_array($userXML) || is_object($userXML)) {
-					$usernameLower = strtolower($username);
-					foreach ($userXML as $child) {
-						if (isset($child['username']) && strtolower($child['username']) == $usernameLower || isset($child['email']) && strtolower($child['email']) == $usernameLower) {
-							writeLog('success', 'Plex User Check - Found User on Friends List', $username);
-							$machineMatches = false;
-							if ($GLOBALS['plexStrictFriends']) {
-								foreach ($child->Server as $server) {
-									if ((string)$server['machineIdentifier'] == $GLOBALS['plexID']) {
-										$machineMatches = true;
-									}
-								}
-							} else {
-								$machineMatches = true;
-							}
-							if ($machineMatches) {
-								writeLog('success', 'Plex User Check - User Approved for Login', $username);
-								return true;
-							} else {
-								writeLog('error', 'Plex User Check - User not Approved User', $username);
-							}
-						}
-					}
+	
+	public function checkPlexToken($token = '')
+	{
+		try {
+			if (($token !== '')) {
+				$url = 'https://plex.tv/users/account.json';
+				$headers = array(
+					'X-Plex-Token' => $token,
+					'Content-Type' => 'application/json',
+					'Accept' => 'application/json'
+				);
+				$response = Requests::get($url, $headers);
+				if ($response->success) {
+					return json_decode($response->body, true);
 				}
+			} else {
+				return false;
 			}
+			
+		} catch (Requests_Exception $e) {
+			$this->writeLog('success', 'Plex Token Check Function - Error: ' . $e->getMessage(), 'SYSTEM');
 		}
 		return false;
-	} catch (Requests_Exception $e) {
-		writeLog('error', 'Plex User Check Function - Error: ' . $e->getMessage(), $username);
 	}
-	return false;
-}
-
-function allPlexUsers($newOnly = false)
-{
-	try {
-		if (!empty($GLOBALS['plexToken'])) {
-			$url = 'https://plex.tv/api/users';
-			$headers = array(
-				'X-Plex-Token' => $GLOBALS['plexToken'],
-			);
-			$response = Requests::get($url, $headers);
-			if ($response->success) {
-				libxml_use_internal_errors(true);
-				$userXML = simplexml_load_string($response->body);
-				if (is_array($userXML) || is_object($userXML)) {
-					$results = array();
-					foreach ($userXML as $child) {
-						if (((string)$child['restricted'] == '0')) {
-							if ($newOnly) {
-								$taken = usernameTaken((string)$child['username'], (string)$child['email']);
-								if (!$taken) {
-									$results[] = array(
-										'username' => (string)$child['username'],
-										'email' => (string)$child['email']
-									);
+	
+	public function checkPlexUser($username)
+	{
+		try {
+			if (!empty($this->config['plexToken'])) {
+				$url = 'https://plex.tv/api/users';
+				$headers = array(
+					'X-Plex-Token' => $this->config['plexToken'],
+				);
+				$response = Requests::get($url, $headers);
+				if ($response->success) {
+					libxml_use_internal_errors(true);
+					$userXML = simplexml_load_string($response->body);
+					if (is_array($userXML) || is_object($userXML)) {
+						$usernameLower = strtolower($username);
+						foreach ($userXML as $child) {
+							if (isset($child['username']) && strtolower($child['username']) == $usernameLower || isset($child['email']) && strtolower($child['email']) == $usernameLower) {
+								$this->writeLog('success', 'Plex User Check - Found User on Friends List', $username);
+								$machineMatches = false;
+								if ($this->config['plexStrictFriends']) {
+									foreach ($child->Server as $server) {
+										if ((string)$server['machineIdentifier'] == $this->config['plexID']) {
+											$machineMatches = true;
+										}
+									}
+								} else {
+									$machineMatches = true;
+								}
+								if ($machineMatches) {
+									$this->writeLog('success', 'Plex User Check - User Approved for Login', $username);
+									return true;
+								} else {
+									$this->writeLog('error', 'Plex User Check - User not Approved User', $username);
 								}
-							} else {
-								$results[] = array(
-									'username' => (string)$child['username'],
-									'email' => (string)$child['email'],
-								);
 							}
-							
 						}
 					}
-					return $results;
 				}
 			}
+			return false;
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Plex User Check Function - Error: ' . $e->getMessage(), $username);
 		}
 		return false;
-	} catch (Requests_Exception $e) {
-		writeLog('success', 'Plex User Function - Error: ' . $e->getMessage(), $username);
 	}
-	return false;
-}
-
-function plugin_auth_plex($username, $password)
-{
-	try {
-		$usernameLower = strtolower($username);
-		//Login User
-		$url = 'https://plex.tv/users/sign_in.json';
-		$headers = array(
-			'Accept' => 'application/json',
-			'Content-Type' => 'application/x-www-form-urlencoded',
-			'X-Plex-Product' => 'Organizr',
-			'X-Plex-Version' => '2.0',
-			'X-Plex-Client-Identifier' => $GLOBALS['uuid'],
-		);
-		$data = array(
-			'user[login]' => $username,
-			'user[password]' => $password,
-		);
-		$options = array('timeout' => 30);
-		$response = Requests::post($url, $headers, $data, $options);
-		if ($response->success) {
-			$json = json_decode($response->body, true);
-			if ((is_array($json) && isset($json['user']) && isset($json['user']['username'])) && strtolower($json['user']['username']) == $usernameLower || strtolower($json['user']['email']) == $usernameLower) {
-				//writeLog("success", $json['user']['username']." was logged into organizr using plex credentials");
-				if ((!empty($GLOBALS['plexAdmin']) && (strtolower($GLOBALS['plexAdmin']) == strtolower($json['user']['username']) || strtolower($GLOBALS['plexAdmin']) == strtolower($json['user']['email']))) || checkPlexUser($json['user']['username'])) {
-					return array(
-						'username' => $json['user']['username'],
-						'email' => $json['user']['email'],
-						'image' => $json['user']['thumb'],
-						'token' => $json['user']['authToken']
-					);
+	
+	public function plugin_auth_plex($username, $password)
+	{
+		try {
+			$usernameLower = strtolower($username);
+			//Login User
+			$url = 'https://plex.tv/users/sign_in.json';
+			$headers = array(
+				'Accept' => 'application/json',
+				'Content-Type' => 'application/x-www-form-urlencoded',
+				'X-Plex-Product' => 'Organizr',
+				'X-Plex-Version' => '2.0',
+				'X-Plex-Client-Identifier' => $this->config['uuid'],
+			);
+			$data = array(
+				'user[login]' => $username,
+				'user[password]' => $password,
+			);
+			$options = array('timeout' => 30);
+			$response = Requests::post($url, $headers, $data, $options);
+			if ($response->success) {
+				$json = json_decode($response->body, true);
+				if ((is_array($json) && isset($json['user']) && isset($json['user']['username'])) && strtolower($json['user']['username']) == $usernameLower || strtolower($json['user']['email']) == $usernameLower) {
+					if ((!empty($this->config['plexAdmin']) && (strtolower($this->config['plexAdmin']) == strtolower($json['user']['username']) || strtolower($this->config['plexAdmin']) == strtolower($json['user']['email']))) || $this->checkPlexUser($json['user']['username'])) {
+						return array(
+							'username' => $json['user']['username'],
+							'email' => $json['user']['email'],
+							'image' => $json['user']['thumb'],
+							'token' => $json['user']['authToken']
+						);
+					}
 				}
 			}
+			return false;
+		} catch (Requests_Exception $e) {
+			$this->writeLog('success', 'Plex Auth Function - Error: ' . $e->getMessage(), $username);
 		}
 		return false;
-	} catch (Requests_Exception $e) {
-		writeLog('success', 'Plex Auth Function - Error: ' . $e->getMessage(), $username);
 	}
-	return false;
-}
-
-if (function_exists('ldap_connect')) {
+	
 	// Pass credentials to LDAP backend
-	function plugin_auth_ldap($username, $password)
+	public function plugin_auth_ldap($username, $password)
 	{
-		if (!empty($GLOBALS['authBaseDN']) && !empty($GLOBALS['authBackendHost'])) {
+		if (!empty($this->config['authBaseDN']) && !empty($this->config['authBackendHost'])) {
 			$ad = new \Adldap\Adldap();
 			// Create a configuration array.
-			$ldapServers = explode(',', $GLOBALS['authBackendHost']);
+			$ldapServers = explode(',', $this->config['authBackendHost']);
 			$i = 0;
 			foreach ($ldapServers as $key => $value) {
 				// Calculate parts
@@ -216,17 +298,17 @@ if (function_exists('ldap_connect')) {
 			$config = [
 				// Mandatory Configuration Options
 				'hosts' => $ldapHosts,
-				'base_dn' => $GLOBALS['authBaseDN'],
-				'username' => (empty($GLOBALS['ldapBindUsername'])) ? null : $GLOBALS['ldapBindUsername'],
-				'password' => (empty($GLOBALS['ldapBindPassword'])) ? null : decrypt($GLOBALS['ldapBindPassword']),
+				'base_dn' => $this->config['authBaseDN'],
+				'username' => (empty($this->config['ldapBindUsername'])) ? null : $this->config['ldapBindUsername'],
+				'password' => (empty($this->config['ldapBindPassword'])) ? null : $this->decrypt($this->config['ldapBindPassword']),
 				// Optional Configuration Options
-				'schema' => (($GLOBALS['ldapType'] == '1') ? Adldap\Schemas\ActiveDirectory::class : (($GLOBALS['ldapType'] == '2') ? Adldap\Schemas\OpenLDAP::class : Adldap\Schemas\FreeIPA::class)),
-				'account_prefix' => (empty($GLOBALS['authBackendHostPrefix'])) ? null : $GLOBALS['authBackendHostPrefix'],
-				'account_suffix' => (empty($GLOBALS['authBackendHostSuffix'])) ? null : $GLOBALS['authBackendHostSuffix'],
+				'schema' => (($this->config['ldapType'] == '1') ? Adldap\Schemas\ActiveDirectory::class : (($this->config['ldapType'] == '2') ? Adldap\Schemas\OpenLDAP::class : Adldap\Schemas\FreeIPA::class)),
+				'account_prefix' => (empty($this->config['authBackendHostPrefix'])) ? null : $this->config['authBackendHostPrefix'],
+				'account_suffix' => (empty($this->config['authBackendHostSuffix'])) ? null : $this->config['authBackendHostSuffix'],
 				'port' => $ldapPort,
 				'follow_referrals' => false,
-				'use_ssl' => $GLOBALS['ldapSSL'],
-				'use_tls' => $GLOBALS['ldapTLS'],
+				'use_ssl' => $this->config['ldapSSL'],
+				'use_tls' => $this->config['ldapTLS'],
 				'version' => 3,
 				'timeout' => 5,
 				// Custom LDAP Options
@@ -242,205 +324,218 @@ if (function_exists('ldap_connect')) {
 				$provider = $ad->connect();
 				//prettyPrint($provider);
 				if ($provider->auth()->attempt($username, $password)) {
+					try {
+						// Try and get email from LDAP server
+						$accountDN = ((empty($this->config['authBackendHostPrefix'])) ? null : $this->config['authBackendHostPrefix']) . $username . ((empty($this->config['authBackendHostSuffix'])) ? null : $this->config['authBackendHostSuffix']);
+						$record = $provider->search()->findByDnOrFail($accountDN);
+						$email = $record->getFirstAttribute('mail');
+					} catch (Adldap\Models\ModelNotFoundException $e) {
+						// Record wasn't found!
+						$email = null;
+					}
 					// Passed.
-					return true;
+					return array(
+						'email' => $email
+					);
 				} else {
 					// Failed.
 					return false;
 				}
 			} catch (\Adldap\Auth\BindException $e) {
-				writeLog('error', 'LDAP Function - Error: ' . $e->getMessage(), $username);
+				$this->writeLog('error', 'LDAP Function - Error: ' . $e->getMessage(), $username);
 				// There was an issue binding / connecting to the server.
 			} catch (Adldap\Auth\UsernameRequiredException $e) {
-				writeLog('error', 'LDAP Function - Error: ' . $e->getMessage(), $username);
+				$this->writeLog('error', 'LDAP Function - Error: ' . $e->getMessage(), $username);
 				// The user didn't supply a username.
 			} catch (Adldap\Auth\PasswordRequiredException $e) {
-				writeLog('error', 'LDAP Function - Error: ' . $e->getMessage(), $username);
+				$this->writeLog('error', 'LDAP Function - Error: ' . $e->getMessage(), $username);
 				// The user didn't supply a password.
 			}
 		}
 		return false;
 	}
-} else {
+	
 	// Ldap Auth Missing Dependency
-	function plugin_auth_ldap_disabled()
+	public function plugin_auth_ldap_disabled()
 	{
 		return 'LDAP - Disabled (Dependency: php-ldap missing!)';
 	}
-}
-// Pass credentials to FTP backend
-function plugin_auth_ftp($username, $password)
-{
-	// Calculate parts
-	$digest = parse_url($GLOBALS['authBackendHost']);
-	$scheme = strtolower((isset($digest['scheme']) ? $digest['scheme'] : (function_exists('ftp_ssl_connect') ? 'ftps' : 'ftp')));
-	$host = (isset($digest['host']) ? $digest['host'] : (isset($digest['path']) ? $digest['path'] : ''));
-	$port = (isset($digest['port']) ? $digest['port'] : 21);
-	// Determine Connection Type
-	if ($scheme == 'ftps') {
-		$conn_id = ftp_ssl_connect($host, $port, 20);
-	} elseif ($scheme == 'ftp') {
-		$conn_id = ftp_connect($host, $port, 20);
-	} else {
-		return false;
-	}
-	// Check if valid FTP connection
-	if ($conn_id) {
-		// Attempt login
-		@$login_result = ftp_login($conn_id, $username, $password);
-		ftp_close($conn_id);
-		// Return Result
-		if ($login_result) {
-			return true;
+	
+	// Pass credentials to FTP backend
+	public function plugin_auth_ftp($username, $password)
+	{
+		// Calculate parts
+		$digest = parse_url($this->config['authBackendHost']);
+		$scheme = strtolower((isset($digest['scheme']) ? $digest['scheme'] : (function_exists('ftp_ssl_connect') ? 'ftps' : 'ftp')));
+		$host = (isset($digest['host']) ? $digest['host'] : (isset($digest['path']) ? $digest['path'] : ''));
+		$port = (isset($digest['port']) ? $digest['port'] : 21);
+		// Determine Connection Type
+		if ($scheme == 'ftps') {
+			$conn_id = ftp_ssl_connect($host, $port, 20);
+		} elseif ($scheme == 'ftp') {
+			$conn_id = ftp_connect($host, $port, 20);
+		} else {
+			return false;
+		}
+		// Check if valid FTP connection
+		if ($conn_id) {
+			// Attempt login
+			@$login_result = ftp_login($conn_id, $username, $password);
+			ftp_close($conn_id);
+			// Return Result
+			if ($login_result) {
+				return true;
+			} else {
+				return false;
+			}
 		} else {
 			return false;
 		}
-	} else {
-		return false;
 	}
-}
-
-// Pass credentials to Emby Backend
-function plugin_auth_emby_local($username, $password)
-{
-	try {
-		$url = qualifyURL($GLOBALS['embyURL']) . '/Users/AuthenticateByName';
-		$headers = array(
-			'Authorization' => 'Emby UserId="e8837bc1-ad67-520e-8cd2-f629e3155721", Client="None", Device="Organizr", DeviceId="xxx", Version="1.0.0.0"',
-			'Content-Type' => 'application/json',
-		);
-		$data = array(
-			'Username' => $username,
-			'pw' => $password,
-			'Password' => sha1($password),
-			'PasswordMd5' => md5($password),
-		);
-		$response = Requests::post($url, $headers, json_encode($data));
-		if ($response->success) {
-			$json = json_decode($response->body, true);
-			if (is_array($json) && isset($json['SessionInfo']) && isset($json['User']) && $json['User']['HasPassword'] == true) {
-				// Login Success - Now Logout Emby Session As We No Longer Need It
-				$headers = array(
-					'X-Emby-Token' => $json['AccessToken'],
-					'X-Mediabrowser-Token' => $json['AccessToken'],
-				);
-				$response = Requests::post(qualifyURL($GLOBALS['embyURL']) . '/Sessions/Logout', $headers, array());
-				if ($response->success) {
-					return true;
+	
+	// Pass credentials to Emby Backend
+	public function plugin_auth_emby_local($username, $password)
+	{
+		try {
+			$url = $this->qualifyURL($this->config['embyURL']) . '/Users/AuthenticateByName';
+			$headers = array(
+				'Authorization' => 'Emby UserId="e8837bc1-ad67-520e-8cd2-f629e3155721", Client="None", Device="Organizr", DeviceId="xxx", Version="1.0.0.0"',
+				'Content-Type' => 'application/json',
+			);
+			$data = array(
+				'Username' => $username,
+				'pw' => $password,
+				'Password' => sha1($password),
+				'PasswordMd5' => md5($password),
+			);
+			$response = Requests::post($url, $headers, json_encode($data));
+			if ($response->success) {
+				$json = json_decode($response->body, true);
+				if (is_array($json) && isset($json['SessionInfo']) && isset($json['User']) && $json['User']['HasPassword'] == true) {
+					// Login Success - Now Logout Emby Session As We No Longer Need It
+					$headers = array(
+						'X-Emby-Token' => $json['AccessToken'],
+						'X-Mediabrowser-Token' => $json['AccessToken'],
+					);
+					$response = Requests::post($this->qualifyURL($this->config['embyURL']) . '/Sessions/Logout', $headers, array());
+					if ($response->success) {
+						return true;
+					}
 				}
 			}
+			return false;
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Emby Local Auth Function - Error: ' . $e->getMessage(), $username);
 		}
 		return false;
-	} catch (Requests_Exception $e) {
-		writeLog('error', 'Emby Local Auth Function - Error: ' . $e->getMessage(), $username);
 	}
-	return false;
-}
-
-// Pass credentials to JellyFin Backend
-function plugin_auth_jellyfin($username, $password)
-{
-	try {
-		$url = qualifyURL($GLOBALS['embyURL']) . '/Users/authenticatebyname';
-		$headers = array(
-			'X-Emby-Authorization' => 'MediaBrowser Client="Organizr Auth", Device="Organizr", DeviceId="orgv2", Version="2.0"',
-			'Content-Type' => 'application/json',
-		);
-		$data = array(
-			'Username' => $username,
-			'Pw' => $password
-		);
-		$response = Requests::post($url, $headers, json_encode($data));
-		if ($response->success) {
-			$json = json_decode($response->body, true);
-			if (is_array($json) && isset($json['SessionInfo']) && isset($json['User']) && $json['User']['HasPassword'] == true) {
-				writeLog('success', 'JellyFin Auth Function - Found User and Logged In', $username);
-				// Login Success - Now Logout JellyFin Session As We No Longer Need It
-				$headers = array(
-					'X-Emby-Authorization' => 'MediaBrowser Client="Organizr Auth", Device="Organizr", DeviceId="orgv2", Version="2.0", Token="' . $json['AccessToken'] . '"',
-					'Content-Type' => 'application/json',
-				);
-				$response = Requests::post(qualifyURL($GLOBALS['embyURL']) . '/Sessions/Logout', $headers, array());
-				if ($response->success) {
-					return true;
+	
+	// Pass credentials to JellyFin Backend
+	public function plugin_auth_jellyfin($username, $password)
+	{
+		try {
+			$url = $this->qualifyURL($this->config['embyURL']) . '/Users/authenticatebyname';
+			$headers = array(
+				'X-Emby-Authorization' => 'MediaBrowser Client="Organizr Auth", Device="Organizr", DeviceId="orgv2", Version="2.0"',
+				'Content-Type' => 'application/json',
+			);
+			$data = array(
+				'Username' => $username,
+				'Pw' => $password
+			);
+			$response = Requests::post($url, $headers, json_encode($data));
+			if ($response->success) {
+				$json = json_decode($response->body, true);
+				if (is_array($json) && isset($json['SessionInfo']) && isset($json['User']) && $json['User']['HasPassword'] == true) {
+					$this->writeLog('success', 'JellyFin Auth Function - Found User and Logged In', $username);
+					// Login Success - Now Logout JellyFin Session As We No Longer Need It
+					$headers = array(
+						'X-Emby-Authorization' => 'MediaBrowser Client="Organizr Auth", Device="Organizr", DeviceId="orgv2", Version="2.0", Token="' . $json['AccessToken'] . '"',
+						'Content-Type' => 'application/json',
+					);
+					$response = Requests::post($this->qualifyURL($this->config['embyURL']) . '/Sessions/Logout', $headers, array());
+					if ($response->success) {
+						return true;
+					}
 				}
 			}
+			return false;
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'JellyFin Auth Function - Error: ' . $e->getMessage(), $username);
 		}
 		return false;
-	} catch (Requests_Exception $e) {
-		writeLog('error', 'JellyFin Auth Function - Error: ' . $e->getMessage(), $username);
 	}
-	return false;
-}
-
-// Authenticate against emby connect
-function plugin_auth_emby_connect($username, $password)
-{
-	// Emby disabled EmbyConnect on their API
-	// https://github.com/MediaBrowser/Emby/issues/3553
-	//return plugin_auth_emby_local($username, $password);
-	try {
-		// Get A User
-		$connectUserName = '';
-		$url = qualifyURL($GLOBALS['embyURL']) . '/Users?api_key=' . $GLOBALS['embyToken'];
-		$response = Requests::get($url);
-		if ($response->success) {
-			$json = json_decode($response->body, true);
-			if (is_array($json)) {
-				foreach ($json as $key => $value) { // Scan for this user
-					if (isset($value['ConnectUserName']) && isset($value['ConnectLinkType'])) { // Qualify as connect account
-						if (strtolower($value['ConnectUserName']) == $username || strtolower($value['Name']) == $username) {
-							$connectUserName = $value['ConnectUserName'];
-							writeLog('success', 'Emby Connect Auth Function - Found User', $username);
-							break;
+	
+	// Authenticate against emby connect
+	public function plugin_auth_emby_connect($username, $password)
+	{
+		// Emby disabled EmbyConnect on their API
+		// https://github.com/MediaBrowser/Emby/issues/3553
+		//return plugin_auth_emby_local($username, $password);
+		try {
+			// Get A User
+			$connectUserName = '';
+			$url = $this->qualifyURL($this->config['embyURL']) . '/Users?api_key=' . $this->config['embyToken'];
+			$response = Requests::get($url);
+			if ($response->success) {
+				$json = json_decode($response->body, true);
+				if (is_array($json)) {
+					foreach ($json as $key => $value) { // Scan for this user
+						if (isset($value['ConnectUserName']) && isset($value['ConnectLinkType'])) { // Qualify as connect account
+							if (strtolower($value['ConnectUserName']) == $username || strtolower($value['Name']) == $username) {
+								$connectUserName = $value['ConnectUserName'];
+								$this->writeLog('success', 'Emby Connect Auth Function - Found User', $username);
+								break;
+							}
 						}
 					}
-				}
-				if ($connectUserName) {
-					writeLog('success', 'Emby Connect Auth Function - Attempting to Login with Emby ID: ' . $connectUserName, $username);
-					$connectURL = 'https://connect.emby.media/service/user/authenticate';
-					$headers = array(
-						'Accept' => 'application/json',
-						'X-Application' => 'Organizr/2.0'
-					);
-					$data = array(
-						'nameOrEmail' => $username,
-						'rawpw' => $password,
-					);
-					$response = Requests::post($connectURL, $headers, $data);
-					if ($response->success) {
-						$json = json_decode($response->body, true);
-						if (is_array($json) && isset($json['AccessToken']) && isset($json['User']) && $json['User']['Name'] == $connectUserName) {
-							return array(
-								'email' => $json['User']['Email'],
-								//'image' => $json['User']['ImageUrl'],
-							);
+					if ($connectUserName) {
+						$this->writeLog('success', 'Emby Connect Auth Function - Attempting to Login with Emby ID: ' . $connectUserName, $username);
+						$connectURL = 'https://connect.emby.media/service/user/authenticate';
+						$headers = array(
+							'Accept' => 'application/json',
+							'X-Application' => 'Organizr/2.0'
+						);
+						$data = array(
+							'nameOrEmail' => $username,
+							'rawpw' => $password,
+						);
+						$response = Requests::post($connectURL, $headers, $data);
+						if ($response->success) {
+							$json = json_decode($response->body, true);
+							if (is_array($json) && isset($json['AccessToken']) && isset($json['User']) && $json['User']['Name'] == $connectUserName) {
+								return array(
+									'email' => $json['User']['Email'],
+									//'image' => $json['User']['ImageUrl'],
+								);
+							} else {
+								$this->writeLog('error', 'Emby Connect Auth Function - Bad Response', $username);
+							}
 						} else {
-							writeLog('error', 'Emby Connect Auth Function - Bad Response', $username);
+							$this->writeLog('error', 'Emby Connect Auth Function - 401 From Emby Connect', $username);
 						}
-					} else {
-						writeLog('error', 'Emby Connect Auth Function - 401 From Emby Connect', $username);
 					}
 				}
 			}
+			return false;
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Emby Connect Auth Function - Error: ' . $e->getMessage(), $username);
+			return false;
 		}
-		return false;
-	} catch (Requests_Exception $e) {
-		writeLog('error', 'Emby Connect Auth Function - Error: ' . $e->getMessage(), $username);
-		return false;
 	}
-}
-
-// Authenticate Against Emby Local (first) and Emby Connect
-function plugin_auth_emby_all($username, $password)
-{
-	// Emby disabled EmbyConnect on their API
-	// https://github.com/MediaBrowser/Emby/issues/3553
-	$localResult = plugin_auth_emby_local($username, $password);
-	//return $localResult;
-	if ($localResult) {
-		return $localResult;
-	} else {
-		return plugin_auth_emby_connect($username, $password);
+	
+	// Authenticate Against Emby Local (first) and Emby Connect
+	public function plugin_auth_emby_all($username, $password)
+	{
+		// Emby disabled EmbyConnect on their API
+		// https://github.com/MediaBrowser/Emby/issues/3553
+		$localResult = $this->plugin_auth_emby_local($username, $password);
+		//return $localResult;
+		if ($localResult) {
+			return $localResult;
+		} else {
+			return $this->plugin_auth_emby_connect($username, $password);
+		}
 	}
+	
 }

+ 112 - 44
api/functions/backup-functions.php

@@ -1,56 +1,124 @@
 <?php
-function fileArray($files)
+
+trait BackupFunctions
 {
-	foreach ($files as $file) {
-		if (file_exists($file)) {
-			$list[] = $file;
+	
+	public function fileArray($files)
+	{
+		foreach ($files as $file) {
+			if (file_exists($file)) {
+				$list[] = $file;
+			}
+		}
+		if (!empty($list)) {
+			return $list;
 		}
 	}
-	if (!empty($list)) {
-		return $list;
+	
+	public function deleteBackup($filename)
+	{
+		$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
+		$path = $this->config['dbLocation'] . 'backups' . DIRECTORY_SEPARATOR;
+		$filename = $path . $filename;
+		if ($ext == 'zip') {
+			if (file_exists($filename)) {
+				$this->writeLog('success', 'Backup Manager Function -  Deleted Backup [' . pathinfo($filename, PATHINFO_BASENAME) . ']', $this->user['username']);
+				$this->setAPIResponse(null, pathinfo($filename, PATHINFO_BASENAME) . ' has been deleted', null);
+				return (unlink($filename));
+			} else {
+				$this->setAPIResponse('error', 'File does not exist', 404);
+				return false;
+			}
+		} else {
+			$this->setAPIResponse('error', pathinfo($filename, PATHINFO_BASENAME) . ' is not approved to be deleted', 409);
+			return false;
+		}
 	}
-}
-
-function backupDB($type = 'config')
-{
-	$directory = $GLOBALS['dbLocation'] . 'backups' . DIRECTORY_SEPARATOR;
-	@mkdir($directory, 0770, true);
-	switch ($type) {
-		case 'config':
-			break;
-		case 'full':
-			break;
-		default:
+	
+	public function downloadBackup($filename)
+	{
+		$path = $this->config['dbLocation'] . 'backups' . DIRECTORY_SEPARATOR;
+		$filename = $path . $filename;
+		if (file_exists($filename)) {
+			header('Content-Type: application/zip');
+			header('Content-Disposition: attachment; filename="' . basename($filename) . '"');
+			header('Content-Length: ' . filesize($filename));
+			flush();
+			readfile($filename);
+			exit();
+		} else {
+			$this->setAPIResponse('error', 'File does not exist', 404);
+			return false;
+		}
+	}
+	
+	public function backupOrganizr($type = 'config')
+	{
+		$directory = $this->config['dbLocation'] . 'backups' . DIRECTORY_SEPARATOR;
+		@mkdir($directory, 0770, true);
+		switch ($type) {
+			case 'config':
+				break;
+			case 'full':
+				break;
+			default:
+			
+		}
+		$orgFiles = array(
+			'orgLog' => $this->organizrLog,
+			'loginLog' => $this->organizrLoginLog,
+			'config' => $this->userConfigPath,
+			'database' => $this->config['dbLocation'] . $this->config['dbName']
+		);
+		$files = $this->fileArray($orgFiles);
+		if (!empty($files)) {
+			$this->writeLog('success', 'BACKUP: backup process started', 'SYSTEM');
+			$zipname = $directory . 'backup[' . date('Y-m-d_H-i') . '][' . $this->version . '].zip';
+			$zip = new ZipArchive;
+			$zip->open($zipname, ZipArchive::CREATE);
+			foreach ($files as $file) {
+				$zip->addFile($file);
+			}
+			$zip->close();
+			$this->writeLog('success', 'BACKUP: backup process finished', 'SYSTEM');
+			$this->setAPIResponse('success', 'Backup has been created', 200);
+			return true;
+		} else {
+			$this->setAPIResponse('error', 'Backup creation failed', 409);
+			return false;
+		}
 		
 	}
-	$orgFiles = array(
-		'orgLog' => $GLOBALS['organizrLog'],
-		'loginLog' => $GLOBALS['organizrLoginLog'],
-		'config' => $GLOBALS['userConfigPath'],
-		'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName']
-	);
-	$files = fileArray($orgFiles);
-	if (!empty($files)) {
-		writeLog('success', 'BACKUP: backup process started', 'SYSTEM');
-		$zipname = $directory . 'backup[' . date('Y-m-d_H-i') . '][' . $GLOBALS['installedVersion'] . '].zip';
-		$zip = new ZipArchive;
-		$zip->open($zipname, ZipArchive::CREATE);
+	
+	public function getBackups()
+	{
+		$path = $this->config['dbLocation'] . 'backups' . DIRECTORY_SEPARATOR;
+		@mkdir($path, 0770, true);
+		$files = array_diff(scandir($path), array('.', '..'));
+		$fileList = [];
+		$totalFiles = 0;
+		$totalFileSize = 0;
 		foreach ($files as $file) {
-			$zip->addFile($file);
+			if (file_exists($path . $file)) {
+				$size = filesize($path . $file);
+				$totalFileSize = $totalFileSize + $size;
+				$totalFiles = $totalFiles + 1;
+				try {
+					$fileList['files'][] = [
+						'name' => $file,
+						'size' => $this->human_filesize($size, 0),
+						'date' => gmdate("Y-m-d\TH:i:s\Z", (filemtime($path . $file)))
+					];
+				} catch (Exception $e) {
+					$this->setAPIResponse('error', 'Backup list failed', 409, $e->getMessage());
+					return false;
+				}
+			}
 		}
-		$zip->close();
-		writeLog('success', 'BACKUP: backup process finished', 'SYSTEM');
-		return true;
-	} else {
-		return false;
+		$fileList['total_files'] = $totalFiles;
+		$fileList['total_size'] = $this->human_filesize($totalFileSize, 2);
+		$this->setAPIResponse('success', null, 200, array_reverse($fileList));
+		return array_reverse($fileList);
 	}
 	
 }
-
-function getBackups()
-{
-	$path = $GLOBALS['dbLocation'] . 'backups' . DIRECTORY_SEPARATOR;
-	@mkdir($path, 0770, true);
-	$files = array_diff(scandir($path), array('.', '..'));
-	return array_reverse($files);
-}

+ 2 - 141
api/functions/config-functions.php

@@ -1,145 +1,6 @@
 <?php
-// Create config file in the return syntax
-function createConfig($array, $path = null, $nest = 0)
-{
-	$path = ($path) ? $path : $GLOBALS['userConfigPath'];
-	// Define Initial Value
-	$output = array();
-	// Sort Items
-	ksort($array);
-	// Update the current config version
-	if (!$nest) {
-		// Inject Current Version
-		$output[] = "\t'configVersion' => '" . (isset($array['apply_CONFIG_VERSION']) ? $array['apply_CONFIG_VERSION'] : $GLOBALS['installedVersion']) . "'";
-	}
-	unset($array['configVersion']);
-	unset($array['apply_CONFIG_VERSION']);
-	// Process Settings
-	foreach ($array as $k => $v) {
-		$allowCommit = true;
-		$item = '';
-		switch (gettype($v)) {
-			case 'boolean':
-				$item = ($v ? 'true' : 'false');
-				break;
-			case 'integer':
-			case 'double':
-			case 'NULL':
-				$item = $v;
-				break;
-			case 'string':
-				$item = "'" . str_replace(array('\\', "'"), array('\\\\', "\'"), $v) . "'";
-				break;
-			case 'array':
-				$item = createConfig($v, false, $nest + 1);
-				break;
-			default:
-				$allowCommit = false;
-		}
-		if ($allowCommit) {
-			$output[] = str_repeat("\t", $nest + 1) . "'$k' => $item";
-		}
-	}
-	// Build output
-	$output = (!$nest ? "<?php\nreturn " : '') . "array(\n" . implode(",\n", $output) . "\n" . str_repeat("\t", $nest) . ')' . (!$nest ? ';' : '');
-	if (!$nest && $path) {
-		$pathDigest = pathinfo($path);
-		@mkdir($pathDigest['dirname'], 0770, true);
-		if (file_exists($path)) {
-			rename($path, $pathDigest['dirname'] . '/' . $pathDigest['filename'] . '.bak.php');
-		}
-		$file = fopen($path, 'w');
-		fwrite($file, $output);
-		fclose($file);
-		if (file_exists($path)) {
-			return true;
-		}
-		// writeLog("error", "config was unable to write");
-		return false;
-	} else {
-		// writeLog("success", "config was updated with new values");
-		return $output;
-	}
-}
-
-// Commit new values to the configuration
-function updateConfig($new, $current = false)
-{
-	// Get config if not supplied
-	if ($current === false) {
-		$current = loadConfig();
-	} elseif (is_string($current) && is_file($current)) {
-		$current = loadConfig($current);
-	}
-	// Inject Parts
-	foreach ($new as $k => $v) {
-		$current[$k] = $v;
-	}
-	// Return Create
-	return createConfig($current);
-}
-
-function configLazy()
-{
-	// Load config or default
-	if (file_exists($GLOBALS['userConfigPath'])) {
-		$config = fillDefaultConfig(loadConfig($GLOBALS['userConfigPath']));
-	} else {
-		$config = fillDefaultConfig(loadConfig($GLOBALS['defaultConfigPath']));
-	}
-	if (is_array($config)) {
-		defineConfig($config);
-	}
-	return $config;
-}
 
-function loadConfig($path = null)
+trait ConfigFunctions
 {
-	$path = ($path) ? $path : $GLOBALS['userConfigPath'];
-	if (!is_file($path)) {
-		return null;
-	} else {
-		return (array)call_user_func(function () use ($path) {
-			return include($path);
-		});
-	}
-}
 
-function fillDefaultConfig($array)
-{
-	$path = $GLOBALS['defaultConfigPath'];
-	if (is_string($path)) {
-		$loadedDefaults = loadConfig($path);
-	} else {
-		$loadedDefaults = $path;
-	}
-	// Include all plugin config files
-	foreach (glob(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . "*.php") as $filename) {
-		$loadedDefaults = array_merge($loadedDefaults, loadConfig($filename));
-	}
-	return (is_array($loadedDefaults) ? fillDefaultConfig_recurse($array, $loadedDefaults) : false);
-}
-
-function fillDefaultConfig_recurse($current, $defaults)
-{
-	foreach ($defaults as $k => $v) {
-		if (!isset($current[$k])) {
-			$current[$k] = $v;
-		} elseif (is_array($current[$k]) && is_array($v)) {
-			$current[$k] = fillDefaultConfig_recurse($current[$k], $v);
-		}
-	}
-	return $current;
-}
-
-function defineConfig($array, $anyCase = true, $nest_prefix = false)
-{
-	foreach ($array as $k => $v) {
-		if (is_scalar($v) && !defined($nest_prefix . $k)) {
-			$GLOBALS[$nest_prefix . $k] = $v;
-		} elseif (is_array($v)) {
-			$GLOBALS[$nest_prefix . $k] = $v;
-			defineConfig($v, $anyCase, $nest_prefix . $k . '_');
-		}
-	}
-}
+}

+ 1 - 12
api/functions/custom-class.php

@@ -1,13 +1,2 @@
 <?php
-class Requests_Auth_Digest extends Requests_Auth_Basic {
-	
-	/**
-	 * Set cURL parameters before the data is sent
-	 *
-	 * @param resource $handle cURL resource
-	 */
-	public function curl_before_send( &$handle ) {
-		curl_setopt( $handle, CURLOPT_USERPWD, $this->getAuthString() );
-		curl_setopt( $handle, CURLOPT_HTTPAUTH, CURLAUTH_ANY ); //CURLAUTH_ANY work with Wowza RESTful
-	}
-}
+/* Depreciated */

+ 1 - 571
api/functions/deluge.class.php

@@ -1,572 +1,2 @@
 <?php
-
-class deluge
-{
-	private $ch;
-	private $url;
-	private $request_id;
-	
-	public function __construct($host, $password)
-	{
-		$this->url = $host . (substr($host, -1) == "/" ? "" : "/") . "json";
-		$this->request_id = 0;
-		$this->ch = curl_init($this->url);
-		$curl_options = array(
-			CURLOPT_RETURNTRANSFER => true,
-			CURLOPT_HTTPHEADER => array("Accept: application/json", "Content-Type: application/json"),
-			CURLOPT_ENCODING => "",
-			CURLOPT_COOKIEJAR => "",
-			CURLOPT_CONNECTTIMEOUT => 10,
-			CURLOPT_TIMEOUT => 10,
-			CURLOPT_CAINFO => getCert(),
-			CURLOPT_SSL_VERIFYHOST => localURL($host) ? 0 : 2,
-			CURLOPT_SSL_VERIFYPEER => localURL($host) ? 0 : 2,
-			//CURLOPT_SSL_VERIFYPEER => false, THIS IS INSECURE!! However, deluge appears not to send intermediate certificates, so it can be necessary. Use with caution!
-		);
-		curl_setopt_array($this->ch, $curl_options);
-		//Log in and get cookies
-		$result = $this->makeRequest("auth.login", array($password));
-		if (gettype($result) == 'boolean' && $result !== true) {
-			throw new Exception("Login failed");
-		} elseif (gettype($result) == 'string') {
-			throw new Exception($result);
-		}
-	}
-	
-	/////////////////////////////
-	//
-	//webapi functions (https://github.com/idlesign/deluge-webapi)
-	//
-	/////////////////////////////
-	//ids is an array of hashes, or null for all
-	//params is an array of params:
-	//active_time, all_time_download, comment, compact, distributed_copies, download_payload_rate, eta, file_priorities, file_progress, files, hash, is_auto_managed, is_finished, is_seed, max_connections, max_download_speed, max_upload_slots, max_upload_speed, message, move_completed, move_completed_path, move_on_completed, move_on_completed_path, name, next_announce, num_files, num_peers, num_pieces, num_seeds, paused, peers, piece_length, prioritize_first_last, private, progress, queue, ratio, remove_at_ratio, save_path, seed_rank, seeding_time, seeds_peers_ratio, state, stop_at_ratio, stop_ratio, time_added, total_done, total_payload_download, total_payload_upload, total_peers, total_seeds, total_size, total_uploaded, total_wanted, tracker, tracker_host, tracker_status, trackers, upload_payload_rate
-	//or an empty array for all (or null, which returns comment, hash, name, save_path)
-	public function getTorrents($ids, $params)
-	{
-		//if (isset($this->makeRequest("webapi.get_torrents", array($ids, $params))->torrents)) {
-		return $this->makeRequest("webapi.get_torrents", array($ids, $params))->torrents;
-		//} else {
-		//throw new Exception("Login failed");
-		//}
-	}
-	
-	//metainfo is base64 encoded torrent data or a magnet url
-	//returns a torrent hash or null if the torrent wasn't aded
-	//params are torrent addition parameters like download_location?
-	public function addTorrent($metaInfo, $params)
-	{
-		return $this->makeRequest("webapi.add_torrent", array($metaInfo, $params));
-	}
-	
-	/*implemented in core
-	public function removeTorrent($hash, $removeData) {
-		return $this->makeRequest("webapi.remove_torrent", array($hash, $removeData));
-	}*/
-	public function getWebAPIVersion()
-	{
-		return $this->makeRequest("webapi.get_api_version", array());
-	}
-	
-	/////////////////////////////
-	//
-	//core functions
-	//
-	//parsed from https://web.archive.org/web/20150423162855/http://deluge-torrent.org:80/docs/master/core/rpc.html
-	//
-	/////////////////////////////
-	//Adds a torrent file to the session.
-	//Parameters:
-	//filename (string) – the filename of the torrent
-	//filedump (string) – a base64 encoded string of the torrent file contents
-	//options (dict) – the options to apply to the torrent on add
-	public function addTorrentFile($filename, $filedump, $options)
-	{
-		return $this->makeRequest("core.add_torrent_file", array($filename, $filedump, $options));
-	}
-	
-	//Adds a torrent from a magnet link.
-	//Parameters:
-	//uri (string) – the magnet link
-	//options (dict) – the options to apply to the torrent on add
-	public function addTorrentMagnet($uri, $options)
-	{
-		return $this->makeRequest("core.add_torrent_magnet", array($uri, $options));
-	}
-	
-	//Adds a torrent from a url. Deluge will attempt to fetch the torrentfrom url prior to adding it to the session.
-	//Parameters:
-	//url (string) – the url pointing to the torrent file
-	//options (dict) – the options to apply to the torrent on add
-	//headers (dict) – any optional headers to send
-	public function addTorrentUrl($url, $options, $headers)
-	{
-		return $this->makeRequest("core.add_torrent_url", array($url, $options, $headers));
-	}
-	
-	public function connectPeer($torrentId, $ip, $port)
-	{
-		return $this->makeRequest("core.connect_peer", array($torrentId, $ip, $port));
-	}
-	
-	public function createTorrent($path, $tracker, $pieceLength, $comment, $target, $webseeds, $private, $createdBy, $trackers, $addToSession)
-	{
-		return $this->makeRequest("core.create_torrent", array($path, $tracker, $pieceLength, $comment, $target, $webseeds, $private, $createdBy, $trackers, $addToSession));
-	}
-	
-	public function disablePlugin($plugin)
-	{
-		return $this->makeRequest("core.disable_plugin", array($plugin));
-	}
-	
-	public function enablePlugin($plugin)
-	{
-		return $this->makeRequest("core.enable_plugin", array($plugin));
-	}
-	
-	public function forceReannounce($torrentIds)
-	{
-		return $this->makeRequest("core.force_reannounce", array($torrentIds));
-	}
-	
-	//Forces a data recheck on torrent_ids
-	public function forceRecheck($torrentIds)
-	{
-		return $this->makeRequest("core.force_recheck", array($torrentIds));
-	}
-	
-	//Returns a list of plugins available in the core
-	public function getAvailablePlugins()
-	{
-		return $this->makeRequest("core.get_available_plugins", array());
-	}
-	
-	//Returns a dictionary of the session’s cache status.
-	public function getCacheStatus()
-	{
-		return $this->makeRequest("core.get_cache_status", array());
-	}
-	
-	//Get all the preferences as a dictionary
-	public function getConfig()
-	{
-		return $this->makeRequest("core.get_config", array());
-	}
-	
-	//Get the config value for key
-	public function getConfigValue($key)
-	{
-		return $this->makeRequest("core.get_config_value", array($key));
-	}
-	
-	//Get the config values for the entered keys
-	public function getConfigValues($keys)
-	{
-		return $this->makeRequest("core.get_config_values", array($keys));
-	}
-	
-	//Returns a list of enabled plugins in the core
-	public function getEnabledPlugins()
-	{
-		return $this->makeRequest("core.get_enabled_plugins", array());
-	}
-	
-	//returns {field: [(value,count)] }for use in sidebar(s)
-	public function getFilterTree($showZeroHits, $hideCat)
-	{
-		return $this->makeRequest("core.get_filter_tree", array($showZeroHits, $hideCat));
-	}
-	
-	//Returns the number of free bytes at path
-	public function getFreeSpace($path)
-	{
-		return $this->makeRequest("core.get_free_space", array($path));
-	}
-	
-	//Returns the libtorrent version.
-	public function getLibtorrentVersion()
-	{
-		return $this->makeRequest("core.get_libtorrent_version", array());
-	}
-	
-	//Returns the active listen port
-	public function getListenPort()
-	{
-		return $this->makeRequest("core.get_listen_port", array());
-	}
-	
-	//Returns the current number of connections
-	public function getNumConnections()
-	{
-		return $this->makeRequest("core.get_num_connections", array());
-	}
-	
-	public function getPathSize($path)
-	{
-		return $this->makeRequest("core.get_path_size", array($path));
-	}
-	
-	//Returns a list of torrent_ids in the session.
-	public function getSessionState()
-	{
-		return $this->makeRequest("core.get_session_state", array());
-	}
-	
-	//Gets the session status values for ‘keys’, these keys are takingfrom libtorrent’s session status.
-	public function getSessionStatus($keys)
-	{
-		return $this->makeRequest("core.get_session_status", array($keys));
-	}
-	
-	public function getTorrentStatus($torrentId, $keys, $diff)
-	{
-		return $this->makeRequest("core.get_torrent_status", array($torrentId, $keys, $diff));
-	}
-	
-	//returns all torrents , optionally filtered by filter_dict.
-	public function getTorrentsStatus($filterDict, $keys, $diff)
-	{
-		return $this->makeRequest("core.get_torrents_status", array($filterDict, $keys, $diff));
-	}
-	
-	public function glob($path)
-	{
-		return $this->makeRequest("core.glob", array($path));
-	}
-	
-	public function moveStorage($torrentIds, $dest)
-	{
-		return $this->makeRequest("core.move_storage", array($torrentIds, $dest));
-	}
-	
-	//Pause all torrents in the session
-	public function pauseAllTorrents()
-	{
-		return $this->makeRequest("core.pause_all_torrents", array());
-	}
-	
-	public function pauseTorrent($torrentIds)
-	{
-		return $this->makeRequest("core.pause_torrent", array($torrentIds));
-	}
-	
-	public function queueBottom($torrentIds)
-	{
-		return $this->makeRequest("core.queue_bottom", array($torrentIds));
-	}
-	
-	public function queueDown($torrentIds)
-	{
-		return $this->makeRequest("core.queue_down", array($torrentIds));
-	}
-	
-	public function queueTop($torrentIds)
-	{
-		return $this->makeRequest("core.queue_top", array($torrentIds));
-	}
-	
-	public function queueUp($torrentIds)
-	{
-		return $this->makeRequest("core.queue_up", array($torrentIds));
-	}
-	
-	//Removes a torrent from the session.
-	//Parameters:
-	//torrentId (string) – the torrentId of the torrent to remove
-	//removeData (boolean) – if True, remove the data associated with this torrent
-	public function removeTorrent($torrentId, $removeData)
-	{
-		return $this->makeRequest("core.remove_torrent", array($torrentId, $removeData));
-	}
-	
-	//Rename files in torrent_id.  Since this is an asynchronous operation bylibtorrent, watch for the TorrentFileRenamedEvent to know when thefiles have been renamed.
-	//Parameters:
-	//torrentId (string) – the torrentId to rename files
-	//filenames (((index, filename), ...)) – a list of index, filename pairs
-	public function renameFiles($torrentId, $filenames)
-	{
-		return $this->makeRequest("core.rename_files", array($torrentId, $filenames));
-	}
-	
-	//Renames the ‘folder’ to ‘new_folder’ in ‘torrent_id’.  Watch for theTorrentFolderRenamedEvent which is emitted when the folder has beenrenamed successfully.
-	//Parameters:
-	//torrentId (string) – the torrent to rename folder in
-	//folder (string) – the folder to rename
-	//newFolder (string) – the new folder name
-	public function renameFolder($torrentId, $folder, $newFolder)
-	{
-		return $this->makeRequest("core.rename_folder", array($torrentId, $folder, $newFolder));
-	}
-	
-	//Rescans the plugin folders for new plugins
-	public function rescanPlugins()
-	{
-		return $this->makeRequest("core.rescan_plugins", array());
-	}
-	
-	//Resume all torrents in the session
-	public function resumeAllTorrents()
-	{
-		return $this->makeRequest("core.resume_all_torrents", array());
-	}
-	
-	public function resumeTorrent($torrentIds)
-	{
-		return $this->makeRequest("core.resume_torrent", array($torrentIds));
-	}
-	
-	//Set the config with values from dictionary
-	public function setConfig($config)
-	{
-		return $this->makeRequest("core.set_config", array($config));
-	}
-	
-	//Sets the auto managed flag for queueing purposes
-	public function setTorrentAutoManaged($torrentId, $value)
-	{
-		return $this->makeRequest("core.set_torrent_auto_managed", array($torrentId, $value));
-	}
-	
-	//Sets a torrents file priorities
-	public function setTorrentFilePriorities($torrentId, $priorities)
-	{
-		return $this->makeRequest("core.set_torrent_file_priorities", array($torrentId, $priorities));
-	}
-	
-	//Sets a torrents max number of connections
-	public function setTorrentMaxConnections($torrentId, $value)
-	{
-		return $this->makeRequest("core.set_torrent_max_connections", array($torrentId, $value));
-	}
-	
-	//Sets a torrents max download speed
-	public function setTorrentMaxDownloadSpeed($torrentId, $value)
-	{
-		return $this->makeRequest("core.set_torrent_max_download_speed", array($torrentId, $value));
-	}
-	
-	//Sets a torrents max number of upload slots
-	public function setTorrentMaxUploadSlots($torrentId, $value)
-	{
-		return $this->makeRequest("core.set_torrent_max_upload_slots", array($torrentId, $value));
-	}
-	
-	//Sets a torrents max upload speed
-	public function setTorrentMaxUploadSpeed($torrentId, $value)
-	{
-		return $this->makeRequest("core.set_torrent_max_upload_speed", array($torrentId, $value));
-	}
-	
-	//Sets the torrent to be moved when completed
-	public function setTorrentMoveCompleted($torrentId, $value)
-	{
-		return $this->makeRequest("core.set_torrent_move_completed", array($torrentId, $value));
-	}
-	
-	//Sets the path for the torrent to be moved when completed
-	public function setTorrentMoveCompletedPath($torrentId, $value)
-	{
-		return $this->makeRequest("core.set_torrent_move_completed_path", array($torrentId, $value));
-	}
-	
-	//Sets the torrent options for torrent_ids
-	public function setTorrentOptions($torrentIds, $options)
-	{
-		return $this->makeRequest("core.set_torrent_options", array($torrentIds, $options));
-	}
-	
-	//Sets a higher priority to the first and last pieces
-	public function setTorrentPrioritizeFirstLast($torrentId, $value)
-	{
-		return $this->makeRequest("core.set_torrent_prioritize_first_last", array($torrentId, $value));
-	}
-	
-	//Sets the torrent to be removed at ‘stop_ratio’
-	public function setTorrentRemoveAtRatio($torrentId, $value)
-	{
-		return $this->makeRequest("core.set_torrent_remove_at_ratio", array($torrentId, $value));
-	}
-	
-	//Sets the torrent to stop at ‘stop_ratio’
-	public function setTorrentStopAtRatio($torrentId, $value)
-	{
-		return $this->makeRequest("core.set_torrent_stop_at_ratio", array($torrentId, $value));
-	}
-	
-	//Sets the ratio when to stop a torrent if ‘stop_at_ratio’ is set
-	public function setTorrentStopRatio($torrentId, $value)
-	{
-		return $this->makeRequest("core.set_torrent_stop_ratio", array($torrentId, $value));
-	}
-	
-	//Sets a torrents tracker list.  trackers will be [{“url”, “tier”}]
-	public function setTorrentTrackers($torrentId, $trackers)
-	{
-		return $this->makeRequest("core.set_torrent_trackers", array($torrentId, $trackers));
-	}
-	
-	//Checks if the active port is open
-	public function testListenPort()
-	{
-		return $this->makeRequest("core.test_listen_port", array());
-	}
-	
-	public function uploadPlugin($filename, $filedump)
-	{
-		return $this->makeRequest("core.upload_plugin", array($filename, $filedump));
-	}
-	
-	//Returns a list of the exported methods.
-	public function getMethodList()
-	{
-		return $this->makeRequest("daemon.get_method_list", array());
-	}
-	
-	//Returns some info from the daemon.
-	public function info()
-	{
-		return $this->makeRequest("daemon.info", array());
-	}
-	
-	public function shutdown(...$params)
-	{
-		return $this->makeRequest("daemon.shutdown", $params);
-	}
-	
-	/////////////////////////////
-	//
-	//web ui functions
-	//
-	//parsed from https://web.archive.org/web/20150423143401/http://deluge-torrent.org:80/docs/master/modules/ui/web/json_api.html#module-deluge.ui.web.json_api
-	//
-	/////////////////////////////
-	//Parameters:
-	//host (string) – the hostname
-	//port (int) – the port
-	//username (string) – the username to login as
-	//password (string) – the password to login with
-	public function addHost($host, $port, $username, $password)
-	{
-		return $this->makeRequest("web.add_host", array($host, $port, $username, $password));
-	}
-	
-	//Usage
-	public function addTorrents($torrents)
-	{
-		return $this->makeRequest("web.add_torrents", array($torrents));
-	}
-	
-	public function connect($hostId)
-	{
-		return $this->makeRequest("web.connect", array($hostId));
-	}
-	
-	public function connected()
-	{
-		return $this->makeRequest("web.connected", array());
-	}
-	
-	public function deregisterEventListener($event)
-	{
-		return $this->makeRequest("web.deregister_event_listener", array($event));
-	}
-	
-	public function disconnect()
-	{
-		return $this->makeRequest("web.disconnect", array());
-	}
-	
-	public function downloadTorrentFromUrl($url, $cookie)
-	{
-		return $this->makeRequest("web.download_torrent_from_url", array($url, $cookie));
-	}
-	
-	/* in core
-	public function getConfig() {
-		return $this->makeRequest("web.get_config", array());
-	}*/
-	public function getEvents()
-	{
-		return $this->makeRequest("web.get_events", array());
-	}
-	
-	public function getHost($hostId)
-	{
-		return $this->makeRequest("web.get_host", array($hostId));
-	}
-	
-	public function getHostStatus($hostId)
-	{
-		return $this->makeRequest("web.get_host_status", array($hostId));
-	}
-	
-	public function getHosts()
-	{
-		return $this->makeRequest("web.get_hosts", array());
-	}
-	
-	public function getTorrentFiles($torrentId)
-	{
-		return $this->makeRequest("web.get_torrent_files", array($torrentId));
-	}
-	
-	public function getTorrentInfo($filename)
-	{
-		return $this->makeRequest("web.get_torrent_info", array($filename));
-	}
-	
-	public function registerEventListener($event)
-	{
-		return $this->makeRequest("web.register_event_listener", array($event));
-	}
-	
-	public function removeHost($connectionId)
-	{
-		return $this->makeRequest("web.remove_host", array($connectionId));
-	}
-	
-	/*in core
-	public function setConfig($config) {
-		return $this->makeRequest("web.set_config", array($config));
-	}*/
-	public function startDaemon($port)
-	{
-		return $this->makeRequest("web.start_daemon", array($port));
-	}
-	
-	public function stopDaemon($hostId)
-	{
-		return $this->makeRequest("web.stop_daemon", array($hostId));
-	}
-	
-	//Parameters:
-	//keys (list) – the information about the torrents to gather
-	//filterDict (dictionary) – the filters to apply when selecting torrents.
-	public function updateUi($keys, $filterDict)
-	{
-		return $this->makeRequest("web.update_ui", array($keys, $filterDict));
-	}
-	
-	private function makeRequest($method, $params)
-	{
-		$post_data = array("id" => $this->request_id, "method" => $method, "params" => $params);
-		curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($post_data));
-		$result = curl_exec($this->ch);
-		if ($result === false) {
-			throw new Exception("Could not log in due to curl error (no. " . curl_errno($this->ch) . "): " . curl_error($this->ch));
-		}
-		$http_code = curl_getinfo($this->ch, CURLINFO_HTTP_CODE);
-		if ($http_code != 200) {
-			throw new Exception("Request for method $method returned http code $http_code");
-		}
-		$result = json_decode($result);
-		if (!is_null($result->error)) {
-			throw new Exception("Method request returned an error (no. " . $result->error->code . "): " . $result->error->message);
-		}
-		if ($result->id != $this->request_id) {
-			throw new Exception("Response id did not match request id");
-		}
-		$this->request_id++;
-		return $result->result;
-	}
-}
+/* Depreciated */

+ 71 - 3237
api/functions/homepage-connect-functions.php

@@ -1,3259 +1,93 @@
 <?php
 /** @noinspection PhpUndefinedFieldInspection */
-function homepageConnect($array)
-{
-	switch ($array['data']['action']) {
-		case 'getPlexStreams':
-			return (qualifyRequest($GLOBALS['homepagePlexStreamsAuth'])) ? plexConnect('streams') : false;
-			break;
-		case 'getPlexRecent':
-			return (qualifyRequest($GLOBALS['homepagePlexRecentAuth'])) ? plexConnect('recent') : false;
-			break;
-		case 'getPlexMetadata':
-			return (qualifyRequest($GLOBALS['homepagePlexAuth'])) ? plexConnect('metadata', $array['data']['key']) : false;
-			break;
-		case 'getPlexSearch':
-			return (qualifyRequest($GLOBALS['mediaSearchAuth'])) ? plexConnect('search', $array['data']['query']) : false;
-			break;
-		case 'getPlexPlaylists':
-			return (qualifyRequest($GLOBALS['homepagePlexPlaylistAuth'])) ? getPlexPlaylists() : false;
-			break;
-		case 'getEmbyStreams':
-			return (qualifyRequest($GLOBALS['homepageEmbyStreamsAuth']) && $GLOBALS['homepageEmbyEnabled']) ? embyConnect('streams') : false;
-			break;
-		case 'getEmbyRecent':
-			return (qualifyRequest($GLOBALS['homepageEmbyRecentAuth']) && $GLOBALS['homepageEmbyEnabled']) ? embyConnect('recent') : false;
-			break;
-		case 'getEmbyMetadata':
-			return (qualifyRequest($GLOBALS['homepageEmbyAuth']) && $GLOBALS['homepageEmbyEnabled']) ? embyConnect('metadata', $array['data']['key'], true) : false;
-			break;
-		case 'getJdownloader':
-			return jdownloaderConnect();
-			break;
-		case 'getSabnzbd':
-			return sabnzbdConnect();
-			break;
-		case 'getNzbget':
-			return nzbgetConnect();
-			break;
-		case 'getTransmission':
-			return transmissionConnect();
-			break;
-		case 'getqBittorrent':
-			return qBittorrentConnect();
-			break;
-		case 'getrTorrent':
-			return rTorrentConnect();
-			break;
-		case 'getDeluge':
-			return delugeConnect();
-			break;
-		case 'getCalendar':
-			return getCalendar();
-			break;
-		case 'getRequests':
-			return getOmbiRequests('both', $GLOBALS['ombiLimit']);
-			break;
-		case 'getHealthChecks':
-			return (qualifyRequest($GLOBALS['homepageHealthChecksAuth'])) ? getHealthChecks($array['data']['tags']) : false;
-			break;
-		case 'getUnifi':
-			return unifiConnect();
-			break;
-		case 'getTautulli':
-			return getTautulli();
-		case 'getPihole':
-			return getPihole();
-			break;
-		case 'getMonitorr':
-			return getMonitorr();
-			break;
-		case 'getWeatherAndAir':
-			return getWeatherAndAir();
-			break;
-		case 'getSpeedtest':
-			return getSpeedtest();
-			break;
-		case 'getNetdata':
-			return getNetdata();
-			break;
-		default:
-			# code...
-			break;
-	}
-	return false;
-}
-
-function healthChecksTags($tags)
-{
-	$return = '?tag=';
-	if (!$tags) {
-		return '';
-	} elseif ($tags == '*') {
-		return '';
-	} else {
-		if (strpos($tags, ',') !== false) {
-			$list = explode(',', $tags);
-			return $return . implode("&tag=", $list);
-		} else {
-			return $return . $tags;
-		}
-	}
-}
-
-function getWeatherAndAir()
-{
-	if ($GLOBALS['homepageWeatherAndAirEnabled'] && !empty($GLOBALS['homepageWeatherAndAirLatitude']) && !empty($GLOBALS['homepageWeatherAndAirLongitude']) && qualifyRequest($GLOBALS['homepageWeatherAndAirAuth'])) {
-		$api['content'] = array(
-			'weather' => false,
-			'air' => false,
-			'pollen' => false
-		);
-		$apiURL = qualifyURL('https://api.breezometer.com/');
-		$info = '&lat=' . $GLOBALS['homepageWeatherAndAirLatitude'] . '&lon=' . $GLOBALS['homepageWeatherAndAirLongitude'] . '&units=' . $GLOBALS['homepageWeatherAndAirUnits'] . '&key=b7401295888443538a7ebe04719c8394';
-		try {
-			$headers = array();
-			$options = array();
-			if ($GLOBALS['homepageWeatherAndAirWeatherEnabled']) {
-				$endpoint = '/weather/v1/forecast/hourly?hours=120&metadata=true';
-				$response = Requests::get($apiURL . $endpoint . $info, $headers, $options);
-				if ($response->success) {
-					$apiData = json_decode($response->body, true);
-					$api['content']['weather'] = ($apiData['error'] === null) ? $apiData : false;
-					unset($apiData);
-				}
-			}
-			if ($GLOBALS['homepageWeatherAndAirAirQualityEnabled']) {
-				$endpoint = '/air-quality/v2/current-conditions?features=breezometer_aqi,local_aqi,health_recommendations,sources_and_effects,dominant_pollutant_concentrations,pollutants_concentrations,pollutants_aqi_information&metadata=true';
-				$response = Requests::get($apiURL . $endpoint . $info, $headers, $options);
-				if ($response->success) {
-					$apiData = json_decode($response->body, true);
-					$api['content']['air'] = ($apiData['error'] === null) ? $apiData : false;
-					unset($apiData);
-				}
-			}
-			if ($GLOBALS['homepageWeatherAndAirPollenEnabled']) {
-				$endpoint = '/pollen/v2/forecast/daily/?features=plants_information,types_information&days=1&metadata=true';
-				$response = Requests::get($apiURL . $endpoint . $info, $headers, $options);
-				if ($response->success) {
-					$apiData = json_decode($response->body, true);
-					$api['content']['pollen'] = ($apiData['error'] === null) ? $apiData : false;
-					unset($apiData);
-				}
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'Weather And Air Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-		$api['content'] = isset($api['content']) ? $api['content'] : false;
-		return $api;
-	}
-	return false;
-}
-
-function getHealthChecks($tags = null)
-{
-	if ($GLOBALS['homepageHealthChecksEnabled'] && !empty($GLOBALS['healthChecksToken']) && !empty($GLOBALS['healthChecksURL']) && qualifyRequest($GLOBALS['homepageHealthChecksAuth'])) {
-		$api['content']['checks'] = array();
-		$tags = ($tags) ? healthChecksTags($tags) : '';
-		$healthChecks = explode(',', $GLOBALS['healthChecksToken']);
-		foreach ($healthChecks as $token) {
-			$url = qualifyURL($GLOBALS['healthChecksURL']) . '/' . $tags;
-			try {
-				$headers = array('X-Api-Key' => $token);
-				$options = (localURL($url)) ? array('verify' => false) : array();
-				$response = Requests::get($url, $headers, $options);
-				if ($response->success) {
-					$healthResults = json_decode($response->body, true);
-					$api['content']['checks'] = array_merge($api['content']['checks'], $healthResults['checks']);
-				}
-			} catch (Requests_Exception $e) {
-				writeLog('error', 'HealthChecks Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-			};
-		}
-		usort($api['content']['checks'], function ($a, $b) {
-			return $a['status'] <=> $b['status'];
-		});
-		$api['content']['checks'] = isset($api['content']['checks']) ? $api['content']['checks'] : false;
-		return $api;
-	}
-	return false;
-}
-
-function getPihole()
-{
-	if ($GLOBALS['homepagePiholeEnabled'] && !empty($GLOBALS['piholeURL']) && qualifyRequest($GLOBALS['homepagePiholeAuth'])) {
-		$api = array();
-		$urls = explode(',', $GLOBALS['piholeURL']);
-		foreach ($urls as $url) {
-			$url = $url . '/api.php?';
-			try {
-				$response = Requests::get($url, [], []);
-				if ($response->success) {
-					$piholeResults = json_decode($response->body, true);
-					$ip = qualifyURL($url, true)['host'];
-					$api['data'][$ip] = $piholeResults;
-				}
-			} catch (Requests_Exception $e) {
-				writeLog('error', 'Pi-hole Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-			};
-		}
-		$api['options']['combine'] = $GLOBALS['homepagePiholeCombine'];
-		$api['options']['title'] = $GLOBALS['piholeHeaderToggle'];
-		$api = isset($api) ? $api : false;
-		return $api;
-	}
-	return false;
-}
-
-function streamType($value)
-{
-	if ($value == "transcode" || $value == "Transcode") {
-		return "Transcode";
-	} elseif ($value == "copy" || $value == "DirectStream") {
-		return "Direct Stream";
-	} elseif ($value == "directplay" || $value == "DirectPlay") {
-		return "Direct Play";
-	} else {
-		return "Direct Play";
-	}
-}
-
-function resolveEmbyItem($itemDetails)
-{
-	/*
-	// Grab Each item info from Emby (extra call)
-	$id = isset($itemDetails['NowPlayingItem']['Id']) ? $itemDetails['NowPlayingItem']['Id'] : $itemDetails['Id'];
-	$url = qualifyURL($GLOBALS['embyURL']);
-	$url = $url . '/Items?Ids=' . $id . '&api_key=' . $GLOBALS['embyToken'] . '&Fields=Overview,People,Genres,CriticRating,Studios,Taglines';
-	try {
-		$options = (localURL($url)) ? array('verify' => false) : array();
-		$response = Requests::get($url, array(), $options);
-		if ($response->success) {
-			$item = json_decode($response->body, true)['Items'][0];
-		}
-	} catch (Requests_Exception $e) {
-		return false;
-	};
-	*/
-	$item = isset($itemDetails['NowPlayingItem']['Id']) ? $itemDetails['NowPlayingItem'] : $itemDetails;
-	// Static Height & Width
-	$height = getCacheImageSize('h');
-	$width = getCacheImageSize('w');
-	$nowPlayingHeight = getCacheImageSize('nph');
-	$nowPlayingWidth = getCacheImageSize('npw');
-	$actorHeight = 450;
-	$actorWidth = 300;
-	// Cache Directories
-	$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
-	$cacheDirectoryWeb = 'plugins/images/cache/';
-	// Types
-	$embyItem['array-item'] = $item;
-	$embyItem['array-itemdetails'] = $itemDetails;
-	switch (@$item['Type']) {
-		case 'Series':
-			$embyItem['type'] = 'tv';
-			$embyItem['title'] = $item['Name'];
-			$embyItem['secondaryTitle'] = '';
-			$embyItem['summary'] = '';
-			$embyItem['ratingKey'] = $item['Id'];
-			$embyItem['thumb'] = $item['Id'];
-			$embyItem['key'] = $item['Id'] . "-list";
-			$embyItem['nowPlayingThumb'] = $item['Id'];
-			$embyItem['nowPlayingKey'] = $item['Id'] . "-np";
-			$embyItem['metadataKey'] = $item['Id'];
-			$embyItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? 'Thumb' : (isset($item['BackdropImageTags'][0]) ? 'Backdrop' : '');
-			break;
-		case 'Episode':
-			$embyItem['type'] = 'tv';
-			$embyItem['title'] = $item['SeriesName'];
-			$embyItem['secondaryTitle'] = '';
-			$embyItem['summary'] = '';
-			$embyItem['ratingKey'] = $item['Id'];
-			$embyItem['thumb'] = (isset($item['SeriesId']) ? $item['SeriesId'] : $item['Id']);
-			$embyItem['key'] = (isset($item['SeriesId']) ? $item['SeriesId'] : $item['Id']) . "-list";
-			$embyItem['nowPlayingThumb'] = isset($item['ParentThumbItemId']) ? $item['ParentThumbItemId'] : (isset($item['ParentBackdropItemId']) ? $item['ParentBackdropItemId'] : false);
-			$embyItem['nowPlayingKey'] = isset($item['ParentThumbItemId']) ? $item['ParentThumbItemId'] . '-np' : (isset($item['ParentBackdropItemId']) ? $item['ParentBackdropItemId'] . '-np' : false);
-			$embyItem['metadataKey'] = $item['Id'];
-			$embyItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? 'Thumb' : (isset($item['ParentBackdropImageTags'][0]) ? 'Backdrop' : '');
-			$embyItem['nowPlayingTitle'] = @$item['SeriesName'] . ' - ' . @$item['Name'];
-			$embyItem['nowPlayingBottom'] = 'S' . @$item['ParentIndexNumber'] . ' · E' . @$item['IndexNumber'];
-			break;
-		case 'MusicAlbum':
-		case 'Audio':
-			$embyItem['type'] = 'music';
-			$embyItem['title'] = $item['Name'];
-			$embyItem['secondaryTitle'] = '';
-			$embyItem['summary'] = '';
-			$embyItem['ratingKey'] = $item['Id'];
-			$embyItem['thumb'] = $item['Id'];
-			$embyItem['key'] = $item['Id'] . "-list";
-			$embyItem['nowPlayingThumb'] = (isset($item['AlbumId']) ? $item['AlbumId'] : @$item['ParentBackdropItemId']);
-			$embyItem['nowPlayingKey'] = $item['Id'] . "-np";
-			$embyItem['metadataKey'] = isset($item['AlbumId']) ? $item['AlbumId'] : $item['Id'];
-			$embyItem['nowPlayingImageType'] = (isset($item['ParentBackdropItemId']) ? "Primary" : "Backdrop");
-			$embyItem['nowPlayingTitle'] = @$item['AlbumArtist'] . ' - ' . @$item['Name'];
-			$embyItem['nowPlayingBottom'] = @$item['Album'];
-			break;
-		case 'Movie':
-			$embyItem['type'] = 'movie';
-			$embyItem['title'] = $item['Name'];
-			$embyItem['secondaryTitle'] = '';
-			$embyItem['summary'] = '';
-			$embyItem['ratingKey'] = $item['Id'];
-			$embyItem['thumb'] = $item['Id'];
-			$embyItem['key'] = $item['Id'] . "-list";
-			$embyItem['nowPlayingThumb'] = $item['Id'];
-			$embyItem['nowPlayingKey'] = $item['Id'] . "-np";
-			$embyItem['metadataKey'] = $item['Id'];
-			$embyItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? "Thumb" : (isset($item['BackdropImageTags']) ? "Backdrop" : false);
-			$embyItem['nowPlayingTitle'] = @$item['Name'];
-			$embyItem['nowPlayingBottom'] = @$item['ProductionYear'];
-			break;
-		case 'Video':
-			$embyItem['type'] = 'video';
-			$embyItem['title'] = $item['Name'];
-			$embyItem['secondaryTitle'] = '';
-			$embyItem['summary'] = '';
-			$embyItem['ratingKey'] = $item['Id'];
-			$embyItem['thumb'] = $item['Id'];
-			$embyItem['key'] = $item['Id'] . "-list";
-			$embyItem['nowPlayingThumb'] = $item['Id'];
-			$embyItem['nowPlayingKey'] = $item['Id'] . "-np";
-			$embyItem['metadataKey'] = $item['Id'];
-			$embyItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? "Thumb" : (isset($item['BackdropImageTags']) ? "Backdrop" : false);
-			$embyItem['nowPlayingTitle'] = @$item['Name'];
-			$embyItem['nowPlayingBottom'] = @$item['ProductionYear'];
-			break;
-		default:
-			return false;
-	}
-	$embyItem['uid'] = $item['Id'];
-	$embyItem['imageType'] = (isset($item['ImageTags']['Primary']) ? "Primary" : false);
-	$embyItem['elapsed'] = isset($itemDetails['PlayState']['PositionTicks']) && $itemDetails['PlayState']['PositionTicks'] !== '0' ? (int)$itemDetails['PlayState']['PositionTicks'] : null;
-	$embyItem['duration'] = isset($itemDetails['NowPlayingItem']['RunTimeTicks']) ? (int)$itemDetails['NowPlayingItem']['RunTimeTicks'] : (int)(isset($item['RunTimeTicks']) ? $item['RunTimeTicks'] : '');
-	$embyItem['watched'] = ($embyItem['elapsed'] && $embyItem['duration'] ? floor(($embyItem['elapsed'] / $embyItem['duration']) * 100) : 0);
-	$embyItem['transcoded'] = isset($itemDetails['TranscodingInfo']['CompletionPercentage']) ? floor((int)$itemDetails['TranscodingInfo']['CompletionPercentage']) : 100;
-	$embyItem['stream'] = @$itemDetails['PlayState']['PlayMethod'];
-	$embyItem['id'] = $item['ServerId'];
-	$embyItem['session'] = @$itemDetails['DeviceId'];
-	$embyItem['bandwidth'] = isset($itemDetails['TranscodingInfo']['Bitrate']) ? $itemDetails['TranscodingInfo']['Bitrate'] / 1000 : '';
-	$embyItem['bandwidthType'] = 'wan';
-	$embyItem['sessionType'] = (@$itemDetails['PlayState']['PlayMethod'] == 'Transcode') ? 'Transcoding' : 'Direct Playing';
-	$embyItem['state'] = ((@(string)$itemDetails['PlayState']['IsPaused'] == '1') ? "pause" : "play");
-	$embyItem['user'] = ($GLOBALS['homepageShowStreamNames'] && qualifyRequest($GLOBALS['homepageShowStreamNamesAuth'])) ? @(string)$itemDetails['UserName'] : "";
-	$embyItem['userThumb'] = '';
-	$embyItem['userAddress'] = (isset($itemDetails['RemoteEndPoint']) ? $itemDetails['RemoteEndPoint'] : "x.x.x.x");
-	$embyURL = (strpos($GLOBALS['embyURL'], 'jellyfin') !== false) ? $GLOBALS['embyURL'] . '/web/index.html#!/itemdetails.html?id=' : 'https://app.emby.media/#!/item/item.html?id=';
-	$embyItem['address'] = $GLOBALS['embyTabURL'] ? rtrim($GLOBALS['embyTabURL'], '/') . "/web/#!/item/item.html?id=" . $embyItem['uid'] : $embyURL . $embyItem['uid'] . "&serverId=" . $embyItem['id'];
-	$embyItem['nowPlayingOriginalImage'] = 'api/?v1/image&source=emby&type=' . $embyItem['nowPlayingImageType'] . '&img=' . $embyItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $embyItem['nowPlayingKey'] . '$' . randString();
-	$embyItem['originalImage'] = 'api/?v1/image&source=emby&type=' . $embyItem['imageType'] . '&img=' . $embyItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $embyItem['key'] . '$' . randString();
-	$embyItem['openTab'] = $GLOBALS['embyTabURL'] && $GLOBALS['embyTabName'] ? true : false;
-	$embyItem['tabName'] = $GLOBALS['embyTabName'] ? $GLOBALS['embyTabName'] : '';
-	// Stream info
-	$embyItem['userStream'] = array(
-		'platform' => @(string)$itemDetails['Client'],
-		'product' => @(string)$itemDetails['Client'],
-		'device' => @(string)$itemDetails['DeviceName'],
-		'stream' => @$itemDetails['PlayState']['PlayMethod'],
-		'videoResolution' => isset($itemDetails['NowPlayingItem']['MediaStreams'][0]['Width']) ? $itemDetails['NowPlayingItem']['MediaStreams'][0]['Width'] : '',
-		'throttled' => false,
-		'sourceVideoCodec' => isset($itemDetails['NowPlayingItem']['MediaStreams'][0]) ? $itemDetails['NowPlayingItem']['MediaStreams'][0]['Codec'] : '',
-		'videoCodec' => @$itemDetails['TranscodingInfo']['VideoCodec'],
-		'audioCodec' => @$itemDetails['TranscodingInfo']['AudioCodec'],
-		'sourceAudioCodec' => isset($itemDetails['NowPlayingItem']['MediaStreams'][1]) ? $itemDetails['NowPlayingItem']['MediaStreams'][1]['Codec'] : (isset($itemDetails['NowPlayingItem']['MediaStreams'][0]) ? $itemDetails['NowPlayingItem']['MediaStreams'][0]['Codec'] : ''),
-		'videoDecision' => streamType(@$itemDetails['PlayState']['PlayMethod']),
-		'audioDecision' => streamType(@$itemDetails['PlayState']['PlayMethod']),
-		'container' => isset($itemDetails['NowPlayingItem']['Container']) ? $itemDetails['NowPlayingItem']['Container'] : '',
-		'audioChannels' => @$itemDetails['TranscodingInfo']['AudioChannels']
-	);
-	// Genre catch all
-	if (isset($item['Genres'])) {
-		$genres = array();
-		foreach ($item['Genres'] as $genre) {
-			$genres[] = $genre;
-		}
-	}
-	// Actor catch all
-	if (isset($item['People'])) {
-		$actors = array();
-		foreach ($item['People'] as $key => $value) {
-			if (@$value['PrimaryImageTag'] && @$value['Role']) {
-				if (file_exists($cacheDirectory . (string)$value['Id'] . '-cast.jpg')) {
-					$actorImage = $cacheDirectoryWeb . (string)$value['Id'] . '-cast.jpg';
-				}
-				if (file_exists($cacheDirectory . (string)$value['Id'] . '-cast.jpg') && (time() - 604800) > filemtime($cacheDirectory . (string)$value['Id'] . '-cast.jpg') || !file_exists($cacheDirectory . (string)$value['Id'] . '-cast.jpg')) {
-					$actorImage = 'api/?v1/image&source=emby&type=Primary&img=' . (string)$value['Id'] . '&height=' . $actorHeight . '&width=' . $actorWidth . '&key=' . (string)$value['Id'] . '-cast';
-				}
-				$actors[] = array(
-					'name' => (string)$value['Name'],
-					'role' => (string)$value['Role'],
-					'thumb' => $actorImage
-				);
-			}
-		}
-	}
-	// Metadata information
-	$embyItem['metadata'] = array(
-		'guid' => $item['Id'],
-		'summary' => @(string)$item['Overview'],
-		'rating' => @(string)$item['CommunityRating'],
-		'duration' => @(string)$item['RunTimeTicks'],
-		'originallyAvailableAt' => @(string)$item['PremiereDate'],
-		'year' => (string)isset($item['ProductionYear']) ? $item['ProductionYear'] : '',
-		//'studio' => (string)$item['studio'],
-		'tagline' => @(string)$item['Taglines'][0],
-		'genres' => (isset($item['Genres'])) ? $genres : '',
-		'actors' => (isset($item['People'])) ? $actors : ''
-	);
-	if (file_exists($cacheDirectory . $embyItem['nowPlayingKey'] . '.jpg')) {
-		$embyItem['nowPlayingImageURL'] = $cacheDirectoryWeb . $embyItem['nowPlayingKey'] . '.jpg';
-	}
-	if (file_exists($cacheDirectory . $embyItem['key'] . '.jpg')) {
-		$embyItem['imageURL'] = $cacheDirectoryWeb . $embyItem['key'] . '.jpg';
-	}
-	if (file_exists($cacheDirectory . $embyItem['nowPlayingKey'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $embyItem['nowPlayingKey'] . '.jpg') || !file_exists($cacheDirectory . $embyItem['nowPlayingKey'] . '.jpg')) {
-		$embyItem['nowPlayingImageURL'] = 'api/?v1/image&source=emby&type=' . $embyItem['nowPlayingImageType'] . '&img=' . $embyItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $embyItem['nowPlayingKey'] . '';
-	}
-	if (file_exists($cacheDirectory . $embyItem['key'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $embyItem['key'] . '.jpg') || !file_exists($cacheDirectory . $embyItem['key'] . '.jpg')) {
-		$embyItem['imageURL'] = 'api/?v1/image&source=emby&type=' . $embyItem['imageType'] . '&img=' . $embyItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $embyItem['key'] . '';
-	}
-	if (!$embyItem['nowPlayingThumb']) {
-		$embyItem['nowPlayingOriginalImage'] = $embyItem['nowPlayingImageURL'] = "plugins/images/cache/no-np.png";
-		$embyItem['nowPlayingKey'] = "no-np";
-	}
-	if (!$embyItem['thumb']) {
-		$embyItem['originalImage'] = $embyItem['imageURL'] = "plugins/images/cache/no-list.png";
-		$embyItem['key'] = "no-list";
-	}
-	if (isset($useImage)) {
-		$embyItem['useImage'] = $useImage;
-	}
-	return $embyItem;
-}
-
-function getCacheImageSize($type)
-{
-	switch ($type) {
-		case 'height':
-		case 'h':
-			return 300 * $GLOBALS['cacheImageSize'];
-			break;
-		case 'width':
-		case 'w':
-			return 200 * $GLOBALS['cacheImageSize'];
-			break;
-		case 'nowPlayingHeight':
-		case 'nph':
-			return 675 * $GLOBALS['cacheImageSize'];
-			break;
-		case 'nowPlayingWidth':
-		case 'npw':
-			return 1200 * $GLOBALS['cacheImageSize'];
-			break;
-	}
-}
 
-function resolvePlexItem($item)
-{
-	// Static Height & Width
-	$height = getCacheImageSize('h');
-	$width = getCacheImageSize('w');
-	$nowPlayingHeight = getCacheImageSize('nph');
-	$nowPlayingWidth = getCacheImageSize('npw');
-	// Cache Directories
-	$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
-	$cacheDirectoryWeb = 'plugins/images/cache/';
-	// Types
-	switch ($item['type']) {
-		case 'show':
-			$plexItem['type'] = 'tv';
-			$plexItem['title'] = (string)$item['title'];
-			$plexItem['secondaryTitle'] = (string)$item['year'];
-			$plexItem['summary'] = (string)$item['summary'];
-			$plexItem['ratingKey'] = (string)$item['ratingKey'];
-			$plexItem['thumb'] = (string)$item['thumb'];
-			$plexItem['key'] = (string)$item['ratingKey'] . "-list";
-			$plexItem['nowPlayingThumb'] = (string)$item['art'];
-			$plexItem['nowPlayingKey'] = (string)$item['ratingKey'] . "-np";
-			$plexItem['nowPlayingTitle'] = (string)$item['title'];
-			$plexItem['nowPlayingBottom'] = (string)$item['year'];
-			$plexItem['metadataKey'] = (string)$item['ratingKey'];
-			break;
-		case 'season':
-			$plexItem['type'] = 'tv';
-			$plexItem['title'] = (string)$item['parentTitle'];
-			$plexItem['secondaryTitle'] = (string)$item['title'];
-			$plexItem['summary'] = (string)$item['parentSummary'];
-			$plexItem['ratingKey'] = (string)$item['parentRatingKey'];
-			$plexItem['thumb'] = (string)$item['thumb'];
-			$plexItem['key'] = (string)$item['ratingKey'] . "-list";
-			$plexItem['nowPlayingThumb'] = (string)$item['art'];
-			$plexItem['nowPlayingKey'] = (string)$item['ratingKey'] . "-np";
-			$plexItem['metadataKey'] = (string)$item['parentRatingKey'];
-			break;
-		case 'episode':
-			$plexItem['type'] = 'tv';
-			$plexItem['title'] = (string)$item['grandparentTitle'];
-			$plexItem['secondaryTitle'] = (string)$item['parentTitle'];
-			$plexItem['summary'] = (string)$item['title'];
-			$plexItem['ratingKey'] = (string)$item['parentRatingKey'];
-			$plexItem['thumb'] = ($item['parentThumb'] ? (string)$item['parentThumb'] : (string)$item['grandparentThumb']);
-			$plexItem['key'] = (string)$item['ratingKey'] . "-list";
-			$plexItem['nowPlayingThumb'] = (string)$item['grandparentArt'];
-			$plexItem['nowPlayingKey'] = (string)$item['grandparentRatingKey'] . "-np";
-			$plexItem['nowPlayingTitle'] = (string)$item['grandparentTitle'] . ' - ' . (string)$item['title'];
-			$plexItem['nowPlayingBottom'] = 'S' . (string)$item['parentIndex'] . ' · E' . (string)$item['index'];
-			$plexItem['metadataKey'] = (string)$item['grandparentRatingKey'];
-			break;
-		case 'clip':
-			$useImage = (isset($item['live']) ? "plugins/images/cache/livetv.png" : null);
-			$plexItem['type'] = 'clip';
-			$plexItem['title'] = (isset($item['live']) ? 'Live TV' : (string)$item['title']);
-			$plexItem['secondaryTitle'] = '';
-			$plexItem['summary'] = (string)$item['summary'];
-			$plexItem['ratingKey'] = (string)$item['parentRatingKey'];
-			$plexItem['thumb'] = (string)$item['thumb'];
-			$plexItem['key'] = (string)$item['ratingKey'] . "-list";
-			$plexItem['nowPlayingThumb'] = (string)$item['art'];
-			$plexItem['nowPlayingKey'] = isset($item['ratingKey']) ? (string)$item['ratingKey'] . "-np" : (isset($item['live']) ? "livetv.png" : ":)");
-			$plexItem['nowPlayingTitle'] = $plexItem['title'];
-			$plexItem['nowPlayingBottom'] = isset($item['extraType']) ? "Trailer" : (isset($item['live']) ? "Live TV" : ":)");
-			break;
-		case 'album':
-		case 'track':
-			$plexItem['type'] = 'music';
-			$plexItem['title'] = (string)$item['parentTitle'];
-			$plexItem['secondaryTitle'] = (string)$item['title'];
-			$plexItem['summary'] = (string)$item['title'];
-			$plexItem['ratingKey'] = (string)$item['parentRatingKey'];
-			$plexItem['thumb'] = (string)$item['thumb'];
-			$plexItem['key'] = (string)$item['ratingKey'] . "-list";
-			$plexItem['nowPlayingThumb'] = ($item['parentThumb']) ? (string)$item['parentThumb'] : (string)$item['art'];
-			$plexItem['nowPlayingKey'] = (string)$item['parentRatingKey'] . "-np";
-			$plexItem['nowPlayingTitle'] = (string)$item['grandparentTitle'] . ' - ' . (string)$item['title'];
-			$plexItem['nowPlayingBottom'] = (string)$item['parentTitle'];
-			$plexItem['metadataKey'] = isset($item['grandparentRatingKey']) ? (string)$item['grandparentRatingKey'] : (string)$item['parentRatingKey'];
-			break;
-		default:
-			$plexItem['type'] = 'movie';
-			$plexItem['title'] = (string)$item['title'];
-			$plexItem['secondaryTitle'] = (string)$item['year'];
-			$plexItem['summary'] = (string)$item['summary'];
-			$plexItem['ratingKey'] = (string)$item['ratingKey'];
-			$plexItem['thumb'] = (string)$item['thumb'];
-			$plexItem['key'] = (string)$item['ratingKey'] . "-list";
-			$plexItem['nowPlayingThumb'] = (string)$item['art'];
-			$plexItem['nowPlayingKey'] = (string)$item['ratingKey'] . "-np";
-			$plexItem['nowPlayingTitle'] = (string)$item['title'];
-			$plexItem['nowPlayingBottom'] = (string)$item['year'];
-			$plexItem['metadataKey'] = (string)$item['ratingKey'];
-	}
-	$plexItem['originalType'] = $item['type'];
-	$plexItem['uid'] = (string)$item['ratingKey'];
-	$plexItem['elapsed'] = isset($item['viewOffset']) && $item['viewOffset'] !== '0' ? (int)$item['viewOffset'] : null;
-	$plexItem['duration'] = isset($item['duration']) ? (int)$item['duration'] : (int)$item->Media['duration'];
-	$plexItem['addedAt'] = isset($item['addedAt']) ? (int)$item['addedAt'] : null;
-	$plexItem['watched'] = ($plexItem['elapsed'] && $plexItem['duration'] ? floor(($plexItem['elapsed'] / $plexItem['duration']) * 100) : 0);
-	$plexItem['transcoded'] = isset($item->TranscodeSession['progress']) ? floor((int)$item->TranscodeSession['progress'] - $plexItem['watched']) : '';
-	$plexItem['stream'] = isset($item->Media->Part->Stream['decision']) ? (string)$item->Media->Part->Stream['decision'] : '';
-	$plexItem['id'] = str_replace('"', '', (string)$item->Player['machineIdentifier']);
-	$plexItem['session'] = (string)$item->Session['id'];
-	$plexItem['bandwidth'] = (string)$item->Session['bandwidth'];
-	$plexItem['bandwidthType'] = (string)$item->Session['location'];
-	$plexItem['sessionType'] = isset($item->TranscodeSession['progress']) ? 'Transcoding' : 'Direct Playing';
-	$plexItem['state'] = (((string)$item->Player['state'] == "paused") ? "pause" : "play");
-	$plexItem['user'] = ($GLOBALS['homepageShowStreamNames'] && qualifyRequest($GLOBALS['homepageShowStreamNamesAuth'])) ? (string)$item->User['title'] : "";
-	$plexItem['userThumb'] = ($GLOBALS['homepageShowStreamNames'] && qualifyRequest($GLOBALS['homepageShowStreamNamesAuth'])) ? (string)$item->User['thumb'] : "";
-	$plexItem['userAddress'] = ($GLOBALS['homepageShowStreamNames'] && qualifyRequest($GLOBALS['homepageShowStreamNamesAuth'])) ? (string)$item->Player['address'] : "x.x.x.x";
-	$plexItem['address'] = $GLOBALS['plexTabURL'] ? $GLOBALS['plexTabURL'] . "/web/index.html#!/server/" . $GLOBALS['plexID'] . "/details?key=/library/metadata/" . $item['ratingKey'] : "https://app.plex.tv/web/app#!/server/" . $GLOBALS['plexID'] . "/details?key=/library/metadata/" . $item['ratingKey'];
-	$plexItem['nowPlayingOriginalImage'] = 'api/?v1/image&source=plex&img=' . $plexItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $plexItem['nowPlayingKey'] . '$' . randString();
-	$plexItem['originalImage'] = 'api/?v1/image&source=plex&img=' . $plexItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $plexItem['key'] . '$' . randString();
-	$plexItem['openTab'] = $GLOBALS['plexTabURL'] && $GLOBALS['plexTabName'] ? true : false;
-	$plexItem['tabName'] = $GLOBALS['plexTabName'] ? $GLOBALS['plexTabName'] : '';
-	// Stream info
-	$plexItem['userStream'] = array(
-		'platform' => (string)$item->Player['platform'],
-		'product' => (string)$item->Player['product'],
-		'device' => (string)$item->Player['device'],
-		'stream' => isset($item->Media) ? (string)$item->Media->Part['decision'] . ($item->TranscodeSession['throttled'] == '1' ? ' (Throttled)' : '') : '',
-		'videoResolution' => (string)$item->Media['videoResolution'],
-		'throttled' => ($item->TranscodeSession['throttled'] == 1) ? true : false,
-		'sourceVideoCodec' => (string)$item->TranscodeSession['sourceVideoCodec'],
-		'videoCodec' => (string)$item->TranscodeSession['videoCodec'],
-		'audioCodec' => (string)$item->TranscodeSession['audioCodec'],
-		'sourceAudioCodec' => (string)$item->TranscodeSession['sourceAudioCodec'],
-		'videoDecision' => streamType((string)$item->TranscodeSession['videoDecision']),
-		'audioDecision' => streamType((string)$item->TranscodeSession['audioDecision']),
-		'container' => (string)$item->TranscodeSession['container'],
-		'audioChannels' => (string)$item->TranscodeSession['audioChannels']
-	);
-	// Genre catch all
-	if ($item->Genre) {
-		$genres = array();
-		foreach ($item->Genre as $key => $value) {
-			$genres[] = (string)$value['tag'];
-		}
-	}
-	// Actor catch all
-	if ($item->Role) {
-		$actors = array();
-		foreach ($item->Role as $key => $value) {
-			if ($value['thumb']) {
-				$actors[] = array(
-					'name' => (string)$value['tag'],
-					'role' => (string)$value['role'],
-					'thumb' => (string)$value['thumb']
+trait HomepageConnectFunctions
+{
+	public function csvHomepageUrlToken($url, $token)
+	{
+		$list = array();
+		$urlList = explode(',', $url);
+		$tokenList = explode(',', $token);
+		if (count($urlList) == count($tokenList)) {
+			foreach ($urlList as $key => $value) {
+				$list[$key] = array(
+					'url' => $this->qualifyURL($value),
+					'token' => $tokenList[$key]
 				);
 			}
 		}
+		return $list;
 	}
-	// Metadata information
-	$plexItem['metadata'] = array(
-		'guid' => (string)$item['guid'],
-		'summary' => (string)$item['summary'],
-		'rating' => (string)$item['rating'],
-		'duration' => (string)$item['duration'],
-		'originallyAvailableAt' => (string)$item['originallyAvailableAt'],
-		'year' => (string)$item['year'],
-		'studio' => (string)$item['studio'],
-		'tagline' => (string)$item['tagline'],
-		'genres' => ($item->Genre) ? $genres : '',
-		'actors' => ($item->Role) ? $actors : ''
-	);
-	if (file_exists($cacheDirectory . $plexItem['nowPlayingKey'] . '.jpg')) {
-		$plexItem['nowPlayingImageURL'] = $cacheDirectoryWeb . $plexItem['nowPlayingKey'] . '.jpg';
-	}
-	if (file_exists($cacheDirectory . $plexItem['key'] . '.jpg')) {
-		$plexItem['imageURL'] = $cacheDirectoryWeb . $plexItem['key'] . '.jpg';
-	}
-	if (file_exists($cacheDirectory . $plexItem['nowPlayingKey'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $plexItem['nowPlayingKey'] . '.jpg') || !file_exists($cacheDirectory . $plexItem['nowPlayingKey'] . '.jpg')) {
-		$plexItem['nowPlayingImageURL'] = 'api/?v1/image&source=plex&img=' . $plexItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $plexItem['nowPlayingKey'] . '';
-	}
-	if (file_exists($cacheDirectory . $plexItem['key'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $plexItem['key'] . '.jpg') || !file_exists($cacheDirectory . $plexItem['key'] . '.jpg')) {
-		$plexItem['imageURL'] = 'api/?v1/image&source=plex&img=' . $plexItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $plexItem['key'] . '';
-	}
-	if (!$plexItem['nowPlayingThumb']) {
-		$plexItem['nowPlayingOriginalImage'] = $plexItem['nowPlayingImageURL'] = "plugins/images/cache/no-np.png";
-		$plexItem['nowPlayingKey'] = "no-np";
-	}
-	if (!$plexItem['thumb'] || $plexItem['addedAt'] >= (time() - 300)) {
-		$plexItem['originalImage'] = $plexItem['imageURL'] = "plugins/images/cache/no-list.png";
-		$plexItem['key'] = "no-list";
-	}
-	if (isset($useImage)) {
-		$plexItem['useImage'] = $useImage;
-	}
-	return $plexItem;
-}
-
-function plexConnect($action, $key = null)
-{
-	if ($GLOBALS['homepagePlexEnabled'] && !empty($GLOBALS['plexURL']) && !empty($GLOBALS['plexToken']) && !empty($GLOBALS['plexID'] && qualifyRequest($GLOBALS['homepagePlexAuth']))) {
-		$url = qualifyURL($GLOBALS['plexURL']);
-		$multipleURL = false;
-		$ignore = array();
-		$resolve = true;
-		switch ($action) {
-			case 'streams':
-				$url = $url . "/status/sessions?X-Plex-Token=" . $GLOBALS['plexToken'];
-				break;
-			case 'libraries':
-				$url = $url . "/library/sections?X-Plex-Token=" . $GLOBALS['plexToken'];
-				$resolve = false;
-				break;
-			case 'recent':
-				//$url = $url . "/library/recentlyAdded?X-Plex-Token=" . $GLOBALS['plexToken'] . "&limit=" . $GLOBALS['homepageRecentLimit'];
-				$urls['movie'] = $url . "/hubs/home/recentlyAdded?X-Plex-Token=" . $GLOBALS['plexToken'] . "&X-Plex-Container-Start=0&X-Plex-Container-Size=" . $GLOBALS['homepageRecentLimit'] . "&type=1";
-				$urls['tv'] = $url . "/hubs/home/recentlyAdded?X-Plex-Token=" . $GLOBALS['plexToken'] . "&X-Plex-Container-Start=0&X-Plex-Container-Size=" . $GLOBALS['homepageRecentLimit'] . "&type=2";
-				$urls['music'] = $url . "/hubs/home/recentlyAdded?X-Plex-Token=" . $GLOBALS['plexToken'] . "&X-Plex-Container-Start=0&X-Plex-Container-Size=" . $GLOBALS['homepageRecentLimit'] . "&type=8";
-				$multipleURL = true;
-				break;
-			case 'metadata':
-				$url = $url . "/library/metadata/" . $key . "?X-Plex-Token=" . $GLOBALS['plexToken'];
-				break;
-			case 'playlists':
-				$url = $url . "/playlists?X-Plex-Token=" . $GLOBALS['plexToken'];
-				break;
-			case 'search':
-				$url = $url . "/search?query=" . rawurlencode($key) . "&X-Plex-Token=" . $GLOBALS['plexToken'];
-				$ignore = array('artist', 'episode');
-				break;
-			default:
-				# code...
-				break;
-		}
-		try {
-			if (!$multipleURL) {
-				$options = (localURL($url)) ? array('verify' => false) : array();
-				$response = Requests::get($url, array(), $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[] = resolvePlexItem($child);
-						}
-					}
-					$api['content'] = ($resolve) ? $items : $plex;
-					$api['plexID'] = $GLOBALS['plexID'];
-					$api['showNames'] = true;
-					$api['group'] = '1';
-					return $api;
-				}
-			} else {
-				foreach ($urls as $k => $v) {
-					$options = (localURL($v)) ? array('verify' => false) : array();
-					$response = Requests::get($v, array(), $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[] = resolvePlexItem($child);
-							}
-						}
-						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'] = $GLOBALS['plexID'];
-				$api['showNames'] = true;
-				$api['group'] = '1';
-				return $api;
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'Plex Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-	}
-	return false;
-}
-
-function getPlexPlaylists()
-{
-	if ($GLOBALS['homepagePlexEnabled'] && !empty($GLOBALS['plexURL']) && !empty($GLOBALS['plexToken']) && !empty($GLOBALS['plexID'] && qualifyRequest($GLOBALS['homepagePlexAuth']) && qualifyRequest($GLOBALS['homepagePlexPlaylistAuth']) && $GLOBALS['homepagePlexPlaylist'])) {
-		$url = qualifyURL($GLOBALS['plexURL']);
-		$url = $url . "/playlists?X-Plex-Token=" . $GLOBALS['plexToken'];
-		try {
-			$options = (localURL($url)) ? array('verify' => false) : array();
-			$response = Requests::get($url, array(), $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 = qualifyURL($GLOBALS['plexURL']);
-						$playlistURL = $playlistURL . $child['key'] . "?X-Plex-Token=" . $GLOBALS['plexToken'];
-						$options = (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][] = resolvePlexItem($playlistItem);
-							}
-						}
-					}
-				}
-				$api['content'] = $items;
-				$api['plexID'] = $GLOBALS['plexID'];
-				$api['showNames'] = true;
-				$api['group'] = '1';
-				return $api;
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'Plex Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-	}
-	return false;
-}
-
-function embyConnect($action, $key = 'Latest', $skip = false)
-{
-	if ($GLOBALS['homepageEmbyEnabled'] && !empty($GLOBALS['embyURL']) && !empty($GLOBALS['embyToken']) && qualifyRequest($GLOBALS['homepageEmbyAuth'])) {
-		$url = qualifyURL($GLOBALS['embyURL']);
-		switch ($action) {
-			case 'streams':
-				$url = $url . '/Sessions?api_key=' . $GLOBALS['embyToken'] . '&Fields=Overview,People,Genres,CriticRating,Studios,Taglines';
-				break;
-			case 'recent':
-			case 'metadata':
-				$username = false;
-				if (isset($GLOBALS['organizrUser']['username'])) {
-					$username = strtolower($GLOBALS['organizrUser']['username']);
-				}
-				// Get A User
-				$userIds = $url . "/Users?api_key=" . $GLOBALS['embyToken'];
-				$showPlayed = false;
-				try {
-					$options = (localURL($userIds)) ? array('verify' => false) : array();
-					$response = Requests::get($userIds, array(), $options);
-					if ($response->success) {
-						$emby = json_decode($response->body, true);
-						foreach ($emby as $value) { // Scan for admin user
-							if (isset($value['Policy']) && isset($value['Policy']['IsAdministrator']) && $value['Policy']['IsAdministrator']) {
-								$userId = $value['Id'];
-							}
-							if ($username && strtolower($value['Name']) == $username) {
-								$userId = $value['Id'];
-								$showPlayed = false;
-								break;
-							}
-						}
-						$url = $url . '/Users/' . $userId . '/Items/' . $key . '?EnableImages=true&Limit=' . $GLOBALS['homepageRecentLimit'] . '&api_key=' . $GLOBALS['embyToken'] . ($showPlayed ? '' : '&IsPlayed=false') . '&Fields=Overview,People,Genres,CriticRating,Studios,Taglines';
-					}
-				} catch (Requests_Exception $e) {
-					writeLog('error', 'Emby Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-				};
-				break;
-			default:
-				# code...
-				break;
-		}
-		try {
-			$options = (localURL($url)) ? array('verify' => false) : array();
-			$response = Requests::get($url, array(), $options);
-			if ($response->success) {
-				$items = array();
-				$emby = json_decode($response->body, true);
-				if ($key !== 'Latest') {
-					if (isset($emby['NowPlayingItem']) || isset($emby['Name'])) {
-						$items[] = resolveEmbyItem($emby);
-					}
-				} else {
-					foreach ($emby as $child) {
-						if (isset($child['NowPlayingItem']) || isset($child['Name'])) {
-							$items[] = resolveEmbyItem($child);
-						}
-					}
-				}
-				$api['content'] = array_filter($items);
-				return $api;
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'Emby Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-	}
-	return false;
-}
-
-function jdownloaderConnect()
-{
-	if ($GLOBALS['homepageJdownloaderEnabled'] && !empty($GLOBALS['jdownloaderURL']) && qualifyRequest($GLOBALS['homepageJdownloaderAuth'])) {
-		$url = qualifyURL($GLOBALS['jdownloaderURL']);
-		try {
-			$options = (localURL($url)) ? array('verify' => false, 'timeout' => 30) : array('timeout' => 30);
-			$response = Requests::get($url, array(), $options);
-			if ($response->success) {
-				$temp = json_decode($response->body, true);
-				$packages = $temp['packages'];
-				if ($packages['downloader']) {
-					$api['content']['queueItems'] = $packages['downloader'];
-				} else {
-					$api['content']['queueItems'] = [];
-				}
-				if ($packages['linkgrabber_decrypted']) {
-					$api['content']['grabberItems'] = $packages['linkgrabber_decrypted'];
-				} else {
-					$api['content']['grabberItems'] = [];
-				}
-				if ($packages['linkgrabber_failed']) {
-					$api['content']['encryptedItems'] = $packages['linkgrabber_failed'];
-				} else {
-					$api['content']['encryptedItems'] = [];
-				}
-				if ($packages['linkgrabber_offline']) {
-					$api['content']['offlineItems'] = $packages['linkgrabber_offline'];
-				} else {
-					$api['content']['offlineItems'] = [];
-				}
-				$api['content']['$status'] = array($temp['downloader_state'], $temp['grabber_collecting'], $temp['update_ready']);
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'JDownloader Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-		$api['content'] = isset($api['content']) ? $api['content'] : false;
-		return $api;
-	}
-	return false;
-}
-
-function sabnzbdConnect()
-{
-	if ($GLOBALS['homepageSabnzbdEnabled'] && !empty($GLOBALS['sabnzbdURL']) && !empty($GLOBALS['sabnzbdToken']) && qualifyRequest($GLOBALS['homepageSabnzbdAuth'])) {
-		$url = qualifyURL($GLOBALS['sabnzbdURL']);
-		$url = $url . '/api?mode=queue&output=json&apikey=' . $GLOBALS['sabnzbdToken'];
-		try {
-			$options = (localURL($url)) ? array('verify' => false) : array();
-			$response = Requests::get($url, array(), $options);
-			if ($response->success) {
-				$api['content']['queueItems'] = json_decode($response->body, true);
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-		$url = qualifyURL($GLOBALS['sabnzbdURL']);
-		$url = $url . '/api?mode=history&output=json&limit=100&apikey=' . $GLOBALS['sabnzbdToken'];
-		try {
-			$options = (localURL($url)) ? array('verify' => false) : array();
-			$response = Requests::get($url, array(), $options);
-			if ($response->success) {
-				$api['content']['historyItems'] = json_decode($response->body, true);
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-		$api['content'] = isset($api['content']) ? $api['content'] : false;
-		return $api;
-	}
-	return false;
-}
-
-function nzbgetConnect()
-{
-	if ($GLOBALS['homepageNzbgetEnabled'] && !empty($GLOBALS['nzbgetURL']) && qualifyRequest($GLOBALS['homepageNzbgetAuth'])) {
-		$url = qualifyURL($GLOBALS['nzbgetURL']);
-		$url = $url . '/jsonrpc/listgroups';
-		try {
-			$options = (localURL($url)) ? array('verify' => false) : array();
-			if ($GLOBALS['nzbgetUsername'] !== '' && decrypt($GLOBALS['nzbgetPassword']) !== '') {
-				$credentials = array('auth' => new Requests_Auth_Basic(array($GLOBALS['nzbgetUsername'], decrypt($GLOBALS['nzbgetPassword']))));
-				$options = array_merge($options, $credentials);
-			}
-			$response = Requests::get($url, array(), $options);
-			if ($response->success) {
-				$api['content']['queueItems'] = json_decode($response->body, true);
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'NZBGet Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-		$url = qualifyURL($GLOBALS['nzbgetURL']);
-		$url = $url . '/jsonrpc/history';
-		try {
-			$options = (localURL($url)) ? array('verify' => false) : array();
-			if ($GLOBALS['nzbgetUsername'] !== '' && decrypt($GLOBALS['nzbgetPassword']) !== '') {
-				$credentials = array('auth' => new Requests_Auth_Basic(array($GLOBALS['nzbgetUsername'], decrypt($GLOBALS['nzbgetPassword']))));
-				$options = array_merge($options, $credentials);
-			}
-			$response = Requests::get($url, array(), $options);
-			if ($response->success) {
-				$api['content']['historyItems'] = json_decode($response->body, true);
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'NZBGet Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-		$api['content'] = isset($api['content']) ? $api['content'] : false;
-		return $api;
-	}
-	return false;
-}
-
-function transmissionConnect()
-{
-	if ($GLOBALS['homepageTransmissionEnabled'] && !empty($GLOBALS['transmissionURL']) && qualifyRequest($GLOBALS['homepageTransmissionAuth'])) {
-		$digest = qualifyURL($GLOBALS['transmissionURL'], true);
-		$passwordInclude = ($GLOBALS['transmissionUsername'] != '' && $GLOBALS['transmissionPassword'] != '') ? $GLOBALS['transmissionUsername'] . ':' . decrypt($GLOBALS['transmissionPassword']) . "@" : '';
-		$url = $digest['scheme'] . '://' . $passwordInclude . $digest['host'] . $digest['port'] . $digest['path'] . '/rpc';
-		try {
-			$options = (localURL($GLOBALS['transmissionURL'])) ? array('verify' => false) : array();
-			$response = Requests::get($url, array(), $options);
-			if ($response->headers['x-transmission-session-id']) {
-				$headers = array(
-					'X-Transmission-Session-Id' => $response->headers['x-transmission-session-id'],
-					'Content-Type' => 'application/json'
-				);
-				$data = array(
-					'method' => 'torrent-get',
-					'arguments' => array(
-						'fields' => array(
-							"id", "name", "totalSize", "eta", "isFinished", "isStalled", "percentDone", "rateDownload", "status", "downloadDir", "errorString"
-						),
-					),
-					'tags' => ''
-				);
-				$response = Requests::post($url, $headers, json_encode($data), $options);
-				if ($response->success) {
-					$torrentList = json_decode($response->body, true)['arguments']['torrents'];
-					if ($GLOBALS['transmissionHideSeeding'] || $GLOBALS['transmissionHideCompleted']) {
-						$filter = array();
-						$torrents['arguments']['torrents'] = array();
-						if ($GLOBALS['transmissionHideSeeding']) {
-							array_push($filter, 6, 5);
-						}
-						if ($GLOBALS['transmissionHideCompleted']) {
-							array_push($filter, 0);
-						}
-						foreach ($torrentList as $key => $value) {
-							if (!in_array($value['status'], $filter)) {
-								$torrents['arguments']['torrents'][] = $value;
-							}
-						}
-					} else {
-						$torrents = json_decode($response->body, true);
-					}
-					$api['content']['queueItems'] = $torrents;
-					$api['content']['historyItems'] = false;
-				}
-			} else {
-				writeLog('error', 'Transmission Connect Function - Error: Could not get session ID', 'SYSTEM');
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'Transmission Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-		$api['content'] = isset($api['content']) ? $api['content'] : false;
-		return $api;
-	}
-	return false;
-}
-
-function rTorrentStatus($completed, $state, $status)
-{
-	if ($completed && $state && $status == 'seed') {
-		$state = 'Seeding';
-	} elseif (!$completed && !$state && $status == 'leech') {
-		$state = 'Stopped';
-	} elseif (!$completed && $state && $status == 'leech') {
-		$state = 'Downloading';
-	} elseif ($completed && !$state && $status == 'seed') {
-		$state = 'Finished';
-	}
-	return ($state) ? $state : $status;
-}
-
-function rTorrentConnect()
-{
-	if ($GLOBALS['homepagerTorrentEnabled'] && (!empty($GLOBALS['rTorrentURL']) || !empty($GLOBALS['rTorrentURLOverride'])) && qualifyRequest($GLOBALS['homepagerTorrentAuth'])) {
-		try {
-			$torrents = array();
-			$digest = (empty($GLOBALS['rTorrentURLOverride'])) ? qualifyURL($GLOBALS['rTorrentURL'], true) : qualifyURL(checkOverrideURL($GLOBALS['rTorrentURL'], $GLOBALS['rTorrentURLOverride']), true);
-			$passwordInclude = ($GLOBALS['rTorrentUsername'] !== '' && $GLOBALS['rTorrentPassword'] !== '') ? $GLOBALS['rTorrentUsername'] . ':' . decrypt($GLOBALS['rTorrentPassword']) . "@" : '';
-			$extraPath = (strpos($GLOBALS['rTorrentURL'], '.php') !== false) ? '' : '/RPC2';
-			$extraPath = (empty($GLOBALS['rTorrentURLOverride'])) ? $extraPath : '';
-			$url = $digest['scheme'] . '://' . $passwordInclude . $digest['host'] . $digest['port'] . $digest['path'] . $extraPath;
-			$options = (localURL($url, $GLOBALS['rTorrentDisableCertCheck'])) ? array('verify' => false) : array();
-			if ($GLOBALS['rTorrentUsername'] !== '' && decrypt($GLOBALS['rTorrentPassword']) !== '') {
-				$credentials = array('auth' => new Requests_Auth_Digest(array($GLOBALS['rTorrentUsername'], decrypt($GLOBALS['rTorrentPassword']))));
-				$options = array_merge($options, $credentials);
-			}
-			$data = xmlrpc_encode_request("d.multicall2", array(
-				"",
-				"main",
-				"d.name=",
-				"d.base_path=",
-				"d.up.total=",
-				"d.size_bytes=",
-				"d.down.total=",
-				"d.completed_bytes=",
-				"d.connection_current=",
-				"d.down.rate=",
-				"d.up.rate=",
-				"d.timestamp.started=",
-				"d.state=",
-				"d.group.name=",
-				"d.hash=",
-				"d.complete=",
-				"d.ratio=",
-				"d.chunk_size=",
-				"f.size_bytes=",
-				"f.size_chunks=",
-				"f.completed_chunks=",
-				"d.custom=",
-				"d.custom1=",
-				"d.custom2=",
-				"d.custom3=",
-				"d.custom4=",
-				"d.custom5=",
-			), array());
-			$response = Requests::post($url, array(), $data, $options);
-			if ($response->success) {
-				$torrentList = xmlrpc_decode(str_replace('i8>', 'string>', $response->body));
-				foreach ($torrentList as $key => $value) {
-					$tempStatus = rTorrentStatus($value[13], $value[10], $value[6]);
-					if ($tempStatus == 'Seeding' && $GLOBALS['rTorrentHideSeeding']) {
-						//do nothing
-					} elseif ($tempStatus == 'Finished' && $GLOBALS['rTorrentHideCompleted']) {
-						//do nothing
-					} else {
-						$torrents[$key] = array(
-							'name' => $value[0],
-							'base' => $value[1],
-							'upTotal' => $value[2],
-							'size' => $value[3],
-							'downTotal' => $value[4],
-							'downloaded' => $value[5],
-							'connectionState' => $value[6],
-							'leech' => $value[7],
-							'seed' => $value[8],
-							'date' => $value[9],
-							'state' => ($value[10]) ? 'on' : 'off',
-							'group' => $value[11],
-							'hash' => $value[12],
-							'complete' => ($value[13]) ? 'yes' : 'no',
-							'ratio' => $value[14],
-							'label' => $value[20],
-							'status' => $tempStatus,
-							'temp' => $value[16] . ' - ' . $value[17] . ' - ' . $value[18],
-							'custom' => $value[19] . ' - ' . $value[20] . ' - ' . $value[21],
-							'custom2' => $value[22] . ' - ' . $value[23] . ' - ' . $value[24],
-						);
-					}
-				}
-				if (count($torrents) !== 0) {
-					usort($torrents, function ($a, $b) {
-						$direction = substr($GLOBALS['rTorrentSortOrder'], -1);
-						$sort = substr($GLOBALS['rTorrentSortOrder'], 0, strlen($GLOBALS['rTorrentSortOrder']) - 1);
-						switch ($direction) {
-							case 'a':
-								return $a[$sort] <=> $b[$sort];
-								break;
-							case 'd':
-								return $b[$sort] <=> $a[$sort];
-								break;
-							default:
-								return $b['date'] <=> $a['date'];
-						}
-					});
-				}
-				$api['content']['queueItems'] = $torrents;
-				$api['content']['historyItems'] = false;
-			}
-		} catch
-		(Requests_Exception $e) {
-			writeLog('error', 'rTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-		$api['content'] = isset($api['content']) ? $api['content'] : false;
-		return $api;
-	}
-	return false;
-}
-
-function qBittorrentConnect()
-{
-	if ($GLOBALS['homepageqBittorrentEnabled'] && !empty($GLOBALS['qBittorrentURL']) && qualifyRequest($GLOBALS['homepageqBittorrentAuth'])) {
-		$digest = qualifyURL($GLOBALS['qBittorrentURL'], true);
-		$data = array('username' => $GLOBALS['qBittorrentUsername'], 'password' => decrypt($GLOBALS['qBittorrentPassword']));
-		$apiVersionLogin = ($GLOBALS['qBittorrentApiVersion'] == '1') ? '/login' : '/api/v2/auth/login';
-		$apiVersionQuery = ($GLOBALS['qBittorrentApiVersion'] == '1') ? '/query/torrents?sort=' : '/api/v2/torrents/info?sort=';
-		$url = $digest['scheme'] . '://' . $digest['host'] . $digest['port'] . $digest['path'] . $apiVersionLogin;
-		try {
-			$options = (localURL($GLOBALS['qBittorrentURL'])) ? array('verify' => false) : array();
-			$response = Requests::post($url, array(), $data, $options);
-			$reflection = new ReflectionClass($response->cookies);
-			$cookie = $reflection->getProperty("cookies");
-			$cookie->setAccessible(true);
-			$cookie = $cookie->getValue($response->cookies);
-			if ($cookie) {
-				$headers = array(
-					'Cookie' => 'SID=' . $cookie['SID']->value
-				);
-				$reverse = $GLOBALS['qBittorrentReverseSorting'] ? 'true' : 'false';
-				$url = $digest['scheme'] . '://' . $digest['host'] . $digest['port'] . $digest['path'] . $apiVersionQuery . $GLOBALS['qBittorrentSortOrder'] . '&reverse=' . $reverse;
-				$response = Requests::get($url, $headers, $options);
-				if ($response) {
-					$torrentList = json_decode($response->body, true);
-					if ($GLOBALS['qBittorrentHideSeeding'] || $GLOBALS['qBittorrentHideCompleted']) {
-						$filter = array();
-						$torrents['arguments']['torrents'] = array();
-						if ($GLOBALS['qBittorrentHideSeeding']) {
-							array_push($filter, 'uploading', 'stalledUP', 'queuedUP');
-						}
-						if ($GLOBALS['qBittorrentHideCompleted']) {
-							array_push($filter, 'pausedUP');
-						}
-						foreach ($torrentList as $key => $value) {
-							if (!in_array($value['state'], $filter)) {
-								$torrents['arguments']['torrents'][] = $value;
-							}
-						}
-					} else {
-						$torrents['arguments']['torrents'] = json_decode($response->body, true);
-					}
-					$api['content']['queueItems'] = $torrents;
-					$api['content']['historyItems'] = false;
-				}
-			} else {
-				writeLog('error', 'qBittorrent Connect Function - Error: Could not get session ID', 'SYSTEM');
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'qBittorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-		$api['content'] = isset($api['content']) ? $api['content'] : false;
-		return $api;
-	}
-	return false;
-}
-
-function delugeStatus($queued, $status, $state)
-{
-	if ($queued == '-1' && $state == '100' && ($status == 'Seeding' || $status == 'Queued' || $status == 'Paused')) {
-		$state = 'Seeding';
-	} elseif ($state !== '100') {
-		$state = 'Downloading';
-	} else {
-		$state = 'Finished';
-	}
-	return ($state) ? $state : $status;
-}
-
-function delugeConnect()
-{
-	if ($GLOBALS['homepageDelugeEnabled'] && !empty($GLOBALS['delugeURL']) && !empty($GLOBALS['delugePassword']) && qualifyRequest($GLOBALS['homepageDelugeAuth'])) {
-		try {
-			$deluge = new deluge($GLOBALS['delugeURL'], decrypt($GLOBALS['delugePassword']));
-			$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 = delugeStatus($value->queue, $value->state, $value->progress);
-				if ($tempStatus == 'Seeding' && $GLOBALS['delugeHideSeeding']) {
-					//do nothing
-				} elseif ($tempStatus == 'Finished' && $GLOBALS['delugeHideCompleted']) {
-					//do nothing
-				} else {
-					$api['content']['queueItems'][] = $value;
-				}
-			}
-			$api['content']['queueItems'] = (empty($api['content']['queueItems'])) ? [] : $api['content']['queueItems'];
-			$api['content']['historyItems'] = false;
-		} catch (Excecption $e) {
-			writeLog('error', 'Deluge Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		}
-	}
-	$api['content'] = isset($api['content']) ? $api['content'] : false;
-	return $api;
-}
-
-function getCalendar()
-{
-	$startDate = date('Y-m-d', strtotime("-" . $GLOBALS['calendarStart'] . " days"));
-	$endDate = date('Y-m-d', strtotime("+" . $GLOBALS['calendarEnd'] . " days"));
-	$icalCalendarSources = array();
-	$calendarItems = array();
-	// SONARR CONNECT
-	if ($GLOBALS['homepageSonarrEnabled'] && qualifyRequest($GLOBALS['homepageSonarrAuth']) && !empty($GLOBALS['sonarrURL']) && !empty($GLOBALS['sonarrToken'])) {
-		$sonarrs = array();
-		$sonarrURLList = explode(',', $GLOBALS['sonarrURL']);
-		$sonarrTokenList = explode(',', $GLOBALS['sonarrToken']);
-		if (count($sonarrURLList) == count($sonarrTokenList)) {
-			foreach ($sonarrURLList as $key => $value) {
-				$sonarrs[$key] = array(
-					'url' => $value,
-					'token' => $sonarrTokenList[$key]
-				);
-			}
-			foreach ($sonarrs as $key => $value) {
-				try {
-					$sonarr = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
-					$sonarr = $sonarr->getCalendar($startDate, $endDate, $GLOBALS['sonarrUnmonitored']);
-					$result = json_decode($sonarr, true);
-					if (is_array($result) || is_object($result)) {
-						$sonarrCalendar = (array_key_exists('error', $result)) ? '' : getSonarrCalendar($sonarr, $key);
-					} else {
-						$sonarrCalendar = '';
-					}
-				} catch (Exception $e) {
-					writeLog('error', 'Sonarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-				}
-				if (!empty($sonarrCalendar)) {
-					$calendarItems = array_merge($calendarItems, $sonarrCalendar);
-				}
-			}
-		}
-	}
-	// LIDARR CONNECT
-	if ($GLOBALS['homepageLidarrEnabled'] && qualifyRequest($GLOBALS['homepageLidarrAuth']) && !empty($GLOBALS['lidarrURL']) && !empty($GLOBALS['lidarrToken'])) {
-		$lidarrs = array();
-		$lidarrURLList = explode(',', $GLOBALS['lidarrURL']);
-		$lidarrTokenList = explode(',', $GLOBALS['lidarrToken']);
-		if (count($lidarrURLList) == count($lidarrTokenList)) {
-			foreach ($lidarrURLList as $key => $value) {
-				$lidarrs[$key] = array(
-					'url' => $value,
-					'token' => $lidarrTokenList[$key]
-				);
-			}
-			foreach ($lidarrs as $key => $value) {
-				try {
-					$lidarr = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token'], true);
-					$lidarr = $lidarr->getCalendar($startDate, $endDate);
-					$result = json_decode($lidarr, true);
-					if (is_array($result) || is_object($result)) {
-						$lidarrCalendar = (array_key_exists('error', $result)) ? '' : getLidarrCalendar($lidarr, $key);
-					} else {
-						$lidarrCalendar = '';
-					}
-				} catch (Exception $e) {
-					writeLog('error', 'Lidarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-				}
-				if (!empty($lidarrCalendar)) {
-					$calendarItems = array_merge($calendarItems, $lidarrCalendar);
-				}
-			}
-		}
-	}
-	// RADARR CONNECT
-	if ($GLOBALS['homepageRadarrEnabled'] && qualifyRequest($GLOBALS['homepageRadarrAuth']) && !empty($GLOBALS['radarrURL']) && !empty($GLOBALS['radarrToken'])) {
-		$radarrs = array();
-		$radarrURLList = explode(',', $GLOBALS['radarrURL']);
-		$radarrTokenList = explode(',', $GLOBALS['radarrToken']);
-		if (count($radarrURLList) == count($radarrTokenList)) {
-			foreach ($radarrURLList as $key => $value) {
-				$radarrs[$key] = array(
-					'url' => $value,
-					'token' => $radarrTokenList[$key]
-				);
-			}
-			foreach ($radarrs as $key => $value) {
-				try {
-					$radarr = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
-					$radarr = $radarr->getCalendar($startDate, $endDate);
-					$result = json_decode($radarr, true);
-					if (is_array($result) || is_object($result)) {
-						$radarrCalendar = (array_key_exists('error', $result)) ? '' : getRadarrCalendar($radarr, $key, $value['url']);
-					} else {
-						$radarrCalendar = '';
-					}
-				} catch (Exception $e) {
-					writeLog('error', 'Radarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-				}
-				if (!empty($radarrCalendar)) {
-					$calendarItems = array_merge($calendarItems, $radarrCalendar);
-				}
-			}
-		}
-	}
-	// SICKRAGE/BEARD/MEDUSA CONNECT
-	if ($GLOBALS['homepageSickrageEnabled'] && qualifyRequest($GLOBALS['homepageSickrageAuth']) && !empty($GLOBALS['sickrageURL']) && !empty($GLOBALS['sickrageToken'])) {
-		$sicks = array();
-		$sickURLList = explode(',', $GLOBALS['sickrageURL']);
-		$sickTokenList = explode(',', $GLOBALS['sickrageToken']);
-		if (count($sickURLList) == count($sickTokenList)) {
-			foreach ($sickURLList as $key => $value) {
-				$sicks[$key] = array(
-					'url' => $value,
-					'token' => $sickTokenList[$key]
-				);
-			}
-			foreach ($sicks as $key => $value) {
-				try {
-					$sickrage = new Kryptonit3\SickRage\SickRage($value['url'], $value['token']);
-					$sickrageFuture = getSickrageCalendarWanted($sickrage->future(), $key);
-					$sickrageHistory = getSickrageCalendarHistory($sickrage->history("100", "downloaded"), $key);
-					if (!empty($sickrageFuture)) {
-						$calendarItems = array_merge($calendarItems, $sickrageFuture);
-					}
-					if (!empty($sickrageHistory)) {
-						$calendarItems = array_merge($calendarItems, $sickrageHistory);
-					}
-				} catch (Exception $e) {
-					writeLog('error', 'Sickrage Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-				}
-			}
-		}
-	}
-	// COUCHPOTATO CONNECT
-	if ($GLOBALS['homepageCouchpotatoEnabled'] && qualifyRequest($GLOBALS['homepageCouchpotatoAuth']) && !empty($GLOBALS['couchpotatoURL']) && !empty($GLOBALS['couchpotatoToken'])) {
-		$couchs = array();
-		$couchpotatoURLList = explode(',', $GLOBALS['couchpotatoURL']);
-		$couchpotatoTokenList = explode(',', $GLOBALS['couchpotatoToken']);
-		if (count($couchpotatoURLList) == count($couchpotatoTokenList)) {
-			foreach ($couchpotatoURLList as $key => $value) {
-				$couchs[$key] = array(
-					'url' => $value,
-					'token' => $couchpotatoTokenList[$key]
-				);
-			}
-			foreach ($couchs as $key => $value) {
-				try {
-					$couchpotato = new Kryptonit3\CouchPotato\CouchPotato($value['url'], $value['token']);
-					$couchCalendar = getCouchCalendar($couchpotato->getMediaList(), $key);
-					if (!empty($couchCalendar)) {
-						$calendarItems = array_merge($calendarItems, $couchCalendar);
-					}
-				} catch (Exception $e) {
-					writeLog('error', 'Sickrage Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-				}
-			}
-		}
-	}
-	// iCal URL
-	if ($GLOBALS['homepageCalendarEnabled'] && qualifyRequest($GLOBALS['homepageCalendarAuth']) && !empty($GLOBALS['calendariCal'])) {
-		$calendars = array();
-		$calendarURLList = explode(',', $GLOBALS['calendariCal']);
-		$icalEvents = array();
-		foreach ($calendarURLList as $key => $value) {
-			$icsEvents = getIcsEventsAsArray($value);
-			if (isset($icsEvents) && !empty($icsEvents)) {
-				$timeZone = isset($icsEvents [1] ['X-WR-TIMEZONE']) ? trim($icsEvents[1]['X-WR-TIMEZONE']) : date_default_timezone_get();
-				$originalTimeZone = isset($icsEvents [1] ['X-WR-TIMEZONE']) ? str_replace('"', '', trim($icsEvents[1]['X-WR-TIMEZONE'])) : false;
-				unset($icsEvents [1]);
-				foreach ($icsEvents as $icsEvent) {
-					$startKeys = array_filter_key($icsEvent, function ($key) {
-						return strpos($key, 'DTSTART') === 0;
-					});
-					$endKeys = array_filter_key($icsEvent, function ($key) {
-						return strpos($key, 'DTEND') === 0;
-					});
-					if (!empty($startKeys) && !empty($endKeys) && isset($icsEvent['SUMMARY'])) {
-						/* Getting start date and time */
-						$repeat = isset($icsEvent ['RRULE']) ? $icsEvent ['RRULE'] : false;
-						if (!$originalTimeZone) {
-							$tzKey = array_keys($startKeys);
-							if (strpos($tzKey[0], 'TZID=') !== false) {
-								$originalTimeZone = explode('TZID=', (string)$tzKey[0]);
-								$originalTimeZone = (count($originalTimeZone) >= 2) ? str_replace('"', '', $originalTimeZone[1]) : false;
-							}
-						}
-						$start = reset($startKeys);
-						$end = reset($endKeys);
-						$totalDays = $GLOBALS['calendarStart'] + $GLOBALS['calendarEnd'];
-						if ($repeat) {
-							$repeatOverride = getCalenderRepeatCount(trim($icsEvent["RRULE"]));
-							switch (trim(strtolower(getCalenderRepeat($repeat)))) {
-								case 'daily':
-									$repeat = ($repeatOverride) ? $repeatOverride : $totalDays;
-									$term = 'days';
-									break;
-								case 'weekly':
-									$repeat = ($repeatOverride) ? $repeatOverride : round($totalDays / 7);
-									$term = 'weeks';
-									break;
-								case 'monthly':
-									$repeat = ($repeatOverride) ? $repeatOverride : round($totalDays / 30);
-									$term = 'months';
-									break;
-								case 'yearly':
-									$repeat = ($repeatOverride) ? $repeatOverride : round($totalDays / 365);
-									$term = 'years';
-									break;
-								default:
-									$repeat = ($repeatOverride) ? $repeatOverride : $totalDays;
-									$term = 'days';
-									break;
-							}
-						} else {
-							$repeat = 1;
-							$term = 'day';
-						}
-						$calendarTimes = 0;
-						while ($calendarTimes < $repeat) {
-							$currentDate = new DateTime ($GLOBALS['currentTime']);
-							$oldestDay = new DateTime ($GLOBALS['currentTime']);
-							$oldestDay->modify('-' . $GLOBALS['calendarStart'] . ' days');
-							$newestDay = new DateTime ($GLOBALS['currentTime']);
-							$newestDay->modify('+' . $GLOBALS['calendarEnd'] . ' days');
-							/* Converting to datetime and apply the timezone to get proper date time */
-							$startDt = new DateTime ($start);
-							/* Getting end date with time */
-							$endDt = new DateTime ($end);
-							if ($calendarTimes !== 0) {
-								$dateDiff = date_diff($startDt, $currentDate);
-								$startDt->modify($dateDiff->format('%R') . (round(($dateDiff->days) / 7)) . ' weeks');
-								$startDt->modify('+' . $calendarTimes . ' ' . $term);
-								$endDt->modify($dateDiff->format('%R') . (round(($dateDiff->days) / 7)) . ' weeks');
-								$endDt->modify('+' . $calendarTimes . ' ' . $term);
-							} elseif ($calendarTimes == 0 && $repeat !== 1) {
-								$dateDiff = date_diff($startDt, $currentDate);
-								$startDt->modify($dateDiff->format('%R') . (round(($dateDiff->days) / 7)) . ' weeks');
-								$endDt->modify($dateDiff->format('%R') . (round(($dateDiff->days) / 7)) . ' weeks');
-							}
-							$calendarStartDiff = date_diff($startDt, $newestDay);
-							$calendarEndDiff = date_diff($startDt, $oldestDay);
-							if ($originalTimeZone && $originalTimeZone !== 'UTC' && (strpos($start, 'Z') == false)) {
-								$originalTimeZone = calendarStandardizeTimezone($originalTimeZone);
-								$dateTimeOriginalTZ = new DateTimeZone($originalTimeZone);
-								$dateTimeOriginal = new DateTime('now', $dateTimeOriginalTZ);
-								$dateTimeUTCTZ = new DateTimeZone(date_default_timezone_get());
-								$dateTimeUTC = new DateTime('now', $dateTimeUTCTZ);
-								$dateTimeOriginalOffset = $dateTimeOriginal->getOffset() / 3600;
-								$dateTimeUTCOffset = $dateTimeUTC->getOffset() / 3600;
-								$diff = $dateTimeUTCOffset - $dateTimeOriginalOffset;
-								$startDt->modify('+ ' . $diff . ' hour');
-								$endDt->modify('+ ' . $diff . ' hour');
-							}
-							$startDt->setTimeZone(new DateTimezone ($timeZone));
-							$endDt->setTimeZone(new DateTimezone ($timeZone));
-							$startDate = $startDt->format(DateTime::ATOM);
-							$endDate = $endDt->format(DateTime::ATOM);
-							if (new DateTime() < $endDt) {
-								$extraClass = 'text-info';
-							} else {
-								$extraClass = 'text-success';
-							}
-							/* Getting the name of event */
-							$eventName = $icsEvent['SUMMARY'];
-							if (!calendarDaysCheck($calendarStartDiff->format('%R') . $calendarStartDiff->days, $calendarEndDiff->format('%R') . $calendarEndDiff->days)) {
-								break;
-							}
-							if (isset($icsEvent["RRULE"]) && getCalenderRepeatUntil(trim($icsEvent["RRULE"]))) {
-								$untilDate = new DateTime (getCalenderRepeatUntil(trim($icsEvent["RRULE"])));
-								$untilDiff = date_diff($currentDate, $untilDate);
-								if ($untilDiff->days > 0) {
-									break;
-								}
-							}
-							$icalEvents[] = array(
-								'title' => $eventName,
-								'imagetype' => 'calendar-o text-warning text-custom-calendar ' . $extraClass,
-								'imagetypeFilter' => 'ical',
-								'className' => 'bg-calendar calendar-item bg-custom-calendar',
-								'start' => $startDate,
-								'end' => $endDate,
-								'bgColor' => str_replace('text', 'bg', $extraClass),
-							);
-							$calendarTimes = $calendarTimes + 1;
-						}
-					}
-				}
-			}
-		}
-		$calendarSources['ical'] = $icalEvents;
-	}
-	$calendarSources['events'] = $calendarItems;
-	return ($calendarSources) ? $calendarSources : false;
-}
-
-function calendarDaysCheck($entryStart, $entryEnd)
-{
-	$success = false;
-	$entryStart = intval($entryStart);
-	$entryEnd = intval($entryEnd);
-	if ($entryStart >= 0 && $entryEnd <= 0) {
-		$success = true;
-	}
-	return $success;
-}
-
-function calendarStandardizeTimezone($timezone)
-{
-	switch ($timezone) {
-		case('CST'):
-		case('Central Time'):
-		case('Central Standard Time'):
-			$timezone = 'America/Chicago';
-			break;
-		case('CET'):
-		case('Central European Time'):
-			$timezone = 'Europe/Berlin';
-			break;
-		case('EST'):
-		case('Eastern Time'):
-		case('Eastern Standard Time'):
-			$timezone = 'America/New_York';
-			break;
-		case('PST'):
-		case('Pacific Time'):
-		case('Pacific Standard Time'):
-			$timezone = 'America/Los_Angeles';
-			break;
-		case('China Time'):
-		case('China Standard Time'):
-			$timezone = 'Asia/Beijing';
-			break;
-		case('IST'):
-		case('India Time'):
-		case('India Standard Time'):
-			$timezone = 'Asia/New_Delhi';
-			break;
-		case('JST');
-		case('Japan Time'):
-		case('Japan Standard Time'):
-			$timezone = 'Asia/Tokyo';
-			break;
-	}
-	return $timezone;
-}
-
-function getCalenderRepeat($value)
-{
-	//FREQ=DAILY
-	//RRULE:FREQ=WEEKLY;BYDAY=TH
-	$first = explode('=', $value);
-	if (count($first) > 1) {
-		$second = explode(';', $first[1]);
-	} else {
-		return $value;
-	}
-	if ($second) {
-		return $second[0];
-	} else {
-		return $first[1];
-	}
-}
-
-function getCalenderRepeatUntil($value)
-{
-	$first = explode('UNTIL=', $value);
-	if (count($first) > 1) {
-		if (strpos($first[1], ';') !== false) {
-			$check = explode(';', $first[1]);
-			return $check[0];
-		} else {
-			return $first[1];
-		}
-	} else {
-		return false;
-	}
-}
-
-function getCalenderRepeatCount($value)
-{
-	$first = explode('COUNT=', $value);
-	if (count($first) > 1) {
-		return $first[1];
-	} else {
-		return false;
-	}
-}
-
-function getSonarrCalendar($array, $number)
-{
-	$array = json_decode($array, true);
-	$gotCalendar = array();
-	$i = 0;
-	foreach ($array as $child) {
-		$i++;
-		$seriesName = $child['series']['title'];
-		$seriesID = $child['series']['tvdbId'];
-		$episodeID = $child['series']['tvdbId'];
-		$monitored = $child['monitored'];
-		if (!isset($episodeID)) {
-			$episodeID = "";
-		}
-		//$episodeName = htmlentities($child['title'], ENT_QUOTES);
-		$episodeAirDate = $child['airDateUtc'];
-		$episodeAirDate = strtotime($episodeAirDate);
-		$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
-		if (new DateTime() < new DateTime($episodeAirDate)) {
-			$unaired = true;
-		}
-		if ($child['episodeNumber'] == "1") {
-			$episodePremier = "true";
-		} else {
-			$episodePremier = "false";
-			$date = new DateTime($episodeAirDate);
-			$date->add(new DateInterval("PT1S"));
-			$date->format(DateTime::ATOM);
-			$child['airDateUtc'] = gmdate('Y-m-d\TH:i:s\Z', strtotime($date->format(DateTime::ATOM)));
-		}
-		$downloaded = $child['hasFile'];
-		if ($downloaded == "0" && isset($unaired) && $episodePremier == "true") {
-			$downloaded = "text-primary animated flash";
-		} elseif ($downloaded == "0" && isset($unaired) && $monitored == "0") {
-			$downloaded = "text-dark";
-		} elseif ($downloaded == "0" && isset($unaired)) {
-			$downloaded = "text-info";
-		} elseif ($downloaded == "1") {
-			$downloaded = "text-success";
-		} else {
-			$downloaded = "text-danger";
-		}
-		$fanart = "/plugins/images/cache/no-np.png";
-		foreach ($child['series']['images'] as $image) {
-			if ($image['coverType'] == "fanart") {
-				$fanart = $image['url'];
-			}
-		}
-		if ($fanart !== "/plugins/images/cache/no-np.png" || (strpos($fanart, '://') === false)) {
-			$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
-			$imageURL = $fanart;
-			$cacheFile = $cacheDirectory . $seriesID . '.jpg';
-			$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
-			if (!file_exists($cacheFile)) {
-				cacheImage($imageURL, $seriesID);
-				unset($imageURL);
-				unset($cacheFile);
-			}
-		}
-		$bottomTitle = 'S' . sprintf("%02d", $child['seasonNumber']) . 'E' . sprintf("%02d", $child['episodeNumber']) . ' - ' . $child['title'];
-		$details = array(
-			"seasonCount" => $child['series']['seasonCount'],
-			"status" => $child['series']['status'],
-			"topTitle" => $seriesName,
-			"bottomTitle" => $bottomTitle,
-			"overview" => isset($child['overview']) ? $child['overview'] : '',
-			"runtime" => $child['series']['runtime'],
-			"image" => $fanart,
-			"ratings" => $child['series']['ratings']['value'],
-			"videoQuality" => $child["hasFile"] && isset($child['episodeFile']['quality']['quality']['name']) ? $child['episodeFile']['quality']['quality']['name'] : "unknown",
-			"audioChannels" => $child["hasFile"] && isset($child['episodeFile']['mediaInfo']) ? $child['episodeFile']['mediaInfo']['audioChannels'] : "unknown",
-			"audioCodec" => $child["hasFile"] && isset($child['episodeFile']['mediaInfo']) ? $child['episodeFile']['mediaInfo']['audioCodec'] : "unknown",
-			"videoCodec" => $child["hasFile"] && isset($child['episodeFile']['mediaInfo']) ? $child['episodeFile']['mediaInfo']['videoCodec'] : "unknown",
-			"size" => $child["hasFile"] && isset($child['episodeFile']['size']) ? $child['episodeFile']['size'] : "unknown",
-			"genres" => $child['series']['genres'],
-		);
-		array_push($gotCalendar, array(
-			"id" => "Sonarr-" . $number . "-" . $i,
-			"title" => $seriesName,
-			"start" => $child['airDateUtc'],
-			"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
-			"imagetype" => "tv " . $downloaded,
-			"imagetypeFilter" => "tv",
-			"downloadFilter" => $downloaded,
-			"bgColor" => str_replace('text', 'bg', $downloaded),
-			"details" => $details
-		));
-	}
-	if ($i != 0) {
-		return $gotCalendar;
-	}
-	return false;
-}
-
-function getLidarrCalendar($array, $number)
-{
-	$array = json_decode($array, true);
-	$gotCalendar = array();
-	$i = 0;
-	foreach ($array as $child) {
-		$i++;
-		$albumName = $child['title'];
-		$artistName = $child['artist']['artistName'];
-		$albumID = '';
-		$releaseDate = $child['releaseDate'];
-		$releaseDate = strtotime($releaseDate);
-		$releaseDate = date("Y-m-d H:i:s", $releaseDate);
-		if (new DateTime() < new DateTime($releaseDate)) {
-			$unaired = true;
-		}
-		if (isset($child['statistics']['percentOfTracks'])) {
-			if ($child['statistics']['percentOfTracks'] == '100.0') {
-				$downloaded = '1';
-			} else {
-				$downloaded = '0';
-			}
-		} else {
-			$downloaded = '0';
-		}
-		if ($downloaded == "0" && isset($unaired)) {
-			$downloaded = "text-info";
-		} elseif ($downloaded == "1") {
-			$downloaded = "text-success";
-		} else {
-			$downloaded = "text-danger";
-		}
-		$fanart = "/plugins/images/cache/no-np.png";
-		foreach ($child['artist']['images'] as $image) {
-			if ($image['coverType'] == "fanart") {
-				$fanart = str_replace('http://', 'https://', $image['url']);
-			}
-		}
-		$details = array(
-			"seasonCount" => '',
-			"status" => '',
-			"topTitle" => $albumName,
-			"bottomTitle" => $artistName,
-			"overview" => isset($child['artist']['overview']) ? $child['artist']['overview'] : '',
-			"runtime" => '',
-			"image" => $fanart,
-			"ratings" => $child['artist']['ratings']['value'],
-			"videoQuality" => "unknown",
-			"audioChannels" => "unknown",
-			"audioCodec" => "unknown",
-			"videoCodec" => "unknown",
-			"size" => "unknown",
-			"genres" => $child['genres'],
-		);
-		array_push($gotCalendar, array(
-			"id" => "Lidarr-" . $number . "-" . $i,
-			"title" => $artistName,
-			"start" => $child['releaseDate'],
-			"className" => "inline-popups bg-calendar calendar-item musicID--",
-			"imagetype" => "music " . $downloaded,
-			"imagetypeFilter" => "music",
-			"downloadFilter" => $downloaded,
-			"bgColor" => str_replace('text', 'bg', $downloaded),
-			"details" => $details,
-			"data" => $child
-		));
-	}
-	if ($i != 0) {
-		return $gotCalendar;
-	}
-	return false;
-}
-
-function getRadarrCalendar($array, $number, $url)
-{
-	$url = rtrim($url, '/'); //remove trailing slash
-	$url = $url . '/api';
-	$array = json_decode($array, true);
-	$gotCalendar = array();
-	$i = 0;
-	foreach ($array as $child) {
-		if (isset($child['physicalRelease'])) {
-			$i++;
-			$movieName = $child['title'];
-			$movieID = $child['tmdbId'];
-			if (!isset($movieID)) {
-				$movieID = "";
-			}
-			$physicalRelease = $child['physicalRelease'];
-			$physicalRelease = strtotime($physicalRelease);
-			$physicalRelease = date("Y-m-d", $physicalRelease);
-			if (new DateTime() < new DateTime($physicalRelease)) {
-				$notReleased = "true";
-			} else {
-				$notReleased = "false";
-			}
-			$downloaded = $child['hasFile'];
-			if ($downloaded == "0" && $notReleased == "true") {
-				$downloaded = "text-info";
-			} elseif ($downloaded == "1") {
-				$downloaded = "text-success";
-			} else {
-				$downloaded = "text-danger";
-			}
-			$banner = "/plugins/images/cache/no-np.png";
-			foreach ($child['images'] as $image) {
-				if ($image['coverType'] == "banner" || $image['coverType'] == "fanart") {
-					if (strpos($image['url'], '://') === false) {
-						$imageUrl = $image['url'];
-						$urlParts = explode("/", $url);
-						$imageParts = explode("/", $image['url']);
-						if ($imageParts[1] == end($urlParts)) {
-							unset($imageParts[1]);
-							$imageUrl = implode("/", $imageParts);
-						}
-						$banner = $url . $imageUrl . '?apikey=' . $GLOBALS['radarrToken'];
-					} else {
-						$banner = $image['url'];
-					}
-					
-				}
-			}
-			if ($banner !== "/plugins/images/cache/no-np.png" || (strpos($banner, 'apikey') !== false)) {
-				$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
-				$imageURL = $banner;
-				$cacheFile = $cacheDirectory . $movieID . '.jpg';
-				$banner = 'plugins/images/cache/' . $movieID . '.jpg';
-				if (!file_exists($cacheFile)) {
-					cacheImage($imageURL, $movieID);
-					unset($imageURL);
-					unset($cacheFile);
-				}
-			}
-			$alternativeTitles = "";
-			foreach ($child['alternativeTitles'] as $alternative) {
-				$alternativeTitles .= $alternative['title'] . ', ';
-			}
-			$alternativeTitles = empty($child['alternativeTitles']) ? "" : substr($alternativeTitles, 0, -2);
-			$details = array(
-				"topTitle" => $movieName,
-				"bottomTitle" => $alternativeTitles,
-				"status" => $child['status'],
-				"overview" => $child['overview'],
-				"runtime" => $child['runtime'],
-				"image" => $banner,
-				"ratings" => $child['ratings']['value'],
-				"videoQuality" => $child["hasFile"] ? @$child['movieFile']['quality']['quality']['name'] : "unknown",
-				"audioChannels" => $child["hasFile"] ? @$child['movieFile']['mediaInfo']['audioChannels'] : "unknown",
-				"audioCodec" => $child["hasFile"] ? @$child['movieFile']['mediaInfo']['audioFormat'] : "unknown",
-				"videoCodec" => $child["hasFile"] ? @$child['movieFile']['mediaInfo']['videoCodec'] : "unknown",
-				"size" => $child["hasFile"] ? @$child['movieFile']['size'] : "unknown",
-				"genres" => $child['genres'],
-				"year" => isset($child['year']) ? $child['year'] : '',
-				"studio" => isset($child['studio']) ? $child['studio'] : '',
-			);
-			array_push($gotCalendar, array(
-				"id" => "Radarr-" . $number . "-" . $i,
-				"title" => $movieName,
-				"start" => $physicalRelease,
-				"className" => "inline-popups bg-calendar movieID--" . $movieID,
-				"imagetype" => "film " . $downloaded,
-				"imagetypeFilter" => "film",
-				"downloadFilter" => $downloaded,
-				"bgColor" => str_replace('text', 'bg', $downloaded),
-				"details" => $details
-			));
-		}
-	}
-	if ($i != 0) {
-		return $gotCalendar;
-	}
-	return false;
-}
-
-function getCouchCalendar($array, $number)
-{
-	$api = json_decode($array, true);
-	$gotCalendar = array();
-	$i = 0;
-	foreach ($api['movies'] as $child) {
-		if ($child['status'] == "active" || $child['status'] == "done") {
-			$i++;
-			$movieName = $child['info']['original_title'];
-			$movieID = $child['info']['tmdb_id'];
-			if (!isset($movieID)) {
-				$movieID = "";
-			}
-			$physicalRelease = (isset($child['info']['released']) ? $child['info']['released'] : null);
-			$backupRelease = (isset($child['info']['release_date']['theater']) ? $child['info']['release_date']['theater'] : null);
-			$physicalRelease = (isset($physicalRelease) ? $physicalRelease : $backupRelease);
-			$physicalRelease = strtotime($physicalRelease);
-			$physicalRelease = date("Y-m-d", $physicalRelease);
-			if (new DateTime() < new DateTime($physicalRelease)) {
-				$notReleased = "true";
-			} else {
-				$notReleased = "false";
-			}
-			$downloaded = ($child['status'] == "active") ? "0" : "1";
-			if ($downloaded == "0" && $notReleased == "true") {
-				$downloaded = "text-info";
-			} elseif ($downloaded == "1") {
-				$downloaded = "text-success";
-			} else {
-				$downloaded = "text-danger";
-			}
-			if (!empty($child['info']['images']['backdrop_original'])) {
-				$banner = $child['info']['images']['backdrop_original'][0];
-			} elseif (!empty($child['info']['images']['backdrop'])) {
-				$banner = $child['info']['images']['backdrop_original'][0];
-			} else {
-				$banner = "/plugins/images/cache/no-np.png";
-			}
-			if ($banner !== "/plugins/images/cache/no-np.png") {
-				$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
-				$imageURL = $banner;
-				$cacheFile = $cacheDirectory . $movieID . '.jpg';
-				$banner = 'plugins/images/cache/' . $movieID . '.jpg';
-				if (!file_exists($cacheFile)) {
-					cacheImage($imageURL, $movieID);
-					unset($imageURL);
-					unset($cacheFile);
-				}
-			}
-			$hasFile = (!empty($child['releases']) && !empty($child['releases'][0]['files']['movie']));
-			$details = array(
-				"topTitle" => $movieName,
-				"bottomTitle" => $child['info']['tagline'],
-				"status" => $child['status'],
-				"overview" => $child['info']['plot'],
-				"runtime" => $child['info']['runtime'],
-				"image" => $banner,
-				"ratings" => isset($child['info']['rating']['imdb'][0]) ? $child['info']['rating']['imdb'][0] : '',
-				"videoQuality" => $hasFile ? $child['releases'][0]['quality'] : "unknown",
-				"audioChannels" => "",
-				"audioCodec" => "",
-				"videoCodec" => "",
-				"genres" => $child['info']['genres'],
-				"year" => isset($child['info']['year']) ? $child['info']['year'] : '',
-				"studio" => isset($child['info']['year']) ? $child['info']['year'] : '',
-			);
-			array_push($gotCalendar, array(
-				"id" => "CouchPotato-" . $number . "-" . $i,
-				"title" => $movieName,
-				"start" => $physicalRelease,
-				"className" => "inline-popups bg-calendar calendar-item movieID--" . $movieID,
-				"imagetype" => "film " . $downloaded,
-				"imagetypeFilter" => "film",
-				"downloadFilter" => $downloaded,
-				"bgColor" => str_replace('text', 'bg', $downloaded),
-				"details" => $details
-			));
-		}
-	}
-	if ($i != 0) {
-		return $gotCalendar;
-	}
-	return false;
-}
-
-function getSickrageCalendarWanted($array, $number)
-{
-	$array = json_decode($array, true);
-	$gotCalendar = array();
-	$i = 0;
-	foreach ($array['data']['missed'] as $child) {
-		$i++;
-		$seriesName = $child['show_name'];
-		$seriesID = $child['tvdbid'];
-		$episodeID = $child['tvdbid'];
-		$episodeAirDate = $child['airdate'];
-		$episodeAirDateTime = explode(" ", $child['airs']);
-		$episodeAirDateTime = date("H:i:s", strtotime($episodeAirDateTime[1] . $episodeAirDateTime[2]));
-		$episodeAirDate = strtotime($episodeAirDate . $episodeAirDateTime);
-		$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
-		if (new DateTime() < new DateTime($episodeAirDate)) {
-			$unaired = true;
-		}
-		$downloaded = "0";
-		if ($downloaded == "0" && isset($unaired)) {
-			$downloaded = "text-info";
-		} elseif ($downloaded == "1") {
-			$downloaded = "text-success";
-		} else {
-			$downloaded = "text-danger";
-		}
-		$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']) . ' - ' . $child['ep_name'];
-		$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
-		$cacheFile = $cacheDirectory . $seriesID . '.jpg';
-		$fanart = "/plugins/images/cache/no-np.png";
-		if (file_exists($cacheFile)) {
-			$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
-			unset($cacheFile);
-		}
-		$details = array(
-			"seasonCount" => "",
-			"status" => $child['show_status'],
-			"topTitle" => $seriesName,
-			"bottomTitle" => $bottomTitle,
-			"overview" => isset($child['ep_plot']) ? $child['ep_plot'] : '',
-			"runtime" => "",
-			"image" => $fanart,
-			"ratings" => "",
-			"videoQuality" => isset($child["quality"]) ? $child["quality"] : "",
-			"audioChannels" => "",
-			"audioCodec" => "",
-			"videoCodec" => "",
-			"size" => "",
-			"genres" => "",
-		);
-		array_push($gotCalendar, array(
-			"id" => "Sick-" . $number . "-Miss-" . $i,
-			"title" => $seriesName,
-			"start" => $episodeAirDate,
-			"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
-			"imagetype" => "tv " . $downloaded,
-			"imagetypeFilter" => "tv",
-			"downloadFilter" => $downloaded,
-			"bgColor" => str_replace('text', 'bg', $downloaded),
-			"details" => $details,
-		));
-	}
-	foreach ($array['data']['today'] as $child) {
-		$i++;
-		$seriesName = $child['show_name'];
-		$seriesID = $child['tvdbid'];
-		$episodeID = $child['tvdbid'];
-		$episodeAirDate = $child['airdate'];
-		$episodeAirDateTime = explode(" ", $child['airs']);
-		$episodeAirDateTime = date("H:i:s", strtotime($episodeAirDateTime[1] . $episodeAirDateTime[2]));
-		$episodeAirDate = strtotime($episodeAirDate . $episodeAirDateTime);
-		$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
-		if (new DateTime() < new DateTime($episodeAirDate)) {
-			$unaired = true;
-		}
-		$downloaded = "0";
-		if ($downloaded == "0" && isset($unaired)) {
-			$downloaded = "text-info";
-		} elseif ($downloaded == "1") {
-			$downloaded = "text-success";
-		} else {
-			$downloaded = "text-danger";
-		}
-		$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']) . ' - ' . $child['ep_name'];
-		$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
-		$cacheFile = $cacheDirectory . $seriesID . '.jpg';
-		$fanart = "/plugins/images/cache/no-np.png";
-		if (file_exists($cacheFile)) {
-			$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
-			unset($cacheFile);
-		}
-		$details = array(
-			"seasonCount" => "",
-			"status" => $child['show_status'],
-			"topTitle" => $seriesName,
-			"bottomTitle" => $bottomTitle,
-			"overview" => isset($child['ep_plot']) ? $child['ep_plot'] : '',
-			"runtime" => "",
-			"image" => $fanart,
-			"ratings" => "",
-			"videoQuality" => isset($child["quality"]) ? $child["quality"] : "",
-			"audioChannels" => "",
-			"audioCodec" => "",
-			"videoCodec" => "",
-			"size" => "",
-			"genres" => "",
-		);
-		array_push($gotCalendar, array(
-			"id" => "Sick-" . $number . "-Today-" . $i,
-			"title" => $seriesName,
-			"start" => $episodeAirDate,
-			"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
-			"imagetype" => "tv " . $downloaded,
-			"imagetypeFilter" => "tv",
-			"downloadFilter" => $downloaded,
-			"bgColor" => str_replace('text', 'bg', $downloaded),
-			"details" => $details,
-		));
-	}
-	foreach ($array['data']['soon'] as $child) {
-		$i++;
-		$seriesName = $child['show_name'];
-		$seriesID = $child['tvdbid'];
-		$episodeID = $child['tvdbid'];
-		$episodeAirDate = $child['airdate'];
-		$episodeAirDateTime = explode(" ", $child['airs']);
-		$episodeAirDateTime = date("H:i:s", strtotime($episodeAirDateTime[1] . $episodeAirDateTime[2]));
-		$episodeAirDate = strtotime($episodeAirDate . $episodeAirDateTime);
-		$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
-		if (new DateTime() < new DateTime($episodeAirDate)) {
-			$unaired = true;
-		}
-		$downloaded = "0";
-		if ($downloaded == "0" && isset($unaired)) {
-			$downloaded = "text-info";
-		} elseif ($downloaded == "1") {
-			$downloaded = "text-success";
-		} else {
-			$downloaded = "text-danger";
-		}
-		$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']) . ' - ' . $child['ep_name'];
-		$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
-		$cacheFile = $cacheDirectory . $seriesID . '.jpg';
-		$fanart = "/plugins/images/cache/no-np.png";
-		if (file_exists($cacheFile)) {
-			$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
-			unset($cacheFile);
-		}
-		$details = array(
-			"seasonCount" => "",
-			"status" => $child['show_status'],
-			"topTitle" => $seriesName,
-			"bottomTitle" => $bottomTitle,
-			"overview" => isset($child['ep_plot']) ? $child['ep_plot'] : '',
-			"runtime" => "",
-			"image" => $fanart,
-			"ratings" => "",
-			"videoQuality" => isset($child["quality"]) ? $child["quality"] : "",
-			"audioChannels" => "",
-			"audioCodec" => "",
-			"videoCodec" => "",
-			"size" => "",
-			"genres" => "",
-		);
-		array_push($gotCalendar, array(
-			"id" => "Sick-" . $number . "-Soon-" . $i,
-			"title" => $seriesName,
-			"start" => $episodeAirDate,
-			"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
-			"imagetype" => "tv " . $downloaded,
-			"imagetypeFilter" => "tv",
-			"downloadFilter" => $downloaded,
-			"bgColor" => str_replace('text', 'bg', $downloaded),
-			"details" => $details,
-		));
-	}
-	foreach ($array['data']['later'] as $child) {
-		$i++;
-		$seriesName = $child['show_name'];
-		$seriesID = $child['tvdbid'];
-		$episodeID = $child['tvdbid'];
-		$episodeAirDate = $child['airdate'];
-		$episodeAirDateTime = explode(" ", $child['airs']);
-		$episodeAirDateTime = date("H:i:s", strtotime($episodeAirDateTime[1] . $episodeAirDateTime[2]));
-		$episodeAirDate = strtotime($episodeAirDate . $episodeAirDateTime);
-		$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
-		if (new DateTime() < new DateTime($episodeAirDate)) {
-			$unaired = true;
-		}
-		$downloaded = "0";
-		if ($downloaded == "0" && isset($unaired)) {
-			$downloaded = "text-info";
-		} elseif ($downloaded == "1") {
-			$downloaded = "text-success";
+	
+	public function streamType($value)
+	{
+		if ($value == "transcode" || $value == "Transcode") {
+			return "Transcode";
+		} elseif ($value == "copy" || $value == "DirectStream") {
+			return "Direct Stream";
+		} elseif ($value == "directplay" || $value == "DirectPlay") {
+			return "Direct Play";
 		} else {
-			$downloaded = "text-danger";
-		}
-		$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']) . ' - ' . $child['ep_name'];
-		$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
-		$cacheFile = $cacheDirectory . $seriesID . '.jpg';
-		$fanart = "/plugins/images/cache/no-np.png";
-		if (file_exists($cacheFile)) {
-			$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
-			unset($cacheFile);
-		}
-		$details = array(
-			"seasonCount" => "",
-			"status" => $child['show_status'],
-			"topTitle" => $seriesName,
-			"bottomTitle" => $bottomTitle,
-			"overview" => isset($child['ep_plot']) ? $child['ep_plot'] : '',
-			"runtime" => "",
-			"image" => $fanart,
-			"ratings" => "",
-			"videoQuality" => isset($child["quality"]) ? $child["quality"] : "",
-			"audioChannels" => "",
-			"audioCodec" => "",
-			"videoCodec" => "",
-			"size" => "",
-			"genres" => "",
-		);
-		array_push($gotCalendar, array(
-			"id" => "Sick-" . $number . "-Later-" . $i,
-			"title" => $seriesName,
-			"start" => $episodeAirDate,
-			"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
-			"imagetype" => "tv " . $downloaded,
-			"imagetypeFilter" => "tv",
-			"downloadFilter" => $downloaded,
-			"bgColor" => str_replace('text', 'bg', $downloaded),
-			"details" => $details,
-		));
-	}
-	if ($i != 0) {
-		return $gotCalendar;
-	}
-	return false;
-}
-
-function getSickrageCalendarHistory($array, $number)
-{
-	$array = json_decode($array, true);
-	$gotCalendar = array();
-	$i = 0;
-	foreach ($array['data'] as $child) {
-		$i++;
-		$seriesName = $child['show_name'];
-		$seriesID = $child['tvdbid'];
-		$episodeID = $child['tvdbid'];
-		$episodeAirDate = $child['date'];
-		$downloaded = "text-success";
-		$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']);
-		$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
-		$cacheFile = $cacheDirectory . $seriesID . '.jpg';
-		$fanart = "/plugins/images/cache/no-np.png";
-		if (file_exists($cacheFile)) {
-			$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
-			unset($cacheFile);
+			return "Direct Play";
 		}
-		$details = array(
-			"seasonCount" => "",
-			"status" => $child['status'],
-			"topTitle" => $seriesName,
-			"bottomTitle" => $bottomTitle,
-			"overview" => '',
-			"runtime" => isset($child['series']['runtime']) ? $child['series']['runtime'] : 30,
-			"image" => $fanart,
-			"ratings" => isset($child['series']['ratings']['value']) ? $child['series']['ratings']['value'] : "unknown",
-			"videoQuality" => isset($child["quality"]) ? $child['quality'] : "unknown",
-			"audioChannels" => "",
-			"audioCodec" => "",
-			"videoCodec" => "",
-			"size" => "",
-			"genres" => "",
-		);
-		array_push($gotCalendar, array(
-			"id" => "Sick-" . $number . "-History-" . $i,
-			"title" => $seriesName,
-			"start" => $episodeAirDate,
-			"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
-			"imagetype" => "tv " . $downloaded,
-			"imagetypeFilter" => "tv",
-			"downloadFilter" => $downloaded,
-			"bgColor" => str_replace('text', 'bg', $downloaded),
-			"details" => $details,
-		));
 	}
-	if ($i != 0) {
-		return $gotCalendar;
-	}
-	return false;
-}
-
-function ombiAPI($array)
-{
-	return ombiAction($array['data']['id'], $array['data']['action'], $array['data']['type'], $array['data']);
-}
-
-function ombiImport($type = null)
-{
-	if (!empty($GLOBALS['ombiURL']) && !empty($GLOBALS['ombiToken']) && !empty($type)) {
-		try {
-			$url = qualifyURL($GLOBALS['ombiURL']);
-			$headers = array(
-				"Accept" => "application/json",
-				"Content-Type" => "application/json",
-				"Apikey" => $GLOBALS['ombiToken']
-			);
-			$options = (localURL($url)) ? array('verify' => false) : array();
-			switch ($type) {
-				case 'emby':
-				case 'emby_local':
-				case 'emby_connect':
-				case 'emby_all':
-					$response = Requests::post($url . "/api/v1/Job/embyuserimporter", $headers, $options);
-					break;
-				case 'plex':
-					$response = Requests::post($url . "/api/v1/Job/plexuserimporter", $headers, $options);
-					break;
-				default:
-					return false;
-					break;
-			}
-			if ($response->success) {
-				writeLog('success', 'OMBI Connect Function - Ran User Import', 'SYSTEM');
-				return true;
-			} else {
-				writeLog('error', 'OMBI Connect Function - Error: Connection Unsuccessful', 'SYSTEM');
-				return false;
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-			return false;
-		};
-	}
-	return false;
-}
-
-function ombiAction($id, $action, $type, $fullArray = null)
-{
-	if ($GLOBALS['homepageOmbiEnabled'] && !empty($GLOBALS['ombiURL']) && !empty($GLOBALS['ombiToken']) && qualifyRequest($GLOBALS['homepageOmbiAuth'])) {
-		$url = qualifyURL($GLOBALS['ombiURL']);
-		$headers = array(
-			"Accept" => "application/json",
-			"Content-Type" => "application/json",
-			"Apikey" => $GLOBALS['ombiToken']
-		);
-		$data = array(
-			'id' => $id,
-		);
+	
+	public function getCacheImageSize($type)
+	{
 		switch ($type) {
-			case 'season':
-			case 'tv':
-				$type = 'tv';
-				$add = array(
-					'tvDbId' => $id,
-					'requestAll' => ombiTVDefault('all'),
-					'latestSeason' => ombiTVDefault('last'),
-					'firstSeason' => ombiTVDefault('first')
-				);
-				break;
-			default:
-				$type = 'movie';
-				$add = array("theMovieDbId" => (int)$id);
-				break;
-		}
-		$success['head'] = $headers;
-		$success['act'] = $action;
-		$success['data'] = $data;
-		$success['add'] = $add;
-		$success['type'] = $type;
-		try {
-			$options = (localURL($url)) ? array('verify' => false) : array();
-			switch ($action) {
-				case 'add':
-					if (isset($_COOKIE['Auth'])) {
-						$headers = array(
-							"Accept" => "application/json",
-							"Content-Type" => "application/json",
-							"Authorization" => "Bearer " . $_COOKIE['Auth']
-						);
-						$success['head'] = $headers;
-					} else {
-						return false;
-					}
-					$response = Requests::post($url . "/api/v1/Request/" . $type, $headers, json_encode($add), $options);
-					break;
-				default:
-					if (qualifyRequest(1)) {
-						switch ($action) {
-							case 'approve':
-								$response = Requests::post($url . "/api/v1/Request/" . $type . "/approve", $headers, json_encode($data), $options);
-								break;
-							case 'available':
-								$response = Requests::post($url . "/api/v1/Request/" . $type . "/available", $headers, json_encode($data), $options);
-								break;
-							case 'unavailable':
-								$response = Requests::post($url . "/api/v1/Request/" . $type . "/unavailable", $headers, json_encode($data), $options);
-								break;
-							case 'deny':
-								$response = Requests::put($url . "/api/v1/Request/" . $type . "/deny", $headers, json_encode($data), $options);
-								break;
-							case 'delete':
-								$response = Requests::delete($url . "/api/v1/Request/" . $type . "/" . $id, $headers, $options);
-								break;
-							default:
-								return false;
-						}
-					}
-					break;
-			}
-			$success['api'] = $response;
-			$success['bd'] = $response->body;
-			$success['hd'] = $response->headers;
-			if ($response->success) {
-				$success['ok'] = true;
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-	}
-	return isset($success['ok']) ? $success : false;
-}
-
-function getOmbiRequests($type = "both", $limit = 50)
-{
-	$api['count'] = array(
-		'movie' => 0,
-		'tv' => 0,
-		'limit' => (integer)$limit
-	);
-	if ($GLOBALS['homepageOmbiEnabled'] && !empty($GLOBALS['ombiURL']) && !empty($GLOBALS['ombiToken']) && qualifyRequest($GLOBALS['homepageOmbiAuth'])) {
-		$url = qualifyURL($GLOBALS['ombiURL']);
-		$headers = array(
-			"Accept" => "application/json",
-			"Apikey" => $GLOBALS['ombiToken'],
-		);
-		$requests = array();
-		try {
-			$options = (localURL($url)) ? array('verify' => false) : array();
-			switch ($type) {
-				case 'movie':
-					$movie = Requests::get($url . "/api/v1/Request/movie", $headers, $options);
-					break;
-				case 'tv':
-					$tv = Requests::get($url . "/api/v1/Request/tv", $headers, $options);
-					break;
-				default:
-					$movie = Requests::get($url . "/api/v1/Request/movie", $headers, $options);
-					$tv = Requests::get($url . "/api/v1/Request/tv", $headers, $options);
-					break;
-			}
-			if ($movie->success || $tv->success) {
-				if (isset($movie)) {
-					$movie = json_decode($movie->body, true);
-					//$movie = array_reverse($movie);
-					foreach ($movie as $key => $value) {
-						$proceed = (($GLOBALS['ombiLimitUser']) && strtolower($GLOBALS['organizrUser']['username']) == strtolower($value['requestedUser']['userName'])) || (!$GLOBALS['ombiLimitUser']) || qualifyRequest(1) ? true : false;
-						if ($proceed) {
-							$api['count']['movie']++;
-							$requests[] = array(
-								'id' => $value['theMovieDbId'],
-								'title' => $value['title'],
-								'overview' => $value['overview'],
-								'poster' => (isset($value['posterPath']) && $value['posterPath'] !== '') ? 'https://image.tmdb.org/t/p/w300/' . $value['posterPath'] : 'plugins/images/cache/no-list.png',
-								'background' => (isset($value['background']) && $value['background'] !== '') ? 'https://image.tmdb.org/t/p/w1280/' . $value['background'] : '',
-								'approved' => $value['approved'],
-								'available' => $value['available'],
-								'denied' => $value['denied'],
-								'deniedReason' => $value['deniedReason'],
-								'user' => $value['requestedUser']['userName'],
-								'userAlias' => $value['requestedUser']['userAlias'],
-								'request_id' => $value['id'],
-								'request_date' => $value['requestedDate'],
-								'release_date' => $value['releaseDate'],
-								'type' => 'movie',
-								'icon' => 'mdi mdi-filmstrip',
-								'color' => 'palette-Deep-Purple-900 bg white',
-							);
-						}
-					}
-				}
-				if (isset($tv) && (is_array($tv) || is_object($tv))) {
-					$tv = json_decode($tv->body, true);
-					foreach ($tv as $key => $value) {
-						if (count($value['childRequests']) > 0) {
-							$proceed = (($GLOBALS['ombiLimitUser']) && strtolower($GLOBALS['organizrUser']['username']) == strtolower($value['childRequests'][0]['requestedUser']['userName'])) || (!$GLOBALS['ombiLimitUser']) || qualifyRequest(1) ? true : false;
-							if ($proceed) {
-								$api['count']['tv']++;
-								$requests[] = array(
-									'id' => $value['tvDbId'],
-									'title' => $value['title'],
-									'overview' => $value['overview'],
-									'poster' => (isset($value['posterPath']) && $value['posterPath'] !== '') ? $value['posterPath'] : 'plugins/images/cache/no-list.png',
-									'background' => (isset($value['background']) && $value['background'] !== '') ? 'https://image.tmdb.org/t/p/w1280/' . $value['background'] : '',
-									'approved' => $value['childRequests'][0]['approved'],
-									'available' => $value['childRequests'][0]['available'],
-									'denied' => $value['childRequests'][0]['denied'],
-									'deniedReason' => $value['childRequests'][0]['deniedReason'],
-									'user' => $value['childRequests'][0]['requestedUser']['userName'],
-									'userAlias' => $value['childRequests'][0]['requestedUser']['userAlias'],
-									'request_id' => $value['id'],
-									'request_date' => $value['childRequests'][0]['requestedDate'],
-									'release_date' => $value['releaseDate'],
-									'type' => 'tv',
-									'icon' => 'mdi mdi-television',
-									'color' => '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) {
-			writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-	}
-	$api['content'] = isset($requests) ? array_slice($requests, 0, $limit) : false;
-	return $api;
-}
-
-function unifiConnect()
-{
-	if ($GLOBALS['homepageUnifiEnabled'] && !empty($GLOBALS['unifiURL']) && !empty($GLOBALS['unifiSiteName']) && !empty($GLOBALS['unifiUsername']) && !empty($GLOBALS['unifiPassword']) && qualifyRequest($GLOBALS['homepageUnifiAuth'])) {
-		$api['content']['unifi'] = array();
-		$url = qualifyURL($GLOBALS['unifiURL']);
-		$urlStat = $url . '/api/s/' . $GLOBALS['unifiSiteName'] . '/stat/health';
-		try {
-			$options = array('verify' => false, 'verifyname' => false, 'follow_redirects' => false);
-			$data = array(
-				'username' => $GLOBALS['unifiUsername'],
-				'password' => decrypt($GLOBALS['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;
-			} else {
-				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);
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'Unifi Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-		$api['content']['unifi'] = isset($api['content']['unifi']) ? $api['content']['unifi'] : false;
-		return $api;
-	}
-	return false;
-}
-
-function getTautulli()
-{
-	if ($GLOBALS['homepageTautulliEnabled'] && !empty($GLOBALS['tautulliURL']) && !empty($GLOBALS['tautulliApikey']) && qualifyRequest($GLOBALS['homepageTautulliAuth'])) {
-		$api = [];
-		$url = qualifyURL($GLOBALS['tautulliURL']);
-		$apiURL = $url . '/api/v2?apikey=' . $GLOBALS['tautulliApikey'];
-		$height = getCacheImageSize('h');
-		$width = getCacheImageSize('w');
-		$nowPlayingHeight = getCacheImageSize('nph');
-		$nowPlayingWidth = getCacheImageSize('npw');
-		try {
-			$homestatsUrl = $apiURL . '&cmd=get_home_stats&grouping=1';
-			$homestats = Requests::get($homestatsUrl, [], []);
-			if ($homestats->success) {
-				$homestats = json_decode($homestats->body, true);
-				$api['homestats'] = $homestats['response'];
-				// Cache art & thumb for first result in each tautulli API result
-				$categories = ['top_movies', 'top_tv', 'popular_movies', 'popular_tv'];
-				foreach ($categories as $cat) {
-					$key = array_search($cat, array_column($api['homestats']['data'], 'stat_id'));
-					$img = $api['homestats']['data'][$key]['rows'][0];
-					cacheImage($url . '/pms_image_proxy?img=' . $img['art'] . '&rating_key=' . $img['rating_key'] . '&width=' . $nowPlayingWidth . '&height=' . $nowPlayingHeight, $img['rating_key'] . '-np');
-					cacheImage($url . '/pms_image_proxy?img=' . $img['thumb'] . '&rating_key=' . $img['rating_key'] . '&width=' . $width . '&height=' . $height, $img['rating_key'] . '-list');
-					$img['art'] = 'plugins/images/cache/' . $img['rating_key'] . '-np.jpg';
-					$img['thumb'] = 'plugins/images/cache/' . $img['rating_key'] . '-list.jpg';
-					$api['homestats']['data'][$key]['rows'][0] = $img;
-				}
-				// Cache the platform icon
-				$key = array_search('top_platforms', array_column($api['homestats']['data'], 'stat_id'));
-				$platform = $api['homestats']['data'][$key]['rows'][0]['platform_name'];
-				cacheImage($url . '/images/platforms/' . $platform . '.svg', 'tautulli-' . $platform, 'svg');
-			}
-			$libstatsUrl = $apiURL . '&cmd=get_libraries';
-			$libstats = Requests::get($libstatsUrl, [], []);
-			if ($libstats->success) {
-				$libstats = json_decode($libstats->body, true);
-				$api['libstats'] = $libstats['response'];
-				$categories = ['movie.svg', 'show.svg', 'artist.svg'];
-				foreach ($categories as $cat) {
-					$parts = explode('.', $cat);
-					cacheImage($url . '/images/libraries/' . $cat, 'tautulli-' . $parts[0], $parts[1]);
-				}
-			}
-			$api['options'] = [
-				'url' => $url,
-				'libraries' => $GLOBALS['tautulliLibraries'],
-				'topMovies' => $GLOBALS['tautulliTopMovies'],
-				'topTV' => $GLOBALS['tautulliTopTV'],
-				'topUsers' => $GLOBALS['tautulliTopUsers'],
-				'topPlatforms' => $GLOBALS['tautulliTopPlatforms'],
-				'popularMovies' => $GLOBALS['tautulliPopularMovies'],
-				'popularTV' => $GLOBALS['tautulliPopularTV'],
-				'title' => $GLOBALS['tautulliHeaderToggle'],
-			];
-			$ids = []; // Array of stat_ids to remove from the returned array
-			if (!qualifyRequest($GLOBALS['homepageTautulliLibraryAuth'])) {
-				$api['options']['libraries'] = false;
-				unset($api['libstats']);
-			}
-			if (!qualifyRequest($GLOBALS['homepageTautulliViewsAuth'])) {
-				$api['options']['topMovies'] = false;
-				$api['options']['topTV'] = false;
-				$api['options']['popularMovies'] = false;
-				$api['options']['popularTV'] = false;
-				$ids = array_merge(['top_movies', 'popular_movies', 'popular_tv', 'top_tv'], $ids);
-				$api['homestats']['data'] = array_values($api['homestats']['data']);
-			}
-			if (!qualifyRequest($GLOBALS['homepageTautulliMiscAuth'])) {
-				$api['options']['topUsers'] = false;
-				$api['options']['topPlatforms'] = false;
-				$ids = array_merge(['top_platforms', 'top_users'], $ids);
-				$api['homestats']['data'] = array_values($api['homestats']['data']);
-			}
-			$ids = array_merge(['top_music', 'popular_music', 'last_watched', 'most_concurrent'], $ids);
-			foreach ($ids as $id) {
-				if ($key = array_search($id, array_column($api['homestats']['data'], 'stat_id'))) {
-					unset($api['homestats']['data'][$key]);
-					$api['homestats']['data'] = array_values($api['homestats']['data']);
-				}
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'Tautulli Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-		$api = isset($api) ? $api : false;
-		return $api;
-	}
-	return false;
-}
-
-function getMonitorr()
-{
-	if ($GLOBALS['homepageMonitorrEnabled'] && !empty($GLOBALS['monitorrURL']) && qualifyRequest($GLOBALS['homepageMonitorrAuth'])) {
-		$api = [];
-		$url = qualifyURL($GLOBALS['monitorrURL']);
-		$dataUrl = $url . '/assets/php/loop.php';
-		try {
-			$response = Requests::get($dataUrl, ['Token' => $GLOBALS['organizrAPI']], []);
-			if ($response->success) {
-				$html = html_entity_decode($response->body);
-				// This section grabs the names of all services by regex
-				$services = [];
-				$servicesMatch = [];
-				$servicePattern = '/<div id="servicetitle"><div>(.*)<\/div><\/div><div class="btnonline">Online<\/div><\/a><\/div><\/div>|<div id="servicetitleoffline".*><div>(.*)<\/div><\/div><div class="btnoffline".*>Offline<\/div><\/div><\/div>|<div id="servicetitlenolink".*><div>(.*)<\/div><\/div><div class="btnonline".*>Online<\/div><\/div><\/div>|<div id="servicetitle"><div>(.*)<\/div><\/div><div class="btnunknown">/';
-				preg_match_all($servicePattern, $html, $servicesMatch);
-				unset($servicesMatch[0]);
-				$servicesMatch = array_values($servicesMatch);
-				foreach ($servicesMatch as $group) {
-					foreach ($group as $service) {
-						if ($service !== '') {
-							array_push($services, $service);
-						}
-					}
-				}
-				// This section then grabs the status and image of that service with regex
-				$statuses = [];
-				foreach ($services as $service) {
-					$statusPattern = '/' . $service . '<\/div><\/div><div class="btnonline">(Online)<\/div>|' . $service . '<\/div><\/div><div class="btnoffline".*>(Offline)<\/div><\/div><\/div>|' . $service . '<\/div><\/div><div class="btnunknown">(.*)<\/div><\/a>/';
-					$status = [];
-					preg_match($statusPattern, $html, $status);
-					$statuses[$service] = $status;
-					foreach ($status as $match) {
-						if ($match == 'Online') {
-							$statuses[$service] = [
-								'status' => true
-							];
-						} else if ($match == 'Offline') {
-							$statuses[$service] = [
-								'status' => false
-							];
-						} else if ($match == 'Unresponsive') {
-							$statuses[$service] = [
-								'status' => 'unresponsive'
-							];
-						}
-					}
-					$imageMatch = [];
-					$imgPattern = '/assets\/img\/\.\.(.*)" class="serviceimg" alt=.*><\/div><\/div><div id="servicetitle"><div>' . $service . '|assets\/img\/\.\.(.*)" class="serviceimg imgoffline" alt=.*><\/div><\/div><div id="servicetitleoffline".*><div>' . $service . '|assets\/img\/\.\.(.*)" class="serviceimg" alt=.*><\/div><\/div><div id="servicetitlenolink".*><div>' . $service . '/';
-					preg_match($imgPattern, $html, $imageMatch);
-					unset($imageMatch[0]);
-					$imageMatch = array_values($imageMatch);
-					// array_push($api['imagematches'][$service], $imageMatch);
-					foreach ($imageMatch as $match) {
-						if ($match !== '') {
-							$image = $match;
-						}
-					}
-					$ext = explode('.', $image);
-					$ext = $ext[key(array_slice($ext, -1, 1, true))];
-					$imageUrl = $url . '/assets' . $image;
-					$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
-					$img = Requests::get($imageUrl, ['Token' => $GLOBALS['organizrAPI']], []);
-					if ($img->success) {
-						$base64 = 'data:image/' . $ext . ';base64,' . base64_encode($img->body);
-						$statuses[$service]['image'] = $base64;
-					} else {
-						$statuses[$service]['image'] = $cacheDirectory . 'no-list.png';
-					}
-					$linkMatch = [];
-					$linkPattern = '/<a class="servicetile" href="(.*)" target="_blank" style="display: block"><div id="serviceimg"><div><img id="' . strtolower($service) . '-service-img/';
-					preg_match($linkPattern, $html, $linkMatch);
-					$linkMatch = array_values($linkMatch);
-					unset($linkMatch[0]);
-					foreach ($linkMatch as $link) {
-						if ($link !== '') {
-							$statuses[$service]['link'] = $link;
-						}
-					}
-				}
-				ksort($statuses);
-				$api['services'] = $statuses;
-				$api['options'] = [
-					'title' => $GLOBALS['monitorrHeader'],
-					'titleToggle' => $GLOBALS['monitorrHeaderToggle'],
-					'compact' => $GLOBALS['monitorrCompact'],
-				];
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'Monitorr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-			$api['error'] = $e->getMessage();
-		};
-		$api = isset($api) ? $api : false;
-		return $api;
-	}
-}
-
-function getSpeedtest()
-{
-	if ($GLOBALS['homepageSpeedtestEnabled'] && !empty($GLOBALS['speedtestURL']) && qualifyRequest($GLOBALS['homepageSpeedtestAuth'])) {
-		$api = [];
-		$url = qualifyURL($GLOBALS['speedtestURL']);
-		$dataUrl = $url . '/api/speedtest/latest';
-		try {
-			$response = Requests::get($dataUrl);
-			if ($response->success) {
-				$json = json_decode($response->body, true);
-				$api['data'] = [
-					'current' => $json['data'],
-					'average' => $json['average'],
-					'max' => $json['max'],
-				];
-				$api['options'] = [
-					'title' => $GLOBALS['speedtestHeader'],
-					'titleToggle' => $GLOBALS['speedtestHeaderToggle'],
-				];
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'Speedtest Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-		$api = isset($api) ? $api : false;
-		return $api;
-	}
-}
-
-function getNetdata()
-{
-	if ($GLOBALS['homepageNetdataEnabled'] && !empty($GLOBALS['netdataURL']) && qualifyRequest($GLOBALS['homepageNetdataAuth'])) {
-		require_once('netdata-functions.php');
-		$api = [];
-		$api['data'] = [];
-		$api['url'] = $GLOBALS['netdataURL'];
-		$url = qualifyURL($GLOBALS['netdataURL']);
-		for ($i = 1; $i < 8; $i++) {
-			if ($GLOBALS['netdata' . ($i) . 'Enabled']) {
-				switch ($GLOBALS['netdata' . $i . 'Data']) {
-					case 'disk-read':
-						$data = disk('in', $url);
-						break;
-					case 'disk-write':
-						$data = disk('out', $url);
-						$data['value'] = abs($data['value']);
-						$data['percent'] = abs($data['percent']);
-						break;
-					case 'cpu':
-						$data = cpu($url);
-						break;
-					case 'net-in':
-						$data = net('received', $url);
-						break;
-					case 'net-out':
-						$data = net('sent', $url);
-						$data['value'] = abs($data['value']);
-						$data['percent'] = abs($data['percent']);
-						break;
-					case 'ram-used':
-						$data = ram($url);
-						break;
-					case 'swap-used':
-						$data = swap($url);
-						break;
-					case 'disk-avail':
-						$data = diskSpace('avail', $url);
-						break;
-					case 'disk-used':
-						$data = diskSpace('used', $url);
-						break;
-					case 'ipmi-temp-c':
-						$data = ipmiTemp($url, 'c');
-						break;
-					case 'ipmi-temp-f':
-						$data = ipmiTemp($url, 'f');
-						break;
-					case 'cpu-temp-c':
-						$data = cpuTemp($url, 'c');
-						break;
-					case 'cpu-temp-f':
-						$data = cpuTemp($url, 'f');
-						break;
-					case 'custom':
-						$data = customNetdata($url, $i);
-						break;
-					default:
-						$data = [
-							'title' => 'DNC',
-							'value' => 0,
-							'units' => 'N/A',
-							'max' => 100,
-						];
-						break;
-				}
-				$data['title'] = $GLOBALS['netdata' . $i . 'Title'];
-				$data['colour'] = $GLOBALS['netdata' . $i . 'Colour'];
-				$data['chart'] = $GLOBALS['netdata' . $i . 'Chart'];
-				$data['size'] = $GLOBALS['netdata' . $i . 'Size'];
-				$data['lg'] = $GLOBALS['netdata' . ($i) . 'lg'];
-				$data['md'] = $GLOBALS['netdata' . ($i) . 'md'];
-				$data['sm'] = $GLOBALS['netdata' . ($i) . 'sm'];
-				array_push($api['data'], $data);
-			}
-		}
-		$api = isset($api) ? $api : false;
-		return $api;
-	}
-}
-
-function testAPIConnection($array)
-{
-	switch ($array['data']['action']) {
-		case 'unifiSite':
-			if (!empty($GLOBALS['unifiURL']) && !empty($GLOBALS['unifiUsername']) && !empty($GLOBALS['unifiPassword'])) {
-				$url = qualifyURL($GLOBALS['unifiURL']);
-				try {
-					$options = array('verify' => false, 'verifyname' => false, 'follow_redirects' => false);
-					$data = array(
-						'username' => $GLOBALS['unifiUsername'],
-						'password' => decrypt($GLOBALS['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;
-					} else {
-						return false;
-					}
-					$headers = array(
-						'cookie' => 'unifises=' . $cookie['unifises'] . ';' . 'csrf_token=' . $cookie['csrf_token'] . ';'
-					);
-					$response = Requests::get($url . '/api/self/sites', $headers, $options);
-					if ($response->success) {
-						$body = json_decode($response->body, true);
-						return $body;
-					} else {
-						return false;
-					}
-				} catch (Requests_Exception $e) {
-					writeLog('error', 'Unifi Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-				};
-			}
-			break;
-		case 'unifi':
-			if (!empty($GLOBALS['unifiURL']) && !empty($GLOBALS['unifiUsername']) && !empty($GLOBALS['unifiPassword']) && !empty($GLOBALS['unifiSiteName'])) {
-				$url = qualifyURL($GLOBALS['unifiURL']);
-				try {
-					$options = array('verify' => false, 'verifyname' => false, 'follow_redirects' => false);
-					$data = array(
-						'username' => $GLOBALS['unifiUsername'],
-						'password' => decrypt($GLOBALS['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;
-					} else {
-						return 'Failed to Login';
-					}
-					$headers = array(
-						'cookie' => 'unifises=' . $cookie['unifises'] . ';' . 'csrf_token=' . $cookie['csrf_token'] . ';'
-					);
-					$response = Requests::get($url . '/api/s/' . $GLOBALS['unifiSiteName'] . '/self', $headers, $options);
-					$body = json_decode($response->body, true);
-					return ($body['meta']['rc'] == 'ok') ? true : $body['meta']['msg'];
-				} catch (Requests_Exception $e) {
-					writeLog('error', 'Unifi Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-				};
-			} else {
-				return 'Not all data is filled in...';
-			}
-			break;
-		case 'ombi':
-			if (!empty($GLOBALS['ombiURL']) && !empty($GLOBALS['ombiToken'])) {
-				$url = qualifyURL($GLOBALS['ombiURL']);
-				$url = $url . "/api/v1/Status/info";
+			case 'height':
+			case 'h':
+				return 300 * $this->config['cacheImageSize'];
+			case 'width':
+			case 'w':
+				return 200 * $this->config['cacheImageSize'];
+			case 'nowPlayingHeight':
+			case 'nph':
+				return 675 * $this->config['cacheImageSize'];
+			case 'nowPlayingWidth':
+			case 'npw':
+				return 1200 * $this->config['cacheImageSize'];
+			
+		}
+	}
+	
+	public function ombiImport($type = null)
+	{
+		if (!empty($this->config['ombiURL']) && !empty($this->config['ombiToken']) && !empty($type)) {
+			try {
+				$url = $this->qualifyURL($this->config['ombiURL']);
 				$headers = array(
 					"Accept" => "application/json",
 					"Content-Type" => "application/json",
 					"Apikey" => $GLOBALS['ombiToken']
 				);
-				try {
-					$options = (localURL($url)) ? array('verify' => false) : array();
-					$response = Requests::get($url, $headers, $options);
-					if ($response->success) {
-						return true;
-					} else {
-						return $response->body;
-					}
-				} catch (Requests_Exception $e) {
-					return $e->getMessage();
-				};
-			} else {
-				return 'URL and/or Token not setup';
-			}
-			break;
-		case 'plex':
-			if (!empty($GLOBALS['plexURL']) && !empty($GLOBALS['plexToken'])) {
-				$url = qualifyURL($GLOBALS['plexURL']);
-				$url = $url . "/?X-Plex-Token=" . $GLOBALS['plexToken'];
-				try {
-					$options = (localURL($url)) ? array('verify' => false) : array();
-					$response = Requests::get($url, array(), $options);
-					libxml_use_internal_errors(true);
-					if ($response->success) {
-						return true;
-					}
-				} catch (Requests_Exception $e) {
-					return $e->getMessage();
-				};
-			} else {
-				return 'URL and/or Token not setup';
-			}
-			break;
-		case 'emby':
-			break;
-		case 'sonarr':
-			if (!empty($GLOBALS['sonarrURL']) && !empty($GLOBALS['sonarrToken'])) {
-				$sonarrs = array();
-				$sonarrURLList = explode(',', $GLOBALS['sonarrURL']);
-				$sonarrTokenList = explode(',', $GLOBALS['sonarrToken']);
-				if (count($sonarrURLList) == count($sonarrTokenList)) {
-					foreach ($sonarrURLList as $key => $value) {
-						$sonarrs[$key] = array(
-							'url' => $value,
-							'token' => $sonarrTokenList[$key]
-						);
-					}
-					foreach ($sonarrs as $key => $value) {
-						try {
-							$sonarr = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
-							$result = json_decode($sonarr->getSystemStatus(), true);
-							return (array_key_exists('error', $result)) ? $result['error']['msg'] : true;
-						} catch (Exception $e) {
-							return strip($e->getMessage());
-						}
-					}
-				}
-			} else {
-				return 'URL/s and/or Token/s not setup';
-			}
-			break;
-		case 'lidarr':
-			if (!empty($GLOBALS['lidarrURL']) && !empty($GLOBALS['lidarrToken'])) {
-				$sonarrs = array();
-				$sonarrURLList = explode(',', $GLOBALS['lidarrURL']);
-				$sonarrTokenList = explode(',', $GLOBALS['lidarrToken']);
-				if (count($sonarrURLList) == count($sonarrTokenList)) {
-					foreach ($sonarrURLList as $key => $value) {
-						$sonarrs[$key] = array(
-							'url' => $value,
-							'token' => $sonarrTokenList[$key]
-						);
-					}
-					foreach ($sonarrs as $key => $value) {
-						try {
-							$sonarr = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token'], true);
-							$result = json_decode($sonarr->getSystemStatus(), true);
-							return (array_key_exists('error', $result)) ? $result['error']['msg'] : true;
-						} catch (Exception $e) {
-							return $e->getMessage();
-						}
-					}
-				}
-			} else {
-				return 'URL/s and/or Token/s not setup';
-			}
-			break;
-		case 'radarr':
-			if (!empty($GLOBALS['radarrURL']) && !empty($GLOBALS['radarrToken'])) {
-				$sonarrs = array();
-				$sonarrURLList = explode(',', $GLOBALS['radarrURL']);
-				$sonarrTokenList = explode(',', $GLOBALS['radarrToken']);
-				if (count($sonarrURLList) == count($sonarrTokenList)) {
-					foreach ($sonarrURLList as $key => $value) {
-						$sonarrs[$key] = array(
-							'url' => $value,
-							'token' => $sonarrTokenList[$key]
-						);
-					}
-					foreach ($sonarrs as $key => $value) {
-						try {
-							$sonarr = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
-							$result = json_decode($sonarr->getSystemStatus(), true);
-							return (array_key_exists('error', $result)) ? $result['error']['msg'] : true;
-						} catch (Exception $e) {
-							return $e->getMessage();
-						}
-					}
+				$options = ($this->localURL($url)) ? array('verify' => false) : array();
+				switch ($type) {
+					case 'emby':
+					case 'emby_local':
+					case 'emby_connect':
+					case 'emby_all':
+						$response = Requests::post($url . "/api/v1/Job/embyuserimporter", $headers, $options);
+						break;
+					case 'plex':
+						$response = Requests::post($url . "/api/v1/Job/plexuserimporter", $headers, $options);
+						break;
+					default:
+						return false;
+						break;
 				}
-			} else {
-				return 'URL/s and/or Token/s not setup';
-			}
-			break;
-		case 'jdownloader':
-			if (!empty($GLOBALS['jdownloaderURL'])) {
-				$url = qualifyURL($GLOBALS['jdownloaderURL']);
-				try {
-					$options = (localURL($url)) ? array('verify' => false, 'timeout' => 30) : array('timeout' => 30);
-					$response = Requests::get($url, array(), $options);
-					if ($response->success) {
-						return true;
-					}
-				} catch (Requests_Exception $e) {
-					return $e->getMessage();
-				};
-			} else {
-				return 'URL and/or Token not setup';
-			}
-			break;
-		case 'sabnzbd':
-			if (!empty($GLOBALS['sabnzbdURL']) && !empty($GLOBALS['sabnzbdToken'])) {
-				$url = qualifyURL($GLOBALS['sabnzbdURL']);
-				$url = $url . '/api?mode=queue&output=json&apikey=' . $GLOBALS['sabnzbdToken'];
-				try {
-					$options = (localURL($url)) ? array('verify' => false) : array();
-					$response = Requests::get($url, array(), $options);
-					if ($response->success) {
-						return true;
-					}
-				} catch (Requests_Exception $e) {
-					return $e->getMessage();
-				};
-			} else {
-				return 'URL and/or Token not setup';
-			}
-			break;
-		case 'nzbget':
-			if (!empty($GLOBALS['nzbgetURL'])) {
-				$url = qualifyURL($GLOBALS['nzbgetURL']);
-				$url = $url . '/jsonrpc/listgroups';
-				try {
-					$options = (localURL($url)) ? array('verify' => false) : array();
-					if ($GLOBALS['nzbgetUsername'] !== '' && decrypt($GLOBALS['nzbgetPassword']) !== '') {
-						$credentials = array('auth' => new Requests_Auth_Basic(array($GLOBALS['nzbgetUsername'], decrypt($GLOBALS['nzbgetPassword']))));
-						$options = array_merge($options, $credentials);
-					}
-					$response = Requests::get($url, array(), $options);
-					if ($response->success) {
-						return true;
-					}
-				} catch (Requests_Exception $e) {
-					return $e->getMessage();
-				};
-			} else {
-				return 'URL and/or Username/Password not setup';
-			}
-			break;
-		case 'deluge':
-			if (!empty($GLOBALS['delugeURL']) && !empty($GLOBALS['delugePassword'])) {
-				try {
-					$deluge = new deluge($GLOBALS['delugeURL'], decrypt($GLOBALS['delugePassword']));
-					$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');
+				if ($response->success) {
+					$this->writeLog('success', 'OMBI Connect Function - Ran User Import', 'SYSTEM');
 					return true;
-				} catch (Exception $e) {
-					return $e->getMessage();
-				}
-			} else {
-				return 'URL and/or Password not setup';
-			}
-			break;
-		case 'rtorrent':
-			if (!empty($GLOBALS['rTorrentURL']) || !empty($GLOBALS['rTorrentURLOverride'])) {
-				try {
-					$digest = (empty($GLOBALS['rTorrentURLOverride'])) ? qualifyURL($GLOBALS['rTorrentURL'], true) : qualifyURL(checkOverrideURL($GLOBALS['rTorrentURL'], $GLOBALS['rTorrentURLOverride']), true);
-					$passwordInclude = ($GLOBALS['rTorrentUsername'] !== '' && $GLOBALS['rTorrentPassword'] !== '') ? $GLOBALS['rTorrentUsername'] . ':' . decrypt($GLOBALS['rTorrentPassword']) . "@" : '';
-					$extraPath = (strpos($GLOBALS['rTorrentURL'], '.php') !== false) ? '' : '/RPC2';
-					$extraPath = (empty($GLOBALS['rTorrentURLOverride'])) ? $extraPath : '';
-					$url = $digest['scheme'] . '://' . $passwordInclude . $digest['host'] . $digest['port'] . $digest['path'] . $extraPath;
-					$options = (localURL($url, $GLOBALS['rTorrentDisableCertCheck'])) ? array('verify' => false) : array();
-					if ($GLOBALS['rTorrentUsername'] !== '' && decrypt($GLOBALS['rTorrentPassword']) !== '') {
-						$credentials = array('auth' => new Requests_Auth_Digest(array($GLOBALS['rTorrentUsername'], decrypt($GLOBALS['rTorrentPassword']))));
-						$options = array_merge($options, $credentials);
-					}
-					$data = xmlrpc_encode_request("system.listMethods", null);
-					$response = Requests::post($url, array(), $data, $options);
-					if ($response->success) {
-						$methods = xmlrpc_decode(str_replace('i8>', 'i4>', $response->body));
-						if (count($methods) !== 0) {
-							return true;
-						}
-					}
+				} else {
+					$this->writeLog('error', 'OMBI Connect Function - Error: Connection Unsuccessful', 'SYSTEM');
 					return false;
-				} catch
-				(Requests_Exception $e) {
-					writeLog('error', 'rTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-					return $e->getMessage();
-				};
-			}
-			break;
-		case 'ldap_login':
-			$username = $array['data']['data']['username'];
-			$password = $array['data']['data']['password'];
-			if (empty($username) || empty($password)) {
-				return 'Missing Username or Password';
-			}
-			if (!empty($GLOBALS['authBaseDN']) && !empty($GLOBALS['authBackendHost'])) {
-				$ad = new \Adldap\Adldap();
-				// Create a configuration array.
-				$ldapServers = explode(',', $GLOBALS['authBackendHost']);
-				$i = 0;
-				foreach ($ldapServers as $key => $value) {
-					// Calculate parts
-					$digest = parse_url(trim($value));
-					$scheme = strtolower((isset($digest['scheme']) ? $digest['scheme'] : 'ldap'));
-					$host = (isset($digest['host']) ? $digest['host'] : (isset($digest['path']) ? $digest['path'] : ''));
-					$port = (isset($digest['port']) ? $digest['port'] : (strtolower($scheme) == 'ldap' ? 389 : 636));
-					// Reassign
-					$ldapHosts[] = $host;
-					$ldapServersNew[$key] = $scheme . '://' . $host . ':' . $port; // May use this later
-					if ($i == 0) {
-						$ldapPort = $port;
-					}
-					$i++;
-				}
-				$config = [
-					// Mandatory Configuration Options
-					'hosts' => $ldapHosts,
-					'base_dn' => $GLOBALS['authBaseDN'],
-					'username' => (empty($GLOBALS['ldapBindUsername'])) ? null : $GLOBALS['ldapBindUsername'],
-					'password' => (empty($GLOBALS['ldapBindPassword'])) ? null : decrypt($GLOBALS['ldapBindPassword']),
-					// Optional Configuration Options
-					'schema' => (($GLOBALS['ldapType'] == '1') ? Adldap\Schemas\ActiveDirectory::class : (($GLOBALS['ldapType'] == '2') ? Adldap\Schemas\OpenLDAP::class : Adldap\Schemas\FreeIPA::class)),
-					'account_prefix' => (empty($GLOBALS['authBackendHostPrefix'])) ? null : $GLOBALS['authBackendHostPrefix'],
-					'account_suffix' => (empty($GLOBALS['authBackendHostSuffix'])) ? null : $GLOBALS['authBackendHostSuffix'],
-					'port' => $ldapPort,
-					'follow_referrals' => false,
-					'use_ssl' => $GLOBALS['ldapSSL'],
-					'use_tls' => $GLOBALS['ldapTLS'],
-					'version' => 3,
-					'timeout' => 5,
-					// Custom LDAP Options
-					'custom_options' => [
-						// See: http://php.net/ldap_set_option
-						//LDAP_OPT_X_TLS_REQUIRE_CERT => LDAP_OPT_X_TLS_HARD
-					]
-				];
-				// Add a connection provider to Adldap.
-				$ad->addProvider($config);
-				try {
-					// If a successful connection is made to your server, the provider will be returned.
-					$provider = $ad->connect();
-					//prettyPrint($provider);
-					if ($provider->auth()->attempt($username, $password, true)) {
-						// Passed.
-						$user = $provider->search()->find($username);
-						//return $user->getFirstAttribute('cn');
-						//return $user->getGroups(['cn']);
-						//return $user;
-						//return $user->getUserPrincipalName();
-						//return $user->getGroups(['cn']);
-						return true;
-					} else {
-						// Failed.
-						return 'Username/Password Failed to authenticate';
-					}
-				} catch (\Adldap\Auth\BindException $e) {
-					$detailedError = $e->getDetailedError();
-					writeLog('error', 'LDAP Function - Error: ' . $detailedError->getErrorMessage(), $username);
-					return $detailedError->getErrorMessage();
-					// There was an issue binding / connecting to the server.
-				} catch (Adldap\Auth\UsernameRequiredException $e) {
-					$detailedError = $e->getDetailedError();
-					writeLog('error', 'LDAP Function - Error: ' . $detailedError->getErrorMessage(), $username);
-					return $detailedError->getErrorMessage();
-					// The user didn't supply a username.
-				} catch (Adldap\Auth\PasswordRequiredException $e) {
-					$detailedError = $e->getDetailedError();
-					writeLog('error', 'LDAP Function - Error: ' . $detailedError->getErrorMessage(), $username);
-					return $detailedError->getErrorMessage();
-					// The user didn't supply a password.
-				}
-			}
-			break;
-		case 'ldap':
-			if (!empty($GLOBALS['authBaseDN']) && !empty($GLOBALS['authBackendHost'])) {
-				$ad = new \Adldap\Adldap();
-				// Create a configuration array.
-				$ldapServers = explode(',', $GLOBALS['authBackendHost']);
-				$i = 0;
-				foreach ($ldapServers as $key => $value) {
-					// Calculate parts
-					$digest = parse_url(trim($value));
-					$scheme = strtolower((isset($digest['scheme']) ? $digest['scheme'] : 'ldap'));
-					$host = (isset($digest['host']) ? $digest['host'] : (isset($digest['path']) ? $digest['path'] : ''));
-					$port = (isset($digest['port']) ? $digest['port'] : (strtolower($scheme) == 'ldap' ? 389 : 636));
-					// Reassign
-					$ldapHosts[] = $host;
-					if ($i == 0) {
-						$ldapPort = $port;
-					}
-					$i++;
-				}
-				$config = [
-					// Mandatory Configuration Options
-					'hosts' => $ldapHosts,
-					'base_dn' => $GLOBALS['authBaseDN'],
-					'username' => (empty($GLOBALS['ldapBindUsername'])) ? null : $GLOBALS['ldapBindUsername'],
-					'password' => (empty($GLOBALS['ldapBindPassword'])) ? null : decrypt($GLOBALS['ldapBindPassword']),
-					// Optional Configuration Options
-					'schema' => (($GLOBALS['ldapType'] == '1') ? Adldap\Schemas\ActiveDirectory::class : (($GLOBALS['ldapType'] == '2') ? Adldap\Schemas\OpenLDAP::class : Adldap\Schemas\FreeIPA::class)),
-					'account_prefix' => '',
-					'account_suffix' => '',
-					'port' => $ldapPort,
-					'follow_referrals' => false,
-					'use_ssl' => $GLOBALS['ldapSSL'],
-					'use_tls' => $GLOBALS['ldapTLS'],
-					'version' => 3,
-					'timeout' => 5,
-					// Custom LDAP Options
-					'custom_options' => [
-						// See: http://php.net/ldap_set_option
-						//LDAP_OPT_X_TLS_REQUIRE_CERT => LDAP_OPT_X_TLS_HARD
-					]
-				];
-				// Add a connection provider to Adldap.
-				$ad->addProvider($config);
-				try {
-					// If a successful connection is made to your server, the provider will be returned.
-					$provider = $ad->connect();
-				} catch (\Adldap\Auth\BindException $e) {
-					$detailedError = $e->getDetailedError();
-					writeLog('error', 'LDAP Function - Error: ' . $detailedError->getErrorMessage(), 'SYSTEM');
-					return $detailedError->getErrorMessage();
-					// There was an issue binding / connecting to the server.
 				}
-				return ($provider) ? true : false;
+			} catch (Requests_Exception $e) {
+				$this->writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				return false;
 			}
-			return false;
-			break;
-		default :
-			return false;
+		}
+		return false;
 	}
-	return false;
-}
+}

+ 89 - 3179
api/functions/homepage-functions.php

@@ -1,3188 +1,98 @@
 <?php
-//homepage order
-function homepageOrder()
-{
-	$homepageOrder = array(
-		"homepageOrdercustomhtml" => $GLOBALS['homepageOrdercustomhtml'],
-		"homepageOrdercustomhtmlTwo" => $GLOBALS['homepageOrdercustomhtmlTwo'],
-		"homepageOrdernzbget" => $GLOBALS['homepageOrdernzbget'],
-		"homepageOrderjdownloader" => $GLOBALS['homepageOrderjdownloader'],
-		"homepageOrdersabnzbd" => $GLOBALS['homepageOrdersabnzbd'],
-		"homepageOrderplexnowplaying" => $GLOBALS['homepageOrderplexnowplaying'],
-		"homepageOrderplexrecent" => $GLOBALS['homepageOrderplexrecent'],
-		"homepageOrderplexplaylist" => $GLOBALS['homepageOrderplexplaylist'],
-		"homepageOrderembynowplaying" => $GLOBALS['homepageOrderembynowplaying'],
-		"homepageOrderembyrecent" => $GLOBALS['homepageOrderembyrecent'],
-		"homepageOrderombi" => $GLOBALS['homepageOrderombi'],
-		"homepageOrdercalendar" => $GLOBALS['homepageOrdercalendar'],
-		"homepageOrdertransmission" => $GLOBALS['homepageOrdertransmission'],
-		"homepageOrderqBittorrent" => $GLOBALS['homepageOrderqBittorrent'],
-		"homepageOrderdeluge" => $GLOBALS['homepageOrderdeluge'],
-		"homepageOrderrTorrent" => $GLOBALS['homepageOrderrTorrent'],
-		"homepageOrderdownloader" => $GLOBALS['homepageOrderdownloader'],
-		"homepageOrderhealthchecks" => $GLOBALS['homepageOrderhealthchecks'],
-		"homepageOrderunifi" => $GLOBALS['homepageOrderunifi'],
-		"homepageOrdertautulli" => $GLOBALS['homepageOrdertautulli'],
-		"homepageOrderPihole" => $GLOBALS['homepageOrderPihole'],
-		"homepageOrderMonitorr" => $GLOBALS['homepageOrderMonitorr'],
-		"homepageOrderWeatherAndAir" => $GLOBALS['homepageOrderWeatherAndAir'],
-		"homepageOrderSpeedtest" => $GLOBALS['homepageOrderSpeedtest'],
-		"homepageOrderNetdata" => $GLOBALS['homepageOrderNetdata'],
-	);
-	asort($homepageOrder);
-	return $homepageOrder;
-}
 
-function buildHomepage()
+trait HomepageFunctions
 {
-	$homepageOrder = homepageOrder();
-	$homepageBuilt = '';
-	foreach ($homepageOrder as $key => $value) {
-		$homepageBuilt .= buildHomepageItem($key);
+	public function getHomepageSettingsList()
+	{
+		$methods = get_class_methods($this);
+		$searchTerm = 'SettingsArray';
+		return array_filter($methods, function ($k) use ($searchTerm) {
+			return stripos($k, $searchTerm) !== false;
+		}, 0);
 	}
-	return $homepageBuilt;
-}
-
-function buildHomepageItem($homepageItem)
-{
-	$item = '<div id="' . $homepageItem . '">';
-	switch ($homepageItem) {
-		case 'homepageOrdercustomhtml':
-			if ($GLOBALS['homepagCustomHTMLoneEnabled'] && qualifyRequest($GLOBALS['homepagCustomHTMLoneAuth'])) {
-				$item .= ($GLOBALS['customHTMLone'] !== '') ? $GLOBALS['customHTMLone'] : '';
-			}
-			break;
-		case 'homepageOrdercustomhtmlTwo':
-			if ($GLOBALS['homepagCustomHTMLtwoEnabled'] && qualifyRequest($GLOBALS['homepagCustomHTMLtwoAuth'])) {
-				$item .= ($GLOBALS['customHTMLtwo'] !== '') ? $GLOBALS['customHTMLtwo'] : '';
-			}
-			break;
-		case 'homepageOrdernotice':
-			break;
-		case 'homepageOrdernoticeguest':
-			break;
-		case 'homepageOrderqBittorrent':
-			if ($GLOBALS['homepageqBittorrentEnabled'] && qualifyRequest($GLOBALS['homepageqBittorrentAuth'])) {
-				if ($GLOBALS['qBittorrentCombine']) {
-					$item .= '
-	                <script>
-	                // homepageOrderqBittorrent
-	                buildDownloaderCombined(\'qBittorrent\');
-	                homepageDownloader("qBittorrent", "' . $GLOBALS['homepageDownloadRefresh'] . '");
-	                // End homepageOrderqBittorrent
-	                </script>
-	                ';
-				} else {
-					$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
-					$item .= '
-	                <script>
-	                // homepageOrderqBittorrent
-	                $("#' . $homepageItem . '").html(buildDownloader("qBittorrent"));
-	                homepageDownloader("qBittorrent", "' . $GLOBALS['homepageDownloadRefresh'] . '");
-	                // End homepageOrderqBittorrent
-	                </script>
-	                ';
-				}
-			}
-			break;
-		case 'homepageOrderrTorrent':
-			if ($GLOBALS['homepagerTorrentEnabled'] && qualifyRequest($GLOBALS['homepagerTorrentAuth'])) {
-				if ($GLOBALS['rTorrentCombine']) {
-					$item .= '
-	                <script>
-	                // homepageOrderrTorrent
-	                buildDownloaderCombined(\'rTorrent\');
-	                homepageDownloader("rTorrent", "' . $GLOBALS['homepageDownloadRefresh'] . '");
-	                // End homepageOrderrTorrent
-	                </script>
-	                ';
-				} else {
-					$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
-					$item .= '
-	                <script>
-	                // homepageOrderrTorrent
-	                $("#' . $homepageItem . '").html(buildDownloader("rTorrent"));
-	                homepageDownloader("rTorrent", "' . $GLOBALS['homepageDownloadRefresh'] . '");
-	                // End homepageOrderrTorrent
-	                </script>
-	                ';
-				}
-			}
-			break;
-		case 'homepageOrderdeluge':
-			if ($GLOBALS['homepageDelugeEnabled'] && qualifyRequest($GLOBALS['homepageDelugeAuth'])) {
-				if ($GLOBALS['delugeCombine']) {
-					$item .= '
-					<script>
-					// Deluge
-					buildDownloaderCombined(\'deluge\');
-					homepageDownloader("deluge", "' . $GLOBALS['homepageDownloadRefresh'] . '");
-					// End Deluge
-					</script>
-					';
-				} else {
-					$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
-					$item .= '
-					<script>
-					// Deluge
-					$("#' . $homepageItem . '").html(buildDownloader("deluge"));
-					homepageDownloader("deluge", "' . $GLOBALS['homepageDownloadRefresh'] . '");
-					// End Deluge
-					</script>
-					';
-				}
-			}
-			break;
-		case 'homepageOrdertransmission':
-			if ($GLOBALS['homepageTransmissionEnabled'] && qualifyRequest($GLOBALS['homepageTransmissionAuth'])) {
-				if ($GLOBALS['transmissionCombine']) {
-					$item .= '
-					<script>
-					// Transmission
-					buildDownloaderCombined(\'transmission\');
-					homepageDownloader("transmission", "' . $GLOBALS['homepageDownloadRefresh'] . '");
-					// End Transmission
-					</script>
-					';
-				} else {
-					$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
-					$item .= '
-					<script>
-					// Transmission
-					$("#' . $homepageItem . '").html(buildDownloader("transmission"));
-					homepageDownloader("transmission", "' . $GLOBALS['homepageDownloadRefresh'] . '");
-					// End Transmission
-					</script>
-					';
-				}
-			}
-			break;
-		case 'homepageOrdernzbget':
-			if ($GLOBALS['homepageNzbgetEnabled'] && qualifyRequest($GLOBALS['homepageNzbgetAuth'])) {
-				if ($GLOBALS['nzbgetCombine']) {
-					$item .= '
-					<script>
-					// NZBGet
-					buildDownloaderCombined(\'nzbget\');
-					homepageDownloader("nzbget", "' . $GLOBALS['homepageDownloadRefresh'] . '");
-					// End NZBGet
-					</script>
-					';
-				} else {
-					$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
-					$item .= '
-					<script>
-					// NZBGet
-					$("#' . $homepageItem . '").html(buildDownloader("nzbget"));
-					homepageDownloader("nzbget", "' . $GLOBALS['homepageDownloadRefresh'] . '");
-					// End NZBGet
-					</script>
-					';
-				}
-			}
-			break;
-		case 'homepageOrderjdownloader':
-			if ($GLOBALS['homepageJdownloaderEnabled'] && qualifyRequest($GLOBALS['homepageJdownloaderAuth'])) {
-				if ($GLOBALS['jdownloaderCombine']) {
-					$item .= '
-					<script>
-					// JDownloader
-					buildDownloaderCombined(\'jdownloader\');
-					homepageDownloader("jdownloader", "' . $GLOBALS['homepageDownloadRefresh'] . '");
-					// End JDownloader
-					</script>
-					';
-				} else {
-					$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
-					$item .= '
-					<script>
-					// JDownloader
-					$("#' . $homepageItem . '").html(buildDownloader("jdownloader"));
-					homepageDownloader("jdownloader", "' . $GLOBALS['homepageDownloadRefresh'] . '");
-					// End JDownloader
-					</script>
-					';
-				}
-			}
-			break;
-		case 'homepageOrdersabnzbd':
-			if ($GLOBALS['homepageSabnzbdEnabled'] && qualifyRequest($GLOBALS['homepageSabnzbdAuth'])) {
-				if ($GLOBALS['sabnzbdCombine']) {
-					$item .= '
-					<script>
-					// SabNZBd
-					buildDownloaderCombined(\'sabnzbd\');
-					homepageDownloader("sabnzbd", "' . $GLOBALS['homepageDownloadRefresh'] . '");
-					// End SabNZBd
-					</script>
-					';
-				} else {
-					$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
-					$item .= '
-					<script>
-					// SabNZBd
-					$("#' . $homepageItem . '").html(buildDownloader("sabnzbd"));
-					homepageDownloader("sabnzbd", "' . $GLOBALS['homepageDownloadRefresh'] . '");
-					// End SabNZBd
-					</script>
-					';
-				}
-			}
-			break;
-		case 'homepageOrderplexnowplaying':
-			if ($GLOBALS['homepagePlexStreams']) {
-				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Now Playing...</h2></div>';
-				$item .= '
-				<script>
-				// Plex Stream
-				homepageStream("plex", "' . $GLOBALS['homepageStreamRefresh'] . '");
-				// End Plex Stream
-				</script>
-				';
-			}
-			break;
-		case 'homepageOrderplexrecent':
-			if ($GLOBALS['homepagePlexRecent']) {
-				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Recent...</h2></div>';
-				$item .= '
-				<script>
-				// Plex Recent
-				homepageRecent("plex", "' . $GLOBALS['homepageRecentRefresh'] . '");
-				// End Plex Recent
-				</script>
-				';
-			}
-			break;
-		case 'homepageOrderplexplaylist':
-			if ($GLOBALS['homepagePlexPlaylist']) {
-				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Playlists...</h2></div>';
-				$item .= '
-				<script>
-				// Plex Playlist
-				homepagePlaylist("plex");
-				// End Plex Playlist
-				</script>
-				';
-			}
-			break;
-		case 'homepageOrderembynowplaying':
-			if ($GLOBALS['homepageEmbyStreams'] && $GLOBALS['homepageEmbyEnabled']) {
-				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Now Playing...</h2></div>';
-				$item .= '
-				<script>
-				// Emby Stream
-				homepageStream("emby", "' . $GLOBALS['homepageStreamRefresh'] . '");
-				// End Emby Stream
-				</script>
-				';
-			}
-			break;
-		case 'homepageOrderembyrecent':
-			if ($GLOBALS['homepageEmbyRecent'] && $GLOBALS['homepageEmbyEnabled']) {
-				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Recent...</h2></div>';
-				$item .= '
-				<script>
-				// Emby Recent
-				homepageRecent("emby", "' . $GLOBALS['homepageRecentRefresh'] . '");
-				// End Emby Recent
-				</script>
-				';
-			}
-			break;
-		case 'homepageOrderombi':
-			if ($GLOBALS['homepageOmbiEnabled']) {
-				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Requests...</h2></div>';
-				$item .= '
-				<script>
-				// Ombi Requests
-				homepageRequests("' . $GLOBALS['ombiRefresh'] . '");
-				// End Ombi Requests
-				</script>
-				';
-			}
-			break;
-		case 'homepageOrdercalendar':
-			if ($GLOBALS['homepageSonarrEnabled'] && qualifyRequest($GLOBALS['homepageSonarrAuth']) || ($GLOBALS['homepageRadarrEnabled'] && qualifyRequest($GLOBALS['homepageRadarrAuth'])) || ($GLOBALS['homepageSickrageEnabled'] && qualifyRequest($GLOBALS['homepageSickrageAuth'])) || ($GLOBALS['homepageCouchpotatoEnabled'] && qualifyRequest($GLOBALS['homepageCouchpotatoAuth'])) || ($GLOBALS['homepageCalendarEnabled'] && qualifyRequest($GLOBALS['homepageCalendarAuth']) && $GLOBALS['calendariCal'] !== '')) {
-				$item .= '
-				<div id="calendar" class="fc fc-ltr m-b-30"></div>
-				<script>
-				// Calendar
-				homepageCalendar("' . $GLOBALS['calendarRefresh'] . '");
-				// End Calendar
-				</script>
-				';
-			}
-			break;
-		case 'homepageOrderhealthchecks':
-			if ($GLOBALS['homepageHealthChecksEnabled'] && qualifyRequest($GLOBALS['homepageHealthChecksAuth'])) {
-				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Health Checks...</h2></div>';
-				$item .= '
-				<script>
-				// Health Checks
-				homepageHealthChecks("' . $GLOBALS['healthChecksTags'] . '","' . $GLOBALS['homepageHealthChecksRefresh'] . '");
-				// End Health Checks
-				</script>
-				';
-			}
-			break;
-		case 'homepageOrderunifi':
-			if ($GLOBALS['homepageUnifiEnabled'] && qualifyRequest($GLOBALS['homepageUnifiAuth'])) {
-				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Unifi...</h2></div>';
-				$item .= '
-				<script>
-				// Unifi
-				homepageUnifi("' . $GLOBALS['homepageHealthChecksRefresh'] . '");
-				// End Unifi
-				</script>
-				';
-			}
-			break;
-		case 'homepageOrdertautulli':
-			if ($GLOBALS['homepageTautulliEnabled'] && qualifyRequest($GLOBALS['homepageTautulliAuth'])) {
-				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Tautulli...</h2></div>';
-				$item .= '
-				<script>
-				// Tautulli
-				homepageTautulli("' . $GLOBALS['homepageTautulliRefresh'] . '");
-				// End Tautulli
-				</script>
-				';
-			}
-			break;
-		case 'homepageOrderPihole':
-			if ($GLOBALS['homepagePiholeEnabled']) {
-				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Pi-hole Stats...</h2></div>';
-				$item .= '
-				<script>
-				// Pi-hole Stats
-				homepagePihole("' . $GLOBALS['homepagePiholeRefresh'] . '");
-				// End Pi-hole Stats
-				</script>
-				';
-			}
-			break;
-		case 'homepageOrderMonitorr':
-			if ($GLOBALS['homepageMonitorrEnabled']) {
-				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Monitorr...</h2></div>';
-				$item .= '
-				<script>
-				// Monitorr
-				homepageMonitorr("' . $GLOBALS['homepageMonitorrRefresh'] . '");
-				// End Monitorr
-				</script>
-				';
-			}
-			break;
-		case 'homepageOrderWeatherAndAir':
-			if ($GLOBALS['homepageWeatherAndAirEnabled']) {
-				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Weather And Air...</h2></div>';
-				$item .= '
-				<script>
-				// Weather And Air
-				homepageWeatherAndAir("' . $GLOBALS['homepageWeatherAndAirRefresh'] . '");
-				// End Weather And Air
-				</script>
-				';
-			}
-			break;
-		case 'homepageOrderSpeedtest':
-			if ($GLOBALS['homepageSpeedtestEnabled']) {
-				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Speedtest...</h2></div>';
-				$item .= '
-				<script>
-				// Speedtest
-				homepageSpeedtest("' . $GLOBALS['homepageSpeedtestRefresh'] . '");
-				// End Speedtest
-				</script>
-				';
+	
+	public function getHomepageSettingsCombined()
+	{
+		$list = $this->getHomepageSettingsList();
+		$combined = [];
+		foreach ($list as $item) {
+			$combined[] = $this->$item();
+		}
+		return $combined;
+	}
+	
+	public function homepageItemPermissions($settings = false, $api = false)
+	{
+		if (!$settings) {
+			if ($api) {
+				$this->setAPIResponse('error', 'No settings were supplied', 422);
 			}
-			break;
-		case 'homepageOrderNetdata':
-			if ($GLOBALS['homepageNetdataEnabled']) {
-				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Netdata...</h2></div>';
-				$item .= '
-				<script>
-				// Netdata
-				homepageNetdata("' . $GLOBALS['homepageNetdataRefresh'] . '");
-				// End Netdata
-				</script>
-				';
+			return false;
+		}
+		foreach ($settings as $type => $setting) {
+			$settingsType = gettype($setting);
+			switch ($type) {
+				case 'enabled':
+					if ($settingsType == 'string') {
+						if (!$this->config[$setting]) {
+							if ($api) {
+								$this->setAPIResponse('error', $setting . ' module is not enabled', 409);
+							}
+							return false;
+						}
+					} else {
+						foreach ($setting as $item) {
+							if (!$this->config[$item]) {
+								if ($api) {
+									$this->setAPIResponse('error', $item . ' module is not enabled', 409);
+								}
+								return false;
+							}
+						}
+					}
+					break;
+				case 'auth':
+					if ($settingsType == 'string') {
+						if (!$this->qualifyRequest($this->config[$setting])) {
+							if ($api) {
+								$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+							}
+							return false;
+						}
+					} else {
+						foreach ($setting as $item) {
+							if (!$this->qualifyRequest($this->config[$item])) {
+								if ($api) {
+									$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+								}
+								return false;
+							}
+						}
+					}
+					break;
+				case 'not_empty':
+					if ($settingsType == 'string') {
+						if (empty($this->config[$setting])) {
+							if ($api) {
+								$this->setAPIResponse('error', $setting . 'was not supplied', 422);
+							}
+							return false;
+						}
+					} else {
+						foreach ($setting as $item) {
+							if (empty($this->config[$item])) {
+								if ($api) {
+									$this->setAPIResponse('error', $item . 'was not supplied', 422);
+								}
+								return false;
+							}
+						}
+					}
+					break;
+				default:
+					//return false;
 			}
-			break;
-		default:
-			# code...
-			break;
-	}
-	return $item . '</div>';
-}
-
-function getHomepageList()
-{
-	$groups = groupSelect();
-	$ombiTvOptions = array(
-		array(
-			'name' => 'All Seasons',
-			'value' => 'all'
-		),
-		array(
-			'name' => 'First Season Only',
-			'value' => 'first'
-		),
-		array(
-			'name' => 'Last Season Only',
-			'value' => 'last'
-		),
-	);
-	$mediaServers = array(
-		array(
-			'name' => 'N/A',
-			'value' => ''
-		),
-		array(
-			'name' => 'Plex',
-			'value' => 'plex'
-		),
-		array(
-			'name' => 'Emby [Not Available]',
-			'value' => 'emby'
-		)
-	);
-	$limit = array(
-		array(
-			'name' => '1 Item',
-			'value' => '1'
-		),
-		array(
-			'name' => '2 Items',
-			'value' => '2'
-		),
-		array(
-			'name' => '3 Items',
-			'value' => '3'
-		),
-		array(
-			'name' => '4 Items',
-			'value' => '4'
-		),
-		array(
-			'name' => '5 Items',
-			'value' => '5'
-		),
-		array(
-			'name' => '6 Items',
-			'value' => '6'
-		),
-		array(
-			'name' => '7 Items',
-			'value' => '7'
-		),
-		array(
-			'name' => '8 Items',
-			'value' => '8'
-		),
-		array(
-			'name' => 'Unlimited',
-			'value' => '1000'
-		),
-	);
-	$day = array(
-		array(
-			'name' => 'Sunday',
-			'value' => '0'
-		),
-		array(
-			'name' => 'Monday',
-			'value' => '1'
-		),
-		array(
-			'name' => 'Tueday',
-			'value' => '2'
-		),
-		array(
-			'name' => 'Wednesday',
-			'value' => '3'
-		),
-		array(
-			'name' => 'Thursday',
-			'value' => '4'
-		),
-		array(
-			'name' => 'Friday',
-			'value' => '5'
-		),
-		array(
-			'name' => 'Saturday',
-			'value' => '6'
-		)
-	);
-	$calendarDefault = array(
-		array(
-			'name' => 'Month',
-			'value' => 'month'
-		),
-		array(
-			'name' => 'Day',
-			'value' => 'basicDay'
-		),
-		array(
-			'name' => 'Week',
-			'value' => 'basicWeek'
-		),
-		array(
-			'name' => 'List',
-			'value' => 'list'
-		)
-	);
-	$timeFormat = array(
-		array(
-			'name' => '6p',
-			'value' => 'h(:mm)t'
-		),
-		array(
-			'name' => '6:00p',
-			'value' => 'h:mmt'
-		),
-		array(
-			'name' => '6:00',
-			'value' => 'h:mm'
-		),
-		array(
-			'name' => '18',
-			'value' => 'H(:mm)'
-		),
-		array(
-			'name' => '18:00',
-			'value' => 'H:mm'
-		)
-	);
-	$rTorrentSortOptions = array(
-		array(
-			'name' => 'Date Desc',
-			'value' => 'dated'
-		),
-		array(
-			'name' => 'Date Asc',
-			'value' => 'datea'
-		),
-		array(
-			'name' => 'Hash Desc',
-			'value' => 'hashd'
-		),
-		array(
-			'name' => 'Hash Asc',
-			'value' => 'hasha'
-		),
-		array(
-			'name' => 'Name Desc',
-			'value' => 'named'
-		),
-		array(
-			'name' => 'Name Asc',
-			'value' => 'namea'
-		),
-		array(
-			'name' => 'Size Desc',
-			'value' => 'sized'
-		),
-		array(
-			'name' => 'Size Asc',
-			'value' => 'sizea'
-		),
-		array(
-			'name' => 'Label Desc',
-			'value' => 'labeld'
-		),
-		array(
-			'name' => 'Label Asc',
-			'value' => 'labela'
-		),
-		array(
-			'name' => 'Status Desc',
-			'value' => 'statusd'
-		),
-		array(
-			'name' => 'Status Asc',
-			'value' => 'statusa'
-		),
-	);
-	$qBittorrentApiOptions = array(
-		array(
-			'name' => 'V1',
-			'value' => '1'
-		),
-		array(
-			'name' => 'V2',
-			'value' => '2'
-		),
-	);
-	$qBittorrentSortOptions = array(
-		array(
-			'name' => 'Hash',
-			'value' => 'hash'
-		),
-		array(
-			'name' => 'Name',
-			'value' => 'name'
-		),
-		array(
-			'name' => 'Size',
-			'value' => 'size'
-		),
-		array(
-			'name' => 'Progress',
-			'value' => 'progress'
-		),
-		array(
-			'name' => 'Download Speed',
-			'value' => 'dlspeed'
-		),
-		array(
-			'name' => 'Upload Speed',
-			'value' => 'upspeed'
-		),
-		array(
-			'name' => 'Priority',
-			'value' => 'priority'
-		),
-		array(
-			'name' => 'Number of Seeds',
-			'value' => 'num_seeds'
-		),
-		array(
-			'name' => 'Number of Seeds in Swarm',
-			'value' => 'num_complete'
-		),
-		array(
-			'name' => 'Number of Leechers',
-			'value' => 'num_leechs'
-		),
-		array(
-			'name' => 'Number of Leechers in Swarm',
-			'value' => 'num_incomplete'
-		),
-		array(
-			'name' => 'Ratio',
-			'value' => 'ratio'
-		),
-		array(
-			'name' => 'ETA',
-			'value' => 'eta'
-		),
-		array(
-			'name' => 'State',
-			'value' => 'state'
-		),
-		array(
-			'name' => 'Category',
-			'value' => 'category'
-		)
-	);
-	$xmlStatus = (extension_loaded('xmlrpc')) ? 'Installed' : 'Not Installed';
-	return array(
-		array(
-			'name' => 'Calendar',
-			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/calendar.png',
-			'category' => 'HOMEPAGE',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageCalendarEnabled',
-						'label' => 'Enable iCal',
-						'value' => $GLOBALS['homepageCalendarEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageCalendarAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageCalendarAuth'],
-						'options' => $groups
-					),
-					array(
-						'type' => 'input',
-						'name' => 'calendariCal',
-						'label' => 'iCal URL\'s',
-						'value' => $GLOBALS['calendariCal'],
-						'placeholder' => 'separate by comma\'s'
-					),
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'number',
-						'name' => 'calendarStart',
-						'label' => '# of Days Before',
-						'value' => $GLOBALS['calendarStart'],
-						'placeholder' => ''
-					),
-					array(
-						'type' => 'number',
-						'name' => 'calendarEnd',
-						'label' => '# of Days After',
-						'value' => $GLOBALS['calendarEnd'],
-						'placeholder' => ''
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarFirstDay',
-						'label' => 'Start Day',
-						'value' => $GLOBALS['calendarFirstDay'],
-						'options' => $day
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarDefault',
-						'label' => 'Default View',
-						'value' => $GLOBALS['calendarDefault'],
-						'options' => $calendarDefault
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarTimeFormat',
-						'label' => 'Time Format',
-						'value' => $GLOBALS['calendarTimeFormat'],
-						'options' => $timeFormat
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarLimit',
-						'label' => 'Items Per Day',
-						'value' => $GLOBALS['calendarLimit'],
-						'options' => $limit
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['calendarRefresh'],
-						'options' => optionTime()
-					)
-				),
-			)
-		),
-		array(
-			'name' => 'Plex',
-			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/plex.png',
-			'category' => 'Media Server',
-			//'license' => $GLOBALS['license'],
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepagePlexEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepagePlexEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepagePlexAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepagePlexAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'plexURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['plexURL'],
-						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-						'placeholder' => 'http(s)://hostname:port'
-					),
-					array(
-						'type' => 'password-alt',
-						'name' => 'plexToken',
-						'label' => 'Token',
-						'value' => $GLOBALS['plexToken']
-					),
-					array(
-						'type' => 'password-alt',
-						'name' => 'plexID',
-						'label' => 'Plex Machine',
-						'value' => $GLOBALS['plexID']
-					)
-				),
-				'Active Streams' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepagePlexStreams',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepagePlexStreams']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepagePlexStreamsAuth',
-						'label' => 'Minimum Authorization',
-						'value' => $GLOBALS['homepagePlexStreamsAuth'],
-						'options' => $groups
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'homepageShowStreamNames',
-						'label' => 'User Information',
-						'value' => $GLOBALS['homepageShowStreamNames']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageShowStreamNamesAuth',
-						'label' => 'Minimum Authorization',
-						'value' => $GLOBALS['homepageShowStreamNamesAuth'],
-						'options' => $groups
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageStreamRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['homepageStreamRefresh'],
-						'options' => optionTime()
-					),
-				),
-				'Recent Items' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepagePlexRecent',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepagePlexRecent']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepagePlexRecentAuth',
-						'label' => 'Minimum Authorization',
-						'value' => $GLOBALS['homepagePlexRecentAuth'],
-						'options' => $groups
-					),
-					array(
-						'type' => 'number',
-						'name' => 'homepageRecentLimit',
-						'label' => 'Item Limit',
-						'value' => $GLOBALS['homepageRecentLimit'],
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageRecentRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['homepageRecentRefresh'],
-						'options' => optionTime()
-					),
-				),
-				'Media Search' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'mediaSearch',
-						'label' => 'Enable',
-						'value' => $GLOBALS['mediaSearch']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'mediaSearchAuth',
-						'label' => 'Minimum Authorization',
-						'value' => $GLOBALS['mediaSearchAuth'],
-						'options' => $groups
-					),
-					array(
-						'type' => 'select',
-						'name' => 'mediaSearchType',
-						'label' => 'Media Server',
-						'value' => $GLOBALS['mediaSearchType'],
-						'options' => $mediaServers
-					),
-				),
-				'Playlists' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepagePlexPlaylist',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepagePlexPlaylist']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepagePlexPlaylistAuth',
-						'label' => 'Minimum Authorization',
-						'value' => $GLOBALS['homepagePlexPlaylistAuth'],
-						'options' => $groups
-					),
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'input',
-						'name' => 'plexTabName',
-						'label' => 'Plex Tab Name',
-						'value' => $GLOBALS['plexTabName'],
-						'placeholder' => 'Only use if you have Plex in a reverse proxy'
-					),
-					array(
-						'type' => 'input',
-						'name' => 'plexTabURL',
-						'label' => 'Plex Tab WAN URL',
-						'value' => $GLOBALS['plexTabURL'],
-						'placeholder' => 'http(s)://hostname:port'
-					),
-					array(
-						'type' => 'select',
-						'name' => 'cacheImageSize',
-						'label' => 'Image Cache Size',
-						'value' => $GLOBALS['cacheImageSize'],
-						'options' => array(
-							array(
-								'name' => 'Low',
-								'value' => '.5'
-							),
-							array(
-								'name' => '1x',
-								'value' => '1'
-							),
-							array(
-								'name' => '2x',
-								'value' => '2'
-							),
-							array(
-								'name' => '3x',
-								'value' => '3'
-							)
-						)
-					)
-				),
-				'Test Connection' => array(
-					array(
-						'type' => 'blank',
-						'label' => 'Please Save before Testing'
-					),
-					array(
-						'type' => 'button',
-						'label' => '',
-						'icon' => 'fa fa-flask',
-						'class' => 'pull-right',
-						'text' => 'Test Connection',
-						'attr' => 'onclick="testAPIConnection(\'plex\')"'
-					),
-				)
-			)
-		),
-		array(
-			'name' => 'Emby-Jellyfin',
-			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/emby.png',
-			'category' => 'Media Server',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageEmbyEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageEmbyEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageEmbyAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageEmbyAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'embyURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['embyURL'],
-						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too. Make sure if Jelly fin to end url with /jellyfin',
-						'placeholder' => 'http(s)://hostname:port - make sure if Jelly fin to end url with /jellyfin'
-					),
-					array(
-						'type' => 'password-alt',
-						'name' => 'embyToken',
-						'label' => 'Token',
-						'value' => $GLOBALS['embyToken']
-					)
-				),
-				'Active Streams' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageEmbyStreams',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageEmbyStreams']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageEmbyStreamsAuth',
-						'label' => 'Minimum Authorization',
-						'value' => $GLOBALS['homepageEmbyStreamsAuth'],
-						'options' => $groups
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'homepageShowStreamNames',
-						'label' => 'User Information',
-						'value' => $GLOBALS['homepageShowStreamNames']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageShowStreamNamesAuth',
-						'label' => 'Minimum Authorization',
-						'value' => $GLOBALS['homepageShowStreamNamesAuth'],
-						'options' => $groups
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageStreamRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['homepageStreamRefresh'],
-						'options' => optionTime()
-					),
-				),
-				'Recent Items' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageEmbyRecent',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageEmbyRecent']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageEmbyRecentAuth',
-						'label' => 'Minimum Authorization',
-						'value' => $GLOBALS['homepageEmbyRecentAuth'],
-						'options' => $groups
-					),
-					array(
-						'type' => 'number',
-						'name' => 'homepageRecentLimit',
-						'label' => 'Item Limit',
-						'value' => $GLOBALS['homepageRecentLimit'],
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageRecentRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['homepageRecentRefresh'],
-						'options' => optionTime()
-					),
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'input',
-						'name' => 'embyTabName',
-						'label' => 'Emby Tab Name',
-						'value' => $GLOBALS['embyTabName'],
-						'placeholder' => 'Only use if you have Emby in a reverse proxy'
-					),
-					array(
-						'type' => 'input',
-						'name' => 'embyTabURL',
-						'label' => 'Emby Tab WAN URL',
-						'value' => $GLOBALS['embyTabURL'],
-						'placeholder' => 'http(s)://hostname:port'
-					),
-					array(
-						'type' => 'select',
-						'name' => 'cacheImageSize',
-						'label' => 'Image Cache Size',
-						'value' => $GLOBALS['cacheImageSize'],
-						'options' => array(
-							array(
-								'name' => 'Low',
-								'value' => '.5'
-							),
-							array(
-								'name' => '1x',
-								'value' => '1'
-							),
-							array(
-								'name' => '2x',
-								'value' => '2'
-							),
-							array(
-								'name' => '3x',
-								'value' => '3'
-							)
-						)
-					)
-				)
-			)
-		),
-		array(
-			'name' => 'JDownloader',
-			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/jdownloader.png',
-			'category' => 'Downloader',
-			'settings' => array(
-				'custom' => '
-				<div class="row">
-                    <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">
-									<ul class="list-icons">
-                                        <li><i class="fa fa-chevron-right text-danger"></i> <a href="https://pypi.org/project/myjd-api/" target="_blank">Download [myjd-api] Module</a></li>
-                                        <li><i class="fa fa-chevron-right text-danger"></i> Add <b>/api/myjd</b> to the URL if you are using <a href="https://pypi.org/project/RSScrawler/" target="_blank">RSScrawler</a></li>
-                                    </ul>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-				</div>
-				',
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageJdownloaderEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageJdownloaderEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageJdownloaderAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageJdownloaderAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'jdownloaderURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['jdownloaderURL'],
-						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-						'placeholder' => 'http(s)://hostname:port'
-					)
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'select',
-						'name' => 'homepageDownloadRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['homepageDownloadRefresh'],
-						'options' => optionTime()
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'jdownloaderCombine',
-						'label' => 'Add to Combined Downloader',
-						'value' => $GLOBALS['jdownloaderCombine']
-					),
-				),
-				'Test Connection' => array(
-					array(
-						'type' => 'blank',
-						'label' => 'Please Save before Testing'
-					),
-					array(
-						'type' => 'button',
-						'label' => '',
-						'icon' => 'fa fa-flask',
-						'class' => 'pull-right',
-						'text' => 'Test Connection',
-						'attr' => 'onclick="testAPIConnection(\'jdownloader\')"'
-					),
-				)
-			)
-		),
-		array(
-			'name' => 'SabNZBD',
-			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/sabnzbd.png',
-			'category' => 'Downloader',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageSabnzbdEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageSabnzbdEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageSabnzbdAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageSabnzbdAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'sabnzbdURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['sabnzbdURL'],
-						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-						'placeholder' => 'http(s)://hostname:port'
-					),
-					array(
-						'type' => 'password-alt',
-						'name' => 'sabnzbdToken',
-						'label' => 'Token',
-						'value' => $GLOBALS['sabnzbdToken']
-					)
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'select',
-						'name' => 'homepageDownloadRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['homepageDownloadRefresh'],
-						'options' => optionTime()
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'sabnzbdCombine',
-						'label' => 'Add to Combined Downloader',
-						'value' => $GLOBALS['sabnzbdCombine']
-					),
-				),
-				'Test Connection' => array(
-					array(
-						'type' => 'blank',
-						'label' => 'Please Save before Testing'
-					),
-					array(
-						'type' => 'button',
-						'label' => '',
-						'icon' => 'fa fa-flask',
-						'class' => 'pull-right',
-						'text' => 'Test Connection',
-						'attr' => 'onclick="testAPIConnection(\'sabnzbd\')"'
-					),
-				)
-			)
-		),
-		array(
-			'name' => 'NZBGet',
-			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/nzbget.png',
-			'category' => 'Downloader',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageNzbgetEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageNzbgetEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageNzbgetAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageNzbgetAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'nzbgetURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['nzbgetURL'],
-						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-						'placeholder' => 'http(s)://hostname:port'
-					),
-					array(
-						'type' => 'input',
-						'name' => 'nzbgetUsername',
-						'label' => 'Username',
-						'value' => $GLOBALS['nzbgetUsername']
-					),
-					array(
-						'type' => 'password',
-						'name' => 'nzbgetPassword',
-						'label' => 'Password',
-						'value' => $GLOBALS['nzbgetPassword']
-					)
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'select',
-						'name' => 'homepageDownloadRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['homepageDownloadRefresh'],
-						'options' => optionTime()
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'nzbgetCombine',
-						'label' => 'Add to Combined Downloader',
-						'value' => $GLOBALS['nzbgetCombine']
-					),
-				),
-				'Test Connection' => array(
-					array(
-						'type' => 'blank',
-						'label' => 'Please Save before Testing'
-					),
-					array(
-						'type' => 'button',
-						'label' => '',
-						'icon' => 'fa fa-flask',
-						'class' => 'pull-right',
-						'text' => 'Test Connection',
-						'attr' => 'onclick="testAPIConnection(\'nzbget\')"'
-					),
-				)
-			)
-		),
-		array(
-			'name' => 'Transmission',
-			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/transmission.png',
-			'category' => 'Downloader',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageTransmissionEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageTransmissionEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageTransmissionAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageTransmissionAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'transmissionURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['transmissionURL'],
-						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-						'placeholder' => 'http(s)://hostname:port'
-					),
-					array(
-						'type' => 'input',
-						'name' => 'transmissionUsername',
-						'label' => 'Username',
-						'value' => $GLOBALS['transmissionUsername']
-					),
-					array(
-						'type' => 'password',
-						'name' => 'transmissionPassword',
-						'label' => 'Password',
-						'value' => $GLOBALS['transmissionPassword']
-					)
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'transmissionHideSeeding',
-						'label' => 'Hide Seeding',
-						'value' => $GLOBALS['transmissionHideSeeding']
-					), array(
-						'type' => 'switch',
-						'name' => 'transmissionHideCompleted',
-						'label' => 'Hide Completed',
-						'value' => $GLOBALS['transmissionHideCompleted']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageDownloadRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['homepageDownloadRefresh'],
-						'options' => optionTime()
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'transmissionCombine',
-						'label' => 'Add to Combined Downloader',
-						'value' => $GLOBALS['transmissionCombine']
-					),
-				)
-			)
-		),
-		array(
-			'name' => 'qBittorrent',
-			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/qBittorrent.png',
-			'category' => 'Downloader',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageqBittorrentEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageqBittorrentEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageqBittorrentAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageqBittorrentAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'qBittorrentURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['qBittorrentURL'],
-						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-						'placeholder' => 'http(s)://hostname:port'
-					),
-					array(
-						'type' => 'select',
-						'name' => 'qBittorrentApiVersion',
-						'label' => 'API Version',
-						'value' => $GLOBALS['qBittorrentApiVersion'],
-						'options' => $qBittorrentApiOptions
-					),
-					array(
-						'type' => 'input',
-						'name' => 'qBittorrentUsername',
-						'label' => 'Username',
-						'value' => $GLOBALS['qBittorrentUsername']
-					),
-					array(
-						'type' => 'password',
-						'name' => 'qBittorrentPassword',
-						'label' => 'Password',
-						'value' => $GLOBALS['qBittorrentPassword']
-					)
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'qBittorrentHideSeeding',
-						'label' => 'Hide Seeding',
-						'value' => $GLOBALS['qBittorrentHideSeeding']
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'qBittorrentHideCompleted',
-						'label' => 'Hide Completed',
-						'value' => $GLOBALS['qBittorrentHideCompleted']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'qBittorrentSortOrder',
-						'label' => 'Order',
-						'value' => $GLOBALS['qBittorrentSortOrder'],
-						'options' => $qBittorrentSortOptions
-					), array(
-						'type' => 'switch',
-						'name' => 'qBittorrentReverseSorting',
-						'label' => 'Reverse Sorting',
-						'value' => $GLOBALS['qBittorrentReverseSorting']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageDownloadRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['homepageDownloadRefresh'],
-						'options' => optionTime()
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'qBittorrentCombine',
-						'label' => 'Add to Combined Downloader',
-						'value' => $GLOBALS['qBittorrentCombine']
-					),
-				)
-			)
-		),
-		array(
-			'name' => 'rTorrent',
-			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/rTorrent.png',
-			'category' => 'Downloader',
-			'settings' => array(
-				'FYI' => array(
-					array(
-						'type' => 'html',
-						'label' => '',
-						'override' => 12,
-						'html' => '
-						<div class="row">
-						    <div class="col-lg-12">
-						        <div class="panel panel-info">
-						            <div class="panel-heading">
-						                <span lang="en">ATTENTION</span>
-						            </div>
-						            <div class="panel-wrapper collapse in" aria-expanded="true">
-						                <div class="panel-body">
-						                	<h4 lang="en">This module requires XMLRPC</h4>
-						                    <span lang="en">Status: [ <b>' . $xmlStatus . '</b> ]</span>
-						                    <br/></br>
-						                    <span lang="en">
-						                    	<h4><b>Note about API URL</b></h4>
-						                    	Organizr appends the url with <code>/RPC2</code> unless the URL ends in <code>.php</code><br/>
-						                    	<h5>Possible URLs:</h5>
-						                    	<li>http://localhost:8080</li>
-						                    	<li>https://domain.site/xmlrpc.php</li>
-						                    	<li>https://seedbox.site/rutorrent/plugins/httprpc/action.php</li>
-						                    </span>
-						                </div>
-						            </div>
-						        </div>
-						    </div>
-						</div>
-						'
-					)
-				),
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepagerTorrentEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepagerTorrentEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepagerTorrentAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepagerTorrentAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'rTorrentURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['rTorrentURL'],
-						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-						'placeholder' => 'http(s)://hostname:port'
-					),
-					array(
-						'type' => 'input',
-						'name' => 'rTorrentURLOverride',
-						'label' => 'rTorrent API URL Override',
-						'value' => $GLOBALS['rTorrentURLOverride'],
-						'help' => 'Only use if you cannot connect.  Please make sure to use local IP address and port - You also may use local dns name too.',
-						'placeholder' => 'http(s)://hostname:port/xmlrpc'
-					),
-					array(
-						'type' => 'input',
-						'name' => 'rTorrentUsername',
-						'label' => 'Username',
-						'value' => $GLOBALS['rTorrentUsername']
-					),
-					array(
-						'type' => 'password',
-						'name' => 'rTorrentPassword',
-						'label' => 'Password',
-						'value' => $GLOBALS['rTorrentPassword']
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'rTorrentDisableCertCheck',
-						'label' => 'Disable Certificate Check',
-						'value' => $GLOBALS['rTorrentDisableCertCheck']
-					),
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'rTorrentHideSeeding',
-						'label' => 'Hide Seeding',
-						'value' => $GLOBALS['rTorrentHideSeeding']
-					), array(
-						'type' => 'switch',
-						'name' => 'rTorrentHideCompleted',
-						'label' => 'Hide Completed',
-						'value' => $GLOBALS['rTorrentHideCompleted']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'rTorrentSortOrder',
-						'label' => 'Order',
-						'value' => $GLOBALS['rTorrentSortOrder'],
-						'options' => $rTorrentSortOptions
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageDownloadRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['homepageDownloadRefresh'],
-						'options' => optionTime()
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'rTorrentCombine',
-						'label' => 'Add to Combined Downloader',
-						'value' => $GLOBALS['rTorrentCombine']
-					),
-				),
-				'Test Connection' => array(
-					array(
-						'type' => 'blank',
-						'label' => 'Please Save before Testing'
-					),
-					array(
-						'type' => 'button',
-						'label' => '',
-						'icon' => 'fa fa-flask',
-						'class' => 'pull-right',
-						'text' => 'Test Connection',
-						'attr' => 'onclick="testAPIConnection(\'rtorrent\')"'
-					),
-				)
-			)
-		),
-		array(
-			'name' => 'Deluge',
-			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/deluge.png',
-			'category' => 'Downloader',
-			'settings' => array(
-				'custom' => '
-				<div class="row">
-                    <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">
-									<ul class="list-icons">
-                                        <li><i class="fa fa-chevron-right text-danger"></i> <a href="https://github.com/idlesign/deluge-webapi/tree/master/dist" target="_blank">Download Plugin</a></li>
-                                        <li><i class="fa fa-chevron-right text-danger"></i> Open Deluge Web UI, go to "Preferences -> Plugins -> Install plugin" and choose egg file.</li>
-                                        <li><i class="fa fa-chevron-right text-danger"></i> Activate WebAPI plugin </li>
-                                    </ul>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-				</div>
-				',
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageDelugeEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageDelugeEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageDelugeAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageDelugeAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'delugeURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['delugeURL'],
-						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-						'placeholder' => 'http(s)://hostname:port'
-					),
-					array(
-						'type' => 'password',
-						'name' => 'delugePassword',
-						'label' => 'Password',
-						'value' => $GLOBALS['delugePassword']
-					)
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'delugeHideSeeding',
-						'label' => 'Hide Seeding',
-						'value' => $GLOBALS['delugeHideSeeding']
-					), array(
-						'type' => 'switch',
-						'name' => 'delugeHideCompleted',
-						'label' => 'Hide Completed',
-						'value' => $GLOBALS['delugeHideCompleted']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageDownloadRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['homepageDownloadRefresh'],
-						'options' => optionTime()
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'delugeCombine',
-						'label' => 'Add to Combined Downloader',
-						'value' => $GLOBALS['delugeCombine']
-					),
-				),
-				'Test Connection' => array(
-					array(
-						'type' => 'blank',
-						'label' => 'Please Save before Testing'
-					),
-					array(
-						'type' => 'button',
-						'label' => '',
-						'icon' => 'fa fa-flask',
-						'class' => 'pull-right',
-						'text' => 'Test Connection',
-						'attr' => 'onclick="testAPIConnection(\'deluge\')"'
-					),
-				)
-			)
-		),
-		array(
-			'name' => 'Sonarr',
-			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/sonarr.png',
-			'category' => 'PVR',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageSonarrEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageSonarrEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageSonarrAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageSonarrAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'sonarrURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['sonarrURL'],
-						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-						'placeholder' => 'http(s)://hostname:port'
-					),
-					array(
-						'type' => 'password-alt',
-						'name' => 'sonarrToken',
-						'label' => 'Token',
-						'value' => $GLOBALS['sonarrToken']
-					)
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'number',
-						'name' => 'calendarStart',
-						'label' => '# of Days Before',
-						'value' => $GLOBALS['calendarStart'],
-						'placeholder' => ''
-					),
-					array(
-						'type' => 'number',
-						'name' => 'calendarEnd',
-						'label' => '# of Days After',
-						'value' => $GLOBALS['calendarEnd'],
-						'placeholder' => ''
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarFirstDay',
-						'label' => 'Start Day',
-						'value' => $GLOBALS['calendarFirstDay'],
-						'options' => $day
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarDefault',
-						'label' => 'Default View',
-						'value' => $GLOBALS['calendarDefault'],
-						'options' => $calendarDefault
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarTimeFormat',
-						'label' => 'Time Format',
-						'value' => $GLOBALS['calendarTimeFormat'],
-						'options' => $timeFormat
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarLimit',
-						'label' => 'Items Per Day',
-						'value' => $GLOBALS['calendarLimit'],
-						'options' => $limit
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['calendarRefresh'],
-						'options' => optionTime()
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'sonarrUnmonitored',
-						'label' => 'Show Unmonitored',
-						'value' => $GLOBALS['sonarrUnmonitored']
-					)
-				),
-				'Test Connection' => array(
-					array(
-						'type' => 'blank',
-						'label' => 'Please Save before Testing'
-					),
-					array(
-						'type' => 'button',
-						'label' => '',
-						'icon' => 'fa fa-flask',
-						'class' => 'pull-right',
-						'text' => 'Test Connection',
-						'attr' => 'onclick="testAPIConnection(\'sonarr\')"'
-					),
-				)
-			)
-		),
-		array(
-			'name' => 'Lidarr',
-			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/lidarr.png',
-			'category' => 'PMR',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageLidarrEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageLidarrEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageLidarrAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageLidarrAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'lidarrURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['lidarrURL'],
-						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-						'placeholder' => 'http(s)://hostname:port'
-					),
-					array(
-						'type' => 'password-alt',
-						'name' => 'lidarrToken',
-						'label' => 'Token',
-						'value' => $GLOBALS['lidarrToken']
-					)
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'number',
-						'name' => 'calendarStart',
-						'label' => '# of Days Before',
-						'value' => $GLOBALS['calendarStart'],
-						'placeholder' => ''
-					),
-					array(
-						'type' => 'number',
-						'name' => 'calendarEnd',
-						'label' => '# of Days After',
-						'value' => $GLOBALS['calendarEnd'],
-						'placeholder' => ''
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarFirstDay',
-						'label' => 'Start Day',
-						'value' => $GLOBALS['calendarFirstDay'],
-						'options' => $day
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarDefault',
-						'label' => 'Default View',
-						'value' => $GLOBALS['calendarDefault'],
-						'options' => $calendarDefault
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarTimeFormat',
-						'label' => 'Time Format',
-						'value' => $GLOBALS['calendarTimeFormat'],
-						'options' => $timeFormat
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarLimit',
-						'label' => 'Items Per Day',
-						'value' => $GLOBALS['calendarLimit'],
-						'options' => $limit
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['calendarRefresh'],
-						'options' => optionTime()
-					),
-				),
-				'Test Connection' => array(
-					array(
-						'type' => 'blank',
-						'label' => 'Please Save before Testing'
-					),
-					array(
-						'type' => 'button',
-						'label' => '',
-						'icon' => 'fa fa-flask',
-						'class' => 'pull-right',
-						'text' => 'Test Connection',
-						'attr' => 'onclick="testAPIConnection(\'lidarr\')"'
-					),
-				)
-			)
-		),
-		array(
-			'name' => 'Radarr',
-			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/radarr.png',
-			'category' => 'PVR',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageRadarrEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageRadarrEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageRadarrAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageRadarrAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'radarrURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['radarrURL'],
-						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-						'placeholder' => 'http(s)://hostname:port'
-					),
-					array(
-						'type' => 'password-alt',
-						'name' => 'radarrToken',
-						'label' => 'Token',
-						'value' => $GLOBALS['radarrToken']
-					)
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'number',
-						'name' => 'calendarStart',
-						'label' => '# of Days Before',
-						'value' => $GLOBALS['calendarStart'],
-						'placeholder' => ''
-					),
-					array(
-						'type' => 'number',
-						'name' => 'calendarEnd',
-						'label' => '# of Days After',
-						'value' => $GLOBALS['calendarEnd'],
-						'placeholder' => ''
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarFirstDay',
-						'label' => 'Start Day',
-						'value' => $GLOBALS['calendarFirstDay'],
-						'options' => $day
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarDefault',
-						'label' => 'Default View',
-						'value' => $GLOBALS['calendarDefault'],
-						'options' => $calendarDefault
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarTimeFormat',
-						'label' => 'Time Format',
-						'value' => $GLOBALS['calendarTimeFormat'],
-						'options' => $timeFormat
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarLimit',
-						'label' => 'Items Per Day',
-						'value' => $GLOBALS['calendarLimit'],
-						'options' => $limit
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['calendarRefresh'],
-						'options' => optionTime()
-					)
-				),
-				'Test Connection' => array(
-					array(
-						'type' => 'blank',
-						'label' => 'Please Save before Testing'
-					),
-					array(
-						'type' => 'button',
-						'label' => '',
-						'icon' => 'fa fa-flask',
-						'class' => 'pull-right',
-						'text' => 'Test Connection',
-						'attr' => 'onclick="testAPIConnection(\'radarr\')"'
-					),
-				)
-			)
-		),
-		array(
-			'name' => 'CouchPotato',
-			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/couchpotato.png',
-			'category' => 'PVR',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageCouchpotatoEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageCouchpotatoEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageCouchpotatoAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageCouchpotatoAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'couchpotatoURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['couchpotatoURL'],
-						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-						'placeholder' => 'http(s)://hostname:port'
-					),
-					array(
-						'type' => 'password-alt',
-						'name' => 'couchpotatoToken',
-						'label' => 'Token',
-						'value' => $GLOBALS['couchpotatoToken']
-					)
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'select',
-						'name' => 'calendarFirstDay',
-						'label' => 'Start Day',
-						'value' => $GLOBALS['calendarFirstDay'],
-						'options' => $day
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarDefault',
-						'label' => 'Default View',
-						'value' => $GLOBALS['calendarDefault'],
-						'options' => $calendarDefault
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarTimeFormat',
-						'label' => 'Time Format',
-						'value' => $GLOBALS['calendarTimeFormat'],
-						'options' => $timeFormat
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarLimit',
-						'label' => 'Items Per Day',
-						'value' => $GLOBALS['calendarLimit'],
-						'options' => $limit
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['calendarRefresh'],
-						'options' => optionTime()
-					)
-				)
-			)
-		),
-		array(
-			'name' => 'SickRage',
-			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/sickrage.png',
-			'category' => 'PVR',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageSickrageEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageSickrageEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageSickrageAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageSickrageAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'sickrageURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['sickrageURL'],
-						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-						'placeholder' => 'http(s)://hostname:port'
-					),
-					array(
-						'type' => 'password-alt',
-						'name' => 'sickrageToken',
-						'label' => 'Token',
-						'value' => $GLOBALS['sickrageToken']
-					)
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'select',
-						'name' => 'calendarFirstDay',
-						'label' => 'Start Day',
-						'value' => $GLOBALS['calendarFirstDay'],
-						'options' => $day
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarDefault',
-						'label' => 'Default View',
-						'value' => $GLOBALS['calendarDefault'],
-						'options' => $calendarDefault
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarTimeFormat',
-						'label' => 'Time Format',
-						'value' => $GLOBALS['calendarTimeFormat'],
-						'options' => $timeFormat
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarLimit',
-						'label' => 'Items Per Day',
-						'value' => $GLOBALS['calendarLimit'],
-						'options' => $limit
-					),
-					array(
-						'type' => 'select',
-						'name' => 'calendarRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['calendarRefresh'],
-						'options' => optionTime()
-					)
-				)
-			)
-		),
-		array(
-			'name' => 'Ombi',
-			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/ombi.png',
-			'category' => 'Requests',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageOmbiEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageOmbiEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageOmbiAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageOmbiAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'ombiURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['ombiURL'],
-						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-						'placeholder' => 'http(s)://hostname:port'
-					),
-					array(
-						'type' => 'password-alt',
-						'name' => 'ombiToken',
-						'label' => 'Token',
-						'value' => $GLOBALS['ombiToken']
-					)
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'select',
-						'name' => 'homepageOmbiRequestAuth',
-						'label' => 'Minimum Group to Request',
-						'value' => $GLOBALS['homepageOmbiRequestAuth'],
-						'options' => $groups
-					),
-					array(
-						'type' => 'select',
-						'name' => 'ombiTvDefault',
-						'label' => 'TV Show Default Request',
-						'value' => $GLOBALS['ombiTvDefault'],
-						'options' => $ombiTvOptions
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'ombiLimitUser',
-						'label' => 'Limit to User',
-						'value' => $GLOBALS['ombiLimitUser']
-					),
-					array(
-						'type' => 'number',
-						'name' => 'ombiLimit',
-						'label' => 'Item Limit',
-						'value' => $GLOBALS['ombiLimit'],
-					),
-					array(
-						'type' => 'select',
-						'name' => 'ombiRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['ombiRefresh'],
-						'options' => optionTime()
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'ombiAlias',
-						'label' => 'Use Ombi Alias Names',
-						'value' => $GLOBALS['ombiAlias'],
-						'help' => 'Use Ombi Alias Names instead of Usernames - If Alias is blank, Alias will fallback to Username'
-					)
-				),
-				'Default Filter' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'ombiDefaultFilterAvailable',
-						'label' => 'Show Available',
-						'value' => $GLOBALS['ombiDefaultFilterAvailable'],
-						'help' => 'Show All Available Ombi Requests'
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'ombiDefaultFilterUnavailable',
-						'label' => 'Show Unavailable',
-						'value' => $GLOBALS['ombiDefaultFilterUnavailable'],
-						'help' => 'Show All Unavailable Ombi Requests'
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'ombiDefaultFilterApproved',
-						'label' => 'Show Approved',
-						'value' => $GLOBALS['ombiDefaultFilterApproved'],
-						'help' => 'Show All Approved Ombi Requests'
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'ombiDefaultFilterUnapproved',
-						'label' => 'Show Unapproved',
-						'value' => $GLOBALS['ombiDefaultFilterUnapproved'],
-						'help' => 'Show All Unapproved Ombi Requests'
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'ombiDefaultFilterDenied',
-						'label' => 'Show Denied',
-						'value' => $GLOBALS['ombiDefaultFilterDenied'],
-						'help' => 'Show All Denied Ombi Requests'
-					)
-				),
-				'Test Connection' => array(
-					array(
-						'type' => 'blank',
-						'label' => 'Please Save before Testing'
-					),
-					array(
-						'type' => 'button',
-						'label' => '',
-						'icon' => 'fa fa-flask',
-						'class' => 'pull-right',
-						'text' => 'Test Connection',
-						'attr' => 'onclick="testAPIConnection(\'ombi\')"'
-					),
-				)
-			)
-		),
-		array(
-			'name' => 'Unifi',
-			'enabled' => true,
-			'image' => 'plugins/images/tabs/ubnt.png',
-			'category' => 'Monitor',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageUnifiEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageUnifiEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageUnifiAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageUnifiAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'unifiURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['unifiURL'],
-						'help' => 'URL for Unifi',
-						'placeholder' => 'Unifi API URL'
-					),
-					array(
-						'type' => 'blank',
-						'label' => ''
-					),
-					array(
-						'type' => 'input',
-						'name' => 'unifiUsername',
-						'label' => 'Username',
-						'value' => $GLOBALS['unifiUsername']
-					),
-					array(
-						'type' => 'password',
-						'name' => 'unifiPassword',
-						'label' => 'Password',
-						'value' => $GLOBALS['unifiPassword']
-					),
-					array(
-						'type' => 'input',
-						'name' => 'unifiSiteName',
-						'label' => 'Site Name',
-						'value' => $GLOBALS['unifiSiteName'],
-						'help' => 'Site Name - not Site ID nor Site Description',
-					),
-					array(
-						'type' => 'button',
-						'label' => 'Grab Unifi Site',
-						'icon' => 'fa fa-building',
-						'text' => 'Get Unifi Site',
-						'attr' => 'onclick="getUnifiSite(\'unifiSite\')"'
-					),
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'select',
-						'name' => 'homepageUnifiRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['homepageUnifiRefresh'],
-						'options' => optionTime()
-					),
-				),
-				'Test Connection' => array(
-					array(
-						'type' => 'blank',
-						'label' => 'Please Save before Testing'
-					),
-					array(
-						'type' => 'button',
-						'label' => '',
-						'icon' => 'fa fa-flask',
-						'class' => 'pull-right',
-						'text' => 'Test Connection',
-						'attr' => 'onclick="testAPIConnection(\'unifi\')"'
-					),
-				)
-			)
-		),
-		array(
-			'name' => 'HealthChecks',
-			'enabled' => true,
-			'image' => 'plugins/images/tabs/healthchecks.png',
-			'category' => 'Monitor',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageHealthChecksEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageHealthChecksEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageHealthChecksAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageHealthChecksAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'healthChecksURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['healthChecksURL'],
-						'help' => 'URL for HealthChecks API',
-						'placeholder' => 'HealthChecks API URL'
-					),
-					array(
-						'type' => 'password-alt',
-						'name' => 'healthChecksToken',
-						'label' => 'Token',
-						'value' => $GLOBALS['healthChecksToken']
-					)
-				),
-				'Misc Options' => array(
-					array(
-						'type' => 'input',
-						'name' => 'healthChecksTags',
-						'label' => 'Tags',
-						'value' => $GLOBALS['healthChecksTags'],
-						'help' => 'Pull only checks with this tag - Blank for all',
-						'placeholder' => 'Multiple tags using CSV - tag1,tag2'
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageHealthChecksRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['homepageHealthChecksRefresh'],
-						'options' => optionTime()
-					),
-				),
-			)
-		),
-		array(
-			'name' => 'CustomHTML-1',
-			'enabled' => (strpos('personal,business', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/custom1.png',
-			'category' => 'Custom',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepagCustomHTMLoneEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepagCustomHTMLoneEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepagCustomHTMLoneAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepagCustomHTMLoneAuth'],
-						'options' => $groups
-					)
-				),
-				'Code' => array(
-					array(
-						'type' => 'textbox',
-						'name' => 'customHTMLone',
-						'class' => 'hidden customHTMLoneTextarea',
-						'label' => '',
-						'value' => $GLOBALS['customHTMLone'],
-					),
-					array(
-						'type' => 'html',
-						'override' => 12,
-						'label' => 'Custom HTML/JavaScript',
-						'html' => '<button type="button" class="hidden savecustomHTMLoneTextarea btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customHTMLoneEditor" style="height:300px">' . htmlentities($GLOBALS['customHTMLone']) . '</div>'
-					),
-				)
-			)
-		),
-		array(
-			'name' => 'CustomHTML-2',
-			'enabled' => (strpos('personal,business', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/custom2.png',
-			'category' => 'Custom',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepagCustomHTMLtwoEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepagCustomHTMLtwoEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepagCustomHTMLtwoAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepagCustomHTMLtwoAuth'],
-						'options' => $groups
-					)
-				),
-				'Code' => array(
-					array(
-						'type' => 'textbox',
-						'name' => 'customHTMLtwo',
-						'class' => 'hidden customHTMLtwoTextarea',
-						'label' => '',
-						'value' => $GLOBALS['customHTMLtwo'],
-					),
-					array(
-						'type' => 'html',
-						'override' => 12,
-						'label' => 'Custom HTML/JavaScript',
-						'html' => '<button type="button" class="hidden savecustomHTMLtwoTextarea btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customHTMLtwoEditor" style="height:300px">' . htmlentities($GLOBALS['customHTMLtwo']) . '</div>'
-					),
-				)
-			)
-		),
-		array(
-			'name' => 'Misc',
-			'enabled' => true,
-			'image' => 'plugins/images/organizr/logo-no-border.png',
-			'category' => 'Custom',
-			'settings' => array(
-				'YouTube' => array(
-					array(
-						'type' => 'input',
-						'name' => 'youtubeAPI',
-						'label' => 'Youtube API Key',
-						'value' => $GLOBALS['youtubeAPI'],
-						'help' => 'Please make sure to input this API key as the organizr one gets limited'
-					),
-					array(
-						'type' => 'html',
-						'override' => 6,
-						'label' => 'Instructions',
-						'html' => '<a href="https://www.slickremix.com/docs/get-api-key-for-youtube/" target="_blank">Click here for instructions</a>'
-					),
-				)
-			)
-		),
-		array(
-			'name' => 'Pi-hole',
-			'enabled' => true,
-			'image' => 'plugins/images/tabs/pihole.png',
-			'category' => 'Monitor',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepagePiholeEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepagePiholeEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepagePiholeAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepagePiholeAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'piholeURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['piholeURL'],
-						'help' => 'Please make sure to use local IP address and port and to include \'/admin/\' at the end of the URL. You can add multiple Pi-holes by comma separating the URLs.',
-						'placeholder' => 'http(s)://hostname:port/admin/'
-					),
-				),
-				'Misc' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'piholeHeaderToggle',
-						'label' => 'Toggle Title',
-						'value' => $GLOBALS['piholeHeaderToggle'],
-						'help' => 'Shows/hides the title of this homepage module'
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'homepagePiholeCombine',
-						'label' => 'Combine stat cards',
-						'value' => $GLOBALS['homepagePiholeCombine'],
-						'help' => 'This controls whether to combine the stats for multiple piholes into 1 card.',
-					),
-				),
-			)
-		),
-		array(
-			'name' => 'Tautulli',
-			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
-			'image' => 'plugins/images/tabs/tautulli.png',
-			'category' => 'Monitor',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageTautulliEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageTautulliEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageTautulliAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageTautulliAuth'],
-						'options' => $groups
-					)
-				),
-				'Options' => array(
-					array(
-						'type' => 'input',
-						'name' => 'tautulliHeader',
-						'label' => 'Title',
-						'value' => $GLOBALS['tautulliHeader'],
-						'help' => 'Sets the title of this homepage module'
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'tautulliHeaderToggle',
-						'label' => 'Toggle Title',
-						'value' => $GLOBALS['tautulliHeaderToggle'],
-						'help' => 'Shows/hides the title of this homepage module'
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'tautulliURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['tautulliURL'],
-						'help' => 'URL for Tautulli API, include the IP, the port and the base URL (e.g. /tautulli/) in the URL',
-						'placeholder' => 'http://<ip>:<port>'
-					),
-					array(
-						'type' => 'password-alt',
-						'name' => 'tautulliApikey',
-						'label' => 'API Key',
-						'value' => $GLOBALS['tautulliApikey']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageTautulliRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['homepageTautulliRefresh'],
-						'options' => optionTime()
-					),
-				),
-				'Library Stats' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'tautulliLibraries',
-						'label' => 'Libraries',
-						'value' => $GLOBALS['tautulliLibraries'],
-						'help' => 'Shows/hides the card with library information.',
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageTautulliLibraryAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageTautulliLibraryAuth'],
-						'options' => $groups
-					),
-				),
-				'Viewing Stats' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'tautulliPopularMovies',
-						'label' => 'Popular Movies',
-						'value' => $GLOBALS['tautulliPopularMovies'],
-						'help' => 'Shows/hides the card with Popular Movies information.',
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'tautulliPopularTV',
-						'label' => 'Popular TV',
-						'value' => $GLOBALS['tautulliPopularTV'],
-						'help' => 'Shows/hides the card with Popular TV information.',
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'tautulliTopMovies',
-						'label' => 'Top Movies',
-						'value' => $GLOBALS['tautulliTopMovies'],
-						'help' => 'Shows/hides the card with Top Movies information.',
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'tautulliTopTV',
-						'label' => 'Top TV',
-						'value' => $GLOBALS['tautulliTopTV'],
-						'help' => 'Shows/hides the card with Top TV information.',
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageTautulliViewsAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageTautulliViewsAuth'],
-						'options' => $groups
-					),
-				),
-				'Misc Stats' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'tautulliTopUsers',
-						'label' => 'Top Users',
-						'value' => $GLOBALS['tautulliTopUsers'],
-						'help' => 'Shows/hides the card with Top Users information.',
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'tautulliTopPlatforms',
-						'label' => 'Top Platforms',
-						'value' => $GLOBALS['tautulliTopPlatforms'],
-						'help' => 'Shows/hides the card with Top Platforms information.',
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageTautulliMiscAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageTautulliMiscAuth'],
-						'options' => $groups
-					),
-				),
-			)
-		),
-		array(
-			'name' => 'Monitorr',
-			'enabled' => true,
-			'image' => 'plugins/images/tabs/monitorr.png',
-			'category' => 'Monitor',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageMonitorrEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageMonitorrEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageMonitorrAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageMonitorrAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'monitorrURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['monitorrURL'],
-						'help' => 'URL for Monitorr. Please use the revers proxy URL i.e. https://domain.com/monitorr/.',
-						'placeholder' => 'http://domain.com/monitorr/'
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageMonitorrRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['homepageMonitorrRefresh'],
-						'options' => optionTime()
-					),
-				),
-				'Options' => array(
-					array(
-						'type' => 'input',
-						'name' => 'monitorrHeader',
-						'label' => 'Title',
-						'value' => $GLOBALS['monitorrHeader'],
-						'help' => 'Sets the title of this homepage module',
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'monitorrHeaderToggle',
-						'label' => 'Toggle Title',
-						'value' => $GLOBALS['monitorrHeaderToggle'],
-						'help' => 'Shows/hides the title of this homepage module'
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'monitorrCompact',
-						'label' => 'Compact view',
-						'value' => $GLOBALS['monitorrCompact'],
-						'help' => 'Toggles the compact view of this homepage module'
-					),
-				),
-			)
-		),
-		array(
-			'name' => 'Weather-Air',
-			'enabled' => true,
-			'image' => 'plugins/images/tabs/wind.png',
-			'category' => 'Monitor',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'switch',
-						'name' => 'homepageWeatherAndAirEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageWeatherAndAirEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageWeatherAndAirAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageWeatherAndAirAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'homepageWeatherAndAirLatitude',
-						'label' => 'Latitude',
-						'value' => $GLOBALS['homepageWeatherAndAirLatitude'],
-						'help' => 'Please enter full latitude including minus if needed'
-					),
-					array(
-						'type' => 'input',
-						'name' => 'homepageWeatherAndAirLongitude',
-						'label' => 'Longitude',
-						'value' => $GLOBALS['homepageWeatherAndAirLongitude'],
-						'help' => 'Please enter full longitude including minus if needed'
-					),
-					array(
-						'type' => 'blank',
-						'label' => ''
-					),
-					array(
-						'type' => 'button',
-						'label' => '',
-						'icon' => 'fa fa-search',
-						'class' => 'pull-right',
-						'text' => 'Need Help With Coordinates?',
-						'attr' => 'onclick="showLookupCoordinatesModal()"'
-					),
-				),
-				'Options' => array(
-					array(
-						'type' => 'input',
-						'name' => 'homepageWeatherAndAirWeatherHeader',
-						'label' => 'Title',
-						'value' => $GLOBALS['homepageWeatherAndAirWeatherHeader'],
-						'help' => 'Sets the title of this homepage module',
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'homepageWeatherAndAirWeatherHeaderToggle',
-						'label' => 'Toggle Title',
-						'value' => $GLOBALS['homepageWeatherAndAirWeatherHeaderToggle'],
-						'help' => 'Shows/hides the title of this homepage module'
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'homepageWeatherAndAirWeatherEnabled',
-						'label' => 'Enable Weather',
-						'value' => $GLOBALS['homepageWeatherAndAirWeatherEnabled'],
-						'help' => 'Toggles the view module for Weather'
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'homepageWeatherAndAirAirQualityEnabled',
-						'label' => 'Enable Air Quality',
-						'value' => $GLOBALS['homepageWeatherAndAirAirQualityEnabled'],
-						'help' => 'Toggles the view module for Air Quality'
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'homepageWeatherAndAirPollenEnabled',
-						'label' => 'Enable Pollen',
-						'value' => $GLOBALS['homepageWeatherAndAirPollenEnabled'],
-						'help' => 'Toggles the view module for Pollen'
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageWeatherAndAirUnits',
-						'label' => 'Unit of Measurement',
-						'value' => $GLOBALS['homepageWeatherAndAirUnits'],
-						'options' => array(
-							array(
-								'name' => 'Imperial',
-								'value' => 'imperial'
-							),
-							array(
-								'name' => 'Metric',
-								'value' => 'metric'
-							)
-						)
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageWeatherAndAirRefresh',
-						'label' => 'Refresh Seconds',
-						'value' => $GLOBALS['homepageWeatherAndAirRefresh'],
-						'options' => optionTime()
-					),
-				),
-			)
-		),
-		array(
-			'name' => 'Speedtest',
-			'enabled' => true,
-			'image' => 'plugins/images/tabs/speedtest-icon.png',
-			'category' => 'Monitor',
-			'settings' => array(
-				'Enable' => array(
-					array(
-						'type' => 'html',
-						'override' => 6,
-						'label' => 'Info',
-						'html' => '<p>This homepage item requires <a href="https://github.com/henrywhitaker3/Speedtest-Tracker" target="_blank" rel="noreferrer noopener">Speedtest-Tracker <i class="fa fa-external-link" aria-hidden="true"></i></a> to be running on your network.</p>'
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'homepageSpeedtestEnabled',
-						'label' => 'Enable',
-						'value' => $GLOBALS['homepageSpeedtestEnabled']
-					),
-					array(
-						'type' => 'select',
-						'name' => 'homepageSpeedtestAuth',
-						'label' => 'Minimum Authentication',
-						'value' => $GLOBALS['homepageSpeedtestAuth'],
-						'options' => $groups
-					)
-				),
-				'Connection' => array(
-					array(
-						'type' => 'input',
-						'name' => 'speedtestURL',
-						'label' => 'URL',
-						'value' => $GLOBALS['speedtestURL'],
-						'help' => 'Enter the IP:PORT of your speedtest instance e.g. http(s)://<ip>:<port>'
-					),
-				),
-				'Options' => array(
-					array(
-						'type' => 'input',
-						'name' => 'speedtestHeader',
-						'label' => 'Title',
-						'value' => $GLOBALS['speedtestHeader'],
-						'help' => 'Sets the title of this homepage module',
-					),
-					array(
-						'type' => 'switch',
-						'name' => 'speedtestHeaderToggle',
-						'label' => 'Toggle Title',
-						'value' => $GLOBALS['speedtestHeaderToggle'],
-						'help' => 'Shows/hides the title of this homepage module'
-					),
-				),
-			)
-		),
-		netdataSettngsArray()
-	);
-}
-
-function buildHomepageSettings()
-{
-	$homepageOrder = homepageOrder();
-	$homepageList = '<h4>Drag Homepage Items to Order Them</h4><div id="homepage-items-sort" class="external-events">';
-	$inputList = '<form id="homepage-values" class="row">';
-	foreach ($homepageOrder as $key => $val) {
-		switch ($key) {
-			case 'homepageOrdercustomhtml':
-				$class = 'bg-info';
-				$image = 'plugins/images/tabs/custom1.png';
-				if (!$GLOBALS['homepagCustomHTMLoneEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrdercustomhtmlTwo':
-				$class = 'bg-info';
-				$image = 'plugins/images/tabs/custom2.png';
-				if (!$GLOBALS['homepagCustomHTMLtwoEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrdertransmission':
-				$class = 'bg-transmission';
-				$image = 'plugins/images/tabs/transmission.png';
-				if (!$GLOBALS['homepageTransmissionEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrdernzbget':
-				$class = 'bg-nzbget';
-				$image = 'plugins/images/tabs/nzbget.png';
-				if (!$GLOBALS['homepageNzbgetEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrderjdownloader':
-				$class = 'bg-sab';
-				$image = 'plugins/images/tabs/jdownloader.png';
-				if (!$GLOBALS['homepageJdownloaderEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrdersabnzbd':
-				$class = 'bg-sab';
-				$image = 'plugins/images/tabs/sabnzbd.png';
-				if (!$GLOBALS['homepageSabnzbdEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrderdeluge':
-				$class = 'bg-deluge';
-				$image = 'plugins/images/tabs/deluge.png';
-				if (!$GLOBALS['homepageDelugeEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrderqBittorrent':
-				$class = 'bg-qbit';
-				$image = 'plugins/images/tabs/qBittorrent.png';
-				if (!$GLOBALS['homepageqBittorrentEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrderrTorrent':
-				$class = 'bg-qbit';
-				$image = 'plugins/images/tabs/rTorrent.png';
-				if (!$GLOBALS['homepagerTorrentEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrderplexnowplaying':
-			case 'homepageOrderplexrecent':
-			case 'homepageOrderplexplaylist':
-				$class = 'bg-plex';
-				$image = 'plugins/images/tabs/plex.png';
-				if (!$GLOBALS['homepagePlexEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrderembynowplaying':
-			case 'homepageOrderembyrecent':
-				$class = 'bg-emby';
-				$image = 'plugins/images/tabs/emby.png';
-				if (!$GLOBALS['homepageEmbyEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrderombi':
-				$class = 'bg-inverse';
-				$image = 'plugins/images/tabs/ombi.png';
-				if (!$GLOBALS['homepageOmbiEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrdercalendar':
-				$class = 'bg-primary';
-				$image = 'plugins/images/tabs/calendar.png';
-				if (!$GLOBALS['homepageSonarrEnabled'] && !$GLOBALS['homepageRadarrEnabled'] && !$GLOBALS['homepageSickrageEnabled'] && !$GLOBALS['homepageCouchpotatoEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrderdownloader':
-				$class = 'bg-inverse';
-				$image = 'plugins/images/tabs/downloader.png';
-				if (!$GLOBALS['jdownloaderCombine'] && !$GLOBALS['sabnzbdCombine'] && !$GLOBALS['nzbgetCombine'] && !$GLOBALS['rTorrentCombine'] && !$GLOBALS['delugeCombine'] && !$GLOBALS['transmissionCombine'] && !$GLOBALS['qBittorrentCombine']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrderhealthchecks':
-				$class = 'bg-healthchecks';
-				$image = 'plugins/images/tabs/healthchecks.png';
-				if (!$GLOBALS['homepageHealthChecksEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrderunifi':
-				$class = 'bg-info';
-				$image = 'plugins/images/tabs/ubnt.png';
-				if (!$GLOBALS['homepageUnifiEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrdertautulli':
-				$class = 'bg-info';
-				$image = 'plugins/images/tabs/tautulli.png';
-				if (!$GLOBALS['homepageTautulliEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrderPihole':
-				$class = 'bg-info';
-				$image = 'plugins/images/tabs/pihole.png';
-				if (!$GLOBALS['homepagePiholeEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrderMonitorr':
-				$class = 'bg-info';
-				$image = 'plugins/images/tabs/monitorr.png';
-				if (!$GLOBALS['homepageMonitorrEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrderWeatherAndAir':
-				$class = 'bg-success';
-				$image = 'plugins/images/tabs/wind.png';
-				if (!$GLOBALS['homepageWeatherAndAirEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrderSpeedtest':
-				$class = 'bg-success';
-				$image = 'plugins/images/tabs/speedtest-icon.png';
-				if (!$GLOBALS['homepageSpeedtestEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			case 'homepageOrderNetdata':
-				$class = 'bg-success';
-				$image = 'plugins/images/tabs/netdata.png';
-				if (!$GLOBALS['homepageNetdataEnabled']) {
-					$class .= ' faded';
-				}
-				break;
-			default:
-				$class = 'blue-bg';
-				$image = '';
-				break;
 		}
-		$homepageList .= '
-		<div class="col-md-3 col-xs-12 sort-homepage m-t-10 hvr-grow clearfix">
-			<div class="homepage-drag fc-event ' . $class . ' lazyload"  data-src="' . $image . '">
-				<span class="ordinal-position text-uppercase badge bg-org homepage-number" data-link="' . $key . '" style="float:left;width: 30px;">' . $val . '</span>
-				<span class="homepage-text">&nbsp; ' . strtoupper(substr($key, 13)) . '</span>
-
-			</div>
-		</div>
-		';
-		$inputList .= '<input type="hidden" name="' . $key . '">';
+		return true;
 	}
-	$homepageList .= '</div>';
-	$inputList .= '</form>';
-	return $homepageList . $inputList;
 }
-
-function ombiTVDefault($type)
-{
-	switch ($type) {
-		case 'all':
-			return ($type == $GLOBALS['ombiTvDefault']) ? true : false;
-		case 'first':
-			return ($type == $GLOBALS['ombiTvDefault']) ? true : false;
-		case 'last':
-			return ($type == $GLOBALS['ombiTvDefault']) ? true : false;
-		default:
-			return false;
-	}
-	return false;
-}

+ 4 - 69
api/functions/log-functions.php

@@ -1,75 +1,10 @@
 <?php
-function checkLog($path)
-{
-	if (file_exists($path)) {
-		if (filesize($path) > 500000) {
-			rename($path, $path . '[' . date('Y-m-d') . '].json');
-			return false;
-		}
-		return true;
-	} else {
-		return false;
-	}
-}
 
-function writeLoginLog($username, $authType)
+trait LogFunctions
 {
-	$username = htmlspecialchars($username, ENT_QUOTES);
-	if (checkLog($GLOBALS['organizrLoginLog'])) {
-		$getLog = str_replace("\r\ndate", "date", file_get_contents($GLOBALS['organizrLoginLog']));
-		$gotLog = json_decode($getLog, true);
-	}
-	$logEntryFirst = array('logType' => 'login_log', 'auth' => array(array('date' => date("Y-m-d H:i:s"), 'utc_date' => $GLOBALS['currentTime'], 'username' => $username, 'ip' => userIP(), 'auth_type' => $authType)));
-	$logEntry = array('date' => date("Y-m-d H:i:s"), 'utc_date' => $GLOBALS['currentTime'], 'username' => $username, 'ip' => userIP(), 'auth_type' => $authType);
-	if (isset($gotLog)) {
-		array_push($gotLog["auth"], $logEntry);
-		$writeFailLog = str_replace("date", "\r\ndate", json_encode($gotLog));
-	} else {
-		$writeFailLog = str_replace("date", "\r\ndate", json_encode($logEntryFirst));
-	}
-	file_put_contents($GLOBALS['organizrLoginLog'], $writeFailLog);
-}
 
-function writeLog($type = 'error', $message, $username = null)
-{
-	$GLOBALS['timeExecution'] = timeExecution($GLOBALS['timeExecution']);
-	$message = $message . ' [Execution Time: ' . formatSeconds($GLOBALS['timeExecution']) . ']';
-	$username = ($username) ? htmlspecialchars($username, ENT_QUOTES) : $GLOBALS['organizrUser']['username'];
-	if (checkLog($GLOBALS['organizrLog'])) {
-		$getLog = str_replace("\r\ndate", "date", file_get_contents($GLOBALS['organizrLog']));
-		$gotLog = json_decode($getLog, true);
-	}
-	$logEntryFirst = array('logType' => 'organizr_log', 'log_items' => array(array('date' => date("Y-m-d H:i:s"), 'utc_date' => $GLOBALS['currentTime'], 'type' => $type, 'username' => $username, 'ip' => userIP(), 'message' => $message)));
-	$logEntry = array('date' => date("Y-m-d H:i:s"), 'utc_date' => $GLOBALS['currentTime'], 'type' => $type, 'username' => $username, 'ip' => userIP(), 'message' => $message);
-	if (isset($gotLog)) {
-		array_push($gotLog["log_items"], $logEntry);
-		$writeFailLog = str_replace("date", "\r\ndate", json_encode($gotLog));
-	} else {
-		$writeFailLog = str_replace("date", "\r\ndate", json_encode($logEntryFirst));
-	}
-	file_put_contents($GLOBALS['organizrLog'], $writeFailLog);
 }
 
-function getLog($type, $reverse = true)
-{
-	switch ($type) {
-		case 'login':
-		case 'loginLog':
-			$file = $GLOBALS['organizrLoginLog'];
-			$parent = 'auth';
-			break;
-		case 'org':
-		case 'organizrLog':
-			$file = $GLOBALS['organizrLog'];
-			$parent = 'log_items';
-		// no break
-		default:
-			break;
-	}
-	if (!file_exists($file)) {
-		return false;
-	}
-	$getLog = str_replace("\r\ndate", "date", file_get_contents($file));
-	$gotLog = json_decode($getLog, true);
-	return ($reverse) ? array_reverse($gotLog[$parent]) : $gotLog[$parent];
-}
+
+
+

+ 1 - 421
api/functions/netdata-functions.php

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

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 540 - 439
api/functions/normal-functions.php


+ 707 - 252
api/functions/option-functions.php

@@ -1,257 +1,712 @@
 <?php
-function optionLimit()
-{
-	return array(
-		array(
-			'name' => '1 Item',
-			'value' => '1'
-		),
-		array(
-			'name' => '2 Items',
-			'value' => '2'
-		),
-		array(
-			'name' => '3 Items',
-			'value' => '3'
-		),
-		array(
-			'name' => '4 Items',
-			'value' => '4'
-		),
-		array(
-			'name' => '5 Items',
-			'value' => '5'
-		),
-		array(
-			'name' => '6 Items',
-			'value' => '6'
-		),
-		array(
-			'name' => '7 Items',
-			'value' => '7'
-		),
-		array(
-			'name' => '8 Items',
-			'value' => '8'
-		),
-		array(
-			'name' => 'Unlimited',
-			'value' => '1000'
-		),
-	);
-}
-
-function optionNotificationTypes()
-{
-	return array(
-		array(
-			'name' => 'Toastr',
-			'value' => 'toastr'
-		),
-		array(
-			'name' => 'Izi',
-			'value' => 'izi'
-		),
-		array(
-			'name' => 'Alertify',
-			'value' => 'alertify'
-		),
-		array(
-			'name' => 'Noty',
-			'value' => 'noty'
-		),
-	);
-}
 
-function optionNotificationPositions()
+trait OptionsFunction
 {
-	return array(
-		array(
-			'name' => 'Bottom Right',
-			'value' => 'br'
-		),
-		array(
-			'name' => 'Bottom Left',
-			'value' => 'bl'
-		),
-		array(
-			'name' => 'Bottom Center',
-			'value' => 'bc'
-		),
-		array(
-			'name' => 'Top Right',
-			'value' => 'tr'
-		),
-		array(
-			'name' => 'Top Left',
-			'value' => 'tl'
-		),
-		array(
-			'name' => 'Top Center',
-			'value' => 'tc'
-		),
-		array(
-			'name' => 'Center',
-			'value' => 'c'
-		),
-	);
-}
-
-function optionTime()
-{
-	return array(
-		array(
-			'name' => '2.5',
-			'value' => '2500'
-		),
-		array(
-			'name' => '5',
-			'value' => '5000'
-		),
-		array(
-			'name' => '10',
-			'value' => '10000'
-		),
-		array(
-			'name' => '15',
-			'value' => '15000'
-		),
-		array(
-			'name' => '30',
-			'value' => '30000'
-		),
-		array(
-			'name' => '60 [1 Minute]',
-			'value' => '60000'
-		),
-		array(
-			'name' => '300 [5 Minutes]',
-			'value' => '300000'
-		),
-		array(
-			'name' => '600 [10 Minutes]',
-			'value' => '600000'
-		),
-		array(
-			'name' => '900 [15 Minutes]',
-			'value' => '900000'
-		),
-		array(
-			'name' => '1800 [30 Minutes]',
-			'value' => '1800000'
-		),
-		array(
-			'name' => '3600 [1 Hour]',
-			'value' => '3600000'
-		),
-	);
+	public function calendarLocaleOptions()
+	{
+		return [
+			[
+				'name' => 'Arabic (Standard)',
+				'value' => 'ar',
+			],
+			[
+				'name' => 'Arabic (Morocco)',
+				'value' => 'ar-ma',
+			],
+			[
+				'name' => 'Arabic (Saudi Arabia)',
+				'value' => 'ar-sa'
+			],
+			[
+				'value' => 'ar-tn',
+				'name' => 'Arabic (Tunisia)'
+			],
+			[
+				'value' => 'bg',
+				'name' => 'Bulgarian'
+			],
+			[
+				'value' => 'ca',
+				'name' => 'Catalan'
+			],
+			[
+				'value' => 'cs',
+				'name' => 'Czech'
+			],
+			[
+				'value' => 'da',
+				'name' => 'Danish'
+			],
+			[
+				'value' => 'de',
+				'name' => 'German (Standard)'
+			],
+			[
+				'value' => 'de-at',
+				'name' => 'German (Austria)'
+			],
+			[
+				'value' => 'el',
+				'name' => 'Greek'
+			],
+			[
+				'value' => 'en',
+				'name' => 'English'
+			],
+			[
+				'value' => 'en-au',
+				'name' => 'English (Australia)'
+			],
+			[
+				'value' => 'en-ca',
+				'name' => 'English (Canada)'
+			],
+			[
+				'value' => 'en-gb',
+				'name' => 'English (United Kingdom)'
+			],
+			[
+				'value' => 'es',
+				'name' => 'Spanish'
+			],
+			[
+				'value' => 'fa',
+				'name' => 'Farsi'
+			],
+			[
+				'value' => 'fi',
+				'name' => 'Finnish'
+			],
+			[
+				'value' => 'fr',
+				'name' => 'French (Standard)'
+			],
+			[
+				'value' => 'fr-ca',
+				'name' => 'French (Canada)'
+			],
+			[
+				'value' => 'he',
+				'name' => 'Hebrew'
+			],
+			[
+				'value' => 'hi',
+				'name' => 'Hindi'
+			],
+			[
+				'value' => 'hr',
+				'name' => 'Croatian'
+			],
+			[
+				'value' => 'hu',
+				'name' => 'Hungarian'
+			],
+			[
+				'value' => 'id',
+				'name' => 'Indonesian'
+			],
+			[
+				'value' => 'is',
+				'name' => 'Icelandic'
+			],
+			[
+				'value' => 'it',
+				'name' => 'Italian'
+			],
+			[
+				'value' => 'ja',
+				'name' => 'Japanese'
+			],
+			[
+				'value' => 'ko',
+				'name' => 'Korean'
+			],
+			[
+				'value' => 'lt',
+				'name' => 'Lithuanian'
+			],
+			[
+				'value' => 'lv',
+				'name' => 'Latvian'
+			],
+			[
+				'value' => 'nb',
+				'name' => 'Norwegian (Bokmal)'
+			],
+			[
+				'value' => 'nl',
+				'name' => 'Dutch (Standard)'
+			],
+			[
+				'value' => 'pl',
+				'name' => 'Polish'
+			],
+			[
+				'value' => 'pt',
+				'name' => 'Portuguese'
+			],
+			[
+				'value' => 'pt-br',
+				'name' => 'Portuguese (Brazil)'
+			],
+			[
+				'value' => 'ro',
+				'name' => 'Romanian'
+			],
+			[
+				'value' => 'ru',
+				'name' => 'Russian'
+			],
+			[
+				'value' => 'sk',
+				'name' => 'Slovak'
+			],
+			[
+				'value' => 'sl',
+				'name' => 'Slovenian'
+			],
+			[
+				'value' => 'sr',
+				'name' => 'Serbian'
+			],
+			[
+				'value' => 'sv',
+				'name' => 'Swedish'
+			],
+			[
+				'value' => 'th',
+				'name' => 'Thai'
+			],
+			[
+				'value' => 'tr',
+				'name' => 'Turkish'
+			],
+			[
+				'value' => 'uk',
+				'name' => 'Ukrainian'
+			],
+			[
+				'value' => 'vi',
+				'name' => 'Vietnamese'
+			],
+			[
+				'value' => 'zh-cn',
+				'name' => 'Chinese (PRC)'
+			],
+			[
+				'value' => 'zh-tw',
+				'name' => 'Chinese (Taiwan)'
+			]
+		];
+		
+	}
 	
-}
-
-function netdataOptions()
-{
-	return [
-		[
-			'name' => 'Disk Read',
-			'value' => 'disk-read',
-		],
-		[
-			'name' => 'Disk Write',
-			'value' => 'disk-write',
-		],
-		[
-			'name' => 'CPU',
-			'value' => 'cpu'
-		],
-		[
-			'name' => 'Network Inbound',
-			'value' => 'net-in',
-		],
-		[
-			'name' => 'Network Outbound',
-			'value' => 'net-out',
-		],
-		[
-			'name' => 'Used RAM',
-			'value' => 'ram-used',
-		],
-		[
-			'name' => 'Used Swap',
-			'value' => 'swap-used',
-		],
-		[
-			'name' => 'Disk space used',
-			'value' => 'disk-used',
-		],
-		[
-			'name' => 'Disk space available',
-			'value' => 'disk-avail',
-		],
-		[
-			'name' => 'Custom',
-			'value' => 'custom',
-		]
-	];
-}
-
-function netdataChartOptions()
-{
-	return [
-		[
-			'name' => 'Easy Pie Chart',
-			'value' => 'easypiechart',
-		],
-		[
-			'name' => 'Gauge',
-			'value' => 'gauge'
-		]
-	];
-}
-
-function netdataColourOptions()
-{
-	return [
-		[
-			'name' => 'Red',
-			'value' => 'fe3912',
-		],
-		[
-			'name' => 'Green',
-			'value' => '46e302',
-		],
-		[
-			'name' => 'Purple',
-			'value' => 'CC22AA'
-		],
-		[
-			'name' => 'Blue',
-			'value' => '5054e6',
-		],
-		[
-			'name' => 'Yellow',
-			'value' => 'dddd00',
-		],
-		[
-			'name' => 'Orange',
-			'value' => 'd66300',
-		]
-	];
-}
-
-function netdataSizeOptions()
-{
-	return [
-		[
-			'name' => 'Large',
-			'value' => 'lg',
-		],
-		[
-			'name' => 'Medium',
-			'value' => 'md',
-		],
-		[
-			'name' => 'Small',
-			'value' => 'sm'
-		]
-	];
+	public function daysOptions()
+	{
+		return array(
+			array(
+				'name' => 'Sunday',
+				'value' => '0'
+			),
+			array(
+				'name' => 'Monday',
+				'value' => '1'
+			),
+			array(
+				'name' => 'Tueday',
+				'value' => '2'
+			),
+			array(
+				'name' => 'Wednesday',
+				'value' => '3'
+			),
+			array(
+				'name' => 'Thursday',
+				'value' => '4'
+			),
+			array(
+				'name' => 'Friday',
+				'value' => '5'
+			),
+			array(
+				'name' => 'Saturday',
+				'value' => '6'
+			)
+		);
+	}
+	
+	public function mediaServerOptions()
+	{
+		return array(
+			array(
+				'name' => 'N/A',
+				'value' => ''
+			),
+			array(
+				'name' => 'Plex',
+				'value' => 'plex'
+			),
+			array(
+				'name' => 'Emby [Not Available]',
+				'value' => 'emby'
+			)
+		);
+	}
+	
+	public function ombiTvOptions()
+	{
+		return array(
+			array(
+				'name' => 'All Seasons',
+				'value' => 'all'
+			),
+			array(
+				'name' => 'First Season Only',
+				'value' => 'first'
+			),
+			array(
+				'name' => 'Last Season Only',
+				'value' => 'last'
+			),
+		);
+	}
+	
+	public function limitOptions()
+	{
+		return array(
+			array(
+				'name' => '1 Item',
+				'value' => '1'
+			),
+			array(
+				'name' => '2 Items',
+				'value' => '2'
+			),
+			array(
+				'name' => '3 Items',
+				'value' => '3'
+			),
+			array(
+				'name' => '4 Items',
+				'value' => '4'
+			),
+			array(
+				'name' => '5 Items',
+				'value' => '5'
+			),
+			array(
+				'name' => '6 Items',
+				'value' => '6'
+			),
+			array(
+				'name' => '7 Items',
+				'value' => '7'
+			),
+			array(
+				'name' => '8 Items',
+				'value' => '8'
+			),
+			array(
+				'name' => 'Unlimited',
+				'value' => '1000'
+			),
+		);
+	}
+	
+	public function notificationTypesOptions()
+	{
+		return array(
+			array(
+				'name' => 'Toastr',
+				'value' => 'toastr'
+			),
+			array(
+				'name' => 'Izi',
+				'value' => 'izi'
+			),
+			array(
+				'name' => 'Alertify',
+				'value' => 'alertify'
+			),
+			array(
+				'name' => 'Noty',
+				'value' => 'noty'
+			),
+		);
+	}
+	
+	public function notificationPositionsOptions()
+	{
+		return array(
+			array(
+				'name' => 'Bottom Right',
+				'value' => 'br'
+			),
+			array(
+				'name' => 'Bottom Left',
+				'value' => 'bl'
+			),
+			array(
+				'name' => 'Bottom Center',
+				'value' => 'bc'
+			),
+			array(
+				'name' => 'Top Right',
+				'value' => 'tr'
+			),
+			array(
+				'name' => 'Top Left',
+				'value' => 'tl'
+			),
+			array(
+				'name' => 'Top Center',
+				'value' => 'tc'
+			),
+			array(
+				'name' => 'Center',
+				'value' => 'c'
+			),
+		);
+	}
+	
+	public function timeOptions()
+	{
+		return array(
+			array(
+				'name' => '2.5',
+				'value' => '2500'
+			),
+			array(
+				'name' => '5',
+				'value' => '5000'
+			),
+			array(
+				'name' => '10',
+				'value' => '10000'
+			),
+			array(
+				'name' => '15',
+				'value' => '15000'
+			),
+			array(
+				'name' => '30',
+				'value' => '30000'
+			),
+			array(
+				'name' => '60 [1 Minute]',
+				'value' => '60000'
+			),
+			array(
+				'name' => '300 [5 Minutes]',
+				'value' => '300000'
+			),
+			array(
+				'name' => '600 [10 Minutes]',
+				'value' => '600000'
+			),
+			array(
+				'name' => '900 [15 Minutes]',
+				'value' => '900000'
+			),
+			array(
+				'name' => '1800 [30 Minutes]',
+				'value' => '1800000'
+			),
+			array(
+				'name' => '3600 [1 Hour]',
+				'value' => '3600000'
+			),
+		);
+		
+	}
+	
+	public function netdataOptions()
+	{
+		return [
+			[
+				'name' => 'Disk Read',
+				'value' => 'disk-read',
+			],
+			[
+				'name' => 'Disk Write',
+				'value' => 'disk-write',
+			],
+			[
+				'name' => 'CPU',
+				'value' => 'cpu'
+			],
+			[
+				'name' => 'Network Inbound',
+				'value' => 'net-in',
+			],
+			[
+				'name' => 'Network Outbound',
+				'value' => 'net-out',
+			],
+			[
+				'name' => 'Used RAM',
+				'value' => 'ram-used',
+			],
+			[
+				'name' => 'Used Swap',
+				'value' => 'swap-used',
+			],
+			[
+				'name' => 'Disk space used',
+				'value' => 'disk-used',
+			],
+			[
+				'name' => 'Disk space available',
+				'value' => 'disk-avail',
+			],
+			[
+				'name' => 'Custom',
+				'value' => 'custom',
+			]
+		];
+	}
+	
+	public function netdataChartOptions()
+	{
+		return [
+			[
+				'name' => 'Easy Pie Chart',
+				'value' => 'easypiechart',
+			],
+			[
+				'name' => 'Gauge',
+				'value' => 'gauge'
+			]
+		];
+	}
+	
+	public function netdataColourOptions()
+	{
+		return [
+			[
+				'name' => 'Red',
+				'value' => 'fe3912',
+			],
+			[
+				'name' => 'Green',
+				'value' => '46e302',
+			],
+			[
+				'name' => 'Purple',
+				'value' => 'CC22AA'
+			],
+			[
+				'name' => 'Blue',
+				'value' => '5054e6',
+			],
+			[
+				'name' => 'Yellow',
+				'value' => 'dddd00',
+			],
+			[
+				'name' => 'Orange',
+				'value' => 'd66300',
+			]
+		];
+	}
+	
+	public function netdataSizeOptions()
+	{
+		return [
+			[
+				'name' => 'Large',
+				'value' => 'lg',
+			],
+			[
+				'name' => 'Medium',
+				'value' => 'md',
+			],
+			[
+				'name' => 'Small',
+				'value' => 'sm'
+			]
+		];
+	}
+	
+	public function timeFormatOptions()
+	{
+		return array(
+			array(
+				'name' => '6p',
+				'value' => 'h(:mm)t'
+			),
+			array(
+				'name' => '6:00p',
+				'value' => 'h:mmt'
+			),
+			array(
+				'name' => '6:00',
+				'value' => 'h:mm'
+			),
+			array(
+				'name' => '18',
+				'value' => 'H(:mm)'
+			),
+			array(
+				'name' => '18:00',
+				'value' => 'H:mm'
+			)
+		);
+	}
+	
+	public function rTorrentSortOptions()
+	{
+		return array(
+			array(
+				'name' => 'Date Desc',
+				'value' => 'dated'
+			),
+			array(
+				'name' => 'Date Asc',
+				'value' => 'datea'
+			),
+			array(
+				'name' => 'Hash Desc',
+				'value' => 'hashd'
+			),
+			array(
+				'name' => 'Hash Asc',
+				'value' => 'hasha'
+			),
+			array(
+				'name' => 'Name Desc',
+				'value' => 'named'
+			),
+			array(
+				'name' => 'Name Asc',
+				'value' => 'namea'
+			),
+			array(
+				'name' => 'Size Desc',
+				'value' => 'sized'
+			),
+			array(
+				'name' => 'Size Asc',
+				'value' => 'sizea'
+			),
+			array(
+				'name' => 'Label Desc',
+				'value' => 'labeld'
+			),
+			array(
+				'name' => 'Label Asc',
+				'value' => 'labela'
+			),
+			array(
+				'name' => 'Status Desc',
+				'value' => 'statusd'
+			),
+			array(
+				'name' => 'Status Asc',
+				'value' => 'statusa'
+			),
+		);
+	}
+	
+	public function qBittorrentApiOptions()
+	{
+		return array(
+			array(
+				'name' => 'V1',
+				'value' => '1'
+			),
+			array(
+				'name' => 'V2',
+				'value' => '2'
+			),
+		);
+	}
+	
+	public function qBittorrentSortOptions()
+	{
+		return array(
+			array(
+				'name' => 'Hash',
+				'value' => 'hash'
+			),
+			array(
+				'name' => 'Name',
+				'value' => 'name'
+			),
+			array(
+				'name' => 'Size',
+				'value' => 'size'
+			),
+			array(
+				'name' => 'Progress',
+				'value' => 'progress'
+			),
+			array(
+				'name' => 'Download Speed',
+				'value' => 'dlspeed'
+			),
+			array(
+				'name' => 'Upload Speed',
+				'value' => 'upspeed'
+			),
+			array(
+				'name' => 'Priority',
+				'value' => 'priority'
+			),
+			array(
+				'name' => 'Number of Seeds',
+				'value' => 'num_seeds'
+			),
+			array(
+				'name' => 'Number of Seeds in Swarm',
+				'value' => 'num_complete'
+			),
+			array(
+				'name' => 'Number of Leechers',
+				'value' => 'num_leechs'
+			),
+			array(
+				'name' => 'Number of Leechers in Swarm',
+				'value' => 'num_incomplete'
+			),
+			array(
+				'name' => 'Ratio',
+				'value' => 'ratio'
+			),
+			array(
+				'name' => 'ETA',
+				'value' => 'eta'
+			),
+			array(
+				'name' => 'State',
+				'value' => 'state'
+			),
+			array(
+				'name' => 'Category',
+				'value' => 'category'
+			)
+		);
+	}
+	
+	public function calendarDefaultOptions()
+	{
+		return array(
+			array(
+				'name' => 'Month',
+				'value' => 'month'
+			),
+			array(
+				'name' => 'Day',
+				'value' => 'basicDay'
+			),
+			array(
+				'name' => 'Week',
+				'value' => 'basicWeek'
+			),
+			array(
+				'name' => 'List',
+				'value' => 'list'
+			)
+		);
+	}
 }

+ 543 - 2708
api/functions/organizr-functions.php

@@ -1,2768 +1,603 @@
 <?php
-function checkPlexAdminFilled()
-{
-	if ($GLOBALS['plexAdmin'] == '') {
-		return false;
-	} else {
-		if ((strpos($GLOBALS['plexAdmin'], '@') !== false)) {
-			return 'email';
-		} else {
-			return 'username';
-		}
-	}
-}
-
-function organizrSpecialSettings()
-{
-	$refreshSearch = "Refresh";
-	$tautulliSearch = "tautulli_token";
-	$tautulli = array_filter($_COOKIE, function ($k) use ($tautulliSearch) {
-		return stripos($k, $tautulliSearch) !== false;
-	}, ARRAY_FILTER_USE_KEY);
-	return array(
-		'homepage' => array(
-			'refresh' => array_filter($GLOBALS, function ($k) use ($refreshSearch) {
-				return stripos($k, $refreshSearch) !== false;
-			}, ARRAY_FILTER_USE_KEY),
-			'search' => array(
-				'enabled' => (qualifyRequest($GLOBALS['mediaSearchAuth']) && $GLOBALS['mediaSearch'] == true && $GLOBALS['plexToken']) ? true : false,
-				'type' => $GLOBALS['mediaSearchType'],
-			),
-			'ombi' => array(
-				'enabled' => (qualifyRequest($GLOBALS['homepageOmbiAuth']) && qualifyRequest($GLOBALS['homepageOmbiRequestAuth']) && $GLOBALS['homepageOmbiEnabled'] == true && $GLOBALS['ssoOmbi'] && isset($_COOKIE['Auth'])) ? true : false,
-				'authView' => (qualifyRequest($GLOBALS['homepageOmbiAuth'])) ? true : false,
-				'authRequest' => (qualifyRequest($GLOBALS['homepageOmbiRequestAuth'])) ? true : false,
-				'sso' => ($GLOBALS['ssoOmbi']) ? true : false,
-				'cookie' => (isset($_COOKIE['Auth'])) ? true : false,
-				'alias' => ($GLOBALS['ombiAlias']) ? true : false,
-				'ombiDefaultFilterAvailable' => $GLOBALS['ombiDefaultFilterAvailable'] ? true : false,
-				'ombiDefaultFilterUnavailable' => $GLOBALS['ombiDefaultFilterUnavailable'] ? true : false,
-				'ombiDefaultFilterApproved' => $GLOBALS['ombiDefaultFilterApproved'] ? true : false,
-				'ombiDefaultFilterUnapproved' => $GLOBALS['ombiDefaultFilterUnapproved'] ? true : false,
-				'ombiDefaultFilterDenied' => $GLOBALS['ombiDefaultFilterDenied'] ? true : false
-			),
-			'options' => array(
-				'alternateHomepageHeaders' => $GLOBALS['alternateHomepageHeaders'],
-				'healthChecksTags' => $GLOBALS['healthChecksTags'],
-				'titles' => array(
-					'tautulli' => $GLOBALS['tautulliHeader']
-				)
-			),
-			'media' => array(
-				'jellyfin' => (strpos($GLOBALS['embyURL'], 'jellyfin') !== false) ? true : false
-			)
-		),
-		'sso' => array(
-			'misc' => array(
-				'oAuthLogin' => isset($_COOKIE['oAuth']) ? true : false,
-				'rememberMe' => $GLOBALS['rememberMe'],
-				'rememberMeDays' => $GLOBALS['rememberMeDays']
-			),
-			'plex' => array(
-				'enabled' => ($GLOBALS['ssoPlex']) ? true : false,
-				'cookie' => isset($_COOKIE['mpt']) ? true : false,
-				'machineID' => (strlen($GLOBALS['plexID']) == 40) ? true : false,
-				'token' => ($GLOBALS['plexToken'] !== '') ? true : false,
-				'plexAdmin' => checkPlexAdminFilled(),
-				'strict' => ($GLOBALS['plexStrictFriends']) ? true : false,
-				'oAuthEnabled' => ($GLOBALS['plexoAuth']) ? true : false,
-				'backend' => ($GLOBALS['authBackend'] == 'plex') ? true : false,
-			),
-			'ombi' => array(
-				'enabled' => ($GLOBALS['ssoOmbi']) ? true : false,
-				'cookie' => isset($_COOKIE['Auth']) ? true : false,
-				'url' => ($GLOBALS['ombiURL'] !== '') ? $GLOBALS['ombiURL'] : false,
-				'api' => ($GLOBALS['ombiToken'] !== '') ? true : false,
-			),
-			'tautulli' => array(
-				'enabled' => ($GLOBALS['ssoTautulli']) ? true : false,
-				'cookie' => !empty($tautulli) ? true : false,
-				'url' => ($GLOBALS['tautulliURL'] !== '') ? $GLOBALS['tautulliURL'] : false,
-			),
-		),
-		'ping' => array(
-			'onlineSound' => $GLOBALS['pingOnlineSound'],
-			'offlineSound' => $GLOBALS['pingOfflineSound'],
-			'statusSounds' => $GLOBALS['statusSounds'],
-			'auth' => $GLOBALS['pingAuth'],
-			'authMessage' => $GLOBALS['pingAuthMessage'],
-			'authMs' => $GLOBALS['pingAuthMs'],
-			'ms' => $GLOBALS['pingMs'],
-			'adminRefresh' => $GLOBALS['adminPingRefresh'],
-			'everyoneRefresh' => $GLOBALS['otherPingRefresh'],
-		),
-		'notifications' => array(
-			'backbone' => $GLOBALS['notificationBackbone'],
-			'position' => $GLOBALS['notificationPosition']
-		),
-		'lockout' => array(
-			'enabled' => $GLOBALS['lockoutSystem'],
-			'timer' => $GLOBALS['lockoutTimeout'],
-			'minGroup' => $GLOBALS['lockoutMinAuth'],
-			'maxGroup' => $GLOBALS['lockoutMaxAuth']
-		),
-		'user' => array(
-			'agent' => isset($_SERVER ['HTTP_USER_AGENT']) ? $_SERVER ['HTTP_USER_AGENT'] : null,
-			'oAuthLogin' => isset($_COOKIE['oAuth']) ? true : false,
-			'local' => (isLocal()) ? true : false,
-			'ip' => userIP()
-		),
-		'login' => array(
-			'rememberMe' => $GLOBALS['rememberMe'],
-			'rememberMeDays' => $GLOBALS['rememberMeDays'],
-			'wanDomain' => $GLOBALS['wanDomain'],
-			'localAddress' => $GLOBALS['localAddress'],
-			'enableLocalAddressForward' => $GLOBALS['enableLocalAddressForward'],
-		),
-		'misc' => array(
-			'installedPlugins' => qualifyRequest(1) ? $GLOBALS['installedPlugins'] : '',
-			'installedThemes' => qualifyRequest(1) ? $GLOBALS['installedThemes'] : '',
-			'return' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : false,
-			'authDebug' => $GLOBALS['authDebug'],
-			'minimalLoginScreen' => $GLOBALS['minimalLoginScreen'],
-			'unsortedTabs' => $GLOBALS['unsortedTabs'],
-			'authType' => $GLOBALS['authType'],
-			'authBackend' => $GLOBALS['authBackend'],
-			'newMessageSound' => (isset($GLOBALS['CHAT-newMessageSound-include'])) ? $GLOBALS['CHAT-newMessageSound-include'] : '',
-			'uuid' => $GLOBALS['uuid'],
-			'docker' => qualifyRequest(1) ? $GLOBALS['docker'] : '',
-			'githubCommit' => qualifyRequest(1) ? $GLOBALS['commit'] : '',
-			'schema' => qualifyRequest(1) ? getSchema() : '',
-			'debugArea' => qualifyRequest($GLOBALS['debugAreaAuth']),
-			'debugErrors' => $GLOBALS['debugErrors'],
-			'sandbox' => $GLOBALS['sandbox'],
-		),
-		'menuLink' => array(
-			'githubMenuLink' => $GLOBALS['githubMenuLink'],
-			'organizrSupportMenuLink' => $GLOBALS['organizrSupportMenuLink'],
-			'organizrDocsMenuLink' => $GLOBALS['organizrDocsMenuLink'],
-			'organizrSignoutMenuLink' => $GLOBALS['organizrSignoutMenuLink']
-		)
-	);
-}
-
-function wizardConfig($array)
-{
-	foreach ($array['data'] as $items) {
-		foreach ($items as $key => $value) {
-			if ($key == 'name') {
-				$newKey = $value;
-			}
-			if ($key == 'value') {
-				$newValue = $value;
-			}
-			if (isset($newKey) && isset($newValue)) {
-				$$newKey = $newValue;
-			}
-		}
-	}
-	$location = cleanDirectory($location);
-	$dbName = dbExtension($dbName);
-	$configVersion = $GLOBALS['installedVersion'];
-	$configArray = array(
-		'dbName' => $dbName,
-		'dbLocation' => $location,
-		'license' => $license,
-		'organizrHash' => $hashKey,
-		'organizrAPI' => $api,
-		'registrationPassword' => $registrationPassword,
-		'uuid' => gen_uuid()
-	);
-	// Create Config
-	$GLOBALS['dbLocation'] = $location;
-	$GLOBALS['dbName'] = $dbName;
-	if (createConfig($configArray)) {
-		// Call DB Create
-		if (createDB($location, $dbName)) {
-			// Add in first user
-			if (createFirstAdmin($location, $dbName, $username, $password, $email)) {
-				if (createToken($username, $email, gravatar($email), 'Admin', 0, $hashKey, 1)) {
-					return true;
-				} else {
-					return 'token';
-				}
-			} else {
-				return 'admin';
-			}
-		} else {
-			return 'db';
-		}
-	} else {
-		return 'config';
-	}
-	return false;
-}
 
-function register($array)
+trait OrganizrFunctions
 {
-	// Grab username and password from login form
-	foreach ($array['data'] as $items) {
-		foreach ($items as $key => $value) {
-			if ($key == 'name') {
-				$newKey = $value;
-			}
-			if ($key == 'value') {
-				$newValue = $value;
-			}
-			if (isset($newKey) && isset($newValue)) {
-				$$newKey = $newValue;
-			}
-		}
-	}
-	if ($registrationPassword == $GLOBALS['registrationPassword']) {
-		$defaults = defaultUserGroup();
-		writeLog('success', 'Registration Function - Registration Password Verified', $username);
-		if (createUser($username, $password, $defaults, $email)) {
-			writeLog('success', 'Registration Function - A User has registered', $username);
-			if (createToken($username, $email, gravatar($email), $defaults['group'], $defaults['group_id'], $GLOBALS['organizrHash'], $GLOBALS['rememberMeDays'])) {
-				writeLoginLog($username, 'success');
-				writeLog('success', 'Login Function - A User has logged in', $username);
-				return true;
-			}
-		} else {
-			writeLog('error', 'Registration Function - An error occured', $username);
-			return 'username taken';
-		}
-	} else {
-		writeLog('warning', 'Registration Function - Wrong Password', $username);
-		return 'mismatch';
-	}
-}
-
-function removeFile($array)
-{
-	$filePath = $array['data']['path'];
-	$fileName = $array['data']['name'];
-	if (file_exists($filePath)) {
-		if (unlink($filePath)) {
-			writeLog('success', 'Log Management Function - Log: ' . $fileName . ' has been purged/deleted', 'SYSTEM');
-			return true;
-		} else {
-			writeLog('error', 'Log Management Function - Log: ' . $fileName . ' - Error Occured', 'SYSTEM');
+	public function embyJoinAPI($array)
+	{
+		$username = ($array['username']) ?? null;
+		$email = ($array['email']) ?? null;
+		$password = ($array['password']) ?? null;
+		if (!$username) {
+			$this->setAPIResponse('error', 'Username not supplied', 422);
 			return false;
 		}
-	} else {
-		writeLog('error', 'Log Management Function - Log: ' . $fileName . ' does not exist', 'SYSTEM');
-		return false;
-	}
-}
-
-function recover($array)
-{
-	$email = $array['data']['email'];
-	$newPassword = randString(10);
-	try {
-		$connect = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-		]);
-		$isUser = $connect->fetch('SELECT * FROM users WHERE email = ? COLLATE NOCASE', $email);
-		if ($isUser) {
-			$connect->query('
-                UPDATE users SET', [
-				'password' => password_hash($newPassword, PASSWORD_BCRYPT)
-			], '
-                WHERE email=? COLLATE NOCASE', $email);
-			if ($GLOBALS['PHPMAILER-enabled']) {
-				$emailTemplate = array(
-					'type' => 'reset',
-					'body' => $GLOBALS['PHPMAILER-emailTemplateResetPassword'],
-					'subject' => $GLOBALS['PHPMAILER-emailTemplateResetPasswordSubject'],
-					'user' => $isUser['username'],
-					'password' => $newPassword,
-					'inviteCode' => null,
-				);
-				$emailTemplate = phpmEmailTemplate($emailTemplate);
-				$sendEmail = array(
-					'to' => $email,
-					'user' => $isUser['username'],
-					'subject' => $emailTemplate['subject'],
-					'body' => phpmBuildEmail($emailTemplate),
-				);
-				phpmSendEmail($sendEmail);
-			}
-			writeLog('success', 'User Management Function - User: ' . $isUser['username'] . '\'s password was reset', $isUser['username']);
-			return true;
-		} else {
-			writeLog('error', 'User Management Function - Error - User: ' . $email . ' An error Occured', $email);
-			return 'an error occured';
-		}
-	} catch (Dibi\Exception $e) {
-		writeLog('error', 'User Management Function - Error - User: ' . $email . ' An error Occured', $email);
-		return 'an error occured';
-	}
-}
-
-function unlock($array)
-{
-	if ($array['data']['password'] == '') {
-		return 'Password Not Set';
-	}
-	try {
-		$connect = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-		]);
-		$result = $connect->fetch('SELECT * FROM users WHERE id = ?', $GLOBALS['organizrUser']['userID']);
-		if (!password_verify($array['data']['password'], $result['password'])) {
-			return 'Password Incorrect';
-		}
-		$connect->query('
-            UPDATE users SET', [
-			'locked' => ''
-		], '
-            WHERE id=?', $GLOBALS['organizrUser']['userID']);
-		writeLog('success', 'User Lockout Function - User: ' . $GLOBALS['organizrUser']['username'] . '\'s account unlocked', $GLOBALS['organizrUser']['username']);
-		return true;
-	} catch (Dibi\Exception $e) {
-		writeLog('error', 'User Management Function - Error - User: ' . $GLOBALS['organizrUser']['username'] . ' An error Occured', $GLOBALS['organizrUser']['username']);
-		return 'an error occured';
-	}
-}
-
-function lock()
-{
-	if ($GLOBALS['organizrUser']['userID'] == '999') {
-		return 'Not Allowed on Guest';
-	}
-	try {
-		$connect = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-		]);
-		$connect->query('
-            UPDATE users SET', [
-			'locked' => '1'
-		], '
-            WHERE id=?', $GLOBALS['organizrUser']['userID']);
-		writeLog('success', 'User Lockout Function - User: ' . $GLOBALS['organizrUser']['username'] . '\'s account unlocked', $GLOBALS['organizrUser']['username']);
-		return true;
-	} catch (Dibi\Exception $e) {
-		writeLog('error', 'User Management Function - Error - User: ' . $GLOBALS['organizrUser']['username'] . ' An error Occured', $GLOBALS['organizrUser']['username']);
-		return 'an error occured';
-	}
-}
-
-function editUser($array)
-{
-	if ($array['data']['username'] == '' && $array['data']['username'] == '') {
-		return 'Username/email not set';
-	}
-	try {
-		$connect = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-		]);
-		if (!usernameTakenExcept($array['data']['username'], $array['data']['email'], $GLOBALS['organizrUser']['userID'])) {
-			$connect->query('
-                UPDATE users SET', [
-				'username' => $array['data']['username'],
-				'email' => $array['data']['email'],
-				'image' => gravatar($array['data']['email']),
-			], '
-                WHERE id=?', $GLOBALS['organizrUser']['userID']);
-			if (!empty($array['data']['password'])) {
-				$connect->query('
-                    UPDATE users SET', [
-					'password' => password_hash($array['data']['password'], PASSWORD_BCRYPT)
-				], '
-                    WHERE id=?', $GLOBALS['organizrUser']['userID']);
-			}
-			writeLog('success', 'User Management Function - User: ' . $array['data']['username'] . '\'s info was changed', $GLOBALS['organizrUser']['username']);
-			return true;
-		} else {
-			return 'Username/Email Already Taken';
-		}
-	} catch (Dibi\Exception $e) {
-		writeLog('error', 'User Management Function - Error - User: ' . $array['data']['username'] . ' An error Occured', $GLOBALS['organizrUser']['username']);
-		return 'an error occured';
-	}
-}
-
-function clearTautulliTokens()
-{
-	foreach (array_keys($_COOKIE) as $k => $v) {
-		if (strpos($v, 'tautulli') !== false) {
-			coookie('delete', $v);
-		}
-	}
-}
-
-function logout()
-{
-	coookie('delete', $GLOBALS['cookieName']);
-	coookie('delete', 'mpt');
-	coookie('delete', 'Auth');
-	coookie('delete', 'oAuth');
-	clearTautulliTokens();
-	revokeToken(array('data' => array('token' => $GLOBALS['organizrUser']['token'])));
-	$GLOBALS['organizrUser'] = false;
-	return true;
-}
-
-function qualifyRequest($accessLevelNeeded)
-{
-	if (getUserLevel() <= $accessLevelNeeded && getUserLevel() !== null) {
-		return true;
-	} else {
-		return false;
-	}
-}
-
-function isApprovedRequest($method)
-{
-	$requesterToken = isset(getallheaders()['Token']) ? getallheaders()['Token'] : (isset($_GET['apikey']) ? $_GET['apikey'] : false);
-	if (isset($_POST['data']['formKey'])) {
-		$formKey = $_POST['data']['formKey'];
-	} elseif (isset(getallheaders()['Formkey'])) {
-		$formKey = getallheaders()['Formkey'];
-	} elseif (isset(getallheaders()['formkey'])) {
-		$formKey = getallheaders()['formkey'];
-	} elseif (isset(getallheaders()['formKey'])) {
-		$formKey = getallheaders()['formKey'];
-	} elseif (isset(getallheaders()['FormKey'])) {
-		$formKey = getallheaders()['FormKey'];
-	} else {
-		$formKey = false;
-	}
-	// Check token or API key
-	// If API key, return 0 for admin
-	if (strlen($requesterToken) == 20 && $requesterToken == $GLOBALS['organizrAPI']) {
-		//DO API CHECK
-		return true;
-	} elseif ($method == 'POST') {
-		if (checkFormKey($formKey)) {
-			return true;
-		} else {
-			writeLog('error', 'API ERROR: Unable to authenticate Form Key: ' . $formKey, $GLOBALS['organizrUser']['username']);
-		}
-	} else {
-		return true;
-	}
-	return false;
-}
-
-function getUserLevel()
-{
-	// Grab token
-	//$requesterToken = isset(getallheaders()['Token']) ? getallheaders()['Token'] : false;
-	$requesterToken = isset(getallheaders()['Token']) ? getallheaders()['Token'] : (isset($_GET['apikey']) ? $_GET['apikey'] : false);
-	// Check token or API key
-	// If API key, return 0 for admin
-	if (strlen($requesterToken) == 20 && $requesterToken == $GLOBALS['organizrAPI']) {
-		//DO API CHECK
-		return 0;
-	} elseif (isset($GLOBALS['organizrUser'])) {
-		return $GLOBALS['organizrUser']['groupID'];
-	}
-	// All else fails?  return guest id
-	return 999;
-}
-
-function organizrStatus()
-{
-	$status = array();
-	$dependenciesActive = array();
-	$dependenciesInactive = array();
-	$extensions = array("PDO_SQLITE", "PDO", "SQLITE3", "zip", "cURL", "openssl", "simplexml", "json", "session", "filter");
-	$functions = array("hash", "fopen", "fsockopen", "fwrite", "fclose", "readfile");
-	foreach ($extensions as $check) {
-		if (extension_loaded($check)) {
-			array_push($dependenciesActive, $check);
-		} else {
-			array_push($dependenciesInactive, $check);
-		}
-	}
-	foreach ($functions as $check) {
-		if (function_exists($check)) {
-			array_push($dependenciesActive, $check);
-		} else {
-			array_push($dependenciesInactive, $check);
-		}
-	}
-	if (!file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
-		$status['status'] = "wizard";//wizard - ok for test
-	}
-	if (count($dependenciesInactive) > 0 || !is_writable(dirname(__DIR__, 2)) || !(version_compare(PHP_VERSION, $GLOBALS['minimumPHP']) >= 0)) {
-		$status['status'] = "dependencies";
-	}
-	$status['status'] = (!empty($status['status'])) ? $status['status'] : $status['status'] = "ok";
-	$status['writable'] = is_writable(dirname(__DIR__, 2)) ? 'yes' : 'no';
-	$status['minVersion'] = (version_compare(PHP_VERSION, $GLOBALS['minimumPHP']) >= 0) ? 'yes' : 'no';
-	$status['dependenciesActive'] = $dependenciesActive;
-	$status['dependenciesInactive'] = $dependenciesInactive;
-	$status['version'] = $GLOBALS['installedVersion'];
-	$status['os'] = getOS();
-	$status['php'] = phpversion();
-	return $status;
-}
-
-function pathsWritable($paths)
-{
-	$results = array();
-	foreach ($paths as $k => $v) {
-		$results[$k] = is_writable($v);
-	}
-	return $results;
-}
-
-function getSettingsMain()
-{
-	return array(
-		'Github' => array(
-			array(
-				'type' => 'select',
-				'name' => 'branch',
-				'label' => 'Branch',
-				'value' => $GLOBALS['branch'],
-				'options' => getBranches(),
-				'disabled' => $GLOBALS['docker'],
-				'help' => ($GLOBALS['docker']) ? 'Since you are using the Official Docker image, Change the image to change the branch' : 'Choose which branch to download from'
-			),
-			array(
-				'type' => 'button',
-				'name' => 'force-install-branch',
-				'label' => 'Force Install Branch',
-				'class' => 'updateNow',
-				'icon' => 'fa fa-download',
-				'text' => 'Retrieve',
-				'attr' => ($GLOBALS['docker']) ? 'title="You can just restart your docker to update"' : '',
-				'help' => ($GLOBALS['docker']) ? 'Since you are using the official Docker image, you can just restart your Docker container to update Organizr' : 'This will re-download all of the source files for Organizr'
-			)
-		),
-		'API' => array(
-			array(
-				'type' => 'password-alt',
-				'name' => 'organizrAPI',
-				'label' => 'Organizr API',
-				'value' => $GLOBALS['organizrAPI']
-			),
-			array(
-				'type' => 'button',
-				'label' => 'Generate New API Key',
-				'class' => 'newAPIKey',
-				'icon' => 'fa fa-refresh',
-				'text' => 'Generate'
-			)
-		),
-		'Authentication' => array(
-			array(
-				'type' => 'select',
-				'name' => 'authType',
-				'id' => 'authSelect',
-				'label' => 'Authentication Type',
-				'value' => $GLOBALS['authType'],
-				'options' => getAuthTypes()
-			),
-			array(
-				'type' => 'select',
-				'name' => 'authBackend',
-				'id' => 'authBackendSelect',
-				'label' => 'Authentication Backend',
-				'class' => 'backendAuth switchAuth',
-				'value' => $GLOBALS['authBackend'],
-				'options' => getAuthBackends()
-			),
-			array(
-				'type' => 'password-alt',
-				'name' => 'plexToken',
-				'class' => 'plexAuth switchAuth',
-				'label' => 'Plex Token',
-				'value' => $GLOBALS['plexToken'],
-				'placeholder' => 'Use Get Token Button'
-			),
-			array(
-				'type' => 'button',
-				'label' => 'Get Plex Token',
-				'class' => 'popup-with-form getPlexTokenAuth plexAuth switchAuth',
-				'icon' => 'fa fa-ticket',
-				'text' => 'Retrieve',
-				'href' => '#auth-plex-token-form',
-				'attr' => 'data-effect="mfp-3d-unfold"'
-			),
-			array(
-				'type' => 'password-alt',
-				'name' => 'plexID',
-				'class' => 'plexAuth switchAuth',
-				'label' => 'Plex Machine',
-				'value' => $GLOBALS['plexID'],
-				'placeholder' => 'Use Get Plex Machine Button'
-			),
-			array(
-				'type' => 'button',
-				'label' => 'Get Plex Machine',
-				'class' => 'popup-with-form getPlexMachineAuth plexAuth switchAuth',
-				'icon' => 'fa fa-id-badge',
-				'text' => 'Retrieve',
-				'href' => '#auth-plex-machine-form',
-				'attr' => 'data-effect="mfp-3d-unfold"'
-			),
-			array(
-				'type' => 'input',
-				'name' => 'plexAdmin',
-				'label' => 'Admin Username',
-				'class' => 'plexAuth switchAuth',
-				'value' => $GLOBALS['plexAdmin'],
-				'placeholder' => 'Admin username for Plex'
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'plexoAuth',
-				'label' => 'Enable Plex oAuth',
-				'class' => 'plexAuth switchAuth',
-				'value' => $GLOBALS['plexoAuth']
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'plexStrictFriends',
-				'label' => 'Strict Plex Friends ',
-				'class' => 'plexAuth switchAuth',
-				'value' => $GLOBALS['plexStrictFriends'],
-				'help' => 'Enabling this will only allow Friends that have shares to the Machine ID entered above to login, Having this disabled will allow all Friends on your Friends list to login'
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'ignoreTFALocal',
-				'label' => 'Ignore External 2FA on Local Subnet',
-				'value' => $GLOBALS['ignoreTFALocal'],
-				'help' => 'Enabling this will bypass external 2FA security if user is on local Subnet'
-			),
-			array(
-				'type' => 'input',
-				'name' => 'authBackendHost',
-				'class' => 'ldapAuth ftpAuth switchAuth',
-				'label' => 'Host Address',
-				'value' => $GLOBALS['authBackendHost'],
-				'placeholder' => 'http{s) | ftp(s) | ldap(s)://hostname:port'
-			),
-			array(
-				'type' => 'input',
-				'name' => 'authBaseDN',
-				'class' => 'ldapAuth switchAuth',
-				'label' => 'Host Base DN',
-				'value' => $GLOBALS['authBaseDN'],
-				'placeholder' => 'cn=%s,dc=sub,dc=domain,dc=com'
-			),
-			array(
-				'type' => 'select',
-				'name' => 'ldapType',
-				'id' => 'ldapType',
-				'label' => 'LDAP Backend Type',
-				'class' => 'ldapAuth switchAuth',
-				'value' => $GLOBALS['ldapType'],
-				'options' => getLDAPOptions()
-			),
-			array(
-				'type' => 'input',
-				'name' => 'authBackendHostPrefix',
-				'class' => 'ldapAuth switchAuth',
-				'label' => 'Account Prefix',
-				'id' => 'authBackendHostPrefix-input',
-				'value' => $GLOBALS['authBackendHostPrefix'],
-				'placeholder' => 'Account prefix - i.e. Controller\ from Controller\Username for AD - uid= for OpenLDAP'
-			),
-			array(
-				'type' => 'input',
-				'name' => 'authBackendHostSuffix',
-				'class' => 'ldapAuth switchAuth',
-				'label' => 'Account Suffix',
-				'id' => 'authBackendHostSuffix-input',
-				'value' => $GLOBALS['authBackendHostSuffix'],
-				'placeholder' => 'Account suffix - start with comma - ,ou=people,dc=domain,dc=tld'
-			),
-			array(
-				'type' => 'input',
-				'name' => 'ldapBindUsername',
-				'class' => 'ldapAuth switchAuth',
-				'label' => 'Bind Username',
-				'value' => $GLOBALS['ldapBindUsername'],
-				'placeholder' => ''
-			),
-			array(
-				'type' => 'password',
-				'name' => 'ldapBindPassword',
-				'class' => 'ldapAuth switchAuth',
-				'label' => 'Password',
-				'value' => $GLOBALS['ldapBindPassword']
-			),
-			array(
-				'type' => 'html',
-				'class' => 'ldapAuth switchAuth',
-				'label' => 'Account DN',
-				'html' => '<span id="accountDN" class="ldapAuth switchAuth">' . $GLOBALS['authBackendHostPrefix'] . 'TestAcct' . $GLOBALS['authBackendHostSuffix'] . '</span>'
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'ldapSSL',
-				'class' => 'ldapAuth switchAuth',
-				'label' => 'Enable LDAP SSL',
-				'value' => $GLOBALS['ldapSSL'],
-				'help' => 'This will enable the use of SSL for LDAP connections'
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'ldapSSL',
-				'class' => 'ldapAuth switchAuth',
-				'label' => 'Enable LDAP TLS',
-				'value' => $GLOBALS['ldapTLS'],
-				'help' => 'This will enable the use of TLS for LDAP connections'
-			),
-			array(
-				'type' => 'button',
-				'name' => 'test-button-ldap',
-				'label' => 'Test Connection',
-				'icon' => 'fa fa-flask',
-				'class' => 'ldapAuth switchAuth',
-				'text' => 'Test Connection',
-				'attr' => 'onclick="testAPIConnection(\'ldap\')"',
-				'help' => 'Remember! Please save before using the test button!'
-			),
-			array(
-				'type' => 'button',
-				'name' => 'test-button-ldap-login',
-				'label' => 'Test Login',
-				'icon' => 'fa fa-flask',
-				'class' => 'ldapAuth switchAuth',
-				'text' => 'Test Login',
-				'attr' => 'onclick="showLDAPLoginTest()"'
-			),
-			array(
-				'type' => 'input',
-				'name' => 'embyURL',
-				'class' => 'embyAuth switchAuth',
-				'label' => 'Emby/Jellyfin URL',
-				'value' => $GLOBALS['embyURL'],
-				'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-				'placeholder' => 'http(s)://hostname:port'
-			),
-			array(
-				'type' => 'password-alt',
-				'name' => 'embyToken',
-				'class' => 'embyAuth switchAuth',
-				'label' => 'Emby/Jellyin Token',
-				'value' => $GLOBALS['embyToken'],
-				'placeholder' => ''
-			),
-			/*array(
-				'type' => 'button',
-				'label' => 'Send Test',
-				'class' => 'phpmSendTestEmail',
-				'icon' => 'fa fa-paper-plane',
-				'text' => 'Send'
-			)*/
-		),
-		'Security' => array(
-			array(
-				'type' => 'number',
-				'name' => 'loginAttempts',
-				'label' => 'Max Login Attempts',
-				'value' => $GLOBALS['loginAttempts'],
-				'placeholder' => ''
-			),
-			array(
-				'type' => 'select',
-				'name' => 'loginLockout',
-				'label' => 'Login Lockout Seconds',
-				'value' => $GLOBALS['loginLockout'],
-				'options' => optionTime()
-			),
-			array(
-				'type' => 'number',
-				'name' => 'lockoutTimeout',
-				'label' => 'Inactivity Timer [Minutes]',
-				'value' => $GLOBALS['lockoutTimeout'],
-				'placeholder' => ''
-			),
-			array(
-				'type' => 'select',
-				'name' => 'lockoutMinAuth',
-				'label' => 'Lockout Groups From',
-				'value' => $GLOBALS['lockoutMinAuth'],
-				'options' => groupSelect()
-			),
-			array(
-				'type' => 'select',
-				'name' => 'lockoutMaxAuth',
-				'label' => 'Lockout Groups To',
-				'value' => $GLOBALS['lockoutMaxAuth'],
-				'options' => groupSelect()
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'lockoutSystem',
-				'label' => 'Inactivity Lock',
-				'value' => $GLOBALS['lockoutSystem']
-			),
-			array(
-				'type' => 'select',
-				'name' => 'debugAreaAuth',
-				'label' => 'Minimum Authentication for Debug Area',
-				'value' => $GLOBALS['debugAreaAuth'],
-				'options' => groupSelect()
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'authDebug',
-				'label' => 'Nginx Auth Debug',
-				'help' => 'Important! Do not keep this enabled for too long as this opens up Authentication while testing.',
-				'value' => $GLOBALS['authDebug'],
-				'class' => 'authDebug'
-			),
-			array(
-				'type' => 'select2',
-				'class' => 'select2-multiple',
-				'id' => 'sandbox-select',
-				'name' => 'sandbox',
-				'label' => 'iFrame Sandbox',
-				'value' => $GLOBALS['sandbox'],
-				'help' => 'WARNING! This can potentially mess up your iFrames',
-				'options' => array(
-					array(
-						'name' => 'Allow Presentation',
-						'value' => 'allow-presentation'
-					),
-					array(
-						'name' => 'Allow Forms',
-						'value' => 'allow-forms'
-					),
-					array(
-						'name' => 'Allow Same Origin',
-						'value' => 'allow-same-origin'
-					),
-					array(
-						'name' => 'Allow Pointer Lock',
-						'value' => 'allow-pointer-lock'
-					),
-					array(
-						'name' => 'Allow Scripts',
-						'value' => 'allow-scripts'
-					), array(
-						'name' => 'Allow Popups',
-						'value' => 'allow-popups'
-					),
-					array(
-						'name' => 'Allow Modals',
-						'value' => 'allow-modals'
-					),
-					array(
-						'name' => 'Allow Top Navigation',
-						'value' => 'allow-top-navigation'
-					),
-					array(
-						'name' => 'Allow Downloads',
-						'value' => 'allow-downloads'
-					),
-				)
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'traefikAuthEnable',
-				'label' => 'Enable Traefik Auth Redirect',
-				'help' => 'This will enable the webserver to forward errors so traefik will accept them',
-				'value' => $GLOBALS['traefikAuthEnable']
-			),
-		),
-		'Performance' => array(
-			array(
-				'type' => 'switch',
-				'name' => 'performanceDisableIconDropdown',
-				'label' => 'Disable Icon Dropdown',
-				'help' => 'Disable select dropdown boxes on new and edit tab forms',
-				'value' => $GLOBALS['performanceDisableIconDropdown'],
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'performanceDisableImageDropdown',
-				'label' => 'Disable Image Dropdown',
-				'help' => 'Disable select dropdown boxes on new and edit tab forms',
-				'value' => $GLOBALS['performanceDisableImageDropdown'],
-			),
-		),
-		'Login' => array(
-			array(
-				'type' => 'password-alt',
-				'name' => 'registrationPassword',
-				'label' => 'Registration Password',
-				'help' => 'Sets the password for the Registration form on the login screen',
-				'value' => $GLOBALS['registrationPassword'],
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'hideRegistration',
-				'label' => 'Hide Registration',
-				'help' => 'Enable this to hide the Registration button on the login screen',
-				'value' => $GLOBALS['hideRegistration'],
-			),
-			array(
-				'type' => 'number',
-				'name' => 'rememberMeDays',
-				'label' => 'Remember Me Length',
-				'help' => 'Number of days cookies and tokens will be valid for',
-				'value' => $GLOBALS['rememberMeDays'],
-				'placeholder' => '',
-				'attr' => 'min="1"'
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'rememberMe',
-				'label' => 'Remember Me',
-				'help' => 'Default status of Remember Me button on login screen',
-				'value' => $GLOBALS['rememberMe'],
-			),
-			array(
-				'type' => 'input',
-				'name' => 'localIPFrom',
-				'label' => 'Override Local IP From',
-				'value' => $GLOBALS['localIPFrom'],
-				'placeholder' => 'i.e. 123.123.123.123',
-				'help' => 'IPv4 only at the moment - This will set your login as local if your IP falls within the From and To'
-			),
-			array(
-				'type' => 'input',
-				'name' => 'localIPTo',
-				'label' => 'Override Local IP To',
-				'value' => $GLOBALS['localIPTo'],
-				'placeholder' => 'i.e. 123.123.123.123',
-				'help' => 'IPv4 only at the moment - This will set your login as local if your IP falls within the From and To'
-			),
-			array(
-				'type' => 'input',
-				'name' => 'wanDomain',
-				'label' => 'WAN Domain',
-				'value' => $GLOBALS['wanDomain'],
-				'placeholder' => 'only domain and tld - i.e. domain.com',
-				'help' => 'Enter domain if you wish to be forwarded to a local address - Local Address filled out on next item'
-			),
-			array(
-				'type' => 'input',
-				'name' => 'localAddress',
-				'label' => 'Local Address',
-				'value' => $GLOBALS['localAddress'],
-				'placeholder' => 'http://home.local',
-				'help' => 'Full local address of organizr install - i.e. http://home.local or http://192.168.0.100'
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'enableLocalAddressForward',
-				'label' => 'Enable Local Address Forward',
-				'help' => 'Enables the local address forward if on local address and accessed from WAN Domain',
-				'value' => $GLOBALS['enableLocalAddressForward'],
-			),
-		),
-		'Auth Proxy' => array(
-			array(
-				'type' => 'switch',
-				'name' => 'authProxyEnabled',
-				'label' => 'Auth Proxy',
-				'help' => 'Enable option to set Auth Proxy Header Login',
-				'value' => $GLOBALS['authProxyEnabled'],
-			),
-			array(
-				'type' => 'input',
-				'name' => 'authProxyHeaderName',
-				'label' => 'Auth Proxy Header Name',
-				'value' => $GLOBALS['authProxyHeaderName'],
-				'placeholder' => 'i.e. X-Forwarded-User',
-				'help' => 'Please choose a unique value for added security'
-			),
-			array(
-				'type' => 'input',
-				'name' => 'authProxyWhitelist',
-				'label' => 'Auth Proxy Whitelist',
-				'value' => $GLOBALS['authProxyWhitelist'],
-				'placeholder' => 'i.e. 10.0.0.0/24 or 10.0.0.20',
-				'help' => 'IPv4 only at the moment - This must be set to work, will accept subnet or IP address'
-			),
-		),
-		'Ping' => array(
-			array(
-				'type' => 'select',
-				'name' => 'pingAuth',
-				'label' => 'Minimum Authentication',
-				'value' => $GLOBALS['pingAuth'],
-				'options' => groupSelect()
-			),
-			array(
-				'type' => 'select',
-				'name' => 'pingAuthMessage',
-				'label' => 'Minimum Authentication for Message and Sound',
-				'value' => $GLOBALS['pingAuthMessage'],
-				'options' => groupSelect()
-			),
-			array(
-				'type' => 'select',
-				'name' => 'pingOnlineSound',
-				'label' => 'Online Sound',
-				'value' => $GLOBALS['pingOnlineSound'],
-				'options' => getSounds()
-			),
-			array(
-				'type' => 'select',
-				'name' => 'pingOfflineSound',
-				'label' => 'Offline Sound',
-				'value' => $GLOBALS['pingOfflineSound'],
-				'options' => getSounds()
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'pingMs',
-				'label' => 'Show Ping Time',
-				'value' => $GLOBALS['pingMs']
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'statusSounds',
-				'label' => 'Enable Notify Sounds',
-				'value' => $GLOBALS['statusSounds'],
-				'help' => 'Will play a sound if the server goes down and will play sound if comes back up.',
-			),
-			array(
-				'type' => 'select',
-				'name' => 'pingAuthMs',
-				'label' => 'Minimum Authentication for Time Display',
-				'value' => $GLOBALS['pingAuthMs'],
-				'options' => groupSelect()
-			),
-			array(
-				'type' => 'select',
-				'name' => 'adminPingRefresh',
-				'label' => 'Admin Refresh Seconds',
-				'value' => $GLOBALS['adminPingRefresh'],
-				'options' => optionTime()
-			),
-			array(
-				'type' => 'select',
-				'name' => 'otherPingRefresh',
-				'label' => 'Everyone Refresh Seconds',
-				'value' => $GLOBALS['otherPingRefresh'],
-				'options' => optionTime()
-			),
-		)
-	);
-}
-
-function getSSO()
-{
-	return array(
-		'FYI' => array(
-			array(
-				'type' => 'html',
-				'label' => 'Important Information',
-				'override' => 12,
-				'html' => '
-				<div class="row">
-						    <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">
-						                    <span lang="en">This is not the same as database authentication - i.e. Plex Authentication | Emby Authentication | FTP Authentication<br/>Click Main on the sub-menu above.</span>
-						                </div>
-						            </div>
-						        </div>
-						    </div>
-						</div>
-				'
-			)
-		),
-		'Plex' => array(
-			array(
-				'type' => 'password-alt',
-				'name' => 'plexToken',
-				'label' => 'Plex Token',
-				'value' => $GLOBALS['plexToken'],
-				'placeholder' => 'Use Get Token Button'
-			),
-			array(
-				'type' => 'button',
-				'label' => 'Get Plex Token',
-				'class' => 'popup-with-form getPlexTokenSSO',
-				'icon' => 'fa fa-ticket',
-				'text' => 'Retrieve',
-				'href' => '#sso-plex-token-form',
-				'attr' => 'data-effect="mfp-3d-unfold"'
-			),
-			array(
-				'type' => 'password-alt',
-				'name' => 'plexID',
-				'label' => 'Plex Machine',
-				'value' => $GLOBALS['plexID'],
-				'placeholder' => 'Use Get Plex Machine Button'
-			),
-			array(
-				'type' => 'button',
-				'label' => 'Get Plex Machine',
-				'class' => 'popup-with-form getPlexMachineSSO',
-				'icon' => 'fa fa-id-badge',
-				'text' => 'Retrieve',
-				'href' => '#sso-plex-machine-form',
-				'attr' => 'data-effect="mfp-3d-unfold"'
-			),
-			array(
-				'type' => 'input',
-				'name' => 'plexAdmin',
-				'label' => 'Admin Username',
-				'value' => $GLOBALS['plexAdmin'],
-				'placeholder' => 'Admin username for Plex'
-			),
-			array(
-				'type' => 'blank',
-				'label' => ''
-			),
-			array(
-				'type' => 'html',
-				'label' => 'Plex Note',
-				'html' => '<span lang="en">Please make sure both Token and Machine are filled in</span>'
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'ssoPlex',
-				'label' => 'Enable',
-				'value' => $GLOBALS['ssoPlex']
-			)
-		),
-		'Tautulli' => array(
-			array(
-				'type' => 'input',
-				'name' => 'tautulliURL',
-				'label' => 'Tautulli URL',
-				'value' => $GLOBALS['tautulliURL'],
-				'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-				'placeholder' => 'http(s)://hostname:port'
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'ssoTautulli',
-				'label' => 'Enable',
-				'value' => $GLOBALS['ssoTautulli']
-			)
-		),
-		'Ombi' => array(
-			array(
-				'type' => 'input',
-				'name' => 'ombiURL',
-				'label' => 'Ombi URL',
-				'value' => $GLOBALS['ombiURL'],
-				'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
-				'placeholder' => 'http(s)://hostname:port'
-			),
-			array(
-				'type' => 'password-alt',
-				'name' => 'ombiToken',
-				'label' => 'Token',
-				'value' => $GLOBALS['ombiToken']
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'ssoOmbi',
-				'label' => 'Enable',
-				'value' => $GLOBALS['ssoOmbi']
-			)
-		)
-	);
-}
-
-function loadAppearance()
-{
-	$appearance = array();
-	$appearance['logo'] = $GLOBALS['logo'];
-	$appearance['title'] = $GLOBALS['title'];
-	$appearance['useLogo'] = $GLOBALS['useLogo'];
-	$appearance['useLogoLogin'] = $GLOBALS['useLogoLogin'];
-	$appearance['headerColor'] = $GLOBALS['headerColor'];
-	$appearance['headerTextColor'] = $GLOBALS['headerTextColor'];
-	$appearance['sidebarColor'] = $GLOBALS['sidebarColor'];
-	$appearance['headerTextColor'] = $GLOBALS['headerTextColor'];
-	$appearance['sidebarTextColor'] = $GLOBALS['sidebarTextColor'];
-	$appearance['accentColor'] = $GLOBALS['accentColor'];
-	$appearance['accentTextColor'] = $GLOBALS['accentTextColor'];
-	$appearance['buttonColor'] = $GLOBALS['buttonColor'];
-	$appearance['buttonTextColor'] = $GLOBALS['buttonTextColor'];
-	$appearance['buttonTextHoverColor'] = $GLOBALS['buttonTextHoverColor'];
-	$appearance['buttonHoverColor'] = $GLOBALS['buttonHoverColor'];
-	$appearance['loginWallpaper'] = $GLOBALS['loginWallpaper'];
-	$appearance['loginLogo'] = $GLOBALS['loginLogo'];
-	$appearance['customCss'] = $GLOBALS['customCss'];
-	$appearance['customThemeCss'] = $GLOBALS['customThemeCss'];
-	$appearance['customJava'] = $GLOBALS['customJava'];
-	$appearance['customThemeJava'] = $GLOBALS['customThemeJava'];
-	return $appearance;
-}
-
-function getCustomizeAppearance()
-{
-	if (file_exists(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.php')) {
-		return array(
-			'Top Bar' => array(
-				array(
-					'type' => 'input',
-					'name' => 'logo',
-					'label' => 'Logo',
-					'value' => $GLOBALS['logo']
-				),
-				array(
-					'type' => 'input',
-					'name' => 'title',
-					'label' => 'Title',
-					'value' => $GLOBALS['title']
-				),
-				array(
-					'type' => 'switch',
-					'name' => 'useLogo',
-					'label' => 'Use Logo instead of Title',
-					'value' => $GLOBALS['useLogo'],
-					'help' => 'Also sets the title of your site'
-				),
-				array(
-					'type' => 'input',
-					'name' => 'description',
-					'label' => 'Meta Description',
-					'value' => $GLOBALS['description'],
-					'help' => 'Used to set the description for SEO meta tags'
-				),
-			),
-			'Login Page' => array(
-				array(
-					'type' => 'input',
-					'name' => 'loginLogo',
-					'label' => 'Login Logo',
-					'value' => $GLOBALS['loginLogo'],
-				),
-				array(
-					'type' => 'input',
-					'name' => 'loginWallpaper',
-					'label' => 'Login Wallpaper',
-					'value' => $GLOBALS['loginWallpaper'],
-					'help' => 'You may enter multiple URL\'s using the CSV format.  i.e. link#1,link#2,link#3'
-				),
-				array(
-					'type' => 'switch',
-					'name' => 'useLogoLogin',
-					'label' => 'Use Logo instead of Title on Login Page',
-					'value' => $GLOBALS['useLogoLogin']
-				),
-				array(
-					'type' => 'switch',
-					'name' => 'minimalLoginScreen',
-					'label' => 'Minimal Login Screen',
-					'value' => $GLOBALS['minimalLoginScreen']
-				)
-			),
-			'Options' => array(
-				array(
-					'type' => 'switch',
-					'name' => 'alternateHomepageHeaders',
-					'label' => 'Alternate Homepage Titles',
-					'value' => $GLOBALS['alternateHomepageHeaders']
-				),
-				array(
-					'type' => 'switch',
-					'name' => 'debugErrors',
-					'label' => 'Show Debug Errors',
-					'value' => $GLOBALS['debugErrors']
-				),
-				array(
-					'type' => 'switch',
-					'name' => 'githubMenuLink',
-					'label' => 'Show GitHub Repo Link',
-					'value' => $GLOBALS['githubMenuLink']
-				),
-				array(
-					'type' => 'switch',
-					'name' => 'organizrSupportMenuLink',
-					'label' => 'Show Organizr Support Link',
-					'value' => $GLOBALS['organizrSupportMenuLink']
-				),
-				array(
-					'type' => 'switch',
-					'name' => 'organizrDocsMenuLink',
-					'label' => 'Show Organizr Docs Link',
-					'value' => $GLOBALS['organizrDocsMenuLink']
-				),
-				array(
-					'type' => 'switch',
-					'name' => 'organizrSignoutMenuLink',
-					'label' => 'Show Organizr Sign out & in Button on Sidebar',
-					'value' => $GLOBALS['organizrSignoutMenuLink']
-				),
-				array(
-					'type' => 'select',
-					'name' => 'unsortedTabs',
-					'label' => 'Unsorted Tab Placement',
-					'value' => $GLOBALS['unsortedTabs'],
-					'options' => array(
-						array(
-							'name' => 'Top',
-							'value' => 'top'
-						),
-						array(
-							'name' => 'Bottom',
-							'value' => 'bottom'
-						)
-					)
-				),
-				array(
-					'type' => 'input',
-					'name' => 'gaTrackingID',
-					'label' => 'Google Analytics Tracking ID',
-					'placeholder' => 'e.g. UA-XXXXXXXXX-X',
-					'value' => $GLOBALS['gaTrackingID']
-				)
-			),
-			'Colors & Themes' => array(
-				array(
-					'type' => 'html',
-					'override' => 12,
-					'label' => 'Custom CSS [Can replace colors from above]',
-					'html' => '
-					<div class="row">
-					    <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">
-					                    <span lang="en">The value of #987654 is just a placeholder, you can change to any value you like.</span>
-					                    <span lang="en">To revert back to default, save with no value defined in the relevant field.</span>
-					                </div>
-					            </div>
-					        </div>
-					    </div>
-					</div>
-					',
-				),
-				array(
-					'type' => 'blank',
-					'label' => ''
-				),
-				array(
-					'type' => 'input',
-					'name' => 'headerColor',
-					'label' => 'Nav Bar Color',
-					'value' => $GLOBALS['headerColor'],
-					'class' => 'pick-a-color',
-					'attr' => 'data-original="' . $GLOBALS['headerColor'] . '"'
-				),
-				array(
-					'type' => 'input',
-					'name' => 'headerTextColor',
-					'label' => 'Nav Bar Text Color',
-					'value' => $GLOBALS['headerTextColor'],
-					'class' => 'pick-a-color',
-					'attr' => 'data-original="' . $GLOBALS['headerTextColor'] . '"'
-				),
-				array(
-					'type' => 'input',
-					'name' => 'sidebarColor',
-					'label' => 'Side Bar Color',
-					'value' => $GLOBALS['sidebarColor'],
-					'class' => 'pick-a-color',
-					'attr' => 'data-original="' . $GLOBALS['sidebarColor'] . '"'
-				),
-				array(
-					'type' => 'input',
-					'name' => 'sidebarTextColor',
-					'label' => 'Side Bar Text Color',
-					'value' => $GLOBALS['sidebarTextColor'],
-					'class' => 'pick-a-color',
-					'attr' => 'data-original="' . $GLOBALS['sidebarTextColor'] . '"'
-				),
-				array(
-					'type' => 'input',
-					'name' => 'accentColor',
-					'label' => 'Accent Color',
-					'value' => $GLOBALS['accentColor'],
-					'class' => 'pick-a-color',
-					'attr' => 'data-original="' . $GLOBALS['accentColor'] . '"'
-				),
-				array(
-					'type' => 'input',
-					'name' => 'accentTextColor',
-					'label' => 'Accent Text Color',
-					'value' => $GLOBALS['accentTextColor'],
-					'class' => 'pick-a-color',
-					'attr' => 'data-original="' . $GLOBALS['accentTextColor'] . '"'
-				),
-				array(
-					'type' => 'input',
-					'name' => 'buttonColor',
-					'label' => 'Button Color',
-					'value' => $GLOBALS['buttonColor'],
-					'class' => 'pick-a-color',
-					'attr' => 'data-original="' . $GLOBALS['buttonColor'] . '"'
-				),
-				array(
-					'type' => 'input',
-					'name' => 'buttonTextColor',
-					'label' => 'Button Text Color',
-					'value' => $GLOBALS['buttonTextColor'],
-					'class' => 'pick-a-color',
-					'attr' => 'data-original="' . $GLOBALS['buttonTextColor'] . '"'
-				),/*
-                array(
-                    'type' => 'input',
-                    'name' => 'buttonHoverColor',
-                    'label' => 'Button Hover Color',
-                    'value' => $GLOBALS['buttonHoverColor'],
-                    'class' => 'pick-a-color',
-                    'disabled' => true
-                ),
-                array(
-                    'type' => 'input',
-                    'name' => 'buttonTextHoverColor',
-                    'label' => 'Button Hover Text Color',
-                    'value' => $GLOBALS['buttonTextHoverColor'],
-                    'class' => 'pick-a-color',
-                    'disabled' => true
-                ),*/
-				array(
-					'type' => 'select',
-					'name' => 'theme',
-					'label' => 'Theme',
-					'class' => 'themeChanger',
-					'value' => $GLOBALS['theme'],
-					'options' => getThemes()
-				),
-				array(
-					'type' => 'select',
-					'name' => 'style',
-					'label' => 'Style',
-					'class' => 'styleChanger',
-					'value' => $GLOBALS['style'],
-					'options' => array(
-						array(
-							'name' => 'Light',
-							'value' => 'light'
-						),
-						array(
-							'name' => 'Dark',
-							'value' => 'dark'
-						),
-						array(
-							'name' => 'Horizontal',
-							'value' => 'horizontal'
-						)
-					)
-				)
-			),
-			'Notifications' => array(
-				array(
-					'type' => 'select',
-					'name' => 'notificationBackbone',
-					'class' => 'notifyChanger',
-					'label' => 'Type',
-					'value' => $GLOBALS['notificationBackbone'],
-					'options' => optionNotificationTypes()
-				),
-				array(
-					'type' => 'select',
-					'name' => 'notificationPosition',
-					'class' => 'notifyPositionChanger',
-					'label' => 'Position',
-					'value' => $GLOBALS['notificationPosition'],
-					'options' => optionNotificationPositions()
-				),
-				array(
-					'type' => 'html',
-					'label' => 'Test Message',
-					'html' => '
-					<div class="btn-group m-r-10 dropup">
-						<button aria-expanded="false" data-toggle="dropdown" class="btn btn-info btn-outline dropdown-toggle waves-effect waves-light" type="button">
-							<i class="fa fa-comment m-r-5"></i>
-							<span>Test </span>
-						</button>
-						<ul role="menu" class="dropdown-menu">
-							<li><a onclick="message(\'Test Message\',\'This is a success Message\',activeInfo.settings.notifications.position,\'#FFF\',\'success\',\'5000\');">Success</a></li>
-							<li><a onclick="message(\'Test Message\',\'This is a info Message\',activeInfo.settings.notifications.position,\'#FFF\',\'info\',\'5000\');">Info</a></li>
-							<li><a onclick="message(\'Test Message\',\'This is a warning Message\',activeInfo.settings.notifications.position,\'#FFF\',\'warning\',\'5000\');">Warning</a></li>
-							<li><a onclick="message(\'Test Message\',\'This is a error Message\',activeInfo.settings.notifications.position,\'#FFF\',\'error\',\'5000\');">Error</a></li>
-						</ul>
-					</div>
-					'
-				)
-			),
-			'FavIcon' => array(
-				array(
-					'type' => 'textbox',
-					'name' => 'favIcon',
-					'class' => '',
-					'label' => 'Fav Icon Code',
-					'value' => $GLOBALS['favIcon'],
-					'placeholder' => 'Paste Contents from https://realfavicongenerator.net/',
-					'attr' => 'rows="10"',
-				),
-				array(
-					'type' => 'html',
-					'label' => 'Instructions',
-					'html' => '
-					<div class="panel panel-default">
-						<div class="panel-heading">
-							<a href="https://realfavicongenerator.net/" target="_blank"><span class="label label-info m-l-5">Visit FavIcon Site</span></a>
-						</div>
-						<div class="panel-wrapper collapse in">
-							<div class="panel-body">
-								<ul class="list-icons">
-									<li lang="en"><i class="fa fa-caret-right text-info"></i> Click [Select your Favicon picture]</li>
-									<li lang="en"><i class="fa fa-caret-right text-info"></i> Choose your image to use</li>
-									<li lang="en"><i class="fa fa-caret-right text-info"></i> Edit settings to your liking</li>
-									<li lang="en"><i class="fa fa-caret-right text-info"></i> At bottom of page on [Favicon Generator Options] under [Path] choose [I cannot or I do not want to place favicon files at the root of my web site.]</li>
-									<li lang="en"><i class="fa fa-caret-right text-info"></i> Enter this path <code>plugins/images/faviconCustom</code></li>
-									<li lang="en"><i class="fa fa-caret-right text-info"></i> Click [Generate your Favicons and HTML code]</li>
-									<li lang="en"><i class="fa fa-caret-right text-info"></i> Download and unzip file and place in <code>plugins/images/faviconCustom</code></li>
-									<li lang="en"><i class="fa fa-caret-right text-info"></i> Copy code and paste inside left box</li>
-								</ul>
-							</div>
-						</div>
-					</div>
-					'
-				),
-			),
-			'Custom CSS' => array(
-				array(
-					'type' => 'html',
-					'override' => 12,
-					'label' => 'Custom CSS [Can replace colors from above]',
-					'html' => '<button type="button" class="hidden saveCss btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customCSSEditor" style="height:300px">' . htmlentities($GLOBALS['customCss']) . '</div>'
-				),
-				array(
-					'type' => 'textbox',
-					'name' => 'customCss',
-					'class' => 'hidden cssTextarea',
-					'label' => '',
-					'value' => $GLOBALS['customCss'],
-					'placeholder' => 'No <style> tags needed',
-					'attr' => 'rows="10"',
-				),
-			),
-			'Theme CSS' => array(
-				array(
-					'type' => 'html',
-					'override' => 12,
-					'label' => 'Theme CSS [Can replace colors from above]',
-					'html' => '<button type="button" class="hidden saveCssTheme btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customThemeCSSEditor" style="height:300px">' . htmlentities($GLOBALS['customThemeCss']) . '</div>'
-				),
-				array(
-					'type' => 'textbox',
-					'name' => 'customThemeCss',
-					'class' => 'hidden cssThemeTextarea',
-					'label' => '',
-					'value' => $GLOBALS['customThemeCss'],
-					'placeholder' => 'No <style> tags needed',
-					'attr' => 'rows="10"',
-				),
-			),
-			'Custom Javascript' => array(
-				array(
-					'type' => 'html',
-					'override' => 12,
-					'label' => 'Custom Javascript',
-					'html' => '<button type="button" class="hidden saveJava btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customJavaEditor" style="height:300px">' . htmlentities($GLOBALS['customJava']) . '</div>'
-				),
-				array(
-					'type' => 'textbox',
-					'name' => 'customJava',
-					'class' => 'hidden javaTextarea',
-					'label' => '',
-					'value' => $GLOBALS['customJava'],
-					'placeholder' => 'No <script> tags needed',
-					'attr' => 'rows="10"',
-				),
-			),
-			'Theme Javascript' => array(
-				array(
-					'type' => 'html',
-					'override' => 12,
-					'label' => 'Theme Javascript',
-					'html' => '<button type="button" class="hidden saveJavaTheme btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customThemeJavaEditor" style="height:300px">' . htmlentities($GLOBALS['customThemeJava']) . '</div>'
-				),
-				array(
-					'type' => 'textbox',
-					'name' => 'customThemeJava',
-					'class' => 'hidden javaThemeTextarea',
-					'label' => '',
-					'value' => $GLOBALS['customThemeJava'],
-					'placeholder' => 'No <script> tags needed',
-					'attr' => 'rows="10"',
-				),
-			),
-		);
-	}
-}
-
-function editAppearance($array)
-{
-	switch ($array['data']['value']) {
-		case 'true':
-			$array['data']['value'] = (bool)true;
-			break;
-		case 'false':
-			$array['data']['value'] = (bool)false;
-			break;
-		default:
-			$array['data']['value'] = $array['data']['value'];
-	}
-	//return gettype($array['data']['value']).' - '.$array['data']['value'];
-	switch ($array['data']['action']) {
-		case 'editCustomizeAppearance':
-			$newItem = array(
-				$array['data']['name'] => $array['data']['value']
-			);
-			return (updateConfig($newItem)) ? true : false;
-			break;
-		default:
-			# code...
-			break;
-	}
-}
-
-function updateConfigMultiple($array)
-{
-	return (updateConfig($array['data']['payload'])) ? true : false;
-}
-
-function updateConfigMultipleForm($array)
-{
-	$newItem = array();
-	foreach ($array['data']['payload'] as $k => $v) {
-		$v['value'] = $v['value'] ?? '';
-		switch ($v['value']) {
-			case 'true':
-				$v['value'] = (bool)true;
-				break;
-			case 'false':
-				$v['value'] = (bool)false;
-				break;
-			default:
-				$v['value'] = $v['value'];
-		}
-		// Hash
-		if ($v['type'] == 'password') {
-			if (isEncrypted($v['value']) || $v['value'] == '') {
-				$v['value'] = $v['value'];
-			} else {
-				$v['value'] = encrypt($v['value']);
-			}
+		if (!$email) {
+			$this->setAPIResponse('error', 'Email not supplied', 422);
+			return false;
 		}
-		$newItem[$v['name']] = $v['value'];
-	}
-	//return $newItem;
-	return (updateConfig($newItem)) ? true : false;
-}
-
-function updateConfigItem($array)
-{
-	$array['data']['value'] = $array['data']['value'] ?? '';
-	switch ($array['data']['value']) {
-		case 'true':
-			$array['data']['value'] = (bool)true;
-			break;
-		case 'false':
-			$array['data']['value'] = (bool)false;
-			break;
-		default:
-			$array['data']['value'] = $array['data']['value'];
-	}
-	// Hash
-	if ($array['data']['type'] == 'password') {
-		$array['data']['value'] = encrypt($array['data']['value']);
-	}
-	$newItem = array(
-		$array['data']['name'] => $array['data']['value']
-	);
-	return (updateConfig($newItem)) ? true : false;
-}
-
-function getPlugins()
-{
-	if (file_exists(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.php')) {
-		$pluginList = [];
-		foreach ($GLOBALS['plugins'] as $plugin) {
-			foreach ($plugin as $key => $value) {
-				if (strpos($value['license'], $GLOBALS['license']) !== false) {
-					$plugin[$key]['enabled'] = $GLOBALS[$value['configPrefix'] . '-enabled'];
-					$pluginList[$key] = $plugin[$key];
-				}
-			}
+		if (!$password) {
+			$this->setAPIResponse('error', 'Password not supplied', 422);
+			return false;
 		}
-		return $pluginList;
+		return $this->embyJoin($username, $email, $password);
 	}
-	return false;
-}
-
-function editPlugins($array)
-{
-	switch ($array['data']['action']) {
-		case 'enable':
-			$newItem = array(
-				$array['data']['configName'] => true
-			);
-			writeLog('success', 'Plugin Function -  Enabled Plugin [' . $array['data']['name'] . ']', $GLOBALS['organizrUser']['username']);
-			return (updateConfig($newItem)) ? true : false;
-			break;
-		case 'disable':
-			$newItem = array(
-				$array['data']['configName'] => false
+	
+	public function embyJoin($username, $email, $password)
+	{
+		try {
+			#create user in emby.
+			$headers = array(
+				"Accept" => "application/json"
 			);
-			writeLog('success', 'Plugin Function -  Disabled Plugin [' . $array['data']['name'] . ']', $GLOBALS['organizrUser']['username']);
-			return (updateConfig($newItem)) ? true : false;
-			break;
-		default:
-			# code...
-			break;
-	}
-}
-
-function auth()
-{
-	$debug = $GLOBALS['authDebug']; // CAREFUL WHEN SETTING TO TRUE AS THIS OPENS AUTH UP
-	$ban = isset($_GET['ban']) ? strtoupper($_GET['ban']) : "";
-	$whitelist = isset($_GET['whitelist']) ? $_GET['whitelist'] : false;
-	$blacklist = isset($_GET['blacklist']) ? $_GET['blacklist'] : false;
-	$group = 0;
-	$groupParam = $_GET['group'];
-	$redirect = false;
-	if (isset($groupParam)) {
-		if (is_numeric($groupParam)) {
-			$group = (int)$groupParam;
-		} else {
-			$group = getTabGroup($groupParam);
-		}
-	}
-	$currentIP = userIP();
-	$unlocked = ($GLOBALS['organizrUser']['locked'] == '1') ? false : true;
-	if (isset($GLOBALS['organizrUser'])) {
-		$currentUser = $GLOBALS['organizrUser']['username'];
-		$currentGroup = $GLOBALS['organizrUser']['groupID'];
-		$currentEmail = $GLOBALS['organizrUser']['email'];
-	} else {
-		$currentUser = 'Guest';
-		$currentGroup = getUserLevel();
-		$currentEmail = 'guest@guest.com';
-	}
-	$userInfo = "User: $currentUser | Group: $currentGroup | IP: $currentIP | Requesting Access to Group $group | Result: ";
-	if ($whitelist) {
-		if (in_array($currentIP, arrayIP($whitelist))) {
-			!$debug ? exit(http_response_code(200)) : die("$userInfo Whitelist Authorized");
-		}
-	}
-	if ($blacklist) {
-		if (in_array($currentIP, arrayIP($blacklist))) {
-			!$debug ? exit(http_response_code(401)) : die("$userInfo Blacklisted");
-		}
-	}
-	if ($group !== null) {
-		if ((isset($_SERVER['HTTP_X_FORWARDED_SERVER']) && $_SERVER['HTTP_X_FORWARDED_SERVER'] == 'traefik') || $GLOBALS['traefikAuthEnable']) {
-			$redirect = 'Location: ' . getServerPath();
-		}
-		if (qualifyRequest($group) && $unlocked) {
-			header("X-Organizr-User: $currentUser");
-			header("X-Organizr-Email: $currentEmail");
-			!$debug ? exit(http_response_code(200)) : die("$userInfo Authorized");
-		} else {
-			!$debug ? (!$redirect ? exit(http_response_code(401)) : exit(http_response_code(401) . header($redirect))) : die("$userInfo Not Authorized");
-		}
-	} else {
-		!$debug ? (!$redirect ? exit(http_response_code(401)) : exit(http_response_code(401) . header($redirect))) : die("Not Authorized Due To No Parameters Set");
-	}
-}
-
-function getTabGroup($tab)
-{
-	try {
-		$connect = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],]);
-		$row = $connect->fetch('SELECT group_id FROM tabs WHERE name LIKE %~like~', $tab);
-		return $row ? $row['group_id'] : 0;
-	} catch (\Dibi\Exception $e) {
-		writeLog('error', 'Tab Group Function - Error Fetching Tab Group', $tab);
-		return 0;
-	}
-}
-
-function logoOrText()
-{
-	if ($GLOBALS['useLogoLogin'] == false) {
-		return '<h1>' . $GLOBALS['title'] . '</h1>';
-	} else {
-		return '<img class="loginLogo" src="' . $GLOBALS['loginLogo'] . '" alt="Home" />';
-	}
-}
-
-function showLogin()
-{
-	if ($GLOBALS['hideRegistration'] == false) {
-		return '<p><span lang="en">Don\'t have an account?</span><a href="#" class="text-primary m-l-5 to-register"><b lang="en">Sign Up</b></a></p>';
-	}
-}
-
-function checkoAuth()
-{
-	return ($GLOBALS['plexoAuth'] && $GLOBALS['authBackend'] == 'plex' && $GLOBALS['authType'] !== 'internal') ? true : false;
-}
-
-function checkoAuthOnly()
-{
-	return ($GLOBALS['plexoAuth'] && $GLOBALS['authBackend'] == 'plex' && $GLOBALS['authType'] == 'external') ? true : false;
-}
-
-function showoAuth()
-{
-	$buttons = '';
-	if ($GLOBALS['plexoAuth'] && $GLOBALS['authBackend'] == 'plex' && $GLOBALS['authType'] !== 'internal') {
-		$buttons .= '<a href="javascript:void(0)" onclick="oAuthStart(\'plex\')" class="btn btn-lg btn-block text-uppercase waves-effect waves-light bg-plex text-muted" data-toggle="tooltip" title="" data-original-title="Login with Plex"> <span>Login</span><i aria-hidden="true" class="mdi mdi-plex m-l-5"></i> </a>';
-	}
-	return ($buttons) ? '
-		<div class="panel">
-            <div class="panel-heading bg-org" id="plex-login-heading" role="tab">
-            	<a class="panel-title" data-toggle="collapse" href="#plex-login-collapse" data-parent="#login-panels" aria-expanded="false" aria-controls="organizr-login-collapse">
-	                <img class="lazyload loginTitle" data-src="plugins/images/tabs/plex.png"> &nbsp;
-                    <span class="text-uppercase fw300" lang="en">Login with Plex</span>
-            	</a>
-            </div>
-            <div class="panel-collapse collapse in" id="plex-login-collapse" aria-labelledby="plex-login-heading" role="tabpanel">
-                <div class="panel-body">
-               		<div class="row">
-			            <div class="col-xs-12 col-sm-12 col-md-12 text-center">
-			                <div class="social m-b-0">' . $buttons . '</div>
-			            </div>
-			        </div>
-               </div>
-            </div>
-        </div>
-	' : '';
-}
-
-function getImages()
-{
-	$allIconsPrep = array();
-	$allIcons = array();
-	$ignore = array(".", "..", "._.DS_Store", ".DS_Store", ".pydio_id", "index.html");
-	$dirname = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'tabs' . DIRECTORY_SEPARATOR;
-	$path = 'plugins/images/tabs/';
-	$images = scandir($dirname);
-	foreach ($images as $image) {
-		if (!in_array($image, $ignore)) {
-			$allIconsPrep[$image] = array(
-				'path' => $path,
-				'name' => $image
+			$data = array();
+			$url = $this->config['embyURL'] . '/emby/Users/New?name=' . $username . '&api_key=' . $this->config['embyToken'];
+			$response = Requests::Post($url, $headers, json_encode($data), array());
+			$response = $response->body;
+			//return($response);
+			$response = json_decode($response, true);
+			//return($response);
+			$userID = $response["Id"];
+			//return($userID);
+			#authenticate as user to update password.
+			//randomizer four digits of DeviceId
+			// I dont think ther would be security problems with hardcoding deviceID but randomizing it would mitigate any issue.
+			$deviceIdSeceret = rand(0, 9) . "" . rand(0, 9) . "" . rand(0, 9) . "" . rand(0, 9);
+			//hardcoded device id with the first three digits random 0-9,0-9,0-9,0-9
+			$embyAuthHeader = 'MediaBrowser Client="Emby Mobile", Device="Firefox", DeviceId="' . $deviceIdSeceret . 'aWxssS81LgAggFdpbmRvd3MgTlQgMTAuMDsgV2luNjxx7IHf2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzcyLjAuMzYyNi4xMTkgU2FmYXJpLzUzNy4zNnwxNTUxNTczMTAyNDI4", Version="4.0.2.0"';
+			$headers = array(
+				"Accept" => "application/json",
+				"Content-Type" => "application/json",
+				"X-Emby-Authorization" => $embyAuthHeader
 			);
-		}
-	}
-	$dirname = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'userTabs' . DIRECTORY_SEPARATOR;
-	$path = 'plugins/images/userTabs/';
-	$images = scandir($dirname);
-	foreach ($images as $image) {
-		if (!in_array($image, $ignore)) {
-			$allIconsPrep[$image] = array(
-				'path' => $path,
-				'name' => $image
+			$data = array(
+				"Pw" => "",
+				"Username" => $username
 			);
-		}
-	}
-	ksort($allIconsPrep);
-	foreach ($allIconsPrep as $item) {
-		$allIcons[] = $item['path'] . $item['name'];
-	}
-	return $allIcons;
-}
-
-function imageSelect($form)
-{
-	$i = 1;
-	$images = getImages();
-	$return = '<select class="form-control tabIconImageList" id="' . $form . '-chooseImage" name="chooseImage"><option lang="en">Select or type Icon</option>';
-	foreach ($images as $image) {
-		$i++;
-		$return .= '<option value="' . $image . '">' . basename($image) . '</option>';
-	}
-	return $return . '</select>';
-}
-
-function editImages()
-{
-	$array = array();
-	$postCheck = array_filter($_POST);
-	$filesCheck = array_filter($_FILES);
-	$approvedPath = 'plugins/images/userTabs/';
-	if (!empty($postCheck)) {
-		$removeImage = $approvedPath . pathinfo($_POST['data']['imagePath'], PATHINFO_BASENAME);
-		if ($_POST['data']['action'] == 'deleteImage' && approvedFileExtension($removeImage)) {
-			if (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . $removeImage)) {
-				writeLog('success', 'Image Manager Function -  Deleted Image [' . $_POST['data']['imageName'] . ']', $GLOBALS['organizrUser']['username']);
-				return (unlink(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . $removeImage)) ? true : false;
-			}
-		}
-	}
-	if (!empty($filesCheck) && approvedFileExtension($_FILES['file']['name']) && strpos($_FILES['file']['type'], 'image/') !== false) {
-		ini_set('upload_max_filesize', '10M');
-		ini_set('post_max_size', '10M');
-		$tempFile = $_FILES['file']['tmp_name'];
-		$targetPath = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'userTabs' . DIRECTORY_SEPARATOR;
-		$targetFile = $targetPath . $_FILES['file']['name'];
-		return (move_uploaded_file($tempFile, $targetFile)) ? true : false;
-	}
-	return false;
-}
-
-function approvedFileExtension($filename)
-{
-	$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
-	switch ($ext) {
-		case 'gif':
-		case 'png':
-		case 'jpeg':
-		case 'jpg':
-		case 'svg':
-			return true;
-			break;
-		default:
-			return false;
-	}
-}
-
-function getThemes()
-{
-	$themes = array();
-	foreach (glob(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'css' . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . "*.css") as $filename) {
-		$themes[] = array(
-			'name' => preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($filename)),
-			'value' => preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($filename))
-		);
-	}
-	return $themes;
-}
-
-function getSounds()
-{
-	$sounds = array();
-	foreach (glob(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'sounds' . DIRECTORY_SEPARATOR . 'default' . DIRECTORY_SEPARATOR . "*.mp3") as $filename) {
-		$sounds[] = array(
-			'name' => preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($filename)),
-			'value' => preg_replace('/\\.[^.\\s]{3,4}$/', '', 'plugins/sounds/default/' . basename($filename) . '.mp3')
-		);
-	}
-	foreach (glob(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'sounds' . DIRECTORY_SEPARATOR . 'custom' . DIRECTORY_SEPARATOR . "*.mp3") as $filename) {
-		$sounds[] = array(
-			'name' => preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($filename)),
-			'value' => preg_replace('/\\.[^.\\s]{3,4}$/', '', 'plugins/sounds/custom/' . basename($filename) . '.mp3')
-		);
-	}
-	return $sounds;
-}
-
-function getBranches()
-{
-	return array(
-		array(
-			'name' => 'Develop',
-			'value' => 'v2-develop'
-		),
-		array(
-			'name' => 'Master',
-			'value' => 'v2-master'
-		)
-	);
-}
-
-function getAuthTypes()
-{
-	return array(
-		array(
-			'name' => 'Organizr DB',
-			'value' => 'internal'
-		),
-		array(
-			'name' => 'Organizr DB + Backend',
-			'value' => 'both'
-		),
-		array(
-			'name' => 'Backend Only',
-			'value' => 'external'
-		)
-	);
-}
-
-function getLDAPOptions()
-{
-	return array(
-		array(
-			'name' => 'Active Directory',
-			'value' => '1'
-		),
-		array(
-			'name' => 'OpenLDAP',
-			'value' => '2'
-		),
-		array(
-			'name' => 'First IPA',
-			'value' => '3'
-		),
-	);
-}
-
-function getAuthBackends()
-{
-	$backendOptions = array();
-	$backendOptions[] = array(
-		'name' => 'Choose Backend',
-		'value' => false,
-		'disabled' => true
-	);
-	foreach (array_filter(get_defined_functions()['user'], function ($v) {
-		return strpos($v, 'plugin_auth_') === 0;
-	}) as $value) {
-		$name = str_replace('plugin_auth_', '', $value);
-		if (strpos($name, 'disabled') === false) {
-			$backendOptions[] = array(
-				'name' => ucwords(str_replace('_', ' ', $name)),
-				'value' => $name
+			$url = $this->config['embyURL'] . '/emby/Users/AuthenticateByName';
+			$response = Requests::Post($url, $headers, json_encode($data), array());
+			$response = $response->body;
+			$response = json_decode($response, true);
+			$userToken = $response["AccessToken"];
+			#update password
+			$embyAuthHeader = 'MediaBrowser Client="Emby Mobile", Device="Firefox", Token="' . $userToken . '", DeviceId="' . $deviceIdSeceret . 'aWxssS81LgAggFdpbmRvd3MgTlQgMTAuMDsgV2luNjxx7IHf2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzcyLjAuMzYyNi4xMTkgU2FmYXJpLzUzNy4zNnwxNTUxNTczMTAyNDI4", Version="4.0.2.0"';
+			$headers = array(
+				"Accept" => "application/json",
+				"Content-Type" => "application/json",
+				"X-Emby-Authorization" => $embyAuthHeader
 			);
-		} else {
-			$backendOptions[] = array(
-				'name' => $value(),
-				'value' => 'none',
-				'disabled' => true,
+			$data = array(
+				"CurrentPw" => "",
+				"NewPw" => $password,
+				"Id" => $userID
+			);
+			$url = $this->config['embyURL'] . '/emby/Users/' . $userID . '/Password';
+			Requests::Post($url, $headers, json_encode($data), array());
+			#update config
+			$headers = array(
+				"Accept" => "application/json",
+				"Content-Type" => "application/json"
 			);
+			$url = $this->config['embyURL'] . '/emby/Users/' . $userID . '/Policy?api_key=' . $this->config['embyToken'];
+			$response = Requests::Post($url, $headers, $this->getEmbyTemplateUserJson(), array());
+			#add emby.media
+			try {
+				#seperate because this is not required
+				$headers = array(
+					"Accept" => "application/json",
+					"X-Emby-Authorization" => $embyAuthHeader
+				);
+				$data = array(
+					"ConnectUsername " => $email
+				);
+				$url = $this->config['embyURL'] . '/emby/Users/' . $userID . '/Connect/Link';
+				Requests::Post($url, $headers, json_encode($data), array());
+			} catch (Requests_Exception $e) {
+				$this->writeLog('error', 'Emby Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setAPIResponse('error', $e->getMessage(), 500);
+				return false;
+			}
+			$this->setAPIResponse('success', 'User has joined Emby', 200);
+			return true;
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Emby create Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
 		}
 	}
-	ksort($backendOptions);
-	return $backendOptions;
-}
-
-function wizardPath($array)
-{
-	$path = $array['data']['path'];
-	if (file_exists($path)) {
-		if (is_writable($path)) {
-			return true;
+	
+	/*loads users from emby and returns a correctly formated policy for a new user.
+	*/
+	public function getEmbyTemplateUserJson()
+	{
+		$headers = array(
+			"Accept" => "application/json"
+		);
+		$data = array();
+		$url = $this->config['embyURL'] . '/emby/Users?api_key=' . $this->config['embyToken'];
+		$response = Requests::Get($url, $headers, array());
+		$response = $response->body;
+		$response = json_decode($response, true);
+		//error_Log("response ".json_encode($response));
+		$this->writeLog('error', 'userList:' . json_encode($response), 'SYSTEM');
+		//$correct stores the template users object
+		$correct = null;
+		foreach ($response as $element) {
+			if ($element['Name'] == $this->config['INVITES-EmbyTemplate']) {
+				$correct = $element;
+			}
 		}
-	} else {
-		if (is_writable(dirname($path, 1))) {
-			if (mkdir($path, 0760, true)) {
+		$this->writeLog('error', 'Correct user:' . json_encode($correct), 'SYSTEM');
+		if ($correct == null) {
+			//return empty JSON if user incorrectly configured template
+			return "{}";
+		}
+		//select policy section and remove possibly dangerous rows.
+		$policy = $correct['Policy'];
+		//writeLog('error', 'policy update'.$policy, 'SYSTEM');
+		unset($policy['AuthenticationProviderId']);
+		unset($policy['InvalidLoginAttemptCount']);
+		unset($policy['DisablePremiumFeatures']);
+		unset($policy['DisablePremiumFeatures']);
+		return (json_encode($policy));
+	}
+	
+	public function checkHostPrefix($s)
+	{
+		if (empty($s)) {
+			return $s;
+		}
+		return (substr($s, -1, 1) == '\\') ? $s : $s . '\\';
+	}
+	
+	public function approvedFileExtension($filename)
+	{
+		$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
+		switch ($ext) {
+			case 'gif':
+			case 'png':
+			case 'jpeg':
+			case 'jpg':
+			case 'svg':
 				return true;
-			}
+				break;
+			default:
+				return false;
 		}
 	}
-	return 'permissions';
-}
-
-function groupSelect()
-{
-	$groups = allGroups();
-	$select = array();
-	foreach ($groups as $key => $value) {
-		$select[] = array(
-			'name' => $value['group'],
-			'value' => $value['group_id']
-		);
-	}
-	return $select;
-}
-
-function getImage()
-{
-	$refresh = false;
-	$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
-	if (!file_exists($cacheDirectory)) {
-		mkdir($cacheDirectory, 0777, true);
-	}
-	@$image_url = $_GET['img'];
-	@$key = $_GET['key'];
-	@$image_height = $_GET['height'];
-	@$image_width = $_GET['width'];
-	@$source = $_GET['source'];
-	@$itemType = $_GET['type'];
-	if (strpos($key, '$') !== false) {
-		$key = explode('$', $key)[0];
-		$refresh = true;
-	}
-	switch ($source) {
-		case 'plex':
-			$plexAddress = qualifyURL($GLOBALS['plexURL']);
-			$image_src = $plexAddress . '/photo/:/transcode?height=' . $image_height . '&width=' . $image_width . '&upscale=1&url=' . $image_url . '&X-Plex-Token=' . $GLOBALS['plexToken'];
-			break;
-		case 'emby':
-			$embyAddress = qualifyURL($GLOBALS['embyURL']);
-			$imgParams = array();
-			if (isset($_GET['height'])) {
-				$imgParams['height'] = 'maxHeight=' . $_GET['height'];
+	
+	public function getImages()
+	{
+		$allIconsPrep = array();
+		$allIcons = array();
+		$ignore = array(".", "..", "._.DS_Store", ".DS_Store", ".pydio_id", "index.html");
+		$dirname = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'tabs' . DIRECTORY_SEPARATOR;
+		$path = 'plugins/images/tabs/';
+		$images = scandir($dirname);
+		foreach ($images as $image) {
+			if (!in_array($image, $ignore)) {
+				$allIconsPrep[$image] = array(
+					'path' => $path,
+					'name' => $image
+				);
 			}
-			if (isset($_GET['width'])) {
-				$imgParams['width'] = 'maxWidth=' . $_GET['width'];
+		}
+		$dirname = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'userTabs' . DIRECTORY_SEPARATOR;
+		$path = 'plugins/images/userTabs/';
+		$images = scandir($dirname);
+		foreach ($images as $image) {
+			if (!in_array($image, $ignore)) {
+				$allIconsPrep[$image] = array(
+					'path' => $path,
+					'name' => $image
+				);
 			}
-			$image_src = $embyAddress . '/Items/' . $image_url . '/Images/' . $itemType . '?' . implode('&', $imgParams);
-			break;
-		default:
-			# code...
-			break;
+		}
+		ksort($allIconsPrep);
+		foreach ($allIconsPrep as $item) {
+			$allIcons[] = $item['path'] . $item['name'];
+		}
+		return $allIcons;
+	}
+	
+	public function imageSelect($form)
+	{
+		$i = 1;
+		$images = $this->getImages();
+		$return = '<select class="form-control tabIconImageList" id="' . $form . '-chooseImage" name="chooseImage"><option lang="en">Select or type Icon</option>';
+		foreach ($images as $image) {
+			$i++;
+			$return .= '<option value="' . $image . '">' . basename($image) . '</option>';
+		}
+		return $return . '</select>';
+	}
+	
+	public function getThemes()
+	{
+		$themes = array();
+		foreach (glob(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'css' . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . "*.css") as $filename) {
+			$themes[] = array(
+				'name' => preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($filename)),
+				'value' => preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($filename))
+			);
+		}
+		return $themes;
 	}
-	if (isset($image_url) && isset($image_height) && isset($image_width) && isset($image_src)) {
-		$cachefile = $cacheDirectory . $key . '.jpg';
-		$cachetime = 604800;
-		// Serve from the cache if it is younger than $cachetime
-		if (file_exists($cachefile) && time() - $cachetime < filemtime($cachefile) && $refresh == false) {
-			header("Content-type: image/jpeg");
-			//@readfile($cachefile);
-			//echo @curl('get', $cachefile)['content'];
-			$options = array('verify' => false);
-			$response = Requests::get($cachefile, array(), $options);
-			if ($response->success) {
-				echo $response->body;
-			}
-			exit;
+	
+	public function getSounds()
+	{
+		$sounds = array();
+		foreach (glob(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'sounds' . DIRECTORY_SEPARATOR . 'default' . DIRECTORY_SEPARATOR . "*.mp3") as $filename) {
+			$sounds[] = array(
+				'name' => preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($filename)),
+				'value' => preg_replace('/\\.[^.\\s]{3,4}$/', '', 'plugins/sounds/default/' . basename($filename) . '.mp3')
+			);
 		}
-		ob_start(); // Start the output buffer
-		header('Content-type: image/jpeg');
-		//@readfile($image_src);
-		//echo @curl('get', $image_src)['content'];
-		$options = array('verify' => false);
-		$response = Requests::get($image_src, array(), $options);
-		if ($response->success) {
-			echo $response->body;
+		foreach (glob(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'sounds' . DIRECTORY_SEPARATOR . 'custom' . DIRECTORY_SEPARATOR . "*.mp3") as $filename) {
+			$sounds[] = array(
+				'name' => preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($filename)),
+				'value' => preg_replace('/\\.[^.\\s]{3,4}$/', '', 'plugins/sounds/custom/' . basename($filename) . '.mp3')
+			);
 		}
-		// Cache the output to a file
-		$fp = fopen($cachefile, 'wb');
-		fwrite($fp, ob_get_contents());
-		fclose($fp);
-		ob_end_flush(); // Send the output to the browser
-		die();
-	} else {
-		die("Invalid Request");
+		return $sounds;
 	}
-}
-
-function cacheImage($url, $name, $extension = 'jpg')
-{
-	$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
-	if (!file_exists($cacheDirectory)) {
-		mkdir($cacheDirectory, 0777, true);
+	
+	public function getBranches()
+	{
+		return array(
+			array(
+				'name' => 'Develop',
+				'value' => 'v2-develop'
+			),
+			array(
+				'name' => 'Master',
+				'value' => 'v2-master'
+			)
+		);
+	}
+	
+	public function getAuthTypes()
+	{
+		return array(
+			array(
+				'name' => 'Organizr DB',
+				'value' => 'internal'
+			),
+			array(
+				'name' => 'Organizr DB + Backend',
+				'value' => 'both'
+			),
+			array(
+				'name' => 'Backend Only',
+				'value' => 'external'
+			)
+		);
 	}
-	$cachefile = $cacheDirectory . $name . '.' . $extension;
-	$cachetime = 604800;
-	if ((file_exists($cachefile) && time() - $cachetime < filemtime($cachefile)) || !file_exists($cachefile)) {
-		@copy($url, $cachefile);
+	
+	public function getLDAPOptions()
+	{
+		return array(
+			array(
+				'name' => 'Active Directory',
+				'value' => '1'
+			),
+			array(
+				'name' => 'OpenLDAP',
+				'value' => '2'
+			),
+			array(
+				'name' => 'First IPA',
+				'value' => '3'
+			),
+		);
 	}
-}
-
-function downloader($array)
-{
-	switch ($array['data']['source']) {
-		case 'sabnzbd':
-			switch ($array['data']['action']) {
-				case 'resume':
-				case 'pause':
-					sabnzbdAction($array['data']['action'], $array['data']['target']);
-					break;
-				default:
-					# code...
-					break;
-			}
-			break;
-		case 'jdownloader':
-			switch ($array['data']['action']) {
-				case 'start':
-					jdownloaderAction($array['data']['action'], $array['data']['target']);
-					break;
-				case 'stop':
-					jdownloaderAction($array['data']['action'], $array['data']['target']);
-					break;
-				case 'resume':
-					jdownloaderAction($array['data']['action'], $array['data']['target']);
-					break;
-				case 'pause':
-					jdownloaderAction($array['data']['action'], $array['data']['target']);
-					break;
-				case 'update':
-					jdownloaderAction($array['data']['action'], $array['data']['target']);
-					break;
-				case 'retry':
-					jdownloaderAction($array['data']['action'], $array['data']['target']);
-					break;
-				case 'remove':
-					jdownloaderAction($array['data']['action'], $array['data']['target']);
-					break;
-				default:
-					# code...
-					break;
+	
+	public function getAuthBackends()
+	{
+		$backendOptions = array();
+		$backendOptions[] = array(
+			'name' => 'Choose Backend',
+			'value' => false,
+			'disabled' => true
+		);
+		foreach (array_filter(get_class_methods('Organizr'), function ($v) {
+			return strpos($v, 'plugin_auth_') === 0;
+		}) as $value) {
+			$name = str_replace('plugin_auth_', '', $value);
+			if (strpos($name, 'disabled') === false) {
+				$backendOptions[] = array(
+					'name' => ucwords(str_replace('_', ' ', $name)),
+					'value' => $name
+				);
+			} else {
+				$backendOptions[] = array(
+					'name' => $this->$value(),
+					'value' => 'none',
+					'disabled' => true,
+				);
 			}
-			break;
-		case 'nzbget':
-			break;
-		default:
-			# code...
-			break;
-	}
-}
-
-function jdownloaderAction($action = null, $target = null)
-{
-	if ($GLOBALS['homepageJdownloaderEnabled'] && !empty($GLOBALS['jdownloaderURL']) && qualifyRequest($GLOBALS['homepageJdownloaderAuth'])) {
-		$url = qualifyURL($GLOBALS['jdownloaderURL']);
-		# This ensures compatibility with RSScrawler
-		$url = str_replace('/myjd', '', $url);
-		if (substr($url, -1) == '/') {
-			$url = substr_replace($url, "", -1);
 		}
-		switch ($action) {
-			case 'start':
-				$url = $url . '/myjd_start/';
-				break;
-			case 'stop':
-				$url = $url . '/myjd_stop/';
-				break;
-			case 'resume':
-				$url = $url . '/myjd_pause/false';
-				break;
-			case 'pause':
-				$url = $url . '/myjd_pause/true';
-				break;
-			case 'update':
-				$url = $url . '/myjd_update';
+		ksort($backendOptions);
+		return $backendOptions;
+	}
+	
+	public function importUserButtons()
+	{
+		$emptyButtons = '
+		<div class="col-md-12">
+            <div class="white-box bg-org">
+                <h3 class="box-title m-0" lang="en">Currently User import is available for Plex only.</h3> </div>
+        </div>
+	';
+		$buttons = '';
+		if (!empty($this->config['plexToken'])) {
+			$buttons .= '<button class="btn m-b-20 m-r-20 bg-plex text-muted waves-effect waves-light importUsersButton" onclick="importUsers(\'plex\')" type="button"><span class="btn-label"><i class="mdi mdi-plex"></i></span><span lang="en">Import Plex Users</span></button>';
+		}
+		if (!empty($this->config['jellyfinURL']) && !empty($this->config['jellyfinToken'])) {
+			$buttons .= '<button class="btn m-b-20 m-r-20 bg-primary text-muted waves-effect waves-light importUsersButton" onclick="importUsers(\'jellyfin\')" type="button"><span class="btn-label"><i class="mdi mdi-fish"></i></span><span lang="en">Import Jellyfin Users</span></button>';
+		}
+		if (!empty($this->config['embyURL']) && !empty($this->config['embyToken'])) {
+			$buttons .= '<button class="btn m-b-20 m-r-20 bg-emby text-muted waves-effect waves-light importUsersButton" onclick="importUsers(\'emby\')" type="button"><span class="btn-label"><i class="mdi mdi-emby"></i></span><span lang="en">Import Jellyfin Users</span></button>';
+		}
+		return ($buttons !== '') ? $buttons : $emptyButtons;
+	}
+	
+	public function getHomepageMediaImage()
+	{
+		$refresh = false;
+		$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+		if (!file_exists($cacheDirectory)) {
+			mkdir($cacheDirectory, 0777, true);
+		}
+		@$image_url = $_GET['img'];
+		@$key = $_GET['key'];
+		@$image_height = $_GET['height'];
+		@$image_width = $_GET['width'];
+		@$source = $_GET['source'];
+		@$itemType = $_GET['type'];
+		if (strpos($key, '$') !== false) {
+			$key = explode('$', $key)[0];
+			$refresh = true;
+		}
+		switch ($source) {
+			case 'plex':
+				$plexAddress = $this->qualifyURL($this->config['plexURL']);
+				$image_src = $plexAddress . '/photo/:/transcode?height=' . $image_height . '&width=' . $image_width . '&upscale=1&url=' . $image_url . '&X-Plex-Token=' . $this->config['plexToken'];
 				break;
-			case 'retry':
-				# code...
+			case 'emby':
+				$embyAddress = $this->qualifyURL($this->config['embyURL']);
+				$imgParams = array();
+				if (isset($_GET['height'])) {
+					$imgParams['height'] = 'maxHeight=' . $_GET['height'];
+				}
+				if (isset($_GET['width'])) {
+					$imgParams['width'] = 'maxWidth=' . $_GET['width'];
+				}
+				$image_src = $embyAddress . '/Items/' . $image_url . '/Images/' . $itemType . '?' . implode('&', $imgParams);
 				break;
-			case 'remove':
-				# code...
+			case 'jellyfin':
+				$jellyfinAddress = $this->qualifyURL($this->config['jellyfinURL']);
+				$imgParams = array();
+				if (isset($_GET['height'])) {
+					$imgParams['height'] = 'maxHeight=' . $_GET['height'];
+				}
+				if (isset($_GET['width'])) {
+					$imgParams['width'] = 'maxWidth=' . $_GET['width'];
+				}
+				$image_src = $jellyfinAddress . '/Items/' . $image_url . '/Images/' . $itemType . '?' . implode('&', $imgParams);
 				break;
 			default:
 				# code...
 				break;
 		}
-		try {
-			$options = (localURL($url)) ? array('verify' => false) : array();
-			$response = Requests::post($url, array(), $options);
-			if ($response->success) {
-				$api['content'] = json_decode($response->body, true);
+		if (isset($image_url) && isset($image_height) && isset($image_width) && isset($image_src)) {
+			$cachefile = $cacheDirectory . $key . '.jpg';
+			$cachetime = 604800;
+			// Serve from the cache if it is younger than $cachetime
+			if (file_exists($cachefile) && time() - $cachetime < filemtime($cachefile) && $refresh == false) {
+				header("Content-type: image/jpeg");
+				@readfile($cachefile);
+				exit;
 			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'JDownloader Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-		$api['content'] = isset($api['content']) ? $api['content'] : false;
-		return $api;
-	}
-}
-
-function sabnzbdAction($action = null, $target = null)
-{
-	if ($GLOBALS['homepageSabnzbdEnabled'] && !empty($GLOBALS['sabnzbdURL']) && !empty($GLOBALS['sabnzbdToken']) && qualifyRequest($GLOBALS['homepageSabnzbdAuth'])) {
-		$url = qualifyURL($GLOBALS['sabnzbdURL']);
-		switch ($action) {
-			case 'pause':
-				$id = ($target !== '' && $target !== 'main' && isset($target)) ? 'mode=queue&name=pause&value=' . $target . '&' : 'mode=pause';
-				$url = $url . '/api?' . $id . '&output=json&apikey=' . $GLOBALS['sabnzbdToken'];
-				break;
-			case 'resume':
-				$id = ($target !== '' && $target !== 'main' && isset($target)) ? 'mode=queue&name=resume&value=' . $target . '&' : 'mode=resume';
-				$url = $url . '/api?' . $id . '&output=json&apikey=' . $GLOBALS['sabnzbdToken'];
-				break;
-			default:
-				# code...
-				break;
-		}
-		try {
-			$options = (localURL($url)) ? array('verify' => false) : array();
-			$response = Requests::get($url, array(), $options);
+			ob_start(); // Start the output buffer
+			header('Content-type: image/jpeg');
+			$options = array('verify' => false);
+			$response = Requests::get($image_src, array(), $options);
 			if ($response->success) {
-				$api['content'] = json_decode($response->body, true);
-			}
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		};
-		$api['content'] = isset($api['content']) ? $api['content'] : false;
-		return $api;
-	}
-}
-
-// Deluge API isn't working ATM - will get with dev.
-function delugeAction($action = null, $target = null)
-{
-	if ($GLOBALS['homepageDelugeEnabled'] && !empty($GLOBALS['delugeURL']) && !empty($GLOBALS['delugePassword']) && qualifyRequest($GLOBALS['homepageDelugeAuth'])) {
-		$url = qualifyURL($GLOBALS['delugeURL']);
-		try {
-			$deluge = new deluge($GLOBALS['delugeURL'], decrypt($GLOBALS['delugePassword']));
-			switch ($action) {
-				case 'pause':
-					$torrents = $deluge->pauseTorrent($target);
-					break;
-				case 'pauseAll':
-					$torrents = $deluge->pauseAllTorrents();
-					break;
-				case 'resume':
-					$torrents = $deluge->resumeTorrent($target);
-					break;
-				case 'resumeAll':
-					$torrents = $deluge->resumeAllTorrents();
-					break;
-				default:
-					# code...
-					break;
+				echo $response->body;
 			}
-			$api['content'] = $torrents;
-		} catch (Excecption $e) {
-			writeLog('error', 'Deluge Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			// Cache the output to a file
+			$fp = fopen($cachefile, 'wb');
+			fwrite($fp, ob_get_contents());
+			fclose($fp);
+			ob_end_flush(); // Send the output to the browser
+			die();
+		} else {
+			die("Invalid Request");
 		}
-		$api['content'] = isset($api['content']) ? $api['content'] : false;
-		return $api;
 	}
-	return false;
-}
-
-function getOrgUsers()
-{
-	$result = allUsers();
-	if (is_array($result) || is_object($result)) {
-		foreach ($result['users'] as $k => $v) {
-			$return[$v['username']] = $v['email'];
+	
+	public function cacheImage($url, $name, $extension = 'jpg')
+	{
+		$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+		if (!file_exists($cacheDirectory)) {
+			mkdir($cacheDirectory, 0777, true);
+		}
+		$cacheFile = $cacheDirectory . $name . '.' . $extension;
+		$cacheTime = 604800;
+		if ((file_exists($cacheFile) && time() - $cacheTime < filemtime($cacheFile)) || !file_exists($cacheFile)) {
+			@copy($url, $cacheFile);
 		}
-		return $return;
-	}
-}
-
-function convertPlexName($user, $type)
-{
-	$array = userList('plex');
-	switch ($type) {
-		case "username":
-		case "u":
-			$plexUser = array_search($user, $array['users']);
-			break;
-		case "id":
-			if (array_key_exists(strtolower($user), $array['users'])) {
-				$plexUser = $array['users'][strtolower($user)];
-			}
-			break;
-		default:
-			$plexUser = false;
-	}
-	return (!empty($plexUser) ? $plexUser : null);
-}
-
-function userList($type = null)
-{
-	switch ($type) {
-		case 'plex':
-			if (!empty($GLOBALS['plexToken']) && !empty($GLOBALS['plexID'])) {
-				$url = 'https://plex.tv/api/servers/' . $GLOBALS['plexID'] . '/shared_servers';
-				try {
-					$headers = array(
-						"Accept" => "application/json",
-						"X-Plex-Token" => $GLOBALS['plexToken']
-					);
-					$response = Requests::get($url, $headers, array());
-					libxml_use_internal_errors(true);
-					if ($response->success) {
-						$libraryList = array();
-						$plex = simplexml_load_string($response->body);
-						foreach ($plex->SharedServer as $child) {
-							if (!empty($child['username'])) {
-								$username = (string)strtolower($child['username']);
-								$email = (string)strtolower($child['email']);
-								$libraryList['users'][$username] = (string)$child['id'];
-								$libraryList['emails'][$email] = (string)$child['id'];
-								$libraryList['both'][$username] = $email;
-							}
-						}
-						$libraryList = array_change_key_case($libraryList, CASE_LOWER);
-						return $libraryList;
-					}
-				} catch (Requests_Exception $e) {
-					writeLog('error', 'Plex Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-				};
-			}
-			break;
-		default:
-			# code...
-			break;
-	}
-	return false;
-}
-
-function libraryList($type = null)
-{
-	switch ($type) {
-		case 'plex':
-			if (!empty($GLOBALS['plexToken']) && !empty($GLOBALS['plexID'])) {
-				$url = 'https://plex.tv/api/servers/' . $GLOBALS['plexID'];
-				try {
-					$headers = array(
-						"Accept" => "application/json",
-						"X-Plex-Token" => $GLOBALS['plexToken']
-					);
-					$response = Requests::get($url, $headers, array());
-					libxml_use_internal_errors(true);
-					if ($response->success) {
-						$libraryList = array();
-						$plex = simplexml_load_string($response->body);
-						foreach ($plex->Server->Section as $child) {
-							$libraryList['libraries'][(string)$child['title']] = (string)$child['id'];
-						}
-						$libraryList = array_change_key_case($libraryList, CASE_LOWER);
-						return $libraryList;
-					}
-				} catch (Requests_Exception $e) {
-					writeLog('error', 'Plex Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-				};
-			}
-			break;
-		default:
-			# code...
-			break;
 	}
-	return false;
-}
-
-function plexJoinAPI($array)
-{
-	return plexJoin($array['data']['username'], $array['data']['email'], $array['data']['password']);
-}
-
-function embyJoinAPI($array)
-{
-	return embyJoin($array['data']['username'], $array['data']['email'], $array['data']['password']);
-}
-
-function plexJoin($username, $email, $password)
-{
-	try {
-		$url = 'https://plex.tv/users.json';
-		$headers = array(
-			'Accept' => 'application/json',
-			'Content-Type' => 'application/x-www-form-urlencoded',
-			'X-Plex-Product' => 'Organizr',
-			'X-Plex-Version' => '2.0',
-			'X-Plex-Client-Identifier' => $GLOBALS['uuid'],
-		);
-		$data = array(
-			'user[email]' => $email,
-			'user[username]' => $username,
-			'user[password]' => $password,
-		);
-		$response = Requests::post($url, $headers, $data, array());
-		$json = json_decode($response->body, true);
-		$errors = (!empty($json['errors']) ? true : false);
-		$success = (!empty($json['user']) ? true : false);
-		//Use This for later
-		$usernameError = (!empty($json['errors']['username']) ? $json['errors']['username'][0] : false);
-		$emailError = (!empty($json['errors']['email']) ? $json['errors']['email'][0] : false);
-		$passwordError = (!empty($json['errors']['password']) ? $json['errors']['password'][0] : false);
-		$errorMessage = "";
-		if ($errors) {
-			if ($usernameError) {
-				$errorMessage .= "[Username Error: " . $usernameError . "]";
-			}
-			if ($emailError) {
-				$errorMessage .= "[Email Error: " . $emailError . "]";
+	
+	public function checkFrame($array, $url)
+	{
+		if (array_key_exists("x-frame-options", $array)) {
+			if ($array['x-frame-options'] == "deny") {
+				return false;
+			} elseif ($array['x-frame-options'] == "sameorgin") {
+				$digest = parse_url($url);
+				$host = (isset($digest['host']) ? $digest['host'] : '');
+				if ($this->getServer() == $host) {
+					return true;
+				} else {
+					return false;
+				}
 			}
-			if ($passwordError) {
-				$errorMessage .= "[Password Error: " . $passwordError . "]";
+		} else {
+			if (!$array) {
+				return false;
 			}
+			return true;
 		}
-		return (!empty($success) && empty($errors) ? true : $errorMessage);
-	} catch (Requests_Exception $e) {
-		writeLog('error', 'Plex.TV Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-	};
-	return false;
-}
-
-function embyJoin($username, $email, $password)
-{
-	try {
-		#create user in emby.
-		$headers = array(
-			"Accept" => "application/json"
-		);
-		$data = array();
-		$url = $GLOBALS['embyURL'] . '/emby/Users/New?name=' . $username . '&api_key=' . $GLOBALS['embyToken'];
-		$response = Requests::Post($url, $headers, json_encode($data), array());
-		$response = $response->body;
-		//return($response);
-		$response = json_decode($response, true);
-		//return($response);
-		$userID = $response["Id"];
-		//return($userID);
-		#authenticate as user to update password.
-		//randomizer four digits of DeviceId
-		// I dont think ther would be security problems with hardcoding deviceID but randomizing it would mitigate any issue.
-		$deviceIdSeceret = rand(0, 9) . "" . rand(0, 9) . "" . rand(0, 9) . "" . rand(0, 9);
-		//hardcoded device id with the first three digits random 0-9,0-9,0-9,0-9
-		$embyAuthHeader = 'MediaBrowser Client="Emby Mobile", Device="Firefox", DeviceId="' . $deviceIdSeceret . 'aWxssS81LgAggFdpbmRvd3MgTlQgMTAuMDsgV2luNjxx7IHf2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzcyLjAuMzYyNi4xMTkgU2FmYXJpLzUzNy4zNnwxNTUxNTczMTAyNDI4", Version="4.0.2.0"';
-		$headers = array(
-			"Accept" => "application/json",
-			"Content-Type" => "application/json",
-			"X-Emby-Authorization" => $embyAuthHeader
-		);
-		$data = array(
-			"Pw" => "",
-			"Username" => $username
-		);
-		$url = $GLOBALS['embyURL'] . '/emby/Users/AuthenticateByName';
-		$response = Requests::Post($url, $headers, json_encode($data), array());
-		$response = $response->body;
-		$response = json_decode($response, true);
-		$userToken = $response["AccessToken"];
-		#update password
-		$embyAuthHeader = 'MediaBrowser Client="Emby Mobile", Device="Firefox", Token="' . $userToken . '", DeviceId="' . $deviceIdSeceret . 'aWxssS81LgAggFdpbmRvd3MgTlQgMTAuMDsgV2luNjxx7IHf2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzcyLjAuMzYyNi4xMTkgU2FmYXJpLzUzNy4zNnwxNTUxNTczMTAyNDI4", Version="4.0.2.0"';
-		$headers = array(
-			"Accept" => "application/json",
-			"Content-Type" => "application/json",
-			"X-Emby-Authorization" => $embyAuthHeader
-		);
-		$data = array(
-			"CurrentPw" => "",
-			"NewPw" => $password,
-			"Id" => $userID
-		);
-		$url = $GLOBALS['embyURL'] . '/emby/Users/' . $userID . '/Password';
-		Requests::Post($url, $headers, json_encode($data), array());
-		#update config
-		$headers = array(
-			"Accept" => "application/json",
-			"Content-Type" => "application/json"
-		);
-		$url = $GLOBALS['embyURL'] . '/emby/Users/' . $userID . '/Policy?api_key=' . $GLOBALS['embyToken'];
-		$response = Requests::Post($url, $headers, getEmbyTemplateUserJson(), array());
-		#add emby.media
-		try {
-			#seperate because this is not required
-			$headers = array(
-				"Accept" => "application/json",
-				"X-Emby-Authorization" => $embyAuthHeader
-			);
-			$data = array(
-				"ConnectUsername " => $email
-			);
-			$url = $GLOBALS['embyURL'] . '/emby/Users/' . $userID . '/Connect/Link';
-			Requests::Post($url, $headers, json_encode($data), array());
-		} catch (Requests_Exception $e) {
-			writeLog('error', 'Emby Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
-		}
-		return (true);
-		//return( "USERID:".$userID);
-	} catch (Requests_Exception $e) {
-		writeLog('error', 'Emby create Function - Error: ' . $e->getMessage(), 'SYSTEM');
-	};
-	return false;
-}
-
-function checkFrame($array, $url)
-{
-	if (array_key_exists("x-frame-options", $array)) {
-		if ($array['x-frame-options'] == "deny") {
+	}
+	
+	public function frameTest($url)
+	{
+		if (!$url || $url == '') {
+			$this->setAPIResponse('error', 'URL not supplied', 404);
 			return false;
-		} elseif ($array['x-frame-options'] == "sameorgin") {
-			$digest = parse_url($url);
-			$host = (isset($digest['host']) ? $digest['host'] : '');
-			if (getServer() == $host) {
-				return true;
-			} else {
-				return false;
-			}
 		}
-	} else {
-		if (!$array) {
+		$array = array_change_key_case(get_headers($this->qualifyURL($url), 1));
+		$url = $this->qualifyURL($url);
+		if ($this->checkFrame($array, $url)) {
+			$this->setAPIResponse('success', 'URL approved for iFrame', 200);
+			return true;
+		} else {
+			$this->setAPIResponse('error', 'URL failed approval for iFrame', 409);
 			return false;
 		}
-		return true;
 	}
-}
-
-/*loads users from emby and returns a correctly formated policy for a new user.
-*/
-function getEmbyTemplateUserJson()
-{
-	$headers = array(
-		"Accept" => "application/json"
-	);
-	$data = array();
-	$url = $GLOBALS['embyURL'] . '/emby/Users?api_key=' . $GLOBALS['embyToken'];
-	$response = Requests::Get($url, $headers, array());
-	$response = $response->body;
-	$response = json_decode($response, true);
-	//error_Log("response ".json_encode($response));
-	writeLog('error', 'userList:' . json_encode($response), 'SYSTEM');
-	//$correct stores the template users object
-	$correct = null;
-	foreach ($response as $element) {
-		if ($element['Name'] == $GLOBALS['INVITES-EmbyTemplate']) {
-			$correct = $element;
+	
+	public function groupSelect()
+	{
+		$groups = $this->getAllGroups();
+		$select = array();
+		foreach ($groups as $key => $value) {
+			$select[] = array(
+				'name' => $value['group'],
+				'value' => $value['group_id']
+			);
 		}
+		return $select;
 	}
-	writeLog('error', 'Correct user:' . json_encode($correct), 'SYSTEM');
-	if ($correct == null) {
-		//return empty JSON if user incorectly configured template
-		return "{}";
-	}
-	//select policy section and remove possibly dangeours rows.
-	$policy = $correct['Policy'];
-	//writeLog('error', 'policy update'.$policy, 'SYSTEM');
-	unset($policy['AuthenticationProviderId']);
-	unset($policy['InvalidLoginAttemptCount']);
-	unset($policy['DisablePremiumFeatures']);
-	unset($policy['DisablePremiumFeatures']);
-	return (json_encode($policy));
-}
-
-function frameTest($url)
-{
-	$array = array_change_key_case(get_headers(qualifyURL($url), 1));
-	$url = qualifyURL($url);
-	if (checkFrame($array, $url)) {
-		return true;
-	} else {
-		return false;
-	}
-}
-
-function ping($pings)
-{
-	if (qualifyRequest($GLOBALS['pingAuth'])) {
-		$type = gettype($pings);
-		$ping = new Ping("");
-		$ping->setTtl(128);
-		$ping->setTimeout(2);
-		switch ($type) {
-			case "array":
-				$results = [];
-				foreach ($pings as $k => $v) {
-					if (strpos($v, ':') !== false) {
-						$domain = explode(':', $v)[0];
-						$port = explode(':', $v)[1];
-						$ping->setHost($domain);
-						$ping->setPort($port);
-						$latency = $ping->ping('fsockopen');
-					} else {
-						$ping->setHost($v);
-						$latency = $ping->ping();
-					}
-					if ($latency || $latency === 0) {
-						$results[$v] = $latency;
-					} else {
-						$results[$v] = false;
-					}
-				}
-				break;
-			case "string":
-				if (strpos($pings, ':') !== false) {
-					$domain = explode(':', $pings)[0];
-					$port = explode(':', $pings)[1];
-					$ping->setHost($domain);
-					$ping->setPort($port);
-					$latency = $ping->ping('fsockopen');
-				} else {
-					$ping->setHost($pings);
-					$latency = $ping->ping();
-				}
-				if ($latency || $latency === 0) {
-					$results = $latency;
-				} else {
-					$results = false;
-				}
-				break;
+	
+	public function showLogin()
+	{
+		if ($this->config['hideRegistration'] == false) {
+			return '<p><span lang="en">Don\'t have an account?</span><a href="#" class="text-primary m-l-5 to-register"><b lang="en">Sign Up</b></a></p>';
 		}
-		return $results;
 	}
-	return false;
-}
-
-function guestHash($start, $end)
-{
-	$ip = $_SERVER['REMOTE_ADDR'];
-	$ip = md5($ip);
-	return substr($ip, $start, $end);
-}
-
-function importUserButtons()
-{
-	$emptyButtons = '
-		<div class="col-md-12">
-            <div class="white-box bg-org">
-                <h3 class="box-title m-0" lang="en">Currently User import is available for Plex only.</h3> </div>
-        </div>
-	';
-	$buttons = '';
-	if (!empty($GLOBALS['plexToken'])) {
-		$buttons .= '<button class="btn bg-plex text-muted waves-effect waves-light importUsersButton" onclick="importUsers(\'plex\')" type="button"><span class="btn-label"><i class="mdi mdi-plex"></i></span><span lang="en">Import Plex Users</span></button>';
-	}
-	return ($buttons !== '') ? $buttons : $emptyButtons;
-}
-
-function settingsDocker()
-{
-	$type = ($GLOBALS['docker']) ? 'Official Docker' : 'Native';
-	return '<li><div class="bg-info"><i class="mdi mdi-flag mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Install Type</span> ' . $type . '</li>';
-}
-
-function settingsPathChecks()
-{
-	$items = '';
-	$type = (array_search(false, pathsWritable($GLOBALS['paths']))) ? 'Not Writable' : 'Writable';
-	$result = '<li class="mouse" onclick="toggleWritableFolders();"><div class="bg-info"><i class="mdi mdi-folder mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Organizr Paths</span> ' . $type . '</li>';
-	foreach (pathsWritable($GLOBALS['paths']) as $k => $v) {
-		$items .= '<li class="folders-writable hidden"><div class="bg-info"><i class="mdi mdi-folder mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">' . $k . '</span> ' . (($v) ? 'Writable' : 'Not Writable') . '</li>';
+	
+	public function checkoAuth()
+	{
+		return $this->config['plexoAuth'] && $this->config['authBackend'] == 'plex' && $this->config['authType'] !== 'internal';
 	}
-	return $result . $items;
-}
-
-function dockerUpdate()
-{
-	$dockerUpdate = null;
-	chdir('/etc/cont-init.d/');
-	if (file_exists('./30-install')) {
-		$dockerUpdate = shell_exec('./30-install');
-	} elseif (file_exists('./40-install')) {
-		$dockerUpdate = shell_exec('./40-install');
+	
+	public function checkoAuthOnly()
+	{
+		return $this->config['plexoAuth'] && $this->config['authBackend'] == 'plex' && $this->config['authType'] == 'external';
 	}
-	return $dockerUpdate ?? 'Update Failed';
-}
-
-function windowsUpdate()
-{
-	$branch = ($GLOBALS['branch'] == 'v2-master') ? '-m' : '-d';
-	ini_set('max_execution_time', 0);
-	set_time_limit(0);
-	$logFile = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'log.txt';
-	$windowsScript = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'windows-update.bat ' . $branch . ' > ' . $logFile . ' 2>&1';
-	$windowsUpdate = shell_exec($windowsScript);
-	return ($windowsUpdate) ? $windowsUpdate : 'Update Complete - check log.txt for output';
-}
-
-function checkHostPrefix($s)
-{
-	if (empty($s)) {
-		return $s;
+	
+	public function showoAuth()
+	{
+		$buttons = '';
+		if ($this->config['plexoAuth'] && $this->config['authBackend'] == 'plex' && $this->config['authType'] !== 'internal') {
+			$buttons .= '<a href="javascript:void(0)" onclick="oAuthStart(\'plex\')" class="btn btn-lg btn-block text-uppercase waves-effect waves-light bg-plex text-muted" data-toggle="tooltip" title="" data-original-title="Login with Plex"> <span>Login</span><i aria-hidden="true" class="mdi mdi-plex m-l-5"></i> </a>';
+		}
+		return ($buttons) ? '
+		<div class="panel">
+            <div class="panel-heading bg-org" id="plex-login-heading" role="tab">
+            	<a class="panel-title" data-toggle="collapse" href="#plex-login-collapse" data-parent="#login-panels" aria-expanded="false" aria-controls="organizr-login-collapse">
+	                <img class="lazyload loginTitle" data-src="plugins/images/tabs/plex.png"> &nbsp;
+                    <span class="text-uppercase fw300" lang="en">Login with Plex</span>
+            	</a>
+            </div>
+            <div class="panel-collapse collapse in" id="plex-login-collapse" aria-labelledby="plex-login-heading" role="tabpanel">
+                <div class="panel-body">
+               		<div class="row">
+			            <div class="col-xs-12 col-sm-12 col-md-12 text-center">
+			                <div class="social m-b-0">' . $buttons . '</div>
+			            </div>
+			        </div>
+               </div>
+            </div>
+        </div>
+	' : '';
 	}
-	return (substr($s, -1, 1) == '\\') ? $s : $s . '\\';
-}
-
-function analyzeIP($ip)
-{
-	if (strpos($ip, '/') !== false) {
-		$explodeIP = explode('/', $ip);
-		$prefix = $explodeIP[1];
-		$start_ip = $explodeIP[0];
-		$ip_count = 1 << (32 - $prefix);
-		$start_ip_long = ip2long($start_ip);
-		$last_ip_long = ip2long($start_ip) + $ip_count - 1;
-	} elseif (substr_count($ip, '.') == 3) {
-		$start_ip_long = ip2long($ip);
-		$last_ip_long = ip2long($ip);
+	
+	public function logoOrText()
+	{
+		if ($this->config['useLogoLogin'] == false) {
+			return '<h1>' . $this->config['title'] . '</h1>';
+		} else {
+			return '<img class="loginLogo" src="' . $this->config['loginLogo'] . '" alt="Home" />';
+		}
+	}
+	
+	public function settingsDocker()
+	{
+		$type = ($this->docker) ? 'Official Docker' : 'Native';
+		return '<li><div class="bg-info"><i class="mdi mdi-flag mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Install Type</span> ' . $type . '</li>';
+	}
+	
+	public function settingsPathChecks()
+	{
+		$paths = $this->pathsWritable($this->paths);
+		$items = '';
+		$type = (array_search(false, $paths)) ? 'Not Writable' : 'Writable';
+		$result = '<li class="mouse" onclick="toggleWritableFolders();"><div class="bg-info"><i class="mdi mdi-folder mdi-24px text-white"></i></div><span class="text-muted hidden-xs m-t-10" lang="en">Organizr Paths</span> ' . $type . '</li>';
+		foreach ($paths as $k => $v) {
+			$items .= '<li class="folders-writable hidden"><div class="bg-primary"><i class="mdi mdi-folder mdi-24px text-white"></i></div><a tabindex="0" type="button" class="btn btn-default btn-outline popover-info pull-right clipboard" lang="en" data-container="body" title="" data-toggle="popover" data-placement="left" data-content="' . $v['path'] . '" data-original-title="File Path" data-clipboard-text="' . $v['path'] . '">' . $k . '</a> ' . (($v['writable']) ? 'Writable' : 'Not Writable') . '</li>';
+		}
+		return $result . $items;
+	}
+	
+	public function pathsWritable($paths)
+	{
+		$results = array();
+		foreach ($paths as $k => $v) {
+			$results[$k] = [
+				'writable' => is_writable($v),
+				'path' => $v
+			];
+		}
+		return $results;
 	}
-	return (isset($start_ip_long) && isset($last_ip_long)) ? array('from' => $start_ip_long, 'to' => $last_ip_long) : false;
-}
-
-function authProxyRangeCheck($from, $to)
-{
-	$approved = false;
-	$userIP = ip2long($_SERVER['REMOTE_ADDR']);
-	$low = $from;
-	$high = $to;
-	if ($userIP <= $high && $low <= $userIP) {
-		$approved = true;
+	
+	public function clearTautulliTokens()
+	{
+		foreach (array_keys($_COOKIE) as $k => $v) {
+			if (strpos($v, 'tautulli') !== false) {
+				$this->coookie('delete', $v);
+			}
+		}
 	}
-	return $approved;
-}
+	
+	public function analyzeIP($ip)
+	{
+		if (strpos($ip, '/') !== false) {
+			$explodeIP = explode('/', $ip);
+			$prefix = $explodeIP[1];
+			$start_ip = $explodeIP[0];
+			$ip_count = 1 << (32 - $prefix);
+			$start_ip_long = ip2long($start_ip);
+			$last_ip_long = ip2long($start_ip) + $ip_count - 1;
+		} elseif (substr_count($ip, '.') == 3) {
+			$start_ip_long = ip2long($ip);
+			$last_ip_long = ip2long($ip);
+		}
+		return (isset($start_ip_long) && isset($last_ip_long)) ? array('from' => $start_ip_long, 'to' => $last_ip_long) : false;
+	}
+	
+	public function authProxyRangeCheck($from, $to)
+	{
+		$approved = false;
+		$userIP = ip2long($_SERVER['REMOTE_ADDR']);
+		$low = $from;
+		$high = $to;
+		if ($userIP <= $high && $low <= $userIP) {
+			$approved = true;
+		}
+		return $approved;
+	}
+}

+ 1 - 327
api/functions/ping.class.php

@@ -1,328 +1,2 @@
 <?php
-
-class Ping
-{
-	
-	private $host;
-	private $ttl;
-	private $timeout;
-	private $port = 80;
-	private $data = 'Ping';
-	private $commandOutput;
-	
-	/**
-	 * Called when the Ping object is created.
-	 *
-	 * @param string $host
-	 *   The host to be pinged.
-	 * @param int $ttl
-	 *   Time-to-live (TTL) (You may get a 'Time to live exceeded' error if this
-	 *   value is set too low. The TTL value indicates the scope or range in which
-	 *   a packet may be forwarded. By convention:
-	 *     - 0 = same host
-	 *     - 1 = same subnet
-	 *     - 32 = same site
-	 *     - 64 = same region
-	 *     - 128 = same continent
-	 *     - 255 = unrestricted
-	 * @param int $timeout
-	 *   Timeout (in seconds) used for ping and fsockopen().
-	 * @throws \Exception if the host is not set.
-	 */
-	public function __construct($host, $ttl = 255, $timeout = 10)
-	{
-		if (!isset($host)) {
-			throw new \Exception("Error: Host name not supplied.");
-		}
-		$this->host = $host;
-		$this->ttl = $ttl;
-		$this->timeout = $timeout;
-	}
-	
-	/**
-	 * Set the ttl (in hops).
-	 *
-	 * @param int $ttl
-	 *   TTL in hops.
-	 */
-	public function setTtl($ttl)
-	{
-		$this->ttl = $ttl;
-	}
-	
-	/**
-	 * Get the ttl.
-	 *
-	 * @return int
-	 *   The current ttl for Ping.
-	 */
-	public function getTtl()
-	{
-		return $this->ttl;
-	}
-	
-	/**
-	 * Set the timeout.
-	 *
-	 * @param int $timeout
-	 *   Time to wait in seconds.
-	 */
-	public function setTimeout($timeout)
-	{
-		$this->timeout = $timeout;
-	}
-	
-	/**
-	 * Get the timeout.
-	 *
-	 * @return int
-	 *   Current timeout for Ping.
-	 */
-	public function getTimeout()
-	{
-		return $this->timeout;
-	}
-	
-	/**
-	 * Set the host.
-	 *
-	 * @param string $host
-	 *   Host name or IP address.
-	 */
-	public function setHost($host)
-	{
-		$this->host = $host;
-	}
-	
-	/**
-	 * Get the host.
-	 *
-	 * @return string
-	 *   The current hostname for Ping.
-	 */
-	public function getHost()
-	{
-		return $this->host;
-	}
-	
-	/**
-	 * Set the port (only used for fsockopen method).
-	 *
-	 * Since regular pings use ICMP and don't need to worry about the concept of
-	 * 'ports', this is only used for the fsockopen method, which pings servers by
-	 * checking port 80 (by default).
-	 *
-	 * @param int $port
-	 *   Port to use for fsockopen ping (defaults to 80 if not set).
-	 */
-	public function setPort($port)
-	{
-		$this->port = $port;
-	}
-	
-	/**
-	 * Get the port (only used for fsockopen method).
-	 *
-	 * @return int
-	 *   The port used by fsockopen pings.
-	 */
-	public function getPort()
-	{
-		return $this->port;
-	}
-	
-	/**
-	 * Return the command output when method=exec.
-	 * @return string
-	 */
-	public function getCommandOutput()
-	{
-		return $this->commandOutput;
-	}
-	
-	/**
-	 * Matches an IP on command output and returns.
-	 * @return string
-	 */
-	public function getIpAddress()
-	{
-		$out = array();
-		if (preg_match('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', $this->commandOutput, $out)) {
-			return $out[0];
-		}
-		return null;
-	}
-	
-	/**
-	 * Ping a host.
-	 *
-	 * @param string $method
-	 *   Method to use when pinging:
-	 *     - exec (default): Pings through the system ping command. Fast and
-	 *       robust, but a security risk if you pass through user-submitted data.
-	 *     - fsockopen: Pings a server on port 80.
-	 *     - socket: Creates a RAW network socket. Only usable in some
-	 *       environments, as creating a SOCK_RAW socket requires root privileges.
-	 *
-	 * @throws InvalidArgumentException if $method is not supported.
-	 *
-	 * @return mixed
-	 *   Latency as integer, in ms, if host is reachable or FALSE if host is down.
-	 */
-	public function ping($method = 'exec')
-	{
-		$latency = false;
-		switch ($method) {
-			case 'exec':
-				$latency = $this->pingExec();
-				break;
-			case 'fsockopen':
-				$latency = $this->pingFsockopen();
-				break;
-			case 'socket':
-				$latency = $this->pingSocket();
-				break;
-			default:
-				throw new \InvalidArgumentException('Unsupported ping method.');
-		}
-		// Return the latency.
-		return $latency;
-	}
-	
-	/**
-	 * The exec method uses the possibly insecure exec() function, which passes
-	 * the input to the system. This is potentially VERY dangerous if you pass in
-	 * any user-submitted data. Be SURE you sanitize your inputs!
-	 *
-	 * @return int
-	 *   Latency, in ms.
-	 */
-	private function pingExec()
-	{
-		$latency = false;
-		$ttl = escapeshellcmd($this->ttl);
-		$timeout = escapeshellcmd($this->timeout);
-		$host = escapeshellcmd($this->host);
-		// Exec string for Windows-based systems.
-		if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
-			// -n = number of pings; -i = ttl; -w = timeout (in milliseconds).
-			$exec_string = 'ping -n 1 -i ' . $ttl . ' -w ' . ($timeout * 1000) . ' ' . $host;
-		} // Exec string for Darwin based systems (OS X).
-		else if (strtoupper(PHP_OS) === 'DARWIN') {
-			// -n = numeric output; -c = number of pings; -m = ttl; -t = timeout.
-			$exec_string = 'ping -n -c 1 -m ' . $ttl . ' -t ' . $timeout . ' ' . $host;
-		} // Exec string for other UNIX-based systems (Linux).
-		else {
-			// -n = numeric output; -c = number of pings; -t = ttl; -W = timeout
-			$exec_string = 'ping -n -c 1 -t ' . $ttl . ' -W ' . $timeout . ' ' . $host . ' 2>&1';
-		}
-		exec($exec_string, $output, $return);
-		// Strip empty lines and reorder the indexes from 0 (to make results more
-		// uniform across OS versions).
-		$this->commandOutput = implode($output, '');
-		$output = array_values(array_filter($output));
-		// If the result line in the output is not empty, parse it.
-		if (!empty($output[1])) {
-			// Search for a 'time' value in the result line.
-			$response = preg_match("/time(?:=|<)(?<time>[\.0-9]+)(?:|\s)ms/", $output[1], $matches);
-			// If there's a result and it's greater than 0, return the latency.
-			if ($response > 0 && isset($matches['time'])) {
-				$latency = round($matches['time'], 2);
-			}
-		}
-		return $latency;
-	}
-	
-	/**
-	 * The fsockopen method simply tries to reach the host on a port. This method
-	 * is often the fastest, but not necessarily the most reliable. Even if a host
-	 * doesn't respond, fsockopen may still make a connection.
-	 *
-	 * @return int
-	 *   Latency, in ms.
-	 */
-	private function pingFsockopen()
-	{
-		$start = microtime(true);
-		// fsockopen prints a bunch of errors if a host is unreachable. Hide those
-		// irrelevant errors and deal with the results instead.
-		$fp = @fsockopen($this->host, $this->port, $errno, $errstr, $this->timeout);
-		if (!$fp) {
-			$latency = false;
-		} else {
-			$latency = microtime(true) - $start;
-			$latency = round($latency * 1000, 2);
-		}
-		return $latency;
-	}
-	
-	/**
-	 * The socket method uses raw network packet data to try sending an ICMP ping
-	 * packet to a server, then measures the response time. Using this method
-	 * requires the script to be run with root privileges, though, so this method
-	 * only works reliably on Windows systems and on Linux servers where the
-	 * script is not being run as a web user.
-	 *
-	 * @return int
-	 *   Latency, in ms.
-	 */
-	private function pingSocket()
-	{
-		// Create a package.
-		$type = "\x08";
-		$code = "\x00";
-		$checksum = "\x00\x00";
-		$identifier = "\x00\x00";
-		$seq_number = "\x00\x00";
-		$package = $type . $code . $checksum . $identifier . $seq_number . $this->data;
-		// Calculate the checksum.
-		$checksum = $this->calculateChecksum($package);
-		// Finalize the package.
-		$package = $type . $code . $checksum . $identifier . $seq_number . $this->data;
-		// Create a socket, connect to server, then read socket and calculate.
-		if ($socket = socket_create(AF_INET, SOCK_RAW, 1)) {
-			socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array(
-				'sec' => 10,
-				'usec' => 0,
-			));
-			// Prevent errors from being printed when host is unreachable.
-			@socket_connect($socket, $this->host, null);
-			$start = microtime(true);
-			// Send the package.
-			@socket_send($socket, $package, strlen($package), 0);
-			if (socket_read($socket, 255) !== false) {
-				$latency = microtime(true) - $start;
-				$latency = round($latency * 1000, 2);
-			} else {
-				$latency = false;
-			}
-		} else {
-			$latency = false;
-		}
-		// Close the socket.
-		socket_close($socket);
-		return $latency;
-	}
-	
-	/**
-	 * Calculate a checksum.
-	 *
-	 * @param string $data
-	 *   Data for which checksum will be calculated.
-	 *
-	 * @return string
-	 *   Binary string checksum of $data.
-	 */
-	private function calculateChecksum($data)
-	{
-		if (strlen($data) % 2) {
-			$data .= "\x00";
-		}
-		$bit = unpack('n*', $data);
-		$sum = array_sum($bit);
-		while ($sum >> 16) {
-			$sum = ($sum >> 16) + ($sum & 0xffff);
-		}
-		return pack('n*', ~$sum);
-	}
-}
+/* Depreciated */

+ 2 - 77
api/functions/plugin-functions.php

@@ -1,81 +1,6 @@
 <?php
-function installPlugin($plugin)
-{
-	$name = $plugin['data']['plugin']['name'];
-	$version = $plugin['data']['plugin']['version'];
-	foreach ($plugin['data']['plugin']['downloadList'] as $k => $v) {
-		$file = array(
-			'from' => $v['githubPath'],
-			'to' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $GLOBALS['root'] . $v['path'] . $v['fileName']),
-			'path' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $GLOBALS['root'] . $v['path'])
-		);
-		if (!downloadFileToPath($file['from'], $file['to'], $file['path'])) {
-			writeLog('error', 'Plugin Function -  Downloaded File Failed  for: ' . $v['githubPath'], $GLOBALS['organizrUser']['username']);
-			return false;
-		}
-	}
-	if ($GLOBALS['installedPlugins'] !== '') {
-		$installedPlugins = explode('|', $GLOBALS['installedPlugins']);
-		foreach ($installedPlugins as $k => $v) {
-			$plugins = explode(':', $v);
-			$installedPluginsList[$plugins[0]] = $plugins[1];
-		}
-		if (isset($installedPluginsList[$name])) {
-			$installedPluginsList[$name] = $version;
-			$installedPluginsNew = '';
-			foreach ($installedPluginsList as $k => $v) {
-				if ($installedPluginsNew == '') {
-					$installedPluginsNew .= $k . ':' . $v;
-				} else {
-					$installedPluginsNew .= '|' . $k . ':' . $v;
-				}
-			}
-		} else {
-			$installedPluginsNew = $GLOBALS['installedPlugins'] . '|' . $name . ':' . $version;
-		}
-	} else {
-		$installedPluginsNew = $name . ':' . $version;
-	}
-	updateConfig(array('installedPlugins' => $installedPluginsNew));
-	return 'Success!@!' . $installedPluginsNew;
-}
 
-function removePlugin($plugin)
+trait PluginFunctions
 {
-	$name = $plugin['data']['plugin']['name'];
-	$version = $plugin['data']['plugin']['version'];
-	foreach ($plugin['data']['plugin']['downloadList'] as $k => $v) {
-		$file = array(
-			'from' => $v['githubPath'],
-			'to' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $GLOBALS['root'] . $v['path'] . $v['fileName']),
-			'path' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $GLOBALS['root'] . $v['path'])
-		);
-		if (!rrmdir($file['to'])) {
-			writeLog('error', 'Plugin Function -  Remove File Failed  for: ' . $v['githubPath'], $GLOBALS['organizrUser']['username']);
-			return false;
-		}
-	}
-	if ($GLOBALS['installedPlugins'] !== '') {
-		$installedPlugins = explode('|', $GLOBALS['installedPlugins']);
-		foreach ($installedPlugins as $k => $v) {
-			$plugins = explode(':', $v);
-			$installedPluginsList[$plugins[0]] = $plugins[1];
-		}
-		if (isset($installedPluginsList[$name])) {
-			$installedPluginsNew = '';
-			foreach ($installedPluginsList as $k => $v) {
-				if ($k !== $name) {
-					if ($installedPluginsNew == '') {
-						$installedPluginsNew .= $k . ':' . $v;
-					} else {
-						$installedPluginsNew .= '|' . $k . ':' . $v;
-					}
-				}
-			}
-		}
-	} else {
-		$installedPluginsNew = '';
-	}
-	updateConfig(array('installedPlugins' => $installedPluginsNew));
-	return 'Success!@!' . $installedPluginsNew;
+
 }

+ 96 - 80
api/functions/sso-functions.php

@@ -1,92 +1,108 @@
 <?php
-function ssoCheck($username, $password, $token = null)
+
+trait SSOFunctions
 {
-	$test = '';
-	if ($GLOBALS['ssoPlex'] && $token) {
-		coookie('set', 'mpt', $token, $GLOBALS['rememberMeDays'], false);
-	}
-	if ($GLOBALS['ssoOmbi']) {
-		$ombiToken = getOmbiToken($username, $password, $token);
-		if ($ombiToken) {
-			coookie('set', 'Auth', $ombiToken, $GLOBALS['rememberMeDays'], false);
+	
+	public function ssoCheck($username, $password, $token = null)
+	{
+		if ($this->config['ssoPlex'] && $token) {
+			$this->coookie('set', 'mpt', $token, $this->config['rememberMeDays'], false);
 		}
-	}
-	if ($GLOBALS['ssoTautulli']) {
-		$tautulliToken = getTautulliToken($username, $password, $token);
-		if ($tautulliToken) {
-			foreach ($tautulliToken as $key => $value) {
-				coookie('set', 'tautulli_token_' . $value['uuid'], $value['token'], $GLOBALS['rememberMeDays'], true, $value['path']);
+		if ($this->config['ssoOmbi']) {
+			$fallback = ($this->config['ombiFallbackUser'] !== '' && $this->config['ombiFallbackPassword'] !== '');
+			$ombiToken = $this->getOmbiToken($username, $password, $token, $fallback);
+			if ($ombiToken) {
+				$this->coookie('set', 'Auth', $ombiToken, $this->config['rememberMeDays'], false);
+			}
+		}
+		if ($this->config['ssoTautulli']) {
+			$tautulliToken = $this->getTautulliToken($username, $password, $token);
+			if ($tautulliToken) {
+				foreach ($tautulliToken as $key => $value) {
+					$this->coookie('set', 'tautulli_token_' . $value['uuid'], $value['token'], $this->config['rememberMeDays'], true, $value['path']);
+				}
 			}
 		}
+		return true;
 	}
-	return true;
-}
-
-function getOmbiToken($username, $password, $oAuthToken = null)
-{
-	$token = null;
-	try {
-		$url = qualifyURL($GLOBALS['ombiURL']);
-		$headers = array(
-			"Accept" => "application/json",
-			"Content-Type" => "application/json"
-		);
-		$data = array(
-			"username" => ($oAuthToken ? "" : $username),
-			"password" => ($oAuthToken ? "" : $password),
-			"rememberMe" => "true",
-			"plexToken" => $oAuthToken
-		);
-		$endpoint = ($oAuthToken) ? '/api/v1/Token/plextoken' : '/api/v1/Token';
-		$options = (localURL($url)) ? array('verify' => false) : array();
-		$response = Requests::post($url . $endpoint, $headers, json_encode($data), $options);
-		if ($response->success) {
-			$token = json_decode($response->body, true)['access_token'];
-			writeLog('success', 'Ombi Token Function - Grabbed token.', $username);
+	
+	public function getOmbiToken($username, $password, $oAuthToken = null, $fallback = false)
+	{
+		$token = null;
+		try {
+			$url = $this->qualifyURL($this->config['ombiURL']);
+			$headers = array(
+				"Accept" => "application/json",
+				"Content-Type" => "application/json"
+			);
+			$data = array(
+				"username" => ($oAuthToken ? "" : $username),
+				"password" => ($oAuthToken ? "" : $password),
+				"rememberMe" => "true",
+				"plexToken" => $oAuthToken
+			);
+			$endpoint = ($oAuthToken) ? '/api/v1/Token/plextoken' : '/api/v1/Token';
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::post($url . $endpoint, $headers, json_encode($data), $options);
+			if ($response->success) {
+				$token = json_decode($response->body, true)['access_token'];
+				$this->writeLog('success', 'Ombi Token Function - Grabbed token.', $username);
+			} else {
+				if ($fallback) {
+					$this->writeLog('error', 'Ombi Token Function - Ombi did not return Token - Will retry using fallback credentials', $username);
+				} else {
+					$this->writeLog('error', 'Ombi Token Function - Ombi did not return Token', $username);
+				}
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Ombi Token Function - Error: ' . $e->getMessage(), $username);
+		}
+		if ($token) {
+			return $token;
+		} elseif ($fallback) {
+			return $this->getOmbiToken($this->config['ombiFallbackUser'], $this->decrypt($this->config['ombiFallbackPassword']), null, false);
 		} else {
-			writeLog('error', 'Ombi Token Function - Ombi did not return Token', $username);
+			return false;
 		}
-	} catch (Requests_Exception $e) {
-		writeLog('error', 'Ombi Token Function - Error: ' . $e->getMessage(), $username);
-	};
-	return ($token) ? $token : false;
-}
-
-function getTautulliToken($username, $password, $plexToken = null)
-{
-	$token = null;
-	$tautulliURLList = explode(',', $GLOBALS['tautulliURL']);
-	if (count($tautulliURLList) !== 0) {
-		foreach ($tautulliURLList as $key => $value) {
-			try {
-				$url = qualifyURL($value);
-				$headers = array(
-					"Accept" => "application/json",
-					"Content-Type" => "application/x-www-form-urlencoded",
-					"User-Agent" => isset($_SERVER ['HTTP_USER_AGENT']) ? $_SERVER ['HTTP_USER_AGENT'] : null
-				);
-				$data = array(
-					"username" => ($plexToken ? "" : $username),
-					"password" => ($plexToken ? "" : $password),
-					"token" => $plexToken,
-					"remember_me" => 1,
-				);
-				$options = (localURL($url)) ? array('verify' => false) : array();
-				$response = Requests::post($url . '/auth/signin', $headers, $data, $options);
-				if ($response->success) {
-					$qualifiedURL = qualifyURL($url, true);
-					$path = ($qualifiedURL['path']) ? $qualifiedURL['path'] : '/';
-					$token[$key]['token'] = json_decode($response->body, true)['token'];
-					$token[$key]['uuid'] = json_decode($response->body, true)['uuid'];
-					$token[$key]['path'] = $path;
-					writeLog('success', 'Tautulli Token Function - Grabbed token from: ' . $url, $username);
-				} else {
-					writeLog('error', 'Tautulli Token Function - Error on URL: ' . $url, $username);
+	}
+	
+	public function getTautulliToken($username, $password, $plexToken = null)
+	{
+		$token = null;
+		$tautulliURLList = explode(',', $this->config['tautulliURL']);
+		if (count($tautulliURLList) !== 0) {
+			foreach ($tautulliURLList as $key => $value) {
+				try {
+					$url = $this->qualifyURL($value);
+					$headers = array(
+						"Accept" => "application/json",
+						"Content-Type" => "application/x-www-form-urlencoded",
+						"User-Agent" => isset($_SERVER ['HTTP_USER_AGENT']) ? $_SERVER ['HTTP_USER_AGENT'] : null
+					);
+					$data = array(
+						"username" => ($plexToken ? "" : $username),
+						"password" => ($plexToken ? "" : $password),
+						"token" => $plexToken,
+						"remember_me" => 1,
+					);
+					$options = ($this->localURL($url)) ? array('verify' => false) : array();
+					$response = Requests::post($url . '/auth/signin', $headers, $data, $options);
+					if ($response->success) {
+						$qualifiedURL = $this->qualifyURL($url, true);
+						$path = ($qualifiedURL['path']) ? $qualifiedURL['path'] : '/';
+						$token[$key]['token'] = json_decode($response->body, true)['token'];
+						$token[$key]['uuid'] = json_decode($response->body, true)['uuid'];
+						$token[$key]['path'] = $path;
+						$this->writeLog('success', 'Tautulli Token Function - Grabbed token from: ' . $url, $username);
+					} else {
+						$this->writeLog('error', 'Tautulli Token Function - Error on URL: ' . $url, $username);
+					}
+				} catch (Requests_Exception $e) {
+					$this->writeLog('error', 'Tautulli Token Function - Error: [' . $url . ']' . $e->getMessage(), $username);
 				}
-			} catch (Requests_Exception $e) {
-				writeLog('error', 'Tautulli Token Function - Error: [' . $url . ']' . $e->getMessage(), $username);
-			};
+			}
 		}
+		return ($token) ? $token : false;
 	}
-	return ($token) ? $token : false;
+	
 }

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

@@ -1,148 +1,6 @@
 <?php
-// ===================================
-// Organizr Version
-$GLOBALS['installedVersion'] = '2.0.650';
-// ===================================
-// Quick php Version check
-$GLOBALS['minimumPHP'] = '7.1.3';
-if (!(version_compare(PHP_VERSION, $GLOBALS['minimumPHP']) >= 0)) {
-	die('Organizr needs PHP Version: ' . $GLOBALS['minimumPHP'] . '<br/> You have PHP Version: ' . PHP_VERSION);
-}
-// Set GLOBALS from config file
-$GLOBALS['userConfigPath'] = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.php';
-$GLOBALS['defaultConfigPath'] = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'default.php';
-$GLOBALS['currentTime'] = gmdate("Y-m-d\TH:i:s\Z");
-$GLOBALS['docker'] = (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Docker.txt')) ? true : false;
-if ($GLOBALS['docker']) {
-	$getCommit = file_get_contents(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Github.txt');
-	$getCommit = (empty($getCommit)) ? 'n/a' : $getCommit;
-	$GLOBALS['quickCommit'] = $getCommit;
-}
-$GLOBALS['fileHash'] = (isset($GLOBALS['quickCommit'])) ? $GLOBALS['quickCommit'] : $GLOBALS['installedVersion'];
-$GLOBALS['quickConfig'] = (file_exists($GLOBALS['userConfigPath'])) ? loadConfigOnce($GLOBALS['userConfigPath']) : null;
-$GLOBALS['organizrIndexTitle'] = (isset($GLOBALS['quickConfig']['title'])) ? $GLOBALS['quickConfig']['title'] : 'Organizr v2';
-$GLOBALS['organizrIndexDescription'] = (isset($GLOBALS['quickConfig']['description'])) ? $GLOBALS['quickConfig']['description'] : 'Organizr v2';
-// Quick function for plugins
-function pluginFiles($type)
-{
-	$files = '';
-	switch ($type) {
-		case 'js':
-			foreach (glob(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . "*.js") as $filename) {
-				$files .= '<script src="api/plugins/js/' . basename($filename) . '?v=' . $GLOBALS['fileHash'] . '" defer="true"></script>';
-			}
-			break;
-		case 'css':
-			foreach (glob(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'css' . DIRECTORY_SEPARATOR . "*.css") as $filename) {
-				$files .= '<link href="api/plugins/css/' . basename($filename) . '?v=' . $GLOBALS['fileHash'] . '" rel="stylesheet">';
-			}
-			break;
-		default:
-			break;
-	}
-	return $files;
-}
-
-function loadConfigOnce($path = null)
-{
-	$path = ($path) ? $path : $GLOBALS['userConfigPath'];
-	if (!is_file($path)) {
-		return null;
-	} else {
-		return (array)call_user_func(function () use ($path) {
-			return include($path);
-		});
-	}
-}
-
-function formKey($script = true)
-{
-	if (isset($GLOBALS['quickConfig']['organizrHash'])) {
-		if ($GLOBALS['quickConfig']['organizrHash'] !== '') {
-			$hash = password_hash(substr($GLOBALS['quickConfig']['organizrHash'], 2, 10), PASSWORD_BCRYPT);
-			return ($script) ? '<script>local("s","formKey","' . $hash . '");</script>' : $hash;
-		}
-	}
-}
-
-function checkFormKey($formKey = '')
-{
-	return password_verify(substr($GLOBALS['quickConfig']['organizrHash'], 2, 10), $formKey);
-}
-
-function favIcons()
-{
-	$favicon = '
-	<link rel="apple-touch-icon" sizes="180x180" href="plugins/images/favicon/apple-touch-icon.png">
-	<link rel="icon" type="image/png" sizes="32x32" href="plugins/images/favicon/favicon-32x32.png">
-	<link rel="icon" type="image/png" sizes="16x16" href="plugins/images/favicon/favicon-16x16.png">
-	<link rel="manifest" href="plugins/images/favicon/site.webmanifest">
-	<link rel="mask-icon" href="plugins/images/favicon/safari-pinned-tab.svg" color="#5bbad5">
-	<link rel="shortcut icon" href="plugins/images/favicon/favicon.ico">
-	<meta name="msapplication-TileColor" content="#da532c">
-	<meta name="msapplication-TileImage" content="plugins/images/favicon/mstile-144x144.png">
-	<meta name="msapplication-config" content="plugins/images/favicon/browserconfig.xml">
-	<meta name="theme-color" content="#ffffff">
-	';
-	if (isset($GLOBALS['quickConfig']['favIcon'])) {
-		if ($GLOBALS['quickConfig']['favIcon'] !== '') {
-			$favicon = $GLOBALS['quickConfig']['favIcon'];
-		}
-	}
-	return $favicon;
-}
 
-function languagePacks($encode = false)
+trait StaticFunctions
 {
-	$files = array();
-	foreach (glob(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . 'langpack' . DIRECTORY_SEPARATOR . "*.json") as $filename) {
-		if (strpos(basename($filename), '[') !== false) {
-			$explode = explode('[', basename($filename));
-			$files[] = array(
-				'filename' => basename($filename),
-				'code' => $explode[0],
-				'language' => matchBrackets(basename($filename))
-			);
-		}
-	}
-	usort($files, function ($a, $b) {
-		return $a['language'] <=> $b['language'];
-	});
-	return ($encode) ? json_encode($files) : $files;
-}
 
-function matchBrackets($text, $brackets = 's')
-{
-	switch ($brackets) {
-		case 's':
-		case 'square':
-			$pattern = '#\[(.*?)\]#';
-			break;
-		case 'c':
-		case 'curly':
-			$pattern = '#\((.*?)\)#';
-			break;
-		default:
-			return null;
-	}
-	preg_match($pattern, $text, $match);
-	return $match[1];
-}
-
-function googleTracking()
-{
-	if (isset($GLOBALS['quickConfig']['gaTrackingID'])) {
-		if ($GLOBALS['quickConfig']['gaTrackingID'] !== '') {
-			return '
-				<script async src="https://www.googletagmanager.com/gtag/js?id=' . $GLOBALS['quickConfig']['gaTrackingID'] . '"></script>
-    			<script>
-				    window.dataLayer = window.dataLayer || [];
-				    function gtag(){dataLayer.push(arguments);}
-				    gtag("js", new Date());
-				    gtag("config","' . $GLOBALS['quickConfig']['gaTrackingID'] . '");
-    			</script>
-			';
-		}
-	}
-	return null;
 }

+ 1 - 80
api/functions/theme-functions.php

@@ -1,81 +1,2 @@
 <?php
-function installTheme($theme)
-{
-	$name = $theme['data']['theme']['name'];
-	$version = $theme['data']['theme']['version'];
-	foreach ($theme['data']['theme']['downloadList'] as $k => $v) {
-		$file = array(
-			'from' => $v['githubPath'],
-			'to' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $GLOBALS['root'] . $v['path'] . $v['fileName']),
-			'path' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $GLOBALS['root'] . $v['path'])
-		);
-		if (!downloadFileToPath($file['from'], $file['to'], $file['path'])) {
-			writeLog('error', 'Theme Function -  Downloaded File Failed  for: ' . $v['githubPath'], $GLOBALS['organizrUser']['username']);
-			return false;
-		}
-	}
-	if ($GLOBALS['installedThemes'] !== '') {
-		$installedThemes = explode('|', $GLOBALS['installedThemes']);
-		foreach ($installedThemes as $k => $v) {
-			$themes = explode(':', $v);
-			$installedThemesList[$themes[0]] = $themes[1];
-		}
-		if (isset($installedThemesList[$name])) {
-			$installedThemesList[$name] = $version;
-			$installedThemesNew = '';
-			foreach ($installedThemesList as $k => $v) {
-				if ($installedThemesNew == '') {
-					$installedThemesNew .= $k . ':' . $v;
-				} else {
-					$installedThemesNew .= '|' . $k . ':' . $v;
-				}
-			}
-		} else {
-			$installedThemesNew = $GLOBALS['installedThemes'] . '|' . $name . ':' . $version;
-		}
-	} else {
-		$installedThemesNew = $name . ':' . $version;
-	}
-	updateConfig(array('installedThemes' => $installedThemesNew));
-	return 'Success!@!' . $installedThemesNew;
-}
-
-function removeTheme($theme)
-{
-	$name = $theme['data']['theme']['name'];
-	$version = $theme['data']['theme']['version'];
-	foreach ($theme['data']['theme']['downloadList'] as $k => $v) {
-		$file = array(
-			'from' => $v['githubPath'],
-			'to' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $GLOBALS['root'] . $v['path'] . $v['fileName']),
-			'path' => str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $GLOBALS['root'] . $v['path'])
-		);
-		if (!rrmdir($file['to'])) {
-			writeLog('error', 'Theme Function -  Remove File Failed  for: ' . $v['githubPath'], $GLOBALS['organizrUser']['username']);
-			return false;
-		}
-	}
-	if ($GLOBALS['installedThemes'] !== '') {
-		$installedThemes = explode('|', $GLOBALS['installedThemes']);
-		foreach ($installedThemes as $k => $v) {
-			$themes = explode(':', $v);
-			$installedThemesList[$themes[0]] = $themes[1];
-		}
-		if (isset($installedThemesList[$name])) {
-			$installedThemesNew = '';
-			foreach ($installedThemesList as $k => $v) {
-				if ($k !== $name) {
-					if ($installedThemesNew == '') {
-						$installedThemesNew .= $k . ':' . $v;
-					} else {
-						$installedThemesNew .= '|' . $k . ':' . $v;
-					}
-				}
-			}
-		}
-	} else {
-		$installedThemesNew = '';
-	}
-	updateConfig(array('installedThemes' => $installedThemesNew));
-	return 'Success!@!' . $installedThemesNew;
-}
+/* Depreciated */

+ 47 - 170
api/functions/token-functions.php

@@ -1,177 +1,54 @@
 <?php
-function jwtParse($token)
-{
-	try {
-		$result = array();
-		$result['valid'] = false;
-		// Check Token with JWT
-		// Set key
-		if (!isset($GLOBALS['organizrHash'])) {
-			return null;
-		}
-		$key = $GLOBALS['organizrHash'];
-		// SHA256 Encryption
-		$signer = new Lcobucci\JWT\Signer\Hmac\Sha256();
-		$jwttoken = (new Lcobucci\JWT\Parser())->parse((string)$token); // Parses from a string
-		$jwttoken->getHeaders(); // Retrieves the token header
-		$jwttoken->getClaims(); // Retrieves the token claims
-		// Start Validation
-		if ($jwttoken->verify($signer, $key)) {
-			$data = new Lcobucci\JWT\ValidationData(); // It will use the current time to validate (iat, nbf and exp)
-			$data->setIssuer('Organizr');
-			$data->setAudience('Organizr');
-			if ($jwttoken->validate($data)) {
-				$result['valid'] = true;
-				$result['username'] = $jwttoken->getClaim('username');
-				$result['group'] = $jwttoken->getClaim('group');
-				$result['groupID'] = $jwttoken->getClaim('groupID');
-				$result['userID'] = $jwttoken->getClaim('userID');
-				$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');
-			}
-		}
-		if ($result['valid'] == true) {
-			return $result;
-		} else {
-			return false;
-		}
-	} catch (\RunException $e) {
-		return false;
-	} catch (\OutOfBoundsException $e) {
-		return false;
-	} catch (\RunTimeException $e) {
-		return false;
-	} catch (\InvalidArgumentException $e) {
-		return false;
-	}
-}
-
-function createToken($username, $email, $image, $group, $groupID, $key, $days = 1)
-{
-	if (!isset($GLOBALS['dbLocation']) || !isset($GLOBALS['dbName'])) {
-		return false;
-	}
-	$days = ($days > 365) ? 365 : $days;
-	//Quick get user ID
-	try {
-		$database = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-		]);
-		$result = $database->fetch('SELECT * FROM users WHERE username = ? COLLATE NOCASE OR email = ? COLLATE NOCASE', $username, $email);
-		// Create JWT
-		// Set key
-		// SHA256 Encryption
-		$signer = new Lcobucci\JWT\Signer\Hmac\Sha256();
-		// Start Builder
-		$jwttoken = (new Lcobucci\JWT\Builder())->setIssuer('Organizr')// Configures the issuer (iss claim)
-		->setAudience('Organizr')// Configures the audience (aud claim)
-		->setId('4f1g23a12aa', true)// Configures the id (jti claim), replicating as a header item
-		->setIssuedAt(time())// Configures the time that the token was issue (iat claim)
-		->setExpiration(time() + (86400 * $days))// Configures the expiration time of the token (exp claim)
-		->set('username', $result['username'])// Configures a new claim, called "username"
-		->set('group', $result['group'])// Configures a new claim, called "group"
-		->set('groupID', $result['group_id'])// Configures a new claim, called "groupID"
-		->set('email', $result['email'])// Configures a new claim, called "email"
-		->set('image', $result['image'])// Configures a new claim, called "image"
-		->set('userID', $result['id'])// Configures a new claim, called "image"
-		->sign($signer, $key)// creates a signature using "testing" as key
-		->getToken(); // Retrieves the generated token
-		$jwttoken->getHeaders(); // Retrieves the token headers
-		$jwttoken->getClaims(); // Retrieves the token claims
-		coookie('set', $GLOBALS['cookieName'], $jwttoken, $days);
-		// Add token to DB
-		$addToken = [
-			'token' => (string)$jwttoken,
-			'user_id' => $result['id'],
-			'created' => $GLOBALS['currentTime'],
-			'browser' => isset($_SERVER ['HTTP_USER_AGENT']) ? $_SERVER ['HTTP_USER_AGENT'] : null,
-			'ip' => userIP(),
-			'expires' => gmdate("Y-m-d\TH:i:s\Z", time() + (86400 * $days))
-		];
-		$database->query('INSERT INTO [tokens]', $addToken);
-		return $jwttoken;
-	} catch (Dibi\Exception $e) {
-		writeLog('error', 'Token Error: ' . $e, 'SYSTEM');
-		return false;
-	}
-}
 
-function validateToken($token, $global = false)
+trait TokenFunctions
 {
-	// Validate script
-	$userInfo = jwtParse($token);
-	$validated = $userInfo ? true : false;
-	if ($global == true) {
-		if ($validated == true) {
-			try {
-				$database = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$all = $database->fetchAll('SELECT * FROM `tokens` WHERE `user_id` = ? AND `expires` > ?', $userInfo['userID'], $GLOBALS['currentTime']);
-				$tokenCheck = (searchArray($all, 'token', $token) !== false);
-				if (!$tokenCheck) {
-					// Delete cookie & reload page
-					coookie('delete', $GLOBALS['cookieName']);
-					$GLOBALS['organizrUser'] = false;
+	public function jwtParse($token)
+	{
+		try {
+			$result = array();
+			$result['valid'] = false;
+			// Check Token with JWT
+			// Set key
+			if (!isset($this->config['organizrHash'])) {
+				return null;
+			}
+			$key = $this->config['organizrHash'];
+			// SHA256 Encryption
+			$signer = new Lcobucci\JWT\Signer\Hmac\Sha256();
+			$jwttoken = (new Lcobucci\JWT\Parser())->parse((string)$token); // Parses from a string
+			$jwttoken->getHeaders(); // Retrieves the token header
+			$jwttoken->getClaims(); // Retrieves the token claims
+			// Start Validation
+			if ($jwttoken->verify($signer, $key)) {
+				$data = new Lcobucci\JWT\ValidationData(); // It will use the current time to validate (iat, nbf and exp)
+				$data->setIssuer('Organizr');
+				$data->setAudience('Organizr');
+				if ($jwttoken->validate($data)) {
+					$result['valid'] = true;
+					$result['username'] = $jwttoken->getClaim('username');
+					$result['group'] = $jwttoken->getClaim('group');
+					$result['groupID'] = $jwttoken->getClaim('groupID');
+					$result['userID'] = $jwttoken->getClaim('userID');
+					$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');
 				}
-				$result = $database->fetch('SELECT * FROM users WHERE id = ?', $userInfo['userID']);
-				$GLOBALS['organizrUser'] = array(
-					"token" => $token,
-					"tokenDate" => $userInfo['tokenDate'],
-					"tokenExpire" => $userInfo['tokenExpire'],
-					"username" => $result['username'],
-					"uid" => guestHash(0, 5),
-					"group" => $result['group'],
-					"groupID" => $result['group_id'],
-					"email" => $result['email'],
-					"image" => $result['image'],
-					"userID" => $result['id'],
-					"loggedin" => true,
-					"locked" => $result['locked'],
-					"tokenList" => $all,
-					"authService" => explode('::', $result['auth_service'])[0]
-				);
-			} catch (Dibi\Exception $e) {
-				$GLOBALS['organizrUser'] = false;
 			}
-		} else {
-			// Delete cookie & reload page
-			coookie('delete', $GLOBALS['cookieName']);
-			$GLOBALS['organizrUser'] = false;
+			if ($result['valid'] == true) {
+				return $result;
+			} else {
+				return false;
+			}
+		} catch (\RunException $e) {
+			return false;
+		} catch (\OutOfBoundsException $e) {
+			return false;
+		} catch (\RunTimeException $e) {
+			return false;
+		} catch (\InvalidArgumentException $e) {
+			return false;
 		}
-	} else {
-		return $userInfo;
-	}
-	return false;
-}
-
-function getOrganizrUserToken()
-{
-	if (isset($_COOKIE[$GLOBALS['cookieName']])) {
-		// Get token form cookie and validate
-		validateToken($_COOKIE[$GLOBALS['cookieName']], true);
-	} else {
-		$GLOBALS['organizrUser'] = array(
-			"token" => null,
-			"tokenDate" => null,
-			"tokenExpire" => null,
-			"username" => "Guest",
-			"uid" => guestHash(0, 5),
-			"group" => getGuest()['group'],
-			"groupID" => getGuest()['group_id'],
-			"email" => null,
-			//"groupImage"=>getGuest()['image'],
-			"image" => getGuest()['image'],
-			"userID" => null,
-			"loggedin" => false,
-			"locked" => false,
-			"tokenList" => null,
-			"authService" => null
-		);
 	}
-}
+}

+ 105 - 192
api/functions/update-functions.php

@@ -1,205 +1,118 @@
 <?php
-// Upgrade the installation
-function upgradeInstall($branch = 'v2-master', $stage)
-{
-	$notWritable = array_search(false, pathsWritable($GLOBALS['paths']));
-	if ($notWritable == false) {
-		ini_set('max_execution_time', 0);
-		set_time_limit(0);
-		$url = 'https://github.com/causefx/Organizr/archive/' . $branch . '.zip';
-		$file = "upgrade.zip";
-		$source = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'upgrade' . DIRECTORY_SEPARATOR . 'Organizr-' . str_replace('v2', '2', $branch) . DIRECTORY_SEPARATOR;
-		$cleanup = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . "upgrade" . DIRECTORY_SEPARATOR;
-		$destination = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR;
-		switch ($stage) {
-			case '1':
-				writeLog('success', 'Update Function -  Started Upgrade Process', $GLOBALS['organizrUser']['username']);
-				if (downloadFile($url, $file)) {
-					writeLog('success', 'Update Function -  Downloaded Update File for Branch: ' . $branch, $GLOBALS['organizrUser']['username']);
-					return true;
-				} else {
-					writeLog('error', 'Update Function -  Downloaded Update File Failed  for Branch: ' . $branch, $GLOBALS['organizrUser']['username']);
-					return false;
-				}
-				break;
-			case '2':
-				if (unzipFile($file)) {
-					writeLog('success', 'Update Function -  Unzipped Update File for Branch: ' . $branch, $GLOBALS['organizrUser']['username']);
-					return true;
-				} else {
-					writeLog('error', 'Update Function -  Unzip Failed for Branch: ' . $branch, $GLOBALS['organizrUser']['username']);
-					return false;
-				}
-				break;
-			case '3':
-				if (rcopy($source, $destination)) {
-					writeLog('success', 'Update Function -  Overwrited Files using Updated Files from Branch: ' . $branch, $GLOBALS['organizrUser']['username']);
-					$updateComplete = $GLOBALS['dbLocation'] . 'completed.txt';
-					if (!file_exists($updateComplete)) {
-						touch($updateComplete);
-					}
-					return true;
-				} else {
-					writeLog('error', 'Update Function -  Overwrite Failed for Branch: ' . $branch, $GLOBALS['organizrUser']['username']);
-					return false;
-				}
-				break;
-			case '4':
-				if (rrmdir($cleanup)) {
-					writeLog('success', 'Update Function -  Deleted Update Files from Branch: ' . $branch, $GLOBALS['organizrUser']['username']);
-					writeLog('success', 'Update Function -  Update Completed', $GLOBALS['organizrUser']['username']);
-					return true;
-				} else {
-					writeLog('error', 'Update Function -  Removal of Update Files Failed for Branch: ' . $branch, $GLOBALS['organizrUser']['username']);
-					return false;
-				}
-				break;
-			default:
-				return false;
-				break;
-		}
-	} else {
-		return 'permissions';
-	}
-	
-}
 
-function downloadFile($url, $path)
+trait UpdateFunctions
 {
-	ini_set('max_execution_time', 0);
-	set_time_limit(0);
-	$folderPath = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . "upgrade" . DIRECTORY_SEPARATOR;
-	if (!file_exists($folderPath)) {
-		if (@!mkdir($folderPath)) {
-			writeLog('error', 'Update Function -  Folder Creation failed', $GLOBALS['organizrUser']['username']);
+	public function dockerUpdate()
+	{
+		$dockerUpdate = null;
+		chdir('/etc/cont-init.d/');
+		if (file_exists('./30-install')) {
+			$this->setAPIResponse('error', 'Update failed - OrgTools is deprecated - please use organizr/organizr', 500);
 			return false;
+		} elseif (file_exists('./40-install')) {
+			$dockerUpdate = shell_exec('./40-install');
 		}
-	}
-	$newfname = $folderPath . $path;
-	$context = stream_context_create(
-		array(
-			'ssl' => array(
-				'verify_peer' => true,
-				'cafile' => getCert()
-			)
-		)
-	);
-	$file = fopen($url, 'rb', false, $context);
-	if ($file) {
-		$newf = fopen($newfname, 'wb');
-		if ($newf) {
-			while (!feof($file)) {
-				fwrite($newf, fread($file, 1024 * 8), 1024 * 8);
-			}
+		if ($dockerUpdate) {
+			$this->setAPIResponse('success', $dockerUpdate, 200);
+			return true;
+		} else {
+			$this->setAPIResponse('error', 'Update failed', 500);
+			return false;
 		}
-	} else {
-		writeLog("error", "organizr could not download $url");
-		return false;
-	}
-	if ($file) {
-		fclose($file);
-		writeLog("success", "organizr finished downloading the github zip file");
-	} else {
-		writeLog("error", "organizr could not download the github zip file");
-		return false;
 	}
-	if ($newf) {
-		fclose($newf);
-		writeLog("success", "organizr created upgrade zip file from github zip file");
-	} else {
-		writeLog("error", "organizr could not create upgrade zip file from github zip file");
-		return false;
-	}
-	return true;
-}
-
-function downloadFileToPath($from, $to, $path)
-{
-	ini_set('max_execution_time', 0);
-	set_time_limit(0);
-	if (@!mkdir($path, 0777, true)) {
-		writeLog("error", "organizr could not create upgrade folder");
-	}
-	$file = fopen($from, 'rb');
-	if ($file) {
-		$newf = fopen($to, 'wb');
-		if ($newf) {
-			while (!feof($file)) {
-				fwrite($newf, fread($file, 1024 * 8), 1024 * 8);
-			}
+	
+	public function windowsUpdate()
+	{
+		$branch = ($this->config['branch'] == 'v2-master') ? '-m' : '-d';
+		ini_set('max_execution_time', 0);
+		set_time_limit(0);
+		$logFile = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'log.txt';
+		$windowsScript = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'scripts' . DIRECTORY_SEPARATOR . 'windows-update.bat ' . $branch . ' > ' . $logFile . ' 2>&1';
+		$windowsUpdate = shell_exec($windowsScript);
+		if ($windowsUpdate) {
+			$this->setAPIResponse('success', $windowsUpdate, 200);
+			return true;
+		} else {
+			$this->setAPIResponse('success', 'Update Complete - check log.txt for output', 200);
+			return false;
 		}
-	} else {
-		writeLog("error", "organizr could not download $url");
-	}
-	if ($file) {
-		fclose($file);
-		writeLog("success", "organizr finished downloading the github zip file");
-	} else {
-		writeLog("error", "organizr could not download the github zip file");
-	}
-	if ($newf) {
-		fclose($newf);
-		writeLog("success", "organizr created upgrade zip file from github zip file");
-	} else {
-		writeLog("error", "organizr could not create upgrade zip file from github zip file");
-	}
-	return true;
-}
-
-function unzipFile($zipFile)
-{
-	ini_set('max_execution_time', 0);
-	set_time_limit(0);
-	$zip = new ZipArchive;
-	$extractPath = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . "upgrade/";
-	if ($zip->open($extractPath . $zipFile) != "true") {
-		writeLog("error", "organizr could not unzip upgrade.zip");
-	} else {
-		writeLog("success", "organizr unzipped upgrade.zip");
 	}
-	/* Extract Zip File */
-	$zip->extractTo($extractPath);
-	$zip->close();
-	return true;
-}
-
-// Function to remove folders and files
-function rrmdir($dir)
-{
-	ini_set('max_execution_time', 0);
-	set_time_limit(0);
-	if (is_dir($dir)) {
-		$files = scandir($dir);
-		foreach ($files as $file) {
-			if ($file != "." && $file != "..") {
-				rrmdir("$dir/$file");
-			}
+	
+	public function upgradeInstall($branch = 'v2-master', $stage = '1')
+	{
+		// may kill this function in place for php script to run elsewhere
+		if ($this->docker) {
+			$this->setAPIResponse('error', 'Cannot perform update action on docker install - use script', 500);
+			return false;
 		}
-		rmdir($dir);
-	} elseif (file_exists($dir)) {
-		unlink($dir);
-	}
-	return true;
-}
-
-// Function to Copy folders and files
-function rcopy($src, $dst)
-{
-	ini_set('max_execution_time', 0);
-	set_time_limit(0);
-	$src = cleanPath($src);
-	$dst = cleanPath($dst);
-	if (is_dir($src)) {
-		if (!file_exists($dst)) : mkdir($dst);
-		endif;
-		$files = scandir($src);
-		foreach ($files as $file) {
-			if ($file != "." && $file != "..") {
-				rcopy("$src/$file", "$dst/$file");
+		if ($this->getOS() == 'win') {
+			$this->setAPIResponse('error', 'Cannot perform update action on windows install - use script', 500);
+			return false;
+		}
+		$notWritable = array_search(false, $this->pathsWritable($this->paths));
+		if ($notWritable == false) {
+			ini_set('max_execution_time', 0);
+			set_time_limit(0);
+			$url = 'https://github.com/causefx/Organizr/archive/' . $branch . '.zip';
+			$file = "upgrade.zip";
+			$source = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'upgrade' . DIRECTORY_SEPARATOR . 'Organizr-' . str_replace('v2', '2', $branch) . DIRECTORY_SEPARATOR;
+			$cleanup = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . "upgrade" . DIRECTORY_SEPARATOR;
+			$destination = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR;
+			switch ($stage) {
+				case '1':
+					$this->writeLog('success', 'Update Function -  Started Upgrade Process', $this->user['username']);
+					if ($this->downloadFile($url, $file)) {
+						$this->writeLog('success', 'Update Function -  Downloaded Update File for Branch: ' . $branch, $this->user['username']);
+						$this->setAPIResponse('success', 'Downloaded file successfully', 200);
+						return true;
+					} else {
+						$this->writeLog('error', 'Update Function -  Downloaded Update File Failed  for Branch: ' . $branch, $this->user['username']);
+						$this->setAPIResponse('error', 'Download failed', 500);
+						return false;
+					}
+				case '2':
+					if ($this->unzipFile($file)) {
+						$this->writeLog('success', 'Update Function -  Unzipped Update File for Branch: ' . $branch, $this->user['username']);
+						$this->setAPIResponse('success', 'Unzipped file successfully', 200);
+						return true;
+					} else {
+						$this->writeLog('error', 'Update Function -  Unzip Failed for Branch: ' . $branch, $this->user['username']);
+						$this->setAPIResponse('error', 'Unzip failed', 500);
+						return false;
+					}
+				case '3':
+					if ($this->rcopy($source, $destination)) {
+						$this->writeLog('success', 'Update Function -  Files overwritten using Updated Files from Branch: ' . $branch, $this->user['username']);
+						$updateComplete = $this->config['dbLocation'] . 'completed.txt';
+						if (!file_exists($updateComplete)) {
+							touch($updateComplete);
+						}
+						$this->setAPIResponse('success', 'Files replaced successfully', 200);
+						return true;
+					} else {
+						$this->writeLog('error', 'Update Function -  Overwrite Failed for Branch: ' . $branch, $this->user['username']);
+						$this->setAPIResponse('error', 'File replacement failed', 500);
+						return false;
+					}
+				case '4':
+					if ($this->rrmdir($cleanup)) {
+						$this->writeLog('success', 'Update Function -  Deleted Update Files from Branch: ' . $branch, $this->user['username']);
+						$this->writeLog('success', 'Update Function -  Update Completed', $this->user['username']);
+						$this->setAPIResponse('success', 'Removed update files successfully', 200);
+						return true;
+					} else {
+						$this->writeLog('error', 'Update Function -  Removal of Update Files Failed for Branch: ' . $branch, $this->user['username']);
+						$this->setAPIResponse('error', 'File removal failed', 500);
+						return false;
+					}
+				default:
+					$this->setAPIResponse('error', 'Action not setup', 500);
+					return false;
 			}
+		} else {
+			$this->setAPIResponse('error', 'File permissions not set correctly', 500);
+			return false;
 		}
-	} elseif (file_exists($src)) {
-		copy($src, $dst);
+		
 	}
-	return true;
-}
+}

+ 45 - 49
api/functions/upgrade-functions.php

@@ -1,53 +1,49 @@
 <?php
-function upgradeCheck()
+
+trait UpgradeFunctions
 {
-	if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
-		$tempLock = $GLOBALS['dbLocation'] . 'DBLOCK.txt';
-		$updateComplete = $GLOBALS['dbLocation'] . 'completed.txt';
-		$cleanup = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . "upgrade" . DIRECTORY_SEPARATOR;
-		if (file_exists($updateComplete)) {
-			@unlink($updateComplete);
-			@rrmdir($cleanup);
+	public function upgradeToVersion($version = '2.1.0')
+	{
+		switch ($version) {
+			case '2.1.0':
+				$this->upgradeSettingsTabURL();
+				$this->upgradeHomepageTabURL();
+				$this->setAPIResponse('success', 'Ran update function for version: ' . $version, 200);
+				return true;
+			default:
+				return true;
 		}
-		if (file_exists($tempLock)) {
-			die('upgrading');
-		}
-		$updateDB = false;
-		$updateSuccess = true;
-		$compare = new Composer\Semver\Comparator;
-		$oldVer = $GLOBALS['configVersion'];
-		// Upgrade check start for version below
-		$versionCheck = '2.0.0-beta-200';
-		if ($compare->lessThan($oldVer, $versionCheck)) {
-			$updateDB = true;
-			$oldVer = $versionCheck;
-		}
-		// End Upgrade check start for version above
-		// Upgrade check start for version below
-		$versionCheck = '2.0.0-beta-500';
-		if ($compare->lessThan($oldVer, $versionCheck)) {
-			$updateDB = true;
-			$oldVer = $versionCheck;
-		}
-		// End Upgrade check start for version above
-		$versionCheck = '2.0.0-beta-800';
-		if ($compare->lessThan($oldVer, $versionCheck)) {
-			$updateDB = true;
-			$oldVer = $versionCheck;
-		}
-		// End Upgrade check start for version above
-		if ($updateDB == true) {
-			//return 'Upgraded Needed - Current Version '.$oldVer.' - New Version: '.$versionCheck;
-			// Upgrade database to latest version
-			$updateSuccess = (updateDB($oldVer)) ? true : false;
-		}
-		// Update config.php version if different to the installed version
-		if ($updateSuccess && $GLOBALS['installedVersion'] !== $GLOBALS['configVersion']) {
-			updateConfig(array('apply_CONFIG_VERSION' => $GLOBALS['installedVersion']));
-		}
-		if ($updateSuccess == false) {
-			die('Database update failed - Please manually check logs and fix - Then reload this page');
-		}
-		return true;
 	}
-}
+	
+	public function upgradeSettingsTabURL()
+	{
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE tabs SET',
+					['url' => 'api/v2/page/settings'],
+					'WHERE url = ?',
+					'api/?v1/settings/page'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+	
+	public function upgradeHomepageTabURL()
+	{
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'UPDATE tabs SET',
+					['url' => 'api/v2/page/homepage'],
+					'WHERE url = ?',
+					'api/?v1/homepage/page'
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
+}

+ 184 - 0
api/homepage/calendar.php

@@ -0,0 +1,184 @@
+<?php
+
+trait CalendarHomepageItem
+{
+	public function calendarSettingsArray()
+	{
+		return array(
+			'name' => 'Calendar',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/calendar.png',
+			'category' => 'HOMEPAGE',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageCalendarEnabled',
+						'label' => 'Enable iCal',
+						'value' => $this->config['homepageCalendarEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageCalendarAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageCalendarAuth'],
+						'options' => $this->groupOptions
+					),
+					array(
+						'type' => 'input',
+						'name' => 'calendariCal',
+						'label' => 'iCal URL\'s',
+						'value' => $this->config['calendariCal'],
+						'placeholder' => 'separate by comma\'s'
+					),
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'number',
+						'name' => 'calendarStart',
+						'label' => '# of Days Before',
+						'value' => $this->config['calendarStart'],
+						'placeholder' => ''
+					),
+					array(
+						'type' => 'number',
+						'name' => 'calendarEnd',
+						'label' => '# of Days After',
+						'value' => $this->config['calendarEnd'],
+						'placeholder' => ''
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarFirstDay',
+						'label' => 'Start Day',
+						'value' => $this->config['calendarFirstDay'],
+						'options' => $this->daysOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarDefault',
+						'label' => 'Default View',
+						'value' => $this->config['calendarDefault'],
+						'options' => $this->calendarDefaultOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarTimeFormat',
+						'label' => 'Time Format',
+						'value' => $this->config['calendarTimeFormat'],
+						'options' => $this->timeFormatOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarLocale',
+						'label' => 'Locale',
+						'value' => $this->config['calendarLocale'],
+						'options' => $this->calendarLocaleOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarLimit',
+						'label' => 'Items Per Day',
+						'value' => $this->config['calendarLimit'],
+						'options' => $this->limitOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['calendarRefresh'],
+						'options' => $this->timeOptions()
+					)
+				),
+			)
+		);
+	}
+	
+	public function calendarHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageCalendarEnabled'
+				],
+				'auth' => [
+					'homepageCalendarAuth'
+				],
+				'not_empty' => [
+					'calendariCal'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrdercalendar()
+	{
+		if (
+			$this->homepageItemPermissions($this->sonarrHomepagePermissions('calendar')) ||
+			$this->homepageItemPermissions($this->radarrHomepagePermissions('calendar')) ||
+			$this->homepageItemPermissions($this->lidarrHomepagePermissions('calendar')) ||
+			$this->homepageItemPermissions($this->sickrageHomepagePermissions('calendar')) ||
+			$this->homepageItemPermissions($this->couchPotatoHomepagePermissions('calendar')) ||
+			$this->homepageItemPermissions($this->calendarHomepagePermissions('calendar'))
+		) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div id="calendar" class="fc fc-ltr m-b-30"></div>
+					<script>
+						// Calendar
+						homepageCalendar("' . $this->config['calendarRefresh'] . '");
+						// End Calendar
+					</script>
+					</div>
+				';
+		}
+	}
+	
+	public function loadCalendarJS()
+	{
+		$locale = ($this->config['calendarLocale'] !== 'en') ?? false;
+		return ($locale) ? '<script src="plugins/bower_components/calendar/dist/lang-all.js"></script>' : '';
+	}
+	
+	public function getCalendar()
+	{
+		$startDate = date('Y-m-d', strtotime("-" . $this->config['calendarStart'] . " days"));
+		$endDate = date('Y-m-d', strtotime("+" . $this->config['calendarEnd'] . " days"));
+		$icalCalendarSources = array();
+		$calendarItems = array();
+		// SONARR CONNECT
+		$items = $this->getSonarrCalendar($startDate, $endDate);
+		$calendarItems = is_array($items) ? array_merge($calendarItems, $items) : $calendarItems;
+		unset($items);
+		// LIDARR CONNECT
+		$items = $this->getLidarrCalendar($startDate, $endDate);
+		$calendarItems = is_array($items) ? array_merge($calendarItems, $items) : $calendarItems;
+		unset($items);
+		// RADARR CONNECT
+		$items = $this->getRadarrCalendar($startDate, $endDate);
+		$calendarItems = is_array($items) ? array_merge($calendarItems, $items) : $calendarItems;
+		unset($items);
+		// SICKRAGE/BEARD/MEDUSA CONNECT
+		$items = $this->getSickRageCalendar();
+		$calendarItems = is_array($items) ? array_merge($calendarItems, $items) : $calendarItems;
+		unset($items);
+		// COUCHPOTATO CONNECT
+		$items = $this->getCouchPotatoCalendar();
+		$calendarItems = is_array($items) ? array_merge($calendarItems, $items) : $calendarItems;
+		unset($items);
+		// iCal URL
+		$calendarSources['ical'] = $this->getICalendar();
+		unset($items);
+		// Finish
+		$calendarSources['events'] = $calendarItems;
+		$this->setAPIResponse('success', null, 200, $calendarSources);
+		return $calendarSources;
+	}
+}

+ 233 - 0
api/homepage/couchpotato.php

@@ -0,0 +1,233 @@
+<?php
+
+trait CouchPotatoHomepageItem
+{
+	
+	public function couchPotatoSettingsArray()
+	{
+		return array(
+			'name' => 'CouchPotato',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/couchpotato.png',
+			'category' => 'PVR',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageCouchpotatoEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageCouchpotatoEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageCouchpotatoAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageCouchpotatoAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'couchpotatoURL',
+						'label' => 'URL',
+						'value' => $this->config['couchpotatoURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'couchpotatoToken',
+						'label' => 'Token',
+						'value' => $this->config['couchpotatoToken']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'select',
+						'name' => 'calendarFirstDay',
+						'label' => 'Start Day',
+						'value' => $this->config['calendarFirstDay'],
+						'options' => $this->daysOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarDefault',
+						'label' => 'Default View',
+						'value' => $this->config['calendarDefault'],
+						'options' => $this->calendarDefaultOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarTimeFormat',
+						'label' => 'Time Format',
+						'value' => $this->config['calendarTimeFormat'],
+						'options' => $this->timeFormatOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarLocale',
+						'label' => 'Locale',
+						'value' => $this->config['calendarLocale'],
+						'options' => $this->calendarLocaleOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarLimit',
+						'label' => 'Items Per Day',
+						'value' => $this->config['calendarLimit'],
+						'options' => $this->limitOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['calendarRefresh'],
+						'options' => $this->timeOptions()
+					)
+				)
+			)
+		);
+	}
+	
+	public function couchPotatoHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'calendar' => [
+				'enabled' => [
+					'homepageCouchpotatoEnabled'
+				],
+				'auth' => [
+					'homepageCouchpotatoAuth'
+				],
+				'not_empty' => [
+					'couchpotatoURL',
+					'couchpotatoToken'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function getCouchPotatoCalendar()
+	{
+		if (!$this->homepageItemPermissions($this->couchPotatoHomepagePermissions('calendar'), true)) {
+			return false;
+		}
+		$calendarItems = array();
+		$list = $this->csvHomepageUrlToken($this->config['couchpotatoURL'], $this->config['couchpotatoToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\CouchPotato\CouchPotato($value['url'], $value['token']);
+				$calendar = $this->formatCouchCalendar($downloader->getMediaList(array('status' => 'active,done')), $key);
+			} catch (Exception $e) {
+				$this->writeLog('error', 'Radarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+			if (!empty($calendar)) {
+				$calendarItems = array_merge($calendarItems, $calendar);
+			}
+		}
+		$this->setAPIResponse('success', null, 200, $calendarItems);
+		return $calendarItems;
+	}
+	
+	public function formatCouchCalendar($array, $number)
+	{
+		$api = json_decode($array, true);
+		$gotCalendar = array();
+		$i = 0;
+		foreach ($api['movies'] as $child) {
+			$i++;
+			$movieName = $child['info']['original_title'];
+			$movieID = $child['info']['tmdb_id'];
+			if (!isset($movieID)) {
+				$movieID = "";
+			}
+			$physicalRelease = (isset($child['info']['released']) ? $child['info']['released'] : null);
+			$backupRelease = (isset($child['info']['release_date']['theater']) ? $child['info']['release_date']['theater'] : null);
+			$physicalRelease = (isset($physicalRelease) ? $physicalRelease : $backupRelease);
+			$physicalRelease = strtotime($physicalRelease);
+			$physicalRelease = date("Y-m-d", $physicalRelease);
+			$oldestDay = new DateTime ($this->currentTime);
+			$oldestDay->modify('-' . $this->config['calendarStart'] . ' days');
+			$newestDay = new DateTime ($this->currentTime);
+			$newestDay->modify('+' . $this->config['calendarEnd'] . ' days');
+			$startDt = new DateTime ($physicalRelease);
+			$calendarStartDiff = date_diff($startDt, $newestDay);
+			$calendarEndDiff = date_diff($startDt, $oldestDay);
+			if (!$this->calendarDaysCheck($calendarStartDiff->format('%R') . $calendarStartDiff->days, $calendarEndDiff->format('%R') . $calendarEndDiff->days)) {
+				continue;
+			}
+			if (new DateTime() < $startDt) {
+				$notReleased = "true";
+			} else {
+				$notReleased = "false";
+			}
+			$downloaded = ($child['status'] == "active") ? "0" : "1";
+			if ($downloaded == "0" && $notReleased == "true") {
+				$downloaded = "text-info";
+			} elseif ($downloaded == "1") {
+				$downloaded = "text-success";
+			} else {
+				$downloaded = "text-danger";
+			}
+			if (!empty($child['info']['images']['backdrop_original'])) {
+				$banner = $child['info']['images']['backdrop_original'][0];
+			} elseif (!empty($child['info']['images']['backdrop'])) {
+				$banner = $child['info']['images']['backdrop_original'][0];
+			} else {
+				$banner = "/plugins/images/cache/no-np.png";
+			}
+			if ($banner !== "/plugins/images/cache/no-np.png") {
+				$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+				$imageURL = $banner;
+				$cacheFile = $cacheDirectory . $movieID . '.jpg';
+				$banner = 'plugins/images/cache/' . $movieID . '.jpg';
+				if (!file_exists($cacheFile)) {
+					$this->cacheImage($imageURL, $movieID);
+					unset($imageURL);
+					unset($cacheFile);
+				}
+			}
+			$hasFile = (!empty($child['releases']) && !empty($child['releases'][0]['files']['movie']));
+			$details = array(
+				"topTitle" => $movieName,
+				"bottomTitle" => $child['info']['tagline'],
+				"status" => $child['status'],
+				"overview" => $child['info']['plot'],
+				"runtime" => $child['info']['runtime'],
+				"image" => $banner,
+				"ratings" => isset($child['info']['rating']['imdb'][0]) ? $child['info']['rating']['imdb'][0] : '',
+				"videoQuality" => $hasFile ? $child['releases'][0]['quality'] : "unknown",
+				"audioChannels" => "",
+				"audioCodec" => "",
+				"videoCodec" => "",
+				"genres" => $child['info']['genres'],
+				"year" => isset($child['info']['year']) ? $child['info']['year'] : '',
+				"studio" => isset($child['info']['year']) ? $child['info']['year'] : '',
+			);
+			array_push($gotCalendar, array(
+				"id" => "CouchPotato-" . $number . "-" . $i,
+				"title" => $movieName,
+				"start" => $physicalRelease,
+				"className" => "inline-popups bg-calendar calendar-item movieID--" . $movieID,
+				"imagetype" => "film " . $downloaded,
+				"imagetypeFilter" => "film",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details
+			));
+			
+		}
+		if ($i != 0) {
+			return $gotCalendar;
+		}
+		return false;
+	}
+}

+ 216 - 0
api/homepage/deluge.php

@@ -0,0 +1,216 @@
+<?php
+
+trait DelugeHomepageItem
+{
+	public function delugeSettingsArray()
+	{
+		return array(
+			'name' => 'Deluge',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/deluge.png',
+			'category' => 'Downloader',
+			'settings' => array(
+				'custom' => '
+				<div class="row">
+                    <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">
+									<ul class="list-icons">
+                                        <li><i class="fa fa-chevron-right text-danger"></i> <a href="https://github.com/idlesign/deluge-webapi/tree/master/dist" target="_blank">Download Plugin</a></li>
+                                        <li><i class="fa fa-chevron-right text-danger"></i> Open Deluge Web UI, go to "Preferences -> Plugins -> Install plugin" and choose egg file.</li>
+                                        <li><i class="fa fa-chevron-right text-danger"></i> Activate WebAPI plugin </li>
+                                    </ul>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+				</div>
+				',
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageDelugeEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageDelugeEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageDelugeAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageDelugeAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'delugeURL',
+						'label' => 'URL',
+						'value' => $this->config['delugeURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password',
+						'name' => 'delugePassword',
+						'label' => 'Password',
+						'value' => $this->config['delugePassword']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'delugeHideSeeding',
+						'label' => 'Hide Seeding',
+						'value' => $this->config['delugeHideSeeding']
+					), array(
+						'type' => 'switch',
+						'name' => 'delugeHideCompleted',
+						'label' => 'Hide Completed',
+						'value' => $this->config['delugeHideCompleted']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageDownloadRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageDownloadRefresh'],
+						'options' => $this->timeOptions()
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'delugeCombine',
+						'label' => 'Add to Combined Downloader',
+						'value' => $this->config['delugeCombine']
+					),
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'deluge\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	public function testConnectionDeluge()
+	{
+		if (empty($this->config['delugeURL'])) {
+			$this->setAPIResponse('error', 'Deluge URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['delugePassword'])) {
+			$this->setAPIResponse('error', 'Deluge Password is not defined', 422);
+			return false;
+		}
+		try {
+			$deluge = new deluge($this->config['delugeURL'], $this->decrypt($this->config['delugePassword']));
+			$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');
+			$this->setAPIResponse('success', 'API Connection succeeded', 200);
+			return true;
+		} catch (Exception $e) {
+			$this->writeLog('error', 'NZBGet Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+		
+	}
+	
+	public function delugeHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageDelugeEnabled'
+				],
+				'auth' => [
+					'homepageDelugeAuth'
+				],
+				'not_empty' => [
+					'delugeURL',
+					'delugePassword'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderdeluge()
+	{
+		if ($this->homepageItemPermissions($this->delugeHomepagePermissions('main'))) {
+			$loadingBox = ($this->config['delugeCombine']) ? '' : '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+			$builder = ($this->config['delugeCombine']) ? 'buildDownloaderCombined(\'deluge\');' : '$("#' . __FUNCTION__ . '").html(buildDownloader("deluge"));';
+			return '
+				<div id="' . __FUNCTION__ . '">
+					' . $loadingBox . '
+					<script>
+		                // homepageOrderdeluge
+		                ' . $builder . '
+		                homepageDownloader("deluge", "' . $this->config['homepageDownloadRefresh'] . '");
+		                // End homepageOrderdeluge
+	                </script>
+				</div>
+				';
+		}
+	}
+	
+	public function getDelugeHomepageQueue()
+	{
+		if (!$this->homepageItemPermissions($this->delugeHomepagePermissions('main'), true)) {
+			return false;
+		}
+		try {
+			$deluge = new deluge($this->config['delugeURL'], $this->decrypt($this->config['delugePassword']));
+			$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);
+				if ($tempStatus == 'Seeding' && $this->config['delugeHideSeeding']) {
+					//do nothing
+				} elseif ($tempStatus == 'Finished' && $this->config['delugeHideCompleted']) {
+					//do nothing
+				} else {
+					$api['content']['queueItems'][] = $value;
+				}
+			}
+			$api['content']['queueItems'] = (empty($api['content']['queueItems'])) ? [] : $api['content']['queueItems'];
+			$api['content']['historyItems'] = false;
+		} catch (Excecption $e) {
+			$this->writeLog('error', 'Deluge Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+	
+	public function delugeStatus($queued, $status, $state)
+	{
+		if ($queued == '-1' && $state == '100' && ($status == 'Seeding' || $status == 'Queued' || $status == 'Paused')) {
+			$state = 'Seeding';
+		} elseif ($state !== '100') {
+			$state = 'Downloading';
+		} else {
+			$state = 'Finished';
+		}
+		return ($state) ? $state : $status;
+	}
+}

+ 629 - 0
api/homepage/emby.php

@@ -0,0 +1,629 @@
+<?php
+
+trait EmbyHomepageItem
+{
+	public function embySettingsArray()
+	{
+		return array(
+			'name' => 'Emby',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/emby.png',
+			'category' => 'Media Server',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageEmbyEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageEmbyEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageEmbyAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageEmbyAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'embyURL',
+						'label' => 'URL',
+						'value' => $this->config['embyURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'embyToken',
+						'label' => 'Token',
+						'value' => $this->config['embyToken']
+					)
+				),
+				'Active Streams' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageEmbyStreams',
+						'label' => 'Enable',
+						'value' => $this->config['homepageEmbyStreams']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageEmbyStreamsAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $this->config['homepageEmbyStreamsAuth'],
+						'options' => $this->groupOptions
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'homepageShowStreamNames',
+						'label' => 'User Information',
+						'value' => $this->config['homepageShowStreamNames']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageShowStreamNamesAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $this->config['homepageShowStreamNamesAuth'],
+						'options' => $this->groupOptions
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageStreamRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageStreamRefresh'],
+						'options' => $this->timeOptions()
+					),
+				),
+				'Recent Items' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageEmbyRecent',
+						'label' => 'Enable',
+						'value' => $this->config['homepageEmbyRecent']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageEmbyRecentAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $this->config['homepageEmbyRecentAuth'],
+						'options' => $this->groupOptions
+					),
+					array(
+						'type' => 'number',
+						'name' => 'homepageRecentLimit',
+						'label' => 'Item Limit',
+						'value' => $this->config['homepageRecentLimit'],
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageRecentRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageRecentRefresh'],
+						'options' => $this->timeOptions()
+					),
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'input',
+						'name' => 'embyTabName',
+						'label' => 'Emby Tab Name',
+						'value' => $this->config['embyTabName'],
+						'placeholder' => 'Only use if you have Emby in a reverse proxy'
+					),
+					array(
+						'type' => 'input',
+						'name' => 'embyTabURL',
+						'label' => 'Emby Tab WAN URL',
+						'value' => $this->config['embyTabURL'],
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'select',
+						'name' => 'cacheImageSize',
+						'label' => 'Image Cache Size',
+						'value' => $this->config['cacheImageSize'],
+						'options' => array(
+							array(
+								'name' => 'Low',
+								'value' => '.5'
+							),
+							array(
+								'name' => '1x',
+								'value' => '1'
+							),
+							array(
+								'name' => '2x',
+								'value' => '2'
+							),
+							array(
+								'name' => '3x',
+								'value' => '3'
+							)
+						)
+					)
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'emby\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	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);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['embyURL']);
+		$url = $url . "/Users?api_key=" . $this->config['embyToken'];
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		try {
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$this->setAPIResponse('success', 'API Connection succeeded', 200);
+				return true;
+			} else {
+				$this->setAPIResponse('error', 'Emby Connection Error', 500);
+				return true;
+			}
+		} catch (Requests_Exception $e) {
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function embyHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'streams' => [
+				'enabled' => [
+					'homepageEmbyEnabled',
+					'homepageEmbyStreams'
+				],
+				'auth' => [
+					'homepageEmbyAuth',
+					'homepageEmbyStreamsAuth'
+				],
+				'not_empty' => [
+					'embyURL',
+					'embyToken'
+				]
+			],
+			'recent' => [
+				'enabled' => [
+					'homepageEmbyEnabled',
+					'homepageEmbyRecent'
+				],
+				'auth' => [
+					'homepageEmbyAuth',
+					'homepageEmbyRecentAuth'
+				],
+				'not_empty' => [
+					'embyURL',
+					'embyToken'
+				]
+			],
+			'metadata' => [
+				'enabled' => [
+					'homepageEmbyEnabled'
+				],
+				'auth' => [
+					'homepageEmbyAuth'
+				],
+				'not_empty' => [
+					'embyURL',
+					'embyToken'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderembynowplaying()
+	{
+		if ($this->homepageItemPermissions($this->embyHomepagePermissions('streams'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Now Playing...</h2></div>
+					<script>
+						// Emby Stream
+						homepageStream("emby", "' . $this->config['homepageStreamRefresh'] . '");
+						// End Emby Stream
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	public function homepageOrderembyrecent()
+	{
+		if ($this->homepageItemPermissions($this->embyHomepagePermissions('recent'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Recent...</h2></div>
+					<script>
+						// Emby Recent
+						homepageRecent("emby", "' . $this->config['homepageRecentRefresh'] . '");
+						// End Emby Recent
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	public function getEmbyHomepageStreams()
+	{
+		if (!$this->homepageItemPermissions($this->embyHomepagePermissions('streams'), true)) {
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['embyURL']);
+		$url = $url . '/Sessions?api_key=' . $this->config['embyToken'] . '&Fields=Overview,People,Genres,CriticRating,Studios,Taglines';
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		try {
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$items = array();
+				$emby = json_decode($response->body, true);
+				foreach ($emby as $child) {
+					if (isset($child['NowPlayingItem']) || isset($child['Name'])) {
+						$items[] = $this->resolveEmbyItem($child);
+					}
+				}
+				$api['content'] = array_filter($items);
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
+			} else {
+				$this->setAPIResponse('error', 'Emby Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Emby Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function getEmbyHomepageRecent()
+	{
+		if (!$this->homepageItemPermissions($this->embyHomepagePermissions('recent'), true)) {
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['embyURL']);
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		$username = false;
+		$showPlayed = false;
+		$userId = 0;
+		try {
+			
+			
+			if (isset($this->user['username'])) {
+				$username = strtolower($this->user['username']);
+			}
+			// Get A User
+			$userIds = $url . "/Users?api_key=" . $this->config['embyToken'];
+			$response = Requests::get($userIds, array(), $options);
+			if ($response->success) {
+				$emby = json_decode($response->body, true);
+				foreach ($emby as $value) { // Scan for admin user
+					if (isset($value['Policy']) && isset($value['Policy']['IsAdministrator']) && $value['Policy']['IsAdministrator']) {
+						$userId = $value['Id'];
+					}
+					if ($username && strtolower($value['Name']) == $username) {
+						$userId = $value['Id'];
+						$showPlayed = false;
+						break;
+					}
+				}
+				$url = $url . '/Users/' . $userId . '/Items/Latest?EnableImages=true&Limit=' . $this->config['homepageRecentLimit'] . '&api_key=' . $this->config['embyToken'] . ($showPlayed ? '' : '&IsPlayed=false') . '&Fields=Overview,People,Genres,CriticRating,Studios,Taglines';
+			} else {
+				$this->setAPIResponse('error', 'Emby Error Occurred', 500);
+				return false;
+			}
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$items = array();
+				$emby = json_decode($response->body, true);
+				foreach ($emby as $child) {
+					if (isset($child['NowPlayingItem']) || isset($child['Name'])) {
+						$items[] = $this->resolveEmbyItem($child);
+					}
+				}
+				$api['content'] = array_filter($items);
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
+			} else {
+				$this->setAPIResponse('error', 'Emby Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Emby Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function getEmbyHomepageMetadata($array)
+	{
+		if (!$this->homepageItemPermissions($this->embyHomepagePermissions('metadata'), true)) {
+			return false;
+		}
+		$key = $array['key'] ?? null;
+		if (!$key) {
+			$this->setAPIResponse('error', 'Emby Metadata key is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['embyURL']);
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		$username = false;
+		$showPlayed = false;
+		$userId = 0;
+		try {
+			
+			
+			if (isset($this->user['username'])) {
+				$username = strtolower($this->user['username']);
+			}
+			// Get A User
+			$userIds = $url . "/Users?api_key=" . $this->config['embyToken'];
+			$response = Requests::get($userIds, array(), $options);
+			if ($response->success) {
+				$emby = json_decode($response->body, true);
+				foreach ($emby as $value) { // Scan for admin user
+					if (isset($value['Policy']) && isset($value['Policy']['IsAdministrator']) && $value['Policy']['IsAdministrator']) {
+						$userId = $value['Id'];
+					}
+					if ($username && strtolower($value['Name']) == $username) {
+						$userId = $value['Id'];
+						$showPlayed = false;
+						break;
+					}
+				}
+				$url = $url . '/Users/' . $userId . '/Items/' . $key . '?EnableImages=true&Limit=' . $this->config['homepageRecentLimit'] . '&api_key=' . $this->config['embyToken'] . ($showPlayed ? '' : '&IsPlayed=false') . '&Fields=Overview,People,Genres,CriticRating,Studios,Taglines';
+			} else {
+				$this->setAPIResponse('error', 'Emby Error Occurred', 500);
+				return false;
+			}
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$items = array();
+				$emby = json_decode($response->body, true);
+				if (isset($emby['NowPlayingItem']) || isset($emby['Name'])) {
+					$items[] = $this->resolveEmbyItem($emby);
+				}
+				$api['content'] = array_filter($items);
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
+			} else {
+				$this->setAPIResponse('error', 'Emby Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Emby Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function resolveEmbyItem($itemDetails)
+	{
+		$item = isset($itemDetails['NowPlayingItem']['Id']) ? $itemDetails['NowPlayingItem'] : $itemDetails;
+		// Static Height & Width
+		$height = $this->getCacheImageSize('h');
+		$width = $this->getCacheImageSize('w');
+		$nowPlayingHeight = $this->getCacheImageSize('nph');
+		$nowPlayingWidth = $this->getCacheImageSize('npw');
+		$actorHeight = 450;
+		$actorWidth = 300;
+		// Cache Directories
+		$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+		$cacheDirectoryWeb = 'plugins/images/cache/';
+		// Types
+		//$embyItem['array-item'] = $item;
+		//$embyItem['array-itemdetails'] = $itemDetails;
+		switch (@$item['Type']) {
+			case 'Series':
+				$embyItem['type'] = 'tv';
+				$embyItem['title'] = $item['Name'];
+				$embyItem['secondaryTitle'] = '';
+				$embyItem['summary'] = '';
+				$embyItem['ratingKey'] = $item['Id'];
+				$embyItem['thumb'] = $item['Id'];
+				$embyItem['key'] = $item['Id'] . "-list";
+				$embyItem['nowPlayingThumb'] = $item['Id'];
+				$embyItem['nowPlayingKey'] = $item['Id'] . "-np";
+				$embyItem['metadataKey'] = $item['Id'];
+				$embyItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? 'Thumb' : (isset($item['BackdropImageTags'][0]) ? 'Backdrop' : '');
+				break;
+			case 'Episode':
+				$embyItem['type'] = 'tv';
+				$embyItem['title'] = $item['SeriesName'];
+				$embyItem['secondaryTitle'] = '';
+				$embyItem['summary'] = '';
+				$embyItem['ratingKey'] = $item['Id'];
+				$embyItem['thumb'] = (isset($item['SeriesId']) ? $item['SeriesId'] : $item['Id']);
+				$embyItem['key'] = (isset($item['SeriesId']) ? $item['SeriesId'] : $item['Id']) . "-list";
+				$embyItem['nowPlayingThumb'] = isset($item['ParentThumbItemId']) ? $item['ParentThumbItemId'] : (isset($item['ParentBackdropItemId']) ? $item['ParentBackdropItemId'] : false);
+				$embyItem['nowPlayingKey'] = isset($item['ParentThumbItemId']) ? $item['ParentThumbItemId'] . '-np' : (isset($item['ParentBackdropItemId']) ? $item['ParentBackdropItemId'] . '-np' : false);
+				$embyItem['metadataKey'] = $item['Id'];
+				$embyItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? 'Thumb' : (isset($item['ParentBackdropImageTags'][0]) ? 'Backdrop' : '');
+				$embyItem['nowPlayingTitle'] = @$item['SeriesName'] . ' - ' . @$item['Name'];
+				$embyItem['nowPlayingBottom'] = 'S' . @$item['ParentIndexNumber'] . ' · E' . @$item['IndexNumber'];
+				break;
+			case 'MusicAlbum':
+			case 'Audio':
+				$embyItem['type'] = 'music';
+				$embyItem['title'] = $item['Name'];
+				$embyItem['secondaryTitle'] = '';
+				$embyItem['summary'] = '';
+				$embyItem['ratingKey'] = $item['Id'];
+				$embyItem['thumb'] = $item['Id'];
+				$embyItem['key'] = $item['Id'] . "-list";
+				$embyItem['nowPlayingThumb'] = (isset($item['AlbumId']) ? $item['AlbumId'] : @$item['ParentBackdropItemId']);
+				$embyItem['nowPlayingKey'] = $item['Id'] . "-np";
+				$embyItem['metadataKey'] = isset($item['AlbumId']) ? $item['AlbumId'] : $item['Id'];
+				$embyItem['nowPlayingImageType'] = (isset($item['ParentBackdropItemId']) ? "Primary" : "Backdrop");
+				$embyItem['nowPlayingTitle'] = @$item['AlbumArtist'] . ' - ' . @$item['Name'];
+				$embyItem['nowPlayingBottom'] = @$item['Album'];
+				break;
+			case 'Movie':
+				$embyItem['type'] = 'movie';
+				$embyItem['title'] = $item['Name'];
+				$embyItem['secondaryTitle'] = '';
+				$embyItem['summary'] = '';
+				$embyItem['ratingKey'] = $item['Id'];
+				$embyItem['thumb'] = $item['Id'];
+				$embyItem['key'] = $item['Id'] . "-list";
+				$embyItem['nowPlayingThumb'] = $item['Id'];
+				$embyItem['nowPlayingKey'] = $item['Id'] . "-np";
+				$embyItem['metadataKey'] = $item['Id'];
+				$embyItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? "Thumb" : (isset($item['BackdropImageTags']) ? "Backdrop" : false);
+				$embyItem['nowPlayingTitle'] = @$item['Name'];
+				$embyItem['nowPlayingBottom'] = @$item['ProductionYear'];
+				break;
+			case 'Video':
+				$embyItem['type'] = 'video';
+				$embyItem['title'] = $item['Name'];
+				$embyItem['secondaryTitle'] = '';
+				$embyItem['summary'] = '';
+				$embyItem['ratingKey'] = $item['Id'];
+				$embyItem['thumb'] = $item['Id'];
+				$embyItem['key'] = $item['Id'] . "-list";
+				$embyItem['nowPlayingThumb'] = $item['Id'];
+				$embyItem['nowPlayingKey'] = $item['Id'] . "-np";
+				$embyItem['metadataKey'] = $item['Id'];
+				$embyItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? "Thumb" : (isset($item['BackdropImageTags']) ? "Backdrop" : false);
+				$embyItem['nowPlayingTitle'] = @$item['Name'];
+				$embyItem['nowPlayingBottom'] = @$item['ProductionYear'];
+				break;
+			default:
+				return false;
+		}
+		$embyItem['uid'] = $item['Id'];
+		$embyItem['imageType'] = (isset($item['ImageTags']['Primary']) ? "Primary" : false);
+		$embyItem['elapsed'] = isset($itemDetails['PlayState']['PositionTicks']) && $itemDetails['PlayState']['PositionTicks'] !== '0' ? (int)$itemDetails['PlayState']['PositionTicks'] : null;
+		$embyItem['duration'] = isset($itemDetails['NowPlayingItem']['RunTimeTicks']) ? (int)$itemDetails['NowPlayingItem']['RunTimeTicks'] : (int)(isset($item['RunTimeTicks']) ? $item['RunTimeTicks'] : '');
+		$embyItem['watched'] = ($embyItem['elapsed'] && $embyItem['duration'] ? floor(($embyItem['elapsed'] / $embyItem['duration']) * 100) : 0);
+		$embyItem['transcoded'] = isset($itemDetails['TranscodingInfo']['CompletionPercentage']) ? floor((int)$itemDetails['TranscodingInfo']['CompletionPercentage']) : 100;
+		$embyItem['stream'] = @$itemDetails['PlayState']['PlayMethod'];
+		$embyItem['id'] = $item['ServerId'];
+		$embyItem['session'] = @$itemDetails['DeviceId'];
+		$embyItem['bandwidth'] = isset($itemDetails['TranscodingInfo']['Bitrate']) ? $itemDetails['TranscodingInfo']['Bitrate'] / 1000 : '';
+		$embyItem['bandwidthType'] = 'wan';
+		$embyItem['sessionType'] = (@$itemDetails['PlayState']['PlayMethod'] == 'Transcode') ? 'Transcoding' : 'Direct Playing';
+		$embyItem['state'] = ((@(string)$itemDetails['PlayState']['IsPaused'] == '1') ? "pause" : "play");
+		$embyItem['user'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? @(string)$itemDetails['UserName'] : "";
+		$embyItem['userThumb'] = '';
+		$embyItem['userAddress'] = (isset($itemDetails['RemoteEndPoint']) ? $itemDetails['RemoteEndPoint'] : "x.x.x.x");
+		$embyURL = 'https://app.emby.media/#!/item/item.html?id=';
+		$embyItem['address'] = $this->config['embyTabURL'] ? rtrim($this->config['embyTabURL'], '/') . "/web/#!/item/item.html?id=" . $embyItem['uid'] : $embyURL . $embyItem['uid'] . "&serverId=" . $embyItem['id'];
+		$embyItem['nowPlayingOriginalImage'] = 'api/v2/homepage/image?source=emby&type=' . $embyItem['nowPlayingImageType'] . '&img=' . $embyItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $embyItem['nowPlayingKey'] . '$' . $this->randString();
+		$embyItem['originalImage'] = 'api/v2/homepage/image?source=emby&type=' . $embyItem['imageType'] . '&img=' . $embyItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $embyItem['key'] . '$' . $this->randString();
+		$embyItem['openTab'] = $this->config['embyTabURL'] && $this->config['embyTabName'] ? true : false;
+		$embyItem['tabName'] = $this->config['embyTabName'] ? $this->config['embyTabName'] : '';
+		// Stream info
+		$embyItem['userStream'] = array(
+			'platform' => @(string)$itemDetails['Client'],
+			'product' => @(string)$itemDetails['Client'],
+			'device' => @(string)$itemDetails['DeviceName'],
+			'stream' => @$itemDetails['PlayState']['PlayMethod'],
+			'videoResolution' => isset($itemDetails['NowPlayingItem']['MediaStreams'][0]['Width']) ? $itemDetails['NowPlayingItem']['MediaStreams'][0]['Width'] : '',
+			'throttled' => false,
+			'sourceVideoCodec' => isset($itemDetails['NowPlayingItem']['MediaStreams'][0]) ? $itemDetails['NowPlayingItem']['MediaStreams'][0]['Codec'] : '',
+			'videoCodec' => @$itemDetails['TranscodingInfo']['VideoCodec'],
+			'audioCodec' => @$itemDetails['TranscodingInfo']['AudioCodec'],
+			'sourceAudioCodec' => isset($itemDetails['NowPlayingItem']['MediaStreams'][1]) ? $itemDetails['NowPlayingItem']['MediaStreams'][1]['Codec'] : (isset($itemDetails['NowPlayingItem']['MediaStreams'][0]) ? $itemDetails['NowPlayingItem']['MediaStreams'][0]['Codec'] : ''),
+			'videoDecision' => $this->streamType(@$itemDetails['PlayState']['PlayMethod']),
+			'audioDecision' => $this->streamType(@$itemDetails['PlayState']['PlayMethod']),
+			'container' => isset($itemDetails['NowPlayingItem']['Container']) ? $itemDetails['NowPlayingItem']['Container'] : '',
+			'audioChannels' => @$itemDetails['TranscodingInfo']['AudioChannels']
+		);
+		// Genre catch all
+		if (isset($item['Genres'])) {
+			$genres = array();
+			foreach ($item['Genres'] as $genre) {
+				$genres[] = $genre;
+			}
+		}
+		// Actor catch all
+		if (isset($item['People'])) {
+			$actors = array();
+			foreach ($item['People'] as $key => $value) {
+				if (@$value['PrimaryImageTag'] && @$value['Role']) {
+					if (file_exists($cacheDirectory . (string)$value['Id'] . '-cast.jpg')) {
+						$actorImage = $cacheDirectoryWeb . (string)$value['Id'] . '-cast.jpg';
+					}
+					if (file_exists($cacheDirectory . (string)$value['Id'] . '-cast.jpg') && (time() - 604800) > filemtime($cacheDirectory . (string)$value['Id'] . '-cast.jpg') || !file_exists($cacheDirectory . (string)$value['Id'] . '-cast.jpg')) {
+						$actorImage = 'api/v2/homepage/image?source=emby&type=Primary&img=' . (string)$value['Id'] . '&height=' . $actorHeight . '&width=' . $actorWidth . '&key=' . (string)$value['Id'] . '-cast';
+					}
+					$actors[] = array(
+						'name' => (string)$value['Name'],
+						'role' => (string)$value['Role'],
+						'thumb' => $actorImage
+					);
+				}
+			}
+		}
+		// Metadata information
+		$embyItem['metadata'] = array(
+			'guid' => $item['Id'],
+			'summary' => @(string)$item['Overview'],
+			'rating' => @(string)$item['CommunityRating'],
+			'duration' => @(string)$item['RunTimeTicks'],
+			'originallyAvailableAt' => @(string)$item['PremiereDate'],
+			'year' => (string)isset($item['ProductionYear']) ? $item['ProductionYear'] : '',
+			//'studio' => (string)$item['studio'],
+			'tagline' => @(string)$item['Taglines'][0],
+			'genres' => (isset($item['Genres'])) ? $genres : '',
+			'actors' => (isset($item['People'])) ? $actors : ''
+		);
+		if (file_exists($cacheDirectory . $embyItem['nowPlayingKey'] . '.jpg')) {
+			$embyItem['nowPlayingImageURL'] = $cacheDirectoryWeb . $embyItem['nowPlayingKey'] . '.jpg';
+		}
+		if (file_exists($cacheDirectory . $embyItem['key'] . '.jpg')) {
+			$embyItem['imageURL'] = $cacheDirectoryWeb . $embyItem['key'] . '.jpg';
+		}
+		if (file_exists($cacheDirectory . $embyItem['nowPlayingKey'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $embyItem['nowPlayingKey'] . '.jpg') || !file_exists($cacheDirectory . $embyItem['nowPlayingKey'] . '.jpg')) {
+			$embyItem['nowPlayingImageURL'] = 'api/v2/homepage/image?source=emby&type=' . $embyItem['nowPlayingImageType'] . '&img=' . $embyItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $embyItem['nowPlayingKey'] . '';
+		}
+		if (file_exists($cacheDirectory . $embyItem['key'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $embyItem['key'] . '.jpg') || !file_exists($cacheDirectory . $embyItem['key'] . '.jpg')) {
+			$embyItem['imageURL'] = 'api/v2/homepage/image?source=emby&type=' . $embyItem['imageType'] . '&img=' . $embyItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $embyItem['key'] . '';
+		}
+		if (!$embyItem['nowPlayingThumb']) {
+			$embyItem['nowPlayingOriginalImage'] = $embyItem['nowPlayingImageURL'] = "plugins/images/cache/no-np.png";
+			$embyItem['nowPlayingKey'] = "no-np";
+		}
+		if (!$embyItem['thumb']) {
+			$embyItem['originalImage'] = $embyItem['imageURL'] = "plugins/images/cache/no-list.png";
+			$embyItem['key'] = "no-list";
+		}
+		if (isset($useImage)) {
+			$embyItem['useImage'] = $useImage;
+		}
+		return $embyItem;
+	}
+	
+}

+ 152 - 0
api/homepage/healthchecks.php

@@ -0,0 +1,152 @@
+<?php
+
+trait HealthChecksHomepageItem
+{
+	public function healthChecksSettingsArray()
+	{
+		return array(
+			'name' => 'HealthChecks',
+			'enabled' => true,
+			'image' => 'plugins/images/tabs/healthchecks.png',
+			'category' => 'Monitor',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageHealthChecksEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageHealthChecksEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageHealthChecksAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageHealthChecksAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'healthChecksURL',
+						'label' => 'URL',
+						'value' => $this->config['healthChecksURL'],
+						'help' => 'URL for HealthChecks API',
+						'placeholder' => 'HealthChecks API URL'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'healthChecksToken',
+						'label' => 'Token',
+						'value' => $this->config['healthChecksToken']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'input',
+						'name' => 'healthChecksTags',
+						'label' => 'Tags',
+						'value' => $this->config['healthChecksTags'],
+						'help' => 'Pull only checks with this tag - Blank for all',
+						'placeholder' => 'Multiple tags using CSV - tag1,tag2'
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageHealthChecksRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageHealthChecksRefresh'],
+						'options' => $this->timeOptions()
+					),
+				),
+			)
+		);
+	}
+	
+	public function healthChecksHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageHealthChecksEnabled'
+				],
+				'auth' => [
+					'homepageHealthChecksAuth'
+				],
+				'not_empty' => [
+					'healthChecksURL',
+					'healthChecksToken'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderhealthchecks()
+	{
+		if ($this->homepageItemPermissions($this->healthChecksHomepagePermissions('main'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Health Checks...</h2></div>
+					<script>
+						// Health Checks
+						homepageHealthChecks("' . $this->config['healthChecksTags'] . '","' . $this->config['homepageHealthChecksRefresh'] . '");
+						// End Health Checks
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	public function getHealthChecks($tags = null)
+	{
+		if (!$this->homepageItemPermissions($this->healthChecksHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$api['content']['checks'] = array();
+		$tags = ($tags) ? $this->healthChecksTags($tags) : '';
+		$healthChecks = explode(',', $this->config['healthChecksToken']);
+		foreach ($healthChecks as $token) {
+			$url = $this->qualifyURL($this->config['healthChecksURL']) . '/' . $tags;
+			try {
+				$headers = array('X-Api-Key' => $token);
+				$options = ($this->localURL($url)) ? array('verify' => false) : array();
+				$response = Requests::get($url, $headers, $options);
+				if ($response->success) {
+					$healthResults = json_decode($response->body, true);
+					$api['content']['checks'] = array_merge($api['content']['checks'], $healthResults['checks']);
+				}
+			} catch (Requests_Exception $e) {
+				$this->writeLog('error', 'HealthChecks Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			};
+		}
+		usort($api['content']['checks'], function ($a, $b) {
+			return $a['status'] <=> $b['status'];
+		});
+		$api['content']['checks'] = isset($api['content']['checks']) ? $api['content']['checks'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+	
+	public function healthChecksTags($tags)
+	{
+		$return = '?tag=';
+		if (!$tags) {
+			return '';
+		} elseif ($tags == '*') {
+			return '';
+		} else {
+			if (strpos($tags, ',') !== false) {
+				$list = explode(',', $tags);
+				return $return . implode("&tag=", $list);
+			} else {
+				return $return . $tags;
+			}
+		}
+	}
+}

+ 145 - 0
api/homepage/html.php

@@ -0,0 +1,145 @@
+<?php
+
+trait HTMLHomepageItem
+{
+	public function htmlOneSettingsArray()
+	{
+		return array(
+			'name' => 'CustomHTML-1',
+			'enabled' => strpos('personal,business', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/custom1.png',
+			'category' => 'Custom',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageCustomHTMLoneEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageCustomHTMLoneEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageCustomHTMLoneAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageCustomHTMLoneAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Code' => array(
+					array(
+						'type' => 'textbox',
+						'name' => 'customHTMLone',
+						'class' => 'hidden customHTMLoneTextarea',
+						'label' => '',
+						'value' => $this->config['customHTMLone'],
+					),
+					array(
+						'type' => 'html',
+						'override' => 12,
+						'label' => 'Custom HTML/JavaScript',
+						'html' => '<button type="button" class="hidden savecustomHTMLoneTextarea btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customHTMLoneEditor" style="height:300px">' . htmlentities($this->config['customHTMLone']) . '</div>'
+					),
+				)
+			)
+		);
+	}
+	
+	public function htmlTwoSettingsArray()
+	{
+		return array(
+			'name' => 'CustomHTML-2',
+			'enabled' => strpos('personal,business', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/custom2.png',
+			'category' => 'Custom',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageCustomHTMLtwoEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageCustomHTMLtwoEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageCustomHTMLtwoAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageCustomHTMLtwoAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Code' => array(
+					array(
+						'type' => 'textbox',
+						'name' => 'customHTMLtwo',
+						'class' => 'hidden customHTMLtwoTextarea',
+						'label' => '',
+						'value' => $this->config['customHTMLtwo'],
+					),
+					array(
+						'type' => 'html',
+						'override' => 12,
+						'label' => 'Custom HTML/JavaScript',
+						'html' => '<button type="button" class="hidden savecustomHTMLtwoTextarea btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customHTMLtwoEditor" style="height:300px">' . htmlentities($this->config['customHTMLtwo']) . '</div>'
+					),
+				)
+			)
+		);
+	}
+	
+	public function htmlHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'one' => [
+				'enabled' => [
+					'homepageCustomHTMLoneEnabled'
+				],
+				'auth' => [
+					'homepageCustomHTMLoneAuth'
+				],
+				'not_empty' => [
+					'customHTMLone'
+				]
+			],
+			'two' => [
+				'enabled' => [
+					'homepageCustomHTMLtwoEnabled'
+				],
+				'auth' => [
+					'homepageCustomHTMLtwoAuth'
+				],
+				'not_empty' => [
+					'customHTMLtwo'
+				]
+			]
+		];
+		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>
+				';
+		}
+	}
+	
+	public function homepageOrdercustomhtmlTwo()
+	{
+		if ($this->homepageItemPermissions($this->htmlHomepagePermissions('two'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					' . $this->config['customHTMLtwo'] . '
+				</div>
+				';
+		}
+	}
+}

+ 295 - 0
api/homepage/ical.php

@@ -0,0 +1,295 @@
+<?php
+
+trait ICalHomepageItem
+{
+	public function calendarDaysCheck($entryStart, $entryEnd)
+	{
+		$success = false;
+		$entryStart = intval($entryStart);
+		$entryEnd = intval($entryEnd);
+		if ($entryStart >= 0 && $entryEnd <= 0) {
+			$success = true;
+		}
+		return $success;
+	}
+	
+	public function calendarStandardizeTimezone($timezone)
+	{
+		switch ($timezone) {
+			case('CST'):
+			case('Central Time'):
+			case('Central Standard Time'):
+				$timezone = 'America/Chicago';
+				break;
+			case('CET'):
+			case('Central European Time'):
+				$timezone = 'Europe/Berlin';
+				break;
+			case('EST'):
+			case('Eastern Time'):
+			case('Eastern Standard Time'):
+				$timezone = 'America/New_York';
+				break;
+			case('PST'):
+			case('Pacific Time'):
+			case('Pacific Standard Time'):
+				$timezone = 'America/Los_Angeles';
+				break;
+			case('China Time'):
+			case('China Standard Time'):
+				$timezone = 'Asia/Beijing';
+				break;
+			case('IST'):
+			case('India Time'):
+			case('India Standard Time'):
+				$timezone = 'Asia/New_Delhi';
+				break;
+			case('JST');
+			case('Japan Time'):
+			case('Japan Standard Time'):
+				$timezone = 'Asia/Tokyo';
+				break;
+		}
+		return $timezone;
+	}
+	
+	public function getCalenderRepeat($value)
+	{
+		//FREQ=DAILY
+		//RRULE:FREQ=WEEKLY;BYDAY=TH
+		$first = explode('=', $value);
+		if (count($first) > 1) {
+			$second = explode(';', $first[1]);
+		} else {
+			return $value;
+		}
+		if ($second) {
+			return $second[0];
+		} else {
+			return $first[1];
+		}
+	}
+	
+	public function getCalenderRepeatUntil($value)
+	{
+		$first = explode('UNTIL=', $value);
+		if (count($first) > 1) {
+			if (strpos($first[1], ';') !== false) {
+				$check = explode(';', $first[1]);
+				return $check[0];
+			} else {
+				return $first[1];
+			}
+		} else {
+			return false;
+		}
+	}
+	
+	public function getCalenderRepeatCount($value)
+	{
+		$first = explode('COUNT=', $value);
+		if (count($first) > 1) {
+			return $first[1];
+		} else {
+			return false;
+		}
+	}
+	
+	public function file_get_contents_curl($url)
+	{
+		$ch = curl_init();
+		curl_setopt($ch, CURLOPT_AUTOREFERER, true);
+		curl_setopt($ch, CURLOPT_HEADER, 0);
+		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+		curl_setopt($ch, CURLOPT_URL, $url);
+		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
+		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
+		$data = curl_exec($ch);
+		curl_close($ch);
+		return $data;
+	}
+	
+	public function getIcsEventsAsArray($file)
+	{
+		$icalString = $this->file_get_contents_curl($file);
+		$icsDates = array();
+		/* Explode the ICs Data to get datas as array according to string ‘BEGIN:’ */
+		$icsData = explode("BEGIN:", $icalString);
+		/* Iterating the icsData value to make all the start end dates as sub array */
+		foreach ($icsData as $key => $value) {
+			$icsDatesMeta [$key] = explode("\n", $value);
+		}
+		/* Itearting the Ics Meta Value */
+		foreach ($icsDatesMeta as $key => $value) {
+			foreach ($value as $subKey => $subValue) {
+				/* to get ics events in proper order */
+				$icsDates = $this->getICSDates($key, $subKey, $subValue, $icsDates);
+			}
+		}
+		return $icsDates;
+	}
+	
+	/* funcion is to avaid the elements wich is not having the proper start, end  and summary informations */
+	public function getICSDates($key, $subKey, $subValue, $icsDates)
+	{
+		if ($key != 0 && $subKey == 0) {
+			$icsDates [$key] ["BEGIN"] = $subValue;
+		} else {
+			$subValueArr = explode(":", $subValue, 2);
+			if (isset ($subValueArr [1])) {
+				$icsDates [$key] [$subValueArr [0]] = $subValueArr [1];
+			}
+		}
+		return $icsDates;
+	}
+	
+	public function getICalendar()
+	{
+		if (!$this->config['homepageCalendarEnabled']) {
+			$this->setAPIResponse('error', 'iCal homepage item is not enabled', 409);
+			return false;
+		}
+		if (!$this->qualifyRequest($this->config['homepageCalendarAuth'])) {
+			$this->setAPIResponse('error', 'User not approved to view this homepage item', 401);
+			return false;
+		}
+		if (empty($this->config['calendariCal'])) {
+			$this->setAPIResponse('error', 'iCal URL is not defined', 422);
+			return false;
+		}
+		$calendarItems = array();
+		$calendars = array();
+		$calendarURLList = explode(',', $this->config['calendariCal']);
+		$icalEvents = array();
+		foreach ($calendarURLList as $key => $value) {
+			$icsEvents = $this->getIcsEventsAsArray($value);
+			if (isset($icsEvents) && !empty($icsEvents)) {
+				$timeZone = isset($icsEvents [1] ['X-WR-TIMEZONE']) ? trim($icsEvents[1]['X-WR-TIMEZONE']) : date_default_timezone_get();
+				$originalTimeZone = isset($icsEvents [1] ['X-WR-TIMEZONE']) ? str_replace('"', '', trim($icsEvents[1]['X-WR-TIMEZONE'])) : false;
+				unset($icsEvents [1]);
+				foreach ($icsEvents as $icsEvent) {
+					$startKeys = $this->array_filter_key($icsEvent, function ($key) {
+						return strpos($key, 'DTSTART') === 0;
+					});
+					$endKeys = $this->array_filter_key($icsEvent, function ($key) {
+						return strpos($key, 'DTEND') === 0;
+					});
+					if (!empty($startKeys) && !empty($endKeys) && isset($icsEvent['SUMMARY'])) {
+						/* Getting start date and time */
+						$repeat = isset($icsEvent ['RRULE']) ? $icsEvent ['RRULE'] : false;
+						if (!$originalTimeZone) {
+							$tzKey = array_keys($startKeys);
+							if (strpos($tzKey[0], 'TZID=') !== false) {
+								$originalTimeZone = explode('TZID=', (string)$tzKey[0]);
+								$originalTimeZone = (count($originalTimeZone) >= 2) ? str_replace('"', '', $originalTimeZone[1]) : false;
+							}
+						}
+						$start = reset($startKeys);
+						$end = reset($endKeys);
+						$totalDays = $this->config['calendarStart'] + $this->config['calendarEnd'];
+						if ($repeat) {
+							$repeatOverride = $this->getCalenderRepeatCount(trim($icsEvent["RRULE"]));
+							switch (trim(strtolower($this->getCalenderRepeat($repeat)))) {
+								case 'daily':
+									$repeat = ($repeatOverride) ? $repeatOverride : $totalDays;
+									$term = 'days';
+									break;
+								case 'weekly':
+									$repeat = ($repeatOverride) ? $repeatOverride : round($totalDays / 7);
+									$term = 'weeks';
+									break;
+								case 'monthly':
+									$repeat = ($repeatOverride) ? $repeatOverride : round($totalDays / 30);
+									$term = 'months';
+									break;
+								case 'yearly':
+									$repeat = ($repeatOverride) ? $repeatOverride : round($totalDays / 365);
+									$term = 'years';
+									break;
+								default:
+									$repeat = ($repeatOverride) ? $repeatOverride : $totalDays;
+									$term = 'days';
+									break;
+							}
+						} else {
+							$repeat = 1;
+							$term = 'day';
+						}
+						$calendarTimes = 0;
+						while ($calendarTimes < $repeat) {
+							$currentDate = new DateTime ($this->currentTime);
+							$oldestDay = new DateTime ($this->currentTime);
+							$oldestDay->modify('-' . $this->config['calendarStart'] . ' days');
+							$newestDay = new DateTime ($this->currentTime);
+							$newestDay->modify('+' . $this->config['calendarEnd'] . ' days');
+							/* Converting to datetime and apply the timezone to get proper date time */
+							$startDt = new DateTime ($start);
+							/* Getting end date with time */
+							$endDt = new DateTime ($end);
+							if ($calendarTimes !== 0) {
+								$dateDiff = date_diff($startDt, $currentDate);
+								$startDt->modify($dateDiff->format('%R') . (round(($dateDiff->days) / 7)) . ' weeks');
+								$startDt->modify('+' . $calendarTimes . ' ' . $term);
+								$endDt->modify($dateDiff->format('%R') . (round(($dateDiff->days) / 7)) . ' weeks');
+								$endDt->modify('+' . $calendarTimes . ' ' . $term);
+							} elseif ($calendarTimes == 0 && $repeat !== 1) {
+								$dateDiff = date_diff($startDt, $currentDate);
+								$startDt->modify($dateDiff->format('%R') . (round(($dateDiff->days) / 7)) . ' weeks');
+								$endDt->modify($dateDiff->format('%R') . (round(($dateDiff->days) / 7)) . ' weeks');
+							}
+							$calendarStartDiff = date_diff($startDt, $newestDay);
+							$calendarEndDiff = date_diff($startDt, $oldestDay);
+							if ($originalTimeZone && $originalTimeZone !== 'UTC' && (strpos($start, 'Z') == false)) {
+								$originalTimeZone = $this->calendarStandardizeTimezone($originalTimeZone);
+								$dateTimeOriginalTZ = new DateTimeZone($originalTimeZone);
+								$dateTimeOriginal = new DateTime('now', $dateTimeOriginalTZ);
+								$dateTimeUTCTZ = new DateTimeZone(date_default_timezone_get());
+								$dateTimeUTC = new DateTime('now', $dateTimeUTCTZ);
+								$dateTimeOriginalOffset = $dateTimeOriginal->getOffset() / 3600;
+								$dateTimeUTCOffset = $dateTimeUTC->getOffset() / 3600;
+								$diff = $dateTimeUTCOffset - $dateTimeOriginalOffset;
+								$startDt->modify('+ ' . $diff . ' hour');
+								$endDt->modify('+ ' . $diff . ' hour');
+							}
+							$startDt->setTimeZone(new DateTimezone ($timeZone));
+							$endDt->setTimeZone(new DateTimezone ($timeZone));
+							$startDate = $startDt->format(DateTime::ATOM);
+							$endDate = $endDt->format(DateTime::ATOM);
+							if (new DateTime() < $endDt) {
+								$extraClass = 'text-info';
+							} else {
+								$extraClass = 'text-success';
+							}
+							/* Getting the name of event */
+							$eventName = $icsEvent['SUMMARY'];
+							if (!$this->calendarDaysCheck($calendarStartDiff->format('%R') . $calendarStartDiff->days, $calendarEndDiff->format('%R') . $calendarEndDiff->days)) {
+								break;
+							}
+							if (isset($icsEvent["RRULE"]) && $this->getCalenderRepeatUntil(trim($icsEvent["RRULE"]))) {
+								$untilDate = new DateTime ($this->getCalenderRepeatUntil(trim($icsEvent["RRULE"])));
+								$untilDiff = date_diff($currentDate, $untilDate);
+								if ($untilDiff->days > 0) {
+									break;
+								}
+							}
+							$icalEvents[] = array(
+								'title' => $eventName,
+								'imagetype' => 'calendar-o text-warning text-custom-calendar ' . $extraClass,
+								'imagetypeFilter' => 'ical',
+								'className' => 'bg-calendar calendar-item bg-custom-calendar',
+								'start' => $startDate,
+								'end' => $endDate,
+								'bgColor' => str_replace('text', 'bg', $extraClass),
+							);
+							$calendarTimes = $calendarTimes + 1;
+						}
+					}
+				}
+			}
+		}
+		$calendarSources = $icalEvents;
+		$this->setAPIResponse('success', null, 200, $calendarSources);
+		return $calendarSources;
+	}
+}

+ 119 - 0
api/homepage/jackett.php

@@ -0,0 +1,119 @@
+<?php
+
+trait JackettHomepageItem
+{
+	public function jackettSettingsArray()
+	{
+		return array(
+			'name' => 'Jackett',
+			'enabled' => true,
+			'image' => 'plugins/images/tabs/jackett.png',
+			'category' => 'Utility',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageJackettEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageJackettEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageJackettAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageJackettAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'jackettURL',
+						'label' => 'URL',
+						'value' => $this->config['jackettURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'jackettToken',
+						'label' => 'Token',
+						'value' => $this->config['jackettToken']
+					)
+				),
+				'Options' => array(),
+			)
+		);
+	}
+	
+	public function jackettHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageJackettEnabled'
+				],
+				'auth' => [
+					'homepageJackettAuth'
+				],
+				'not_empty' => [
+					'jackettURL',
+					'jackettToken'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderJackett()
+	{
+		if ($this->homepageItemPermissions($this->jackettHomepagePermissions('main'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Jackett...</h2></div>
+					<script>
+						// Jackett
+						homepageJackett();
+						// End Jackett
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	public function searchJackettIndexers($query = null)
+	{
+		if (!$this->homepageItemPermissions($this->jackettHomepagePermissions('main'), true)) {
+			return false;
+		}
+		if (!$query) {
+			$this->setAPIResponse('error', 'Query was not supplied', 422);
+			return false;
+		}
+		$apiURL = $this->qualifyURL($this->config['jackettURL']);
+		$endpoint = $apiURL . '/api/v2.0/indexers/all/results?apikey=' . $this->config['jackettToken'] . '&Query=' . urlencode($query);
+		try {
+			$headers = array();
+			$options = array('timeout' => 60);
+			$response = Requests::get($endpoint, $headers, $options);
+			if ($response->success) {
+				$apiData = json_decode($response->body, true);
+				$api['content'] = $apiData;
+				unset($apiData);
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Weather And Air Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 200 - 0
api/homepage/jdownloader.php

@@ -0,0 +1,200 @@
+<?php
+
+trait JDownloaderHomepageItem
+{
+	public function jDownloaderSettingsArray()
+	{
+		return array(
+			'name' => 'JDownloader',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/jdownloader.png',
+			'category' => 'Downloader',
+			'settings' => array(
+				'custom' => '
+				<div class="row">
+                    <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">
+									<ul class="list-icons">
+                                        <li><i class="fa fa-chevron-right text-danger"></i> <a href="https://pypi.org/project/myjd-api/" target="_blank">Download [myjd-api] Module</a></li>
+                                        <li><i class="fa fa-chevron-right text-danger"></i> Add <b>/api/myjd</b> to the URL if you are using <a href="https://pypi.org/project/RSScrawler/" target="_blank">RSScrawler</a></li>
+                                    </ul>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+				</div>
+				',
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageJdownloaderEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageJdownloaderEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageJdownloaderAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageJdownloaderAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'jdownloaderURL',
+						'label' => 'URL',
+						'value' => $this->config['jdownloaderURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'select',
+						'name' => 'homepageDownloadRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageDownloadRefresh'],
+						'options' => $this->timeOptions()
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'jdownloaderCombine',
+						'label' => 'Add to Combined Downloader',
+						'value' => $this->config['jdownloaderCombine']
+					),
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'jdownloader\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	public function testConnectionJDownloader()
+	{
+		if (empty($this->config['jdownloaderURL'])) {
+			$this->setAPIResponse('error', 'JDownloader URL is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['jdownloaderURL']);
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false, 'timeout' => 30) : array('timeout' => 30);
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$this->setAPIResponse('success', 'API Connection succeeded', 200);
+				return true;
+			} else {
+				$this->setAPIResponse('success', 'JDownloader Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'JDownloader Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+	}
+	
+	public function jDownloaderHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageJdownloaderEnabled'
+				],
+				'auth' => [
+					'homepageJdownloaderAuth'
+				],
+				'not_empty' => [
+					'jdownloaderURL'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderjdownloader()
+	{
+		if ($this->homepageItemPermissions($this->jDownloaderHomepagePermissions('main'))) {
+			$loadingBox = ($this->config['jdownloaderCombine']) ? '' : '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+			$builder = ($this->config['jdownloaderCombine']) ? 'buildDownloaderCombined(\'jdownloader\');' : '$("#' . __FUNCTION__ . '").html(buildDownloader("jdownloader"));';
+			return '
+				<div id="' . __FUNCTION__ . '">
+					' . $loadingBox . '
+					<script>
+		                // homepageOrderjdownloader
+		                ' . $builder . '
+		                homepageDownloader("jdownloader", "' . $this->config['homepageDownloadRefresh'] . '");
+		                // End homepageOrderjdownloader
+	                </script>
+				</div>
+				';
+		}
+	}
+	
+	public function getJdownloaderHomepageQueue()
+	{
+		if (!$this->homepageItemPermissions($this->jDownloaderHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['jdownloaderURL']);
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false, 'timeout' => 30) : array('timeout' => 30);
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$temp = json_decode($response->body, true);
+				$packages = $temp['packages'];
+				if ($packages['downloader']) {
+					$api['content']['queueItems'] = $packages['downloader'];
+				} else {
+					$api['content']['queueItems'] = [];
+				}
+				if ($packages['linkgrabber_decrypted']) {
+					$api['content']['grabberItems'] = $packages['linkgrabber_decrypted'];
+				} else {
+					$api['content']['grabberItems'] = [];
+				}
+				if ($packages['linkgrabber_failed']) {
+					$api['content']['encryptedItems'] = $packages['linkgrabber_failed'];
+				} else {
+					$api['content']['encryptedItems'] = [];
+				}
+				if ($packages['linkgrabber_offline']) {
+					$api['content']['offlineItems'] = $packages['linkgrabber_offline'];
+				} else {
+					$api['content']['offlineItems'] = [];
+				}
+				$api['content']['$status'] = array($temp['downloader_state'], $temp['grabber_collecting'], $temp['update_ready']);
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'JDownloader Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 630 - 0
api/homepage/jellyfin.php

@@ -0,0 +1,630 @@
+<?php
+
+trait JellyfinHomepageItem
+{
+	
+	public function jellyfinSettingsArray()
+	{
+		return array(
+			'name' => 'Jellyfin',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/jellyfin.png',
+			'category' => 'Media Server',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageJellyfinEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageJellyfinEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageJellyfinAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageJellyfinAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'jellyfinURL',
+						'label' => 'URL',
+						'value' => $this->config['jellyfinURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'jellyfinToken',
+						'label' => 'Token',
+						'value' => $this->config['jellyfinToken']
+					)
+				),
+				'Active Streams' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageJellyfinStreams',
+						'label' => 'Enable',
+						'value' => $this->config['homepageJellyfinStreams']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageJellyStreamsAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $this->config['homepageJellyStreamsAuth'],
+						'options' => $this->groupOptions
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'homepageShowStreamNames',
+						'label' => 'User Information',
+						'value' => $this->config['homepageShowStreamNames']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageShowStreamNamesAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $this->config['homepageShowStreamNamesAuth'],
+						'options' => $this->groupOptions
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageStreamRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageStreamRefresh'],
+						'options' => $this->timeOptions()
+					),
+				),
+				'Recent Items' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageJellyfinRecent',
+						'label' => 'Enable',
+						'value' => $this->config['homepageJellyfinRecent']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageJellyfinRecentAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $this->config['homepageJellyfinRecentAuth'],
+						'options' => $this->groupOptions
+					),
+					array(
+						'type' => 'number',
+						'name' => 'homepageRecentLimit',
+						'label' => 'Item Limit',
+						'value' => $this->config['homepageRecentLimit'],
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageRecentRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageRecentRefresh'],
+						'options' => $this->timeOptions()
+					),
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'input',
+						'name' => 'jellyfinTabName',
+						'label' => 'Jellyfin Tab Name',
+						'value' => $this->config['jellyfinTabName'],
+						'placeholder' => 'Only use if you have Jellyfin in a reverse proxy'
+					),
+					array(
+						'type' => 'input',
+						'name' => 'jellyfinTabURL',
+						'label' => 'Jellyfin Tab WAN URL',
+						'value' => $this->config['jellyfinTabURL'],
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'select',
+						'name' => 'cacheImageSize',
+						'label' => 'Image Cache Size',
+						'value' => $this->config['cacheImageSize'],
+						'options' => array(
+							array(
+								'name' => 'Low',
+								'value' => '.5'
+							),
+							array(
+								'name' => '1x',
+								'value' => '1'
+							),
+							array(
+								'name' => '2x',
+								'value' => '2'
+							),
+							array(
+								'name' => '3x',
+								'value' => '3'
+							)
+						)
+					)
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'jellyfin\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	public function testConnectionJellyfin()
+	{
+		if (empty($this->config['jellyfinURL'])) {
+			$this->setAPIResponse('error', 'Jellyfin URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['jellyfinToken'])) {
+			$this->setAPIResponse('error', 'Jellyfin Token is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['jellyfinURL']);
+		$url = $url . "/Users?api_key=" . $this->config['jellyfinToken'];
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		try {
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$json = json_decode($response->body);
+				if (is_array($json) || is_object($json)) {
+					$this->setAPIResponse('success', 'API Connection succeeded', 200);
+					return true;
+				} else {
+					$this->setAPIResponse('error', 'URL or token incorrect', 409);
+					return false;
+				}
+			} else {
+				$this->setAPIResponse('error', 'Jellyfin Connection Error', 500);
+				return true;
+			}
+		} catch (Requests_Exception $e) {
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function jellyfinHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'streams' => [
+				'enabled' => [
+					'homepageJellyfinEnabled',
+					'homepageJellyfinStreams'
+				],
+				'auth' => [
+					'homepageJellyfinAuth',
+					'homepageJellyStreamsAuth'
+				],
+				'not_empty' => [
+					'jellyfinURL',
+					'jellyfinToken'
+				]
+			],
+			'recent' => [
+				'enabled' => [
+					'homepageJellyfinEnabled',
+					'homepageJellyfinRecent'
+				],
+				'auth' => [
+					'homepageJellyfinAuth',
+					'homepageJellyfinRecentAuth'
+				],
+				'not_empty' => [
+					'jellyfinURL',
+					'jellyfinToken'
+				]
+			],
+			'metadata' => [
+				'enabled' => [
+					'homepageJellyfinEnabled'
+				],
+				'auth' => [
+					'homepageJellyfinAuth'
+				],
+				'not_empty' => [
+					'jellyfinURL',
+					'jellyfinToken'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderjellyfinnowplaying()
+	{
+		if ($this->homepageItemPermissions($this->jellyfinHomepagePermissions('streams'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Now Playing...</h2></div>
+					<script>
+						// Jellyfin Stream
+						homepageStream("jellyfin", "' . $this->config['homepageStreamRefresh'] . '");
+						// End Jellyfin Stream
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	public function homepageOrderjellyfinrecent()
+	{
+		if ($this->homepageItemPermissions($this->jellyfinHomepagePermissions('recent'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Recent...</h2></div>
+					<script>
+						// Jellyfin Recent
+						homepageRecent("jellyfin", "' . $this->config['homepageRecentRefresh'] . '");
+						// End Jellyfin Recent
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	public function getJellyfinHomepageStreams()
+	{
+		if (!$this->homepageItemPermissions($this->jellyfinHomepagePermissions('streams'), true)) {
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['jellyfinURL']);
+		$url = $url . '/Sessions?api_key=' . $this->config['jellyfinToken'] . '&Fields=Overview,People,Genres,CriticRating,Studios,Taglines';
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		try {
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$items = array();
+				$jellyfin = json_decode($response->body, true);
+				foreach ($jellyfin as $child) {
+					if (isset($child['NowPlayingItem']) || isset($child['Name'])) {
+						$items[] = $this->resolveJellyfinItem($child);
+					}
+				}
+				$api['content'] = array_filter($items);
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
+			} else {
+				$this->setAPIResponse('error', 'Jellyfin Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Jellyfin Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function getJellyfinHomepageRecent()
+	{
+		if (!$this->homepageItemPermissions($this->jellyfinHomepagePermissions('recent'), true)) {
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['jellyfinURL']);
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		$username = false;
+		$showPlayed = false;
+		$userId = 0;
+		try {
+			if (isset($this->user['username'])) {
+				$username = strtolower($this->user['username']);
+			}
+			// Get A User
+			$userIds = $url . "/Users?api_key=" . $this->config['jellyfinToken'];
+			$response = Requests::get($userIds, array(), $options);
+			if ($response->success) {
+				$jellyfin = json_decode($response->body, true);
+				foreach ($jellyfin as $value) { // Scan for admin user
+					if (isset($value['Policy']) && isset($value['Policy']['IsAdministrator']) && $value['Policy']['IsAdministrator']) {
+						$userId = $value['Id'];
+					}
+					if ($username && strtolower($value['Name']) == $username) {
+						$userId = $value['Id'];
+						$showPlayed = false;
+						break;
+					}
+				}
+				$url = $url . '/Users/' . $userId . '/Items/Latest?EnableImages=true&Limit=' . $this->config['homepageRecentLimit'] . '&api_key=' . $this->config['jellyfinToken'] . ($showPlayed ? '' : '&IsPlayed=false') . '&Fields=Overview,People,Genres,CriticRating,Studios,Taglines';
+			} else {
+				$this->setAPIResponse('error', 'Jellyfin Error Occurred', 500);
+				return false;
+			}
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$items = array();
+				$jellyfin = json_decode($response->body, true);
+				foreach ($jellyfin as $child) {
+					if (isset($child['NowPlayingItem']) || isset($child['Name'])) {
+						$items[] = $this->resolveJellyfinItem($child);
+					}
+				}
+				$api['content'] = array_filter($items);
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
+			} else {
+				$this->setAPIResponse('error', 'Jellyfin Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Jellyfin Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function getJellyfinHomepageMetadata($array)
+	{
+		if (!$this->homepageItemPermissions($this->jellyfinHomepagePermissions('metadata'), true)) {
+			return false;
+		}
+		$key = $array['key'] ?? null;
+		if (!$key) {
+			$this->setAPIResponse('error', 'Jellyfin Metadata key is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['jellyfinURL']);
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		$username = false;
+		$showPlayed = false;
+		$userId = 0;
+		try {
+			if (isset($this->user['username'])) {
+				$username = strtolower($this->user['username']);
+			}
+			// Get A User
+			$userIds = $url . "/Users?api_key=" . $this->config['jellyfinToken'];
+			$response = Requests::get($userIds, array(), $options);
+			if ($response->success) {
+				$jellyfin = json_decode($response->body, true);
+				foreach ($jellyfin as $value) { // Scan for admin user
+					if (isset($value['Policy']) && isset($value['Policy']['IsAdministrator']) && $value['Policy']['IsAdministrator']) {
+						$userId = $value['Id'];
+					}
+					if ($username && strtolower($value['Name']) == $username) {
+						$userId = $value['Id'];
+						$showPlayed = false;
+						break;
+					}
+				}
+				$url = $url . '/Users/' . $userId . '/Items/' . $key . '?EnableImages=true&Limit=' . $this->config['homepageRecentLimit'] . '&api_key=' . $this->config['jellyfinToken'] . ($showPlayed ? '' : '&IsPlayed=false') . '&Fields=Overview,People,Genres,CriticRating,Studios,Taglines';
+			} else {
+				$this->setAPIResponse('error', 'Jellyfin Error Occurred', 500);
+				return false;
+			}
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$items = array();
+				$jellyfin = json_decode($response->body, true);
+				if (isset($jellyfin['NowPlayingItem']) || isset($jellyfin['Name'])) {
+					$items[] = $this->resolveJellyfinItem($jellyfin);
+				}
+				$api['content'] = array_filter($items);
+				$this->setAPIResponse('success', null, 200, $api);
+				return $api;
+			} else {
+				$this->setAPIResponse('error', 'Jellyfin Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Jellyfin Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function resolveJellyfinItem($itemDetails)
+	{
+		$item = isset($itemDetails['NowPlayingItem']['Id']) ? $itemDetails['NowPlayingItem'] : $itemDetails;
+		// Static Height & Width
+		$height = $this->getCacheImageSize('h');
+		$width = $this->getCacheImageSize('w');
+		$nowPlayingHeight = $this->getCacheImageSize('nph');
+		$nowPlayingWidth = $this->getCacheImageSize('npw');
+		$actorHeight = 450;
+		$actorWidth = 300;
+		// Cache Directories
+		$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+		$cacheDirectoryWeb = 'plugins/images/cache/';
+		// Types
+		switch (@$item['Type']) {
+			case 'Series':
+				$jellyfinItem['type'] = 'tv';
+				$jellyfinItem['title'] = $item['Name'];
+				$jellyfinItem['secondaryTitle'] = '';
+				$jellyfinItem['summary'] = '';
+				$jellyfinItem['ratingKey'] = $item['Id'];
+				$jellyfinItem['thumb'] = $item['Id'];
+				$jellyfinItem['key'] = $item['Id'] . "-list";
+				$jellyfinItem['nowPlayingThumb'] = $item['Id'];
+				$jellyfinItem['nowPlayingKey'] = $item['Id'] . "-np";
+				$jellyfinItem['metadataKey'] = $item['Id'];
+				$jellyfinItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? 'Thumb' : (isset($item['BackdropImageTags'][0]) ? 'Backdrop' : '');
+				break;
+			case 'Episode':
+				$jellyfinItem['type'] = 'tv';
+				$jellyfinItem['title'] = $item['SeriesName'];
+				$jellyfinItem['secondaryTitle'] = '';
+				$jellyfinItem['summary'] = '';
+				$jellyfinItem['ratingKey'] = $item['Id'];
+				$jellyfinItem['thumb'] = (isset($item['SeriesId']) ? $item['SeriesId'] : $item['Id']);
+				$jellyfinItem['key'] = (isset($item['SeriesId']) ? $item['SeriesId'] : $item['Id']) . "-list";
+				$jellyfinItem['nowPlayingThumb'] = isset($item['ParentThumbItemId']) ? $item['ParentThumbItemId'] : (isset($item['ParentBackdropItemId']) ? $item['ParentBackdropItemId'] : false);
+				$jellyfinItem['nowPlayingKey'] = isset($item['ParentThumbItemId']) ? $item['ParentThumbItemId'] . '-np' : (isset($item['ParentBackdropItemId']) ? $item['ParentBackdropItemId'] . '-np' : false);
+				$jellyfinItem['metadataKey'] = $item['Id'];
+				$jellyfinItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? 'Thumb' : (isset($item['ParentBackdropImageTags'][0]) ? 'Backdrop' : '');
+				$jellyfinItem['nowPlayingTitle'] = @$item['SeriesName'] . ' - ' . @$item['Name'];
+				$jellyfinItem['nowPlayingBottom'] = 'S' . @$item['ParentIndexNumber'] . ' · E' . @$item['IndexNumber'];
+				break;
+			case 'MusicAlbum':
+			case 'Audio':
+				$jellyfinItem['type'] = 'music';
+				$jellyfinItem['title'] = $item['Name'];
+				$jellyfinItem['secondaryTitle'] = '';
+				$jellyfinItem['summary'] = '';
+				$jellyfinItem['ratingKey'] = $item['Id'];
+				$jellyfinItem['thumb'] = $item['Id'];
+				$jellyfinItem['key'] = $item['Id'] . "-list";
+				$jellyfinItem['nowPlayingThumb'] = (isset($item['AlbumId']) ? $item['AlbumId'] : @$item['ParentBackdropItemId']);
+				$jellyfinItem['nowPlayingKey'] = $item['Id'] . "-np";
+				$jellyfinItem['metadataKey'] = isset($item['AlbumId']) ? $item['AlbumId'] : $item['Id'];
+				$jellyfinItem['nowPlayingImageType'] = (isset($item['ParentBackdropItemId']) ? "Primary" : "Backdrop");
+				$jellyfinItem['nowPlayingTitle'] = @$item['AlbumArtist'] . ' - ' . @$item['Name'];
+				$jellyfinItem['nowPlayingBottom'] = @$item['Album'];
+				break;
+			case 'Movie':
+				$jellyfinItem['type'] = 'movie';
+				$jellyfinItem['title'] = $item['Name'];
+				$jellyfinItem['secondaryTitle'] = '';
+				$jellyfinItem['summary'] = '';
+				$jellyfinItem['ratingKey'] = $item['Id'];
+				$jellyfinItem['thumb'] = $item['Id'];
+				$jellyfinItem['key'] = $item['Id'] . "-list";
+				$jellyfinItem['nowPlayingThumb'] = $item['Id'];
+				$jellyfinItem['nowPlayingKey'] = $item['Id'] . "-np";
+				$jellyfinItem['metadataKey'] = $item['Id'];
+				$jellyfinItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? "Thumb" : (isset($item['BackdropImageTags']) ? "Backdrop" : false);
+				$jellyfinItem['nowPlayingTitle'] = @$item['Name'];
+				$jellyfinItem['nowPlayingBottom'] = @$item['ProductionYear'];
+				break;
+			case 'Video':
+				$jellyfinItem['type'] = 'video';
+				$jellyfinItem['title'] = $item['Name'];
+				$jellyfinItem['secondaryTitle'] = '';
+				$jellyfinItem['summary'] = '';
+				$jellyfinItem['ratingKey'] = $item['Id'];
+				$jellyfinItem['thumb'] = $item['Id'];
+				$jellyfinItem['key'] = $item['Id'] . "-list";
+				$jellyfinItem['nowPlayingThumb'] = $item['Id'];
+				$jellyfinItem['nowPlayingKey'] = $item['Id'] . "-np";
+				$jellyfinItem['metadataKey'] = $item['Id'];
+				$jellyfinItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? "Thumb" : (isset($item['BackdropImageTags']) ? "Backdrop" : false);
+				$jellyfinItem['nowPlayingTitle'] = @$item['Name'];
+				$jellyfinItem['nowPlayingBottom'] = @$item['ProductionYear'];
+				break;
+			default:
+				return false;
+		}
+		$jellyfinItem['uid'] = $item['Id'];
+		$jellyfinItem['imageType'] = (isset($item['ImageTags']['Primary']) ? "Primary" : false);
+		$jellyfinItem['elapsed'] = isset($itemDetails['PlayState']['PositionTicks']) && $itemDetails['PlayState']['PositionTicks'] !== '0' ? (int)$itemDetails['PlayState']['PositionTicks'] : null;
+		$jellyfinItem['duration'] = isset($itemDetails['NowPlayingItem']['RunTimeTicks']) ? (int)$itemDetails['NowPlayingItem']['RunTimeTicks'] : (int)(isset($item['RunTimeTicks']) ? $item['RunTimeTicks'] : '');
+		$jellyfinItem['watched'] = ($jellyfinItem['elapsed'] && $jellyfinItem['duration'] ? floor(($jellyfinItem['elapsed'] / $jellyfinItem['duration']) * 100) : 0);
+		$jellyfinItem['transcoded'] = isset($itemDetails['TranscodingInfo']['CompletionPercentage']) ? floor((int)$itemDetails['TranscodingInfo']['CompletionPercentage']) : 100;
+		$jellyfinItem['stream'] = @$itemDetails['PlayState']['PlayMethod'];
+		$jellyfinItem['id'] = $item['ServerId'];
+		$jellyfinItem['session'] = @$itemDetails['DeviceId'];
+		$jellyfinItem['bandwidth'] = isset($itemDetails['TranscodingInfo']['Bitrate']) ? $itemDetails['TranscodingInfo']['Bitrate'] / 1000 : '';
+		$jellyfinItem['bandwidthType'] = 'wan';
+		$jellyfinItem['sessionType'] = (@$itemDetails['PlayState']['PlayMethod'] == 'Transcode') ? 'Transcoding' : 'Direct Playing';
+		$jellyfinItem['state'] = ((@(string)$itemDetails['PlayState']['IsPaused'] == '1') ? "pause" : "play");
+		$jellyfinItem['user'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? @(string)$itemDetails['UserName'] : "";
+		$jellyfinItem['userThumb'] = '';
+		$jellyfinItem['userAddress'] = (isset($itemDetails['RemoteEndPoint']) ? $itemDetails['RemoteEndPoint'] : "x.x.x.x");
+		$jellyfinURL = $this->config['jellyfinURL'] . '/web/index.html#!/itemdetails.html?id=';
+		$jellyfinItem['address'] = $this->config['jellyfinTabURL'] ? rtrim($this->config['jellyfinTabURL'], '/') . "/web/#!/item/item.html?id=" . $jellyfinItem['uid'] : $jellyfinURL . $jellyfinItem['uid'] . "&serverId=" . $jellyfinItem['id'];
+		$jellyfinItem['nowPlayingOriginalImage'] = 'api/v2/homepage/image?source=jellyfin&type=' . $jellyfinItem['nowPlayingImageType'] . '&img=' . $jellyfinItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $jellyfinItem['nowPlayingKey'] . '$' . $this->randString();
+		$jellyfinItem['originalImage'] = 'api/v2/homepage/image?source=jellyfin&type=' . $jellyfinItem['imageType'] . '&img=' . $jellyfinItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $jellyfinItem['key'] . '$' . $this->randString();
+		$jellyfinItem['openTab'] = $this->config['jellyfinTabURL'] && $this->config['jellyfinTabName'] ? true : false;
+		$jellyfinItem['tabName'] = $this->config['jellyfinTabName'] ? $this->config['jellyfinTabName'] : '';
+		// Stream info
+		$jellyfinItem['userStream'] = array(
+			'platform' => @(string)$itemDetails['Client'],
+			'product' => @(string)$itemDetails['Client'],
+			'device' => @(string)$itemDetails['DeviceName'],
+			'stream' => @$itemDetails['PlayState']['PlayMethod'],
+			'videoResolution' => isset($itemDetails['NowPlayingItem']['MediaStreams'][0]['Width']) ? $itemDetails['NowPlayingItem']['MediaStreams'][0]['Width'] : '',
+			'throttled' => false,
+			'sourceVideoCodec' => isset($itemDetails['NowPlayingItem']['MediaStreams'][0]) ? $itemDetails['NowPlayingItem']['MediaStreams'][0]['Codec'] : '',
+			'videoCodec' => @$itemDetails['TranscodingInfo']['VideoCodec'],
+			'audioCodec' => @$itemDetails['TranscodingInfo']['AudioCodec'],
+			'sourceAudioCodec' => isset($itemDetails['NowPlayingItem']['MediaStreams'][1]) ? $itemDetails['NowPlayingItem']['MediaStreams'][1]['Codec'] : (isset($itemDetails['NowPlayingItem']['MediaStreams'][0]) ? $itemDetails['NowPlayingItem']['MediaStreams'][0]['Codec'] : ''),
+			'videoDecision' => $this->streamType(@$itemDetails['PlayState']['PlayMethod']),
+			'audioDecision' => $this->streamType(@$itemDetails['PlayState']['PlayMethod']),
+			'container' => isset($itemDetails['NowPlayingItem']['Container']) ? $itemDetails['NowPlayingItem']['Container'] : '',
+			'audioChannels' => @$itemDetails['TranscodingInfo']['AudioChannels']
+		);
+		// Genre catch all
+		if (isset($item['Genres'])) {
+			$genres = array();
+			foreach ($item['Genres'] as $genre) {
+				$genres[] = $genre;
+			}
+		}
+		// Actor catch all
+		if (isset($item['People'])) {
+			$actors = array();
+			foreach ($item['People'] as $key => $value) {
+				if (@$value['PrimaryImageTag'] && @$value['Role']) {
+					if (file_exists($cacheDirectory . (string)$value['Id'] . '-cast.jpg')) {
+						$actorImage = $cacheDirectoryWeb . (string)$value['Id'] . '-cast.jpg';
+					}
+					if (file_exists($cacheDirectory . (string)$value['Id'] . '-cast.jpg') && (time() - 604800) > filemtime($cacheDirectory . (string)$value['Id'] . '-cast.jpg') || !file_exists($cacheDirectory . (string)$value['Id'] . '-cast.jpg')) {
+						$actorImage = 'api/v2/homepage/image?source=jellyfin&type=Primary&img=' . (string)$value['Id'] . '&height=' . $actorHeight . '&width=' . $actorWidth . '&key=' . (string)$value['Id'] . '-cast';
+					}
+					$actors[] = array(
+						'name' => (string)$value['Name'],
+						'role' => (string)$value['Role'],
+						'thumb' => $actorImage
+					);
+				}
+			}
+		}
+		// Metadata information
+		$jellyfinItem['metadata'] = array(
+			'guid' => $item['Id'],
+			'summary' => @(string)$item['Overview'],
+			'rating' => @(string)$item['CommunityRating'],
+			'duration' => @(string)$item['RunTimeTicks'],
+			'originallyAvailableAt' => @(string)$item['PremiereDate'],
+			'year' => (string)isset($item['ProductionYear']) ? $item['ProductionYear'] : '',
+			//'studio' => (string)$item['studio'],
+			'tagline' => @(string)$item['Taglines'][0],
+			'genres' => (isset($item['Genres'])) ? $genres : '',
+			'actors' => (isset($item['People'])) ? $actors : ''
+		);
+		if (file_exists($cacheDirectory . $jellyfinItem['nowPlayingKey'] . '.jpg')) {
+			$jellyfinItem['nowPlayingImageURL'] = $cacheDirectoryWeb . $jellyfinItem['nowPlayingKey'] . '.jpg';
+		}
+		if (file_exists($cacheDirectory . $jellyfinItem['key'] . '.jpg')) {
+			$jellyfinItem['imageURL'] = $cacheDirectoryWeb . $jellyfinItem['key'] . '.jpg';
+		}
+		if (file_exists($cacheDirectory . $jellyfinItem['nowPlayingKey'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $jellyfinItem['nowPlayingKey'] . '.jpg') || !file_exists($cacheDirectory . $jellyfinItem['nowPlayingKey'] . '.jpg')) {
+			$jellyfinItem['nowPlayingImageURL'] = 'api/v2/homepage/image?source=jellyfin&type=' . $jellyfinItem['nowPlayingImageType'] . '&img=' . $jellyfinItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $jellyfinItem['nowPlayingKey'] . '';
+		}
+		if (file_exists($cacheDirectory . $jellyfinItem['key'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $jellyfinItem['key'] . '.jpg') || !file_exists($cacheDirectory . $jellyfinItem['key'] . '.jpg')) {
+			$jellyfinItem['imageURL'] = 'api/v2/homepage/image?source=jellyfin&type=' . $jellyfinItem['imageType'] . '&img=' . $jellyfinItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $jellyfinItem['key'] . '';
+		}
+		if (!$jellyfinItem['nowPlayingThumb']) {
+			$jellyfinItem['nowPlayingOriginalImage'] = $jellyfinItem['nowPlayingImageURL'] = "plugins/images/cache/no-np.png";
+			$jellyfinItem['nowPlayingKey'] = "no-np";
+		}
+		if (!$jellyfinItem['thumb']) {
+			$jellyfinItem['originalImage'] = $jellyfinItem['imageURL'] = "plugins/images/cache/no-list.png";
+			$jellyfinItem['key'] = "no-list";
+		}
+		if (isset($useImage)) {
+			$jellyfinItem['useImage'] = $useImage;
+		}
+		return $jellyfinItem;
+	}
+	
+}

+ 370 - 0
api/homepage/lidarr.php

@@ -0,0 +1,370 @@
+<?php
+
+trait LidarrHomepageItem
+{
+	public function lidarrSettingsArray()
+	{
+		return array(
+			'name' => 'Lidarr',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/lidarr.png',
+			'category' => 'PMR',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageLidarrEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageLidarrEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageLidarrAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageLidarrAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'lidarrURL',
+						'label' => 'URL',
+						'value' => $this->config['lidarrURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'lidarrToken',
+						'label' => 'Token',
+						'value' => $this->config['lidarrToken']
+					)
+				),
+				'API SOCKS' => array(
+					array(
+						'type' => 'html',
+						'override' => 12,
+						'label' => '',
+						'html' => '
+							<div class="panel panel-default">
+								<div class="panel-wrapper collapse in">
+									<div class="panel-body">
+										<h3 lang="en">Lidarr SOCKS API Connection</h3>
+										<p>Using this feature allows you to access the Lidarr API without having to reverse proxy it.  Just access it from: </p>
+										<code>' . $this->getServerPath() . 'api/v2/socks/lidarr/</code>
+									</div>
+								</div>
+							</div>'
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'lidarrSocksEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['lidarrSocksEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'lidarrSocksAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['lidarrSocksAuth'],
+						'options' => $this->groupOptions
+					),
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'number',
+						'name' => 'calendarStart',
+						'label' => '# of Days Before',
+						'value' => $this->config['calendarStart'],
+						'placeholder' => ''
+					),
+					array(
+						'type' => 'number',
+						'name' => 'calendarEnd',
+						'label' => '# of Days After',
+						'value' => $this->config['calendarEnd'],
+						'placeholder' => ''
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarFirstDay',
+						'label' => 'Start Day',
+						'value' => $this->config['calendarFirstDay'],
+						'options' => $this->daysOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarDefault',
+						'label' => 'Default View',
+						'value' => $this->config['calendarDefault'],
+						'options' => $this->calendarDefaultOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarTimeFormat',
+						'label' => 'Time Format',
+						'value' => $this->config['calendarTimeFormat'],
+						'options' => $this->timeFormatOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarLocale',
+						'label' => 'Locale',
+						'value' => $this->config['calendarLocale'],
+						'options' => $this->calendarLocaleOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarLimit',
+						'label' => 'Items Per Day',
+						'value' => $this->config['calendarLimit'],
+						'options' => $this->limitOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['calendarRefresh'],
+						'options' => $this->timeOptions()
+					),
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'lidarr\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	public function testConnectionLidarr()
+	{
+		if (empty($this->config['lidarrURL'])) {
+			$this->setAPIResponse('error', 'Lidarr URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['lidarrToken'])) {
+			$this->setAPIResponse('error', 'Lidarr Token is not defined', 422);
+			return false;
+		}
+		$failed = false;
+		$errors = '';
+		$list = $this->csvHomepageUrlToken($this->config['lidarrURL'], $this->config['lidarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token'], true);
+				$results = $downloader->getSystemStatus();
+				$downloadList = json_decode($results, true);
+				if (is_array($downloadList) || is_object($downloadList)) {
+					$queue = (array_key_exists('error', $downloadList)) ? $downloadList['error']['msg'] : $downloadList;
+					if (!is_array($queue)) {
+						$ip = $value['url'];
+						$errors .= $ip . ': ' . $queue;
+						$failed = true;
+					}
+				} else {
+					$ip = $value['url'];
+					$errors .= $ip . ': Response was not JSON';
+					$failed = true;
+				}
+				
+			} catch (Exception $e) {
+				$failed = true;
+				$ip = $value['url'];
+				$errors .= $ip . ': ' . $e->getMessage();
+				$this->writeLog('error', 'Lidarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+		}
+		if ($failed) {
+			$this->setAPIResponse('error', $errors, 500);
+			return false;
+		} else {
+			$this->setAPIResponse('success', null, 200);
+			return true;
+		}
+	}
+	
+	public function lidarrHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'calendar' => [
+				'enabled' => [
+					'homepageLidarrEnabled'
+				],
+				'auth' => [
+					'homepageLidarrAuth'
+				],
+				'not_empty' => [
+					'lidarrURL',
+					'lidarrToken'
+				]
+			],
+			'queue' => [
+				'enabled' => [
+					'homepageLidarrEnabled',
+					'homepageLidarrQueueEnabled'
+				],
+				'auth' => [
+					'homepageLidarrAuth',
+					'homepageLidarrQueueAuth'
+				],
+				'not_empty' => [
+					'lidarrURL',
+					'lidarrToken'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function getLidarrQueue()
+	{
+		if (!$this->homepageItemPermissions($this->lidarrHomepagePermissions('queue'), true)) {
+			return false;
+		}
+		$queueItems = array();
+		$list = $this->csvHomepageUrlToken($this->config['lidarrURL'], $this->config['lidarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token'], true);
+				$results = $downloader->getQueue();
+				$downloadList = json_decode($results, true);
+				if (is_array($downloadList) || is_object($downloadList)) {
+					$queue = (array_key_exists('error', $downloadList)) ? '' : $downloadList;
+				} else {
+					$queue = '';
+				}
+				if (!empty($queue)) {
+					$queueItems = array_merge($queueItems, $queue);
+				}
+			} catch (Exception $e) {
+				$this->writeLog('error', 'Lidarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+		}
+		$api['content']['queueItems'] = $queueItems;
+		$api['content']['historyItems'] = false;
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;;
+	}
+	
+	public function getLidarrCalendar($startDate = null, $endDate = null)
+	{
+		$startDate = ($startDate) ?? $_GET['start'];
+		$endDate = ($endDate) ?? $_GET['end'];
+		if (!$this->homepageItemPermissions($this->lidarrHomepagePermissions('calendar'), true)) {
+			return false;
+		}
+		$calendarItems = array();
+		$list = $this->csvHomepageUrlToken($this->config['lidarrURL'], $this->config['lidarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token'], true);
+				$results = $downloader->getCalendar($startDate, $endDate);
+				$result = json_decode($results, true);
+				if (is_array($result) || is_object($result)) {
+					$calendar = (array_key_exists('error', $result)) ? '' : $this->formatLidarrCalendar($results, $key);
+				} else {
+					$calendar = '';
+				}
+			} catch (Exception $e) {
+				$this->writeLog('error', 'Lidarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+			if (!empty($calendar)) {
+				$calendarItems = array_merge($calendarItems, $calendar);
+			}
+		}
+		$this->setAPIResponse('success', null, 200, $calendarItems);
+		return $calendarItems;
+	}
+	
+	public function formatLidarrCalendar($array, $number)
+	{
+		$array = json_decode($array, true);
+		$gotCalendar = array();
+		$i = 0;
+		foreach ($array as $child) {
+			$i++;
+			$albumName = $child['title'];
+			$artistName = $child['artist']['artistName'];
+			$albumID = '';
+			$releaseDate = $child['releaseDate'];
+			$releaseDate = strtotime($releaseDate);
+			$releaseDate = date("Y-m-d H:i:s", $releaseDate);
+			if (new DateTime() < new DateTime($releaseDate)) {
+				$unaired = true;
+			}
+			if (isset($child['statistics']['percentOfTracks'])) {
+				if ($child['statistics']['percentOfTracks'] == '100.0') {
+					$downloaded = '1';
+				} else {
+					$downloaded = '0';
+				}
+			} else {
+				$downloaded = '0';
+			}
+			if ($downloaded == "0" && isset($unaired)) {
+				$downloaded = "text-info";
+			} elseif ($downloaded == "1") {
+				$downloaded = "text-success";
+			} else {
+				$downloaded = "text-danger";
+			}
+			$fanart = "/plugins/images/cache/no-np.png";
+			foreach ($child['artist']['images'] as $image) {
+				if ($image['coverType'] == "fanart") {
+					$fanart = str_replace('http://', 'https://', $image['url']);
+				}
+			}
+			$details = array(
+				"seasonCount" => '',
+				"status" => '',
+				"topTitle" => $albumName,
+				"bottomTitle" => $artistName,
+				"overview" => isset($child['artist']['overview']) ? $child['artist']['overview'] : '',
+				"runtime" => '',
+				"image" => $fanart,
+				"ratings" => $child['artist']['ratings']['value'],
+				"videoQuality" => "unknown",
+				"audioChannels" => "unknown",
+				"audioCodec" => "unknown",
+				"videoCodec" => "unknown",
+				"size" => "unknown",
+				"genres" => $child['genres'],
+			);
+			array_push($gotCalendar, array(
+				"id" => "Lidarr-" . $number . "-" . $i,
+				"title" => $artistName,
+				"start" => $child['releaseDate'],
+				"className" => "inline-popups bg-calendar calendar-item musicID--",
+				"imagetype" => "music " . $downloaded,
+				"imagetypeFilter" => "music",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details,
+				"data" => $child
+			));
+		}
+		if ($i != 0) {
+			return $gotCalendar;
+		}
+		return false;
+	}
+	
+}

+ 32 - 0
api/homepage/misc.php

@@ -0,0 +1,32 @@
+<?php
+
+trait MiscHomepageItem
+{
+	public function miscSettingsArray()
+	{
+		return array(
+			'name' => 'Misc',
+			'enabled' => true,
+			'image' => 'plugins/images/organizr/logo-no-border.png',
+			'category' => 'Custom',
+			'settings' => array(
+				'YouTube' => array(
+					array(
+						'type' => 'input',
+						'name' => 'youtubeAPI',
+						'label' => 'Youtube API Key',
+						'value' => $this->config['youtubeAPI'],
+						'help' => 'Please make sure to input this API key as the organizr one gets limited'
+					),
+					array(
+						'type' => 'html',
+						'override' => 6,
+						'label' => 'Instructions',
+						'html' => '<a href="https://www.slickremix.com/docs/get-api-key-for-youtube/" target="_blank">Click here for instructions</a>'
+					),
+				)
+			)
+		);
+	}
+	
+}

+ 208 - 0
api/homepage/monitorr.php

@@ -0,0 +1,208 @@
+<?php
+
+trait MonitorrHomepageItem
+{
+	public function monitorrSettingsArray()
+	{
+		return array(
+			'name' => 'Monitorr',
+			'enabled' => true,
+			'image' => 'plugins/images/tabs/monitorr.png',
+			'category' => 'Monitor',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageMonitorrEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageMonitorrEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageMonitorrAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageMonitorrAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'monitorrURL',
+						'label' => 'URL',
+						'value' => $this->config['monitorrURL'],
+						'help' => 'URL for Monitorr. Please use the revers proxy URL i.e. https://domain.com/monitorr/.',
+						'placeholder' => 'http://domain.com/monitorr/'
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageMonitorrRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageMonitorrRefresh'],
+						'options' => $this->timeOptions()
+					),
+				),
+				'Options' => array(
+					array(
+						'type' => 'input',
+						'name' => 'monitorrHeader',
+						'label' => 'Title',
+						'value' => $this->config['monitorrHeader'],
+						'help' => 'Sets the title of this homepage module',
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'monitorrHeaderToggle',
+						'label' => 'Toggle Title',
+						'value' => $this->config['monitorrHeaderToggle'],
+						'help' => 'Shows/hides the title of this homepage module'
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'monitorrCompact',
+						'label' => 'Compact view',
+						'value' => $this->config['monitorrCompact'],
+						'help' => 'Toggles the compact view of this homepage module'
+					),
+				),
+			)
+		);
+	}
+	
+	public function monitorrHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageMonitorrEnabled'
+				],
+				'auth' => [
+					'homepageMonitorrAuth'
+				],
+				'not_empty' => [
+					'monitorrURL'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderMonitorr()
+	{
+		if ($this->homepageItemPermissions($this->monitorrHomepagePermissions('main'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Monitorr...</h2></div>
+					<script>
+						// Monitorr
+						homepageMonitorr("' . $this->config['homepageMonitorrRefresh'] . '");
+						// End Monitorr
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	public function getMonitorrHomepageData()
+	{
+		if (!$this->homepageItemPermissions($this->monitorrHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$api = [];
+		$url = $this->qualifyURL($this->config['monitorrURL']);
+		$dataUrl = $url . '/assets/php/loop.php';
+		try {
+			$response = Requests::get($dataUrl, ['Token' => $this->config['organizrAPI']], []);
+			if ($response->success) {
+				$html = html_entity_decode($response->body);
+				// This section grabs the names of all services by regex
+				$services = [];
+				$servicesMatch = [];
+				$servicePattern = '/<div id="servicetitle(?:offline|nolink)?".*><div>(.*)<\/div><\/div><div class="(?:btnonline|btnoffline|btnunknown)".*>(Online|Offline|Unresponsive)<\/div>(:?<\/a>)?<\/div><\/div>/';
+				preg_match_all($servicePattern, $html, $servicesMatch);
+				$services = array_filter($servicesMatch[1]);
+                $status = array_filter($servicesMatch[2]);
+				$statuses = [];
+				foreach ($services as $key => $service) {
+					$match = $status[$key];
+					$statuses[$service] = $match;
+                    if ($match == 'Online') {
+                        $statuses[$service] = [
+                            'status' => true
+                        ];
+                    } else if ($match == 'Offline') {
+                        $statuses[$service] = [
+                            'status' => false
+                        ];
+                    } else if ($match == 'Unresponsive') {
+                        $statuses[$service] = [
+                            'status' => 'unresponsive'
+                        ];
+                    }
+
+					$statuses[$service]['sort'] = $key;
+					$imageMatch = [];
+					$imgPattern = '/assets\/img\/\.\.(.*)" class="serviceimg" alt=.*><\/div><\/div><div id="servicetitle"><div>' . $service . '|assets\/img\/\.\.(.*)" class="serviceimg imgoffline" alt=.*><\/div><\/div><div id="servicetitleoffline".*><div>' . $service . '|assets\/img\/\.\.(.*)" class="serviceimg" alt=.*><\/div><\/div><div id="servicetitlenolink".*><div>' . $service . '/';
+					preg_match($imgPattern, $html, $imageMatch);
+					unset($imageMatch[0]);
+					$imageMatch = array_values($imageMatch);
+					// array_push($api['imagematches'][$service], $imageMatch);
+					foreach ($imageMatch as $match) {
+						if ($match !== '') {
+							$image = $match;
+						}
+					}
+					$ext = explode('.', $image);
+					$ext = $ext[key(array_slice($ext, -1, 1, true))];
+					$imageUrl = $url . '/assets' . $image;
+					$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+					$img = Requests::get($imageUrl, ['Token' => $this->config['organizrAPI']], []);
+					if ($img->success) {
+						$base64 = 'data:image/' . $ext . ';base64,' . base64_encode($img->body);
+						$statuses[$service]['image'] = $base64;
+					} else {
+						$statuses[$service]['image'] = $cacheDirectory . 'no-list.png';
+					}
+					$linkMatch = [];
+					$linkPattern = '/<a class="servicetile" href="(.*)" target="_blank" style="display: block"><div id="serviceimg"><div><img id="' . strtolower($service) . '-service-img/';
+					preg_match($linkPattern, $html, $linkMatch);
+					$linkMatch = array_values($linkMatch);
+					unset($linkMatch[0]);
+					foreach ($linkMatch as $link) {
+						if ($link !== '') {
+							$statuses[$service]['link'] = $link;
+						}
+					}
+				}
+				foreach ($statuses as $status) {
+					foreach ($status as $key => $value) {
+						if (!isset($sortArray[$key])) {
+							$sortArray[$key] = array();
+						}
+						$sortArray[$key][] = $value;
+					}
+				}
+				array_multisort($sortArray['status'], SORT_ASC, $sortArray['sort'], SORT_ASC, $statuses);
+				$api['services'] = $statuses;
+				$api['options'] = [
+					'title' => $this->config['monitorrHeader'],
+					'titleToggle' => $this->config['monitorrHeaderToggle'],
+					'compact' => $this->config['monitorrCompact'],
+				];
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Monitorr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 401);
+			return false;
+		};
+		$api = isset($api) ? $api : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 547 - 0
api/homepage/netdata.php

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

+ 186 - 0
api/homepage/nzbget.php

@@ -0,0 +1,186 @@
+<?php
+
+trait NZBGetHomepageItem
+{
+	public function nzbgetSettingsArray()
+	{
+		return array(
+			'name' => 'NZBGet',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/nzbget.png',
+			'category' => 'Downloader',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageNzbgetEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageNzbgetEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageNzbgetAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageNzbgetAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'nzbgetURL',
+						'label' => 'URL',
+						'value' => $this->config['nzbgetURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'input',
+						'name' => 'nzbgetUsername',
+						'label' => 'Username',
+						'value' => $this->config['nzbgetUsername']
+					),
+					array(
+						'type' => 'password',
+						'name' => 'nzbgetPassword',
+						'label' => 'Password',
+						'value' => $this->config['nzbgetPassword']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'select',
+						'name' => 'homepageDownloadRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageDownloadRefresh'],
+						'options' => $this->timeOptions()
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'nzbgetCombine',
+						'label' => 'Add to Combined Downloader',
+						'value' => $this->config['nzbgetCombine']
+					),
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'nzbget\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	public function testConnectionNZBGet()
+	{
+		if (empty($this->config['nzbgetURL'])) {
+			$this->setAPIResponse('error', 'NZBGet URL is not defined', 422);
+			return false;
+		}
+		try {
+			$url = $this->qualifyURL($this->config['nzbgetURL']);
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$urlGroups = $url . '/jsonrpc/listgroups';
+			if ($this->config['nzbgetUsername'] !== '' && $this->decrypt($this->config['nzbgetPassword']) !== '') {
+				$credentials = array('auth' => new Requests_Auth_Basic(array($this->config['nzbgetUsername'], $this->decrypt($this->config['nzbgetPassword']))));
+				$options = array_merge($options, $credentials);
+			}
+			$response = Requests::get($urlGroups, array(), $options);
+			if ($response->success) {
+				$this->setAPIResponse('success', 'API Connection succeeded', 200);
+				return true;
+			} else {
+				$this->setAPIResponse('success', 'NZBGet: An Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'NZBGet Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function nzbgetHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageNzbgetEnabled'
+				],
+				'auth' => [
+					'homepageNzbgetAuth'
+				],
+				'not_empty' => [
+					'nzbgetURL'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrdernzbget()
+	{
+		if ($this->homepageItemPermissions($this->nzbgetHomepagePermissions('main'))) {
+			$loadingBox = ($this->config['nzbgetCombine']) ? '' : '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+			$builder = ($this->config['nzbgetCombine']) ? 'buildDownloaderCombined(\'nzbget\');' : '$("#' . __FUNCTION__ . '").html(buildDownloader("nzbget"));';
+			return '
+				<div id="' . __FUNCTION__ . '">
+					' . $loadingBox . '
+					<script>
+		                // homepageOrdernzbget
+		                ' . $builder . '
+		                homepageDownloader("nzbget", "' . $this->config['homepageDownloadRefresh'] . '");
+		                // End homepageOrdernzbget
+	                </script>
+				</div>
+				';
+		}
+	}
+	
+	public function getNzbgetHomepageQueue()
+	{
+		if (!$this->homepageItemPermissions($this->nzbgetHomepagePermissions('main'), true)) {
+			return false;
+		}
+		try {
+			$url = $this->qualifyURL($this->config['nzbgetURL']);
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$urlGroups = $url . '/jsonrpc/listgroups';
+			$urlHistory = $url . '/jsonrpc/history';
+			if ($this->config['nzbgetUsername'] !== '' && $this->decrypt($this->config['nzbgetPassword']) !== '') {
+				$credentials = array('auth' => new Requests_Auth_Basic(array($this->config['nzbgetUsername'], $this->decrypt($this->config['nzbgetPassword']))));
+				$options = array_merge($options, $credentials);
+			}
+			$response = Requests::get($urlGroups, array(), $options);
+			if ($response->success) {
+				$api['content']['queueItems'] = json_decode($response->body, true);
+			}
+			$response = Requests::get($urlHistory, array(), $options);
+			if ($response->success) {
+				$api['content']['historyItems'] = json_decode($response->body, true);
+			}
+			$api['content'] = isset($api['content']) ? $api['content'] : false;
+			$this->setAPIResponse('success', null, 200, $api);
+			return $api;
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'NZBGet Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+}

+ 140 - 0
api/homepage/octoprint.php

@@ -0,0 +1,140 @@
+<?php
+
+trait OctoPrintHomepageItem
+{
+	public function octoprintSettingsArray()
+	{
+		return array(
+			'name' => 'Octoprint',
+			'enabled' => true,
+			'image' => 'plugins/images/tabs/octoprint.png',
+			'category' => 'Monitor',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageOctoprintEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageOctoprintEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageOctoprintAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageOctoprintAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'octoprintURL',
+						'label' => 'URL',
+						'value' => $this->config['octoprintURL'],
+						'help' => 'Enter the IP:PORT of your Octoprint instance e.g. http://octopi.local'
+					),
+					array(
+						'type' => 'input',
+						'name' => 'octoprintToken',
+						'label' => 'API Key',
+						'value' => $this->config['octoprintToken'],
+						'help' => 'Enter your Octoprint API key, found in Octoprint settings page.'
+					),
+				),
+				'Options' => array(
+					array(
+						'type' => 'input',
+						'name' => 'octoprintHeader',
+						'label' => 'Title',
+						'value' => $this->config['octoprintHeader'],
+						'help' => 'Sets the title of this homepage module',
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'octoprintToggle',
+						'label' => 'Toggle Title',
+						'value' => $this->config['octoprintHeaderToggle'],
+						'help' => 'Shows/hides the title of this homepage module'
+					),
+				),
+			)
+		);
+	}
+	
+	public function octoprintHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageOctoprintEnabled'
+				],
+				'auth' => [
+					'homepageOctoprintAuth'
+				],
+				'not_empty' => [
+					'octoprintURL',
+					'octoprintToken'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderOctoprint()
+	{
+		if ($this->homepageItemPermissions($this->octoprintHomepagePermissions('main'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading OctoPrint...</h2></div>
+					<script>
+						// Octoprint
+						homepageOctoprint("' . $this->config['homepageOctoprintRefresh'] . '");
+						// End Octoprint
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	public function getOctoprintHomepageData()
+	{
+		if (!$this->homepageItemPermissions($this->octoprintHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$api = [];
+		$url = $this->qualifyURL($this->config['octoprintURL']);
+		$endpoints = ['job', 'settings'];
+		$api['data']['url'] = $this->config['octoprintURL'];
+		foreach ($endpoints as $endpoint) {
+			$dataUrl = $url . '/api/' . $endpoint;
+			try {
+				$headers = array('X-API-KEY' => $this->config['octoprintToken']);
+				$response = Requests::get($dataUrl, $headers);
+				if ($response->success) {
+					$json = json_decode($response->body, true);
+					$api['data'][$endpoint] = $json;
+					$api['options'] = [
+						'title' => $this->config['octoprintHeader'],
+						'titleToggle' => $this->config['octoprintHeaderToggle'],
+					];
+				} else {
+					$this->setAPIResponse('error', 'OctoPrint connection error', 409);
+					return false;
+				}
+			} catch (Requests_Exception $e) {
+				$this->writeLog('error', 'Octoprint Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				$this->setAPIResponse('error', $e->getMessage(), 500);
+				return false;
+			};
+		}
+		$api = isset($api) ? $api : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 541 - 0
api/homepage/ombi.php

@@ -0,0 +1,541 @@
+<?php
+
+trait OmbiHomepageItem
+{
+	
+	public function ombiSettingsArray()
+	{
+		return array(
+			'name' => 'Ombi',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/ombi.png',
+			'category' => 'Requests',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageOmbiEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageOmbiEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageOmbiAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageOmbiAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'ombiURL',
+						'label' => 'URL',
+						'value' => $this->config['ombiURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'ombiToken',
+						'label' => 'Token',
+						'value' => $this->config['ombiToken']
+					),
+					array(
+						'type' => 'input',
+						'name' => 'ombiFallbackUser',
+						'label' => 'Ombi Fallback User',
+						'value' => $this->config['ombiFallbackUser'],
+						'help' => 'Organizr will request an Ombi User Token based off of this user credentials'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'ombiFallbackPassword',
+						'label' => 'Ombi Fallback Password',
+						'value' => $this->config['ombiFallbackPassword']
+					),
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'select',
+						'name' => 'homepageOmbiRequestAuth',
+						'label' => 'Minimum Group to Request',
+						'value' => $this->config['homepageOmbiRequestAuth'],
+						'options' => $this->groupOptions
+					),
+					array(
+						'type' => 'select',
+						'name' => 'ombiTvDefault',
+						'label' => 'TV Show Default Request',
+						'value' => $this->config['ombiTvDefault'],
+						'options' => $this->ombiTvOptions()
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'ombiLimitUser',
+						'label' => 'Limit to User',
+						'value' => $this->config['ombiLimitUser']
+					),
+					array(
+						'type' => 'number',
+						'name' => 'ombiLimit',
+						'label' => 'Item Limit',
+						'value' => $this->config['ombiLimit'],
+					),
+					array(
+						'type' => 'select',
+						'name' => 'ombiRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['ombiRefresh'],
+						'options' => $this->timeOptions()
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'ombiAlias',
+						'label' => 'Use Ombi Alias Names',
+						'value' => $this->config['ombiAlias'],
+						'help' => 'Use Ombi Alias Names instead of Usernames - If Alias is blank, Alias will fallback to Username'
+					)
+				),
+				'Default Filter' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'ombiDefaultFilterAvailable',
+						'label' => 'Show Available',
+						'value' => $this->config['ombiDefaultFilterAvailable'],
+						'help' => 'Show All Available Ombi Requests'
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'ombiDefaultFilterUnavailable',
+						'label' => 'Show Unavailable',
+						'value' => $this->config['ombiDefaultFilterUnavailable'],
+						'help' => 'Show All Unavailable Ombi Requests'
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'ombiDefaultFilterApproved',
+						'label' => 'Show Approved',
+						'value' => $this->config['ombiDefaultFilterApproved'],
+						'help' => 'Show All Approved Ombi Requests'
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'ombiDefaultFilterUnapproved',
+						'label' => 'Show Unapproved',
+						'value' => $this->config['ombiDefaultFilterUnapproved'],
+						'help' => 'Show All Unapproved Ombi Requests'
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'ombiDefaultFilterDenied',
+						'label' => 'Show Denied',
+						'value' => $this->config['ombiDefaultFilterDenied'],
+						'help' => 'Show All Denied Ombi Requests'
+					)
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'ombi\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	public function testConnectionOmbi()
+	{
+		if (empty($this->config['ombiURL'])) {
+			$this->setAPIResponse('error', 'Ombi URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['ombiToken'])) {
+			$this->setAPIResponse('error', 'Ombi Token is not defined', 422);
+			return false;
+		}
+		$headers = array(
+			"Accept" => "application/json",
+			"Apikey" => $this->config['ombiToken'],
+		);
+		$url = $this->qualifyURL($this->config['ombiURL']);
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$test = Requests::get($url . "/api/v1/Settings/about", $headers, $options);
+			if ($test->success) {
+				$this->setAPIResponse('success', 'API Connection succeeded', 200);
+				return true;
+			}
+			
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+	}
+	
+	public function ombiHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageOmbiEnabled'
+				],
+				'auth' => [
+					'homepageOmbiAuth'
+				],
+				'not_empty' => [
+					'ombiURL',
+					'ombiToken'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderombi()
+	{
+		if ($this->homepageItemPermissions($this->ombiHomepagePermissions('main'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Requests...</h2></div>
+					<script>
+						// Ombi Requests
+						homepageRequests("' . $this->config['ombiRefresh'] . '");
+						// End Ombi Requests
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	
+	public function getOmbiRequests($type = "both", $limit = 50, $offset = 0)
+	{
+		if (!$this->homepageItemPermissions($this->ombiHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$api['count'] = array(
+			'movie' => 0,
+			'tv' => 0,
+			'limit' => (integer)$limit,
+			'offset' => (integer)$offset
+		);
+		$headers = array(
+			"Accept" => "application/json",
+			"Apikey" => $this->config['ombiToken'],
+		);
+		$requests = array();
+		$url = $this->qualifyURL($this->config['ombiURL']);
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			switch ($type) {
+				case 'movie':
+					$movie = Requests::get($url . "/api/v1/Request/movie", $headers, $options);
+					break;
+				case 'tv':
+					$tv = Requests::get($url . "/api/v1/Request/tv", $headers, $options);
+					break;
+				default:
+					$movie = Requests::get($url . "/api/v1/Request/movie", $headers, $options);
+					$tv = Requests::get($url . "/api/v1/Request/tv", $headers, $options);
+					break;
+			}
+			if ($movie->success || $tv->success) {
+				if (isset($movie)) {
+					$movie = json_decode($movie->body, true);
+					//$movie = array_reverse($movie);
+					foreach ($movie as $key => $value) {
+						$proceed = (($this->config['ombiLimitUser']) && strtolower($this->user['username']) == strtolower($value['requestedUser']['userName'])) || (strtolower($value['requestedUser']['userName']) == strtolower($this->config['ombiFallbackUser'])) || (!$this->config['ombiLimitUser']) || $this->qualifyRequest(1);
+						if ($proceed) {
+							$api['count']['movie']++;
+							$requests[] = array(
+								'id' => $value['theMovieDbId'],
+								'title' => $value['title'],
+								'overview' => $value['overview'],
+								'poster' => (isset($value['posterPath']) && $value['posterPath'] !== '') ? 'https://image.tmdb.org/t/p/w300/' . $value['posterPath'] : 'plugins/images/cache/no-list.png',
+								'background' => (isset($value['background']) && $value['background'] !== '') ? 'https://image.tmdb.org/t/p/w1280/' . $value['background'] : '',
+								'approved' => $value['approved'],
+								'available' => $value['available'],
+								'denied' => $value['denied'],
+								'deniedReason' => $value['deniedReason'],
+								'user' => $value['requestedUser']['userName'],
+								'userAlias' => $value['requestedUser']['userAlias'],
+								'request_id' => $value['id'],
+								'request_date' => $value['requestedDate'],
+								'release_date' => $value['releaseDate'],
+								'type' => 'movie',
+								'icon' => 'mdi mdi-filmstrip',
+								'color' => 'palette-Deep-Purple-900 bg white',
+							);
+						}
+					}
+				}
+				if (isset($tv) && (is_array($tv) || is_object($tv))) {
+					$tv = json_decode($tv->body, true);
+					foreach ($tv as $key => $value) {
+						if (count($value['childRequests']) > 0) {
+							$proceed = (($this->config['ombiLimitUser']) && strtolower($this->user['username']) == strtolower($value['childRequests'][0]['requestedUser']['userName'])) || (!$this->config['ombiLimitUser']) || $this->qualifyRequest(1);
+							if ($proceed) {
+								$api['count']['tv']++;
+								$requests[] = array(
+									'id' => $value['tvDbId'],
+									'title' => $value['title'],
+									'overview' => $value['overview'],
+									'poster' => (isset($value['posterPath']) && $value['posterPath'] !== '') ? $value['posterPath'] : 'plugins/images/cache/no-list.png',
+									'background' => (isset($value['background']) && $value['background'] !== '') ? 'https://image.tmdb.org/t/p/w1280/' . $value['background'] : '',
+									'approved' => $value['childRequests'][0]['approved'],
+									'available' => $value['childRequests'][0]['available'],
+									'denied' => $value['childRequests'][0]['denied'],
+									'deniedReason' => $value['childRequests'][0]['deniedReason'],
+									'user' => $value['childRequests'][0]['requestedUser']['userName'],
+									'userAlias' => $value['childRequests'][0]['requestedUser']['userAlias'],
+									'request_id' => $value['id'],
+									'request_date' => $value['childRequests'][0]['requestedDate'],
+									'release_date' => $value['releaseDate'],
+									'type' => 'tv',
+									'icon' => 'mdi mdi-television',
+									'color' => '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', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($requests) ? array_slice($requests, $offset, $limit) : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+	
+	public function addOmbiRequest($id, $type)
+	{
+		$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->ombiHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['ombiURL']);
+		switch ($type) {
+			case 'season':
+			case 'tv':
+				$type = 'tv';
+				$add = array(
+					'tvDbId' => $id,
+					'requestAll' => $this->ombiTVDefault('all'),
+					'latestSeason' => $this->ombiTVDefault('last'),
+					'firstSeason' => $this->ombiTVDefault('first')
+				);
+				break;
+			default:
+				$type = 'movie';
+				$add = array("theMovieDbId" => (int)$id);
+				break;
+		}
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false, 'timeout' => 30) : array('timeout' => 30);
+			if (isset($_COOKIE['Auth'])) {
+				$headers = array(
+					"Accept" => "application/json",
+					"Content-Type" => "application/json",
+					"Authorization" => "Bearer " . $_COOKIE['Auth']
+				);
+			} else {
+				$this->setAPIResponse('error', 'User does not have Auth Cookie', 500);
+				return false;
+			}
+			//https://api.themoviedb.org/3/movie/157336?api_key=83cf4ee97bb728eeaf9d4a54e64356a1
+			// Lets check if it exists inside Ombi first... but since I can't search with ID - i have to query title from id
+			$tmdbResponse = Requests::get('https://api.themoviedb.org/3/' . $type . '/' . $id . '?api_key=83cf4ee97bb728eeaf9d4a54e64356a1', [], $options);
+			if ($tmdbResponse->success) {
+				$details = json_decode($tmdbResponse->body, true);
+				if (count($details) > 0) {
+					switch ($type) {
+						case 'tv':
+							$title = $details['name'];
+							$idType = 'theTvDbId';
+							$tmdbResponseID = Requests::get('https://api.themoviedb.org/3/tv/' . $id . '/external_ids?api_key=83cf4ee97bb728eeaf9d4a54e64356a1', [], $options);
+							if ($tmdbResponseID->success) {
+								$detailsID = json_decode($tmdbResponseID->body, true);
+								if (count($detailsID) > 0) {
+									if (isset($detailsID['tvdb_id'])) {
+										$id = $detailsID['tvdb_id'];
+										$add['tvDbId'] = $id;
+									} else {
+										$this->setAPIResponse('error', 'Could not get TVDB Id', 422);
+										return false;
+									}
+								} else {
+									$this->setAPIResponse('error', 'Could not get TVDB Id', 422);
+									return false;
+								}
+							}
+							break;
+						case 'movie':
+							$title = $details['title'];
+							$idType = 'theMovieDbId';
+							break;
+						default:
+							$this->setAPIResponse('error', 'Ombi Type was not found', 422);
+							return false;
+					}
+				} else {
+					$this->setAPIResponse('error', 'No data returned from TMDB', 422);
+					return false;
+				}
+			} else {
+				$this->setAPIResponse('error', 'Could not contact TMDB', 422);
+				return false;
+			}
+			$searchResponse = Requests::get($url . '/api/v1/Search/' . $type . '/' . urlencode($title), $headers, $options);
+			if ($searchResponse->success) {
+				$details = json_decode($searchResponse->body, true);
+				if (count($details) > 0) {
+					foreach ($details as $k => $v) {
+						if ($v[$idType] == $id) {
+							if ($v['available']) {
+								$this->setAPIResponse('error', 'Request is already available', 409);
+								return false;
+							} elseif ($v['requested']) {
+								$this->setAPIResponse('error', 'Request is already requested', 409);
+								return false;
+							}
+						}
+					}
+				}
+			} else {
+				$this->setAPIResponse('error', 'Ombi Error Occurred', 500);
+				return false;
+			}
+			$response = Requests::post($url . "/api/v1/Request/" . $type, $headers, json_encode($add), $options);
+			if ($response->success) {
+				$this->setAPIResponse('success', 'Ombi Request submitted', 200);
+				return true;
+			} else {
+				$this->setAPIResponse('error', 'Ombi Error Occurred', 500);
+				return false;
+			}
+			
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function actionOmbiRequest($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->ombiHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['ombiURL']);
+		$headers = array(
+			"Accept" => "application/json",
+			"Content-Type" => "application/json",
+			"Apikey" => $this->config['ombiToken']
+		);
+		$data = array(
+			'id' => $id,
+		);
+		switch ($type) {
+			case 'season':
+			case 'tv':
+				$type = 'tv';
+				break;
+			default:
+				$type = 'movie';
+				break;
+		}
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false, 'timeout' => 30) : array('timeout' => 30);
+			switch ($action) {
+				case 'approve':
+					$response = Requests::post($url . "/api/v1/Request/" . $type . "/approve", $headers, json_encode($data), $options);
+					$message = 'Ombi Request has been approved';
+					break;
+				case 'available':
+					$response = Requests::post($url . "/api/v1/Request/" . $type . "/available", $headers, json_encode($data), $options);
+					$message = 'Ombi Request has been marked available';
+					break;
+				case 'unavailable':
+					$response = Requests::post($url . "/api/v1/Request/" . $type . "/unavailable", $headers, json_encode($data), $options);
+					$message = 'Ombi Request has been marked unavailable';
+					break;
+				case 'deny':
+					$response = Requests::put($url . "/api/v1/Request/" . $type . "/deny", $headers, json_encode($data), $options);
+					$message = 'Ombi Request has been denied';
+					break;
+				case 'delete':
+					$response = Requests::delete($url . "/api/v1/Request/" . $type . "/" . $id, $headers, $options);
+					$message = 'Ombi Request has been deleted';
+					break;
+				default:
+					return false;
+			}
+			if ($response->success) {
+				$this->setAPIResponse('success', $message, 200);
+				return true;
+			} else {
+				$this->setAPIResponse('error', 'Ombi Error Occurred', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+	}
+	
+	public function ombiTVDefault($type)
+	{
+		return $type == $this->config['ombiTvDefault'];
+	}
+}

+ 185 - 0
api/homepage/pihole.php

@@ -0,0 +1,185 @@
+<?php
+
+trait PiHoleHomepageItem
+{
+	public function piholeSettingsArray()
+	{
+		return array(
+			'name' => 'Pi-hole',
+			'enabled' => true,
+			'image' => 'plugins/images/tabs/pihole.png',
+			'category' => 'Monitor',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepagePiholeEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepagePiholeEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepagePiholeAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepagePiholeAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'piholeURL',
+						'label' => 'URL',
+						'value' => $this->config['piholeURL'],
+						'help' => 'Please make sure to use local IP address and port and to include \'/admin/\' at the end of the URL. You can add multiple Pi-holes by comma separating the URLs.',
+						'placeholder' => 'http(s)://hostname:port/admin/'
+					),
+				),
+				'Misc' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'piholeHeaderToggle',
+						'label' => 'Toggle Title',
+						'value' => $this->config['piholeHeaderToggle'],
+						'help' => 'Shows/hides the title of this homepage module'
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'homepagePiholeCombine',
+						'label' => 'Combine stat cards',
+						'value' => $this->config['homepagePiholeCombine'],
+						'help' => 'This controls whether to combine the stats for multiple pihole instances into 1 card.',
+					),
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'pihole\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	public function testConnectionPihole()
+	{
+		if (empty($this->config['piholeURL'])) {
+			$this->setAPIResponse('error', 'Pihole URL is not defined', 422);
+			return false;
+		}
+		$api = array();
+		$failed = false;
+		$errors = '';
+		$urls = explode(',', $this->config['piholeURL']);
+		foreach ($urls as $url) {
+			$url = $url . '/api.php?';
+			try {
+				$response = Requests::get($url, [], []);
+				if ($response->success) {
+					@$test = json_decode($response->body, true);
+					if (!is_array($test)) {
+						$ip = $this->qualifyURL($url, true)['host'];
+						$errors .= $ip . ': Response was not JSON';
+						$failed = true;
+					}
+				}
+				if (!$response->success) {
+					$ip = $this->qualifyURL($url, true)['host'];
+					$errors .= $ip . ': Unknown Failure';
+					$failed = true;
+				}
+			} catch (Requests_Exception $e) {
+				$failed = true;
+				$ip = $this->qualifyURL($url, true)['host'];
+				$errors .= $ip . ': ' . $e->getMessage();
+				$this->writeLog('error', 'Pi-hole Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			};
+		}
+		if ($failed) {
+			$this->setAPIResponse('error', $errors, 500);
+			return false;
+		} else {
+			$this->setAPIResponse('success', null, 200);
+			return true;
+		}
+	}
+	
+	public function piholeHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepagePiholeEnabled'
+				],
+				'auth' => [
+					'homepagePiholeAuth'
+				],
+				'not_empty' => [
+					'piholeURL'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderPihole()
+	{
+		if ($this->homepageItemPermissions($this->piholeHomepagePermissions('main'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Pihole...</h2></div>
+					<script>
+						// Pi-hole Stats
+						homepagePihole("' . $this->config['homepagePiholeRefresh'] . '");
+						// End Pi-hole Stats
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	public function getPiholeHomepageStats()
+	{
+		if (!$this->homepageItemPermissions($this->piholeHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$api = array();
+		$urls = explode(',', $this->config['piholeURL']);
+		foreach ($urls as $url) {
+			$url = $url . '/api.php?';
+			try {
+				$response = Requests::get($url, [], []);
+				if ($response->success) {
+					@$piholeResults = json_decode($response->body, true);
+					if (is_array($piholeResults)) {
+						$ip = $this->qualifyURL($url, true)['host'];
+						$api['data'][$ip] = $piholeResults;
+					}
+				}
+			} catch (Requests_Exception $e) {
+				$this->setAPIResponse('error', $e->getMessage(), 500);
+				$this->writeLog('error', 'Pi-hole Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				return false;
+			};
+		}
+		$api['options']['combine'] = $this->config['homepagePiholeCombine'];
+		$api['options']['title'] = $this->config['piholeHeaderToggle'];
+		$api = isset($api) ? $api : null;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 760 - 0
api/homepage/plex.php

@@ -0,0 +1,760 @@
+<?php
+
+trait PlexHomepageItem
+{
+	
+	public function plexSettingsArray()
+	{
+		return array(
+			'name' => 'Plex',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/plex.png',
+			'category' => 'Media Server',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepagePlexEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepagePlexEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepagePlexAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepagePlexAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'plexURL',
+						'label' => 'URL',
+						'value' => $this->config['plexURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'blank',
+						'name' => '',
+						'label' => '',
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'plexToken',
+						'label' => 'Token',
+						'value' => $this->config['plexToken']
+					),
+					array(
+						'type' => 'button',
+						'label' => 'Get Plex Token',
+						'icon' => 'fa fa-ticket',
+						'text' => 'Retrieve',
+						'attr' => 'onclick="showPlexTokenForm(\'#homepage-Plex-form [name=plexToken]\')"'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'plexID',
+						'label' => 'Plex Machine',
+						'value' => $this->config['plexID']
+					),
+					array(
+						'type' => 'button',
+						'label' => 'Get Plex Machine',
+						'icon' => 'fa fa-id-badge',
+						'text' => 'Retrieve',
+						'attr' => 'onclick="showPlexMachineForm(\'#homepage-Plex-form [name=plexID]\')"'
+					),
+				),
+				'Active Streams' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepagePlexStreams',
+						'label' => 'Enable',
+						'value' => $this->config['homepagePlexStreams']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepagePlexStreamsAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $this->config['homepagePlexStreamsAuth'],
+						'options' => $this->groupOptions
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'homepageShowStreamNames',
+						'label' => 'User Information',
+						'value' => $this->config['homepageShowStreamNames']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageShowStreamNamesAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $this->config['homepageShowStreamNamesAuth'],
+						'options' => $this->groupOptions
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageStreamRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageStreamRefresh'],
+						'options' => $this->timeOptions()
+					),
+				),
+				'Recent Items' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepagePlexRecent',
+						'label' => 'Enable',
+						'value' => $this->config['homepagePlexRecent']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepagePlexRecentAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $this->config['homepagePlexRecentAuth'],
+						'options' => $this->groupOptions
+					),
+					array(
+						'type' => 'number',
+						'name' => 'homepageRecentLimit',
+						'label' => 'Item Limit',
+						'value' => $this->config['homepageRecentLimit'],
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageRecentRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageRecentRefresh'],
+						'options' => $this->timeOptions()
+					),
+				),
+				'Media Search' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'mediaSearch',
+						'label' => 'Enable',
+						'value' => $this->config['mediaSearch']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'mediaSearchAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $this->config['mediaSearchAuth'],
+						'options' => $this->groupOptions
+					),
+					array(
+						'type' => 'select',
+						'name' => 'mediaSearchType',
+						'label' => 'Media Server',
+						'value' => $this->config['mediaSearchType'],
+						'options' => $this->mediaServerOptions()
+					),
+				),
+				'Playlists' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepagePlexPlaylist',
+						'label' => 'Enable',
+						'value' => $this->config['homepagePlexPlaylist']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepagePlexPlaylistAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $this->config['homepagePlexPlaylistAuth'],
+						'options' => $this->groupOptions
+					),
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'input',
+						'name' => 'plexTabName',
+						'label' => 'Plex Tab Name',
+						'value' => $this->config['plexTabName'],
+						'placeholder' => 'Only use if you have Plex in a reverse proxy'
+					),
+					array(
+						'type' => 'input',
+						'name' => 'plexTabURL',
+						'label' => 'Plex Tab WAN URL',
+						'value' => $this->config['plexTabURL'],
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'select',
+						'name' => 'cacheImageSize',
+						'label' => 'Image Cache Size',
+						'value' => $this->config['cacheImageSize'],
+						'options' => array(
+							array(
+								'name' => 'Low',
+								'value' => '.5'
+							),
+							array(
+								'name' => '1x',
+								'value' => '1'
+							),
+							array(
+								'name' => '2x',
+								'value' => '2'
+							),
+							array(
+								'name' => '3x',
+								'value' => '3'
+							)
+						)
+					)
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'plex\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	public function testConnectionPlex()
+	{
+		if (!empty($this->config['plexURL']) && !empty($this->config['plexToken'])) {
+			$url = $this->qualifyURL($this->config['plexURL']) . "/servers?X-Plex-Token=" . $this->config['plexToken'];
+			try {
+				$options = ($this->localURL($url)) ? array('verify' => false) : array();
+				$response = Requests::get($url, array(), $options);
+				libxml_use_internal_errors(true);
+				if ($response->success) {
+					$this->setAPIResponse('success', 'API Connection succeeded', 200);
+					return true;
+				} else {
+					$this->setAPIResponse('error', 'URL and/or Token not setup correctly', 422);
+					return false;
+				}
+			} catch (Requests_Exception $e) {
+				$this->setAPIResponse('error', $e->getMessage(), 500);
+				return false;
+			}
+		} else {
+			$this->setAPIResponse('error', 'URL and/or Token not setup', 422);
+			return 'URL and/or Token not setup';
+		}
+	}
+	
+	public function plexHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'streams' => [
+				'enabled' => [
+					'homepagePlexEnabled',
+					'homepagePlexStreams'
+				],
+				'auth' => [
+					'homepagePlexAuth',
+					'homepagePlexStreamsAuth'
+				],
+				'not_empty' => [
+					'plexURL',
+					'plexToken',
+					'plexID'
+				]
+			],
+			'recent' => [
+				'enabled' => [
+					'homepagePlexEnabled',
+					'homepagePlexRecent'
+				],
+				'auth' => [
+					'homepagePlexAuth',
+					'homepagePlexRecentAuth'
+				],
+				'not_empty' => [
+					'plexURL',
+					'plexToken',
+					'plexID'
+				]
+			],
+			'playlists' => [
+				'enabled' => [
+					'homepagePlexEnabled',
+					'homepagePlexPlaylist'
+				],
+				'auth' => [
+					'homepagePlexAuth',
+					'homepagePlexPlaylistAuth'
+				],
+				'not_empty' => [
+					'plexURL',
+					'plexToken',
+					'plexID'
+				]
+			],
+			'metadata' => [
+				'enabled' => [
+					'homepagePlexEnabled'
+				],
+				'auth' => [
+					'homepagePlexAuth'
+				],
+				'not_empty' => [
+					'plexURL',
+					'plexToken',
+					'plexID'
+				]
+			],
+			'search' => [
+				'enabled' => [
+					'homepagePlexEnabled',
+					'mediaSearch'
+				],
+				'auth' => [
+					'homepagePlexAuth',
+					'mediaSearchAuth'
+				],
+				'not_empty' => [
+					'plexURL',
+					'plexToken',
+					'plexID'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderplexnowplaying()
+	{
+		if ($this->homepageItemPermissions($this->plexHomepagePermissions('streams'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Now Playing...</h2></div>
+					<script>
+						// Plex Stream
+						homepageStream("plex", "' . $this->config['homepageStreamRefresh'] . '");
+						// End Plex Stream
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	public function homepageOrderplexrecent()
+	{
+		if ($this->homepageItemPermissions($this->plexHomepagePermissions('recent'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Recent...</h2></div>
+					<script>
+						// Plex Recent
+						homepageRecent("plex", "' . $this->config['homepageRecentRefresh'] . '");
+						// End Plex Recent
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	public function homepageOrderplexplaylist()
+	{
+		if ($this->homepageItemPermissions($this->plexHomepagePermissions('playlists'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Playlists...</h2></div>
+					<script>
+						// Plex Playlist
+						homepagePlaylist("plex");
+						// End Plex Playlist
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	public function getPlexHomepageStreams()
+	{
+		if (!$this->homepageItemPermissions($this->plexHomepagePermissions('streams'), true)) {
+			return false;
+		}
+		$ignore = array();
+		$resolve = true;
+		$url = $this->qualifyURL($this->config['plexURL']);
+		$url = $url . "/status/sessions?X-Plex-Token=" . $this->config['plexToken'];
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		$response = Requests::get($url, array(), $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;
+		}
+	}
+	
+	public function getPlexHomepageRecent()
+	{
+		if (!$this->homepageItemPermissions($this->plexHomepagePermissions('recent'), true)) {
+			return false;
+		}
+		$ignore = array();
+		$resolve = true;
+		$url = $this->qualifyURL($this->config['plexURL']);
+		$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->localURL($v)) ? array('verify' => false) : array();
+			$response = Requests::get($v, array(), $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);
+					}
+				}
+				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;
+	}
+	
+	public function getPlexHomepagePlaylists()
+	{
+		if (!$this->homepageItemPermissions($this->plexHomepagePermissions('playlists'), true)) {
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['plexURL']);
+		$url = $url . "/playlists?X-Plex-Token=" . $this->config['plexToken'];
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		$response = Requests::get($url, array(), $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;
+		}
+		
+	}
+	
+	public function getPlexHomepageMetadata($array)
+	{
+		if (!$this->homepageItemPermissions($this->plexHomepagePermissions('metadata'), true)) {
+			return false;
+		}
+		$key = $array['key'] ?? null;
+		if (!$key) {
+			$this->setAPIResponse('error', 'Plex Metadata key is not defined', 422);
+			return false;
+		}
+		$ignore = array();
+		$resolve = true;
+		$url = $this->qualifyURL($this->config['plexURL']);
+		$url = $url . "/library/metadata/" . $key . "?X-Plex-Token=" . $this->config['plexToken'];
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		$response = Requests::get($url, array(), $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;
+		}
+	}
+	
+	public function getPlexHomepageSearch($query)
+	{
+		if (!$this->homepageItemPermissions($this->plexHomepagePermissions('search'), true)) {
+			return false;
+		}
+		$query = $query ?? null;
+		if (!$query) {
+			$this->setAPIResponse('error', 'Plex Metadata key is not defined', 422);
+			return false;
+		}
+		$ignore = array('artist', 'episode');
+		$resolve = true;
+		$url = $this->qualifyURL($this->config['plexURL']);
+		$url = $url . "/search?query=" . rawurlencode($query) . "&X-Plex-Token=" . $this->config['plexToken'];
+		$options = ($this->localURL($url)) ? array('verify' => false) : array();
+		$response = Requests::get($url, array(), $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;
+		}
+	}
+	
+	public function resolvePlexItem($item)
+	{
+		// Static Height & Width
+		$height = $this->getCacheImageSize('h');
+		$width = $this->getCacheImageSize('w');
+		$nowPlayingHeight = $this->getCacheImageSize('nph');
+		$nowPlayingWidth = $this->getCacheImageSize('npw');
+		// Cache Directories
+		$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+		$cacheDirectoryWeb = 'plugins/images/cache/';
+		// Types
+		switch ($item['type']) {
+			case 'show':
+				$plexItem['type'] = 'tv';
+				$plexItem['title'] = (string)$item['title'];
+				$plexItem['secondaryTitle'] = (string)$item['year'];
+				$plexItem['summary'] = (string)$item['summary'];
+				$plexItem['ratingKey'] = (string)$item['ratingKey'];
+				$plexItem['thumb'] = (string)$item['thumb'];
+				$plexItem['key'] = (string)$item['ratingKey'] . "-list";
+				$plexItem['nowPlayingThumb'] = (string)$item['art'];
+				$plexItem['nowPlayingKey'] = (string)$item['ratingKey'] . "-np";
+				$plexItem['nowPlayingTitle'] = (string)$item['title'];
+				$plexItem['nowPlayingBottom'] = (string)$item['year'];
+				$plexItem['metadataKey'] = (string)$item['ratingKey'];
+				break;
+			case 'season':
+				$plexItem['type'] = 'tv';
+				$plexItem['title'] = (string)$item['parentTitle'];
+				$plexItem['secondaryTitle'] = (string)$item['title'];
+				$plexItem['summary'] = (string)$item['parentSummary'];
+				$plexItem['ratingKey'] = (string)$item['parentRatingKey'];
+				$plexItem['thumb'] = (string)$item['thumb'];
+				$plexItem['key'] = (string)$item['ratingKey'] . "-list";
+				$plexItem['nowPlayingThumb'] = (string)$item['art'];
+				$plexItem['nowPlayingKey'] = (string)$item['ratingKey'] . "-np";
+				$plexItem['metadataKey'] = (string)$item['parentRatingKey'];
+				break;
+			case 'episode':
+				$plexItem['type'] = 'tv';
+				$plexItem['title'] = (string)$item['grandparentTitle'];
+				$plexItem['secondaryTitle'] = (string)$item['parentTitle'];
+				$plexItem['summary'] = (string)$item['title'];
+				$plexItem['ratingKey'] = (string)$item['parentRatingKey'];
+				$plexItem['thumb'] = ($item['parentThumb'] ? (string)$item['parentThumb'] : (string)$item['grandparentThumb']);
+				$plexItem['key'] = (string)$item['ratingKey'] . "-list";
+				$plexItem['nowPlayingThumb'] = (string)$item['grandparentArt'];
+				$plexItem['nowPlayingKey'] = (string)$item['grandparentRatingKey'] . "-np";
+				$plexItem['nowPlayingTitle'] = (string)$item['grandparentTitle'] . ' - ' . (string)$item['title'];
+				$plexItem['nowPlayingBottom'] = 'S' . (string)$item['parentIndex'] . ' · E' . (string)$item['index'];
+				$plexItem['metadataKey'] = (string)$item['grandparentRatingKey'];
+				break;
+			case 'clip':
+				$useImage = (isset($item['live']) ? "plugins/images/cache/livetv.png" : null);
+				$plexItem['type'] = 'clip';
+				$plexItem['title'] = (isset($item['live']) ? 'Live TV' : (string)$item['title']);
+				$plexItem['secondaryTitle'] = '';
+				$plexItem['summary'] = (string)$item['summary'];
+				$plexItem['ratingKey'] = (string)$item['parentRatingKey'];
+				$plexItem['thumb'] = (string)$item['thumb'];
+				$plexItem['key'] = (string)$item['ratingKey'] . "-list";
+				$plexItem['nowPlayingThumb'] = (string)$item['art'];
+				$plexItem['nowPlayingKey'] = isset($item['ratingKey']) ? (string)$item['ratingKey'] . "-np" : (isset($item['live']) ? "livetv.png" : ":)");
+				$plexItem['nowPlayingTitle'] = $plexItem['title'];
+				$plexItem['nowPlayingBottom'] = isset($item['extraType']) ? "Trailer" : (isset($item['live']) ? "Live TV" : ":)");
+				break;
+			case 'album':
+			case 'track':
+				$plexItem['type'] = 'music';
+				$plexItem['title'] = (string)$item['parentTitle'];
+				$plexItem['secondaryTitle'] = (string)$item['title'];
+				$plexItem['summary'] = (string)$item['title'];
+				$plexItem['ratingKey'] = (string)$item['parentRatingKey'];
+				$plexItem['thumb'] = (string)$item['thumb'];
+				$plexItem['key'] = (string)$item['ratingKey'] . "-list";
+				$plexItem['nowPlayingThumb'] = ($item['parentThumb']) ? (string)$item['parentThumb'] : (string)$item['art'];
+				$plexItem['nowPlayingKey'] = (string)$item['parentRatingKey'] . "-np";
+				$plexItem['nowPlayingTitle'] = (string)$item['grandparentTitle'] . ' - ' . (string)$item['title'];
+				$plexItem['nowPlayingBottom'] = (string)$item['parentTitle'];
+				$plexItem['metadataKey'] = isset($item['grandparentRatingKey']) ? (string)$item['grandparentRatingKey'] : (string)$item['parentRatingKey'];
+				break;
+			default:
+				$plexItem['type'] = 'movie';
+				$plexItem['title'] = (string)$item['title'];
+				$plexItem['secondaryTitle'] = (string)$item['year'];
+				$plexItem['summary'] = (string)$item['summary'];
+				$plexItem['ratingKey'] = (string)$item['ratingKey'];
+				$plexItem['thumb'] = (string)$item['thumb'];
+				$plexItem['key'] = (string)$item['ratingKey'] . "-list";
+				$plexItem['nowPlayingThumb'] = (string)$item['art'];
+				$plexItem['nowPlayingKey'] = (string)$item['ratingKey'] . "-np";
+				$plexItem['nowPlayingTitle'] = (string)$item['title'];
+				$plexItem['nowPlayingBottom'] = (string)$item['year'];
+				$plexItem['metadataKey'] = (string)$item['ratingKey'];
+		}
+		$plexItem['originalType'] = $item['type'];
+		$plexItem['uid'] = (string)$item['ratingKey'];
+		$plexItem['elapsed'] = isset($item['viewOffset']) && $item['viewOffset'] !== '0' ? (int)$item['viewOffset'] : null;
+		$plexItem['duration'] = isset($item['duration']) ? (int)$item['duration'] : (int)$item->Media['duration'];
+		$plexItem['addedAt'] = isset($item['addedAt']) ? (int)$item['addedAt'] : null;
+		$plexItem['watched'] = ($plexItem['elapsed'] && $plexItem['duration'] ? floor(($plexItem['elapsed'] / $plexItem['duration']) * 100) : 0);
+		$plexItem['transcoded'] = isset($item->TranscodeSession['progress']) ? floor((int)$item->TranscodeSession['progress'] - $plexItem['watched']) : '';
+		$plexItem['stream'] = isset($item->Media->Part->Stream['decision']) ? (string)$item->Media->Part->Stream['decision'] : '';
+		$plexItem['id'] = str_replace('"', '', (string)$item->Player['machineIdentifier']);
+		$plexItem['session'] = (string)$item->Session['id'];
+		$plexItem['bandwidth'] = (string)$item->Session['bandwidth'];
+		$plexItem['bandwidthType'] = (string)$item->Session['location'];
+		$plexItem['sessionType'] = isset($item->TranscodeSession['progress']) ? 'Transcoding' : 'Direct Playing';
+		$plexItem['state'] = (((string)$item->Player['state'] == "paused") ? "pause" : "play");
+		$plexItem['user'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? (string)$item->User['title'] : "";
+		$plexItem['userThumb'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? (string)$item->User['thumb'] : "";
+		$plexItem['userAddress'] = ($this->config['homepageShowStreamNames'] && $this->qualifyRequest($this->config['homepageShowStreamNamesAuth'])) ? (string)$item->Player['address'] : "x.x.x.x";
+		$plexItem['address'] = $this->config['plexTabURL'] ? $this->config['plexTabURL'] . "/web/index.html#!/server/" . $this->config['plexID'] . "/details?key=/library/metadata/" . $item['ratingKey'] : "https://app.plex.tv/web/app#!/server/" . $this->config['plexID'] . "/details?key=/library/metadata/" . $item['ratingKey'];
+		$plexItem['nowPlayingOriginalImage'] = 'api/v2/homepage/image?source=plex&img=' . $plexItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $plexItem['nowPlayingKey'] . '$' . $this->randString();
+		$plexItem['originalImage'] = 'api/v2/homepage/image?source=plex&img=' . $plexItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $plexItem['key'] . '$' . $this->randString();
+		$plexItem['openTab'] = $this->config['plexTabURL'] && $this->config['plexTabName'] ? true : false;
+		$plexItem['tabName'] = $this->config['plexTabName'] ? $this->config['plexTabName'] : '';
+		// Stream info
+		$plexItem['userStream'] = array(
+			'platform' => (string)$item->Player['platform'],
+			'product' => (string)$item->Player['product'],
+			'device' => (string)$item->Player['device'],
+			'stream' => isset($item->Media) ? (string)$item->Media->Part['decision'] . ($item->TranscodeSession['throttled'] == '1' ? ' (Throttled)' : '') : '',
+			'videoResolution' => (string)$item->Media['videoResolution'],
+			'throttled' => ($item->TranscodeSession['throttled'] == 1) ? true : false,
+			'sourceVideoCodec' => (string)$item->TranscodeSession['sourceVideoCodec'],
+			'videoCodec' => (string)$item->TranscodeSession['videoCodec'],
+			'audioCodec' => (string)$item->TranscodeSession['audioCodec'],
+			'sourceAudioCodec' => (string)$item->TranscodeSession['sourceAudioCodec'],
+			'videoDecision' => $this->streamType((string)$item->TranscodeSession['videoDecision']),
+			'audioDecision' => $this->streamType((string)$item->TranscodeSession['audioDecision']),
+			'container' => (string)$item->TranscodeSession['container'],
+			'audioChannels' => (string)$item->TranscodeSession['audioChannels']
+		);
+		// Genre catch all
+		if ($item->Genre) {
+			$genres = array();
+			foreach ($item->Genre as $key => $value) {
+				$genres[] = (string)$value['tag'];
+			}
+		}
+		// Actor catch all
+		if ($item->Role) {
+			$actors = array();
+			foreach ($item->Role as $key => $value) {
+				if ($value['thumb']) {
+					$actors[] = array(
+						'name' => (string)$value['tag'],
+						'role' => (string)$value['role'],
+						'thumb' => (string)$value['thumb']
+					);
+				}
+			}
+		}
+		// Metadata information
+		$plexItem['metadata'] = array(
+			'guid' => (string)$item['guid'],
+			'summary' => (string)$item['summary'],
+			'rating' => (string)$item['rating'],
+			'duration' => (string)$item['duration'],
+			'originallyAvailableAt' => (string)$item['originallyAvailableAt'],
+			'year' => (string)$item['year'],
+			'studio' => (string)$item['studio'],
+			'tagline' => (string)$item['tagline'],
+			'genres' => ($item->Genre) ? $genres : '',
+			'actors' => ($item->Role) ? $actors : ''
+		);
+		if (file_exists($cacheDirectory . $plexItem['nowPlayingKey'] . '.jpg')) {
+			$plexItem['nowPlayingImageURL'] = $cacheDirectoryWeb . $plexItem['nowPlayingKey'] . '.jpg';
+		}
+		if (file_exists($cacheDirectory . $plexItem['key'] . '.jpg')) {
+			$plexItem['imageURL'] = $cacheDirectoryWeb . $plexItem['key'] . '.jpg';
+		}
+		if (file_exists($cacheDirectory . $plexItem['nowPlayingKey'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $plexItem['nowPlayingKey'] . '.jpg') || !file_exists($cacheDirectory . $plexItem['nowPlayingKey'] . '.jpg')) {
+			$plexItem['nowPlayingImageURL'] = 'api/v2/homepage/image?source=plex&img=' . $plexItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $plexItem['nowPlayingKey'] . '';
+		}
+		if (file_exists($cacheDirectory . $plexItem['key'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $plexItem['key'] . '.jpg') || !file_exists($cacheDirectory . $plexItem['key'] . '.jpg')) {
+			$plexItem['imageURL'] = 'api/v2/homepage/image?source=plex&img=' . $plexItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $plexItem['key'] . '';
+		}
+		if (!$plexItem['nowPlayingThumb']) {
+			$plexItem['nowPlayingOriginalImage'] = $plexItem['nowPlayingImageURL'] = "plugins/images/cache/no-np.png";
+			$plexItem['nowPlayingKey'] = "no-np";
+		}
+		if (!$plexItem['thumb'] || $plexItem['addedAt'] >= (time() - 300)) {
+			$plexItem['originalImage'] = $plexItem['imageURL'] = "plugins/images/cache/no-list.png";
+			$plexItem['key'] = "no-list";
+		}
+		if (isset($useImage)) {
+			$plexItem['useImage'] = $useImage;
+		}
+		return $plexItem;
+	}
+}

+ 267 - 0
api/homepage/qbittorrent.php

@@ -0,0 +1,267 @@
+<?php
+
+trait QBitTorrentHomepageItem
+{
+	public function qBittorrentSettingsArray()
+	{
+		return array(
+			'name' => 'qBittorrent',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/qBittorrent.png',
+			'category' => 'Downloader',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageqBittorrentEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageqBittorrentEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageqBittorrentAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageqBittorrentAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'qBittorrentURL',
+						'label' => 'URL',
+						'value' => $this->config['qBittorrentURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'select',
+						'name' => 'qBittorrentApiVersion',
+						'label' => 'API Version',
+						'value' => $this->config['qBittorrentApiVersion'],
+						'options' => $this->qBittorrentApiOptions()
+					),
+					array(
+						'type' => 'input',
+						'name' => 'qBittorrentUsername',
+						'label' => 'Username',
+						'value' => $this->config['qBittorrentUsername']
+					),
+					array(
+						'type' => 'password',
+						'name' => 'qBittorrentPassword',
+						'label' => 'Password',
+						'value' => $this->config['qBittorrentPassword']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'qBittorrentHideSeeding',
+						'label' => 'Hide Seeding',
+						'value' => $this->config['qBittorrentHideSeeding']
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'qBittorrentHideCompleted',
+						'label' => 'Hide Completed',
+						'value' => $this->config['qBittorrentHideCompleted']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'qBittorrentSortOrder',
+						'label' => 'Order',
+						'value' => $this->config['qBittorrentSortOrder'],
+						'options' => $this->qBittorrentSortOptions()
+					), array(
+						'type' => 'switch',
+						'name' => 'qBittorrentReverseSorting',
+						'label' => 'Reverse Sorting',
+						'value' => $this->config['qBittorrentReverseSorting']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageDownloadRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageDownloadRefresh'],
+						'options' => $this->timeOptions()
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'qBittorrentCombine',
+						'label' => 'Add to Combined Downloader',
+						'value' => $this->config['qBittorrentCombine']
+					),
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'qbittorrent\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	public function testConnectionQBittorrent()
+	{
+		if (empty($this->config['qBittorrentURL'])) {
+			$this->setAPIResponse('error', 'qBittorrent URL is not defined', 422);
+			return false;
+		}
+		$digest = $this->qualifyURL($this->config['qBittorrentURL'], true);
+		$data = array('username' => $this->config['qBittorrentUsername'], 'password' => $this->decrypt($this->config['qBittorrentPassword']));
+		$apiVersionLogin = ($this->config['qBittorrentApiVersion'] == '1') ? '/login' : '/api/v2/auth/login';
+		$apiVersionQuery = ($this->config['qBittorrentApiVersion'] == '1') ? '/query/torrents?sort=' : '/api/v2/torrents/info?sort=';
+		$url = $digest['scheme'] . '://' . $digest['host'] . $digest['port'] . $digest['path'] . $apiVersionLogin;
+		try {
+			$options = ($this->localURL($this->config['qBittorrentURL'])) ? array('verify' => false) : array();
+			$response = Requests::post($url, array(), $data, $options);
+			$reflection = new ReflectionClass($response->cookies);
+			$cookie = $reflection->getProperty("cookies");
+			$cookie->setAccessible(true);
+			$cookie = $cookie->getValue($response->cookies);
+			if ($cookie) {
+				$headers = array(
+					'Cookie' => 'SID=' . $cookie['SID']->value
+				);
+				$reverse = $this->config['qBittorrentReverseSorting'] ? 'true' : 'false';
+				$url = $digest['scheme'] . '://' . $digest['host'] . $digest['port'] . $digest['path'] . $apiVersionQuery . $this->config['qBittorrentSortOrder'] . '&reverse=' . $reverse;
+				$response = Requests::get($url, $headers, $options);
+				if ($response) {
+					$torrents = json_decode($response->body, true);
+					if (is_array($torrents)) {
+						$this->setAPIResponse('success', 'API Connection succeeded', 200);
+						return true;
+					} else {
+						$this->setAPIResponse('error', 'qBittorrent Error Occurred - Check URL or Credentials', 500);
+						return true;
+					}
+				} else {
+					$this->setAPIResponse('error', 'qBittorrent Connection Error Occurred - Check URL or Credentials', 500);
+					return true;
+				}
+			} else {
+				$this->writeLog('error', 'qBittorrent Connect Function - Error: Could not get session ID', 'SYSTEM');
+				$this->setAPIResponse('error', 'qBittorrent Connect Function - Error: Could not get session ID', 409);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'qBittorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function qBittorrentHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageqBittorrentEnabled'
+				],
+				'auth' => [
+					'homepageqBittorrentAuth'
+				],
+				'not_empty' => [
+					'qBittorrentURL'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderqBittorrent()
+	{
+		if ($this->homepageItemPermissions($this->qBittorrentHomepagePermissions('main'))) {
+			$loadingBox = ($this->config['qBittorrentCombine']) ? '' : '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+			$builder = ($this->config['qBittorrentCombine']) ? 'buildDownloaderCombined(\'qBittorrent\');' : '$("#' . __FUNCTION__ . '").html(buildDownloader("qBittorrent"));';
+			return '
+				<div id="' . __FUNCTION__ . '">
+					' . $loadingBox . '
+					<script>
+		                // homepageOrderqBittorrent
+		                ' . $builder . '
+		                homepageDownloader("qBittorrent", "' . $this->config['homepageDownloadRefresh'] . '");
+		                // End homepageOrderqBittorrent
+	                </script>
+				</div>
+				';
+		}
+	}
+	
+	public function getQBittorrentHomepageQueue()
+	{
+		if (!$this->homepageItemPermissions($this->qBittorrentHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$digest = $this->qualifyURL($this->config['qBittorrentURL'], true);
+		$data = array('username' => $this->config['qBittorrentUsername'], 'password' => $this->decrypt($this->config['qBittorrentPassword']));
+		$apiVersionLogin = ($this->config['qBittorrentApiVersion'] == '1') ? '/login' : '/api/v2/auth/login';
+		$apiVersionQuery = ($this->config['qBittorrentApiVersion'] == '1') ? '/query/torrents?sort=' : '/api/v2/torrents/info?sort=';
+		$url = $digest['scheme'] . '://' . $digest['host'] . $digest['port'] . $digest['path'] . $apiVersionLogin;
+		try {
+			$options = ($this->localURL($this->config['qBittorrentURL'])) ? array('verify' => false) : array();
+			$response = Requests::post($url, array(), $data, $options);
+			$reflection = new ReflectionClass($response->cookies);
+			$cookie = $reflection->getProperty("cookies");
+			$cookie->setAccessible(true);
+			$cookie = $cookie->getValue($response->cookies);
+			if ($cookie) {
+				$headers = array(
+					'Cookie' => 'SID=' . $cookie['SID']->value
+				);
+				$reverse = $this->config['qBittorrentReverseSorting'] ? 'true' : 'false';
+				$url = $digest['scheme'] . '://' . $digest['host'] . $digest['port'] . $digest['path'] . $apiVersionQuery . $this->config['qBittorrentSortOrder'] . '&reverse=' . $reverse;
+				$response = Requests::get($url, $headers, $options);
+				if ($response) {
+					$torrentList = json_decode($response->body, true);
+					if ($this->config['qBittorrentHideSeeding'] || $this->config['qBittorrentHideCompleted']) {
+						$filter = array();
+						$torrents = array();
+						if ($this->config['qBittorrentHideSeeding']) {
+							array_push($filter, 'uploading', 'stalledUP', 'queuedUP');
+						}
+						if ($this->config['qBittorrentHideCompleted']) {
+							array_push($filter, 'pausedUP');
+						}
+						foreach ($torrentList as $key => $value) {
+							if (!in_array($value['state'], $filter)) {
+								$torrents[] = $value;
+							}
+						}
+					} else {
+						$torrents = json_decode($response->body, true);
+					}
+					$api['content']['queueItems'] = $torrents;
+					$api['content']['historyItems'] = false;
+					$api['content'] = isset($api['content']) ? $api['content'] : false;
+					$this->setAPIResponse('success', null, 200, $api);
+					return $api;
+				}
+			} else {
+				$this->writeLog('error', 'qBittorrent Connect Function - Error: Could not get session ID', 'SYSTEM');
+				$this->setAPIResponse('error', 'qBittorrent Connect Function - Error: Could not get session ID', 409);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'qBittorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+}

+ 444 - 0
api/homepage/radarr.php

@@ -0,0 +1,444 @@
+<?php
+
+trait RadarrHomepageItem
+{
+	public function radarrSettingsArray()
+	{
+		return array(
+			'name' => 'Radarr',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/radarr.png',
+			'category' => 'PVR',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageRadarrEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageRadarrEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageRadarrAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageRadarrAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'radarrURL',
+						'label' => 'URL',
+						'value' => $this->config['radarrURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'radarrToken',
+						'label' => 'Token',
+						'value' => $this->config['radarrToken']
+					)
+				),
+				'API SOCKS' => array(
+					array(
+						'type' => 'html',
+						'override' => 12,
+						'label' => '',
+						'html' => '
+							<div class="panel panel-default">
+								<div class="panel-wrapper collapse in">
+									<div class="panel-body">
+										<h3 lang="en">Radarr SOCKS API Connection</h3>
+										<p>Using this feature allows you to access the Radarr API without having to reverse proxy it.  Just access it from: </p>
+										<code>' . $this->getServerPath() . 'api/v2/socks/radarr/</code>
+									</div>
+								</div>
+							</div>'
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'radarrSocksEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['radarrSocksEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'radarrSocksAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['radarrSocksAuth'],
+						'options' => $this->groupOptions
+					),
+				),
+				'Queue' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageRadarrQueueEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageRadarrQueueEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageRadarrQueueAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageRadarrQueueAuth'],
+						'options' => $this->groupOptions
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'homepageRadarrQueueCombine',
+						'label' => 'Add to Combined Downloader',
+						'value' => $this->config['homepageRadarrQueueCombine']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageRadarrQueueRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageRadarrQueueRefresh'],
+						'options' => $this->timeOptions()
+					),
+				),
+				'Calendar' => array(
+					array(
+						'type' => 'number',
+						'name' => 'calendarStart',
+						'label' => '# of Days Before',
+						'value' => $this->config['calendarStart'],
+						'placeholder' => ''
+					),
+					array(
+						'type' => 'number',
+						'name' => 'calendarEnd',
+						'label' => '# of Days After',
+						'value' => $this->config['calendarEnd'],
+						'placeholder' => ''
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarFirstDay',
+						'label' => 'Start Day',
+						'value' => $this->config['calendarFirstDay'],
+						'options' => $this->daysOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarDefault',
+						'label' => 'Default View',
+						'value' => $this->config['calendarDefault'],
+						'options' => $this->calendarDefaultOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarTimeFormat',
+						'label' => 'Time Format',
+						'value' => $this->config['calendarTimeFormat'],
+						'options' => $this->timeFormatOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarLocale',
+						'label' => 'Locale',
+						'value' => $this->config['calendarLocale'],
+						'options' => $this->calendarLocaleOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarLimit',
+						'label' => 'Items Per Day',
+						'value' => $this->config['calendarLimit'],
+						'options' => $this->limitOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['calendarRefresh'],
+						'options' => $this->timeOptions()
+					)
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'radarr\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	public function testConnectionRadarr()
+	{
+		if (empty($this->config['radarrURL'])) {
+			$this->setAPIResponse('error', 'Radarr URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['radarrToken'])) {
+			$this->setAPIResponse('error', 'Radarr Token is not defined', 422);
+			return false;
+		}
+		$failed = false;
+		$errors = '';
+		$list = $this->csvHomepageUrlToken($this->config['radarrURL'], $this->config['radarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
+				$results = $downloader->getSystemStatus();
+				$downloadList = json_decode($results, true);
+				if (is_array($downloadList) || is_object($downloadList)) {
+					$queue = (array_key_exists('error', $downloadList)) ? $downloadList['error']['msg'] : $downloadList;
+					if (!is_array($queue)) {
+						$ip = $value['url'];
+						$errors .= $ip . ': ' . $queue;
+						$failed = true;
+					}
+				} else {
+					$ip = $value['url'];
+					$errors .= $ip . ': Response was not JSON';
+					$failed = true;
+				}
+				
+			} catch (Exception $e) {
+				$failed = true;
+				$ip = $value['url'];
+				$errors .= $ip . ': ' . $e->getMessage();
+				$this->writeLog('error', 'Radarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+		}
+		if ($failed) {
+			$this->setAPIResponse('error', $errors, 500);
+			return false;
+		} else {
+			$this->setAPIResponse('success', null, 200);
+			return true;
+		}
+	}
+	
+	public function radarrHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'calendar' => [
+				'enabled' => [
+					'homepageRadarrEnabled'
+				],
+				'auth' => [
+					'homepageRadarrAuth'
+				],
+				'not_empty' => [
+					'radarrURL',
+					'lidarrToken'
+				]
+			],
+			'queue' => [
+				'enabled' => [
+					'homepageRadarrEnabled',
+					'homepageRadarrQueueEnabled'
+				],
+				'auth' => [
+					'homepageRadarrAuth',
+					'homepageRadarrQueueAuth'
+				],
+				'not_empty' => [
+					'radarrURL',
+					'radarrToken'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderRadarrQueue()
+	{
+		if ($this->homepageItemPermissions($this->radarrHomepagePermissions('queue'))) {
+			$loadingBox = ($this->config['homepageRadarrQueueCombine']) ? '' : '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+			$builder = ($this->config['homepageRadarrQueueCombine']) ? 'buildDownloaderCombined(\'radarr\');' : '$("#' . __FUNCTION__ . '").html(buildDownloader("radarr"));';
+			return '
+				<div id="' . __FUNCTION__ . '">
+					' . $loadingBox . '
+					<script>
+		                // homepageOrderRadarrQueue
+		                ' . $builder . '
+		                homepageDownloader("radarr", "' . $this->config['homepageRadarrQueueRefresh'] . '");
+		                // End homepageOrderRadarrQueue
+	                </script>
+				</div>
+				';
+		}
+	}
+	
+	public function getRadarrQueue()
+	{
+		if (!$this->homepageItemPermissions($this->radarrHomepagePermissions('queue'), true)) {
+			return false;
+		}
+		$queueItems = array();
+		$list = $this->csvHomepageUrlToken($this->config['radarrURL'], $this->config['radarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
+				$results = $downloader->getQueue();
+				$downloadList = json_decode($results, true);
+				if (is_array($downloadList) || is_object($downloadList)) {
+					$queue = (array_key_exists('error', $downloadList)) ? '' : $downloadList;
+				} else {
+					$queue = '';
+				}
+				if (!empty($queue)) {
+					$queueItems = array_merge($queueItems, $queue);
+				}
+			} catch (Exception $e) {
+				$this->writeLog('error', 'Radarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+		}
+		$api['content']['queueItems'] = $queueItems;
+		$api['content']['historyItems'] = false;
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;;
+	}
+	
+	public function getRadarrCalendar($startDate = null, $endDate = null)
+	{
+		$startDate = ($startDate) ?? $_GET['start'];
+		$endDate = ($endDate) ?? $_GET['end'];
+		if (!$this->homepageItemPermissions($this->radarrHomepagePermissions('calendar'), true)) {
+			return false;
+		}
+		$calendarItems = array();
+		$list = $this->csvHomepageUrlToken($this->config['radarrURL'], $this->config['radarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
+				$results = $downloader->getCalendar($startDate, $endDate);
+				$result = json_decode($results, true);
+				if (is_array($result) || is_object($result)) {
+					$calendar = (array_key_exists('error', $result)) ? '' : $this->formatRadarrCalendar($results, $key, $value['url']);
+				} else {
+					$calendar = '';
+				}
+			} catch (Exception $e) {
+				$this->writeLog('error', 'Radarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+			if (!empty($calendar)) {
+				$calendarItems = array_merge($calendarItems, $calendar);
+			}
+		}
+		$this->setAPIResponse('success', null, 200, $calendarItems);
+		return $calendarItems;
+	}
+	
+	public function formatRadarrCalendar($array, $number, $url)
+	{
+		$url = rtrim($url, '/'); //remove trailing slash
+		$url = $url . '/api';
+		$array = json_decode($array, true);
+		$gotCalendar = array();
+		$i = 0;
+		foreach ($array as $child) {
+			if (isset($child['physicalRelease'])) {
+				$i++;
+				$movieName = $child['title'];
+				$movieID = $child['tmdbId'];
+				if (!isset($movieID)) {
+					$movieID = "";
+				}
+				$physicalRelease = $child['physicalRelease'];
+				$physicalRelease = strtotime($physicalRelease);
+				$physicalRelease = date("Y-m-d", $physicalRelease);
+				if (new DateTime() < new DateTime($physicalRelease)) {
+					$notReleased = "true";
+				} else {
+					$notReleased = "false";
+				}
+				$downloaded = $child['hasFile'];
+				if ($downloaded == "0" && $notReleased == "true") {
+					$downloaded = "text-info";
+				} elseif ($downloaded == "1") {
+					$downloaded = "text-success";
+				} else {
+					$downloaded = "text-danger";
+				}
+				$banner = "/plugins/images/cache/no-np.png";
+				foreach ($child['images'] as $image) {
+					if ($image['coverType'] == "banner" || $image['coverType'] == "fanart") {
+						if (strpos($image['url'], '://') === false) {
+							$imageUrl = $image['url'];
+							$urlParts = explode("/", $url);
+							$imageParts = explode("/", $image['url']);
+							if ($imageParts[1] == end($urlParts)) {
+								unset($imageParts[1]);
+								$imageUrl = implode("/", $imageParts);
+							}
+							$banner = $url . $imageUrl . '?apikey=' . $this->config['radarrToken'];
+						} else {
+							$banner = $image['url'];
+						}
+						
+					}
+				}
+				if ($banner !== "/plugins/images/cache/no-np.png" || (strpos($banner, 'apikey') !== false)) {
+					$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+					$imageURL = $banner;
+					$cacheFile = $cacheDirectory . $movieID . '.jpg';
+					$banner = 'plugins/images/cache/' . $movieID . '.jpg';
+					if (!file_exists($cacheFile)) {
+						$this->cacheImage($imageURL, $movieID);
+						unset($imageURL);
+						unset($cacheFile);
+					}
+				}
+				$alternativeTitles = "";
+				foreach ($child['alternativeTitles'] as $alternative) {
+					$alternativeTitles .= $alternative['title'] . ', ';
+				}
+				$alternativeTitles = empty($child['alternativeTitles']) ? "" : substr($alternativeTitles, 0, -2);
+				$details = array(
+					"topTitle" => $movieName,
+					"bottomTitle" => $alternativeTitles,
+					"status" => $child['status'],
+					"overview" => $child['overview'],
+					"runtime" => $child['runtime'],
+					"image" => $banner,
+					"ratings" => $child['ratings']['value'],
+					"videoQuality" => $child["hasFile"] ? @$child['movieFile']['quality']['quality']['name'] : "unknown",
+					"audioChannels" => $child["hasFile"] ? @$child['movieFile']['mediaInfo']['audioChannels'] : "unknown",
+					"audioCodec" => $child["hasFile"] ? @$child['movieFile']['mediaInfo']['audioFormat'] : "unknown",
+					"videoCodec" => $child["hasFile"] ? @$child['movieFile']['mediaInfo']['videoCodec'] : "unknown",
+					"size" => $child["hasFile"] ? @$child['movieFile']['size'] : "unknown",
+					"genres" => $child['genres'],
+					"year" => isset($child['year']) ? $child['year'] : '',
+					"studio" => isset($child['studio']) ? $child['studio'] : '',
+				);
+				array_push($gotCalendar, array(
+					"id" => "Radarr-" . $number . "-" . $i,
+					"title" => $movieName,
+					"start" => $physicalRelease,
+					"className" => "inline-popups bg-calendar movieID--" . $movieID,
+					"imagetype" => "film " . $downloaded,
+					"imagetypeFilter" => "film",
+					"downloadFilter" => $downloaded,
+					"bgColor" => str_replace('text', 'bg', $downloaded),
+					"details" => $details
+				));
+			}
+		}
+		if ($i != 0) {
+			return $gotCalendar;
+		}
+		return false;
+	}
+}

+ 372 - 0
api/homepage/rtorrent.php

@@ -0,0 +1,372 @@
+<?php
+
+trait RTorrentHomepageItem
+{
+	public function rTorrentSettingsArray()
+	{
+		$xmlStatus = (extension_loaded('xmlrpc')) ? 'Installed' : 'Not Installed';
+		return array(
+			'name' => 'rTorrent',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/rTorrent.png',
+			'category' => 'Downloader',
+			'settings' => array(
+				'FYI' => array(
+					array(
+						'type' => 'html',
+						'label' => '',
+						'override' => 12,
+						'html' => '
+						<div class="row">
+						    <div class="col-lg-12">
+						        <div class="panel panel-info">
+						            <div class="panel-heading">
+						                <span lang="en">ATTENTION</span>
+						            </div>
+						            <div class="panel-wrapper collapse in" aria-expanded="true">
+						                <div class="panel-body">
+						                	<h4 lang="en">This module requires XMLRPC</h4>
+						                    <span lang="en">Status: [ <b>' . $xmlStatus . '</b> ]</span>
+						                    <br/></br>
+						                    <span lang="en">
+						                    	<h4><b>Note about API URL</b></h4>
+						                    	Organizr appends the url with <code>/RPC2</code> unless the URL ends in <code>.php</code><br/>
+						                    	<h5>Possible URLs:</h5>
+						                    	<li>http://localhost:8080</li>
+						                    	<li>https://domain.site/xmlrpc.php</li>
+						                    	<li>https://seedbox.site/rutorrent/plugins/httprpc/action.php</li>
+						                    </span>
+						                </div>
+						            </div>
+						        </div>
+						    </div>
+						</div>
+						'
+					)
+				),
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepagerTorrentEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepagerTorrentEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepagerTorrentAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepagerTorrentAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'rTorrentURL',
+						'label' => 'URL',
+						'value' => $this->config['rTorrentURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'input',
+						'name' => 'rTorrentURLOverride',
+						'label' => 'rTorrent API URL Override',
+						'value' => $this->config['rTorrentURLOverride'],
+						'help' => 'Only use if you cannot connect.  Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port/xmlrpc'
+					),
+					array(
+						'type' => 'input',
+						'name' => 'rTorrentUsername',
+						'label' => 'Username',
+						'value' => $this->config['rTorrentUsername']
+					),
+					array(
+						'type' => 'password',
+						'name' => 'rTorrentPassword',
+						'label' => 'Password',
+						'value' => $this->config['rTorrentPassword']
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'rTorrentDisableCertCheck',
+						'label' => 'Disable Certificate Check',
+						'value' => $this->config['rTorrentDisableCertCheck']
+					),
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'rTorrentHideSeeding',
+						'label' => 'Hide Seeding',
+						'value' => $this->config['rTorrentHideSeeding']
+					), array(
+						'type' => 'switch',
+						'name' => 'rTorrentHideCompleted',
+						'label' => 'Hide Completed',
+						'value' => $this->config['rTorrentHideCompleted']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'rTorrentSortOrder',
+						'label' => 'Order',
+						'value' => $this->config['rTorrentSortOrder'],
+						'options' => $this->rTorrentSortOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageDownloadRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageDownloadRefresh'],
+						'options' => $this->timeOptions()
+					),
+					array(
+						'type' => 'number',
+						'name' => 'rTorrentLimit',
+						'label' => 'Item Limit',
+						'value' => $this->config['rTorrentLimit'],
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'rTorrentCombine',
+						'label' => 'Add to Combined Downloader',
+						'value' => $this->config['rTorrentCombine']
+					),
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'rtorrent\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	public function testConnectionRTorrent()
+	{
+		if (empty($this->config['rTorrentURL']) && empty($this->config['rTorrentURLOverride'])) {
+			$this->setAPIResponse('error', 'rTorrent URL is not defined', 422);
+			return false;
+		}
+		try {
+			$digest = (empty($this->config['rTorrentURLOverride'])) ? $this->qualifyURL($this->config['rTorrentURL'], true) : $this->qualifyURL($this->checkOverrideURL($this->config['rTorrentURL'], $this->config['rTorrentURLOverride']), true);
+			$passwordInclude = ($this->config['rTorrentUsername'] !== '' && $this->config['rTorrentPassword'] !== '') ? $this->config['rTorrentUsername'] . ':' . $this->decrypt($this->config['rTorrentPassword']) . "@" : '';
+			$extraPath = (strpos($this->config['rTorrentURL'], '.php') !== false) ? '' : '/RPC2';
+			$extraPath = (empty($this->config['rTorrentURLOverride'])) ? $extraPath : '';
+			$url = $digest['scheme'] . '://' . $passwordInclude . $digest['host'] . $digest['port'] . $digest['path'] . $extraPath;
+			$options = ($this->localURL($url, $this->config['rTorrentDisableCertCheck'])) ? array('verify' => false) : array();
+			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);
+			}
+			$data = xmlrpc_encode_request("system.listMethods", null);
+			$response = Requests::post($url, array(), $data, $options);
+			if ($response->success) {
+				$methods = xmlrpc_decode(str_replace('i8>', 'i4>', $response->body));
+				if (count($methods) !== 0) {
+					$this->setAPIResponse('success', 'API Connection succeeded', 200);
+					return true;
+				}
+			}
+			$this->setAPIResponse('error', 'rTorrent error occurred', 500);
+			return false;
+		} catch
+		(Requests_Exception $e) {
+			$this->writeLog('error', 'rTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function rTorrentHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepagerTorrentEnabled'
+				],
+				'auth' => [
+					'homepagerTorrentAuth'
+				],
+				'not_empty' => []
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderrTorrent()
+	{
+		if ($this->homepageItemPermissions($this->rTorrentHomepagePermissions('main'))) {
+			$loadingBox = ($this->config['rTorrentCombine']) ? '' : '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+			$builder = ($this->config['rTorrentCombine']) ? 'buildDownloaderCombined(\'rTorrent\');' : '$("#' . __FUNCTION__ . '").html(buildDownloader("rTorrent"));';
+			return '
+				<div id="' . __FUNCTION__ . '">
+					' . $loadingBox . '
+					<script>
+		                // homepageOrderrTorrent
+		                ' . $builder . '
+		                homepageDownloader("rTorrent", "' . $this->config['homepageDownloadRefresh'] . '");
+		                // End homepageOrderrTorrent
+	                </script>
+				</div>
+				';
+		}
+	}
+	
+	public function checkOverrideURL($url, $override)
+	{
+		if (strpos($override, $url) !== false) {
+			return $override;
+		} else {
+			return $url . $override;
+		}
+	}
+	
+	public function rTorrentStatus($completed, $state, $status)
+	{
+		if ($completed && $state && $status == 'seed') {
+			$state = 'Seeding';
+		} elseif (!$completed && !$state && $status == 'leech') {
+			$state = 'Stopped';
+		} elseif (!$completed && $state && $status == 'leech') {
+			$state = 'Downloading';
+		} elseif ($completed && !$state && $status == 'seed') {
+			$state = 'Finished';
+		}
+		return ($state) ? $state : $status;
+	}
+	
+	public function getRTorrentHomepageQueue()
+	{
+		if (empty($this->config['rTorrentURL']) && empty($this->config['rTorrentURLOverride'])) {
+			$this->setAPIResponse('error', 'rTorrent URL is not defined', 422);
+			return false;
+		}
+		if (!$this->homepageItemPermissions($this->rTorrentHomepagePermissions('main'), true)) {
+			return false;
+		}
+		try {
+			if ($this->config['rTorrentLimit'] == '0') {
+				$this->config['rTorrentLimit'] = '1000';
+			}
+			$torrents = array();
+			$digest = (empty($this->config['rTorrentURLOverride'])) ? $this->qualifyURL($this->config['rTorrentURL'], true) : $this->qualifyURL($this->checkOverrideURL($this->config['rTorrentURL'], $this->config['rTorrentURLOverride']), true);
+			$passwordInclude = ($this->config['rTorrentUsername'] !== '' && $this->config['rTorrentPassword'] !== '') ? $this->config['rTorrentUsername'] . ':' . $this->decrypt($this->config['rTorrentPassword']) . "@" : '';
+			$extraPath = (strpos($this->config['rTorrentURL'], '.php') !== false) ? '' : '/RPC2';
+			$extraPath = (empty($this->config['rTorrentURLOverride'])) ? $extraPath : '';
+			$url = $digest['scheme'] . '://' . $passwordInclude . $digest['host'] . $digest['port'] . $digest['path'] . $extraPath;
+			$options = (localURL($url, $this->config['rTorrentDisableCertCheck'])) ? array('verify' => false) : array();
+			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);
+			}
+			$data = xmlrpc_encode_request("d.multicall2", array(
+				"",
+				"main",
+				"d.name=",
+				"d.base_path=",
+				"d.up.total=",
+				"d.size_bytes=",
+				"d.down.total=",
+				"d.completed_bytes=",
+				"d.connection_current=",
+				"d.down.rate=",
+				"d.up.rate=",
+				"d.timestamp.started=",
+				"d.state=",
+				"d.group.name=",
+				"d.hash=",
+				"d.complete=",
+				"d.ratio=",
+				"d.chunk_size=",
+				"f.size_bytes=",
+				"f.size_chunks=",
+				"f.completed_chunks=",
+				"d.custom=",
+				"d.custom1=",
+				"d.custom2=",
+				"d.custom3=",
+				"d.custom4=",
+				"d.custom5=",
+			), array());
+			$response = Requests::post($url, array(), $data, $options);
+			if ($response->success) {
+				$torrentList = xmlrpc_decode(str_replace('i8>', 'string>', $response->body));
+				foreach ($torrentList as $key => $value) {
+					$tempStatus = $this->rTorrentStatus($value[13], $value[10], $value[6]);
+					if ($tempStatus == 'Seeding' && $this->config['rTorrentHideSeeding']) {
+						//do nothing
+					} elseif ($tempStatus == 'Finished' && $this->config['rTorrentHideCompleted']) {
+						//do nothing
+					} else {
+						$torrents[$key] = array(
+							'name' => $value[0],
+							'base' => $value[1],
+							'upTotal' => $value[2],
+							'size' => $value[3],
+							'downTotal' => $value[4],
+							'downloaded' => $value[5],
+							'connectionState' => $value[6],
+							'leech' => $value[7],
+							'seed' => $value[8],
+							'date' => $value[9],
+							'state' => ($value[10]) ? 'on' : 'off',
+							'group' => $value[11],
+							'hash' => $value[12],
+							'complete' => ($value[13]) ? 'yes' : 'no',
+							'ratio' => $value[14],
+							'label' => $value[20],
+							'status' => $tempStatus,
+							'temp' => $value[16] . ' - ' . $value[17] . ' - ' . $value[18],
+							'custom' => $value[19] . ' - ' . $value[20] . ' - ' . $value[21],
+							'custom2' => $value[22] . ' - ' . $value[23] . ' - ' . $value[24],
+						);
+					}
+				}
+				if (count($torrents) !== 0) {
+					usort($torrents, function ($a, $b) {
+						$direction = substr($this->config['rTorrentSortOrder'], -1);
+						$sort = substr($this->config['rTorrentSortOrder'], 0, strlen($this->config['rTorrentSortOrder']) - 1);
+						switch ($direction) {
+							case 'a':
+								return $a[$sort] <=> $b[$sort];
+								break;
+							case 'd':
+								return $b[$sort] <=> $a[$sort];
+								break;
+							default:
+								return $b['date'] <=> $a['date'];
+						}
+					});
+					$torrents = array_slice($torrents, 0, $this->config['rTorrentLimit']);
+				}
+				$api['content']['queueItems'] = $torrents;
+				$api['content']['historyItems'] = false;
+			}
+		} catch
+		(Requests_Exception $e) {
+			$this->writeLog('error', 'rTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 227 - 0
api/homepage/sabnzbd.php

@@ -0,0 +1,227 @@
+<?php
+
+trait SabNZBdHomepageItem
+{
+	
+	public function sabNZBdSettingsArray()
+	{
+		return array(
+			'name' => 'SabNZBD',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/sabnzbd.png',
+			'category' => 'Downloader',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageSabnzbdEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageSabnzbdEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageSabnzbdAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageSabnzbdAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'sabnzbdURL',
+						'label' => 'URL',
+						'value' => $this->config['sabnzbdURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'sabnzbdToken',
+						'label' => 'Token',
+						'value' => $this->config['sabnzbdToken']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'select',
+						'name' => 'homepageDownloadRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageDownloadRefresh'],
+						'options' => $this->timeOptions()
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'sabnzbdCombine',
+						'label' => 'Add to Combined Downloader',
+						'value' => $this->config['sabnzbdCombine']
+					),
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'sabnzbd\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	public function testConnectionSabNZBd()
+	{
+		if (!empty($this->config['sabnzbdURL']) && !empty($this->config['sabnzbdToken'])) {
+			$url = $this->qualifyURL($this->config['sabnzbdURL']);
+			$url = $url . '/api?mode=queue&output=json&apikey=' . $this->config['sabnzbdToken'];
+			try {
+				$options = ($this->localURL($url)) ? array('verify' => false) : array();
+				$response = Requests::get($url, array(), $options);
+				if ($response->success) {
+					$this->setAPIResponse('success', 'API Connection succeeded', 200);
+					return true;
+				}
+			} catch (Requests_Exception $e) {
+				$this->setAPIResponse('error', $e->getMessage(), 500);
+				return false;
+			};
+		} else {
+			$this->setAPIResponse('error', 'URL and/or Token not setup', 422);
+			return 'URL and/or Token not setup';
+		}
+	}
+	
+	public function sabNZBdHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageSabnzbdEnabled'
+				],
+				'auth' => [
+					'homepageSabnzbdAuth'
+				],
+				'not_empty' => [
+					'sabnzbdURL',
+					'sabnzbdToken'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrdersabnzbd()
+	{
+		if ($this->homepageItemPermissions($this->sabNZBdHomepagePermissions('main'))) {
+			$loadingBox = ($this->config['sabnzbdCombine']) ? '' : '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+			$builder = ($this->config['sabnzbdCombine']) ? 'buildDownloaderCombined(\'sabnzbd\');' : '$("#' . __FUNCTION__ . '").html(buildDownloader("sabnzbd"));';
+			return '
+				<div id="' . __FUNCTION__ . '">
+					' . $loadingBox . '
+					<script>
+		                // homepageOrdersabnzbd
+		                ' . $builder . '
+		                homepageDownloader("sabnzbd", "' . $this->config['homepageDownloadRefresh'] . '");
+		                // End homepageOrdersabnzbd
+	                </script>
+				</div>
+				';
+		}
+	}
+	
+	public function getSabNZBdHomepageQueue()
+	{
+		if (!$this->homepageItemPermissions($this->sabNZBdHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['sabnzbdURL']);
+		$url = $url . '/api?mode=queue&output=json&apikey=' . $this->config['sabnzbdToken'];
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$api['content']['queueItems'] = json_decode($response->body, true);
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$url = $this->qualifyURL($this->config['sabnzbdURL']);
+		$url = $url . '/api?mode=history&output=json&limit=100&apikey=' . $this->config['sabnzbdToken'];
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$api['content']['historyItems'] = json_decode($response->body, true);
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+	
+	public function pauseSabNZBdQueue($target = null)
+	{
+		if (!$this->homepageItemPermissions($this->sabNZBdHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['sabnzbdURL']);
+		$id = ($target !== '' && $target !== 'main' && isset($target)) ? 'mode=queue&name=pause&value=' . $target . '&' : 'mode=pause';
+		$url = $url . '/api?' . $id . '&output=json&apikey=' . $this->config['sabnzbdToken'];
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$api['content'] = json_decode($response->body, true);
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+	
+	public function resumeSabNZBdQueue($target = null)
+	{
+		if (!$this->homepageItemPermissions($this->sabNZBdHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['sabnzbdURL']);
+		$id = ($target !== '' && $target !== 'main' && isset($target)) ? 'mode=queue&name=resume&value=' . $target . '&' : 'mode=resume';
+		$url = $url . '/api?' . $id . '&output=json&apikey=' . $this->config['sabnzbdToken'];
+		try {
+			$options = ($this->localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$api['content'] = json_decode($response->body, true);
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 496 - 0
api/homepage/sickrage.php

@@ -0,0 +1,496 @@
+<?php
+
+trait SickRageHomepageItem
+{
+	public function sickrageSettingsArray()
+	{
+		return array(
+			'name' => 'SickRage',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/sickrage.png',
+			'category' => 'PVR',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageSickrageEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageSickrageEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageSickrageAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageSickrageAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'sickrageURL',
+						'label' => 'URL',
+						'value' => $this->config['sickrageURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'sickrageToken',
+						'label' => 'Token',
+						'value' => $this->config['sickrageToken']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'select',
+						'name' => 'calendarFirstDay',
+						'label' => 'Start Day',
+						'value' => $this->config['calendarFirstDay'],
+						'options' => $this->daysOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarDefault',
+						'label' => 'Default View',
+						'value' => $this->config['calendarDefault'],
+						'options' => $this->calendarDefaultOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarTimeFormat',
+						'label' => 'Time Format',
+						'value' => $this->config['calendarTimeFormat'],
+						'options' => $this->timeFormatOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarLocale',
+						'label' => 'Locale',
+						'value' => $this->config['calendarLocale'],
+						'options' => $this->calendarLocaleOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarLimit',
+						'label' => 'Items Per Day',
+						'value' => $this->config['calendarLimit'],
+						'options' => $this->limitOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['calendarRefresh'],
+						'options' => $this->timeOptions()
+					)
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'sickrage\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	public function testConnectionSickRage()
+	{
+		if (empty($this->config['sickrageURL'])) {
+			$this->setAPIResponse('error', 'SickRage URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['sickrageToken'])) {
+			$this->setAPIResponse('error', 'SickRage Token is not defined', 422);
+			return false;
+		}
+		$failed = false;
+		$errors = '';
+		$list = $this->csvHomepageUrlToken($this->config['sickrageURL'], $this->config['sickrageToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\SickRage\SickRage($value['url'], $value['token']);
+				$results = $downloader->sb();
+				$downloadList = json_decode($results, true);
+				if (is_array($downloadList) || is_object($downloadList)) {
+					$queue = (array_key_exists('error', $downloadList)) ? $downloadList['error']['msg'] : $downloadList;
+					if (!is_array($queue)) {
+						$ip = $value['url'];
+						$errors .= $ip . ': ' . $queue;
+						$failed = true;
+					}
+				} else {
+					$ip = $value['url'];
+					$errors .= $ip . ': Response was not JSON';
+					$failed = true;
+				}
+				
+			} catch (Exception $e) {
+				$failed = true;
+				$ip = $value['url'];
+				$errors .= $ip . ': ' . $e->getMessage();
+				$this->writeLog('error', 'SickRage Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+		}
+		if ($failed) {
+			$this->setAPIResponse('error', $errors, 500);
+			return false;
+		} else {
+			$this->setAPIResponse('success', null, 200);
+			return true;
+		}
+	}
+	
+	public function sickrageHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'calendar' => [
+				'enabled' => [
+					'homepageSickrageEnabled'
+				],
+				'auth' => [
+					'homepageSickrageAuth'
+				],
+				'not_empty' => [
+					'sickrageURL',
+					'sickrageToken'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function getSickRageCalendar($startDate = null, $endDate = null)
+	{
+		if (!$this->homepageItemPermissions($this->sickrageHomepagePermissions('calendar'), true)) {
+			return false;
+		}
+		$calendarItems = array();
+		$list = $this->csvHomepageUrlToken($this->config['sickrageURL'], $this->config['sickrageToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\SickRage\SickRage($value['url'], $value['token']);
+				$sickrageFuture = $this->formatSickrageCalendarWanted($downloader->future(), $key);
+				$sickrageHistory = $this->formatSickrageCalendarHistory($downloader->history("100", "downloaded"), $key);
+				if (!empty($sickrageFuture)) {
+					$calendarItems = array_merge($calendarItems, $sickrageFuture);
+				}
+				if (!empty($sickrageHistory)) {
+					$calendarItems = array_merge($calendarItems, $sickrageHistory);
+				}
+			} catch (Exception $e) {
+				$this->writeLog('error', 'SickRage Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+		}
+		$this->setAPIResponse('success', null, 200, $calendarItems);
+		return $calendarItems;
+	}
+	
+	public function formatSickrageCalendarWanted($array, $number)
+	{
+		$array = json_decode($array, true);
+		$gotCalendar = array();
+		$i = 0;
+		foreach ($array['data']['missed'] as $child) {
+			$i++;
+			$seriesName = $child['show_name'];
+			$seriesID = $child['tvdbid'];
+			$episodeID = $child['tvdbid'];
+			$episodeAirDate = $child['airdate'];
+			$episodeAirDateTime = explode(" ", $child['airs']);
+			$episodeAirDateTime = date("H:i:s", strtotime($episodeAirDateTime[1] . $episodeAirDateTime[2]));
+			$episodeAirDate = strtotime($episodeAirDate . $episodeAirDateTime);
+			$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
+			if (new DateTime() < new DateTime($episodeAirDate)) {
+				$unaired = true;
+			}
+			$downloaded = "0";
+			if ($downloaded == "0" && isset($unaired)) {
+				$downloaded = "text-info";
+			} elseif ($downloaded == "1") {
+				$downloaded = "text-success";
+			} else {
+				$downloaded = "text-danger";
+			}
+			$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']) . ' - ' . $child['ep_name'];
+			$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+			$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+			$fanart = "/plugins/images/cache/no-np.png";
+			if (file_exists($cacheFile)) {
+				$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+				unset($cacheFile);
+			}
+			$details = array(
+				"seasonCount" => "",
+				"status" => $child['show_status'],
+				"topTitle" => $seriesName,
+				"bottomTitle" => $bottomTitle,
+				"overview" => isset($child['ep_plot']) ? $child['ep_plot'] : '',
+				"runtime" => "",
+				"image" => $fanart,
+				"ratings" => "",
+				"videoQuality" => isset($child["quality"]) ? $child["quality"] : "",
+				"audioChannels" => "",
+				"audioCodec" => "",
+				"videoCodec" => "",
+				"size" => "",
+				"genres" => "",
+			);
+			array_push($gotCalendar, array(
+				"id" => "Sick-" . $number . "-Miss-" . $i,
+				"title" => $seriesName,
+				"start" => $episodeAirDate,
+				"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
+				"imagetype" => "tv " . $downloaded,
+				"imagetypeFilter" => "tv",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details,
+			));
+		}
+		foreach ($array['data']['today'] as $child) {
+			$i++;
+			$seriesName = $child['show_name'];
+			$seriesID = $child['tvdbid'];
+			$episodeID = $child['tvdbid'];
+			$episodeAirDate = $child['airdate'];
+			$episodeAirDateTime = explode(" ", $child['airs']);
+			$episodeAirDateTime = date("H:i:s", strtotime($episodeAirDateTime[1] . $episodeAirDateTime[2]));
+			$episodeAirDate = strtotime($episodeAirDate . $episodeAirDateTime);
+			$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
+			if (new DateTime() < new DateTime($episodeAirDate)) {
+				$unaired = true;
+			}
+			$downloaded = "0";
+			if ($downloaded == "0" && isset($unaired)) {
+				$downloaded = "text-info";
+			} elseif ($downloaded == "1") {
+				$downloaded = "text-success";
+			} else {
+				$downloaded = "text-danger";
+			}
+			$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']) . ' - ' . $child['ep_name'];
+			$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+			$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+			$fanart = "/plugins/images/cache/no-np.png";
+			if (file_exists($cacheFile)) {
+				$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+				unset($cacheFile);
+			}
+			$details = array(
+				"seasonCount" => "",
+				"status" => $child['show_status'],
+				"topTitle" => $seriesName,
+				"bottomTitle" => $bottomTitle,
+				"overview" => isset($child['ep_plot']) ? $child['ep_plot'] : '',
+				"runtime" => "",
+				"image" => $fanart,
+				"ratings" => "",
+				"videoQuality" => isset($child["quality"]) ? $child["quality"] : "",
+				"audioChannels" => "",
+				"audioCodec" => "",
+				"videoCodec" => "",
+				"size" => "",
+				"genres" => "",
+			);
+			array_push($gotCalendar, array(
+				"id" => "Sick-" . $number . "-Today-" . $i,
+				"title" => $seriesName,
+				"start" => $episodeAirDate,
+				"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
+				"imagetype" => "tv " . $downloaded,
+				"imagetypeFilter" => "tv",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details,
+			));
+		}
+		foreach ($array['data']['soon'] as $child) {
+			$i++;
+			$seriesName = $child['show_name'];
+			$seriesID = $child['tvdbid'];
+			$episodeID = $child['tvdbid'];
+			$episodeAirDate = $child['airdate'];
+			$episodeAirDateTime = explode(" ", $child['airs']);
+			$episodeAirDateTime = date("H:i:s", strtotime($episodeAirDateTime[1] . $episodeAirDateTime[2]));
+			$episodeAirDate = strtotime($episodeAirDate . $episodeAirDateTime);
+			$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
+			if (new DateTime() < new DateTime($episodeAirDate)) {
+				$unaired = true;
+			}
+			$downloaded = "0";
+			if ($downloaded == "0" && isset($unaired)) {
+				$downloaded = "text-info";
+			} elseif ($downloaded == "1") {
+				$downloaded = "text-success";
+			} else {
+				$downloaded = "text-danger";
+			}
+			$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']) . ' - ' . $child['ep_name'];
+			$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+			$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+			$fanart = "/plugins/images/cache/no-np.png";
+			if (file_exists($cacheFile)) {
+				$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+				unset($cacheFile);
+			}
+			$details = array(
+				"seasonCount" => "",
+				"status" => $child['show_status'],
+				"topTitle" => $seriesName,
+				"bottomTitle" => $bottomTitle,
+				"overview" => isset($child['ep_plot']) ? $child['ep_plot'] : '',
+				"runtime" => "",
+				"image" => $fanart,
+				"ratings" => "",
+				"videoQuality" => isset($child["quality"]) ? $child["quality"] : "",
+				"audioChannels" => "",
+				"audioCodec" => "",
+				"videoCodec" => "",
+				"size" => "",
+				"genres" => "",
+			);
+			array_push($gotCalendar, array(
+				"id" => "Sick-" . $number . "-Soon-" . $i,
+				"title" => $seriesName,
+				"start" => $episodeAirDate,
+				"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
+				"imagetype" => "tv " . $downloaded,
+				"imagetypeFilter" => "tv",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details,
+			));
+		}
+		foreach ($array['data']['later'] as $child) {
+			$i++;
+			$seriesName = $child['show_name'];
+			$seriesID = $child['tvdbid'];
+			$episodeID = $child['tvdbid'];
+			$episodeAirDate = $child['airdate'];
+			$episodeAirDateTime = explode(" ", $child['airs']);
+			$episodeAirDateTime = date("H:i:s", strtotime($episodeAirDateTime[1] . $episodeAirDateTime[2]));
+			$episodeAirDate = strtotime($episodeAirDate . $episodeAirDateTime);
+			$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
+			if (new DateTime() < new DateTime($episodeAirDate)) {
+				$unaired = true;
+			}
+			$downloaded = "0";
+			if ($downloaded == "0" && isset($unaired)) {
+				$downloaded = "text-info";
+			} elseif ($downloaded == "1") {
+				$downloaded = "text-success";
+			} else {
+				$downloaded = "text-danger";
+			}
+			$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']) . ' - ' . $child['ep_name'];
+			$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+			$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+			$fanart = "/plugins/images/cache/no-np.png";
+			if (file_exists($cacheFile)) {
+				$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+				unset($cacheFile);
+			}
+			$details = array(
+				"seasonCount" => "",
+				"status" => $child['show_status'],
+				"topTitle" => $seriesName,
+				"bottomTitle" => $bottomTitle,
+				"overview" => isset($child['ep_plot']) ? $child['ep_plot'] : '',
+				"runtime" => "",
+				"image" => $fanart,
+				"ratings" => "",
+				"videoQuality" => isset($child["quality"]) ? $child["quality"] : "",
+				"audioChannels" => "",
+				"audioCodec" => "",
+				"videoCodec" => "",
+				"size" => "",
+				"genres" => "",
+			);
+			array_push($gotCalendar, array(
+				"id" => "Sick-" . $number . "-Later-" . $i,
+				"title" => $seriesName,
+				"start" => $episodeAirDate,
+				"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
+				"imagetype" => "tv " . $downloaded,
+				"imagetypeFilter" => "tv",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details,
+			));
+		}
+		if ($i != 0) {
+			return $gotCalendar;
+		}
+		return false;
+	}
+	
+	public function formatSickrageCalendarHistory($array, $number)
+	{
+		$array = json_decode($array, true);
+		$gotCalendar = array();
+		$i = 0;
+		foreach ($array['data'] as $child) {
+			$i++;
+			$seriesName = $child['show_name'];
+			$seriesID = $child['tvdbid'];
+			$episodeID = $child['tvdbid'];
+			$episodeAirDate = $child['date'];
+			$downloaded = "text-success";
+			$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']);
+			$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+			$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+			$fanart = "/plugins/images/cache/no-np.png";
+			if (file_exists($cacheFile)) {
+				$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+				unset($cacheFile);
+			}
+			$details = array(
+				"seasonCount" => "",
+				"status" => $child['status'],
+				"topTitle" => $seriesName,
+				"bottomTitle" => $bottomTitle,
+				"overview" => '',
+				"runtime" => isset($child['series']['runtime']) ? $child['series']['runtime'] : 30,
+				"image" => $fanart,
+				"ratings" => isset($child['series']['ratings']['value']) ? $child['series']['ratings']['value'] : "unknown",
+				"videoQuality" => isset($child["quality"]) ? $child['quality'] : "unknown",
+				"audioChannels" => "",
+				"audioCodec" => "",
+				"videoCodec" => "",
+				"size" => "",
+				"genres" => "",
+			);
+			array_push($gotCalendar, array(
+				"id" => "Sick-" . $number . "-History-" . $i,
+				"title" => $seriesName,
+				"start" => $episodeAirDate,
+				"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
+				"imagetype" => "tv " . $downloaded,
+				"imagetypeFilter" => "tv",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details,
+			));
+		}
+		if ($i != 0) {
+			return $gotCalendar;
+		}
+		return false;
+	}
+}

+ 443 - 0
api/homepage/sonarr.php

@@ -0,0 +1,443 @@
+<?php
+
+trait SonarrHomepageItem
+{
+	public function sonarrSettingsArray()
+	{
+		return array(
+			'name' => 'Sonarr',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/sonarr.png',
+			'category' => 'PVR',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageSonarrEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageSonarrEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageSonarrAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageSonarrAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'sonarrURL',
+						'label' => 'URL',
+						'value' => $this->config['sonarrURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'sonarrToken',
+						'label' => 'Token',
+						'value' => $this->config['sonarrToken']
+					)
+				),
+				'API SOCKS' => array(
+					array(
+						'type' => 'html',
+						'override' => 12,
+						'label' => '',
+						'html' => '
+							<div class="panel panel-default">
+								<div class="panel-wrapper collapse in">
+									<div class="panel-body">
+										<h3 lang="en">Sonarr SOCKS API Connection</h3>
+										<p>Using this feature allows you to access the Sonarr API without having to reverse proxy it.  Just access it from: </p>
+										<code>' . $this->getServerPath() . 'api/v2/socks/sonarr/</code>
+									</div>
+								</div>
+							</div>'
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'sonarrSocksEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['sonarrSocksEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'sonarrSocksAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['sonarrSocksAuth'],
+						'options' => $this->groupOptions
+					),
+				),
+				'Queue' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageSonarrQueueEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageSonarrQueueEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageSonarrQueueAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageSonarrQueueAuth'],
+						'options' => $this->groupOptions
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'homepageSonarrQueueCombine',
+						'label' => 'Add to Combined Downloader',
+						'value' => $this->config['homepageSonarrQueueCombine']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageSonarrQueueRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageSonarrQueueRefresh'],
+						'options' => $this->timeOptions()
+					),
+				),
+				'Calendar' => array(
+					array(
+						'type' => 'number',
+						'name' => 'calendarStart',
+						'label' => '# of Days Before',
+						'value' => $this->config['calendarStart'],
+						'placeholder' => ''
+					),
+					array(
+						'type' => 'number',
+						'name' => 'calendarEnd',
+						'label' => '# of Days After',
+						'value' => $this->config['calendarEnd'],
+						'placeholder' => ''
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarFirstDay',
+						'label' => 'Start Day',
+						'value' => $this->config['calendarFirstDay'],
+						'options' => $this->daysOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarDefault',
+						'label' => 'Default View',
+						'value' => $this->config['calendarDefault'],
+						'options' => $this->calendarDefaultOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarTimeFormat',
+						'label' => 'Time Format',
+						'value' => $this->config['calendarTimeFormat'],
+						'options' => $this->timeFormatOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarLocale',
+						'label' => 'Locale',
+						'value' => $this->config['calendarLocale'],
+						'options' => $this->calendarLocaleOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarLimit',
+						'label' => 'Items Per Day',
+						'value' => $this->config['calendarLimit'],
+						'options' => $this->limitOptions()
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['calendarRefresh'],
+						'options' => $this->timeOptions()
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'sonarrUnmonitored',
+						'label' => 'Show Unmonitored',
+						'value' => $this->config['sonarrUnmonitored']
+					)
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'sonarr\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	public function testConnectionSonarr()
+	{
+		if (empty($this->config['sonarrURL'])) {
+			$this->setAPIResponse('error', 'Sonarr URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['sonarrToken'])) {
+			$this->setAPIResponse('error', 'Sonarr Token is not defined', 422);
+			return false;
+		}
+		$failed = false;
+		$errors = '';
+		$list = $this->csvHomepageUrlToken($this->config['sonarrURL'], $this->config['sonarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
+				$results = $downloader->getSystemStatus();
+				$downloadList = json_decode($results, true);
+				if (is_array($downloadList) || is_object($downloadList)) {
+					$queue = (array_key_exists('error', $downloadList)) ? $downloadList['error']['msg'] : $downloadList;
+					if (!is_array($queue)) {
+						$ip = $value['url'];
+						$errors .= $ip . ': ' . $queue;
+						$failed = true;
+					}
+				} else {
+					$ip = $value['url'];
+					$errors .= $ip . ': Response was not JSON';
+					$failed = true;
+				}
+				
+			} catch (Exception $e) {
+				$failed = true;
+				$ip = $value['url'];
+				$errors .= $ip . ': ' . $e->getMessage();
+				$this->writeLog('error', 'Sonarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+		}
+		if ($failed) {
+			$this->setAPIResponse('error', $errors, 500);
+			return false;
+		} else {
+			$this->setAPIResponse('success', null, 200);
+			return true;
+		}
+	}
+	
+	public function sonarrHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'calendar' => [
+				'enabled' => [
+					'homepageSonarrEnabled'
+				],
+				'auth' => [
+					'homepageSonarrAuth'
+				],
+				'not_empty' => [
+					'sonarrURL',
+					'sonarrToken'
+				]
+			],
+			'queue' => [
+				'enabled' => [
+					'homepageSonarrEnabled',
+					'homepageSonarrQueueEnabled'
+				],
+				'auth' => [
+					'homepageSonarrAuth',
+					'homepageSonarrQueueAuth'
+				],
+				'not_empty' => [
+					'sonarrURL',
+					'sonarrToken'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderSonarrQueue()
+	{
+		if ($this->homepageItemPermissions($this->sonarrHomepagePermissions('queue'))) {
+			$loadingBox = ($this->config['homepageSonarrQueueCombine']) ? '' : '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+			$builder = ($this->config['homepageSonarrQueueCombine']) ? 'buildDownloaderCombined(\'sonarr\');' : '$("#' . __FUNCTION__ . '").html(buildDownloader("sonarr"));';
+			return '
+				<div id="' . __FUNCTION__ . '">
+					' . $loadingBox . '
+					<script>
+		                // homepageOrderSonarrQueue
+		                ' . $builder . '
+		                homepageDownloader("sonarr", "' . $this->config['homepageSonarrQueueRefresh'] . '");
+		                // End homepageOrderSonarrQueue
+	                </script>
+				</div>
+				';
+		}
+	}
+	
+	public function getSonarrQueue()
+	{
+		if (!$this->homepageItemPermissions($this->sonarrHomepagePermissions('queue'), true)) {
+			return false;
+		}
+		$queueItems = array();
+		$list = $this->csvHomepageUrlToken($this->config['sonarrURL'], $this->config['sonarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$downloader = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
+				$results = $downloader->getQueue();
+				$downloadList = json_decode($results, true);
+				if (is_array($downloadList) || is_object($downloadList)) {
+					$queue = (array_key_exists('error', $downloadList)) ? '' : $downloadList;
+				} else {
+					$queue = '';
+				}
+				if (!empty($queue)) {
+					$queueItems = array_merge($queueItems, $queue);
+				}
+			} catch (Exception $e) {
+				$this->writeLog('error', 'Sonarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+		}
+		$api['content']['queueItems'] = $queueItems;
+		$api['content']['historyItems'] = false;
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;;
+	}
+	
+	public function getSonarrCalendar($startDate = null, $endDate = null)
+	{
+		$startDate = ($startDate) ?? $_GET['start'];
+		$endDate = ($endDate) ?? $_GET['end'];
+		if (!$this->homepageItemPermissions($this->sonarrHomepagePermissions('calendar'), true)) {
+			return false;
+		}
+		$calendarItems = array();
+		$list = $this->csvHomepageUrlToken($this->config['sonarrURL'], $this->config['sonarrToken']);
+		foreach ($list as $key => $value) {
+			try {
+				$sonarr = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
+				$sonarr = $sonarr->getCalendar($startDate, $endDate, $this->config['sonarrUnmonitored']);
+				$result = json_decode($sonarr, true);
+				if (is_array($result) || is_object($result)) {
+					$sonarrCalendar = (array_key_exists('error', $result)) ? '' : $this->formatSonarrCalendar($sonarr, $key);
+				} else {
+					$sonarrCalendar = '';
+				}
+			} catch (Exception $e) {
+				$this->writeLog('error', 'Sonarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+			if (!empty($sonarrCalendar)) {
+				$calendarItems = array_merge($calendarItems, $sonarrCalendar);
+			}
+		}
+		$this->setAPIResponse('success', null, 200, $calendarItems);
+		return $calendarItems;
+	}
+	
+	public function formatSonarrCalendar($array, $number)
+	{
+		$array = json_decode($array, true);
+		$gotCalendar = array();
+		$i = 0;
+		foreach ($array as $child) {
+			$i++;
+			$seriesName = $child['series']['title'];
+			$seriesID = $child['series']['tvdbId'];
+			$episodeID = $child['series']['tvdbId'];
+			$monitored = $child['monitored'];
+			if (!isset($episodeID)) {
+				$episodeID = "";
+			}
+			//$episodeName = htmlentities($child['title'], ENT_QUOTES);
+			$episodeAirDate = $child['airDateUtc'];
+			$episodeAirDate = strtotime($episodeAirDate);
+			$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
+			if (new DateTime() < new DateTime($episodeAirDate)) {
+				$unaired = true;
+			}
+			if ($child['episodeNumber'] == "1") {
+				$episodePremier = "true";
+			} else {
+				$episodePremier = "false";
+				$date = new DateTime($episodeAirDate);
+				$date->add(new DateInterval("PT1S"));
+				$date->format(DateTime::ATOM);
+				$child['airDateUtc'] = gmdate('Y-m-d\TH:i:s\Z', strtotime($date->format(DateTime::ATOM)));
+			}
+			$downloaded = $child['hasFile'];
+			if ($downloaded == "0" && isset($unaired) && $episodePremier == "true") {
+				$downloaded = "text-primary animated flash";
+			} elseif ($downloaded == "0" && isset($unaired) && $monitored == "0") {
+				$downloaded = "text-dark";
+			} elseif ($downloaded == "0" && isset($unaired)) {
+				$downloaded = "text-info";
+			} elseif ($downloaded == "1") {
+				$downloaded = "text-success";
+			} else {
+				$downloaded = "text-danger";
+			}
+			$fanart = "/plugins/images/cache/no-np.png";
+			foreach ($child['series']['images'] as $image) {
+				if ($image['coverType'] == "fanart") {
+					$fanart = $image['url'];
+				}
+			}
+			if ($fanart !== "/plugins/images/cache/no-np.png" || (strpos($fanart, '://') === false)) {
+				$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+				$imageURL = $fanart;
+				$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+				$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+				if (!file_exists($cacheFile)) {
+					$this->cacheImage($imageURL, $seriesID);
+					unset($imageURL);
+					unset($cacheFile);
+				}
+			}
+			$bottomTitle = 'S' . sprintf("%02d", $child['seasonNumber']) . 'E' . sprintf("%02d", $child['episodeNumber']) . ' - ' . $child['title'];
+			$details = array(
+				"seasonCount" => $child['series']['seasonCount'],
+				"status" => $child['series']['status'],
+				"topTitle" => $seriesName,
+				"bottomTitle" => $bottomTitle,
+				"overview" => isset($child['overview']) ? $child['overview'] : '',
+				"runtime" => $child['series']['runtime'],
+				"image" => $fanart,
+				"ratings" => $child['series']['ratings']['value'],
+				"videoQuality" => $child["hasFile"] && isset($child['episodeFile']['quality']['quality']['name']) ? $child['episodeFile']['quality']['quality']['name'] : "unknown",
+				"audioChannels" => $child["hasFile"] && isset($child['episodeFile']['mediaInfo']) ? $child['episodeFile']['mediaInfo']['audioChannels'] : "unknown",
+				"audioCodec" => $child["hasFile"] && isset($child['episodeFile']['mediaInfo']) ? $child['episodeFile']['mediaInfo']['audioCodec'] : "unknown",
+				"videoCodec" => $child["hasFile"] && isset($child['episodeFile']['mediaInfo']) ? $child['episodeFile']['mediaInfo']['videoCodec'] : "unknown",
+				"size" => $child["hasFile"] && isset($child['episodeFile']['size']) ? $child['episodeFile']['size'] : "unknown",
+				"genres" => $child['series']['genres'],
+			);
+			array_push($gotCalendar, array(
+				"id" => "Sonarr-" . $number . "-" . $i,
+				"title" => $seriesName,
+				"start" => $child['airDateUtc'],
+				"className" => "inline-popups bg-calendar calendar-item tvID--" . $episodeID,
+				"imagetype" => "tv " . $downloaded,
+				"imagetypeFilter" => "tv",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details
+			));
+		}
+		if ($i != 0) {
+			return $gotCalendar;
+		}
+		return false;
+	}
+}

+ 150 - 0
api/homepage/speedtest.php

@@ -0,0 +1,150 @@
+<?php
+
+trait SpeedTestHomepageItem
+{
+	public function speedTestSettingsArray()
+	{
+		return array(
+			'name' => 'Speedtest',
+			'enabled' => true,
+			'image' => 'plugins/images/tabs/speedtest-icon.png',
+			'category' => 'Monitor',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'html',
+						'override' => 6,
+						'label' => 'Info',
+						'html' => '<p>This homepage item requires <a href="https://github.com/henrywhitaker3/Speedtest-Tracker" target="_blank" rel="noreferrer noopener">Speedtest-Tracker <i class="fa fa-external-link" aria-hidden="true"></i></a> to be running on your network.</p>'
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'homepageSpeedtestEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageSpeedtestEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageSpeedtestAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageSpeedtestAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'speedtestURL',
+						'label' => 'URL',
+						'value' => $this->config['speedtestURL'],
+						'help' => 'Enter the IP:PORT of your speedtest instance e.g. http(s)://<ip>:<port>'
+					),
+				),
+				'Options' => array(
+					array(
+						'type' => 'input',
+						'name' => 'speedtestHeader',
+						'label' => 'Title',
+						'value' => $this->config['speedtestHeader'],
+						'help' => 'Sets the title of this homepage module',
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'speedtestHeaderToggle',
+						'label' => 'Toggle Title',
+						'value' => $this->config['speedtestHeaderToggle'],
+						'help' => 'Shows/hides the title of this homepage module'
+					),
+				),
+			)
+		);
+	}
+	
+	public function speedTestHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageSpeedtestEnabled'
+				],
+				'auth' => [
+					'homepageSpeedtestAuth'
+				],
+				'not_empty' => [
+					'speedtestURL'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderSpeedtest()
+	{
+		if ($this->homepageItemPermissions($this->speedTestHomepagePermissions('main'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Speedtest...</h2></div>
+					<script>
+						// Speedtest
+						homepageSpeedtest("' . $this->config['homepageSpeedtestRefresh'] . '");
+						// End Speedtest
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	public function getSpeedtestHomepageData()
+	{
+		if (!$this->homepageItemPermissions($this->speedTestHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$api = [];
+		$url = $this->qualifyURL($this->config['speedtestURL']);
+		$dataUrl = $url . '/api/speedtest/latest';
+		try {
+			$response = Requests::get($dataUrl);
+			if ($response->success) {
+				$json = json_decode($response->body, true);
+				$api['data'] = [
+					'current' => $json['data'],
+				];
+				$keys = [
+					'average',
+					'max',
+					'maximum',
+					'minimum'
+				];
+				foreach ($keys as $key) {
+					if (array_key_exists($key, $json)) {
+						if ($key == 'max') {
+							$api['data']['maximum'] = $json[$key];
+						} else {
+							$api['data'][$key] = $json[$key];
+						}
+					}
+				}
+				$api['options'] = [
+					'title' => $this->config['speedtestHeader'],
+					'titleToggle' => $this->config['speedtestHeaderToggle'],
+				];
+			} else {
+				$this->setAPIResponse('error', 'SpeedTest connection error', 409);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Speedtest Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api = isset($api) ? $api : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 322 - 0
api/homepage/tautulli.php

@@ -0,0 +1,322 @@
+<?php
+
+trait TautulliHomepageItem
+{
+	public function tautulliSettingsArray()
+	{
+		return array(
+			'name' => 'Tautulli',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/tautulli.png',
+			'category' => 'Monitor',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageTautulliEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageTautulliEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageTautulliAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageTautulliAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Options' => array(
+					array(
+						'type' => 'input',
+						'name' => 'tautulliHeader',
+						'label' => 'Title',
+						'value' => $this->config['tautulliHeader'],
+						'help' => 'Sets the title of this homepage module'
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'tautulliHeaderToggle',
+						'label' => 'Toggle Title',
+						'value' => $this->config['tautulliHeaderToggle'],
+						'help' => 'Shows/hides the title of this homepage module'
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'tautulliURL',
+						'label' => 'URL',
+						'value' => $this->config['tautulliURL'],
+						'help' => 'URL for Tautulli API, include the IP, the port and the base URL (e.g. /tautulli/) in the URL',
+						'placeholder' => 'http://<ip>:<port>'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'tautulliApikey',
+						'label' => 'API Key',
+						'value' => $this->config['tautulliApikey']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageTautulliRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageTautulliRefresh'],
+						'options' => $this->timeOptions()
+					),
+				),
+				'Library Stats' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'tautulliLibraries',
+						'label' => 'Libraries',
+						'value' => $this->config['tautulliLibraries'],
+						'help' => 'Shows/hides the card with library information.',
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageTautulliLibraryAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageTautulliLibraryAuth'],
+						'options' => $this->groupOptions
+					),
+				),
+				'Viewing Stats' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'tautulliPopularMovies',
+						'label' => 'Popular Movies',
+						'value' => $this->config['tautulliPopularMovies'],
+						'help' => 'Shows/hides the card with Popular Movies information.',
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'tautulliPopularTV',
+						'label' => 'Popular TV',
+						'value' => $this->config['tautulliPopularTV'],
+						'help' => 'Shows/hides the card with Popular TV information.',
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'tautulliTopMovies',
+						'label' => 'Top Movies',
+						'value' => $this->config['tautulliTopMovies'],
+						'help' => 'Shows/hides the card with Top Movies information.',
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'tautulliTopTV',
+						'label' => 'Top TV',
+						'value' => $this->config['tautulliTopTV'],
+						'help' => 'Shows/hides the card with Top TV information.',
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageTautulliViewsAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageTautulliViewsAuth'],
+						'options' => $this->groupOptions
+					),
+				),
+				'Misc Stats' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'tautulliTopUsers',
+						'label' => 'Top Users',
+						'value' => $this->config['tautulliTopUsers'],
+						'help' => 'Shows/hides the card with Top Users information.',
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'tautulliTopPlatforms',
+						'label' => 'Top Platforms',
+						'value' => $this->config['tautulliTopPlatforms'],
+						'help' => 'Shows/hides the card with Top Platforms information.',
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageTautulliMiscAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageTautulliMiscAuth'],
+						'options' => $this->groupOptions
+					),
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'tautulli\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	public function testConnectionTautulli()
+	{
+		if (empty($this->config['tautulliURL'])) {
+			$this->setAPIResponse('error', 'Tautulli URL is not defined', 422);
+			return false;
+		}
+		if (empty($this->config['tautulliApikey'])) {
+			$this->setAPIResponse('error', 'Tautulli Token is not defined', 422);
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['tautulliURL']);
+		$apiURL = $url . '/api/v2?apikey=' . $this->config['tautulliApikey'];
+		try {
+			$homestatsUrl = $apiURL . '&cmd=get_home_stats&grouping=1';
+			$homestats = Requests::get($homestatsUrl, [], []);
+			if ($homestats->success) {
+				$this->setAPIResponse('success', 'API Connection succeeded', 200);
+				return true;
+			} else {
+				$this->setAPIResponse('error', 'Tautulli Error Occurred - Check URL or Credentials', 409);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Tautulli Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function tautulliHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageTautulliEnabled'
+				],
+				'auth' => [
+					'homepageTautulliAuth'
+				],
+				'not_empty' => [
+					'tautulliURL',
+					'tautulliApikey'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrdertautulli()
+	{
+		if ($this->homepageItemPermissions($this->tautulliHomepagePermissions('main'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Tautulli...</h2></div>
+					<script>
+						// Tautulli
+						homepageTautulli("' . $this->config['homepageTautulliRefresh'] . '");
+						// End Tautulli
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	public function getTautulliHomepageData()
+	{
+		if (!$this->homepageItemPermissions($this->tautulliHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$api = [];
+		$url = $this->qualifyURL($this->config['tautulliURL']);
+		$apiURL = $url . '/api/v2?apikey=' . $this->config['tautulliApikey'];
+		$height = $this->getCacheImageSize('h');
+		$width = $this->getCacheImageSize('w');
+		$nowPlayingHeight = $this->getCacheImageSize('nph');
+		$nowPlayingWidth = $this->getCacheImageSize('npw');
+		try {
+			$homestatsUrl = $apiURL . '&cmd=get_home_stats&grouping=1';
+			$homestats = Requests::get($homestatsUrl, [], []);
+			if ($homestats->success) {
+				$homestats = json_decode($homestats->body, true);
+				$api['homestats'] = $homestats['response'];
+				// Cache art & thumb for first result in each tautulli API result
+				$categories = ['top_movies', 'top_tv', 'popular_movies', 'popular_tv'];
+				foreach ($categories as $cat) {
+					$key = array_search($cat, array_column($api['homestats']['data'], 'stat_id'));
+					$img = $api['homestats']['data'][$key]['rows'][0];
+					$this->cacheImage($url . '/pms_image_proxy?img=' . $img['art'] . '&rating_key=' . $img['rating_key'] . '&width=' . $nowPlayingWidth . '&height=' . $nowPlayingHeight, $img['rating_key'] . '-np');
+					$this->cacheImage($url . '/pms_image_proxy?img=' . $img['thumb'] . '&rating_key=' . $img['rating_key'] . '&width=' . $width . '&height=' . $height, $img['rating_key'] . '-list');
+					$img['art'] = 'plugins/images/cache/' . $img['rating_key'] . '-np.jpg';
+					$img['thumb'] = 'plugins/images/cache/' . $img['rating_key'] . '-list.jpg';
+					$api['homestats']['data'][$key]['rows'][0] = $img;
+				}
+				// Cache the platform icon
+				$key = array_search('top_platforms', array_column($api['homestats']['data'], 'stat_id'));
+				$platform = $api['homestats']['data'][$key]['rows'][0]['platform_name'];
+				$this->cacheImage($url . '/images/platforms/' . $platform . '.svg', 'tautulli-' . $platform, 'svg');
+			}
+			$libstatsUrl = $apiURL . '&cmd=get_libraries';
+			$libstats = Requests::get($libstatsUrl, [], []);
+			if ($libstats->success) {
+				$libstats = json_decode($libstats->body, true);
+				$api['libstats'] = $libstats['response'];
+				$categories = ['movie.svg', 'show.svg', 'artist.svg'];
+				foreach ($categories as $cat) {
+					$parts = explode('.', $cat);
+					$this->cacheImage($url . '/images/libraries/' . $cat, 'tautulli-' . $parts[0], $parts[1]);
+				}
+			}
+			$api['options'] = [
+				'url' => $url,
+				'libraries' => $this->config['tautulliLibraries'],
+				'topMovies' => $this->config['tautulliTopMovies'],
+				'topTV' => $this->config['tautulliTopTV'],
+				'topUsers' => $this->config['tautulliTopUsers'],
+				'topPlatforms' => $this->config['tautulliTopPlatforms'],
+				'popularMovies' => $this->config['tautulliPopularMovies'],
+				'popularTV' => $this->config['tautulliPopularTV'],
+				'title' => $this->config['tautulliHeaderToggle'],
+			];
+			$ids = []; // Array of stat_ids to remove from the returned array
+			if (!$this->qualifyRequest($this->config['homepageTautulliLibraryAuth'])) {
+				$api['options']['libraries'] = false;
+				unset($api['libstats']);
+			}
+			if (!$this->qualifyRequest($this->config['homepageTautulliViewsAuth'])) {
+				$api['options']['topMovies'] = false;
+				$api['options']['topTV'] = false;
+				$api['options']['popularMovies'] = false;
+				$api['options']['popularTV'] = false;
+				$ids = array_merge(['top_movies', 'popular_movies', 'popular_tv', 'top_tv'], $ids);
+				$api['homestats']['data'] = array_values($api['homestats']['data']);
+			}
+			if (!$this->qualifyRequest($this->config['homepageTautulliMiscAuth'])) {
+				$api['options']['topUsers'] = false;
+				$api['options']['topPlatforms'] = false;
+				$ids = array_merge(['top_platforms', 'top_users'], $ids);
+				$api['homestats']['data'] = array_values($api['homestats']['data']);
+			}
+			$ids = array_merge(['top_music', 'popular_music', 'last_watched', 'most_concurrent'], $ids);
+			foreach ($ids as $id) {
+				if ($key = array_search($id, array_column($api['homestats']['data'], 'stat_id'))) {
+					unset($api['homestats']['data'][$key]);
+					$api['homestats']['data'] = array_values($api['homestats']['data']);
+				}
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Tautulli Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api = isset($api) ? $api : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 248 - 0
api/homepage/transmission.php

@@ -0,0 +1,248 @@
+<?php
+
+trait TransmissionHomepageItem
+{
+	public function transmissionSettingsArray()
+	{
+		return array(
+			'name' => 'Transmission',
+			'enabled' => strpos('personal', $this->config['license']) !== false,
+			'image' => 'plugins/images/tabs/transmission.png',
+			'category' => 'Downloader',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageTransmissionEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageTransmissionEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageTransmissionAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageTransmissionAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'transmissionURL',
+						'label' => 'URL',
+						'value' => $this->config['transmissionURL'],
+						'help' => 'Please do not included /web in URL.  Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'input',
+						'name' => 'transmissionUsername',
+						'label' => 'Username',
+						'value' => $this->config['transmissionUsername']
+					),
+					array(
+						'type' => 'password',
+						'name' => 'transmissionPassword',
+						'label' => 'Password',
+						'value' => $this->config['transmissionPassword']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'transmissionHideSeeding',
+						'label' => 'Hide Seeding',
+						'value' => $this->config['transmissionHideSeeding']
+					), array(
+						'type' => 'switch',
+						'name' => 'transmissionHideCompleted',
+						'label' => 'Hide Completed',
+						'value' => $this->config['transmissionHideCompleted']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageDownloadRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageDownloadRefresh'],
+						'options' => $this->timeOptions()
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'transmissionCombine',
+						'label' => 'Add to Combined Downloader',
+						'value' => $this->config['transmissionCombine']
+					),
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'transmission\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	public function testConnectionTransmission()
+	{
+		if (empty($this->config['transmissionURL'])) {
+			$this->setAPIResponse('error', 'Transmission URL is not defined', 422);
+			return false;
+		}
+		$digest = $this->qualifyURL($this->config['transmissionURL'], true);
+		$passwordInclude = ($this->config['transmissionUsername'] != '' && $this->config['transmissionPassword'] != '') ? $this->config['transmissionUsername'] . ':' . $this->decrypt($this->config['transmissionPassword']) . "@" : '';
+		$url = $digest['scheme'] . '://' . $passwordInclude . $digest['host'] . $digest['port'] . $digest['path'] . '/rpc';
+		try {
+			$options = ($this->localURL($this->config['transmissionURL'])) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->headers['x-transmission-session-id']) {
+				$headers = array(
+					'X-Transmission-Session-Id' => $response->headers['x-transmission-session-id'],
+					'Content-Type' => 'application/json'
+				);
+				$data = array(
+					'method' => 'torrent-get',
+					'arguments' => array(
+						'fields' => array(
+							"id", "name", "totalSize", "eta", "isFinished", "isStalled", "percentDone", "rateDownload", "status", "downloadDir", "errorString"
+						),
+					),
+					'tags' => ''
+				);
+				$response = Requests::post($url, $headers, json_encode($data), $options);
+				if ($response->success) {
+					$this->setAPIResponse('success', 'API Connection succeeded', 200);
+					return true;
+				} else {
+					$this->setAPIResponse('error', 'Transmission Connect Function - Error: Unknown', 500);
+					return false;
+				}
+			} else {
+				$this->writeLog('error', 'Transmission Connect Function - Error: Could not get session ID', 'SYSTEM');
+				$this->setAPIResponse('error', 'Transmission Connect Function - Error: Could not get session ID', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Transmission Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		}
+	}
+	
+	public function transmissionHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageTransmissionEnabled'
+				],
+				'auth' => [
+					'homepageTransmissionAuth'
+				],
+				'not_empty' => [
+					'transmissionURL'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrdertransmission()
+	{
+		if ($this->homepageItemPermissions($this->transmissionHomepagePermissions('main'))) {
+			$loadingBox = ($this->config['transmissionCombine']) ? '' : '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+			$builder = ($this->config['transmissionCombine']) ? 'buildDownloaderCombined(\'transmission\');' : '$("#' . __FUNCTION__ . '").html(buildDownloader("transmission"));';
+			return '
+				<div id="' . __FUNCTION__ . '">
+					' . $loadingBox . '
+					<script>
+		                // homepageOrdertransmission
+		                ' . $builder . '
+		                homepageDownloader("transmission", "' . $this->config['homepageDownloadRefresh'] . '");
+		                // End homepageOrdertransmission
+	                </script>
+				</div>
+				';
+		}
+	}
+	
+	public function getTransmissionHomepageQueue()
+	{
+		if (!$this->homepageItemPermissions($this->transmissionHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$digest = $this->qualifyURL($this->config['transmissionURL'], true);
+		$passwordInclude = ($this->config['transmissionUsername'] != '' && $this->config['transmissionPassword'] != '') ? $this->config['transmissionUsername'] . ':' . $this->decrypt($this->config['transmissionPassword']) . "@" : '';
+		$url = $digest['scheme'] . '://' . $passwordInclude . $digest['host'] . $digest['port'] . $digest['path'] . '/rpc';
+		try {
+			$options = ($this->localURL($this->config['transmissionURL'])) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->headers['x-transmission-session-id']) {
+				$headers = array(
+					'X-Transmission-Session-Id' => $response->headers['x-transmission-session-id'],
+					'Content-Type' => 'application/json'
+				);
+				$data = array(
+					'method' => 'torrent-get',
+					'arguments' => array(
+						'fields' => array(
+							"id", "name", "totalSize", "eta", "isFinished", "isStalled", "percentDone", "rateDownload", "status", "downloadDir", "errorString", "addedDate"
+						),
+					),
+					'tags' => ''
+				);
+				$response = Requests::post($url, $headers, json_encode($data), $options);
+				if ($response->success) {
+					$torrentList = json_decode($response->body, true)['arguments']['torrents'];
+					if ($this->config['transmissionHideSeeding'] || $this->config['transmissionHideCompleted']) {
+						$filter = array();
+						$torrents = array();
+						if ($this->config['transmissionHideSeeding']) {
+							array_push($filter, 6, 5);
+						}
+						if ($this->config['transmissionHideCompleted']) {
+							array_push($filter, 0);
+						}
+						foreach ($torrentList as $key => $value) {
+							if (!in_array($value['status'], $filter)) {
+								$torrents[] = $value;
+							}
+						}
+					} else {
+						$torrents = json_decode($response->body, true)['arguments']['torrents'];
+					}
+					usort($torrents, function ($a, $b) {
+						return $a["addedDate"] < $b["addedDate"];
+					});
+					$api['content']['queueItems'] = $torrents;
+					$api['content']['historyItems'] = false;
+				}
+			} else {
+				$this->writeLog('error', 'Transmission Connect Function - Error: Could not get session ID', 'SYSTEM');
+				$this->setAPIResponse('error', 'Transmission Connect Function - Error: Could not get session ID', 500);
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Transmission Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 311 - 0
api/homepage/unifi.php

@@ -0,0 +1,311 @@
+<?php
+
+trait UnifiHomepageItem
+{
+	public function unifiSettingsArray()
+	{
+		return array(
+			'name' => 'Unifi',
+			'enabled' => true,
+			'image' => 'plugins/images/tabs/ubnt.png',
+			'category' => 'Monitor',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageUnifiEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageUnifiEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageUnifiAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageUnifiAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'unifiURL',
+						'label' => 'URL',
+						'value' => $this->config['unifiURL'],
+						'help' => 'URL for Unifi',
+						'placeholder' => 'Unifi API URL'
+					),
+					array(
+						'type' => 'blank',
+						'label' => ''
+					),
+					array(
+						'type' => 'input',
+						'name' => 'unifiUsername',
+						'label' => 'Username',
+						'value' => $this->config['unifiUsername'],
+						'help' => 'Username is case-sensitive',
+					),
+					array(
+						'type' => 'password',
+						'name' => 'unifiPassword',
+						'label' => 'Password',
+						'value' => $this->config['unifiPassword']
+					),
+					array(
+						'type' => 'input',
+						'name' => 'unifiSiteName',
+						'label' => 'Site Name (Not for UnifiOS)',
+						'value' => $this->config['unifiSiteName'],
+						'help' => 'Site Name - not Site ID nor Site Description',
+					),
+					array(
+						'type' => 'button',
+						'label' => 'Grab Unifi Site (Not for UnifiOS)',
+						'icon' => 'fa fa-building',
+						'text' => 'Get Unifi Site',
+						'attr' => 'onclick="getUnifiSite()"'
+					),
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'select',
+						'name' => 'homepageUnifiRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageUnifiRefresh'],
+						'options' => $this->timeOptions()
+					),
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'unifi\')"'
+					),
+				)
+			)
+		);
+	}
+	
+	public function unifiHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageUnifiEnabled'
+				],
+				'auth' => [
+					'homepageUnifiAuth'
+				],
+				'not_empty' => [
+					'unifiURL',
+					'unifiUsername',
+					'unifiPassword'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderunifi()
+	{
+		if ($this->homepageItemPermissions($this->unifiHomepagePermissions('main'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Unifi...</h2></div>
+					<script>
+						// Unifi
+						homepageUnifi("' . $this->config['homepageHealthChecksRefresh'] . '");
+						// End Unifi
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	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);
+			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 = array('verify' => false, 'verifyname' => false, 'follow_redirects' => false);
+			$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;
+			} 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);
+			if ($response->success) {
+				$body = json_decode($response->body, true);
+				$this->setAPIResponse('success', null, 200, $body);
+				return $body;
+			} else {
+				$this->setAPIResponse('error', 'Unifi response error - Error Occurred', 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 (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();
+		$url = $this->qualifyURL($this->config['unifiURL']);
+		$options = array('verify' => false, 'verifyname' => false, '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;
+			}
+		} 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;
+		$this->setAPIResponse('success', 'API Connection succeeded', 200);
+		return true;
+	}
+	
+	public function getUnifiHomepageData()
+	{
+		if (!$this->homepageItemPermissions($this->unifiHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$api['content']['unifi'] = array();
+		$url = $this->qualifyURL($this->config['unifiURL']);
+		$options = array('verify' => false, 'verifyname' => false, '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;
+			}
+		} 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;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 233 - 0
api/homepage/weather.php

@@ -0,0 +1,233 @@
+<?php
+
+trait WeatherHomepageItem
+{
+	public function weatherSettingsArray()
+	{
+		return array(
+			'name' => 'Weather-Air',
+			'enabled' => true,
+			'image' => 'plugins/images/tabs/wind.png',
+			'category' => 'Monitor',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageWeatherAndAirEnabled',
+						'label' => 'Enable',
+						'value' => $this->config['homepageWeatherAndAirEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageWeatherAndAirAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $this->config['homepageWeatherAndAirAuth'],
+						'options' => $this->groupOptions
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'homepageWeatherAndAirLatitude',
+						'label' => 'Latitude',
+						'value' => $this->config['homepageWeatherAndAirLatitude'],
+						'help' => 'Please enter full latitude including minus if needed'
+					),
+					array(
+						'type' => 'input',
+						'name' => 'homepageWeatherAndAirLongitude',
+						'label' => 'Longitude',
+						'value' => $this->config['homepageWeatherAndAirLongitude'],
+						'help' => 'Please enter full longitude including minus if needed'
+					),
+					array(
+						'type' => 'blank',
+						'label' => ''
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-search',
+						'class' => 'pull-right',
+						'text' => 'Need Help With Coordinates?',
+						'attr' => 'onclick="showLookupCoordinatesModal()"'
+					),
+				),
+				'Options' => array(
+					array(
+						'type' => 'input',
+						'name' => 'homepageWeatherAndAirWeatherHeader',
+						'label' => 'Title',
+						'value' => $this->config['homepageWeatherAndAirWeatherHeader'],
+						'help' => 'Sets the title of this homepage module',
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'homepageWeatherAndAirWeatherHeaderToggle',
+						'label' => 'Toggle Title',
+						'value' => $this->config['homepageWeatherAndAirWeatherHeaderToggle'],
+						'help' => 'Shows/hides the title of this homepage module'
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'homepageWeatherAndAirWeatherEnabled',
+						'label' => 'Enable Weather',
+						'value' => $this->config['homepageWeatherAndAirWeatherEnabled'],
+						'help' => 'Toggles the view module for Weather'
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'homepageWeatherAndAirAirQualityEnabled',
+						'label' => 'Enable Air Quality',
+						'value' => $this->config['homepageWeatherAndAirAirQualityEnabled'],
+						'help' => 'Toggles the view module for Air Quality'
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'homepageWeatherAndAirPollenEnabled',
+						'label' => 'Enable Pollen',
+						'value' => $this->config['homepageWeatherAndAirPollenEnabled'],
+						'help' => 'Toggles the view module for Pollen'
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageWeatherAndAirUnits',
+						'label' => 'Unit of Measurement',
+						'value' => $this->config['homepageWeatherAndAirUnits'],
+						'options' => array(
+							array(
+								'name' => 'Imperial',
+								'value' => 'imperial'
+							),
+							array(
+								'name' => 'Metric',
+								'value' => 'metric'
+							)
+						)
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageWeatherAndAirRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $this->config['homepageWeatherAndAirRefresh'],
+						'options' => $this->timeOptions()
+					),
+				),
+			)
+		);
+	}
+	
+	public function weatherHomepagePermissions($key = null)
+	{
+		$permissions = [
+			'main' => [
+				'enabled' => [
+					'homepageWeatherAndAirEnabled'
+				],
+				'auth' => [
+					'homepageWeatherAndAirAuth'
+				],
+				'not_empty' => [
+					'homepageWeatherAndAirLatitude',
+					'homepageWeatherAndAirLongitude'
+				]
+			]
+		];
+		if (array_key_exists($key, $permissions)) {
+			return $permissions[$key];
+		} elseif ($key == 'all') {
+			return $permissions;
+		} else {
+			return [];
+		}
+	}
+	
+	public function homepageOrderWeatherAndAir()
+	{
+		if ($this->homepageItemPermissions($this->weatherHomepagePermissions('main'))) {
+			return '
+				<div id="' . __FUNCTION__ . '">
+					<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Weather...</h2></div>
+					<script>
+						// Weather And Air
+						homepageWeatherAndAir("' . $this->config['homepageWeatherAndAirRefresh'] . '");
+						// End Weather And Air
+					</script>
+				</div>
+				';
+		}
+	}
+	
+	public function searchCityForCoordinates($query)
+	{
+		try {
+			$query = $query ?? false;
+			if (!$query) {
+				$this->setAPIResponse('error', 'Query was not supplied', 422);
+				return false;
+			}
+			$url = $this->qualifyURL('https://api.mapbox.com/geocoding/v5/mapbox.places/' . urlencode($query) . '.json?access_token=pk.eyJ1IjoiY2F1c2VmeCIsImEiOiJjazhyeGxqeXgwMWd2M2ZydWQ4YmdjdGlzIn0.R50iYuMewh1CnUZ7sFPdHA&limit=5&fuzzyMatch=true');
+			$options = array('verify' => false);
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$this->setAPIResponse('success', null, 200, json_decode($response->body));
+				return json_decode($response->body);
+			}
+		} catch (Requests_Exception $e) {
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+	}
+	
+	public function getWeatherAndAirData()
+	{
+		if (!$this->homepageItemPermissions($this->weatherHomepagePermissions('main'), true)) {
+			return false;
+		}
+		$api['content'] = array(
+			'weather' => false,
+			'air' => false,
+			'pollen' => false
+		);
+		$apiURL = $this->qualifyURL('https://api.breezometer.com/');
+		$info = '&lat=' . $this->config['homepageWeatherAndAirLatitude'] . '&lon=' . $this->config['homepageWeatherAndAirLongitude'] . '&units=' . $this->config['homepageWeatherAndAirUnits'] . '&key=b7401295888443538a7ebe04719c8394';
+		try {
+			$headers = array();
+			$options = array();
+			if ($this->config['homepageWeatherAndAirWeatherEnabled']) {
+				$endpoint = '/weather/v1/forecast/hourly?hours=120&metadata=true';
+				$response = Requests::get($apiURL . $endpoint . $info, $headers, $options);
+				if ($response->success) {
+					$apiData = json_decode($response->body, true);
+					$api['content']['weather'] = ($apiData['error'] === null) ? $apiData : false;
+					unset($apiData);
+				}
+			}
+			if ($this->config['homepageWeatherAndAirAirQualityEnabled']) {
+				$endpoint = '/air-quality/v2/current-conditions?features=breezometer_aqi,local_aqi,health_recommendations,sources_and_effects,dominant_pollutant_concentrations,pollutants_concentrations,pollutants_aqi_information&metadata=true';
+				$response = Requests::get($apiURL . $endpoint . $info, $headers, $options);
+				if ($response->success) {
+					$apiData = json_decode($response->body, true);
+					$api['content']['air'] = ($apiData['error'] === null) ? $apiData : false;
+					unset($apiData);
+				}
+			}
+			if ($this->config['homepageWeatherAndAirPollenEnabled']) {
+				$endpoint = '/pollen/v2/forecast/daily?features=plants_information,types_information&days=1&metadata=true';
+				$response = Requests::get($apiURL . $endpoint . $info, $headers, $options);
+				if ($response->success) {
+					$apiData = json_decode($response->body, true);
+					$api['content']['pollen'] = ($apiData['error'] === null) ? $apiData : false;
+					unset($apiData);
+				}
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Weather And Air Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->setAPIResponse('error', $e->getMessage(), 500);
+			return false;
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		$this->setAPIResponse('success', null, 200, $api);
+		return $api;
+	}
+}

+ 7 - 1906
api/index.php

@@ -1,1914 +1,15 @@
 <?php
-/**
- * @apiDefine       UserNotAuthorizedError
- *
- * @apiError        UserNotAuthorized The user is not authorized or Token not valid
- *
- * @apiErrorExample Error-Response:
- *      HTTP/1.1 401 Not Authorized
- *      {
- *          "status": "error",
- *          "statusText": "API/Token invalid or not set",
- *          "data": null
- *      }
- */
-/**
- * @apiDefine         DataBooleanSuccess
- * @apiSuccess {Boolean} data Output Boolean.
- * @apiSuccessExample Success-Response:
- *      HTTP/1.1 200 OK
- *      {
- *          "status": "success",
- *          "statusText": "success",
- *          "data": true
- *      }
- *
- */
-/**
- * @apiDefine         DataJSONSuccess
- * @apiSuccess {JSON} data Output JSON.
- * @apiSuccessExample Success-Response:
- *      HTTP/1.1 200 OK
- *      {
- *          "status": "success",
- *          "statusText": "success",
- *          "data": { **JSON** }
- *      }
- *
- */
-/**
- * @apiDefine         DataHTMLSuccess
- * @apiSuccess {String} data Output of Page.
- *
- * @apiSuccessExample Success-Response:
- *     HTTP/1.1 200 OK
- *     {
- *       "status": "success",
- *       "statusText": "success",
- *       "data": "<html>html encoded elements</html>"
- *     }
- *
- */
-/**
- * @apiDefine admin Admin or API Key Access Only
- * Only the Admin/Co-Admin and API Key have access to this endpoint
- */
-//include functions
-require_once 'functions.php';
-//Set result array
-$result = array();
-//Get request method
-$method = $_SERVER['REQUEST_METHOD'];
-$pretty = isset($_GET['pretty']) ? true : false;
 reset($_GET);
 $function = (key($_GET) ? str_replace("/", "_", key($_GET)) : false);
-//Exit if $function is blank
-if ($function === false) {
-	$result['status'] = "error";
-	$result['statusText'] = "No API Path Supplied";
-	exit(json_encode($result));
-}
-$approvedFunctionsBypass = array(
-	'v1_upgrade',
-	'v1_update',
-	'v1_force',
-	'v1_auth',
-	'v1_wizard_config',
-	'v1_login',
-	'v1_wizard_path',
-	'v1_login_api'
-);
-if (!in_array($function, $approvedFunctionsBypass)) {
-	if (isApprovedRequest($method) === false) {
-		$result['status'] = "error";
-		$result['statusText'] = "Not Authorized";
-		http_response_code(401);
-		writeLog('success', 'Killed Attack From [' . (isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : 'No Referer') . ']', $GLOBALS['organizrUser']['username']);
-		exit(json_encode($result));
-	}
-}
-$result['request'] = key($_GET);
-$result['params'] = $_POST;
-//Custom Page Check
-if (strpos($function, 'v1_custom_page_') !== false) {
-	$endpoint = explode('v1_custom_page_', $function)[1];
-	$function = 'v1_custom_page';
-}
 switch ($function) {
-	case 'v1_settings_page':
-		switch ($method) {
-			/**
-			 * @api               {get} v1/settings/page Get Admin Settings
-			 * @apiVersion        1.0.0
-			 * @apiName           GetSettingsPage
-			 * @apiGroup          Pages
-			 * @apiUse            DataBooleanSuccess
-			 * @apiUse            UserNotAuthorizedError
-			 */
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = $pageSettings;
-					writeLog('success', 'Admin Function -  Accessed Settings Page', $GLOBALS['organizrUser']['username']);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-					writeLog('error', 'Admin Function -  Tried to access Settings Page', $GLOBALS['organizrUser']['username']);
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_homepage_page':
-		switch ($method) {
-			/**
-			 * @api               {get} v1/homepage/page Get Homepage
-			 * @apiVersion        1.0.0
-			 * @apiName           GetHomepagePage
-			 * @apiGroup          Pages
-			 * @apiUse            DataHTMLSuccess
-			 * @apiUse            UserNotAuthorizedError
-			 */
-			case 'GET':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = $pageHomepage;
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_settings_plugins':
-		switch ($method) {
-			/**
-			 * @api               {get} v1/settings/plugins Get Plugins
-			 * @apiVersion        1.0.0
-			 * @apiName           GetPluginsPage
-			 * @apiGroup          Pages
-			 * @apiUse            DataHTMLSuccess
-			 * @apiUse            UserNotAuthorizedError
-			 */
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = $pageSettingsPlugins;
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_settings_tab_editor_homepage':
-		switch ($method) {
-			/**
-			 * @api               {get} v1/settings/tab/editor/homepage Get Homepage Settings
-			 * @apiVersion        1.0.0
-			 * @apiName           GetSettingsTabEditorHomepagePage
-			 * @apiGroup          Pages
-			 * @apiUse            DataHTMLSuccess
-			 * @apiUse            UserNotAuthorizedError
-			 */
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = $pageSettingsTabEditorHomepage;
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_settings_tab_editor_homepage_order':
-		switch ($method) {
-			/**
-			 * @api               {get} v1/settings/tab/editor/homepage Get Homepage Order
-			 * @apiVersion        1.0.0
-			 * @apiName           GetSettingsTabEditorHomepageOrderPage
-			 * @apiGroup          Pages
-			 * @apiUse            DataHTMLSuccess
-			 * @apiUse            UserNotAuthorizedError
-			 */
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = $pageSettingsTabEditorHomepageOrder;
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_settings_homepage_list':
-		switch ($method) {
-			/**
-			 * @api               {get} v1/settings/homepage/list Get Homepage Settings
-			 * @apiVersion        1.0.0
-			 * @apiName           GetHomepageSettigns
-			 * @apiGroup          Homepage
-			 * @apiSuccess {String} data Output of all Homepage Settings.
-			 * @apiSuccessExample Success-Response:
-			 *      HTTP/1.1 200 OK
-			 *      {
-			 *          "status": "success",
-			 *          "statusText": "success",
-			 *          "data": [{
-			 *              "name": "HealthChecks",
-			 *              "enabled": true,
-			 *              "image": "plugins\/images\/tabs\/healthchecks.png",
-			 *              "category": "Monitor",
-			 *              "settings": {
-			 *                  "Enable": [
-			 *                      {
-			 *                          "type": "switch",
-			 *                          "name": "homepageHealthChecksEnabled",
-			 *                          "label": "Enable",
-			 *                          "value": true
-			 *                      }, {
-			 *                          "type": "select",
-			 *                          "name": "homepageHealthChecksAuth",
-			 *                          "label": "Minimum Authentication",
-			 *                          "value": "1",
-			 *                          "options": [
-			 *                              {
-			 *                                  "name": "Admin",
-			 *                                  "value": 0
-			 *                              }, {
-			 *                                  "name": "Co-Admin",
-			 *                                  "value": 1
-			 *                              }, {
-			 *                                  "name": "Super User",
-			 *                                  "value": 2
-			 *                              }, {
-			 *                                  "name": "Power User",
-			 *                                  "value": 3
-			 *                              }, {
-			 *                                  "name": "User",
-			 *                                  "value": 4
-			 *                              }, {
-			 *                                  "name": "temp again",
-			 *                                  "value": 5
-			 *                              }, {
-			 *                                  "name": "GuestAccts",
-			 *                                  "value": 999
-			 *                              }
-			 *                          ]
-			 *                      }
-			 *                  ],
-			 *              "Connection": [
-			 *                  {
-			 *                      "type": "input",
-			 *                      "name": "healthChecksURL",
-			 *                       "label": "URL",
-			 *                      "value": "https://healthchecks.io/api/v1/checks/",
-			 *                      "help": "URL for HealthChecks API",
-			 *                      "placeholder": "HealthChecks API URL"
-			 *                  }, {
-			 *                      "type": "password-alt",
-			 *                      "name": "healthChecksToken",
-			 *                      "label": "Token",
-			 *                      "value": "TOKENHERE"
-			 *                  }
-			 *              ],
-			 *              "Misc Options": [
-			 *                  {
-			 *                      "type": "input",
-			 *                      "name": "healthChecksTags",
-			 *                      "label": "Tags",
-			 *                      "value": "",
-			 *                      "help": "Pull only checks with this tag - Blank for all",
-			 *                      "placeholder": "Multiple tags using CSV - tag1,tag2"
-			 *                  }, {
-			 *                      "type": "select",
-			 *                      "name": "homepageHealthChecksRefresh",
-			 *                      "label": "Refresh Seconds",
-			 *                      "value": "3600000",
-			 *                      "options": [
-			 *                          {
-			 *                              "name": "5",
-			 *                              "value": "5000"
-			 *                          }, {
-			 *                              "name": "10",
-			 *                              "value": "10000"
-			 *                          }, {
-			 *                              "name": "15",
-			 *                              "value": "15000"
-			 *                          }, {
-			 *                              "name": "30",
-			 *                              "value": "30000"
-			 *                          }, {
-			 *                              "name": "60 [1 Minute]",
-			 *                              "value": "60000"
-			 *                          }, {
-			 *                              "name": "300 [5 Minutes]",
-			 *                              "value": "300000"
-			 *                          }, {
-			 *                              "name": "600 [10 Minutes]",
-			 *                              "value": "600000"
-			 *                          }, {
-			 *                              "name": "900 [15 Minutes]",
-			 *                              "value": "900000"
-			 *                          }, {
-			 *                              "name": "1800 [30 Minutes]",
-			 *                              "value": "1800000"
-			 *                          }, {
-			 *                              "name": "3600 [1 Hour]",
-			 *                              "value": "3600000"
-			 *                          }
-			 *                      ]
-			 *                  }
-			 *              ]
-			 *          }]
-			 *      }
-			 * @apiUse            UserNotAuthorizedError
-			 */
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = getHomepageList();
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_settings_plugins_list':
-		/**
-		 * @api               {get} v1/settings/plugins/list Get List of Plugins
-		 * @apiVersion        1.0.0
-		 * @apiName           GetPlugins
-		 * @apiGroup          Plugins
-		 * @apiSuccess {String} data Output plugins list.
-		 * @apiSuccessExample Success-Response:
-		 *     HTTP/1.1 200 OK
-		 *     {
-		 *       "status": "success",
-		 *       "statusText": "success",
-		 *       "data": {
-		 *         "chat": {
-		 *           "name": "Chat",
-		 *           "author": "CauseFX",
-		 *           "category": "Utilities",
-		 *           "link": "",
-		 *           "license": "personal,business",
-		 *           "idPrefix": "CHAT",
-		 *           "configPrefix": "CHAT",
-		 *           "version": "1.0.0",
-		 *           "image": "plugins/images/chat.png",
-		 *           "settings": true,
-		 *           "homepage": false,
-		 *           "enabled": true
-		 *         }
-		 *       }
-		 *     }
-		 * @apiUse            UserNotAuthorizedError
-		 */
-		/**
-		 * @api               {post} v1/settings/plugins/list Toggle Plugin
-		 * @apiVersion        1.0.0
-		 * @apiName           TogglePlugin
-		 * @apiGroup          Plugins
-		 * @apiParam {Object} data         nested data object.
-		 * @apiParam {String} data[action] enable/disable.
-		 * @apiParam {String} data[name]    Name of Plugin.
-		 * @apiParam {String} data[configName]   configName i.e. CHAT-enabled.
-		 * @apiUse            DataBooleanSuccess
-		 * @apiUse            UserNotAuthorizedError
-		 */
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = getPlugins();
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = editPlugins($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_settings_settings_logs':
-		/**
-		 * @api               {get} v1/settings/settings/logs Get Logs
-		 * @apiVersion        1.0.0
-		 * @apiName           GetLogsPage
-		 * @apiGroup          Pages
-		 * @apiUse            DataHTMLSuccess
-		 * @apiUse            UserNotAuthorizedError
-		 */
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = $pageSettingsSettingsLogs;
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_settings_settings_sso':
-		/**
-		 * @api               {get} v1/settings/settings/sso Get SSO
-		 * @apiVersion        1.0.0
-		 * @apiName           GetSSOPage
-		 * @apiGroup          Pages
-		 * @apiUse            DataHTMLSuccess
-		 * @apiUse            UserNotAuthorizedError
-		 */
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = $pageSettingsSettingsSSO;
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_settings_settings_main':
-		/**
-		 * @api               {get} v1/settings/settings/main Get Settings Main
-		 * @apiVersion        1.0.0
-		 * @apiName           GetSettingsMainPage
-		 * @apiGroup          Pages
-		 * @apiUse            DataHTMLSuccess
-		 * @apiUse            UserNotAuthorizedError
-		 */
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = $pageSettingsSettingsMain;
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_settings_customize_appearance':
-		/**
-		 * @api               {get} v1/settings/customize/appearance Get Customize Appearance
-		 * @apiVersion        1.0.0
-		 * @apiName           GetCustomizePage
-		 * @apiGroup          Pages
-		 * @apiUse            DataHTMLSuccess
-		 * @apiUse            UserNotAuthorizedError
-		 */
-		/**
-		 * @api               {post} v1/settings/customize/appearance Edit Customize Appearance
-		 * @apiVersion        1.0.0
-		 * @apiName           PostCustomizePage
-		 * @apiGroup          Appearance
-		 * @apiParam {Object} data         nested data object.
-		 * @apiParam {String} data[action] editCustomizeAppearance.
-		 * @apiParam {String} data[name]    Name.
-		 * @apiParam {String} data[value]   Value.
-		 * @apiUse            DataBooleanSuccess
-		 * @apiUse            UserNotAuthorizedError
-		 */
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = $pageSettingsCustomizeAppearance;
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = editAppearance($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_remove_file':
-		/**
-		 * @api               {post} v1/remove/file Remove File
-		 * @apiVersion        1.0.0
-		 * @apiName           PostRemoveFile
-		 * @apiGroup          Files
-		 * @apiParam {Object} data         nested data object.
-		 * @apiParam {String} data[path] File Path.
-		 * @apiParam {String} data[name]    File Name.
-		 * @apiUse            DataBooleanSuccess
-		 * @apiUse            UserNotAuthorizedError
-		 */
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = removeFile($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_update_config':
-		/**
-		 * @api               {post} v1/update/config Update Config Item
-		 * @apiVersion        1.0.0
-		 * @apiName           PostUpdateConfig
-		 * @apiGroup          Config
-		 * @apiParam {Object} data         nested data object.
-		 * @apiParam {String} data[type] input|select|switch|password.
-		 * @apiParam {String} data[name]    Name.
-		 * @apiParam {String} data[value]   Value.
-		 * @apiUse            DataBooleanSuccess
-		 * @apiUse            UserNotAuthorizedError
-		 */
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = updateConfigItem($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_update_config_multiple':
-		/**
-		 * @api               {post} v1/update/config/multiple Update Multiple Config Items
-		 * @apiVersion        1.0.0
-		 * @apiName           PostUpdateConfigMultiple
-		 * @apiGroup          Config
-		 * @apiPermission     admin
-		 * @apiParam  {Object} data[payload]         nested payload object.
-		 * @apiParam  {String}   data.:keyName     Value of Name defined from key.
-		 * @apiParamExample {json} Request-Example:
-		 *      {
-		 *          "data": {
-		 *              "payload": {
-		 *                  "title": "Organizr V2",
-		 *                  "logo": "plugins/images/organizr/logo-wide.png"
-		 *              }
-		 *          }
-		 *     }
-		 * @apiUse            DataBooleanSuccess
-		 * @apiUse            UserNotAuthorizedError
-		 */
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = updateConfigMultiple($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_update_config_multiple_form':
-		/**
-		 * @api               {post} v1/update/config/multiple/form Update Multiple Config Items Form
-		 * @apiVersion        1.0.0
-		 * @apiName           PostUpdateConfigMultipleForm
-		 * @apiGroup          Config
-		 * @apiPermission     admin
-		 * @apiParam  {Object} data[payload]         nested payload object.
-		 * @apiParam  {Object}   data.:keyName     Config ID/Key.
-		 * @apiParam  {String}   data.:keyName.name     Config ID/Key.
-		 * @apiParam  {String}   data.:keyName.value     Config Value.
-		 * @apiParam  {String}   data.:keyName.type     Config Type input|select|switch|password.
-		 * @apiParamExample {json} Request-Example:
-		 *      {
-		 *          "data": {
-		 *              "payload": {
-		 *                  "title": {
-		 *                      "name": "title",
-		 *                      "value": "Organizr V2",
-		 *                      "type": "input"
-		 *                  },
-		 *                  "logo": {
-		 *                      "name": "logo",
-		 *                      "value": "plugins/images/organizr/logo-wide.png",
-		 *                      "type": "input"
-		 *                  }
-		 *              }
-		 *          }
-		 *     }
-		 * @apiUse            DataBooleanSuccess
-		 * @apiUse            UserNotAuthorizedError
-		 */
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = updateConfigMultipleForm($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_homepage_connect':
-		/**
-		 * @api               {post} v1/homepage/connect Homepage Item Connect
-		 * @apiVersion        1.0.0
-		 * @apiName           PostHomepageItemConnect
-		 * @apiGroup          Homepage
-		 * @apiPermission     admin
-		 * @apiParam  {Object} data        payload object.
-		 * @apiParam  {Object}   data[action]     Homepage Item i.e. getPlexStreams|getPlexRecent.
-		 * @apiParamExample {json} Request-Example:
-		 *      {
-		 *          "data": {
-		 *              "action": "getPlexStreams"
-		 *          }
-		 *     }
-		 * @apiUse            DataJSONSuccess
-		 * @apiUse            UserNotAuthorizedError
-		 */
-		switch ($method) {
-			case 'POST':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = homepageConnect($_POST);
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_ping_list':
-		/**
-		 * @api               {post} v1/ping/list Homepage Item Connect
-		 * @apiVersion        1.0.0
-		 * @apiName           PostPingList
-		 * @apiGroup          Ping
-		 * @apiParam  {Object} data        payload object.
-		 * @apiParam  {Object[]}   data[pingList]     List of ip/hostname and ports [Optional String of hostname:port]
-		 * @apiParamExample {json} Object
-		 *      {
-		 *          "data": {
-		 *              "pingList": ["docker.home.lab:3579", "docker.home.lab:8181"]
-		 *          }
-		 *     }
-		 * @apiParamExample {json} String
-		 *      {
-		 *          "data": {
-		 *              "pingList": ["docker.home.lab:3579", "docker.home.lab:8181"]
-		 *          }
-		 *     }
-		 * @apiSuccess {String} data Output ping results and response times.
-		 * @apiSuccessExample Success-Response:
-		 *      HTTP/1.1 200 OK
-		 *      {
-		 *          "status": "success",
-		 *          "statusText": "success",
-		 *          "data":{
-		 *              "docker.home.lab:3579":10.77,
-		 *              "docker.home.lab:8181":0.66
-		 *          }
-		 *     }
-		 * @apiUse            UserNotAuthorizedError
-		 */
-		switch ($method) {
-			case 'POST':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = ping($_POST['data']['pingList']);
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_test_api_connection':
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = testAPIConnection($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_settings_tab_editor_tabs':
-		/**
-		 * @api               {get} v1/settings/tab/editor/tabs Get Tab Editor Tabs
-		 * @apiVersion        1.0.0
-		 * @apiName           GetTabEditorTabsPage
-		 * @apiGroup          Pages
-		 * @apiUse            DataHTMLSuccess
-		 * @apiUse            UserNotAuthorizedError
-		 */
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = $pageSettingsTabEditorTabs;
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = editTabs($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_settings_tab_editor_categories':
-		/**
-		 * @api               {get} v1/settings/tab/editor/categories Get Tab Editor Categories
-		 * @apiVersion        1.0.0
-		 * @apiName           GetTabEditorCategoriesPage
-		 * @apiGroup          Pages
-		 * @apiUse            DataHTMLSuccess
-		 * @apiUse            UserNotAuthorizedError
-		 */
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = $pageSettingsTabEditorCategories;
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = editCategories($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_settings_user_manage_users':
-		/**
-		 * @api               {get} v1/settings/user/manage/users Get Manage Users
-		 * @apiVersion        1.0.0
-		 * @apiName           GetManageUsersPage
-		 * @apiGroup          Pages
-		 * @apiUse            DataHTMLSuccess
-		 * @apiUse            UserNotAuthorizedError
-		 */
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = $pageSettingsUserManageUsers;
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = adminEditUser($_POST);
-				} elseif (qualifyRequest(998)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = editUser($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_manage_user':
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(998)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = editUser($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_settings_user_manage_groups':
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = $pageSettingsUserManageGroups;
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = adminEditGroup($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_settings_image_manager_view':
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = $pageSettingsImageManager;
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = editImages();
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_wizard_page':
-		switch ($method) {
-			case 'GET':
-				if (!file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = $pageWizard;
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'Wizard has already been run';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_dependencies_page':
-		switch ($method) {
-			case 'GET':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = $pageDependencies;
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_wizard_config':
-		switch ($method) {
-			case 'POST':
-				if (!file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = wizardConfig($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'Wizard has already been run';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_wizard_path':
-		switch ($method) {
-			case 'POST':
-				if (!file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = wizardPath($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'Wizard has already been run';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_login':
-		switch ($method) {
-			case 'POST':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = login($_POST);
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_login_api':
-		switch ($method) {
-			case 'POST':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = apiLogin();
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_register':
-		switch ($method) {
-			case 'POST':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = register($_POST);
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_recover':
-		switch ($method) {
-			case 'POST':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = recover($_POST);
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_unlock':
-		switch ($method) {
-			case 'POST':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = unlock($_POST);
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_lock':
-		switch ($method) {
-			case 'POST':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = lock();
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_test_iframe':
-		switch ($method) {
-			case 'POST':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = frameTest($_POST['data']['url']);
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_upgrade':
-	case 'v1_update':
-	case 'v1_force':
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = upgradeInstall($_POST['data']['branch'], $_POST['data']['stage']);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_login_page':
-		switch ($method) {
-			case 'GET':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = $pageLogin;
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_lockscreen':
-		switch ($method) {
-			case 'GET':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = $pageLockScreen;
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_login_log':
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = getLog('loginLog');
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_organizr_log':
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = getLog('org');
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_user_list':
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = allUsers();
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_tab_list':
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = allTabs();
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_image_list':
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = getImages();
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_customize_appearance':
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = getCustomizeAppearance();
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_sso':
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = getSSO();
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_settings_main':
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = getSettingsMain();
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_plugin_install':
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = installPlugin($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_plugin_remove':
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = removePlugin($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_theme_install':
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = installTheme($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_theme_remove':
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = removeTheme($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_user_edit':
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = adminEditUser($_POST);
-				} elseif (qualifyRequest(998)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = editUser($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_2fa_create':
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(998)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = create2FA($_POST['data']['type']);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_2fa_save':
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(998)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = save2FA($_POST['data']['secret'], $_POST['data']['type']);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_2fa_verify':
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(998)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = verify2FA($_POST['data']['secret'], $_POST['data']['code'], $_POST['data']['type']);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_2fa_remove':
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(998)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = remove2FA();
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_logout':
-		switch ($method) {
-			case 'GET':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = logout();
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_launch_organizr':
-		switch ($method) {
-			case 'GET':
-				$pluginSearch = '-enabled';
-				$pluginInclude = '-include';
-				$status = array();
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$status['status'] = organizrStatus();
-				$result['appearance'] = loadAppearance();
-				$status['user'] = $GLOBALS['organizrUser'];
-				$status['categories'] = loadTabs('categories');
-				$status['tabs'] = loadTabs('tabs');
-				$status['plugins'] = array_filter($GLOBALS, function ($k) use ($pluginSearch) {
-					return stripos($k, $pluginSearch) !== false;
-				}, ARRAY_FILTER_USE_KEY);
-				$status['plugins']['includes'] = array_filter($GLOBALS, function ($k) use ($pluginInclude) {
-					return stripos($k, $pluginInclude) !== false;
-				}, ARRAY_FILTER_USE_KEY);
-				$result['data'] = $status;
-				$result['branch'] = $GLOBALS['branch'];
-				$result['theme'] = $GLOBALS['theme'];
-				$result['style'] = $GLOBALS['style'];
-				$result['version'] = $GLOBALS['installedVersion'];
-				$result['sso'] = array(
-					'myPlexAccessToken' => isset($_COOKIE['mpt']) ? $_COOKIE['mpt'] : false,
-					'id_token' => isset($_COOKIE['Auth']) ? $_COOKIE['Auth'] : false
-				);
-				$result['settings'] = organizrSpecialSettings();
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
 	case 'v1_auth':
-		switch ($method) {
-			case 'GET':
-				auth();
-				break;
-			default:
-				//exit(http_response_code(401));
-				auth();
-				break;
-		}
-		break;
-	case 'v1_plugin':
-		switch ($method) {
-			case 'POST':
-			case 'GET':
-				// Include all plugin api Calls
-				foreach (glob(__DIR__ . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'api' . DIRECTORY_SEPARATOR . "*.php") as $filename) {
-					require_once $filename;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_image':
-		switch ($method) {
-			case 'GET':
-				getImage();
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_downloader':
-		switch ($method) {
-			case 'POST':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = downloader($_POST);
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_import_users':
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = importUsersType($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_ombi':
-		switch ($method) {
-			case 'POST':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = ombiAPI($_POST);
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_plex_join':
-		switch ($method) {
-			case 'POST':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = plexJoinAPI($_POST);
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_emby_join':
-		switch ($method) {
-			case 'POST':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = embyJoinAPI($_POST);
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_token_revoke':
-		switch ($method) {
-			case 'POST':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = revokeToken($_POST);
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_token_validate':
-		switch ($method) {
-			case 'GET':
-				$token = $_GET['token'] ?? false;
-				break;
-			case 'POST':
-				$token = $_POST['token'] ?? false;
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		$token = validateToken($token);
-		if ($token) {
-			$result['status'] = 'success';
-			$result['statusText'] = 'success';
-			$result['data'] = $token;
-		} else {
-			$result['status'] = 'error';
-			$result['statusText'] = 'Token not validated or empty';
-		}
-		break;
-	case 'v1_update_db_manual':
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = updateDB($GLOBALS['installedVersion']);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_version':
-		switch ($method) {
-			case 'GET':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = $GLOBALS['installedVersion'];
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_ping':
-		switch ($method) {
-			case 'GET':
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = 'pong';
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_docker_update':
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = dockerUpdate();
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_windows_update':
-		switch ($method) {
-			case 'GET':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = windowsUpdate();
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_custom_page':
-		switch ($method) {
-			case 'GET':
-				$customPage = 'customPage' . ucwords($endpoint);
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = $$customPage;
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_youtube_search':
-		switch ($method) {
-			case 'GET':
-				$query = isset($_GET['q']) ? $_GET['q'] : false;
-				$result['status'] = isset($_GET['q']) ? 'success' : 'error';
-				$result['statusText'] = isset($_GET['q']) ? 'success' : 'missing query';
-				$result['data'] = youtubeSearch($query);
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_scrape':
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(998)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = scrapePage($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
-	case 'v1_coordinates_search':
-		switch ($method) {
-			case 'POST':
-				if (qualifyRequest(1)) {
-					$result['status'] = 'success';
-					$result['statusText'] = 'success';
-					$result['data'] = searchCityForCoordinates($_POST);
-				} else {
-					$result['status'] = 'error';
-					$result['statusText'] = 'API/Token invalid or not set';
-					$result['data'] = null;
-				}
-				break;
-			default:
-				$result['status'] = 'error';
-				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
-				break;
-		}
-		break;
+		$group = ($_GET['group']) ?? 0;
+		header('Location: v2/auth?group=' . $group);
+		exit;
 	default:
-		//No Function Available
-		$result['status'] = 'error';
-		$result['statusText'] = 'function requested is not defined';
+		// Forward everything to v2 api
+		$result['status'] = "error";
+		$result['statusText'] = "Please Use api/v2";
 		break;
 }
-//Set Default Result
-if (!$result) {
-	$result['status'] = "error";
-	$result['error'] = "An error has occurred";
-}
-$result['generationDate'] = $GLOBALS['currentTime'];
-$result['generationTime'] = formatSeconds(timeExecution());
-//Set HTTP Code
-if ($result['statusText'] == "API/Token invalid or not set") {
-	http_response_code(401);
-} else {
-	http_response_code(200);
-}
-//return JSON array
-if ($pretty) {
-	echo '<pre>' . safe_json_encode($result, JSON_PRETTY_PRINT) . '</pre>';
-} else {
-	exit(safe_json_encode($result, JSON_HEX_QUOT | JSON_HEX_TAG));
-}
+header('Location: v2/');

+ 39 - 10
api/pages/custom/index.html

@@ -1,14 +1,43 @@
-Place all custom page files here....
+Place all custom page files here in this directory....
 
-make sure to start page contents like so:
+Name file something like "custom_code_presentation.php" and make sure to start page contents like so:
 <pre>
 &lt;?php
-// This is a check to make sure organizr is setup already...
-if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
-    // All variables need to start with $customPage .... Make sure to change Simple to any word (First Letter needs to be capital)... i.e. $customPageNewish
-	// Also make sure to escape single quotes
-	$customPageSimple = '
-        &lt;h1&gt;THIS IS JUST A TEST&lt;/h1&gt;
-    ';
-}
+    /*
+     * Make sure to edit "name_here" with your page name - i.e. custom_code_presentation
+     * You will edit on both "$GLOBALS['organizrPages'][] = 'name_here';" and "function get_page_name_here($Organizr)"
+     */
+    $GLOBALS['organizrPages'][] = 'name_here';
+    function get_page_name_here($Organizr)
+    {
+        if (!$Organizr) {
+            $Organizr = new Organizr();
+        }
+        /*
+         * Take this out if you dont care if DB has been created
+         */
+        if ((!$Organizr->hasDB())) {
+            return false;
+        }
+        /*
+         * Take this out if you dont want to be for admin only
+         */
+        if (!$Organizr->qualifyRequest(1, true)) {
+            return false;
+        }
+        return '
+            &#x3C;script&#x3E;
+                // Custom JS here
+            &#x3C;/script&#x3E;
+            &#x3C;div class=&#x22;panel bg-org panel-info&#x22;&#x3E;
+                &#x3C;div class=&#x22;panel-heading&#x22;&#x3E;
+                    &#x3C;span lang=&#x22;en&#x22;&#x3E;Template&#x3C;/span&#x3E;
+                &#x3C;/div&#x3E;
+                &#x3C;div class=&#x22;panel-wrapper collapse in&#x22; aria-expanded=&#x22;true&#x22;&#x3E;
+                    &#x3C;div class=&#x22;panel-body bg-org&#x22;&#x3E;
+                    &#x3C;/div&#x3E;
+                &#x3C;/div&#x3E;
+            &#x3C;/div&#x3E;
+        ';
+    }
 </pre>

+ 9 - 3
api/pages/dependencies.php

@@ -1,6 +1,11 @@
 <?php
-
-$pageDependencies = '
+$GLOBALS['organizrPages'][] = 'dependencies';
+function get_page_dependencies($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	return '
 <script>
 </script>
 <div class="container-fluid">
@@ -49,7 +54,7 @@ $pageDependencies = '
                     <table class="table table-hover">
                         <tbody>
                             <tr>
-                                <td>'.dirname(__DIR__,2).'</td>
+                                <td>' . dirname(__DIR__, 2) . '</td>
                             </tr>
                             <tr>
                                 <td id="web-folder" lang="en">Loading...</td>
@@ -73,3 +78,4 @@ $pageDependencies = '
 </div>
 <!-- /.container-fluid -->
 ';
+}

+ 19 - 9
api/pages/homepage.php

@@ -1,6 +1,14 @@
 <?php
-if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
-	$pageHomepage = '
+$GLOBALS['organizrPages'][] = 'homepage';
+function get_page_homepage($Organizr = null)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	return '
 <script>
 !function($) {
     "use strict";
@@ -26,6 +34,7 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
 
         var $this = this;
         $this.$calendarObj = $this.$calendar.fullCalendar({
+        	locale: "' . $Organizr->config['calendarLocale'] . '",
         	customButtons: {
 			    filterCalendar: {
 			      text: \'Filter\',
@@ -41,9 +50,9 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
 			      }
 			    }
 			  },
-            defaultView: (activeInfo.mobile) ? "list" : "' . $GLOBALS['calendarDefault'] . '",
-            firstDay: "' . $GLOBALS['calendarFirstDay'] . '",
-            timeFormat: "' . $GLOBALS['calendarTimeFormat'] . '",
+            defaultView: (activeInfo.mobile) ? "list" : "' . $Organizr->config['calendarDefault'] . '",
+            firstDay: "' . $Organizr->config['calendarFirstDay'] . '",
+            timeFormat: "' . $Organizr->config['calendarTimeFormat'] . '",
             handleWindowResize: true,
             header: {
                left: "prev,next,today",
@@ -51,9 +60,9 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
                right: (activeInfo.mobile) ? "refreshCalendar,filterCalendar" : "refreshCalendar,filterCalendar,month,basicWeek,basicDay,list",
             },
             views: {
-               basicDay: { buttonText: window.lang.translate("Day"), eventLimit: ' . $GLOBALS['calendarLimit'] . ' },
-               basicWeek: { buttonText: window.lang.translate("Week"), eventLimit: ' . $GLOBALS['calendarLimit'] . ' },
-               month: { buttonText: window.lang.translate("Month"), eventLimit: ' . $GLOBALS['calendarLimit'] . ' },
+               basicDay: { buttonText: window.lang.translate("Day"), eventLimit: ' . $Organizr->config['calendarLimit'] . ' },
+               basicWeek: { buttonText: window.lang.translate("Week"), eventLimit: ' . $Organizr->config['calendarLimit'] . ' },
+               month: { buttonText: window.lang.translate("Month"), eventLimit: ' . $Organizr->config['calendarLimit'] . ' },
                list: { buttonText: window.lang.translate("List"), duration: {days: 15} },
             },
             timezone: "local",
@@ -87,9 +96,10 @@ function($) {
     "use strict";
     $.CalendarApp.init()
 }(window.jQuery);
+$(".homepage-loading-box").fadeOut(5000);
 </script>
 <div class="container-fluid p-t-30" id="homepage-items">
-    ' . buildHomepage() . '
+    ' . $Organizr->buildHomepage() . '
 </div>
 <div id="open-youtube" class="white-popup mfp-with-anim mfp-hide">
     <div class="col-md-8 col-md-offset-2 youtube-div">  </div>

+ 12 - 4
api/pages/lockscreen.php

@@ -1,6 +1,14 @@
 <?php
-if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
-	$pageLockScreen = '
+$GLOBALS['organizrPages'][] = 'lockscreen';
+function get_page_lockscreen($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	return '
 <script>
 </script>
 <section id="lockScreen" class="lock-screen" onkeydown="blockDev">
@@ -9,8 +17,8 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
       <form class="form-horizontal form-material" id="form-lockscreen" onsubmit="return false;">
         <div class="form-group">
           <div class="col-xs-12 text-center">
-            <div class="user-thumb text-center"> <img alt="thumbnail" class="img-circle" width="100" src="' . $GLOBALS['organizrUser']['image'] . '">
-              <h3>' . $GLOBALS['organizrUser']['username'] . '</h3>
+            <div class="user-thumb text-center"> <img alt="thumbnail" class="img-circle" width="100" src="' . $Organizr->user['image'] . '">
+              <h3>' . $Organizr->user['username'] . '</h3>
             </div>
           </div>
         </div>

+ 16 - 8
api/pages/login.php

@@ -1,9 +1,17 @@
 <?php
-if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
-	$hideOrganizrLogin = (checkoAuth()) ? 'collapse' : 'collapse in';
-	$hideOrganizrLoginHeader = (checkoAuthOnly()) ? 'hidden' : '';
-	$hideOrganizrLoginHeader2 = (checkoAuth()) ? '' : 'hidden';
-	$pageLogin = '
+$GLOBALS['organizrPages'][] = 'login';
+function get_page_login($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	$hideOrganizrLogin = ($Organizr->checkoAuth()) ? 'collapse' : 'collapse in';
+	$hideOrganizrLoginHeader = ($Organizr->checkoAuthOnly()) ? 'hidden' : '';
+	$hideOrganizrLoginHeader2 = ($Organizr->checkoAuth()) ? '' : 'hidden';
+	return '
 <script>
 if(activeInfo.settings.login.rememberMe){
 	$(\'#checkbox-login\').prop(\'checked\',true);
@@ -14,7 +22,7 @@ if(activeInfo.settings.login.rememberMe){
     <div class="white-box">
       <form class="form-horizontal" id="loginform" onsubmit="return false;">
       	<input id="login-attempts" class="form-control" name="loginAttempts" type="hidden">
-        <a href="javascript:void(0)" class="text-center db visible-xs" id="login-logo">' . logoOrText() . '</a>
+        <a href="javascript:void(0)" class="text-center db visible-xs" id="login-logo">' . $Organizr->logoOrText() . '</a>
         <div id="oAuth-div" class="form-group hidden">
           <div class="col-xs-12">
             <div class="panel panel-success animated tada">
@@ -80,7 +88,7 @@ if(activeInfo.settings.login.rememberMe){
 				          <div class="col-sm-12 text-center">
 				            <input id="oAuth-Input" class="form-control" name="oAuth" type="hidden">
 				            <input id="oAuthType-Input" class="form-control" name="oAuthType" type="hidden">
-				            ' . showLogin() . '
+				            ' . $Organizr->showLogin() . '
 				          </div>
 				        </div>
 	                </div>
@@ -88,7 +96,7 @@ if(activeInfo.settings.login.rememberMe){
 	        </div>
 	        <!-- END ORGANIZR LOGIN -->
         	<!-- PLEX OAUTH LOGIN -->
-	        ' . showoAuth() . '
+	        ' . $Organizr->showoAuth() . '
 	        <!-- END PLEX OAUTH LOGIN -->
         </div>
       </form>

+ 15 - 2
api/pages/settings-customize-appearance.php

@@ -1,5 +1,17 @@
 <?php
-$pageSettingsCustomizeAppearance = '
+$GLOBALS['organizrPages'][] = 'settings_customize_appearance';
+function get_page_settings_customize_appearance($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	return '
 <script>
 	buildCustomizeAppearance();
 </script>
@@ -11,8 +23,9 @@ $pageSettingsCustomizeAppearance = '
 	</div>
     <div class="panel-wrapper collapse in" aria-expanded="true">
         <div class="panel-body bg-org">
-            <form id="customize-appearance-form" class="addFormTick" onsbumit="return false;"></form>
+            <form id="customize-appearance-form" class="addFormTick" onsubmit="return false;"></form>
         </div>
     </div>
 </div>
 ';
+}

+ 18 - 3
api/pages/settings-image-manager.php

@@ -1,9 +1,21 @@
 <?php
-$pageSettingsImageManager = '
+$GLOBALS['organizrPages'][] = 'settings_image_manager';
+function get_page_settings_image_manager($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	return '
 <script>
 	buildImageManagerView();
     var myDropzone = new Dropzone("#new-image-form", {
-      url: "api/?v1/settings/image/manager/view",
+      url: "api/v2/image",
       headers:{ "formKey": local("g","formKey") },
       init: function() {
         this.on("complete", function(file) {
@@ -20,7 +32,9 @@ $pageSettingsImageManager = '
 	</div>
     <div class="panel-wrapper collapse in" aria-expanded="true">
         <div class="panel-body bg-org" >
-        <div class="row el-element-overlay m-b-40" id="settings-image-manager-list"></div>
+        	<div id="gallery-content">
+                <div id="gallery-content-center" class="settings-image-manager-list"></div>
+            </div>
         </div>
     </div>
 </div>
@@ -32,3 +46,4 @@ $pageSettingsImageManager = '
     <div class="clearfix"></div>
 </form>
 ';
+}

+ 13 - 2
api/pages/settings-plugins.php

@@ -1,6 +1,17 @@
 <?php
-if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
-	$pageSettingsPlugins = '
+$GLOBALS['organizrPages'][] = 'settings_plugins';
+function get_page_settings_plugins($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	return '
 <script>
 	buildPlugins();
 </script>

+ 55 - 0
api/pages/settings-settings-backup.php

@@ -0,0 +1,55 @@
+<?php
+$GLOBALS['organizrPages'][] = 'settings_settings_backup';
+function get_page_settings_settings_backup($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	return '
+    <script>
+		getOrganizrBackups();
+    </script>
+ 
+    <div class="white-box bg-org">
+		<div class="col-md-3 col-sm-4 col-xs-6 pull-right">
+			<button onclick="createOrganizrBackup()" class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right" type="button"><span class="btn-label"><i class="fa ti-export"></i></span><span lang="en">Create Backup</span></button>
+		</div>
+		<h3 class="box-title" lang="en">Backup Organizr</h3>
+		<div class="row sales-report">
+			<div class="col-md-6 col-sm-6 col-xs-6">
+				<h2 id="backup-total-files"><i class="fa fa-spin fa-spinner"></i></h2>
+				<p lang="en">Files</p>
+			</div>
+			<div class="col-md-6 col-sm-6 col-xs-6 ">
+				<h1 class="text-right text-info m-t-20" id="backup-total-size"><i class="fa fa-spin fa-spinner"></i></h1>
+			</div>
+		</div>
+		<div class="table-responsive">
+			<table class="table">
+				<thead>
+					<tr>
+						<th>#</th>
+						<th lang="en">Name</th>
+						<th lang="en">Version</th>
+						<th lang="en">Size</th>
+						<th lang="en">Date</th>
+						<th lang="en">Action</th>
+					</tr>
+				</thead>
+				<tbody id="backup-file-list">
+					<tr>
+						<td class="text-center" colspan="6"><i class="fa fa-spin fa-spinner"></i></td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+	</div>
+    <!-- /.container-fluid -->
+    ';
+}

+ 51 - 8
api/pages/settings-settings-logs.php

@@ -1,8 +1,30 @@
 <?php
-if(file_exists('config'.DIRECTORY_SEPARATOR.'config.php')){
-    $pageSettingsSettingsLogs = '
+$GLOBALS['organizrPages'][] = 'settings_settings_logs';
+function get_page_settings_settings_logs($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	return '
     <script>
     $(document).on("click", ".swapLog", function(e) {
+    	switch ($(this).attr(\'data-name\')){
+    	case \'loginLog\':
+    		loginLogTable.ajax.reload(null, false);
+    	break;
+    	case \'orgLog\':
+    		organizrLogTable.ajax.reload(null, false);
+    	break;
+    	default:
+    		//nada
+    		//loginLogTable
+    	}
         var log = $(this).attr(\'data-name\')+\'Div\';
         $(\'.logTable\').addClass(\'hidden\');
         $(\'.\'+log).addClass(\'show\').removeClass(\'hidden\');
@@ -11,8 +33,8 @@ if(file_exists('config'.DIRECTORY_SEPARATOR.'config.php')){
     });
     </script>
     <div class="btn-group m-b-20 pull-left">
-        <button type="button" class="btn btn-default btn-outline waves-effect bg-org swapLog active" data-name="loginLog" data-path="'.$GLOBALS['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="'.$GLOBALS['organizrLog'].'" lang="en">Organizr Log</button>
+        <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>
     <div class="clearfix"></div>
@@ -69,8 +91,19 @@ if(file_exists('config'.DIRECTORY_SEPARATOR.'config.php')){
     <!-- /.container-fluid -->
     <script>
     //$.fn.dataTable.moment(\'DD-MMM-Y HH:mm:ss\');
-    $("#loginLogTable").DataTable( {
-            "ajax": "api/?v1/login_log",
+    $.fn.dataTable.ext.errMode = \'none\';
+    var loginLogTable = $("#loginLogTable")
+    .on( \'error.dt\', function ( e, settings, techNote, message ) {
+        console.log( \'An error has been reported by DataTables: \', message );
+        loginLogTable.draw();
+    } )
+    .DataTable( {
+    		"ajax": {
+				"url": "api/v2/log/login",
+				"dataSrc": function ( json ) {
+					return json.response.data;
+				}
+			},
             "columns": [
                 { data: \'utc_date\',
                     render: function ( data, type, row ) {
@@ -98,8 +131,18 @@ if(file_exists('config'.DIRECTORY_SEPARATOR.'config.php')){
             ],
             "order": [[ 0, \'desc\' ]],
     } );
-    $("#organizrLogTable").DataTable( {
-            "ajax": "api/?v1/organizr_log",
+    var organizrLogTable = $("#organizrLogTable")
+    .on( \'error.dt\', function ( e, settings, techNote, message ) {
+        console.log( \'An error has been reported by DataTables: \', message );
+        organizrLogTable.draw();
+    } )
+    .DataTable( {
+            "ajax": {
+				"url": "api/v2/log/organizr",
+				"dataSrc": function ( json ) {
+					return json.response.data;
+				}
+			},
                 "columns": [
                 { data: \'utc_date\',
                     render: function ( data, type, row ) {

+ 15 - 2
api/pages/settings-settings-main.php

@@ -1,5 +1,17 @@
 <?php
-$pageSettingsSettingsMain = '
+$GLOBALS['organizrPages'][] = 'settings_settings_main';
+function get_page_settings_settings_main($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	return '
 <script>
 	buildSettingsMain();
 </script>
@@ -10,7 +22,7 @@ $pageSettingsSettingsMain = '
 	</div>
     <div class="panel-wrapper collapse in" aria-expanded="true">
         <div class="panel-body bg-org">
-            <form id="settings-main-form" class="addFormTick" onsbumit="return false;"></form>
+            <form id="settings-main-form" class="addFormTick" onsubmit="return false;"></form>
         </div>
     </div>
 </div>
@@ -46,3 +58,4 @@ $pageSettingsSettingsMain = '
     <div class="clearfix"></div>
 </form>
 ';
+}

+ 15 - 2
api/pages/settings-settings-sso.php

@@ -1,5 +1,17 @@
 <?php
-$pageSettingsSettingsSSO = '
+$GLOBALS['organizrPages'][] = 'settings_settings_sso';
+function get_page_settings_settings_sso($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	return '
 <script>
 	buildSSO();
 </script>
@@ -10,7 +22,7 @@ $pageSettingsSettingsSSO = '
 	</div>
     <div class="panel-wrapper collapse in" aria-expanded="true">
         <div class="panel-body bg-org">
-            <form id="sso-form" class="addFormTick" onsbumit="return false;"></form>
+            <form id="sso-form" class="addFormTick" onsubmit="return false;"></form>
         </div>
     </div>
 </div>
@@ -46,3 +58,4 @@ $pageSettingsSettingsSSO = '
     <div class="clearfix"></div>
 </form>
 ';
+}

+ 18 - 6
api/pages/settings-tab-editor-categories.php

@@ -1,13 +1,24 @@
 <?php
-
-$pageSettingsTabEditorCategories = '
+$GLOBALS['organizrPages'][] = 'settings_tab_editor_categories';
+function get_page_settings_tab_editor_categories($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	return '
 <script>
 buildCategoryEditor();
 $( \'#categoryEditorTable\' ).sortable({
     stop: function () {
         var inputs = $(\'input.order\');
         var nbElems = inputs.length;
-        $(\'input.order\').each(function(idx) {
+        inputs.each(function(idx) {
             $(this).val(idx + 1);
         });
         submitCategoryOrder();
@@ -32,7 +43,7 @@ $( \'#categoryEditorTable\' ).sortable({
                         <th lang="en" style="text-align:center">DELETE</th>
                     </tr>
                 </thead>
-                <tbody id="categoryEditorTable"></tbody>
+                <tbody id="categoryEditorTable"><td class="text-center" colspan="6"><i class="fa fa-spin fa-spinner"></i></td></tbody>
             </table>
         </form>
     </div>
@@ -42,7 +53,7 @@ $( \'#categoryEditorTable\' ).sortable({
     <fieldset style="border:0;">
         <div class="form-group">
             <label class="control-label" for="new-category-form-inputNameNew" lang="en">Category Name</label>
-            <input type="text" class="form-control" id="new-category-form-inputNameNew" name="name" required="" autofocus>
+            <input type="text" class="form-control" id="new-category-form-inputNameNew" name="category" required="" autofocus>
         </div>
         <div class="form-group">
             <label class="control-label" for="new-category-form-inputImageNew" lang="en">Category Image</label>
@@ -58,7 +69,7 @@ $( \'#categoryEditorTable\' ).sortable({
     <fieldset style="border:0;">
         <div class="form-group">
             <label class="control-label" for="edit-category-form-inputName" lang="en">Category Name</label>
-            <input type="text" class="form-control" id="edit-category-form-inputName" name="name" required="" autofocus>
+            <input type="text" class="form-control" id="edit-category-form-inputName" name="category" required="" autofocus>
         </div>
         <div class="form-group">
             <label class="control-label" for="edit-category-form-inputImage" lang="en">Category Image</label>
@@ -82,3 +93,4 @@ $( \'#categoryEditorTable\' ).sortable({
     <div class="clearfix"></div>
 </form>
 ';
+}

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

@@ -1,6 +1,17 @@
 <?php
-if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
-	$pageSettingsTabEditorHomepageOrder = '
+$GLOBALS['organizrPages'][] = 'settings_tab_editor_homepage_order';
+function get_page_settings_tab_editor_homepage_order($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	return '
 <script>
     $("#homepage-items-sort").sortable({
 	    placeholder:    "sort-placeholder col-md-3 col-xs-12 m-t-10 clearfix",
@@ -41,7 +52,7 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
 	</div>
     <div class="panel-wrapper collapse in" aria-expanded="true">
         <div class="panel-body bg-org" >
-        <div class="row el-element-overlay m-b-40" id="settings-homepage-order">' . buildHomepageSettings() . '</div>
+        <div class="row el-element-overlay m-b-40" id="settings-homepage-order">' . $Organizr->buildHomepageSettings() . '</div>
         </div>
     </div>
 </div>

+ 17 - 4
api/pages/settings-tab-editor-homepage.php

@@ -1,19 +1,32 @@
 <?php
-
-$pageSettingsTabEditorHomepage = '
+$GLOBALS['organizrPages'][] = 'settings_tab_editor_homepage';
+function get_page_settings_tab_editor_homepage($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	return '
 <script>
 	buildHomepage();
 </script>
 <div class="panel bg-org panel-info">
     <div class="panel-heading">
 		<span lang="en">Homepage Items</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> -->
 	</div>
     <div class="panel-wrapper collapse in" aria-expanded="true">
         <div class="panel-body bg-org" >
-        <div class="row el-element-overlay m-b-40" id="settings-homepage-list"></div>
+        	<div class="row el-element-overlay m-b-40" id="settings-homepage-list">
+        		<div class="text-center"><i class="fa fa-spin fa-spinner fa-3x"></i></div>
+			</div>
         </div>
     </div>
 </div>
 
 ';
+}

+ 89 - 37
api/pages/settings-tab-editor-tabs.php

@@ -1,44 +1,94 @@
 <?php
-if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
-	$pageSettingsTabEditorTabsPerformanceIcon = $GLOBALS['performanceDisableIconDropdown'] ? '' : '
-	allIcons().success(function(data) {
+$GLOBALS['organizrPages'][] = 'settings_tab_editor_tabs';
+function get_page_settings_tab_editor_tabs($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	$iconSelectors = '
+
 	    $(".tabIconIconList").select2({
-			data: data,
+	        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,
+			templateSelection: formatIcon
+		});
+		
+		$(".tabIconImageList").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
 		});
-	});
-	$(".tabIconImageList").select2({
-		templateResult: formatImage,
-		templateSelection: formatImage,
-	});
-	';
-	$pageSettingsTabEditorTabsPerformanceImage = $GLOBALS['performanceDisableImageDropdown'] ? '' : '
-	$(".tabIconImageList").select2({
-		templateResult: formatImage,
-		templateSelection: formatImage,
-	});
 	';
-	$pageSettingsTabEditorTabs = '
+	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 () {
 	        $(\'input.order\').each(function(idx) {
 	            $(this).val(idx + 1);
 	        });
 	        var newTabs = $( "#submit-tabs-form" ).serializeToJSON();
-	        submitTabOrder(newTabs);
+	        newTabsGlobal = newTabs;
+	        $(\'.saveTabOrderButton\').removeClass(\'hidden\');
+	        //submitTabOrder(newTabs);
 	    }
 	});
-	'.$pageSettingsTabEditorTabsPerformanceImage.$pageSettingsTabEditorTabsPerformanceIcon.'
-	
+	$( \'#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>
 	    </div>
 	    <div class="table-responsive">
 	        <form id="submit-tabs-form" onsubmit="return false;">
@@ -59,7 +109,9 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
 	                        <th lang="en" style="text-align:center">DELETE</th>
 	                    </tr>
 	                </thead>
-	                <tbody id="tabEditorTable"></tbody>
+	                <tbody id="tabEditorTable">
+	                	<td class="text-center" colspan="12"><i class="fa fa-spin fa-spinner"></i></td>
+					</tbody>
 	            </table>
 	        </form>
 	    </div>
@@ -77,24 +129,24 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
 	        </div>
 	        <div class="form-group">
 	            <label class="control-label" for="new-tab-form-inputNameNew" lang="en">Tab Name</label>
-	            <input type="text" class="form-control" id="new-tab-form-inputNameNew" name="tabName" required="" autofocus>
+	            <input type="text" class="form-control" id="new-tab-form-inputNameNew" name="name" required="" autofocus>
 	        </div>
 	        <div class="form-group">
 	            <label class="control-label" for="new-tab-form-inputURLNew" lang="en">Tab URL</label>
-	            <input type="text" class="form-control" id="new-tab-form-inputURLNew" name="tabURL"  required="">
+	            <input type="text" class="form-control" id="new-tab-form-inputURLNew" name="url"  required="">
 	        </div>
 	        <div class="form-group">
 	            <label class="control-label" for="new-tab-form-inputURLLocalNew" lang="en">Tab Local URL</label>
-	            <input type="text" class="form-control" id="new-tab-form-inputURLLocalNew" name="tabLocalURL">
+	            <input type="text" class="form-control" id="new-tab-form-inputURLLocalNew" name="url_local">
 	        </div>
 	        <div class="form-group">
 	            <label class="control-label" for="new-tab-form-inputPingURLNew" lang="en">Ping URL</label>
-	            <input type="text" class="form-control" id="new-tab-form-inputPingURLNew" name="pingURL"  placeholder="host/ip:port">
+	            <input type="text" class="form-control" id="new-tab-form-inputPingURLNew" name="ping_url"  placeholder="host/ip:port">
 	        </div>
 	        <div class="row">
 		        <div class="form-group col-lg-6">
 		            <label class="control-label" for="new-tab-form-inputTabActionTypeNew" lang="en">Tab Auto Action</label>
-		                <select class="form-control" id="new-tab-form-inputTabActionTypeNew" name="tabActionType">
+		                <select class="form-control" id="new-tab-form-inputTabActionTypeNew" name="timeout">
 		                    <option value="null">None</option>
 		                    <option value="1">Auto Close</option>
 		                    <option value="2">Auto Reload</option>
@@ -102,13 +154,13 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
 		        </div>
 		        <div class="form-group col-lg-6">
 		            <label class="control-label" for="new-tab-form-inputTabActionTimeNew" lang="en">Tab Auto Action Minutes</label>
-		                <input type="number" class="form-control" id="new-tab-form-inputTabActionTimeNew" name="tabActionTime"  placeholder="0">
+		                <input type="number" class="form-control" id="new-tab-form-inputTabActionTimeNew" name="timeout_ms"  placeholder="0">
 		        </div>
 		    </div>
 	        <div class="row">
 		        <div class="form-group col-lg-6">
 		            <label class="control-label" for="new-tab-form-chooseImage" lang="en">Choose Image</label>
-		            ' . imageSelect("new-tab-form") . '
+		            <select class="form-control tabIconImageList" id="new-tab-form-chooseImage" name="chooseImage"><option lang="en">Select or type Image</option></select>
 		        </div>
 		        <div class="form-group col-lg-6">
 		            <label class="control-label" for="new-tab-form-chooseIcon" lang="en">Choose Icon</label>
@@ -117,7 +169,7 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
 		    </div>
 	        <div class="form-group">
 	            <label class="control-label" for="new-tab-form-inputImageNew" lang="en">Tab Image</label>
-	            <input type="text" class="form-control" id="new-tab-form-inputImageNew" name="tabImage"  required="">
+	            <input type="text" class="form-control" id="new-tab-form-inputImageNew" name="image"  required="">
 	        </div>
 	    </fieldset>
 	    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light row b-none testTab" type="button"><span class="btn-label"><i class="fa fa-flask"></i></span><span lang="en">Test Tab</span></button>
@@ -139,24 +191,24 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
 	        </div>
 	        <div class="form-group">
 	            <label class="control-label" for="edit-tab-form-inputName" lang="en">Tab Name</label>
-	            <input type="text" class="form-control" id="edit-tab-form-inputName" name="tabName" required="" autofocus>
+	            <input type="text" class="form-control" id="edit-tab-form-inputName" name="name" required="" autofocus>
 	        </div>
 	        <div class="form-group">
 	            <label class="control-label" for="edit-tab-form-inputURL" lang="en">Tab URL</label>
-	            <input type="text" class="form-control" id="edit-tab-form-inputURL" name="tabURL"  required="">
+	            <input type="text" class="form-control" id="edit-tab-form-inputURL" name="url"  required="">
 	        </div>
 	        <div class="form-group">
 	            <label class="control-label" for="edit-tab-form-inputLocalURL" lang="en">Tab Local URL</label>
-	            <input type="text" class="form-control" id="edit-tab-form-inputLocalURL" name="tabLocalURL">
+	            <input type="text" class="form-control" id="edit-tab-form-inputLocalURL" name="url_local">
 	        </div>
 	        <div class="form-group">
 	            <label class="control-label" for="edit-tab-form-pingURL" lang="en">Ping URL</label>
-	            <input type="text" class="form-control" id="edit-tab-form-pingURL" name="pingURL" placeholder="host/ip:port">
+	            <input type="text" class="form-control" id="edit-tab-form-pingURL" name="ping_url" placeholder="host/ip:port">
 	        </div>
 	        <div class="row">
 		        <div class="form-group col-lg-6">
 		            <label class="control-label" for="edit-tab-form-inputTabActionTypeNew" lang="en">Tab Auto Action</label>
-		                <select class="form-control" id="edit-tab-form-inputTabActionTypeNew" name="tabActionType">
+		                <select class="form-control" id="edit-tab-form-inputTabActionTypeNew" name="timeout">
 		                    <option value="null">None</option>
 		                    <option value="1">Auto Close</option>
 		                    <option value="2">Auto Reload</option>
@@ -164,13 +216,13 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
 		        </div>
 		        <div class="form-group col-lg-6">
 		            <label class="control-label" for="edit-tab-form-inputTabActionTimeNew" lang="en">Tab Auto Action Minutes</label>
-		                <input type="number" class="form-control" id="edit-tab-form-inputTabActionTimeNew" name="tabActionTime">
+		                <input type="number" class="form-control" id="edit-tab-form-inputTabActionTimeNew" name="timeout_ms">
 		        </div>
 		    </div>
 	        <div class="row">
 		        <div class="form-group col-lg-6">
 		            <label class="control-label" for="edit-tab-form-chooseImage" lang="en">Choose Image</label>
-		            ' . imageSelect("edit-tab-form") . '
+		            <select class="form-control tabIconImageList" id="edit-tab-form-chooseImage" name="chooseImage"><option lang="en">Select or type Image</option></select>
 		        </div>
 		        <div class="form-group col-lg-6">
 		            <label class="control-label" for="edit-tab-form-chooseIcon" lang="en">Choose Icon</label>
@@ -179,7 +231,7 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
 		    </div>
 	        <div class="form-group">
 	            <label class="control-label" for="edit-tab-form-inputImage" lang="en">Tab Image</label>
-	            <input type="text" class="form-control" id="edit-tab-form-inputImage" name="tabImage"  required="">
+	            <input type="text" class="form-control" id="edit-tab-form-inputImage" name="image"  required="">
 	        </div>
 	    </fieldset>
 	    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light row b-none testEditTab" type="button"><span class="btn-label"><i class="fa fa-flask"></i></span><span lang="en">Test Tab</span></button>

+ 21 - 3
api/pages/settings-template.php

@@ -1,8 +1,25 @@
 <?php
-
-$pageSettingsTemplate = '
+$GLOBALS['organizrPages'][] = 'settings_template';
+function get_page_settings_template($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	/*
+	 * Take this out if you dont care if DB as been created
+	 */
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	/*
+	 * Take this out if you dont want to be for admin only
+	 */
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	return '
 <script>
-	//buildCustomizeAppearance();
+	// Custom JS here
 </script>
 <div class="panel bg-org panel-info">
     <div class="panel-heading">
@@ -14,3 +31,4 @@ $pageSettingsTemplate = '
     </div>
 </div>
 ';
+}

+ 18 - 9
api/pages/settings-user-manage-groups.php

@@ -1,6 +1,17 @@
 <?php
-
-$pageSettingsUserManageGroups = '
+$GLOBALS['organizrPages'][] = 'settings_user_manage_groups';
+function get_page_settings_user_manage_groups($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	return '
 <script>
     buildGroupManagement();
 </script>
@@ -26,35 +37,33 @@ $pageSettingsUserManageGroups = '
     </div>
 </div>
 <form id="new-group-form" class="mfp-hide white-popup-block mfp-with-anim">
-    <input type="hidden" id="newGroupID" name="groupID" value="0">
-    <input type="hidden" name="groupDefault" value="0" required="">
     <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="groupName" required="" autofocus> </div>
+            <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="groupImage" required=""> </div>
+            <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>
 </form>
 <form id="edit-group-form" class="mfp-hide white-popup-block mfp-with-anim">
     <input type="hidden" name="id" value="x">
-    <input type="hidden" name="oldGroupName" 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="groupName" required="" autofocus>
+            <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="groupImage"  required="">
+            <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>
 </form>
 ';
+}

+ 233 - 4
api/pages/settings-user-manage-users.php

@@ -1,15 +1,243 @@
 <?php
-
-$pageSettingsUserManageUsers = '
+$GLOBALS['organizrPages'][] = 'settings_user_manage_users';
+function get_page_settings_user_manage_users($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	return '
 <script>
-    buildUserManagement();
+	$(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
+        }
+    });
+	$(function() {
+		
+		pageLength = 10;
+		function onPageSelect(newPageLength) {
+            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...",
+		    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;
+                $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");
+				        });
+					}
+				});
+		    },
+		    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");
+						message("User Updated",response.message,activeInfo.settings.notifications.position,"#FFF","success","5000");
+					}catch(e) {
+						console.log(e + " error: " + data);
+						orgErrorAlert("<h4>" + e + "</h4>" + formatDebug(data));
+						return false;
+					}
+				
+				}).fail(function(xhr) {
+					message("User Error", xhr.responseJSON.response.message, activeInfo.settings.notifications.position,"#FFF","error","10000");
+					console.error("Organizr Function: API Connection Failed");
+				});
+		    },
+		    
+		    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" );
+				$(".jsgrid-pager").find(".jsgrid-pager-current-page").addClass( "btn btn-primary m-r-5" );
+				let nav = $(".jsgrid-pager").find(".jsgrid-pager-nav-button");
+				$.each(nav, function(i,v) {
+					if(v.innerText === "..."){
+						$(this).addClass("hidden");
+					}
+				})
+			}
+	    });
+	    
+	});
 </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="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>
+			</div>
+		</div>
     </div>
-    <div class="table-responsive">
+    <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>
@@ -70,3 +298,4 @@ $pageSettingsUserManageUsers = '
     <div class="clearfix"></div>
 </form>
 ';
+}

+ 70 - 31
api/pages/settings.php

@@ -1,6 +1,18 @@
 <?php
-if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
-	$pageSettings = '
+$GLOBALS['organizrPages'][] = 'settings';
+function get_page_settings($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	$Organizr->writeLog('success', 'Admin Function -  Accessed Settings Page', $Organizr->user['username']);
+	return '
 <script>
     (function() {
         updateCheck();
@@ -8,6 +20,7 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
         sponsorLoad();
         newsLoad();
         checkCommitLoad();
+        backersLoad();
         [].slice.call(document.querySelectorAll(\'.sttabs-main-settings-div\')).forEach(function(el) {
             new CBPFWTabs(el);
         });
@@ -36,8 +49,8 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
                         <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\');loadSettingsPage(\'api/?v1/settings/image/manager/view\',\'#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\');loadSettingsPage(\'api/?v1/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::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>
@@ -45,13 +58,13 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
                     <! -- TAB EDITOR -->
                     <section id="settings-main-tab-editor">
                         <ul class="nav customtab2 nav-tabs" role="tablist">
-                            <li onclick="changeSettingsMenu(\'Settings::Tab Editor::Tabs\');loadSettingsPage(\'api/?v1/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 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\');loadSettingsPage(\'api/?v1/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 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\');loadSettingsPage(\'api/?v1/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 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\');loadSettingsPage(\'api/?v1/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 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 -->
@@ -74,7 +87,7 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
                     <! -- Customize -->
                     <section id="settings-main-customize">
                         <ul class="nav customtab2 nav-tabs" role="tablist">
-                            <li onclick="changeSettingsMenu(\'Settings::Customize::Appearance\');loadSettingsPage(\'api/?v1/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 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>
@@ -114,9 +127,9 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
                     <! -- USER MANAGEMENT -->
                     <section id="settings-main-user-management">
                         <ul class="nav customtab2 nav-tabs" role="tablist">
-                            <li onclick="changeSettingsMenu(\'Settings::User Management::Manage Users\');loadSettingsPage(\'api/?v1/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 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\');loadSettingsPage(\'api/?v1/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 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>
@@ -132,7 +145,7 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
                                 <div class="clearfix"></div>
                             </div>
                             <div role="tabpanel" class="tab-pane fade" id="settings-user-import-users">
-                                ' . importUserButtons() . '
+                                ' . $Organizr->importUserButtons() . '
                                 <div class="clearfix"></div>
                             </div>
                         </div>
@@ -156,14 +169,16 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
                         <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\');loadSettingsPage(\'api/?v1/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 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\');loadSettingsPage(\'api/?v1/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 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\');loadSettingsPage(\'api/?v1/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 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>
@@ -181,6 +196,10 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
                                 <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">
@@ -217,28 +236,30 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
     											<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>
-    									<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 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> ' . $GLOBALS['installedVersion'] . '</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/' . $GLOBALS['branch'] . '" target="_blank"> ' . $GLOBALS['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> ' . $GLOBALS['dbLocation'] . $GLOBALS['dbName'] . '</li>
-                                                ' . settingsDocker() . settingsPathChecks() . '
+                                                <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($GLOBALS['license']) . '</li>
+                                                <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>
@@ -257,6 +278,16 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
 							            </div>
 							        </div>
     							</div>
+    							<div class="row">
+    							    <div class="col-lg-12">
+                                        <div class="white-box bg-org">
+	                                        <div class="p-20 p-t-0 text-center">
+							                    <h4 class="font-medium">Backers</h4>
+							                    <ul class="dp-table m-t-30 backers-list"></ul>
+							                </div>
+						                </div>
+                                    </div>
+    							</div>
                                 <div class="clearfix"></div>
                             </div>
                             <div role="tabpanel" class="tab-pane fade" id="settings-settings-donate">
@@ -264,7 +295,7 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
                                     <div class="white-box bg-org">
                                         <ul class="nav nav-tabs tabs customtab">
                                             <li class="tab active">
-                                                <a href="#donate-beer" data-toggle="tab" aria-expanded="true"> <span class=""><i class="fa fa-beer text-warning"></i></span> <span class="hidden-xs" lang="en">Beerpay.io</span> </a>
+                                                <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>
@@ -278,14 +309,17 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
                                             <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-beer">
-                                                <blockquote>Want to show support on Beerpay.io?  Send me a beer :)<br/>Please click the button to continue.</blockquote>
-                                                <button onclick="window.open(\'https://beerpay.io/causefx/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 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>
@@ -318,6 +352,10 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
                                                 <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>
@@ -347,5 +385,6 @@ if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
     <div class="clearfix"></div>
     <div id="about-theme-body" class=""></div>
 </form>
+<div id="editHomepageItemDiv"><div id="editHomepageItem" class=""></div></div>
 ';
 }

+ 19 - 31
api/pages/wizard.php

@@ -1,5 +1,11 @@
 <?php
-$pageWizard = '
+$GLOBALS['organizrPages'][] = 'settings_wizard';
+function get_page_wizard($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	return '
 <script>
     (function() {
         $(\'#adminValidator\').wizard({
@@ -53,7 +59,7 @@ $pageWizard = '
                                 }
                             }
                         },
-                        location: {
+                        dbPath: {
                             validators: {
                                 notEmpty: {
                                     message: \'The database location is required\'
@@ -121,31 +127,12 @@ $pageWizard = '
                 return true;
             },
             onFinish: function() {
-                //$(\'#validation\').submit();
-                var post = $( \'#validation\' ).serializeArray();
-                console.log( post );
-                organizrAPI(\'POST\',\'api/?v1/wizard_config\',post).success(function(data) {
-            		var html = JSON.parse(data);
-                    if(html.data == true){
-                        location.reload();
-                    }else if(html.data == \'token\'){
-                        message("",window.lang.translate(\'Could not create Token\'),activeInfo.settings.notifications.position,"#FFF","error","3500");
-						console.error(\'Organizr Function: Could not create Token\');
-					}else if(html.data == \'db\'){
-						message("",window.lang.translate(\'Could not create DB - check permissions\'),activeInfo.settings.notifications.position,"#FFF","error","3500");
-						console.error(\'Organizr Function: Could not create DB - check permissions\');
-					}else if(html.data == \'admin\'){
-						message("",window.lang.translate(\'Could not create admin acct\'),activeInfo.settings.notifications.position,"#FFF","error","3500");
-						console.error(\'Organizr Function: Could not create admin acct\');
-					}else if(html.data == \'config\'){
-						message("",window.lang.translate(\'Could not create config files - check permissions\'),activeInfo.settings.notifications.position,"#FFF","error","3500");
-						console.error(\'Organizr Function: Could not create config files - check permissions\');
-					}else{
-						message("",window.lang.translate(\'Sign-up Error Occurred\'),activeInfo.settings.notifications.position,"#FFF","error","3500");
-                        console.error(\'Organizr Function: Sign-up Error Occurred\');
-                    }
+                var post = $( \'#validation\' ).serializeToJSON();
+                organizrAPI2(\'POST\',\'api/v2/wizard\',post).success(function(data) {
+            		var html = data.response;
+                    location.reload();
             	}).fail(function(xhr) {
-            		console.error("Organizr Function: Connection Failed");
+            	    OrganizrApiError(xhr, \'API Error\');
             	});
             }
         });
@@ -258,8 +245,8 @@ $pageWizard = '
                                     </div>
                                     <div class="panel-wrapper collapse in" aria-expanded="true">
                                         <div class="panel-body">
-                                            <p lang="en">The Hash Key will be used to decrypt all passwords etc... on the server. {User-Generated]</p>
-                                            <p lang="en">The Registration Password will lockout the registration field with this password. {User-Generated]</p>
+                                            <p lang="en">The Hash Key will be used to decrypt all passwords etc... on the server. [User-Generated]</p>
+                                            <p lang="en">The Registration Password will lockout the registration field with this password. [User-Generated]</p>
                                             <p lang="en">The API Key will be used for all calls to organizr for the UI. [Auto-Generated]</p>
                                         </div>
                                     </div>
@@ -309,10 +296,10 @@ $pageWizard = '
                                     </div>
                                 </div>
                                 <div class="form-group">
-                                    <label for="location" lang="en">Database Location</label>
+                                    <label for="dbPath" lang="en">Database Location</label>
                                     <div class="input-group">
                                         <div class="input-group-addon"><i class="ti-server"></i></div>
-                                        <input type="text" class="form-control wizardInput" name="location" id="form-location" placeholder="Enter path or copy from above">
+                                        <input type="text" class="form-control wizardInput" name="dbPath" id="form-dbPath" placeholder="Enter path or copy from above">
                                         <span class="input-group-btn"><button class="btn btn-info testPath" lang="en" type="button">Test / Create Path</button></span>
                                     </div>
                                 </div>
@@ -375,7 +362,7 @@ $pageWizard = '
                                         <div class="form-group">
                                             <label class="control-label col-md-3" lang="en">Database Location:</label>
                                             <div class="col-md-9">
-                                                <p class="form-control-static" id="verify-location">  </p>
+                                                <p class="form-control-static" id="verify-dbPath">  </p>
                                             </div>
                                         </div>
                                         <div class="form-group">
@@ -399,3 +386,4 @@ $pageWizard = '
 </div>
 <!-- /.container-fluid -->
 ';
+}

+ 191 - 56
api/plugins/api/chat.php

@@ -1,59 +1,194 @@
 <?php
-if (isset($_POST['data']['plugin'])) {
-	switch ($_POST['data']['plugin']) {
-		case 'chat/settings/get':
-			if (qualifyRequest(1)) {
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = chatGetSettings();
-			} else {
-				$result['status'] = 'error';
-				$result['statusText'] = 'API/Token invalid or not set';
-				$result['data'] = null;
-			}
-			break;
-		case 'chat/message':
-			if (qualifyRequest($GLOBALS['CHAT-Auth-include'])) {
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = sendChatMessage($_POST);
-			} else {
-				$result['status'] = 'error';
-				$result['statusText'] = 'API/Token invalid or not set';
-				$result['data'] = null;
-			}
-			break;
-		default:
-			//DO NOTHING!!!
-			break;
+/**
+ * @OA\Tag(
+ *     name="plugins-chat",
+ *     description="Pusher Chat Plugin"
+ * )
+ */
+/**
+ * @OA\Schema(
+ *     schema="getChatMessages",
+ *     type="object",
+ *     @OA\Property(
+ *      property="response",
+ *      type="object",
+ *      @OA\Property(
+ *          property="result",
+ *          description="success or error",
+ *          type="string",
+ *          example="success",
+ *      ),
+ *      @OA\Property(
+ *          property="message",
+ *          description="success or error message",
+ *          type="string",
+ *          example=null,
+ *      ),
+ *      @OA\Property(
+ *          property="data",
+ *          description="data from api",
+ *          type="array",
+ *          @OA\Items({
+ *          @OA\Property(
+ *              property="username",
+ *              type="string",
+ *              example="causefx",
+ *          ),
+ *          @OA\Property(
+ *              property="date",
+ *              type="string",
+ *              example="2018-09-01 02:02:24",
+ *          ),
+ *          @OA\Property(
+ *              property="gravatar",
+ *              type="string",
+ *              example="https://www.gravatar.com/avatar/a47c4a4b915ddf9601cd228f890bc366?s=100&d=mm",
+ *          ),
+ *          @OA\Property(
+ *              property="message",
+ *              type="string",
+ *              example="ok first message!",
+ *          ),
+ *          @OA\Property(
+ *              property="uid",
+ *              type="string",
+ *              example="f5287",
+ *          )
+ * })
+ *      ),
+ *  ),
+ * )
+ */
+/**
+ * @OA\Schema(
+ *     schema="submitMessageData",
+ *     type="object",
+ *     @OA\Property(
+ *      property="message",
+ *      type="string",
+ *      example="This is my message"
+ *  ),
+ * )
+ */
+/**
+ * @OA\Schema(
+ *     schema="submitMessage",
+ *     type="object",
+ *     @OA\Property(
+ *      property="response",
+ *      type="object",
+ *      @OA\Property(
+ *          property="result",
+ *          description="success or error",
+ *          type="string",
+ *          example="success",
+ *      ),
+ *      @OA\Property(
+ *          property="message",
+ *          description="success or error message",
+ *          type="string",
+ *          example="message has been accepted",
+ *      ),
+ *      @OA\Property(
+ *          property="data",
+ *          description="data from api",
+ *          type="string",
+ *          example=null,
+ *      ),
+ *  ),
+ * )
+ */
+$app->get('/plugins/chat/settings', function ($request, $response, $args) {
+	/**
+	 * @OA\Get(
+	 *     tags={"plugins-chat"},
+	 *     path="/api/v2/plugins/chat/settings",
+	 *     summary="Get settings",
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/pluginSettingsPage"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
+	$Chat = new Chat();
+	if ($Chat->checkRoute($request)) {
+		if ($Chat->qualifyRequest(1, true)) {
+			$GLOBALS['api']['response']['data'] = $Chat->_chatPluginGetSettings();
+		}
 	}
-}
-if (isset($_GET['plugin']) && $_GET['plugin'] == 'chat' && isset($_GET['cmd'])) {
-	switch ($_GET['cmd']) {
-		case 'chat/settings/get':
-			if (qualifyRequest(1)) {
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = chatGetSettings();
-			} else {
-				$result['status'] = 'error';
-				$result['statusText'] = 'API/Token invalid or not set';
-				$result['data'] = null;
-			}
-			break;
-		case 'chat/message':
-			if (qualifyRequest($GLOBALS['CHAT-Auth-include'])) {
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = getChatMessage();
-			} else {
-				$result['status'] = 'error';
-				$result['statusText'] = 'API/Token invalid or not set';
-				$result['data'] = null;
-			}
-			break;
-		default:
-			//Do NOTHING!
-			break;
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->get('/plugins/chat/message', function ($request, $response, $args) {
+	/**
+	 * @OA\Get(
+	 *     tags={"plugins-chat"},
+	 *     path="/api/v2/plugins/chat/message",
+	 *     summary="Get all messages",
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/getChatMessages"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
+	$Chat = new Chat();
+	if ($Chat->checkRoute($request)) {
+		if ($Chat->qualifyRequest($Chat->config['CHAT-Auth-include'], true)) {
+			$GLOBALS['api']['response']['data'] = $Chat->_chatPluginGetChatMessages();
+		}
 	}
-}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->post('/plugins/chat/message', function ($request, $response, $args) {
+	/**
+	 * @OA\Post(
+	 *     tags={"plugins-chat"},
+	 *     path="/api/v2/plugins/chat/message",
+	 *     summary="Submit a message",
+	 *     @OA\RequestBody(
+	 *      description="Success",
+	 *      required=true,
+	 *      @OA\JsonContent(ref="#/components/schemas/submitMessageData"),
+	 *      @OA\MediaType(
+	 *          mediaType="application/x-www-form-urlencoded",
+	 *          @OA\Schema(
+	 *              type="object",
+	 *              @OA\Property(
+	 *                  property="message",
+	 *                  description="message to send",
+	 *                  type="string",
+	 *              )
+	 *          )
+	 *      )
+	 *     ),
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/submitMessage"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
+	$Chat = new Chat();
+	if ($Chat->checkRoute($request)) {
+		if ($Chat->qualifyRequest($Chat->config['CHAT-Auth-include'], true)) {
+			$Chat->_chatPluginSendChatMessage($Chat->apiData($request));
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});

+ 127 - 56
api/plugins/api/healthChecks.php

@@ -1,59 +1,130 @@
 <?php
-if (isset($_POST['data']['plugin'])) {
-	switch ($_POST['data']['plugin']) {
-		case 'HealthChecks/settings/get':
-			if (qualifyRequest(1)) {
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = healthCheckGetSettings();
-			} else {
-				$result['status'] = 'error';
-				$result['statusText'] = 'API/Token invalid or not set';
-				$result['data'] = null;
-			}
-			break;
-		case 'HealthChecks/run':
-			if (qualifyRequest($GLOBALS['HEALTHCHECKS-Auth-include'])) {
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = healthCheckRun();
-			} else {
-				$result['status'] = 'error';
-				$result['statusText'] = 'API/Token invalid or not set';
-				$result['data'] = null;
-			}
-			break;
-		default:
-			//DO NOTHING!!
-			break;
+/**
+ * @OA\Tag(
+ *     name="plugins-healthchecks",
+ *     description="Healthchecks.io Ping Plugin"
+ * )
+ */
+/**
+ * @OA\Schema(
+ *     schema="healthChecksRun",
+ *     type="object",
+ *     @OA\Property(
+ *      property="response",
+ *      type="object",
+ *      @OA\Property(
+ *          property="result",
+ *          description="success or error",
+ *          type="string",
+ *          example="success",
+ *      ),
+ *      @OA\Property(
+ *          property="message",
+ *          description="success or error message",
+ *          type="string",
+ *          example=null,
+ *      ),
+ *      @OA\Property(
+ *          property="data",
+ *          description="data from api",
+ *          type="array",
+ *          @OA\Items({
+ *          @OA\Property(
+ *              property="Service Name",
+ *              type="string",
+ *              example="Radarr",
+ *          ),
+ *          @OA\Property(
+ *              property="UUID",
+ *              type="string",
+ *              example="883f0097-8f4c-4ca5-a9cf-053cfab8e334",
+ *          ),
+ *          @OA\Property(
+ *              property="External URL",
+ *              type="string",
+ *              example="https://radarr.com",
+ *          ),
+ *          @OA\Property(
+ *              property="Internal URL",
+ *              type="string",
+ *              example="http://radarr:7878",
+ *          ),
+ *          @OA\Property(
+ *              property="Enabled",
+ *              type="string",
+ *              example="true",
+ *          ),
+ *          @OA\Property(
+ *              property="results",
+ *              type="array",
+ *              @OA\Items({
+ *                  @OA\Property(
+ *                      property="internal",
+ *                      type="string",
+ *                      example="Success",
+ *                  ),
+ *                  @OA\Property(
+ *                      property="external",
+ *                      type="string",
+ *                      example="Success",
+ *                  ),
+ *
+ *              }),
+ *          ),
+ * })
+ *      ),
+ *  ),
+ * )
+ */
+$app->get('/plugins/healthchecks/settings', function ($request, $response, $args) {
+	/**
+	 * @OA\Get(
+	 *     tags={"plugins-healthchecks"},
+	 *     path="/api/v2/plugins/healthchecks/settings",
+	 *     summary="Get settings",
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/pluginSettingsPage"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
+	$HealthChecks = new HealthChecks();
+	if ($HealthChecks->checkRoute($request)) {
+		if ($HealthChecks->qualifyRequest(1, true)) {
+			$GLOBALS['api']['response']['data'] = $HealthChecks->_healthCheckPluginGetSettings();
+		}
 	}
-}
-if (isset($_GET['plugin']) && $_GET['plugin'] == 'HealthChecks' && isset($_GET['cmd'])) {
-	switch ($_GET['cmd']) {
-		case 'HealthChecks/settings/get':
-			if (qualifyRequest(1)) {
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = healthCheckGetSettings();
-			} else {
-				$result['status'] = 'error';
-				$result['statusText'] = 'API/Token invalid or not set';
-				$result['data'] = null;
-			}
-			break;
-		case 'HealthChecks/run':
-			if (qualifyRequest($GLOBALS['HEALTHCHECKS-Auth-include'])) {
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = healthCheckRun();
-			} else {
-				$result['status'] = 'error';
-				$result['statusText'] = 'API/Token invalid or not set';
-				$result['data'] = null;
-			}
-			break;
-		default:
-			//Do NOTHING!
-			break;
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->get('/plugins/healthchecks/run', function ($request, $response, $args) {
+	/**
+	 * @OA\Get(
+	 *     tags={"plugins-healthchecks"},
+	 *     path="/api/v2/plugins/healthchecks/run",
+	 *     summary="Run Healthchecks.io plugin",
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/healthChecksRun"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
+	$HealthChecks = new HealthChecks();
+	if ($HealthChecks->checkRoute($request)) {
+		if ($HealthChecks->qualifyRequest($HealthChecks->config['HEALTHCHECKS-Auth-include'], true)) {
+			$HealthChecks->_healthCheckPluginRun();
+		}
 	}
-}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});

+ 379 - 39
api/plugins/api/invites.php

@@ -1,42 +1,382 @@
 <?php
-if (isset($_POST['data']['plugin'])) {
-	switch ($_POST['data']['plugin']) {
-		case 'Invites/settings/get':
-			if (qualifyRequest(1)) {
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = invitesGetSettings();
-			} else {
-				$result['status'] = 'error';
-				$result['statusText'] = 'API/Token invalid or not set';
-				$result['data'] = null;
-			}
-			break;
-		case 'Invites/codes':
-			$result['status'] = 'success';
-			$result['statusText'] = 'success';
-			$result['data'] = inviteCodes($_POST);
-			break;
-		default:
-			//DO NOTHING!!
-			break;
+/**
+ * @OA\Tag(
+ *     name="plugins-invites",
+ *     description="Media Invite Plugin"
+ * )
+ */
+/**
+ * @OA\Schema(
+ *     schema="getInvites",
+ *     type="object",
+ *     @OA\Property(
+ *      property="response",
+ *      type="object",
+ *      @OA\Property(
+ *          property="result",
+ *          description="success or error",
+ *          type="string",
+ *          example="success",
+ *      ),
+ *      @OA\Property(
+ *          property="message",
+ *          description="success or error message",
+ *          type="string",
+ *          example=null,
+ *      ),
+ *      @OA\Property(
+ *          property="data",
+ *          description="data from api",
+ *          type="array",
+ *          @OA\Items({
+ *          @OA\Property(
+ *              property="id",
+ *              type="number",
+ *              example=1,
+ *          ),
+ *          @OA\Property(
+ *              property="code",
+ *              type="string",
+ *              example="NN9JH9",
+ *          ),
+ *          @OA\Property(
+ *              property="date",
+ *              type="string",
+ *              example="2018-09-01 02:02:24",
+ *          ),
+ *          @OA\Property(
+ *              property="email",
+ *              type="string",
+ *              example="causefX@organizr.app",
+ *          ),
+ *          @OA\Property(
+ *              property="username",
+ *              type="string",
+ *              example="causefx",
+ *          ),
+ *          @OA\Property(
+ *              property="dateused",
+ *              type="string",
+ *              example="2018-09-01 02:02:24",
+ *          ),
+ *          @OA\Property(
+ *              property="usedby",
+ *              type="string",
+ *              example="causefx",
+ *          ),
+ *          @OA\Property(
+ *              property="ip",
+ *              type="string",
+ *              example="10.0.0.0",
+ *          ),
+ *          @OA\Property(
+ *              property="valid",
+ *              type="string",
+ *              example="No",
+ *          ),
+ *          @OA\Property(
+ *              property="type",
+ *              type="string",
+ *              example="Plex",
+ *          )
+ * })
+ *      ),
+ *  ),
+ * )
+ */
+/**
+ * @OA\Schema(
+ *     schema="createInviteCode",
+ *     type="object",
+ *     @OA\Property(
+ *      property="response",
+ *      type="object",
+ *      @OA\Property(
+ *          property="result",
+ *          description="success or error",
+ *          type="string",
+ *          example="success",
+ *      ),
+ *      @OA\Property(
+ *          property="message",
+ *          description="success or error message",
+ *          type="string",
+ *          example="Invite Code: XYXYXY has been created",
+ *      ),
+ *      @OA\Property(
+ *          property="data",
+ *          description="data from api",
+ *          type="string",
+ *          example=null,
+ *      ),
+ *  ),
+ * )
+ */
+/**
+ * @OA\Schema(
+ *     schema="verifyInviteCode",
+ *     type="object",
+ *     @OA\Property(
+ *      property="response",
+ *      type="object",
+ *      @OA\Property(
+ *          property="result",
+ *          description="success or error",
+ *          type="string",
+ *          example="success",
+ *      ),
+ *      @OA\Property(
+ *          property="message",
+ *          description="success or error message",
+ *          type="string",
+ *          example="Code has been verified",
+ *      ),
+ *      @OA\Property(
+ *          property="data",
+ *          description="data from api",
+ *          type="string",
+ *          example=null,
+ *      ),
+ *  ),
+ * )
+ */
+/**
+ * @OA\Schema(
+ *     schema="useInviteCode",
+ *     type="object",
+ *     @OA\Property(
+ *      property="response",
+ *      type="object",
+ *      @OA\Property(
+ *          property="result",
+ *          description="success or error",
+ *          type="string",
+ *          example="success",
+ *      ),
+ *      @OA\Property(
+ *          property="message",
+ *          description="success or error message",
+ *          type="string",
+ *          example="Plex/Emby User now has access to system",
+ *      ),
+ *      @OA\Property(
+ *          property="data",
+ *          description="data from api",
+ *          type="string",
+ *          example=null,
+ *      ),
+ *  ),
+ * )
+ */
+/**
+ * @OA\Schema(
+ *     schema="deleteInviteCode",
+ *     type="object",
+ *     @OA\Property(
+ *      property="response",
+ *      type="object",
+ *      @OA\Property(
+ *          property="result",
+ *          description="success or error",
+ *          type="string",
+ *          example="success",
+ *      ),
+ *      @OA\Property(
+ *          property="message",
+ *          description="success or error message",
+ *          type="string",
+ *          example="Code has been deleted",
+ *      ),
+ *      @OA\Property(
+ *          property="data",
+ *          description="data from api",
+ *          type="string",
+ *          example=null,
+ *      ),
+ *  ),
+ * )
+ */
+$app->get('/plugins/invites/settings', function ($request, $response, $args) {
+	/**
+	 * @OA\Get(
+	 *     tags={"plugins-invites"},
+	 *     path="/api/v2/plugins/invites/settings",
+	 *     summary="Get settings",
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/pluginSettingsPage"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
+	$Invites = new Invites();
+	if ($Invites->checkRoute($request)) {
+		if ($Invites->qualifyRequest(1, true)) {
+			$GLOBALS['api']['response']['data'] = $Invites->_invitesPluginGetSettings();
+		}
 	}
-}
-if (isset($_GET['plugin']) && $_GET['plugin'] == 'Invites' && isset($_GET['cmd'])) {
-	switch ($_GET['cmd']) {
-		case 'Invites/settings/get':
-			if (qualifyRequest(1)) {
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = invitesGetSettings();
-			} else {
-				$result['status'] = 'error';
-				$result['statusText'] = 'API/Token invalid or not set';
-				$result['data'] = null;
-			}
-			break;
-		default:
-			//Do NOTHING!
-			break;
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->get('/plugins/invites', function ($request, $response, $args) {
+	/**
+	 * @OA\Get(
+	 *     tags={"plugins-invites"},
+	 *     path="/api/v2/plugins/invites",
+	 *     summary="Get All Invites",
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/getInvites"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
+	$Invites = new Invites();
+	if ($Invites->checkRoute($request)) {
+		if ($Invites->qualifyRequest(1, true)) {
+			$GLOBALS['api']['response']['data'] = $Invites->_invitesPluginGetCodes();
+		}
 	}
-}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->post('/plugins/invites', function ($request, $response, $args) {
+	/**
+	 * @OA\Post(
+	 *     tags={"plugins-invites"},
+	 *     path="/api/v2/plugins/invites",
+	 *     summary="Create Invite Code",
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/createInviteCode"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
+	$Invites = new Invites();
+	if ($Invites->checkRoute($request)) {
+		if ($Invites->qualifyRequest(1, true)) {
+			$Invites->_invitesPluginCreateCode($Invites->apiData($request));
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->get('/plugins/invites/{code}', function ($request, $response, $args) {
+	/**
+	 * @OA\Get(
+	 *     tags={"plugins-invites"},
+	 *     path="/api/v2/plugins/invites/{code}",
+	 *     summary="Verify Invite Code",
+	 *     @OA\Parameter(
+	 *      name="code",
+	 *      description="The Invite Code",
+	 *      @OA\Schema(
+	 *          type="integer",
+	 *          format="int64",
+	 *      ),
+	 *      in="path",
+	 *      required=true
+	 *      ),
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/verifyInviteCode"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized")
+	 * )
+	 */
+	$Invites = new Invites();
+	if ($Invites->checkRoute($request)) {
+		if ($Invites->qualifyRequest(999, true)) {
+			$Invites->_invitesPluginVerifyCode($args['code']);
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->post('/plugins/invites/{code}', function ($request, $response, $args) {
+	/**
+	 * @OA\Post(
+	 *     tags={"plugins-invites"},
+	 *     path="/api/v2/plugins/invites/{code}",
+	 *     summary="Use Invite Code",
+	 *     @OA\Parameter(
+	 *      name="code",
+	 *      description="The Invite Code",
+	 *      @OA\Schema(
+	 *          type="integer",
+	 *          format="int64",
+	 *      ),
+	 *      in="path",
+	 *      required=true
+	 *      ),
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/useInviteCode"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized")
+	 * )
+	 */
+	$Invites = new Invites();
+	if ($Invites->checkRoute($request)) {
+		if ($Invites->qualifyRequest(999, true)) {
+			$Invites->_invitesPluginUseCode($args['code'], $Invites->apiData($request));
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->delete('/plugins/invites/{code}', function ($request, $response, $args) {
+	/**
+	 * @OA\Delete(
+	 *     tags={"plugins-invites"},
+	 *     path="/api/v2/plugins/invites/{code}",
+	 *     summary="Delete Invite Code",
+	 *     @OA\Parameter(
+	 *      name="code",
+	 *      description="The Invite Code",
+	 *      @OA\Schema(
+	 *          type="integer",
+	 *          format="int64",
+	 *      ),
+	 *      in="path",
+	 *      required=true
+	 *      ),
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/deleteInviteCode"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
+	$Invites = new Invites();
+	if ($Invites->checkRoute($request)) {
+		if ($Invites->qualifyRequest(1, true)) {
+			$Invites->_invitesPluginDeleteCode($args['code']);
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});

+ 135 - 67
api/plugins/api/php-mailer.php

@@ -1,70 +1,138 @@
 <?php
-if (isset($_POST['data']['plugin'])) {
-	switch ($_POST['data']['plugin']) {
-		case 'PHPMailer/settings/get':
-			if (qualifyRequest(1)) {
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = phpmGetSettings();
-			} else {
-				$result['status'] = 'error';
-				$result['statusText'] = 'API/Token invalid or not set';
-				$result['data'] = null;
-			}
-			break;
-		case 'PHPMailer/send/test':
-			if (qualifyRequest(1)) {
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = phpmSendTestEmail();
-			} else {
-				$result['status'] = 'error';
-				$result['statusText'] = 'API/Token invalid or not set';
-				$result['data'] = null;
-			}
-			break;
-		case 'PHPMailer/send/email':
-			if (qualifyRequest(1)) {
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = phpmAdminSendEmail();
-			} else {
-				$result['status'] = 'error';
-				$result['statusText'] = 'API/Token invalid or not set';
-				$result['data'] = null;
-			}
-			break;
-		case 'PHPMailer/users/get':
-			if (qualifyRequest(1)) {
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = getEmails();
-			} else {
-				$result['status'] = 'error';
-				$result['statusText'] = 'API/Token invalid or not set';
-				$result['data'] = null;
-			}
-			break;
-		default:
-			//DO NOTHING!!
-			break;
+/**
+ * @OA\Tag(
+ *     name="plugins-php-mailer",
+ *     description="PHP Mailer Plugin"
+ * )
+ */
+/**
+ * @OA\Schema(
+ *     schema="sendEmailData",
+ *     type="object",
+ *      @OA\Property(
+ *          property="bcc",
+ *          description="email of recipients (csv)",
+ *          type="string",
+ *          example="causefx@organizr.app,elmer@organizr.app",
+ *      ),
+ *      @OA\Property(
+ *          property="subject",
+ *          type="string",
+ *          example="Hey There Buddy?!",
+ *      ),
+ *      @OA\Property(
+ *          property="body",
+ *          type="string",
+ *          example="Hi! Boy, has it been a long time!  Have you seen rox in socks?",
+ *      ),
+ * )
+ */
+$app->get('/plugins/php-mailer/settings', function ($request, $response, $args) {
+	/**
+	 * @OA\Get(
+	 *     tags={"plugins-php-mailer"},
+	 *     path="/api/v2/plugins/php-mailer/settings",
+	 *     summary="Get settings",
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/pluginSettingsPage"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
+	$PhpMailer = new PhpMailer();
+	if ($PhpMailer->checkRoute($request)) {
+		if ($PhpMailer->qualifyRequest(1, true)) {
+			$GLOBALS['api']['response']['data'] = $PhpMailer->_phpMailerPluginGetSettings();
+		}
 	}
-}
-if (isset($_GET['plugin']) && $_GET['plugin'] == 'PHPMailer' && isset($_GET['cmd'])) {
-	switch ($_GET['cmd']) {
-		case 'PHPMailer/settings/get':
-			if (qualifyRequest(1)) {
-				$result['status'] = 'success';
-				$result['statusText'] = 'success';
-				$result['data'] = phpmGetSettings();
-			} else {
-				$result['status'] = 'error';
-				$result['statusText'] = 'API/Token invalid or not set';
-				$result['data'] = null;
-			}
-			break;
-		default:
-			//Do NOTHING!
-			break;
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->get('/plugins/php-mailer/email/test', function ($request, $response, $args) {
+	/**
+	 * @OA\Get(
+	 *     tags={"plugins-php-mailer"},
+	 *     path="/api/v2/plugins/php-mailer/email/test",
+	 *     summary="Send Test Email to Default Admin Email",
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/successNullData"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
+	$PhpMailer = new PhpMailer();
+	if ($PhpMailer->checkRoute($request)) {
+		if ($PhpMailer->qualifyRequest(1, true)) {
+			$PhpMailer->_phpMailerPluginSendTestEmail();
+		}
 	}
-}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->post('/plugins/php-mailer/email/send', function ($request, $response, $args) {
+	/**
+	 * @OA\Post(
+	 *     tags={"plugins-php-mailer"},
+	 *     path="/api/v2/plugins/php-mailer/email/send",
+	 *     summary="Send Email",
+	 *     @OA\RequestBody(
+	 *      description="Success",
+	 *      required=true,
+	 *      @OA\JsonContent(ref="#/components/schemas/sendEmailData"),
+	 *     ),
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/successNullData"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
+	$PhpMailer = new PhpMailer();
+	if ($PhpMailer->checkRoute($request)) {
+		if ($PhpMailer->qualifyRequest(1, true)) {
+			$PhpMailer->_phpMailerPluginAdminSendEmail($PhpMailer->apiData($request));
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->get('/plugins/php-mailer/email/list', function ($request, $response, $args) {
+	/**
+	 * @OA\Get(
+	 *     tags={"plugins-php-mailer"},
+	 *     path="/api/v2/plugins/php-mailer/email/list",
+	 *     summary="Get List of User Emails",
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/php-mailer-email-list"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
+	$PhpMailer = new PhpMailer();
+	if ($PhpMailer->checkRoute($request)) {
+		if ($PhpMailer->qualifyRequest(1, true)) {
+			$GLOBALS['api']['response']['data'] = $PhpMailer->_phpMailerPluginGetEmails();
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});

+ 32 - 18
api/plugins/api/speedTest.php

@@ -1,19 +1,33 @@
 <?php
-if (isset($_POST['data']['plugin'])) {
-    switch ($_POST['data']['plugin']) {
-        case 'SpeedTest/settings/get':
-            if (qualifyRequest(1)) {
-                $result['status'] = 'success';
-                $result['statusText'] = 'success';
-                $result['data'] = speedTestGetSettings();
-            } else {
-                $result['status'] = 'error';
-                $result['statusText'] = 'API/Token invalid or not set';
-                $result['data'] = null;
-            }
-            break;
-        default:
-            //DO NOTHING!!
-            break;
-    }
-}
+/**
+ * @OA\Tag(
+ *     name="plugins-speedtest",
+ *     description="SpeedTest Plugin"
+ * )
+ */
+$app->get('/plugins/speedtest/settings', function ($request, $response, $args) {
+	/**
+	 * @OA\Get(
+	 *     tags={"plugins-speedtest"},
+	 *     path="/api/v2/plugins/speedtest/settings",
+	 *     summary="Get settings",
+	 *     @OA\Response(
+	 *      response="200",
+	 *      description="Success",
+	 *      @OA\JsonContent(ref="#/components/schemas/pluginSettingsPage"),
+	 *     ),
+	 *     @OA\Response(response="401",description="Unauthorized"),
+	 *     security={{ "api_key":{} }}
+	 * )
+	 */
+	$SpeedTest = new SpeedTest();
+	if ($SpeedTest->checkRoute($request)) {
+		if ($SpeedTest->qualifyRequest(1, true)) {
+			$GLOBALS['api']['response']['data'] = $SpeedTest->speedTestGetSettings();
+		}
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});

+ 122 - 115
api/plugins/chat.php

@@ -6,9 +6,6 @@ $GLOBALS['plugins'][]['chat'] = array( // Plugin Name
 	'category' => 'Utilities', // One to Two Word Description
 	'link' => '', // Link to plugin info
 	'license' => 'personal,business', // License Type use , for multiple
-	//'fileName'=>'php-mailer.php',
-	//'configFile'=>'php-mailer.php',
-	//'apiFile'=>'php-mailer.php',
 	'idPrefix' => 'CHAT', // html element id prefix
 	'configPrefix' => 'CHAT', // config file prefix for array items without the hypen
 	'version' => '1.0.0', // SemVer of plugin
@@ -16,13 +13,13 @@ $GLOBALS['plugins'][]['chat'] = array( // Plugin Name
 	'settings' => true, // does plugin need a settings page? true or false
 	'homepage' => false // Is plugin for use on homepage? true or false
 );
-// INCLUDE/REQUIRE FILES
-// PLUGIN FUNCTIONS
-/* GET CHAT SETTINGS */
-function chatGetSettings()
+
+class Chat extends Organizr
 {
-	return array(
-		'custom' => '
+	public function _chatPluginGetSettings()
+	{
+		return array(
+			'custom' => '
 				<div class="row">
                     <div class="col-lg-12">
                         <div class="panel panel-info">
@@ -45,120 +42,130 @@ function chatGetSettings()
                     </div>
 				</div>
 				',
-		'Options' => array(
-			array(
-				'type' => 'select',
-				'name' => 'CHAT-Auth-include',
-				'label' => 'Minimum Authentication',
-				'value' => $GLOBALS['CHAT-Auth-include'],
-				'options' => groupSelect()
+			'Options' => array(
+				array(
+					'type' => 'select',
+					'name' => 'CHAT-Auth-include',
+					'label' => 'Minimum Authentication',
+					'value' => $this->config['CHAT-Auth-include'],
+					'options' => $this->groupSelect()
+				),
+				array(
+					'type' => 'number',
+					'name' => 'CHAT-messageLoadLimit',
+					'label' => '# of Previous Messages',
+					'value' => $this->config['CHAT-messageLoadLimit'],
+					'placeholder' => ''
+				),
+				array(
+					'type' => 'select',
+					'name' => 'CHAT-userRefreshTimeout',
+					'label' => 'Refresh Seconds',
+					'value' => $this->config['CHAT-userRefreshTimeout'],
+					'options' => $this->timeOptions()
+				),
+				array(
+					'type' => 'select',
+					'name' => 'CHAT-newMessageSound-include',
+					'label' => 'Message Sound',
+					'value' => $this->config['CHAT-newMessageSound-include'],
+					'options' => $this->getSounds()
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'CHAT-useSSL',
+					'label' => 'Use Pusher SSL',
+					'help' => 'If messages get stuck sending, please turn this option off.',
+					'value' => $this->config['CHAT-useSSL']
+				)
 			),
-			array(
-				'type' => 'number',
-				'name' => 'CHAT-messageLoadLimit',
-				'label' => '# of Previous Messages',
-				'value' => $GLOBALS['CHAT-messageLoadLimit'],
-				'placeholder' => ''
-			),
-			array(
-				'type' => 'select',
-				'name' => 'CHAT-userRefreshTimeout',
-				'label' => 'Refresh Seconds',
-				'value' => $GLOBALS['CHAT-userRefreshTimeout'],
-				'options' => optionTime()
-			),
-			array(
-				'type' => 'select',
-				'name' => 'CHAT-newMessageSound-include',
-				'label' => 'Message Sound',
-				'value' => $GLOBALS['CHAT-newMessageSound-include'],
-				'options' => getSounds()
-			),
-			array(
-				'type' => 'switch',
-				'name' => 'CHAT-useSSL',
-				'label' => 'Use Pusher SSL',
-				'help' => 'If messages get stuck sending, please turn this option off.',
-				'value' => $GLOBALS['CHAT-useSSL']
+			'Connection' => array(
+				array(
+					'type' => 'password-alt',
+					'name' => 'CHAT-authKey-include',
+					'label' => 'Auth Key',
+					'value' => $this->config['CHAT-authKey-include']
+				),
+				array(
+					'type' => 'password-alt',
+					'name' => 'CHAT-secret',
+					'label' => 'API Secret',
+					'value' => $this->config['CHAT-secret']
+				),
+				array(
+					'type' => 'input',
+					'name' => 'CHAT-appID-include',
+					'label' => 'App ID',
+					'value' => $this->config['CHAT-appID-include']
+				),
+				array(
+					'type' => 'input',
+					'name' => 'CHAT-cluster-include',
+					'label' => 'App Cluster',
+					'value' => $this->config['CHAT-cluster-include']
+				),
 			)
-		),
-		'Connection' => array(
-			array(
-				'type' => 'password-alt',
-				'name' => 'CHAT-authKey-include',
-				'label' => 'Auth Key',
-				'value' => $GLOBALS['CHAT-authKey-include']
-			),
-			array(
-				'type' => 'password-alt',
-				'name' => 'CHAT-secret',
-				'label' => 'API Secret',
-				'value' => $GLOBALS['CHAT-secret']
-			),
-			array(
-				'type' => 'input',
-				'name' => 'CHAT-appID-include',
-				'label' => 'App ID',
-				'value' => $GLOBALS['CHAT-appID-include']
-			),
-			array(
-				'type' => 'input',
-				'name' => 'CHAT-cluster-include',
-				'label' => 'App Cluster',
-				'value' => $GLOBALS['CHAT-cluster-include']
-			),
-		)
-	);
-}
-
-function sendChatMessage($array)
-{
-	$message = isset($array['data']['message']) ? $array['data']['message'] : null;
-	$message = htmlspecialchars($message, ENT_QUOTES);
-	$now = date("Y-m-d H:i:s");
-	$currentIP = userIP();
-	try {
-		$connect = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-		]);
+		);
+	}
+	
+	public function _chatPluginSendChatMessage($array)
+	{
+		$message = isset($array['message']) ? $array['message'] : null;
+		if (!$message) {
+			$this->setAPIResponse('error', 'No message supplied', 409);
+			return false;
+		}
+		$message = htmlspecialchars($message, ENT_QUOTES);
+		$now = date("Y-m-d H:i:s");
+		$currentIP = $this->userIP();
 		$newMessage = [
-			'username' => $GLOBALS['organizrUser']['username'],
-			'gravatar' => $GLOBALS['organizrUser']['image'],
-			'uid' => $GLOBALS['organizrUser']['uid'],
+			'username' => $this->user['username'],
+			'gravatar' => $this->user['image'],
+			'uid' => $this->user['uid'],
 			'date' => $now,
 			'ip' => $currentIP,
 			'message' => $message
 		];
-		$connect->query('INSERT INTO [chatroom]', $newMessage);
-		$options = array(
-			'cluster' => $GLOBALS['CHAT-cluster-include'],
-			'useTLS' => $GLOBALS['CHAT-useSSL']
-		);
-		$pusher = new Pusher\Pusher(
-			$GLOBALS['CHAT-authKey-include'],
-			$GLOBALS['CHAT-secret'],
-			$GLOBALS['CHAT-appID-include'],
-			$options
-		);
-		$pusher->trigger('org_channel', 'my-event', $newMessage);
-		return true;
-	} catch (Dibi\Exception $e) {
-		return $e;
-	}
-}
-
-function getChatMessage()
-{
-	try {
-		$connect = new Dibi\Connection([
-			'driver' => 'sqlite3',
-			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-		]);
-		$all = $connect->fetchAll('SELECT `username`, `gravatar`, `uid`, `date`, `message` FROM chatroom LIMIT ' . $GLOBALS['CHAT-messageLoadLimit']);
-		return $all;
-	} catch (Dibi\Exception $e) {
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [chatroom]',
+					$newMessage
+				)
+			),
+		];
+		$query = $this->processQueries($response);
+		if ($query) {
+			$options = array(
+				'cluster' => $GLOBALS['CHAT-cluster-include'],
+				'useTLS' => $GLOBALS['CHAT-useSSL']
+			);
+			$pusher = new Pusher\Pusher(
+				$GLOBALS['CHAT-authKey-include'],
+				$GLOBALS['CHAT-secret'],
+				$GLOBALS['CHAT-appID-include'],
+				$options
+			);
+			$pusher->trigger('org_channel', 'my-event', $newMessage);
+			$this->setAPIResponse('success', 'Chat message accepted', 200);
+			return true;
+		}
+		$this->setAPIResponse('error', 'Chat error occurred', 409);
 		return false;
 	}
 	
+	public function _chatPluginGetChatMessages()
+	{
+		$response = [
+			array(
+				'function' => 'fetchAll',
+				'query' => array(
+					'SELECT `username`, `gravatar`, `uid`, `date`, `message` FROM (SELECT `username`, `gravatar`, `uid`, `date`, `message` FROM chatroom ORDER BY date DESC LIMIT ?) ORDER BY date ASC',
+					$this->config['CHAT-messageLoadLimit']
+				)
+			),
+		];
+		return $this->processQueries($response);
+	}
 }

+ 1 - 0
api/plugins/config/chat.php

@@ -9,5 +9,6 @@ return array(
 	'CHAT-messageLoadLimit' => '200',
 	'CHAT-userRefreshTimeout' => '60000',
 	'CHAT-newMessageSound-include' => 'plugins/sounds/default/newmessage.mp3',
+	'CHAT-newMessageSoundz-include' => 'plugins/sounds/default/newmessage.mp3',
 	'CHAT-useSSL' => false
 );

+ 2 - 2
api/plugins/config/php-mailer.php

@@ -13,12 +13,12 @@ return array(
 	'PHPMAILER-domain' => '',
 	'PHPMAILER-template' => 'default',
 	'PHPMAILER-logo' => 'https://raw.githubusercontent.com/causefx/Organizr/v2-develop/plugins/images/organizr/logo-wide.png',
-	'PHPMAILER-emailTemplateResetPassword' => '
+	'PHPMAILER-emailTemplateReset' => '
 	<h2>Hey there {user}!</h2><br />
 	Looks like you forgot your password.  Well, I got you...  Here is your new password: {password}<br />
 	If you want to change it once you log in, you can.  Head over to my website: {domain}<br />
 	',
-	'PHPMAILER-emailTemplateResetPasswordSubject' => 'Password Reset',
+	'PHPMAILER-emailTemplateResetSubject' => 'Password Reset',
 	'PHPMAILER-emailTemplateInviteUser' => '
 	<h2>Hey there {user}!</h2><br />
 	Here is the invite code to join my cool media server: {inviteCode}<br/>

+ 107 - 100
api/plugins/healthChecks.php

@@ -13,117 +13,124 @@ $GLOBALS['plugins'][]['healthChecks'] = array( // Plugin Name
 	'settings' => true, // does plugin need a settings page? true or false
 	'homepage' => false // Is plugin for use on homepage? true or false
 );
-// INCLUDE/REQUIRE FILES
-// PLUGIN FUNCTIONS
-function healthCheckTest($url)
-{
-	$success = false;
-	$options = array('verify' => false, 'verifyname' => false, 'follow_redirects' => true, 'redirects' => 1);
-	$headers = array('Token' => $GLOBALS['organizrAPI']);
-	$url = qualifyURL($url);
-	$response = Requests::get($url, $headers, $options);
-	if ($response->success) {
-		$success = true;
-	}
-	if ($response->status_code == 200) {
-		$success = true;
-	}
-	return $success;
-}
 
-function healthCheckUUID($uuid, $pass = false)
+class HealthChecks extends Organizr
 {
-	if (!$uuid || !$pass || $GLOBALS['HEALTHCHECKS-PingURL'] == '') {
-		return false;
+	public function _healthCheckPluginGetSettings()
+	{
+		return array(
+			'Options' => array(
+				array(
+					'type' => 'select',
+					'name' => 'HEALTHCHECKS-Auth-include',
+					'label' => 'Minimum Authentication',
+					'value' => $this->config['HEALTHCHECKS-Auth-include'],
+					'options' => $this->groupSelect()
+				),
+				array(
+					'type' => 'input',
+					'name' => 'HEALTHCHECKS-PingURL',
+					'label' => 'URL',
+					'value' => $this->config['HEALTHCHECKS-PingURL'],
+					'help' => 'URL for HealthChecks Ping',
+					'placeholder' => 'HealthChecks Ping URL'
+				),
+			),
+			'Services' => array(
+				array(
+					'type' => 'arrayMultiple',
+					'name' => 'HEALTHCHECKS-all-items',
+					'label' => 'Services',
+					'value' => $this->config['HEALTHCHECKS-all-items']
+				)
+			)
+		);
 	}
-	$url = qualifyURL($GLOBALS['HEALTHCHECKS-PingURL']);
-	$uuid = '/' . $uuid;
-	$path = !$pass ? '/fail' : '';
-	$response = Requests::get($url . $uuid . $path, [], []);
-	return $response;
-}
-
-function healthCheckRun()
-{
-	$continue = $GLOBALS['HEALTHCHECKS-all-items'] !== '' ? $GLOBALS['HEALTHCHECKS-all-items'] : false;
-	if ($continue && $GLOBALS['HEALTHCHECKS-enabled'] && !empty($GLOBALS['HEALTHCHECKS-PingURL']) && qualifyRequest($GLOBALS['HEALTHCHECKS-Auth-include'])) {
-		$allItems = [];
-		foreach ($GLOBALS['HEALTHCHECKS-all-items'] as $k => $v) {
-			
-			if ($k !== false) {
-				foreach ($v as $item) {
-					$allItems[$k][$item['label']] = $item['value'];
-				}
-			}
+	
+	public function _healthCheckPluginTest($url)
+	{
+		$success = false;
+		$options = array('verify' => false, 'verifyname' => false, 'follow_redirects' => true, 'redirects' => 1);
+		$headers = array('Token' => $this->config['organizrAPI']);
+		$url = $this->qualifyURL($url);
+		$response = Requests::get($url, $headers, $options);
+		if ($response->success) {
+			$success = true;
 		}
-		foreach ($allItems as $k => $v) {
-			if ($v['Enabled'] == 'false') {
-				unset($allItems[$k]);
-			}
-			if (!$v['UUID']) {
-				unset($allItems[$k]);
-			}
+		if ($response->status_code == 200) {
+			$success = true;
 		}
-		foreach ($allItems as $k => $v) {
-			$testLocal = $v['Internal URL'] !== '' ?? false;
-			$testExternal = $v['External URL'] !== '' ?? false;
-			$testBoth = ($testLocal && $testExternal) ?? false;
-			$pass = false;
-			if ($testLocal) {
-				$allItems[$k]['results']['internal'] = (healthCheckTest($v['Internal URL'])) ? 'Success' : 'Error';
+		return $success;
+	}
+	
+	public function _healthCheckPluginUUID($uuid, $pass = false)
+	{
+		if (!$uuid || !$pass || $this->config['HEALTHCHECKS-PingURL'] == '') {
+			return false;
+		}
+		$url = $this->qualifyURL($this->config['HEALTHCHECKS-PingURL']);
+		$uuid = '/' . $uuid;
+		$path = !$pass ? '/fail' : '';
+		return Requests::get($url . $uuid . $path, [], []);
+	}
+	
+	public function _healthCheckPluginRun()
+	{
+		$continue = $this->config['HEALTHCHECKS-all-items'] !== '' ? $this->config['HEALTHCHECKS-all-items'] : false;
+		if (!$continue) {
+			$this->setAPIResponse('error', 'No items are setup', 409);
+		}
+		if ($continue && $this->config['HEALTHCHECKS-enabled'] && !empty($this->config['HEALTHCHECKS-PingURL']) && $this->qualifyRequest($this->config['HEALTHCHECKS-Auth-include'])) {
+			$allItems = [];
+			foreach ($this->config['HEALTHCHECKS-all-items'] as $k => $v) {
+				
+				if ($k !== false) {
+					foreach ($v as $item) {
+						$allItems[$k][$item['label']] = $item['value'];
+					}
+				}
 			}
-			if ($testExternal) {
-				$allItems[$k]['results']['external'] = (healthCheckTest($v['External URL'])) ? 'Success' : 'Error';
+			foreach ($allItems as $k => $v) {
+				if ($v['Enabled'] == false) {
+					unset($allItems[$k]);
+				}
+				if (!$v['UUID']) {
+					unset($allItems[$k]);
+				}
 			}
-			if ($testBoth) {
-				if ($allItems[$k]['results']['external'] == 'Success' && $allItems[$k]['results']['internal'] == 'Success') {
-					$pass = true;
+			foreach ($allItems as $k => $v) {
+				$testLocal = $v['Internal URL'] !== '' ?? false;
+				$testExternal = $v['External URL'] !== '' ?? false;
+				$testBoth = ($testLocal && $testExternal) ?? false;
+				$pass = false;
+				if ($testLocal) {
+					$allItems[$k]['results']['internal'] = ($this->_healthCheckPluginTest($v['Internal URL'])) ? 'Success' : 'Error';
 				}
-			} elseif ($testLocal) {
-				if ($allItems[$k]['results']['internal'] == 'Success') {
-					$pass = true;
+				if ($testExternal) {
+					if (($testBoth && $allItems[$k]['results']['internal'] == 'Error') || !$testBoth) {
+						$allItems[$k]['results']['external'] = ($this->_healthCheckPluginTest($v['External URL'])) ? 'Success' : 'Error';
+					} else {
+						$allItems[$k]['results']['external'] = 'Not needed';
+					}
 				}
-			} elseif ($testExternal) {
-				if ($allItems[$k]['results']['external'] == 'Success') {
-					$pass = true;
+				if ($testBoth) {
+					if ($allItems[$k]['results']['external'] == 'Success' || $allItems[$k]['results']['internal'] == 'Success') {
+						$pass = true;
+					}
+				} elseif ($testLocal) {
+					if ($allItems[$k]['results']['internal'] == 'Success') {
+						$pass = true;
+					}
+				} elseif ($testExternal) {
+					if ($allItems[$k]['results']['external'] == 'Success') {
+						$pass = true;
+					}
 				}
+				$this->_healthCheckPluginUUID($v['UUID'], 'true');
 			}
-			healthCheckUUID($v['UUID'], 'true');
+			$this->setAPIResponse('success', null, 200, $allItems);
+		} else {
+			$this->setAPIResponse('error', 'User does not have access', 401);
 		}
-		return $allItems;
-	} else {
-		'No Access';
 	}
 }
-
-/* GET HEALTHCHECK SETTINGS */
-function healthCheckGetSettings()
-{
-	return array(
-		'Options' => array(
-			array(
-				'type' => 'select',
-				'name' => 'HEALTHCHECKS-Auth-include',
-				'label' => 'Minimum Authentication',
-				'value' => $GLOBALS['HEALTHCHECKS-Auth-include'],
-				'options' => groupSelect()
-			),
-			array(
-				'type' => 'input',
-				'name' => 'HEALTHCHECKS-PingURL',
-				'label' => 'URL',
-				'value' => $GLOBALS['HEALTHCHECKS-PingURL'],
-				'help' => 'URL for HealthChecks Ping',
-				'placeholder' => 'HealthChecks Ping URL'
-			),
-		),
-		'Services' => array(
-			array(
-				'type' => 'arrayMultiple',
-				'name' => 'HEALTHCHECKS-all-items',
-				'label' => 'Services',
-				'value' => $GLOBALS['HEALTHCHECKS-all-items']
-			)
-		)
-	);
-}

+ 431 - 341
api/plugins/invites.php

@@ -4,11 +4,8 @@ $GLOBALS['plugins'][]['Invites'] = array( // Plugin Name
 	'name' => 'Invites', // Plugin Name
 	'author' => 'CauseFX', // Who wrote the plugin
 	'category' => 'Management', // One to Two Word Description
-	'link' => 'https://github.com/PHPMailer/PHPMailer', // Link to plugin info
+	'link' => '', // Link to plugin info
 	'license' => 'personal', // License Type use , for multiple
-	//'fileName'=>'php-mailer.php',
-	//'configFile'=>'php-mailer.php',
-	//'apiFile'=>'php-mailer.php',
 	'idPrefix' => 'INVITES', // html element id prefix
 	'configPrefix' => 'INVITES', // config file prefix for array items without the hypen
 	'version' => '1.0.0', // SemVer of plugin
@@ -16,368 +13,461 @@ $GLOBALS['plugins'][]['Invites'] = array( // Plugin Name
 	'settings' => true, // does plugin need a settings page? true or false
 	'homepage' => false // Is plugin for use on homepage? true or false
 );
-// INCLUDE/REQUIRE FILES
-// PLUGIN FUNCTIONS
-function inviteCodes($array)
-{
-	$action = isset($array['data']['action']) ? $array['data']['action'] : null;
-	$code = isset($array['data']['code']) ? $array['data']['code'] : null;
-	$usedBy = isset($array['data']['usedby']) ? $array['data']['usedby'] : null;
-	$username = isset($array['data']['username']) ? $array['data']['username'] : null;
-	$email = isset($array['data']['email']) ? $array['data']['email'] : null;
-	$id = isset($array['data']['id']) ? $array['data']['id'] : null;
-	$now = date("Y-m-d H:i:s");
-	$currentIP = userIP();
-	switch ($action) {
-		case "check":
-			try {
-				$connect = new Dibi\Connection([
-					'driver' => 'sqlite3',
-					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-				]);
-				$all = $connect->fetch('SELECT * FROM invites WHERE valid = "Yes" AND code = ?', $code);
-				return ($all) ? true : false;
-			} catch (Dibi\Exception $e) {
-				return false;
-			}
-			break;
-		case "use":
-			try {
-				if (inviteCodes(array('data' => array('action' => 'check', 'code' => $code)))) {
-					$connect = new Dibi\Connection([
-						'driver' => 'sqlite3',
-						'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-					]);
-					$connect->query('
-	                	UPDATE invites SET', [
-						'valid' => 'No',
-						'usedby' => $usedBy,
-						'dateused' => $now,
-						'ip' => $currentIP,
-					], '
-	                	WHERE code=?', $code);
-					writeLog('success', 'Invite Management Function -  Invite Used [' . $code . ']', 'SYSTEM');
-					return inviteAction($usedBy, 'share', $GLOBALS['INVITES-type-include']);
-				} else {
-					return false;
-				}
-			} catch (Dibi\Exception $e) {
-				return false;
-			}/*
-            if(ENABLEMAIL){
-                if (!isset($GLOBALS['USER'])) {
-                    require_once("user.php");
-                    $GLOBALS['USER'] = new User('registration_callback');
-                }
-                $emailTemplate = array(
-                    'type' => 'mass',
-                    'body' => 'The user: {user} has reddemed the code: {inviteCode} his IP Address was '.$currentIP,
-                    'subject' => 'Invite Code '.$code.' Has Been Used',
-                    'user' => $usedBy,
-                    'password' => null,
-                    'inviteCode' => $code,
-                );
-                $emailTemplate = emailTemplate($emailTemplate);
-                $subject = $emailTemplate['subject'];
-                $body = buildEmail($emailTemplate);
-                sendEmail($GLOBALS['USER']->adminEmail, "Admin", $subject, $body);
-            }*/
-			break;
-		default:
-			if (qualifyRequest(1)) {
-				switch ($action) {
-					case "create":
-						try {
-							$connect = new Dibi\Connection([
-								'driver' => 'sqlite3',
-								'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-							]);
-							$newCode = [
-								'code' => $code,
-								'email' => $email,
-								'username' => $username,
-								'valid' => 'Yes',
-								'type' => $GLOBALS['INVITES-type-include'],
-							];
-							$connect->query('INSERT INTO [invites]', $newCode);
-							writeLog('success', 'Invite Management Function -  Added Invite [' . $code . ']', $GLOBALS['organizrUser']['username']);
-							if ($GLOBALS['PHPMAILER-enabled']) {
-								$emailTemplate = array(
-									'type' => 'invite',
-									'body' => $GLOBALS['PHPMAILER-emailTemplateInviteUser'],
-									'subject' => $GLOBALS['PHPMAILER-emailTemplateInviteUserSubject'],
-									'user' => $username,
-									'password' => null,
-									'inviteCode' => $code,
-								);
-								$emailTemplate = phpmEmailTemplate($emailTemplate);
-								$sendEmail = array(
-									'to' => $email,
-									'subject' => $emailTemplate['subject'],
-									'body' => phpmBuildEmail($emailTemplate),
-								);
-								phpmSendEmail($sendEmail);
-							}
-							return true;
-						} catch (Dibi\Exception $e) {
-							writeLog('error', 'Invite Management Function  -  Error [' . $e . ']', 'SYSTEM');
-							return false;
-						}
-						break;
-					case "get":
-						try {
-							$connect = new Dibi\Connection([
-								'driver' => 'sqlite3',
-								'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-							]);
-							$invites = $connect->fetchAll('SELECT * FROM invites');
-							return $invites;
-						} catch (Dibi\Exception $e) {
-							writeLog('error', 'Invite Management Function  -  Error [' . $e . ']', 'SYSTEM');
-							return false;
-						}
-						break;
-					case "delete":
-						try {
-							$connect = new Dibi\Connection([
-								'driver' => 'sqlite3',
-								'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
-							]);
-							$connect->query('DELETE FROM invites WHERE id = ?', $id);
-							return true;
-						} catch (Dibi\Exception $e) {
-							writeLog('error', 'Invite Management Function  -  Error [' . $e . ']', 'SYSTEM');
-							return false;
-						}
-						break;
-					default:
-						return false;
-				}
-			}
-	}
-}
 
-/* GET PHPMAILER SETTINGS */
-function invitesGetSettings()
+class Invites extends Organizr
 {
-	if ($GLOBALS['plexID'] !== '' && $GLOBALS['plexToken'] !== '' && $GLOBALS['INVITES-type-include'] == 'plex') {
-		$loop = libraryList($GLOBALS['INVITES-type-include'])['libraries'];
-		foreach ($loop as $key => $value) {
-			$libraryList[] = array(
-				'name' => $key,
-				'value' => $value
-			);
-		}
-	} else {
-		$libraryList = array(
+	public function _invitesPluginGetCodes()
+	{
+		$response = [
 			array(
-				'name' => 'Refresh page to update List',
-				'value' => '',
-				'disabled' => true,
-			),
-		);
+				'function' => 'fetchAll',
+				'query' => 'SELECT * FROM invites'
+			)
+		];
+		return $this->processQueries($response);
 	}
-	return array(
-		'Backend' => array(
+	
+	public function _invitesPluginCreateCode($array)
+	{
+		$code = ($array['code']) ?? null;
+		$username = ($array['username']) ?? null;
+		$email = ($array['email']) ?? null;
+		if (!$code) {
+			$this->setAPIResponse('error', 'Code not supplied', 409);
+			return false;
+		}
+		if (!$username) {
+			$this->setAPIResponse('error', 'Username not supplied', 409);
+			return false;
+		}
+		if (!$email) {
+			$this->setAPIResponse('error', 'Email not supplied', 409);
+			return false;
+		}
+		$newCode = [
+			'code' => $code,
+			'email' => $email,
+			'username' => $username,
+			'valid' => 'Yes',
+			'type' => $this->config['INVITES-type-include'],
+		];
+		$response = [
 			array(
-				'type' => 'select',
-				'name' => 'INVITES-type-include',
-				'label' => 'Media Server',
-				'value' => $GLOBALS['INVITES-type-include'],
-				'options' => array(
-					array(
-						'name' => 'N/A',
-						'value' => 'n/a'
-					),
-					array(
-						'name' => 'Plex',
-						'value' => 'plex'
-					),
-					array(
-						'name' => 'Emby',
-						'value' => 'emby'
-					)
+				'function' => 'query',
+				'query' => array(
+					'INSERT INTO [invites]',
+					$newCode
 				)
 			)
-		),
-		'Plex Settings' => array(
-			array(
-				'type' => 'password-alt',
-				'name' => 'plexToken',
-				'label' => 'Plex Token',
-				'value' => $GLOBALS['plexToken'],
-				'placeholder' => 'Use Get Token Button'
-			),
-			array(
-				'type' => 'password-alt',
-				'name' => 'plexID',
-				'label' => 'Plex Machine',
-				'value' => $GLOBALS['plexID'],
-				'placeholder' => 'Use Get Plex Machine Button'
-			),
-			array(
-				'type' => 'select2',
-				'class' => 'select2-multiple',
-				'id' => 'invite-select',
-				'name' => 'INVITES-plexLibraries',
-				'label' => 'Libraries',
-				'value' => $GLOBALS['INVITES-plexLibraries'],
-				'options' => $libraryList
-			),
-			array(
-				'type' => 'text',
-				'name' => 'INVITES-plex-tv-labels',
-				'label' => 'TV Labels (comma separated)',
-				'value' => $GLOBALS['INVITES-plex-tv-labels'],
-				'placeholder' => 'All'
-			),
-			array(
-				'type' => 'text',
-				'name' => 'INVITES-plex-movies-labels',
-				'label' => 'Movies Labels (comma separated)',
-				'value' => $GLOBALS['INVITES-plex-movies-labels'],
-				'placeholder' => 'All'
-			),
-			array(
-				'type' => 'text',
-				'name' => 'INVITES-plex-music-labels',
-				'label' => 'Music Labels (comma separated)',
-				'value' => $GLOBALS['INVITES-plex-music-labels'],
-				'placeholder' => 'All'
-			),
-		),
-		'Emby Settings' => array(
-			array(
-				'type' => 'password-alt',
-				'name' => 'embyToken',
-				'label' => 'Emby API key',
-				'value' => $GLOBALS['embyToken'],
-				'placeholder' => 'enter key from emby'
-			),
+		];
+		$query = $this->processQueries($response);
+		if ($query) {
+			$this->writeLog('success', 'Invite Management Function -  Added Invite [' . $code . ']', $this->user['username']);
+			if ($this->config['PHPMAILER-enabled']) {
+				$PhpMailer = new PhpMailer();
+				$emailTemplate = array(
+					'type' => 'invite',
+					'body' => $this->config['PHPMAILER-emailTemplateInviteUser'],
+					'subject' => $this->config['PHPMAILER-emailTemplateInviteUserSubject'],
+					'user' => $username,
+					'password' => null,
+					'inviteCode' => $code,
+				);
+				$emailTemplate = $PhpMailer->_phpMailerPluginEmailTemplate($emailTemplate);
+				$sendEmail = array(
+					'to' => $email,
+					'subject' => $emailTemplate['subject'],
+					'body' => $PhpMailer->_phpMailerPluginBuildEmail($emailTemplate),
+				);
+				$PhpMailer->_phpMailerPluginSendEmail($sendEmail);
+			}
+			$this->setAPIResponse('success', 'Invite Code: ' . $code . ' has been created', 200);
+			return true;
+		} else {
+			return false;
+		}
+		
+	}
+	
+	public function _invitesPluginVerifyCode($code)
+	{
+		$response = [
 			array(
-				'type' => 'text',
-				'name' => 'embyURL',
-				'label' => 'Emby server adress',
-				'value' => $GLOBALS['embyURL'],
-				'placeholder' => 'localhost:8086'
-			),
+				'function' => 'fetchAll',
+				'query' => array(
+					'SELECT * FROM invites WHERE valid = "Yes" AND code = ? COLLATE NOCASE',
+					$code
+				)
+			)
+		];
+		if ($this->processQueries($response)) {
+			$this->setAPIResponse('success', 'Code has been verified', 200);
+			return true;
+		} else {
+			$this->setAPIResponse('error', 'Code is invalid', 401);
+			return false;
+		}
+	}
+	
+	public function _invitesPluginDeleteCode($code)
+	{
+		$response = [
 			array(
-				'type' => 'text',
-				'name' => 'INVITES-EmbyTemplate',
-				'label' => 'Emby User to be used as template for new users',
-				'value' => $GLOBALS['INVITES-EmbyTemplate'],
-				'placeholder' => 'AdamSmith'
+				'function' => 'fetchAll',
+				'query' => 'SELECT * FROM invites WHERE code = ? COLLATE NOCASE',
+				$code
 			)
-		),
-		'FYI' => array(
+		];
+		$info = $this->processQueries($response);
+		if (!$info) {
+			$this->setAPIResponse('error', 'Code not found', 404);
+			return false;
+		}
+		$response = [
 			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'
+				'function' => 'query',
+				'query' => array(
+					'DELETE FROM invites WHERE code = ? COLLATE NOCASE',
+					$code
+				)
 			)
-		)
-	);
-}
-
-function inviteAction($username, $action = null, $type = null)
-{
-	if ($action == null) {
-		return false;
+		];
+		$this->setAPIResponse('success', 'Code has been deleted', 200);
+		return $this->processQueries($response);
+		
 	}
-	switch ($type) {
-		case 'plex':
-			if (!empty($GLOBALS['plexToken']) && !empty($GLOBALS['plexID'])) {
-				$url = "https://plex.tv/api/servers/" . $GLOBALS['plexID'] . "/shared_servers/";
-				if ($GLOBALS['INVITES-plexLibraries'] !== "") {
-					$libraries = explode(',', $GLOBALS['INVITES-plexLibraries']);
-				} else {
-					$libraries = '';
+	
+	public function _invitesPluginUseCode($code, $array)
+	{
+		$code = ($code) ?? null;
+		$usedBy = ($array['usedby']) ?? null;
+		$now = date("Y-m-d H:i:s");
+		$currentIP = $this->userIP();
+		if ($this->_invitesPluginVerifyCode($code)) {
+			$updateCode = [
+				'valid' => 'No',
+				'usedby' => $usedBy,
+				'dateused' => $now,
+				'ip' => $currentIP
+			];
+			$response = [
+				array(
+					'function' => 'query',
+					'query' => array(
+						'UPDATE invites SET',
+						$updateCode,
+						'WHERE code=? COLLATE NOCASE',
+						$code
+					)
+				)
+			];
+			$query = $this->processQueries($response);
+			$this->writeLog('success', 'Invite Management Function -  Invite Used [' . $code . ']', 'SYSTEM');
+			return $this->_invitesPluginAction($usedBy, 'share', $this->config['INVITES-type-include']);
+		} else {
+			return false;
+		}
+	}
+	
+	public function _invitesPluginLibraryList($type = null)
+	{
+		switch ($type) {
+			case 'plex':
+				if (!empty($this->config['plexToken']) && !empty($this->config['plexID'])) {
+					$url = 'https://plex.tv/api/servers/' . $this->config['plexID'];
+					try {
+						$headers = array(
+							"Accept" => "application/json",
+							"X-Plex-Token" => $this->config['plexToken']
+						);
+						$response = Requests::get($url, $headers, array());
+						libxml_use_internal_errors(true);
+						if ($response->success) {
+							$libraryList = array();
+							$plex = simplexml_load_string($response->body);
+							foreach ($plex->Server->Section as $child) {
+								$libraryList['libraries'][(string)$child['title']] = (string)$child['id'];
+							}
+							$libraryList = array_change_key_case($libraryList, CASE_LOWER);
+							return $libraryList;
+						}
+					} catch (Requests_Exception $e) {
+						$this->writeLog('error', 'Plex Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+						return false;
+					};
 				}
-				if ($GLOBALS['INVITES-plex-tv-labels'] !== "") {
-					$tv_labels = "label=" . $GLOBALS['INVITES-plex-tv-labels'];
-				} else { $tv_labels = ""; }
-				if ($GLOBALS['INVITES-plex-movies-labels'] !== "") {
-					$movies_labels = "label=" . $GLOBALS['INVITES-plex-movies-labels'];
-				} else { $movies_labels = ""; }
-				if ($GLOBALS['INVITES-plex-music-labels'] !== "") {
-					$music_labels = "label=" . $GLOBALS['INVITES-plex-music-labels'];
-				} else { $music_labels = ""; }
-				
-				$headers = array(
-					"Accept" => "application/json",
-					"Content-Type" => "application/json",
-					"X-Plex-Token" => $GLOBALS['plexToken']
+				break;
+			default:
+				# code...
+				break;
+		}
+		return false;
+	}
+	
+	public function _invitesPluginGetSettings()
+	{
+		if ($this->config['plexID'] !== '' && $this->config['plexToken'] !== '' && $this->config['INVITES-type-include'] == 'plex') {
+			$loop = $this->_invitesPluginLibraryList($this->config['INVITES-type-include'])['libraries'];
+			foreach ($loop as $key => $value) {
+				$libraryList[] = array(
+					'name' => $key,
+					'value' => $value
 				);
-				$data = array(
-					"server_id" => $GLOBALS['plexID'],
-					"shared_server" => array(
-						"library_section_ids" => $libraries,
-						"invited_email" => $username
-					),
-					"sharing_settings" => array(
-						"filterTelevision" => $tv_labels,
-						"filterMovies" => $movies_labels,
-						"filterMusic" => $music_labels
+			}
+		} else {
+			$libraryList = array(
+				array(
+					'name' => 'Refresh page to update List',
+					'value' => '',
+					'disabled' => true,
+				),
+			);
+		}
+		return array(
+			'Backend' => array(
+				array(
+					'type' => 'select',
+					'name' => 'INVITES-type-include',
+					'label' => 'Media Server',
+					'value' => $this->config['INVITES-type-include'],
+					'options' => array(
+						array(
+							'name' => 'N/A',
+							'value' => 'n/a'
+						),
+						array(
+							'name' => 'Plex',
+							'value' => 'plex'
+						),
+						array(
+							'name' => 'Emby',
+							'value' => 'emby'
+						)
 					)
-				);
-				try {
-					switch ($action) {
-						case 'share':
-							$response = Requests::post($url, $headers, json_encode($data), array());
-							break;
-						case 'unshare':
-							$id = (is_numeric($username) ? $username : convertPlexName($username, "id"));
-							$url = $url . $id;
-							$response = Requests::delete($url, $headers, array());
-							break;
-						default:
-							return false;
-							break;
+				)
+			),
+			'Plex Settings' => array(
+				array(
+					'type' => 'password-alt',
+					'name' => 'plexToken',
+					'label' => 'Plex Token',
+					'value' => $this->config['plexToken'],
+					'placeholder' => 'Use Get Token Button'
+				),
+				array(
+					'type' => 'button',
+					'label' => 'Get Plex Token',
+					'icon' => 'fa fa-ticket',
+					'text' => 'Retrieve',
+					'attr' => 'onclick="showPlexTokenForm(\'#INVITES-settings-items [name=plexToken]\')"'
+				),
+				array(
+					'type' => 'password-alt',
+					'name' => 'plexID',
+					'label' => 'Plex Machine',
+					'value' => $this->config['plexID'],
+					'placeholder' => 'Use Get Plex Machine Button'
+				),
+				array(
+					'type' => 'button',
+					'label' => 'Get Plex Machine',
+					'icon' => 'fa fa-id-badge',
+					'text' => 'Retrieve',
+					'attr' => 'onclick="showPlexMachineForm(\'#INVITES-settings-items [name=plexID]\')"'
+				),
+				array(
+					'type' => 'select2',
+					'class' => 'select2-multiple',
+					'id' => 'invite-select',
+					'name' => 'INVITES-plexLibraries',
+					'label' => 'Libraries',
+					'value' => $this->config['INVITES-plexLibraries'],
+					'options' => $libraryList
+				),
+				array(
+					'type' => 'text',
+					'name' => 'INVITES-plex-tv-labels',
+					'label' => 'TV Labels (comma separated)',
+					'value' => $this->config['INVITES-plex-tv-labels'],
+					'placeholder' => 'All'
+				),
+				array(
+					'type' => 'text',
+					'name' => 'INVITES-plex-movies-labels',
+					'label' => 'Movies Labels (comma separated)',
+					'value' => $this->config['INVITES-plex-movies-labels'],
+					'placeholder' => 'All'
+				),
+				array(
+					'type' => 'text',
+					'name' => 'INVITES-plex-music-labels',
+					'label' => 'Music Labels (comma separated)',
+					'value' => $this->config['INVITES-plex-music-labels'],
+					'placeholder' => 'All'
+				),
+			),
+			'Emby Settings' => array(
+				array(
+					'type' => 'password-alt',
+					'name' => 'embyToken',
+					'label' => 'Emby API key',
+					'value' => $this->config['embyToken'],
+					'placeholder' => 'enter key from emby'
+				),
+				array(
+					'type' => 'text',
+					'name' => 'embyURL',
+					'label' => 'Emby server adress',
+					'value' => $this->config['embyURL'],
+					'placeholder' => 'localhost:8086'
+				),
+				array(
+					'type' => 'text',
+					'name' => 'INVITES-EmbyTemplate',
+					'label' => 'Emby User to be used as template for new users',
+					'value' => $this->config['INVITES-EmbyTemplate'],
+					'placeholder' => 'AdamSmith'
+				)
+			),
+			'FYI' => array(
+				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'
+				)
+			)
+		);
+	}
+	
+	public function _invitesPluginAction($username, $action = null, $type = null)
+	{
+		if ($action == null) {
+			$this->setAPIResponse('error', 'No Action supplied', 409);
+			return false;
+		}
+		switch ($type) {
+			case 'plex':
+				if (!empty($this->config['plexToken']) && !empty($this->config['plexID'])) {
+					$url = "https://plex.tv/api/servers/" . $this->config['plexID'] . "/shared_servers/";
+					if ($this->config['INVITES-plexLibraries'] !== "") {
+						$libraries = explode(',', $this->config['INVITES-plexLibraries']);
+					} else {
+						$libraries = '';
 					}
-					if ($response->success) {
-						writeLog('success', 'Plex Invite Function - Plex User now has access to system', $username);
-						return true;
+					if ($this->config['INVITES-plex-tv-labels'] !== "") {
+						$tv_labels = "label=" . $this->config['INVITES-plex-tv-labels'];
 					} else {
-						switch ($response->status_code) {
-							case 400:
-								writeLog('error', 'Plex Invite Function - Plex User already has access', $username);
-								return false;
-								break;
-							case 401:
-								writeLog('error', 'Plex Invite Function - Incorrect Token', 'SYSTEM');
-								return false;
+						$tv_labels = "";
+					}
+					if ($this->config['INVITES-plex-movies-labels'] !== "") {
+						$movies_labels = "label=" . $this->config['INVITES-plex-movies-labels'];
+					} else {
+						$movies_labels = "";
+					}
+					if ($this->config['INVITES-plex-music-labels'] !== "") {
+						$music_labels = "label=" . $this->config['INVITES-plex-music-labels'];
+					} else {
+						$music_labels = "";
+					}
+					$headers = array(
+						"Accept" => "application/json",
+						"Content-Type" => "application/json",
+						"X-Plex-Token" => $this->config['plexToken']
+					);
+					$data = array(
+						"server_id" => $this->config['plexID'],
+						"shared_server" => array(
+							"library_section_ids" => $libraries,
+							"invited_email" => $username
+						),
+						"sharing_settings" => array(
+							"filterTelevision" => $tv_labels,
+							"filterMovies" => $movies_labels,
+							"filterMusic" => $music_labels
+						)
+					);
+					try {
+						switch ($action) {
+							case 'share':
+								$response = Requests::post($url, $headers, json_encode($data), array());
 								break;
-							case 404:
-								writeLog('error', 'Plex Invite Function - Libraries not setup correct [' . $GLOBALS['INVITES-plexLibraries'] . ']', 'SYSTEM');
-								return false;
+							case 'unshare':
+								$id = (is_numeric($username) ? $username : $this->_invitesPluginConvertPlexName($username, "id"));
+								$url = $url . $id;
+								$response = Requests::delete($url, $headers, array());
 								break;
 							default:
-								writeLog('error', 'Plex Invite Function - An error occurred [' . $response->status_code . ']', $username);
+								$this->setAPIResponse('error', 'No Action supplied', 409);
 								return false;
-								break;
 						}
-					}
+						if ($response->success) {
+							$this->writeLog('success', 'Plex Invite Function - Plex User now has access to system', $username);
+							$this->setAPIResponse('success', 'Plex User now has access to system', 200);
+							return true;
+						} else {
+							switch ($response->status_code) {
+								case 400:
+									$this->writeLog('error', 'Plex Invite Function - Plex User already has access', $username);
+									$this->setAPIResponse('error', 'Plex User already has access', 409);
+									return false;
+								case 401:
+									$this->writeLog('error', 'Plex Invite Function - Incorrect Token', 'SYSTEM');
+									$this->setAPIResponse('error', 'Incorrect Token', 409);
+									return false;
+								case 404:
+									$this->writeLog('error', 'Plex Invite Function - Libraries not setup correct [' . $this->config['INVITES-plexLibraries'] . ']', 'SYSTEM');
+									$this->setAPIResponse('error', 'Libraries not setup correct', 409);
+									return false;
+								default:
+									$this->writeLog('error', 'Plex Invite Function - An error occurred [' . $response->status_code . ']', $username);
+									$this->setAPIResponse('error', 'An Error Occurred', 409);
+									return false;
+							}
+						}
+					} catch (Requests_Exception $e) {
+						$this->writeLog('error', 'Plex Invite Function - Error: ' . $e->getMessage(), 'SYSTEM');
+						$this->setAPIResponse('error', $e->getMessage(), 409);
+						return false;
+					};
+				} else {
+					$this->writeLog('error', 'Plex Invite Function - Plex Token/ID not set', 'SYSTEM');
+					$this->setAPIResponse('error', 'Plex Token/ID not set', 409);
+					return false;
+				}
+				break;
+			case 'emby':
+				try {
+					#add emby user to system
+					$this->setAPIResponse('success', 'User now has access to system', 200);
+					return true;
 				} catch (Requests_Exception $e) {
-					writeLog('error', 'Plex Invite Function - Error: ' . $e->getMessage(), 'SYSTEM');
+					$this->writeLog('error', 'Emby Invite Function - Error: ' . $e->getMessage(), 'SYSTEM');
+					$this->setAPIResponse('error', $e->getMessage(), 409);
 					return false;
-				};
-			} else {
-				writeLog('error', 'Plex Invite Function - Plex Token/ID not set', 'SYSTEM');
-				return false;
-			}
-			break;
-		case 'emby':
-			try {
-				#add emby user to sytem
-				return true;
-			} catch (Requests_Exception $e) {
-				writeLog('error', 'Emby Invite Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				}
+			default:
 				return false;
-			}
-			break;
-		default:
-			return false;
-			break;
+		}
+		return false;
+	}
+	
+	public function _invitesPluginConvertPlexName($user, $type)
+	{
+		$array = $this->userList('plex');
+		switch ($type) {
+			case "username":
+			case "u":
+				$plexUser = array_search($user, $array['users']);
+				break;
+			case "id":
+				if (array_key_exists(strtolower($user), $array['users'])) {
+					$plexUser = $array['users'][strtolower($user)];
+				}
+				break;
+			default:
+				$plexUser = false;
+		}
+		return (!empty($plexUser) ? $plexUser : null);
 	}
-	return false;
+	
 }

+ 7 - 14
api/plugins/js/chat.js

@@ -65,8 +65,8 @@ function chatLaunch(){
                     });
                 // check if the user is subscribed to the above channel
                 channel.bind('pusher:subscription_succeeded', function(members) {
-                    console.log('Chat Websocket Connected!');
-                    console.log('Connecting to Organizr Chat DB');
+	                organizrConsole('Plugin Function','Chat Websocket Connected!');
+	                organizrConsole('Plugin Function','Connecting to Organizr Chat DB');
                     getMessagesAndUsers(activeInfo.settings.homepage.refresh["CHAT-userRefreshTimeout"], true);
                 });
                 /*jslint browser: true*/
@@ -97,12 +97,9 @@ function chatLaunch(){
     }
 }
 $(document).on('click', '#CHAT-settings-button', function() {
-    var post = {
-        plugin:'chat/settings/get', // used for switch case in your API call
-    };
     ajaxloader(".content-wrap","in");
-    organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
-        var response = JSON.parse(data);
+	organizrAPI2('GET','api/v2/plugins/chat/settings').success(function(data) {
+        var response = data.response;
         $('#CHAT-settings-items').html(buildFormGroup(response.data));
     }).fail(function(xhr) {
         console.error("Organizr Function: API Connection Failed");
@@ -123,11 +120,7 @@ $('body').on('click', '.custom-send-button', function(e) {
     var message = $('.chat-input-send').val();
     // Validate Name field
     if (message !== '') {
-        var post = {
-            plugin:'chat/message',
-            message:message
-        };
-        organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
+        organizrAPI2('POST','api/v2/plugins/chat/message',{ message : message }).success(function(data) {
             // Nada yet
         }).fail(function(xhr) {
             console.error("Organizr Function: API Connection Failed");
@@ -187,8 +180,8 @@ function chatEntry(){
 }
 function getMessagesAndUsers(timeout, initial = false){
     var timeout = (typeof timeout !== 'undefined') ? timeout : activeInfo.settings.homepage.refresh["CHAT-userRefreshTimeout"];
-    organizrAPI('GET','api/?v1/plugin&plugin=chat&cmd=chat/message').success(function(data) {
-        var response = JSON.parse(data);
+    organizrAPI2('GET','api/v2/plugins/chat/message').success(function(data) {
+        var response = data.response;
         if(initial == true){
             $.each(response.data, function (i, v){
                 $('.chat-list').append(formatMessage(v));

+ 3 - 24
api/plugins/js/healthChecks.js

@@ -1,22 +1,4 @@
-/* PHP MAILER JS FILE */
-/*
-$(document).on('click', '#PHPMAILER-settings-button', function() {
-	var post = {
-        plugin:'PHPMailer/settings/get', // used for switch case in your API call
-        api:'api/?v1/plugin', // API Endpoint will always be this for custom plugin API calls
-        name:$(this).attr('data-plugin-name'),
-        configName:$(this).attr('data-config-name'),
-        messageTitle:'', // Send succees message title (top line)
-        messageBody:'Disabled '+$(this).attr('data-plugin-name'), // Send succees message body (bottom line)
-        error:'Organizr Function: API Connection Failed' // conole error message
-    };
-	var callbacks = $.Callbacks(); // init callbacks var
-    //callbacks.add(  ); // add function to callback to be fired after API call
-    //settingsAPI(post,callbacks); // exec API call
-    //ajaxloader(".content-wrap","in");
-    //setTimeout(function(){ buildPlugins();ajaxloader(); }, 3000);
-});
-*/
+/* HEALTHCHECKS.IO JS FILE */
 
 // FUNCTIONS
 
@@ -25,12 +7,9 @@ $(document).on('click', '#PHPMAILER-settings-button', function() {
 // CHANGE CUSTOMIZE Options
 //
 $(document).on('click', '#HEALTHCHECKS-settings-button', function() {
-    var post = {
-        plugin:'HealthChecks/settings/get', // used for switch case in your API call
-    };
     ajaxloader(".content-wrap","in");
-    organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
-        var response = JSON.parse(data);
+    organizrAPI2('GET','api/v2/plugins/healthchecks/settings').success(function(data) {
+        var response = data.response;
         $('#HEALTHCHECKS-settings-items').html(buildFormGroup(response.data));
         var elAddButtonStart = $('#HEALTHCHECKS-settings-page [id*="Services"] .row.start');
         var testone = $('#HEALTHCHECKS-settings-page [id*="Services"] .row.m-b-40').first('span')

+ 33 - 111
api/plugins/js/invites.js

@@ -1,23 +1,4 @@
-/* PHP MAILER JS FILE */
-/*
-$(document).on('click', '#PHPMAILER-settings-button', function() {
-	var post = {
-        plugin:'PHPMailer/settings/get', // used for switch case in your API call
-        api:'api/?v1/plugin', // API Endpoint will always be this for custom plugin API calls
-        name:$(this).attr('data-plugin-name'),
-        configName:$(this).attr('data-config-name'),
-        messageTitle:'', // Send succees message title (top line)
-        messageBody:'Disabled '+$(this).attr('data-plugin-name'), // Send succees message body (bottom line)
-        error:'Organizr Function: API Connection Failed' // conole error message
-    };
-	var callbacks = $.Callbacks(); // init callbacks var
-    //callbacks.add(  ); // add function to callback to be fired after API call
-    //settingsAPI(post,callbacks); // exec API call
-    //ajaxloader(".content-wrap","in");
-    //setTimeout(function(){ buildPlugins();ajaxloader(); }, 3000);
-});
-*/
-
+/* INVITES JS FILE */
 // FUNCTIONS
 inviteLaunch()
 function inviteLaunch(){
@@ -92,19 +73,19 @@ function joinPlex(){
         message('Invite Error',' Please Enter Email',activeInfo.settings.notifications.position,'#FFF','warning','5000');
     }else if(password.val() == ''){
         password.focus();
-        message('Invite Error',' Please Enter Passowrd',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+        message('Invite Error',' Please Enter Password',activeInfo.settings.notifications.position,'#FFF','warning','5000');
     }
     if(email.val() !== '' && username.val() !== '' && password.val() !== ''){
-        organizrAPI('POST','api/?v1/plex/join',{username:username.val(), email:email.val(), password:password.val()}).success(function(data) {
-    		var response = JSON.parse(data);
-            if(response.data === true){
+        organizrAPI2('POST','api/v2/plex/register',{username:username.val(), email:email.val(), password:password.val()}).success(function(data) {
+    		var response = data.response;
+            if(response.result === 'success'){
                 $('.invite-step-3-plex-no').toggleClass('hidden');
                 $('.invite-step-3-plex-yes').toggleClass('hidden');
                 message('Invite Function',' User Created',activeInfo.settings.notifications.position,'#FFF','success','5000');
                 $('#inviteUsernameInvite').val(username.val());
                 hasPlexUsername();
             }else{
-                message('Invite Error',' '+response.data,activeInfo.settings.notifications.position,'#FFF','warning','5000');
+                message('Invite Error',' '+response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
             }
     	}).fail(function(xhr) {
     		console.error("Organizr Function: API Connection Failed");
@@ -124,19 +105,19 @@ function joinEmby(){
         message('Invite Error',' Please Enter Email',activeInfo.settings.notifications.position,'#FFF','warning','5000');
     }else if(password.val() == ''){
         password.focus();
-        message('Invite Error',' Please Enter Passowrd',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+        message('Invite Error',' Please Enter Password',activeInfo.settings.notifications.position,'#FFF','warning','5000');
     }
     if(email.val() !== '' && username.val() !== '' && password.val() !== ''){
-        organizrAPI('POST','api/?v1/emby/join',{username:username.val(), email:email.val(), password:password.val()}).success(function(data) {
-    		var response = JSON.parse(data);
-            if(response.data === true){
+        organizrAPI2('POST','api/v2/emby/register',{username:username.val(), email:email.val(), password:password.val()}).success(function(data) {
+    		var response = data.response;
+            if(response.result === 'success'){
                 $('.invite-step-3-emby-no').toggleClass('hidden');
                 $('.invite-step-3-emby-yes').toggleClass('hidden');
                 message('Invite Function',' User Created',activeInfo.settings.notifications.position,'#FFF','success','5000');
                 $('#inviteUsernameInviteEmby').val(username.val());
                 hasEmbyUsername();
             }else{
-                message('Invite Error',' '+response.data,activeInfo.settings.notifications.position,'#FFF','warning','5000');
+                message('Invite Error',' '+response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
             }
     	}).fail(function(xhr) {
     		console.error("Organizr Function: API Connection Failed");
@@ -176,22 +157,19 @@ function hasPlexUsername(){
         message('Invite Error',' Please Enter Username',activeInfo.settings.notifications.position,'#FFF','warning','5000');
     }else{
         var post = {
-            plugin:'Invites/codes',
-            action:'use',
-            code:code,
             usedby:username.val()
         };
         ajaxloader(".content-wrap","in");
-        organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
-            var response = JSON.parse(data);
-            if(response.data === true){
+        organizrAPI2('POST','api/v2/plugins/invites/' + code,post).success(function(data) {
+            var response = data.response;
+            if(response.result === 'success'){
                 $('.invite-step-3-plex-yes').toggleClass('hidden');
                 $('.invite-step-4-plex-accept').toggleClass('hidden');
                 if(local('get', 'invite')){
             		local('remove', 'invite');
             	}
             }else{
-                message('Invite Error',' Code Incorrect',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+                message('Invite Error',response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
             }
             ajaxloader();;
         }).fail(function(xhr) {
@@ -208,22 +186,19 @@ function hasEmbyUsername(){
         message('Invite Error',' Please Enter Username',activeInfo.settings.notifications.position,'#FFF','warning','5000');
     }else{
         var post = {
-            plugin:'Invites/codes',
-            action:'use',
-            code:code,
             usedby:username.val()
         };
         ajaxloader(".content-wrap","in");
-        organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
-            var response = JSON.parse(data);
-            if(response.data === true){
+        organizrAPI2('POST','api/v2/plugins/invites/' + code,post).success(function(data) {
+	        var response = data.response;
+	        if(response.result === 'success'){
                 $('.invite-step-3-emby-yes').toggleClass('hidden');
                 $('.invite-step-4-emby-accept').toggleClass('hidden');
                 if(local('get', 'invite')){
             		local('remove', 'invite');
             	}
             }else{
-                message('Invite Error',' Code Incorrect',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+                message('Invite Error',response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
             }
             ajaxloader();;
         }).fail(function(xhr) {
@@ -234,19 +209,14 @@ function hasEmbyUsername(){
 }
 function verifyInvite(){
     var code = $('#inviteCodeInput').val().toUpperCase();
-    var post = {
-        plugin:'Invites/codes',
-        action:'check',
-        code:code
-    };
     ajaxloader(".content-wrap","in");
-    organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
-        var response = JSON.parse(data);
-        if(response.data === true){
+    organizrAPI2('GET','api/v2/plugins/invites/'+code).success(function(data) {
+        var response = data.response;
+        if(response.result === 'success'){
             $('.invite-step-1').toggleClass('hidden');
             $('.invite-step-2').toggleClass('hidden');
         }else{
-            message('Invite Error',' Code Incorrect',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+            message('Invite Error',response.message,activeInfo.settings.notifications.position,'#FFF','warning','5000');
         }
         if(local('get', 'invite')){
             local('remove', 'invite');
@@ -286,15 +256,13 @@ function createNewInvite(){
 
     if(email.val() !== '' && username.val() !== ''){
         var post = {
-            plugin:'Invites/codes',
-            action:'create',
             code:createRandomString(6).toUpperCase(),
             email:email.val(),
             username:username.val(),
         };
         ajaxloader(".content-wrap","in");
-        organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
-            var response = JSON.parse(data);
+        organizrAPI2('POST','api/v2/plugins/invites',post).success(function(data) {
+            var response = data.response;
             $.magnificPopup.close();
             ajaxloader();
             message('Invite',' Invite Created',activeInfo.settings.notifications.position,'#FFF','success','5000');
@@ -306,15 +274,10 @@ function createNewInvite(){
     }
 
 }
-function deleteInvite(id){
-    var post = {
-        plugin:'Invites/codes',
-        action:'delete',
-        id:id,
-    };
+function deleteInvite(code, id){
     ajaxloader(".content-wrap","in");
-    organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
-        var response = JSON.parse(data);
+    organizrAPI2('DELETE','api/v2/plugins/invites/' + code).success(function(data) {
+        var response = data.response;
         $('#inviteItem-'+id).remove();
         //$.magnificPopup.close();
         ajaxloader();
@@ -347,7 +310,7 @@ function buildInvites(array){
             <td>`+v.usedby+`</td>
             <td>`+v.ip+`</td>
             <td>`+v.valid+`</td>
-            <td><button type="button" class="btn btn-danger btn-outline btn-circle btn-lg m-r-5" onclick="deleteInvite('`+v.id+`');"><i class="ti-trash"></i></button></td>
+            <td><button type="button" class="btn btn-danger btn-outline btn-circle btn-lg m-r-5" onclick="deleteInvite('`+v.code+`','`+v.id+`');"><i class="ti-trash"></i></button></td>
         </tr>
         `;
     });
@@ -356,13 +319,9 @@ function buildInvites(array){
 $(document).on('click', '.inviteModal', function() {
     var htmlDOM = '';
     if (activeInfo.user.loggedin === true && activeInfo.user.groupID <= 1) {
-        var post = {
-            plugin:'Invites/codes',
-            action:'get',
-        };
         ajaxloader(".content-wrap","in");
-        organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
-            var response = JSON.parse(data);
+        organizrAPI2('GET','api/v2/plugins/invites').success(function(data) {
+            var response = data.response;
             var htmlDOM = '';
             htmlDOM = `
             <div class="col-md-12">
@@ -496,47 +455,10 @@ $(document).on('click', '.inviteModal', function() {
     }
 });
 
-// CHANGE CUSTOMIZE Options
-$(document).on('change asColorPicker::close', '#INVITES-settings-page1 :input', function(e) {
-    var input = $(this);
-    switch ($(this).attr('type')) {
-        case 'switch':
-        case 'checkbox':
-            var value = $(this).prop("checked") ? true : false;
-            break;
-        default:
-            var value = $(this).val().toString();
-    }
-	var post = {
-        api:'api/?v1/update/config',
-        name:$(this).attr("name"),
-        type:$(this).attr("data-type"),
-        value:value,
-        messageTitle:'',
-        messageBody:'Updated Value for '+$(this).parent().parent().find('label').text(),
-        error:'Organizr Function: API Connection Failed'
-    };
-	var callbacks = $.Callbacks();
-    //callbacks.add( buildCustomizeAppearance );
-    settingsAPI(post,callbacks);
-    //disable button then renable
-    $('#INVITES-settings-page :input').prop('disabled', 'true');
-    setTimeout(
-        function(){
-            $('#INVITES-settings-page :input').prop('disabled', null);
-            input.emulateTab();
-        },
-        2000
-    );
-
-});
 $(document).on('click', '#INVITES-settings-button', function() {
-    var post = {
-        plugin:'Invites/settings/get', // used for switch case in your API call
-    };
     ajaxloader(".content-wrap","in");
-    organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
-        var response = JSON.parse(data);
+    organizrAPI2('GET','api/v2/plugins/invites/settings').success(function(data) {
+        var response = data.response;
         $('#INVITES-settings-items').html(buildFormGroup(response.data));
         $('.selectpicker').selectpicker();
     }).fail(function(xhr) {

+ 23 - 82
api/plugins/js/php-mailer.js

@@ -1,22 +1,4 @@
 /* PHP MAILER JS FILE */
-/*
-$(document).on('click', '#PHPMAILER-settings-button', function() {
-	var post = {
-        plugin:'PHPMailer/settings/get', // used for switch case in your API call
-        api:'api/?v1/plugin', // API Endpoint will always be this for custom plugin API calls
-        name:$(this).attr('data-plugin-name'),
-        configName:$(this).attr('data-config-name'),
-        messageTitle:'', // Send succees message title (top line)
-        messageBody:'Disabled '+$(this).attr('data-plugin-name'), // Send succees message body (bottom line)
-        error:'Organizr Function: API Connection Failed' // conole error message
-    };
-	var callbacks = $.Callbacks(); // init callbacks var
-    //callbacks.add(  ); // add function to callback to be fired after API call
-    //settingsAPI(post,callbacks); // exec API call
-    //ajaxloader(".content-wrap","in");
-    //setTimeout(function(){ buildPlugins();ajaxloader(); }, 3000);
-});
-*/
 
 // FUNCTIONS
 phpmLaunch();
@@ -53,25 +35,26 @@ function sendMail(){
         messageSingle('','Please Enter Subject',activeInfo.settings.notifications.position,'#FFF','error','5000');
     }else if(body == ''){
         messageSingle('','Please Enter Body',activeInfo.settings.notifications.position,'#FFF','error','5000');
+    }else{
+	    messageSingle('','Sending Message',activeInfo.settings.notifications.position,'#FFF','success','5000');
     }
     if(to !== '' && subject !== '' && body !== ''){
         var post = {
-            plugin:'PHPMailer/send/email', // used for switch case in your API call
             bcc:to,
             subject:subject,
             body:body
         };
         ajaxloader(".content-wrap","in");
-        organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
-            var response = JSON.parse(data);
-            if(response.data == true){
+        organizrAPI2('POST','api/v2/plugins/php-mailer/email/send',post).success(function(data) {
+            var response = data.response;
+            if(response.result == 'success'){
                 $.magnificPopup.close();
                 messageSingle('',window.lang.translate('Email Sent Successful'),activeInfo.settings.notifications.position,'#FFF','success','5000');
             }else{
-                messageSingle('',response.data,activeInfo.settings.notifications.position,'#FFF','error','5000');
+                messageSingle('',response.message,activeInfo.settings.notifications.position,'#FFF','error','5000');
             }
         }).fail(function(xhr) {
-            console.error("Organizr Function: API Connection Failed");
+	        OrganizrApiError(xhr);
         });
         ajaxloader();
     }
@@ -209,16 +192,13 @@ $(document).on("change", "#email-user-list", function () {
     $('#sendEmailToInput').val($('#email-user-list').val());
 });
 $(document).on('click', '.loadUserList', function() {
-    var post = {
-        plugin:'PHPMailer/users/get', // used for switch case in your API call
-    };
     ajaxloader(".content-wrap","in");
-    organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
-        var response = JSON.parse(data);
+    organizrAPI2('GET','api/v2/plugins/php-mailer/email/list').success(function(data) {
+        var response = data.response;
         $('#user-list-div').html(buildUserList(response.data));
         $('#email-user-list').multiSelect();
     }).fail(function(xhr) {
-        console.error("Organizr Function: API Connection Failed");
+	    OrganizrApiError(xhr);
     });
     ajaxloader();
 });
@@ -251,71 +231,32 @@ function addForgotPassword(){
         }
     }
 }
-// CHANGE CUSTOMIZE Options
-$(document).on('change asColorPicker::close', '#PHPMAILER-settings-page1 :input', function(e) {
-    var input = $(this);
-    switch ($(this).attr('type')) {
-        case 'switch':
-        case 'checkbox':
-            var value = $(this).prop("checked") ? true : false;
-            break;
-        default:
-            var value = $(this).val();
-    }
-	var post = {
-        api:'api/?v1/update/config',
-        name:$(this).attr("name"),
-        type:$(this).attr("data-type"),
-        value:value,
-        messageTitle:'',
-        messageBody:'Updated Value for '+$(this).parent().parent().find('label').text(),
-        error:'Organizr Function: API Connection Failed'
-    };
-	var callbacks = $.Callbacks();
-    //callbacks.add( buildCustomizeAppearance );
-    settingsAPI(post,callbacks);
-    //disable button then renable
-    $('#PHPMAILER-settings-page :input').prop('disabled', 'true');
-    setTimeout(
-        function(){
-            $('#PHPMAILER-settings-page :input').prop('disabled', null);
-            input.emulateTab();
-        },
-        2000
-    );
-
-});
 $(document).on('click', '#PHPMAILER-settings-button', function() {
-    var post = {
-        plugin:'PHPMailer/settings/get', // used for switch case in your API call
-    };
     ajaxloader(".content-wrap","in");
-    organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
-        var response = JSON.parse(data);
+    organizrAPI2('GET','api/v2/plugins/php-mailer/settings').success(function(data) {
+        var response = data.response;
         $('#PHPMAILER-settings-items').html(buildFormGroup(response.data));
     }).fail(function(xhr) {
-        console.error("Organizr Function: API Connection Failed");
+	    OrganizrApiError(xhr);
     });
     ajaxloader();
 });
 // SEND TEST EMAIL
 $(document).on('click', '.phpmSendTestEmail', function() {
     messageSingle('',window.lang.translate('Sending Test E-Mail'),activeInfo.settings.notifications.position,'#FFF','info','5000');
-    var post = {
-        plugin:'PHPMailer/send/test', // used for switch case in your API call
-    };
     ajaxloader(".content-wrap","in");
-    organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
-        var response = JSON.parse(data);
-        if(response.data == true){
-            messageSingle('',window.lang.translate('Email Test Successful'),activeInfo.settings.notifications.position,'#FFF','success','5000');
-        }else if(response.data.indexOf('|||DEBUG|||') == 0) {
-            messageSingle('',window.lang.translate('Press F11 to check Console for output'),activeInfo.settings.notifications.position,'#FFF','warning','20000');
-            console.warn(response.data);
+    organizrAPI2('GET','api/v2/plugins/php-mailer/email/test').success(function(data) {
+        var response = data.response;
+        if(response.message !== null && response.message.indexOf('|||DEBUG|||') == 0){
+            messageSingle('',window.lang.translate('Press F11 to check Console for output'),activeInfo.settings.notifications.position,'#FFF','warning','5000');
+	        console.warn(response.message);
+        }else if(response.result == 'success') {
+            messageSingle('',window.lang.translate('Email Test Successful'),activeInfo.settings.notifications.position,'#FFF','success','20000');
         }else{
-            messageSingle('',response.data,activeInfo.settings.notifications.position,'#FFF','error','5000');
+            messageSingle('',response.message,activeInfo.settings.notifications.position,'#FFF','error','5000');
         }
-    }).fail(function(xhr) {
+    }).fail(function(xhr, data) {
+    	console.log(data)
         console.error("Organizr Function: API Connection Failed");
     });
     ajaxloader();

+ 3 - 58
api/plugins/js/speedTest.js

@@ -1,22 +1,4 @@
-/* PHP MAILER JS FILE */
-/*
-$(document).on('click', '#PHPMAILER-settings-button', function() {
-	var post = {
-        plugin:'PHPMailer/settings/get', // used for switch case in your API call
-        api:'api/?v1/plugin', // API Endpoint will always be this for custom plugin API calls
-        name:$(this).attr('data-plugin-name'),
-        configName:$(this).attr('data-config-name'),
-        messageTitle:'', // Send succees message title (top line)
-        messageBody:'Disabled '+$(this).attr('data-plugin-name'), // Send succees message body (bottom line)
-        error:'Organizr Function: API Connection Failed' // conole error message
-    };
-	var callbacks = $.Callbacks(); // init callbacks var
-    //callbacks.add(  ); // add function to callback to be fired after API call
-    //settingsAPI(post,callbacks); // exec API call
-    //ajaxloader(".content-wrap","in");
-    //setTimeout(function(){ buildPlugins();ajaxloader(); }, 3000);
-});
-*/
+/* SPEEDTEST JS FILE */
 function clamp(num, min, max) {
   return num <= min ? min : num >= max ? max : num;
 }
@@ -143,47 +125,10 @@ function speedTestLaunch(){
     }
 }
 
-// CHANGE CUSTOMIZE Options
-$(document).on('change asColorPicker::close', '#SPEEDTEST-settings-page1 :input', function(e) {
-    var input = $(this);
-    switch ($(this).attr('type')) {
-        case 'switch':
-        case 'checkbox':
-            var value = $(this).prop("checked") ? true : false;
-            break;
-        default:
-            var value = $(this).val().toString();
-    }
-	var post = {
-        api:'api/?v1/update/config',
-        name:$(this).attr("name"),
-        type:$(this).attr("data-type"),
-        value:value,
-        messageTitle:'',
-        messageBody:'Updated Value for '+$(this).parent().parent().find('label').text(),
-        error:'Organizr Function: API Connection Failed'
-    };
-	var callbacks = $.Callbacks();
-    //callbacks.add( buildCustomizeAppearance );
-    settingsAPI(post,callbacks);
-    //disable button then renable
-    $('#SPEEDTEST-settings-page :input').prop('disabled', 'true');
-    setTimeout(
-        function(){
-            $('#SPEEDTEST-settings-page :input').prop('disabled', null);
-            input.emulateTab();
-        },
-        2000
-    );
-
-});
 $(document).on('click', '#SPEEDTEST-settings-button', function() {
-    var post = {
-        plugin:'SpeedTest/settings/get', // used for switch case in your API call
-    };
     ajaxloader(".content-wrap","in");
-    organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
-        var response = JSON.parse(data);
+    organizrAPI2('GET','api/v2/plugins/speedtest/settings').success(function(data) {
+        var response = data.response;
         $('#SPEEDTEST-settings-items').html(buildFormGroup(response.data));
     }).fail(function(xhr) {
         console.error("Organizr Function: API Connection Failed");

+ 66 - 0
api/plugins/misc/emailTemplates/dark.php

@@ -0,0 +1,66 @@
+<?php
+switch ($extra) {
+	case 'invite':
+		$button = '
+		<center>
+			<a href="' . $this->getServerPath(true) . '?invite=' . $email['inviteCode'] . '" style="display: inline-block; padding: 11px 30px; margin: 20px 0px 30px; font-size: 15px; color: #fff; background: #1e88e5; border-radius: 60px; text-decoration:none;">Use Invite Code</a>
+		</center>
+        ';
+		break;
+	case 'reset':
+		$button = '
+		<center>
+			<a href="' . $this->getServerPath(true) . '" style="display: inline-block; padding: 11px 30px; margin: 20px 0px 30px; font-size: 15px; color: #fff; background: #1e88e5; border-radius: 60px; text-decoration:none;">Goto My Site</a>
+		</center>
+        ';
+		break;
+	default:
+		$button = null;
+		break;
+}
+$email = '
+ <!DOCTYPE html>
+ <html>
+ <head>
+ 	<meta content="width=device-width" name="viewport">
+ 	<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
+ 	<title>Email</title>
+ </head>
+ <body style="margin: 0px; background: rgb(35, 35, 35);">
+ 	<div style="background: #232323; padding: 0px 0px; font-family:arial; line-height:28px; height:100%; width: 100%; color: #514d6a;">
+ 		<div style="max-width: 700px;margin: 0px auto;font-size: 14px;">
+ 			<table border="0" cellpadding="0" cellspacing="0" style="width: 100%; margin-bottom: 20px">
+ 				<tbody>
+ 					<tr>
+ 						<td align="center" style="vertical-align: top; padding-bottom:30px;"><a href="javascript:void(0)" target="_blank"><br>
+ 						<img alt="admin Responsive web app kit" src="' . $this->config['PHPMAILER-logo'] . '" style="border:none;width: 100%;"></a></td>
+ 					</tr>
+ 				</tbody>
+ 			</table>
+ 			<table border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
+ 				<tbody>
+ 					<tr>
+ 						<td style="background: #1b1a1a;padding:20px;color:#fff;text-align:center;">' . $subject . '</td>
+ 					</tr>
+ 				</tbody>
+ 			</table>
+ 			<div style="padding: 40px; background: #fff;">
+ 				<table border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
+ 					<tbody>
+ 						<tr>
+ 							<td>
+ 								<p>' . $body . '</p>
+ 								' . $button . '
+ 							</td>
+ 						</tr>
+ 					</tbody>
+ 				</table>
+ 			</div>
+ 			<div style="text-align: center; font-size: 12px; color: #b2b2b5; margin-top: 20px">
+ 				<p>Powered by Organizr<br></p>
+ 			</div>
+ 		</div>
+ 	</div>
+ </body>
+ </html>
+';

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott