Explorar o código

add new class files

CauseFX %!s(int64=5) %!d(string=hai) anos
pai
achega
ba934d486a

+ 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;
+	}
+}

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

@@ -0,0 +1,8840 @@
+<?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 ICalHomepageItem;
+	use JDownloaderHomepageItem;
+	use LidarrHomepageItem;
+	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 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,
+			'API Folder' => dirname(__DIR__, 1) . DIRECTORY_SEPARATOR,
+			'DB Folder' => ($this->hasDB()) ? $this->config['dbLocation'] : false
+		);
+		// Connect to DB
+		$this->connectDB();
+		// 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'];
+			$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);
+		}
+	}
+	
+	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->upgradeSettingsTabURL();
+				$this->upgradeHomepageTabURL();
+				
+			}
+			// 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
+				);
+			}
+		}
+		ksort($allIconsPrep);
+		foreach ($allIconsPrep as $item) {
+			$allIcons[] = $item['path'] . $item['name'];
+		}
+		return $allIcons;
+	}
+	
+	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->optionNotificationTypes()
+				),
+				array(
+					'type' => 'select',
+					'name' => 'notificationPosition',
+					'class' => 'notifyPositionChanger',
+					'label' => 'Position',
+					'value' => $this->config['notificationPosition'],
+					'options' => $this->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' => $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' => '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' => $this->config['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' => '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/Jellyfin 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/Jellyfin Token',
+					'value' => $this->config['embyToken'],
+					'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->optionTime()
+				),
+				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->optionTime()
+				),
+				array(
+					'type' => 'select',
+					'name' => 'otherPingRefresh',
+					'label' => 'Everyone Refresh Seconds',
+					'value' => $this->config['otherPingRefresh'],
+					'options' => $this->optionTime()
+				),
+			)
+		);
+	}
+	
+	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',
+					'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' => $this->config['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' => $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' => '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)
+	{
+		$response = [
+			array(
+				'function' => 'query',
+				'query' => array(
+					'DELETE FROM tokens WHERE user_id = ? or token = ?',
+					[$this->user['userID']],
+					[$token]
+				)
+			),
+		];
+		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':
+				$file = $this->organizrLoginLog;
+				$parent = 'auth';
+				break;
+			case 'org':
+			case 'organizr':
+			case 'organizrLog':
+				$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':
+				$file = $this->organizrLoginLog;
+				break;
+			case 'org':
+			case 'organizr':
+			case 'organizrLog':
+			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) {
+			$homepageBuilt .= $this->buildHomepageItem($key);
+		}
+		return $homepageBuilt;
+	}
+	
+	public function buildHomepageItem($homepageItem)
+	{
+		$item = '<div id="' . $homepageItem . '">';
+		switch ($homepageItem) {
+			case 'homepageOrdercustomhtml':
+				if ($this->config['homepageCustomHTMLoneEnabled'] && $this->qualifyRequest($this->config['homepageCustomHTMLoneAuth'])) {
+					$item .= ($this->config['customHTMLone'] !== '') ? $this->config['customHTMLone'] : '';
+				}
+				break;
+			case 'homepageOrdercustomhtmlTwo':
+				if ($this->config['homepageCustomHTMLtwoEnabled'] && $this->qualifyRequest($this->config['homepageCustomHTMLtwoAuth'])) {
+					$item .= ($this->config['customHTMLtwo'] !== '') ? $this->config['customHTMLtwo'] : '';
+				}
+				break;
+			case 'homepageOrdernotice':
+				break;
+			case 'homepageOrdernoticeguest':
+				break;
+			case 'homepageOrderqBittorrent':
+				if ($this->config['homepageqBittorrentEnabled'] && $this->qualifyRequest($this->config['homepageqBittorrentAuth'])) {
+					if ($this->config['qBittorrentCombine']) {
+						$item .= '
+	                <script>
+	                // homepageOrderqBittorrent
+	                buildDownloaderCombined(\'qBittorrent\');
+	                homepageDownloader("qBittorrent", "' . $this->config['homepageDownloadRefresh'] . '");
+	                // End homepageOrderqBittorrent
+	                </script>
+	                ';
+					} else {
+						$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+						$item .= '
+	                <script>
+	                // homepageOrderqBittorrent
+	                $("#' . $homepageItem . '").html(buildDownloader("qBittorrent"));
+	                homepageDownloader("qBittorrent", "' . $this->config['homepageDownloadRefresh'] . '");
+	                // End homepageOrderqBittorrent
+	                </script>
+	                ';
+					}
+				}
+				break;
+			case 'homepageOrderrTorrent':
+				if ($this->config['homepagerTorrentEnabled'] && $this->qualifyRequest($this->config['homepagerTorrentAuth'])) {
+					if ($this->config['rTorrentCombine']) {
+						$item .= '
+	                <script>
+	                // homepageOrderrTorrent
+	                buildDownloaderCombined(\'rTorrent\');
+	                homepageDownloader("rTorrent", "' . $this->config['homepageDownloadRefresh'] . '");
+	                // End homepageOrderrTorrent
+	                </script>
+	                ';
+					} else {
+						$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+						$item .= '
+	                <script>
+	                // homepageOrderrTorrent
+	                $("#' . $homepageItem . '").html(buildDownloader("rTorrent"));
+	                homepageDownloader("rTorrent", "' . $this->config['homepageDownloadRefresh'] . '");
+	                // End homepageOrderrTorrent
+	                </script>
+	                ';
+					}
+				}
+				break;
+			case 'homepageOrderdeluge':
+				if ($this->config['homepageDelugeEnabled'] && $this->qualifyRequest($this->config['homepageDelugeAuth'])) {
+					if ($this->config['delugeCombine']) {
+						$item .= '
+					<script>
+					// Deluge
+					buildDownloaderCombined(\'deluge\');
+					homepageDownloader("deluge", "' . $this->config['homepageDownloadRefresh'] . '");
+					// End Deluge
+					</script>
+					';
+					} else {
+						$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+						$item .= '
+					<script>
+					// Deluge
+					$("#' . $homepageItem . '").html(buildDownloader("deluge"));
+					homepageDownloader("deluge", "' . $this->config['homepageDownloadRefresh'] . '");
+					// End Deluge
+					</script>
+					';
+					}
+				}
+				break;
+			case 'homepageOrdertransmission':
+				if ($this->config['homepageTransmissionEnabled'] && $this->qualifyRequest($this->config['homepageTransmissionAuth'])) {
+					if ($this->config['transmissionCombine']) {
+						$item .= '
+					<script>
+					// Transmission
+					buildDownloaderCombined(\'transmission\');
+					homepageDownloader("transmission", "' . $this->config['homepageDownloadRefresh'] . '");
+					// End Transmission
+					</script>
+					';
+					} else {
+						$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+						$item .= '
+					<script>
+					// Transmission
+					$("#' . $homepageItem . '").html(buildDownloader("transmission"));
+					homepageDownloader("transmission", "' . $this->config['homepageDownloadRefresh'] . '");
+					// End Transmission
+					</script>
+					';
+					}
+				}
+				break;
+			case 'homepageOrdernzbget':
+				if ($this->config['homepageNzbgetEnabled'] && $this->qualifyRequest($this->config['homepageNzbgetAuth'])) {
+					if ($this->config['nzbgetCombine']) {
+						$item .= '
+					<script>
+					// NZBGet
+					buildDownloaderCombined(\'nzbget\');
+					homepageDownloader("nzbget", "' . $this->config['homepageDownloadRefresh'] . '");
+					// End NZBGet
+					</script>
+					';
+					} else {
+						$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+						$item .= '
+					<script>
+					// NZBGet
+					$("#' . $homepageItem . '").html(buildDownloader("nzbget"));
+					homepageDownloader("nzbget", "' . $this->config['homepageDownloadRefresh'] . '");
+					// End NZBGet
+					</script>
+					';
+					}
+				}
+				break;
+			case 'homepageOrderjdownloader':
+				if ($this->config['homepageJdownloaderEnabled'] && $this->qualifyRequest($this->config['homepageJdownloaderAuth'])) {
+					if ($this->config['jdownloaderCombine']) {
+						$item .= '
+					<script>
+					// JDownloader
+					buildDownloaderCombined(\'jdownloader\');
+					homepageDownloader("jdownloader", "' . $this->config['homepageDownloadRefresh'] . '");
+					// End JDownloader
+					</script>
+					';
+					} else {
+						$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+						$item .= '
+					<script>
+					// JDownloader
+					$("#' . $homepageItem . '").html(buildDownloader("jdownloader"));
+					homepageDownloader("jdownloader", "' . $this->config['homepageDownloadRefresh'] . '");
+					// End JDownloader
+					</script>
+					';
+					}
+				}
+				break;
+			case 'homepageOrdersabnzbd':
+				if ($this->config['homepageSabnzbdEnabled'] && $this->qualifyRequest($this->config['homepageSabnzbdAuth'])) {
+					if ($this->config['sabnzbdCombine']) {
+						$item .= '
+					<script>
+					// SabNZBd
+					buildDownloaderCombined(\'sabnzbd\');
+					homepageDownloader("sabnzbd", "' . $this->config['homepageDownloadRefresh'] . '");
+					// End SabNZBd
+					</script>
+					';
+					} else {
+						$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+						$item .= '
+					<script>
+					// SabNZBd
+					$("#' . $homepageItem . '").html(buildDownloader("sabnzbd"));
+					homepageDownloader("sabnzbd", "' . $this->config['homepageDownloadRefresh'] . '");
+					// End SabNZBd
+					</script>
+					';
+					}
+				}
+				break;
+			case 'homepageOrderplexnowplaying':
+				if ($this->config['homepagePlexStreams']) {
+					$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Now Playing...</h2></div>';
+					$item .= '
+				<script>
+				// Plex Stream
+				homepageStream("plex", "' . $this->config['homepageStreamRefresh'] . '");
+				// End Plex Stream
+				</script>
+				';
+				}
+				break;
+			case 'homepageOrderplexrecent':
+				if ($this->config['homepagePlexRecent']) {
+					$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Recent...</h2></div>';
+					$item .= '
+				<script>
+				// Plex Recent
+				homepageRecent("plex", "' . $this->config['homepageRecentRefresh'] . '");
+				// End Plex Recent
+				</script>
+				';
+				}
+				break;
+			case 'homepageOrderplexplaylist':
+				if ($this->config['homepagePlexPlaylist']) {
+					$item .= '<div class="white-box homepage-loading-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 ($this->config['homepageEmbyStreams'] && $this->config['homepageEmbyEnabled']) {
+					$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Now Playing...</h2></div>';
+					$item .= '
+				<script>
+				// Emby Stream
+				homepageStream("emby", "' . $this->config['homepageStreamRefresh'] . '");
+				// End Emby Stream
+				</script>
+				';
+				}
+				break;
+			case 'homepageOrderembyrecent':
+				if ($this->config['homepageEmbyRecent'] && $this->config['homepageEmbyEnabled']) {
+					$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Recent...</h2></div>';
+					$item .= '
+				<script>
+				// Emby Recent
+				homepageRecent("emby", "' . $this->config['homepageRecentRefresh'] . '");
+				// End Emby Recent
+				</script>
+				';
+				}
+				break;
+			case 'homepageOrderombi':
+				if ($this->config['homepageOmbiEnabled']) {
+					$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Requests...</h2></div>';
+					$item .= '
+				<script>
+				// Ombi Requests
+				homepageRequests("' . $this->config['ombiRefresh'] . '");
+				// End Ombi Requests
+				</script>
+				';
+				}
+				break;
+			case 'homepageOrdercalendar':
+				if (
+					($this->config['homepageLidarrEnabled'] && $this->qualifyRequest($this->config['homepageLidarrAuth'])) ||
+					($this->config['homepageSonarrEnabled'] && $this->qualifyRequest($this->config['homepageSonarrAuth'])) ||
+					($this->config['homepageRadarrEnabled'] && $this->qualifyRequest($this->config['homepageRadarrAuth'])) ||
+					($this->config['homepageSickrageEnabled'] && $this->qualifyRequest($this->config['homepageSickrageAuth'])) ||
+					($this->config['homepageCouchpotatoEnabled'] && $this->qualifyRequest($this->config['homepageCouchpotatoAuth'])) ||
+					($this->config['homepageCalendarEnabled'] && $this->qualifyRequest($this->config['homepageCalendarAuth']) && $this->config['calendariCal'] !== '')
+				) {
+					$item .= '
+				<div id="calendar" class="fc fc-ltr m-b-30"></div>
+				<script>
+				// Calendar
+				homepageCalendar("' . $this->config['calendarRefresh'] . '");
+				// End Calendar
+				</script>
+				';
+				}
+				break;
+			case 'homepageOrderhealthchecks':
+				if ($this->config['homepageHealthChecksEnabled'] && $this->qualifyRequest($this->config['homepageHealthChecksAuth'])) {
+					$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Health Checks...</h2></div>';
+					$item .= '
+				<script>
+				// Health Checks
+				homepageHealthChecks("' . $this->config['healthChecksTags'] . '","' . $this->config['homepageHealthChecksRefresh'] . '");
+				// End Health Checks
+				</script>
+				';
+				}
+				break;
+			case 'homepageOrderunifi':
+				if ($this->config['homepageUnifiEnabled'] && $this->qualifyRequest($this->config['homepageUnifiAuth'])) {
+					$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Unifi...</h2></div>';
+					$item .= '
+				<script>
+				// Unifi
+				homepageUnifi("' . $this->config['homepageHealthChecksRefresh'] . '");
+				// End Unifi
+				</script>
+				';
+				}
+				break;
+			case 'homepageOrdertautulli':
+				if ($this->config['homepageTautulliEnabled'] && $this->qualifyRequest($this->config['homepageTautulliAuth'])) {
+					$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Tautulli...</h2></div>';
+					$item .= '
+				<script>
+				// Tautulli
+				homepageTautulli("' . $this->config['homepageTautulliRefresh'] . '");
+				// End Tautulli
+				</script>
+				';
+				}
+				break;
+			case 'homepageOrderPihole':
+				if ($this->config['homepagePiholeEnabled']) {
+					$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Pi-hole Stats...</h2></div>';
+					$item .= '
+				<script>
+				// Pi-hole Stats
+				homepagePihole("' . $this->config['homepagePiholeRefresh'] . '");
+				// End Pi-hole Stats
+				</script>
+				';
+				}
+				break;
+			case 'homepageOrderMonitorr':
+				if ($this->config['homepageMonitorrEnabled']) {
+					$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Monitorr...</h2></div>';
+					$item .= '
+				<script>
+				// Monitorr
+				homepageMonitorr("' . $this->config['homepageMonitorrRefresh'] . '");
+				// End Monitorr
+				</script>
+				';
+				}
+				break;
+			case 'homepageOrderWeatherAndAir':
+				if ($this->config['homepageWeatherAndAirEnabled']) {
+					$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Weather And Air...</h2></div>';
+					$item .= '
+				<script>
+				// Weather And Air
+				homepageWeatherAndAir("' . $this->config['homepageWeatherAndAirRefresh'] . '");
+				// End Weather And Air
+				</script>
+				';
+				}
+				break;
+			case 'homepageOrderSpeedtest':
+				if ($this->config['homepageSpeedtestEnabled']) {
+					$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Speedtest...</h2></div>';
+					$item .= '
+				<script>
+				// Speedtest
+				homepageSpeedtest("' . $this->config['homepageSpeedtestRefresh'] . '");
+				// End Speedtest
+				</script>
+				';
+				}
+				break;
+			case 'homepageOrderNetdata':
+				if ($this->config['homepageNetdataEnabled']) {
+					$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Netdata...</h2></div>';
+					$item .= '
+				<script>
+				// Netdata
+				homepageNetdata("' . $this->config['homepageNetdataRefresh'] . '");
+				// End Netdata
+				</script>
+				';
+				}
+				break;
+			case 'homepageOrderOctoprint':
+				if ($this->config['homepageOctoprintEnabled']) {
+					$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Octoprint...</h2></div>';
+					$item .= '
+				<script>
+				// Octoprint
+				homepageOctoprint("' . $this->config['homepageOctoprintRefresh'] . '");
+				// End Octoprint
+				</script>
+				';
+				}
+				break;
+			case 'homepageOrderSonarrQueue':
+				if ($this->config['homepageSonarrQueueEnabled'] && $this->qualifyRequest($this->config['homepageSonarrQueueAuth'])) {
+					if ($this->config['homepageSonarrQueueCombine']) {
+						$item .= '
+					<script>
+					// Sonarr Queue
+					buildDownloaderCombined(\'sonarr\');
+					homepageDownloader("sonarr", "' . $this->config['homepageSonarrQueueRefresh'] . '");
+					// End Sonarr Queue
+					</script>
+					';
+					} else {
+						$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+						$item .= '
+					<script>
+					// Sonarr Queue
+					$("#' . $homepageItem . '").html(buildDownloader("sonarr"));
+					homepageDownloader("sonarr", "' . $this->config['homepageSonarrQueueRefresh'] . '");
+					// End Sonarr Queue
+					</script>
+					';
+					}
+				}
+				break;
+			case 'homepageOrderRadarrQueue':
+				if ($this->config['homepageRadarrQueueEnabled'] && $this->qualifyRequest($this->config['homepageRadarrQueueAuth'])) {
+					if ($this->config['homepageRadarrQueueCombine']) {
+						$item .= '
+					<script>
+					// Radarr Queue
+					buildDownloaderCombined(\'radarr\');
+					homepageDownloader("radarr", "' . $this->config['homepageRadarrQueueRefresh'] . '");
+					// End Radarr Queue
+					</script>
+					';
+					} else {
+						$item .= '<div class="white-box homepage-loading-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+						$item .= '
+					<script>
+					// Radarr Queue
+					$("#' . $homepageItem . '").html(buildDownloader("radarr"));
+					homepageDownloader("radarr", "' . $this->config['homepageRadarrQueueRefresh'] . '");
+					// End Radarr Queue
+					</script>
+					';
+					}
+				}
+				break;
+			default:
+				# code...
+				break;
+		}
+		return $item . '</div>';
+	}
+	
+	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 '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;
+				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 getSettingsHomepage()
+	{
+		$groups = $this->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', $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' => $groups
+						),
+						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' => $day
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarDefault',
+							'label' => 'Default View',
+							'value' => $this->config['calendarDefault'],
+							'options' => $calendarDefault
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarTimeFormat',
+							'label' => 'Time Format',
+							'value' => $this->config['calendarTimeFormat'],
+							'options' => $timeFormat
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarLimit',
+							'label' => 'Items Per Day',
+							'value' => $this->config['calendarLimit'],
+							'options' => $limit
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarRefresh',
+							'label' => 'Refresh Seconds',
+							'value' => $this->config['calendarRefresh'],
+							'options' => $this->optionTime()
+						)
+					),
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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' => 'password-alt',
+							'name' => 'plexToken',
+							'label' => 'Token',
+							'value' => $this->config['plexToken']
+						),
+						array(
+							'type' => 'password-alt',
+							'name' => 'plexID',
+							'label' => 'Plex Machine',
+							'value' => $this->config['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' => $groups
+						),
+						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' => $groups
+						),
+						array(
+							'type' => 'select',
+							'name' => 'homepageStreamRefresh',
+							'label' => 'Refresh Seconds',
+							'value' => $this->config['homepageStreamRefresh'],
+							'options' => $this->optionTime()
+						),
+					),
+					'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' => $groups
+						),
+						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->optionTime()
+						),
+					),
+					'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' => $groups
+						),
+						array(
+							'type' => 'select',
+							'name' => 'mediaSearchType',
+							'label' => 'Media Server',
+							'value' => $this->config['mediaSearchType'],
+							'options' => $mediaServers
+						),
+					),
+					'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' => $groups
+						),
+					),
+					'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\')"'
+						),
+					)
+				)
+			),
+			array(
+				'name' => 'Emby-Jellyfin',
+				'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' => $groups
+						)
+					),
+					'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']
+						),
+						array(
+							'type' => 'switch',
+							'name' => 'homepageJellyfinInstead',
+							'label' => 'Jellyfin',
+							'value' => $this->config['homepageJellyfinInstead']
+						)
+					),
+					'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' => $groups
+						),
+						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' => $groups
+						),
+						array(
+							'type' => 'select',
+							'name' => 'homepageStreamRefresh',
+							'label' => 'Refresh Seconds',
+							'value' => $this->config['homepageStreamRefresh'],
+							'options' => $this->optionTime()
+						),
+					),
+					'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' => $groups
+						),
+						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->optionTime()
+						),
+					),
+					'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\')"'
+						),
+					)
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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->optionTime()
+						),
+						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\')"'
+						),
+					)
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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->optionTime()
+						),
+						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\')"'
+						),
+					)
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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->optionTime()
+						),
+						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\')"'
+						),
+					)
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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->optionTime()
+						),
+						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\')"'
+						),
+					)
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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' => $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' => $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->optionTime()
+						),
+						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\')"'
+						),
+					)
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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' => $rTorrentSortOptions
+						),
+						array(
+							'type' => 'select',
+							'name' => 'homepageDownloadRefresh',
+							'label' => 'Refresh Seconds',
+							'value' => $this->config['homepageDownloadRefresh'],
+							'options' => $this->optionTime()
+						),
+						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\')"'
+						),
+					)
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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->optionTime()
+						),
+						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\')"'
+						),
+					)
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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']
+						)
+					),
+					'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' => $groups
+						),
+						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->optionTime()
+						),
+					),
+					'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' => $day
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarDefault',
+							'label' => 'Default View',
+							'value' => $this->config['calendarDefault'],
+							'options' => $calendarDefault
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarTimeFormat',
+							'label' => 'Time Format',
+							'value' => $this->config['calendarTimeFormat'],
+							'options' => $timeFormat
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarLimit',
+							'label' => 'Items Per Day',
+							'value' => $this->config['calendarLimit'],
+							'options' => $limit
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarRefresh',
+							'label' => 'Refresh Seconds',
+							'value' => $this->config['calendarRefresh'],
+							'options' => $this->optionTime()
+						),
+						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\')"'
+						),
+					)
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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']
+						)
+					),
+					'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' => $day
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarDefault',
+							'label' => 'Default View',
+							'value' => $this->config['calendarDefault'],
+							'options' => $calendarDefault
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarTimeFormat',
+							'label' => 'Time Format',
+							'value' => $this->config['calendarTimeFormat'],
+							'options' => $timeFormat
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarLimit',
+							'label' => 'Items Per Day',
+							'value' => $this->config['calendarLimit'],
+							'options' => $limit
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarRefresh',
+							'label' => 'Refresh Seconds',
+							'value' => $this->config['calendarRefresh'],
+							'options' => $this->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', $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' => $groups
+						)
+					),
+					'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']
+						)
+					),
+					'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' => $groups
+						),
+						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->optionTime()
+						),
+					),
+					'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' => $day
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarDefault',
+							'label' => 'Default View',
+							'value' => $this->config['calendarDefault'],
+							'options' => $calendarDefault
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarTimeFormat',
+							'label' => 'Time Format',
+							'value' => $this->config['calendarTimeFormat'],
+							'options' => $timeFormat
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarLimit',
+							'label' => 'Items Per Day',
+							'value' => $this->config['calendarLimit'],
+							'options' => $limit
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarRefresh',
+							'label' => 'Refresh Seconds',
+							'value' => $this->config['calendarRefresh'],
+							'options' => $this->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', $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' => $groups
+						)
+					),
+					'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' => $day
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarDefault',
+							'label' => 'Default View',
+							'value' => $this->config['calendarDefault'],
+							'options' => $calendarDefault
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarTimeFormat',
+							'label' => 'Time Format',
+							'value' => $this->config['calendarTimeFormat'],
+							'options' => $timeFormat
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarLimit',
+							'label' => 'Items Per Day',
+							'value' => $this->config['calendarLimit'],
+							'options' => $limit
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarRefresh',
+							'label' => 'Refresh Seconds',
+							'value' => $this->config['calendarRefresh'],
+							'options' => $this->optionTime()
+						)
+					)
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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' => $day
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarDefault',
+							'label' => 'Default View',
+							'value' => $this->config['calendarDefault'],
+							'options' => $calendarDefault
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarTimeFormat',
+							'label' => 'Time Format',
+							'value' => $this->config['calendarTimeFormat'],
+							'options' => $timeFormat
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarLimit',
+							'label' => 'Items Per Day',
+							'value' => $this->config['calendarLimit'],
+							'options' => $limit
+						),
+						array(
+							'type' => 'select',
+							'name' => 'calendarRefresh',
+							'label' => 'Refresh Seconds',
+							'value' => $this->config['calendarRefresh'],
+							'options' => $this->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(\'sickrage\')"'
+						),
+					)
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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']
+						)
+					),
+					'Misc Options' => array(
+						array(
+							'type' => 'select',
+							'name' => 'homepageOmbiRequestAuth',
+							'label' => 'Minimum Group to Request',
+							'value' => $this->config['homepageOmbiRequestAuth'],
+							'options' => $groups
+						),
+						array(
+							'type' => 'select',
+							'name' => 'ombiTvDefault',
+							'label' => 'TV Show Default Request',
+							'value' => $this->config['ombiTvDefault'],
+							'options' => $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->optionTime()
+						),
+						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\')"'
+						),
+					)
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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->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' => $this->config['homepageHealthChecksEnabled']
+						),
+						array(
+							'type' => 'select',
+							'name' => 'homepageHealthChecksAuth',
+							'label' => 'Minimum Authentication',
+							'value' => $this->config['homepageHealthChecksAuth'],
+							'options' => $groups
+						)
+					),
+					'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->optionTime()
+						),
+					),
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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>'
+						),
+					)
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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>'
+						),
+					)
+				)
+			),
+			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>'
+						),
+					)
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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\')"'
+						),
+					)
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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->optionTime()
+						),
+					),
+					'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' => $groups
+						),
+					),
+					'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' => $groups
+						),
+					),
+					'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' => $groups
+						),
+					),
+					'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\')"'
+						),
+					)
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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->optionTime()
+						),
+					),
+					'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'
+						),
+					),
+				)
+			),
+			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' => $groups
+						)
+					),
+					'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->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' => $this->config['homepageSpeedtestEnabled']
+						),
+						array(
+							'type' => 'select',
+							'name' => 'homepageSpeedtestAuth',
+							'label' => 'Minimum Authentication',
+							'value' => $this->config['homepageSpeedtestAuth'],
+							'options' => $groups
+						)
+					),
+					'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'
+						),
+					),
+				)
+			),
+			$this->netdataSettingsArray(),
+			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' => $groups
+						)
+					),
+					'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 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 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['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) {
+							// 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 ($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;
+		}
+	}
+	
+	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);
+	}
+}