Parcourir la source

Merge pull request #1043 from causefx/v2-develop

v2 master first commit
causefx il y a 7 ans
Parent
commit
739e91b3c0
100 fichiers modifiés avec 23711 ajouts et 0 suppressions
  1. 58 0
      .gitignore
  2. 15 0
      api/composer.json
  3. 1368 0
      api/composer.lock
  4. 205 0
      api/config/default.php
  5. 38 0
      api/functions.php
  6. 78 0
      api/functions/2fa-functions.php
  7. 1159 0
      api/functions/api-functions.php
  8. 353 0
      api/functions/auth-functions.php
  9. 56 0
      api/functions/backup-functions.php
  10. 1 0
      api/functions/cert/index.html
  11. 144 0
      api/functions/config-functions.php
  12. 572 0
      api/functions/deluge.class.php
  13. 2224 0
      api/functions/homepage-connect-functions.php
  14. 2241 0
      api/functions/homepage-functions.php
  15. 59 0
      api/functions/log-functions.php
  16. 623 0
      api/functions/normal-functions.php
  17. 145 0
      api/functions/option-functions.php
  18. 1983 0
      api/functions/organizr-functions.php
  19. 328 0
      api/functions/ping.class.php
  20. 81 0
      api/functions/plugin-functions.php
  21. 89 0
      api/functions/sso-functions.php
  22. 109 0
      api/functions/static-globals.php
  23. 81 0
      api/functions/theme-functions.php
  24. 173 0
      api/functions/token-functions.php
  25. 184 0
      api/functions/update-functions.php
  26. 53 0
      api/functions/upgrade-functions.php
  27. 1297 0
      api/index.php
  28. 75 0
      api/pages/dependencies.php
  29. 129 0
      api/pages/homepage.php
  30. 32 0
      api/pages/lockscreen.php
  31. 127 0
      api/pages/login.php
  32. 18 0
      api/pages/settings-customize-appearance.php
  33. 34 0
      api/pages/settings-image-manager.php
  34. 14 0
      api/pages/settings-plugins.php
  35. 129 0
      api/pages/settings-settings-logs.php
  36. 48 0
      api/pages/settings-settings-main.php
  37. 48 0
      api/pages/settings-settings-sso.php
  38. 84 0
      api/pages/settings-tab-editor-categories.php
  39. 50 0
      api/pages/settings-tab-editor-homepage-order.php
  40. 19 0
      api/pages/settings-tab-editor-homepage.php
  41. 151 0
      api/pages/settings-tab-editor-tabs.php
  42. 16 0
      api/pages/settings-template.php
  43. 60 0
      api/pages/settings-user-manage-groups.php
  44. 72 0
      api/pages/settings-user-manage-users.php
  45. 332 0
      api/pages/settings.php
  46. 401 0
      api/pages/wizard.php
  47. 59 0
      api/plugins/api/chat.php
  48. 42 0
      api/plugins/api/invites.php
  49. 70 0
      api/plugins/api/php-mailer.php
  50. 19 0
      api/plugins/api/speedTest.php
  51. 164 0
      api/plugins/chat.php
  52. 13 0
      api/plugins/config/chat.php
  53. 6 0
      api/plugins/config/invites.php
  54. 47 0
      api/plugins/config/php-mailer.php
  55. 6 0
      api/plugins/config/speedTest.php
  56. 318 0
      api/plugins/invites.php
  57. 207 0
      api/plugins/js/chat.js
  58. 449 0
      api/plugins/js/invites.js
  59. 319 0
      api/plugins/js/php-mailer.js
  60. 192 0
      api/plugins/js/speedTest.js
  61. 1218 0
      api/plugins/misc/emailTemplates/default.php
  62. 129 0
      api/plugins/misc/emailTemplates/gray.php
  63. 66 0
      api/plugins/misc/emailTemplates/light.php
  64. 42 0
      api/plugins/misc/emailTemplates/plehex.php
  65. 7 0
      api/plugins/misc/speedTest/empty.php
  66. 27 0
      api/plugins/misc/speedTest/garbage.php
  67. 99 0
      api/plugins/misc/speedTest/getIP.php
  68. 0 0
      api/plugins/misc/speedTest/speedtest_worker.min.js
  69. 73 0
      api/plugins/misc/speedTest/telemetry.php
  70. 24 0
      api/plugins/misc/speedTest/telemetry_settings.php
  71. 501 0
      api/plugins/php-mailer.php
  72. 38 0
      api/plugins/speedTest.php
  73. 7 0
      api/vendor/autoload.php
  74. 445 0
      api/vendor/composer/ClassLoader.php
  75. 21 0
      api/vendor/composer/LICENSE
  76. 64 0
      api/vendor/composer/autoload_classmap.php
  77. 17 0
      api/vendor/composer/autoload_files.php
  78. 10 0
      api/vendor/composer/autoload_namespaces.php
  79. 31 0
      api/vendor/composer/autoload_psr4.php
  80. 70 0
      api/vendor/composer/autoload_real.php
  81. 235 0
      api/vendor/composer/autoload_static.php
  82. 1402 0
      api/vendor/composer/installed.json
  83. 66 0
      api/vendor/composer/semver/CHANGELOG.md
  84. 19 0
      api/vendor/composer/semver/LICENSE
  85. 70 0
      api/vendor/composer/semver/README.md
  86. 58 0
      api/vendor/composer/semver/composer.json
  87. 111 0
      api/vendor/composer/semver/src/Comparator.php
  88. 63 0
      api/vendor/composer/semver/src/Constraint/AbstractConstraint.php
  89. 219 0
      api/vendor/composer/semver/src/Constraint/Constraint.php
  90. 32 0
      api/vendor/composer/semver/src/Constraint/ConstraintInterface.php
  91. 59 0
      api/vendor/composer/semver/src/Constraint/EmptyConstraint.php
  92. 120 0
      api/vendor/composer/semver/src/Constraint/MultiConstraint.php
  93. 127 0
      api/vendor/composer/semver/src/Semver.php
  94. 548 0
      api/vendor/composer/semver/src/VersionParser.php
  95. 32 0
      api/vendor/dibi/dibi/composer.json
  96. 31 0
      api/vendor/dibi/dibi/contributing.md
  97. 163 0
      api/vendor/dibi/dibi/examples/connecting-to-databases.php
  98. BIN
      api/vendor/dibi/dibi/examples/data/arrow.png
  99. BIN
      api/vendor/dibi/dibi/examples/data/dibi-powered.gif
  100. BIN
      api/vendor/dibi/dibi/examples/data/sample.dump.sql.gz

+ 58 - 0
.gitignore

@@ -1,3 +1,8 @@
+# PhpStorm Files
+.idea/*
+node_modules/*
+package-lock.json
+
 # Windows image file caches
 Thumbs.db
 ehthumbs.db
@@ -49,6 +54,10 @@ Temporary Items
 # Files from my local computer
 php_errors.log
 
+# Backup Files
+.pydio_id
+
+.idea/*
 # =========================
 # Organizr files
 # =========================
@@ -63,6 +72,7 @@ test.php
 users.db
 speedtest.db
 chatpack.db
+Docker.txt
 config/cacert.pem
 config/config.php
 config/config*.bak.php
@@ -82,6 +92,54 @@ debug.php
 OrganizrV2/*
 orgv2/*
 test.php
+test/*
 organizrLoginLog.json
 organizrLog.json
 api/config/config*
+upgrade/*
+api/upgrade*
+api/functions/cert/cacert.pem
+wallpaper.jpg
+strings.json
+css/themes/*.css
+!css/themes/Organizr.css
+!css/themes/Blue.css
+*sonflix*
+plugins/images/cache/*.jpg
+testdata*
+strings.txt
+strings.php
+plugins/theme_files/*
+plugins/plugin_files/*
+!plugins/theme_files/index.html
+!plugins/plugin_files/index.html
+# =========================
+# Plugin files
+# =========================
+api/plugins/*
+!api/plugins/invites.php
+!api/plugins/api/invites.php
+!api/plugins/config/invites.php
+!api/plugins/js/invites.js
+!api/plugins/chat.php
+!api/plugins/api/chat.php
+!api/plugins/config/chat.php
+!api/plugins/js/chat.js
+!api/plugins/php-mailer.php
+!api/plugins/api/php-mailer.php
+!api/plugins/config/php-mailer.php
+!api/plugins/js/php-mailer.js
+!api/plugins/speedTest.php
+!api/plugins/api/speedTest.php
+!api/plugins/config/speedTest.php
+!api/plugins/js/speedTest.js
+!api/plugins/misc/emailTemplates/default.php
+!api/plugins/misc/emailTemplates/gray.php
+!api/plugins/misc/emailTemplates/light.php
+!api/plugins/misc/emailTemplates/plehex.php
+!api/plugins/misc/speedTest/empty.php
+!api/plugins/misc/speedTest/garbage.php
+!api/plugins/misc/speedTest/getIP.php
+!api/plugins/misc/speedTest/telemetry.php
+!api/plugins/misc/speedTest/telemetry_settings.php
+!api/plugins/misc/speedTest/speedtest_worker.min.js

+ 15 - 0
api/composer.json

@@ -0,0 +1,15 @@
+{
+  "require": {
+    "dibi/dibi": "^3.1",
+    "lcobucci/jwt": "^3.2",
+    "composer/semver": "^1.4",
+    "phpmailer/phpmailer": "^6.0",
+    "rmccue/requests": "^1.7",
+    "kryptonit3/sonarr": "1.0.6.1",
+    "kryptonit3/couchpotato": "^1.0",
+    "kryptonit3/sickrage": "^1.0",
+    "pusher/pusher-php-server": "^3.2",
+    "pragmarx/google2fa": "^3.0",
+    "layershifter/tld-extract": "^2.0"
+  }
+}

+ 1368 - 0
api/composer.lock

@@ -0,0 +1,1368 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "23e6f5f9beec5ee507e532c3264d3d03",
+    "packages": [
+        {
+            "name": "composer/semver",
+            "version": "1.4.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/composer/semver.git",
+                "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573",
+                "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.3.2 || ^7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.5 || ^5.0.5",
+                "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Composer\\Semver\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nils Adermann",
+                    "email": "naderman@naderman.de",
+                    "homepage": "http://www.naderman.de"
+                },
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be",
+                    "homepage": "http://seld.be"
+                },
+                {
+                    "name": "Rob Bast",
+                    "email": "rob.bast@gmail.com",
+                    "homepage": "http://robbast.nl"
+                }
+            ],
+            "description": "Semver library that offers utilities, version constraint parsing and validation.",
+            "keywords": [
+                "semantic",
+                "semver",
+                "validation",
+                "versioning"
+            ],
+            "time": "2016-08-30T16:08:34+00:00"
+        },
+        {
+            "name": "dibi/dibi",
+            "version": "v3.2.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/dg/dibi.git",
+                "reference": "906e1f240742124e49562cace271916e1b08c991"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/dg/dibi/zipball/906e1f240742124e49562cace271916e1b08c991",
+                "reference": "906e1f240742124e49562cace271916e1b08c991",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4.4"
+            },
+            "replace": {
+                "dg/dibi": "*"
+            },
+            "require-dev": {
+                "nette/tester": "~1.7",
+                "tracy/tracy": "~2.2"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.2-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ],
+                "files": [
+                    "src/loader.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause",
+                "GPL-2.0",
+                "GPL-3.0"
+            ],
+            "authors": [
+                {
+                    "name": "David Grudl",
+                    "homepage": "https://davidgrudl.com"
+                }
+            ],
+            "description": "Dibi is Database Abstraction Library for PHP",
+            "homepage": "https://dibiphp.com",
+            "keywords": [
+                "access",
+                "database",
+                "dbal",
+                "mssql",
+                "mysql",
+                "odbc",
+                "oracle",
+                "pdo",
+                "postgresql",
+                "sqlite",
+                "sqlsrv"
+            ],
+            "time": "2018-09-17T11:47:10+00:00"
+        },
+        {
+            "name": "guzzlehttp/guzzle",
+            "version": "6.3.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/guzzle.git",
+                "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba",
+                "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba",
+                "shasum": ""
+            },
+            "require": {
+                "guzzlehttp/promises": "^1.0",
+                "guzzlehttp/psr7": "^1.4",
+                "php": ">=5.5"
+            },
+            "require-dev": {
+                "ext-curl": "*",
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
+                "psr/log": "^1.0"
+            },
+            "suggest": {
+                "psr/log": "Required for using the Log middleware"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "6.3-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/functions_include.php"
+                ],
+                "psr-4": {
+                    "GuzzleHttp\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                }
+            ],
+            "description": "Guzzle is a PHP HTTP client library",
+            "homepage": "http://guzzlephp.org/",
+            "keywords": [
+                "client",
+                "curl",
+                "framework",
+                "http",
+                "http client",
+                "rest",
+                "web service"
+            ],
+            "time": "2018-04-22T15:46:56+00:00"
+        },
+        {
+            "name": "guzzlehttp/promises",
+            "version": "v1.3.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/promises.git",
+                "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
+                "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.5.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.4-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Promise\\": "src/"
+                },
+                "files": [
+                    "src/functions_include.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                }
+            ],
+            "description": "Guzzle promises library",
+            "keywords": [
+                "promise"
+            ],
+            "time": "2016-12-20T10:07:11+00:00"
+        },
+        {
+            "name": "guzzlehttp/psr7",
+            "version": "1.4.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/psr7.git",
+                "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
+                "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4.0",
+                "psr/http-message": "~1.0"
+            },
+            "provide": {
+                "psr/http-message-implementation": "1.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.4-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "GuzzleHttp\\Psr7\\": "src/"
+                },
+                "files": [
+                    "src/functions_include.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "PSR-7 message implementation that also provides common utility methods",
+            "keywords": [
+                "http",
+                "message",
+                "request",
+                "response",
+                "stream",
+                "uri",
+                "url"
+            ],
+            "time": "2017-03-20T17:10:46+00:00"
+        },
+        {
+            "name": "kryptonit3/couchpotato",
+            "version": "1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Kryptonit3/CouchPotato.git",
+                "reference": "7a1fc892f70f120f74ff005850e923a0f1566376"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Kryptonit3/CouchPotato/zipball/7a1fc892f70f120f74ff005850e923a0f1566376",
+                "reference": "7a1fc892f70f120f74ff005850e923a0f1566376",
+                "shasum": ""
+            },
+            "require": {
+                "guzzlehttp/guzzle": "^6.1"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Kryptonit3\\CouchPotato\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jesse Szypulski",
+                    "email": "jesse.szypulski@gmail.com"
+                }
+            ],
+            "description": "PHP Wrapper for CouchPotato API",
+            "time": "2016-02-06T22:02:39+00:00"
+        },
+        {
+            "name": "kryptonit3/sickrage",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Kryptonit3/SickRage.git",
+                "reference": "441a293b5c219c3cdd1ebebd2bcf4518598f84aa"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Kryptonit3/SickRage/zipball/441a293b5c219c3cdd1ebebd2bcf4518598f84aa",
+                "reference": "441a293b5c219c3cdd1ebebd2bcf4518598f84aa",
+                "shasum": ""
+            },
+            "require": {
+                "guzzlehttp/guzzle": "^6.1"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Kryptonit3\\SickRage\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jesse Szypulski",
+                    "email": "jesse.szypulski@gmail.com"
+                }
+            ],
+            "description": "PHP Wrapper for SickRage / SickBeard API",
+            "time": "2016-08-08T00:57:46+00:00"
+        },
+        {
+            "name": "kryptonit3/sonarr",
+            "version": "1.0.6.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Kryptonit3/Sonarr.git",
+                "reference": "e30c5c783a837270bcef81571ca9b95909c52e5e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Kryptonit3/Sonarr/zipball/e30c5c783a837270bcef81571ca9b95909c52e5e",
+                "reference": "e30c5c783a837270bcef81571ca9b95909c52e5e",
+                "shasum": ""
+            },
+            "require": {
+                "guzzlehttp/guzzle": "^6.1"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Kryptonit3\\Sonarr\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jesse Szypulski",
+                    "email": "jesse.szypulski@gmail.com"
+                }
+            ],
+            "description": "PHP Sonarr API Wrapper",
+            "time": "2017-06-30T01:25:49+00:00"
+        },
+        {
+            "name": "layershifter/tld-database",
+            "version": "1.0.64",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/layershifter/TLDDatabase.git",
+                "reference": "16d9b7cd195eda57c08c64f75e9dbc175320e0cc"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/layershifter/TLDDatabase/zipball/16d9b7cd195eda57c08c64f75e9dbc175320e0cc",
+                "reference": "16d9b7cd195eda57c08c64f75e9dbc175320e0cc",
+                "shasum": ""
+            },
+            "require": {
+                "ext-curl": "*",
+                "layershifter/tld-support": "^1.0.0",
+                "php": "^5.5.0 || ^7.0"
+            },
+            "require-dev": {
+                "codeclimate/php-test-reporter": "dev-master",
+                "mikey179/vfsstream": "^1.6",
+                "phpmd/phpmd": "@stable",
+                "phpunit/phpunit": "^4.8 || ^5.0",
+                "squizlabs/php_codesniffer": "~2.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "LayerShifter\\TLDDatabase\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "Alexander Fedyashov",
+                    "email": "a@fedyashov.com"
+                }
+            ],
+            "description": "Database abstraction for Public Suffix List",
+            "keywords": [
+                "PSL",
+                "Public Suffix List",
+                "domain database",
+                "tld database"
+            ],
+            "time": "2018-11-09T09:12:04+00:00"
+        },
+        {
+            "name": "layershifter/tld-extract",
+            "version": "2.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/layershifter/TLDExtract.git",
+                "reference": "236bfa0fd253e70f8db10f896e788e12b58b4a41"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/layershifter/TLDExtract/zipball/236bfa0fd253e70f8db10f896e788e12b58b4a41",
+                "reference": "236bfa0fd253e70f8db10f896e788e12b58b4a41",
+                "shasum": ""
+            },
+            "require": {
+                "layershifter/tld-database": "^1.0",
+                "layershifter/tld-support": "^1.1",
+                "php": "^5.5.0 || ^7.0",
+                "true/punycode": "^2.1.1"
+            },
+            "require-dev": {
+                "phpmd/phpmd": "@stable",
+                "phpunit/phpunit": "^4.8 || ^5.0",
+                "squizlabs/php_codesniffer": "~2.0"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/static.php"
+                ],
+                "psr-4": {
+                    "LayerShifter\\TLDExtract\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "Alexander Fedyashov",
+                    "email": "a@fedyashov.com"
+                }
+            ],
+            "description": "TLDExtract, library for extracting parts of domain, e.q. domain parser",
+            "keywords": [
+                "TLDExtract",
+                "domain parser"
+            ],
+            "time": "2018-09-28T11:11:20+00:00"
+        },
+        {
+            "name": "layershifter/tld-support",
+            "version": "1.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/layershifter/TLDSupport.git",
+                "reference": "9b9eb2350db82239289f4e6eb250a2fe4865b0db"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/layershifter/TLDSupport/zipball/9b9eb2350db82239289f4e6eb250a2fe4865b0db",
+                "reference": "9b9eb2350db82239289f4e6eb250a2fe4865b0db",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.5",
+                "symfony/polyfill-mbstring": "^1.2"
+            },
+            "require-dev": {
+                "codeclimate/php-test-reporter": "dev-master",
+                "phpmd/phpmd": "@stable",
+                "phpunit/phpunit": "4.8.*",
+                "squizlabs/php_codesniffer": "2.*"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "LayerShifter\\TLDSupport\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "Alexander Fedyashov",
+                    "email": "a@fedyashov.com"
+                }
+            ],
+            "description": "Support package for TLDDatabase and TLDExtract",
+            "time": "2016-06-18T12:28:04+00:00"
+        },
+        {
+            "name": "lcobucci/jwt",
+            "version": "3.2.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/lcobucci/jwt.git",
+                "reference": "0b5930be73582369e10c4d4bb7a12bac927a203c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/lcobucci/jwt/zipball/0b5930be73582369e10c4d4bb7a12bac927a203c",
+                "reference": "0b5930be73582369e10c4d4bb7a12bac927a203c",
+                "shasum": ""
+            },
+            "require": {
+                "ext-openssl": "*",
+                "php": ">=5.5"
+            },
+            "require-dev": {
+                "mdanter/ecc": "~0.3.1",
+                "mikey179/vfsstream": "~1.5",
+                "phpmd/phpmd": "~2.2",
+                "phpunit/php-invoker": "~1.1",
+                "phpunit/phpunit": "~4.5",
+                "squizlabs/php_codesniffer": "~2.3"
+            },
+            "suggest": {
+                "mdanter/ecc": "Required to use Elliptic Curves based algorithms."
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.1-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Lcobucci\\JWT\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Luís Otávio Cobucci Oblonczyk",
+                    "email": "lcobucci@gmail.com",
+                    "role": "Developer"
+                }
+            ],
+            "description": "A simple library to work with JSON Web Token and JSON Web Signature",
+            "keywords": [
+                "JWS",
+                "jwt"
+            ],
+            "time": "2017-09-01T08:23:26+00:00"
+        },
+        {
+            "name": "paragonie/constant_time_encoding",
+            "version": "v2.2.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/paragonie/constant_time_encoding.git",
+                "reference": "eccf915f45f911bfb189d1d1638d940ec6ee6e33"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/eccf915f45f911bfb189d1d1638d940ec6ee6e33",
+                "reference": "eccf915f45f911bfb189d1d1638d940ec6ee6e33",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6|^7",
+                "vimeo/psalm": "^1"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "ParagonIE\\ConstantTime\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Paragon Initiative Enterprises",
+                    "email": "security@paragonie.com",
+                    "homepage": "https://paragonie.com",
+                    "role": "Maintainer"
+                },
+                {
+                    "name": "Steve 'Sc00bz' Thomas",
+                    "email": "steve@tobtu.com",
+                    "homepage": "https://www.tobtu.com",
+                    "role": "Original Developer"
+                }
+            ],
+            "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
+            "keywords": [
+                "base16",
+                "base32",
+                "base32_decode",
+                "base32_encode",
+                "base64",
+                "base64_decode",
+                "base64_encode",
+                "bin2hex",
+                "encoding",
+                "hex",
+                "hex2bin",
+                "rfc4648"
+            ],
+            "time": "2018-03-10T19:47:49+00:00"
+        },
+        {
+            "name": "paragonie/random_compat",
+            "version": "v9.99.99",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/paragonie/random_compat.git",
+                "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
+                "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "4.*|5.*",
+                "vimeo/psalm": "^1"
+            },
+            "suggest": {
+                "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
+            },
+            "type": "library",
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Paragon Initiative Enterprises",
+                    "email": "security@paragonie.com",
+                    "homepage": "https://paragonie.com"
+                }
+            ],
+            "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
+            "keywords": [
+                "csprng",
+                "polyfill",
+                "pseudorandom",
+                "random"
+            ],
+            "time": "2018-07-02T15:55:56+00:00"
+        },
+        {
+            "name": "paragonie/sodium_compat",
+            "version": "v1.6.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/paragonie/sodium_compat.git",
+                "reference": "3f2fd07977541b4d630ea0365ad0eceddee5179c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/3f2fd07977541b4d630ea0365ad0eceddee5179c",
+                "reference": "3f2fd07977541b4d630ea0365ad0eceddee5179c",
+                "shasum": ""
+            },
+            "require": {
+                "paragonie/random_compat": ">=1",
+                "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^3|^4|^5"
+            },
+            "suggest": {
+                "ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.",
+                "ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security."
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "autoload.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "ISC"
+            ],
+            "authors": [
+                {
+                    "name": "Paragon Initiative Enterprises",
+                    "email": "security@paragonie.com"
+                },
+                {
+                    "name": "Frank Denis",
+                    "email": "jedisct1@pureftpd.org"
+                }
+            ],
+            "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists",
+            "keywords": [
+                "Authentication",
+                "BLAKE2b",
+                "ChaCha20",
+                "ChaCha20-Poly1305",
+                "Chapoly",
+                "Curve25519",
+                "Ed25519",
+                "EdDSA",
+                "Edwards-curve Digital Signature Algorithm",
+                "Elliptic Curve Diffie-Hellman",
+                "Poly1305",
+                "Pure-PHP cryptography",
+                "RFC 7748",
+                "RFC 8032",
+                "Salpoly",
+                "Salsa20",
+                "X25519",
+                "XChaCha20-Poly1305",
+                "XSalsa20-Poly1305",
+                "Xchacha20",
+                "Xsalsa20",
+                "aead",
+                "cryptography",
+                "ecdh",
+                "elliptic curve",
+                "elliptic curve cryptography",
+                "encryption",
+                "libsodium",
+                "php",
+                "public-key cryptography",
+                "secret-key cryptography",
+                "side-channel resistant"
+            ],
+            "time": "2018-08-29T22:02:48+00:00"
+        },
+        {
+            "name": "phpmailer/phpmailer",
+            "version": "v6.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/PHPMailer/PHPMailer.git",
+                "reference": "44d49bab5ab1fef721d3ee07e75dc0865ddf4cc6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/44d49bab5ab1fef721d3ee07e75dc0865ddf4cc6",
+                "reference": "44d49bab5ab1fef721d3ee07e75dc0865ddf4cc6",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "ext-filter": "*",
+                "php": ">=5.5.0"
+            },
+            "require-dev": {
+                "doctrine/annotations": "1.2.*",
+                "friendsofphp/php-cs-fixer": "^2.2",
+                "phpdocumentor/phpdocumentor": "2.*",
+                "phpunit/phpunit": "^4.8 || ^5.7",
+                "zendframework/zend-eventmanager": "3.0.*",
+                "zendframework/zend-i18n": "2.7.3",
+                "zendframework/zend-serializer": "2.7.*"
+            },
+            "suggest": {
+                "ext-mbstring": "Needed to send email in multibyte encoding charset",
+                "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
+                "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
+                "psr/log": "For optional PSR-3 debug logging",
+                "stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication",
+                "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "PHPMailer\\PHPMailer\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "LGPL-2.1"
+            ],
+            "authors": [
+                {
+                    "name": "Jim Jagielski",
+                    "email": "jimjag@gmail.com"
+                },
+                {
+                    "name": "Marcus Bointon",
+                    "email": "phpmailer@synchromedia.co.uk"
+                },
+                {
+                    "name": "Andy Prevost",
+                    "email": "codeworxtech@users.sourceforge.net"
+                },
+                {
+                    "name": "Brent R. Matzelle"
+                }
+            ],
+            "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
+            "time": "2018-01-05T13:19:58+00:00"
+        },
+        {
+            "name": "pragmarx/google2fa",
+            "version": "v3.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/antonioribeiro/google2fa.git",
+                "reference": "6949226739e4424f40031e6f1c96b1fd64047335"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/6949226739e4424f40031e6f1c96b1fd64047335",
+                "reference": "6949226739e4424f40031e6f1c96b1fd64047335",
+                "shasum": ""
+            },
+            "require": {
+                "paragonie/constant_time_encoding": "~1.0|~2.0",
+                "paragonie/random_compat": ">=1",
+                "php": ">=5.4",
+                "symfony/polyfill-php56": "~1.2"
+            },
+            "require-dev": {
+                "bacon/bacon-qr-code": "~1.0",
+                "phpunit/phpunit": "~4|~5|~6"
+            },
+            "suggest": {
+                "bacon/bacon-qr-code": "Required to generate inline QR Codes."
+            },
+            "type": "library",
+            "extra": {
+                "component": "package",
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "PragmaRX\\Google2FA\\": "src/",
+                    "PragmaRX\\Google2FA\\Tests\\": "tests/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Antonio Carlos Ribeiro",
+                    "email": "acr@antoniocarlosribeiro.com",
+                    "role": "Creator & Designer"
+                }
+            ],
+            "description": "A One Time Password Authentication package, compatible with Google Authenticator.",
+            "keywords": [
+                "2fa",
+                "Authentication",
+                "Two Factor Authentication",
+                "google2fa",
+                "laravel"
+            ],
+            "time": "2018-08-29T13:28:06+00:00"
+        },
+        {
+            "name": "psr/http-message",
+            "version": "1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-message.git",
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Message\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP messages",
+            "homepage": "https://github.com/php-fig/http-message",
+            "keywords": [
+                "http",
+                "http-message",
+                "psr",
+                "psr-7",
+                "request",
+                "response"
+            ],
+            "time": "2016-08-06T14:39:51+00:00"
+        },
+        {
+            "name": "psr/log",
+            "version": "1.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/log.git",
+                "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
+                "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Log\\": "Psr/Log/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for logging libraries",
+            "homepage": "https://github.com/php-fig/log",
+            "keywords": [
+                "log",
+                "psr",
+                "psr-3"
+            ],
+            "time": "2016-10-10T12:19:37+00:00"
+        },
+        {
+            "name": "pusher/pusher-php-server",
+            "version": "v3.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/pusher/pusher-http-php.git",
+                "reference": "4da4d208fe13fd2ff71ede5063a3456f23f38cf7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/4da4d208fe13fd2ff71ede5063a3456f23f38cf7",
+                "reference": "4da4d208fe13fd2ff71ede5063a3456f23f38cf7",
+                "shasum": ""
+            },
+            "require": {
+                "ext-curl": "*",
+                "paragonie/sodium_compat": "^1.6",
+                "php": "^5.4 || ^7.0",
+                "psr/log": "^1.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.8 || ^5.7"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Pusher\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Library for interacting with the Pusher REST API",
+            "keywords": [
+                "events",
+                "messaging",
+                "php-pusher-server",
+                "publish",
+                "push",
+                "pusher",
+                "real time",
+                "real-time",
+                "realtime",
+                "rest",
+                "trigger"
+            ],
+            "time": "2018-08-13T14:57:32+00:00"
+        },
+        {
+            "name": "rmccue/requests",
+            "version": "v1.7.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/rmccue/Requests.git",
+                "reference": "87932f52ffad70504d93f04f15690cf16a089546"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/rmccue/Requests/zipball/87932f52ffad70504d93f04f15690cf16a089546",
+                "reference": "87932f52ffad70504d93f04f15690cf16a089546",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.2"
+            },
+            "require-dev": {
+                "requests/test-server": "dev-master"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "Requests": "library/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "ISC"
+            ],
+            "authors": [
+                {
+                    "name": "Ryan McCue",
+                    "homepage": "http://ryanmccue.info"
+                }
+            ],
+            "description": "A HTTP library written in PHP, for human beings.",
+            "homepage": "http://github.com/rmccue/Requests",
+            "keywords": [
+                "curl",
+                "fsockopen",
+                "http",
+                "idna",
+                "ipv6",
+                "iri",
+                "sockets"
+            ],
+            "time": "2016-10-13T00:11:37+00:00"
+        },
+        {
+            "name": "symfony/polyfill-mbstring",
+            "version": "v1.10.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-mbstring.git",
+                "reference": "c79c051f5b3a46be09205c73b80b346e4153e494"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494",
+                "reference": "c79c051f5b3a46be09205c73b80b346e4153e494",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "suggest": {
+                "ext-mbstring": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.9-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Mbstring\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for the Mbstring extension",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "mbstring",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2018-09-21T13:07:52+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php56",
+            "version": "v1.9.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php56.git",
+                "reference": "7b4fc009172cc0196535b0328bd1226284a28000"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/7b4fc009172cc0196535b0328bd1226284a28000",
+                "reference": "7b4fc009172cc0196535b0328bd1226284a28000",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3",
+                "symfony/polyfill-util": "~1.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.9-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php56\\": ""
+                },
+                "files": [
+                    "bootstrap.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "time": "2018-08-06T14:22:27+00:00"
+        },
+        {
+            "name": "symfony/polyfill-util",
+            "version": "v1.9.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-util.git",
+                "reference": "8e15d04ba3440984d23e7964b2ee1d25c8de1581"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/8e15d04ba3440984d23e7964b2ee1d25c8de1581",
+                "reference": "8e15d04ba3440984d23e7964b2ee1d25c8de1581",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.9-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Polyfill\\Util\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony utilities for portability of PHP codes",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compat",
+                "compatibility",
+                "polyfill",
+                "shim"
+            ],
+            "time": "2018-08-06T14:22:27+00:00"
+        },
+        {
+            "name": "true/punycode",
+            "version": "v2.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/true/php-punycode.git",
+                "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/true/php-punycode/zipball/a4d0c11a36dd7f4e7cd7096076cab6d3378a071e",
+                "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0",
+                "symfony/polyfill-mbstring": "^1.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.7",
+                "squizlabs/php_codesniffer": "~2.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "TrueBV\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Renan Gonçalves",
+                    "email": "renan.saddam@gmail.com"
+                }
+            ],
+            "description": "A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA)",
+            "homepage": "https://github.com/true/php-punycode",
+            "keywords": [
+                "idna",
+                "punycode"
+            ],
+            "time": "2016-11-16T10:37:54+00:00"
+        }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": []
+}

+ 205 - 0
api/config/default.php

@@ -0,0 +1,205 @@
+<?php
+return array(
+	'branch' => 'v2-master',
+	'authType' => 'internal',
+	'authBackend' => '',
+	'authBackendHost' => '',
+	'authBaseDN' => '',
+	'authBackendDomain' => '',
+	'logo' => 'plugins/images/organizr/logo-wide.png',
+	'loginWallpaper' => '',
+	'title' => 'Organizr V2',
+	'useLogo' => false,
+	'headerColor' => '',
+	'headerTextColor' => '',
+	'sidebarColor' => '',
+	'sidebarTextColor' => '',
+	'accentColor' => '',
+	'accentTextColor' => '',
+	'buttonColor' => '',
+	'buttonTextColor' => '',
+	'buttonHoverColor' => '',
+	'buttonTextHoverColor' => '',
+	'alternateHomepageHeaders' => false,
+	'lockScreen' => false,
+	'theme' => 'Organizr',
+	'style' => 'dark',
+	'plexURL' => '',
+	'plexTabURL' => '',
+	'plexToken' => '',
+	'plexTabName' => '',
+	'plexAdmin' => '',
+	'embyTabURL' => '',
+	'embyTabName' => '',
+	'embyURL' => '',
+	'embyToken' => '',
+	'plexID' => '',
+	'tautulliURL' => '',
+	'ombiURL' => '',
+	'ombiToken' => '',
+	'ssoPlex' => false,
+	'ssoOmbi' => false,
+	'ssoTautulli' => false,
+	'sonarrURL' => '',
+	'sonarrUnmonitored' => false,
+	'sonarrToken' => '',
+	'lidarrURL' => '',
+	'lidarrToken' => '',
+	'radarrURL' => '',
+	'radarrToken' => '',
+	'couchpotatoURL' => '',
+	'couchpotatoToken' => '',
+	'sickrageURL' => '',
+	'sickrageToken' => '',
+	'sabnzbdURL' => '',
+	'sabnzbdToken' => '',
+	'sabnzbdCombine' => false,
+	'nzbgetURL' => '',
+	'nzbgetUsername' => '',
+	'nzbgetPassword' => '',
+	'nzbgetCombine' => false,
+	'transmissionURL' => '',
+	'transmissionUsername' => '',
+	'transmissionPassword' => '',
+	'transmissionHideSeeding' => false,
+	'transmissionHideCompleted' => false,
+	'transmissionCombine' => false,
+	'delugeURL' => '',
+	'delugePassword' => '',
+	'delugeHideSeeding' => false,
+	'delugeHideCompleted' => false,
+	'delugeCombine' => false,
+	'qBittorrentURL' => '',
+	'qBittorrentUsername' => '',
+	'qBittorrentPassword' => '',
+	'qBittorrentHideSeeding' => false,
+	'qBittorrentHideCompleted' => false,
+	'qBittorrentSortOrder' => 'eta',
+	'qBittorrentReverseSorting' => false,
+	'qBittorrentCombine' => false,
+	'rTorrentURL' => '',
+	'rTorrentUsername' => '',
+	'rTorrentPassword' => '',
+	'rTorrentHideSeeding' => false,
+	'rTorrentHideCompleted' => false,
+	'rTorrentSortOrder' => 'datea',
+	'rTorrentReverseSorting' => false,
+	'rTorrentCombine' => false,
+	'homepageCalendarEnabled' => 'true',
+	'homepageCalendarAuth' => '4',
+	'calendariCal' => '',
+	'homepagCustomHTMLoneEnabled' => false,
+	'homepagCustomHTMLoneAuth' => '1',
+	'homepagCustomHTMLtwoEnabled' => false,
+	'homepagCustomHTMLtwoAuth' => '1',
+	'homepageDelugeEnabled' => false,
+	'homepageDelugeAuth' => '1',
+	'homepageSabnzbdEnabled' => false,
+	'homepageSabnzbdAuth' => '1',
+	'homepageSonarrEnabled' => false,
+	'homepageSonarrAuth' => '1',
+	'homepageLidarrEnabled' => false,
+	'homepageLidarrAuth' => '1',
+	'homepageCouchpotatoEnabled' => false,
+	'homepageCouchpotatoAuth' => '1',
+	'homepageSickrageEnabled' => false,
+	'homepageSickrageAuth' => '1',
+	'homepageRadarrEnabled' => false,
+	'homepageRadarrAuth' => '1',
+	'homepageTransmissionEnabled' => false,
+	'homepageTransmissionAuth' => '1',
+	'homepageqBittorrentEnabled' => false,
+	'homepageqBittorrentAuth' => '1',
+	'homepagerTorrentEnabled' => false,
+	'homepagerTorrentAuth' => '1',
+	'homepageNzbgetEnabled' => false,
+	'homepageNzbgetAuth' => '1',
+	'homepagePlexEnabled' => false,
+	'homepagePlexAuth' => '1',
+	'homepageEmbyEnabled' => false,
+	'homepageEmbyAuth' => '1',
+	'homepageOmbiEnabled' => false,
+	'homepageOmbiAuth' => '1',
+	'homepageOmbiRequestAuth' => '1',
+	'ombiLimitUser' => false,
+	'ombiRefresh' => '600000',
+	'ombiTvDefault' => 'all',
+	'homepageOrdercustomhtml' => '1',
+	'homepageOrdercustomhtmlTwo' => '2',
+	'homepageOrdertransmission' => '3',
+	'homepageOrderqBittorrent' => '4',
+	'homepageOrderdeluge' => '5',
+	'homepageOrdernzbget' => '6',
+	'homepageOrdersabnzbd' => '7',
+	'homepageOrderplexnowplaying' => '8',
+	'homepageOrderplexrecent' => '9',
+	'homepageOrderplexplaylist' => '10',
+	'homepageOrderembynowplaying' => '11',
+	'homepageOrderembyrecent' => '12',
+	'homepageOrderombi' => '13',
+	'homepageOrdercalendar' => '14',
+	'homepageOrderrTorrent' => '15',
+	'homepageOrderdownloader' => '16',
+	'homepageShowStreamNames' => false,
+	'homepageShowStreamNamesAuth' => '1',
+	'homepageStreamRefresh' => '60000',
+	'homepageRecentRefresh' => '60000',
+	'homepageDownloadRefresh' => '60000',
+	'homepagePlexStreams' => false,
+	'homepagePlexStreamsAuth' => '1',
+	'homepagePlexRecent' => false,
+	'homepageRecentLimit' => '50',
+	'homepagePlexRecentAuth' => '1',
+	'homepagePlexPlaylist' => false,
+	'homepagePlexPlaylistAuth' => '1',
+	'homepageEmbyStreams' => false,
+	'homepageEmbyStreamsAuth' => '1',
+	'homepageEmbyRecent' => false,
+	'homepageEmbyRecentAuth' => '1',
+	'calendarDefault' => 'month',
+	'calendarFirstDay' => '1',
+	'calendarStart' => '14',
+	'calendarEnd' => '14',
+	'calendarRefresh' => '60000',
+	'calendarTimeFormat' => 'h(:mm)t',
+	'calendarLimit' => '1000',
+	'customCss' => '',
+	'customThemeCss' => '',
+	'customHTMLone' => '',
+	'customHTMLtwo' => '',
+	'mediaSearch' => false,
+	'mediaSearchType' => '',
+	'mediaSearchAuth' => '1',
+	'registrationPassword' => '',
+	'hideRegistration' => false,
+	'favIcon' => '',
+	'pingAuth' => '1',
+	'pingAuthMessage' => '1',
+	'adminPingRefresh' => '60000',
+	'otherPingRefresh' => '600000',
+	'pingOfflineSound' => 'plugins/sounds/default/open-ended.mp3',
+	'pingOnlineSound' => 'plugins/sounds/default/awareness.mp3',
+	'pingMs' => false,
+	'pingAuthMs' => '1',
+	'notificationBackbone' => 'izi',
+	'notificationPosition' => 'br',
+	'lockoutSystem' => false,
+	'lockoutTimeout' => '60',
+	'lockoutMaxAuth' => '0',
+	'lockoutMinAuth' => '1',
+	'themeInstalled' => '',
+	'themeVersion' => '',
+	'installedPlugins' => '',
+	'installedThemes' => '',
+	'authDebug' => false,
+	'customJava' => '',
+	'customThemeJava' => '',
+	'minimalLoginScreen' => false,
+	'unsortedTabs' => 'top',
+	'cacheImageSize' => '2',
+	'plexoAuth' => false,
+	'statusSounds' => false,
+	'rememberMeDays' => '7',
+	'rememberMe' => true,
+	'plexStrictFriends' => true
+);

+ 38 - 0
api/functions.php

@@ -0,0 +1,38 @@
+<?php
+// Set UTC timeone
+date_default_timezone_set("UTC");
+// Autoload frameworks
+require_once(__DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php');
+// Include all function files
+foreach (glob(__DIR__ . DIRECTORY_SEPARATOR . 'functions' . DIRECTORY_SEPARATOR . '*.php') as $filename) {
+	require_once $filename;
+}
+// Set Root Directory
+$GLOBALS['root'] = dirname(__DIR__, 1);
+$GLOBALS['uuid'] = '';
+// Add in default and custom settings
+configLazy();
+// Define Logs and files after db location is set
+if (isset($GLOBALS['dbLocation'])) {
+	$GLOBALS['organizrLog'] = $GLOBALS['dbLocation'] . 'organizrLog.json';
+	$GLOBALS['organizrLoginLog'] = $GLOBALS['dbLocation'] . 'organizrLoginLog.json';
+	if (($GLOBALS['uuid'] == '')) {
+		$uuid = gen_uuid();
+		$GLOBALS['uuid'] = $uuid;
+		updateConfig(array('uuid' => $uuid));
+	}
+	//Upgrade Check
+	upgradeCheck();
+}
+// Cookie name
+$GLOBALS['cookieName'] = $GLOBALS['uuid'] !== '' ? 'organizr_token_' . $GLOBALS['uuid'] : 'organizr_token_temp';
+// Validate Token if set and set guest if not - sets GLOBALS
+getOrganizrUserToken();
+// Include all pages files
+foreach (glob(__DIR__ . DIRECTORY_SEPARATOR . 'pages' . DIRECTORY_SEPARATOR . "*.php") as $filename) {
+	require_once $filename;
+}
+// Include all plugin files
+foreach (glob(__DIR__ . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . "*.php") as $filename) {
+	require_once $filename;
+}

+ 78 - 0
api/functions/2fa-functions.php

@@ -0,0 +1,78 @@
+<?php
+function create2FA($type)
+{
+	$result['type'] = $type;
+	switch ($type) {
+		case 'google':
+			try {
+				$google2fa = new PragmaRX\Google2FA\Google2FA();
+				$google2fa->setAllowInsecureCallToGoogleApis(true);
+				$result['secret'] = $google2fa->generateSecretKey();
+				$result['url'] = $google2fa->getQRCodeGoogleUrl(
+					$GLOBALS['title'],
+					$GLOBALS['organizrUser']['username'],
+					$result['secret']
+				);
+			} catch (PragmaRX\Google2FA\Exceptions\InsecureCallException $e) {
+				return false;
+			}
+			break;
+		default:
+			return false;
+	}
+	return $result;
+}
+
+function save2FA($secret, $type)
+{
+	try {
+		$connect = new Dibi\Connection([
+			'driver' => 'sqlite3',
+			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
+		]);
+		$connect->query('
+            UPDATE users SET', [
+			'auth_service' => $type . '::' . $secret
+		], '
+            WHERE id=?', $GLOBALS['organizrUser']['userID']);
+		writeLog('success', 'User Management Function - User added 2FA', $GLOBALS['organizrUser']['username']);
+		return true;
+	} catch (Dibi\Exception $e) {
+		writeLog('error', 'User Management Function - Error Adding User 2FA', $GLOBALS['organizrUser']['username']);
+		return false;
+	}
+}
+
+function verify2FA($secret, $code, $type)
+{
+	switch ($type) {
+		case 'google':
+			$google2fa = new PragmaRX\Google2FA\Google2FA();
+			$google2fa->setWindow(5);
+			$valid = $google2fa->verifyKey($secret, $code);
+			break;
+		default:
+			return false;
+	}
+	return ($valid) ? true : false;
+}
+
+function remove2FA()
+{
+	try {
+		$connect = new Dibi\Connection([
+			'driver' => 'sqlite3',
+			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
+		]);
+		$connect->query('
+            UPDATE users SET', [
+			'auth_service' => 'internal'
+		], '
+            WHERE id=?', $GLOBALS['organizrUser']['userID']);
+		writeLog('success', 'User Management Function - User removed 2FA', $GLOBALS['organizrUser']['username']);
+		return true;
+	} catch (Dibi\Exception $e) {
+		writeLog('error', 'User Management Function - Error Removing User 2FA', $GLOBALS['organizrUser']['username']);
+		return false;
+	}
+}

+ 1159 - 0
api/functions/api-functions.php

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

+ 353 - 0
api/functions/auth-functions.php

@@ -0,0 +1,353 @@
+<?php
+function authRegister($username, $password, $defaults, $email, $token = null)
+{
+	if ($GLOBALS['authBackend'] !== '') {
+		ombiImport($GLOBALS['authBackend']);
+	}
+	ssoCheck($username, $password, $token);
+	if (createUser($username, $password, $defaults, $email)) {
+		writeLog('success', 'Registration Function - A User has registered', $username);
+		if ($GLOBALS['PHPMAILER-enabled']) {
+			$emailTemplate = array(
+				'type' => 'registration',
+				'body' => $GLOBALS['PHPMAILER-emailTemplateRegisterUser'],
+				'subject' => $GLOBALS['PHPMAILER-emailTemplateRegisterUserSubject'],
+				'user' => $username,
+				'password' => null,
+				'inviteCode' => null,
+			);
+			$emailTemplate = phpmEmailTemplate($emailTemplate);
+			$sendEmail = array(
+				'to' => $email,
+				'user' => $username,
+				'subject' => $emailTemplate['subject'],
+				'body' => phpmBuildEmail($emailTemplate),
+			);
+			phpmSendEmail($sendEmail);
+		}
+		if (createToken($username, $email, gravatar($email), $defaults['group'], $defaults['group_id'], $GLOBALS['organizrHash'], $GLOBALS['rememberMeDays'])) {
+			writeLoginLog($username, 'success');
+			writeLog('success', 'Login Function - A User has logged in', $username);
+			return true;
+		}
+	} else {
+		writeLog('error', 'Registration Function - An error occurred', $username);
+		return 'username taken';
+	}
+	return false;
+}
+
+function checkPlexToken($token = '')
+{
+	try {
+		if (($token !== '')) {
+			$url = 'https://plex.tv/users/account.json';
+			$headers = array(
+				'X-Plex-Token' => $token,
+				'Content-Type' => 'application/json',
+				'Accept' => 'application/json'
+			);
+			$response = Requests::get($url, $headers);
+			if ($response->success) {
+				return json_decode($response->body, true);
+			}
+		} else {
+			return false;
+		}
+		
+	} catch (Requests_Exception $e) {
+		writeLog('success', 'Plex Token Check Function - Error: ' . $e->getMessage(), SYSTEM);
+	}
+	return false;
+}
+
+function checkPlexUser($username)
+{
+	try {
+		if (!empty($GLOBALS['plexToken'])) {
+			$url = 'https://plex.tv/api/users';
+			$headers = array(
+				'X-Plex-Token' => $GLOBALS['plexToken'],
+			);
+			$response = Requests::get($url, $headers);
+			if ($response->success) {
+				libxml_use_internal_errors(true);
+				$userXML = simplexml_load_string($response->body);
+				if (is_array($userXML) || is_object($userXML)) {
+					$usernameLower = strtolower($username);
+					foreach ($userXML as $child) {
+						if (isset($child['username']) && strtolower($child['username']) == $usernameLower || isset($child['email']) && strtolower($child['email']) == $usernameLower) {
+							writeLog('success', 'Plex User Check - Found User on Friends List', $username);
+							$machineMatches = false;
+							if ($GLOBALS['plexStrictFriends']) {
+								foreach ($child->Server as $server) {
+									if ((string)$server['machineIdentifier'] == $GLOBALS['plexID']) {
+										$machineMatches = true;
+									}
+								}
+							} else {
+								$machineMatches = true;
+							}
+							if ($machineMatches) {
+								writeLog('success', 'Plex User Check - User Approved for Login', $username);
+								return true;
+							} else {
+								writeLog('error', 'Plex User Check - User not Approved User', $username);
+							}
+						}
+					}
+				}
+			}
+		}
+		return false;
+	} catch (Requests_Exception $e) {
+		writeLog('error', 'Plex User Check Function - Error: ' . $e->getMessage(), $username);
+	}
+	return false;
+}
+
+function allPlexUsers($newOnly = false)
+{
+	try {
+		if (!empty($GLOBALS['plexToken'])) {
+			$url = 'https://plex.tv/api/users';
+			$headers = array(
+				'X-Plex-Token' => $GLOBALS['plexToken'],
+			);
+			$response = Requests::get($url, $headers);
+			if ($response->success) {
+				libxml_use_internal_errors(true);
+				$userXML = simplexml_load_string($response->body);
+				if (is_array($userXML) || is_object($userXML)) {
+					$results = array();
+					foreach ($userXML as $child) {
+						if (((string)$child['restricted'] == '0')) {
+							if ($newOnly) {
+								$taken = usernameTaken((string)$child['username'], (string)$child['email']);
+								if (!$taken) {
+									$results[] = array(
+										'username' => (string)$child['username'],
+										'email' => (string)$child['email']
+									);
+								}
+							} else {
+								$results[] = array(
+									'username' => (string)$child['username'],
+									'email' => (string)$child['email'],
+								);
+							}
+							
+						}
+					}
+					return $results;
+				}
+			}
+		}
+		return false;
+	} catch (Requests_Exception $e) {
+		writeLog('success', 'Plex User Function - Error: ' . $e->getMessage(), $username);
+	}
+	return false;
+}
+
+function plugin_auth_plex($username, $password)
+{
+	try {
+		$usernameLower = strtolower($username);
+		if ((!empty($GLOBALS['plexAdmin']) && strtolower($GLOBALS['plexAdmin']) == $usernameLower) || checkPlexUser($username)) {
+			//Login User
+			$url = 'https://plex.tv/users/sign_in.json';
+			$headers = array(
+				'Accept' => 'application/json',
+				'Content-Type' => 'application/x-www-form-urlencoded',
+				'X-Plex-Product' => 'Organizr',
+				'X-Plex-Version' => '2.0',
+				'X-Plex-Client-Identifier' => $GLOBALS['uuid'],
+			);
+			$data = array(
+				'user[login]' => $username,
+				'user[password]' => $password,
+			);
+			$response = Requests::post($url, $headers, $data);
+			if ($response->success) {
+				$json = json_decode($response->body, true);
+				if ((is_array($json) && isset($json['user']) && isset($json['user']['username'])) && strtolower($json['user']['username']) == $usernameLower || strtolower($json['user']['email']) == $usernameLower) {
+					//writeLog("success", $json['user']['username']." was logged into organizr using plex credentials");
+					return array(
+						'username' => $json['user']['username'],
+						'email' => $json['user']['email'],
+						'image' => $json['user']['thumb'],
+						'token' => $json['user']['authToken']
+					);
+				}
+			}
+		}
+		return false;
+	} catch (Requests_Exception $e) {
+		writeLog('success', 'Plex Auth Function - Error: ' . $e->getMessage(), $username);
+	}
+	return false;
+}
+
+if (function_exists('ldap_connect')) {
+	// Pass credentials to LDAP backend
+	function plugin_auth_ldap($username, $password)
+	{
+		if (!empty($GLOBALS['authBaseDN']) && !empty($GLOBALS['authBackendHost'])) {
+			$ldapServers = explode(',', $GLOBALS['authBackendHost']);
+			foreach ($ldapServers as $key => $value) {
+				// Calculate parts
+				$digest = parse_url(trim($value));
+				$scheme = strtolower((isset($digest['scheme']) ? $digest['scheme'] : 'ldap'));
+				$host = (isset($digest['host']) ? $digest['host'] : (isset($digest['path']) ? $digest['path'] : ''));
+				$port = (isset($digest['port']) ? $digest['port'] : (strtolower($scheme) == 'ldap' ? 389 : 636));
+				// Reassign
+				$ldapServers[$key] = $scheme . '://' . $host . ':' . $port;
+			}
+			$ldap = ldap_connect(implode(' ', $ldapServers));
+			ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
+			ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
+			$bind = @ldap_bind($ldap, sprintf($GLOBALS['authBaseDN'], $username), $password);
+			return ($bind) ? true : false;
+		}
+		return false;
+	}
+} else {
+	// Ldap Auth Missing Dependency
+	function plugin_auth_ldap_disabled()
+	{
+		return 'LDAP - Disabled (Dependency: php-ldap missing!)';
+	}
+}
+// Pass credentials to FTP backend
+function plugin_auth_ftp($username, $password)
+{
+	// Calculate parts
+	$digest = parse_url($GLOBALS['authBackendHost']);
+	$scheme = strtolower((isset($digest['scheme']) ? $digest['scheme'] : (function_exists('ftp_ssl_connect') ? 'ftps' : 'ftp')));
+	$host = (isset($digest['host']) ? $digest['host'] : (isset($digest['path']) ? $digest['path'] : ''));
+	$port = (isset($digest['port']) ? $digest['port'] : 21);
+	// Determine Connection Type
+	if ($scheme == 'ftps') {
+		$conn_id = ftp_ssl_connect($host, $port, 20);
+	} elseif ($scheme == 'ftp') {
+		$conn_id = ftp_connect($host, $port, 20);
+	} else {
+		return false;
+	}
+	// Check if valid FTP connection
+	if ($conn_id) {
+		// Attempt login
+		@$login_result = ftp_login($conn_id, $username, $password);
+		ftp_close($conn_id);
+		// Return Result
+		if ($login_result) {
+			return true;
+		} else {
+			return false;
+		}
+	} else {
+		return false;
+	}
+}
+
+// Pass credentials to Emby Backend
+function plugin_auth_emby_local($username, $password)
+{
+	try {
+		$url = qualifyURL($GLOBALS['embyURL']) . '/Users/AuthenticateByName';
+		$headers = array(
+			'Authorization' => 'MediaBrowser UserId="e8837bc1-ad67-520e-8cd2-f629e3155721", Client="None", Device="Organizr", DeviceId="xxx", Version="1.0.0.0"',
+			'Content-Type' => 'application/json',
+		);
+		$data = array(
+			'Username' => $username,
+			'Password' => sha1($password),
+			'PasswordMd5' => md5($password),
+		);
+		$response = Requests::post($url, $headers, json_encode($data));
+		if ($response->success) {
+			$json = json_decode($response->body, true);
+			if (is_array($json) && isset($json['SessionInfo']) && isset($json['User']) && $json['User']['HasPassword'] == true) {
+				// Login Success - Now Logout Emby Session As We No Longer Need It
+				$headers = array(
+					'X-Mediabrowser-Token' => $json['AccessToken'],
+				);
+				$response = Requests::post(qualifyURL($GLOBALS['embyURL']) . '/Sessions/Logout', $headers, array());
+				if ($response->success) {
+					return true;
+				}
+			}
+		}
+		return false;
+	} catch (Requests_Exception $e) {
+		writeLog('error', 'Emby Local Auth Function - Error: ' . $e->getMessage(), $username);
+	}
+	return false;
+}
+
+// Authenticate against emby connect
+function plugin_auth_emby_connect($username, $password)
+{
+	try {
+		// Get A User
+		$connectId = '';
+		$url = qualifyURL($GLOBALS['embyURL']) . '/Users?api_key=' . $GLOBALS['embyToken'];
+		$response = Requests::get($url);
+		if ($response->success) {
+			$json = json_decode($response->body, true);
+			if (is_array($json)) {
+				foreach ($json as $key => $value) { // Scan for this user
+					if (isset($value['ConnectUserName']) && isset($value['ConnectUserId'])) { // Qualify as connect account
+						if ($value['ConnectUserName'] == $username || $value['Name'] == $username) {
+							$connectId = $value['ConnectUserId'];
+							writeLog('success', 'Emby Connect Auth Function - Found User', $username);
+							break;
+						}
+					}
+				}
+				if ($connectId) {
+					writeLog('success', 'Emby Connect Auth Function - Attempting to Login with Emby ID: ' . $connectId, $username);
+					$connectURL = 'https://connect.emby.media/service/user/authenticate';
+					$headers = array(
+						'Accept' => 'application/json',
+						'X-Application' => 'Organizr/2.0'
+					);
+					$data = array(
+						'nameOrEmail' => $username,
+						'rawpw' => $password,
+					);
+					$response = Requests::post($connectURL, $headers, $data);
+					if ($response->success) {
+						$json = json_decode($response->body, true);
+						if (is_array($json) && isset($json['AccessToken']) && isset($json['User']) && $json['User']['Id'] == $connectId) {
+							return array(
+								'email' => $json['User']['Email'],
+								'image' => $json['User']['ImageUrl'],
+							);
+						} else {
+							writeLog('error', 'Emby Connect Auth Function - Bad Response', $username);
+						}
+					} else {
+						writeLog('error', 'Emby Connect Auth Function - 401 From Emby Connect', $username);
+					}
+				}
+			}
+		}
+		return false;
+	} catch (Requests_Exception $e) {
+		writeLog('error', 'Emby Connect Auth Function - Error: ' . $e->getMessage(), $username);
+		return false;
+	}
+}
+
+// Authenticate Against Emby Local (first) and Emby Connect
+function plugin_auth_emby_all($username, $password)
+{
+	$localResult = plugin_auth_emby_local($username, $password);
+	if ($localResult) {
+		return $localResult;
+	} else {
+		return plugin_auth_emby_connect($username, $password);
+	}
+}

+ 56 - 0
api/functions/backup-functions.php

@@ -0,0 +1,56 @@
+<?php
+function fileArray($files)
+{
+	foreach ($files as $file) {
+		if (file_exists($file)) {
+			$list[] = $file;
+		}
+	}
+	if (!empty($list)) {
+		return $list;
+	}
+}
+
+function backupDB($type = 'config')
+{
+	$directory = $GLOBALS['dbLocation'] . 'backups' . DIRECTORY_SEPARATOR;
+	@mkdir($directory, 0770, true);
+	switch ($type) {
+		case 'config':
+			break;
+		case 'full':
+			break;
+		default:
+		
+	}
+	$orgFiles = array(
+		'orgLog' => $GLOBALS['organizrLog'],
+		'loginLog' => $GLOBALS['organizrLoginLog'],
+		'config' => $GLOBALS['userConfigPath'],
+		'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName']
+	);
+	$files = fileArray($orgFiles);
+	if (!empty($files)) {
+		writeLog('success', 'BACKUP: backup process started', 'SYSTEM');
+		$zipname = $directory . 'backup[' . date('Y-m-d_H-i') . '][' . $GLOBALS['installedVersion'] . '].zip';
+		$zip = new ZipArchive;
+		$zip->open($zipname, ZipArchive::CREATE);
+		foreach ($files as $file) {
+			$zip->addFile($file);
+		}
+		$zip->close();
+		writeLog('success', 'BACKUP: backup process finished', 'SYSTEM');
+		return true;
+	} else {
+		return false;
+	}
+	
+}
+
+function getBackups()
+{
+	$path = $GLOBALS['dbLocation'] . 'backups' . DIRECTORY_SEPARATOR;
+	@mkdir($path, 0770, true);
+	$files = array_diff(scandir($path), array('.', '..'));
+	return array_reverse($files);
+}

+ 1 - 0
api/functions/cert/index.html

@@ -0,0 +1 @@
+cert file in this directory

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

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

+ 572 - 0
api/functions/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;
+	}
+}

+ 2224 - 0
api/functions/homepage-connect-functions.php

@@ -0,0 +1,2224 @@
+<?php
+/** @noinspection PhpUndefinedFieldInspection */
+function homepageConnect($array)
+{
+	switch ($array['data']['action']) {
+		case 'getPlexStreams':
+			return (qualifyRequest($GLOBALS['homepagePlexStreamsAuth'])) ? plexConnect('streams') : false;
+			break;
+		case 'getPlexRecent':
+			return (qualifyRequest($GLOBALS['homepagePlexRecentAuth'])) ? plexConnect('recent') : false;
+			break;
+		case 'getPlexMetadata':
+			return (qualifyRequest($GLOBALS['homepagePlexAuth'])) ? plexConnect('metadata', $array['data']['key']) : false;
+			break;
+		case 'getPlexSearch':
+			return (qualifyRequest($GLOBALS['mediaSearchAuth'])) ? plexConnect('search', $array['data']['query']) : false;
+			break;
+		case 'getPlexPlaylists':
+			return (qualifyRequest($GLOBALS['homepagePlexPlaylistAuth'])) ? getPlexPlaylists() : false;
+			break;
+		case 'getEmbyStreams':
+			return (qualifyRequest($GLOBALS['homepageEmbyStreamsAuth'])) ? embyConnect('streams') : false;
+			break;
+		case 'getEmbyRecent':
+			return (qualifyRequest($GLOBALS['homepageEmbyRecentAuth'])) ? embyConnect('recent') : false;
+			break;
+		case 'getEmbyMetadata':
+			return (qualifyRequest($GLOBALS['homepageEmbyAuth'])) ? embyConnect('metadata', $array['data']['key'], true) : false;
+			break;
+		case 'getSabnzbd':
+			return sabnzbdConnect();
+			break;
+		case 'getNzbget':
+			return nzbgetConnect();
+			break;
+		case 'getTransmission':
+			return transmissionConnect();
+			break;
+		case 'getqBittorrent':
+			return qBittorrentConnect();
+			break;
+		case 'getrTorrent':
+			return rTorrentConnect();
+			break;
+		case 'getDeluge':
+			return delugeConnect();
+			break;
+		case 'getCalendar':
+			return getCalendar();
+			break;
+		case 'getRequests':
+			return getOmbiRequests();
+			break;
+		default:
+			# code...
+			break;
+	}
+	return false;
+}
+
+function streamType($value)
+{
+	if ($value == "transcode" || $value == "Transcode") {
+		return "Transcode";
+	} elseif ($value == "copy" || $value == "DirectStream") {
+		return "Direct Stream";
+	} elseif ($value == "directplay" || $value == "DirectPlay") {
+		return "Direct Play";
+	} else {
+		return "Direct Play";
+	}
+}
+
+function resolveEmbyItem($itemDetails)
+{
+	// Grab Each item info from Emby (extra call)
+	$id = isset($itemDetails['NowPlayingItem']['Id']) ? $itemDetails['NowPlayingItem']['Id'] : $itemDetails['Id'];
+	$url = qualifyURL($GLOBALS['embyURL']);
+	$url = $url . '/Items?Ids=' . $id . '&api_key=' . $GLOBALS['embyToken'] . '&Fields=Overview,People,Genres,CriticRating,Studios,Taglines';
+	try {
+		$options = (localURL($url)) ? array('verify' => false) : array();
+		$response = Requests::get($url, array(), $options);
+		if ($response->success) {
+			$item = json_decode($response->body, true)['Items'][0];
+		}
+	} catch (Requests_Exception $e) {
+		return false;
+	};
+	// Static Height & Width
+	$height = getCacheImageSize('h');
+	$width = getCacheImageSize('w');
+	$nowPlayingHeight = getCacheImageSize('nph');
+	$nowPlayingWidth = getCacheImageSize('npw');
+	$actorHeight = 450;
+	$actorWidth = 300;
+	// Cache Directories
+	$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+	$cacheDirectoryWeb = 'plugins/images/cache/';
+	// Types
+	$embyItem['array-item'] = $item;
+	$embyItem['array-itemdetails'] = $itemDetails;
+	switch (@$item['Type']) {
+		case 'Series':
+			$embyItem['type'] = 'tv';
+			$embyItem['title'] = $item['Name'];
+			$embyItem['secondaryTitle'] = '';
+			$embyItem['summary'] = '';
+			$embyItem['ratingKey'] = $item['Id'];
+			$embyItem['thumb'] = $item['Id'];
+			$embyItem['key'] = $item['Id'] . "-list";
+			$embyItem['nowPlayingThumb'] = $item['Id'];
+			$embyItem['nowPlayingKey'] = $item['Id'] . "-np";
+			$embyItem['metadataKey'] = $item['Id'];
+			$embyItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? 'Thumb' : (isset($item['BackdropImageTags'][0]) ? 'Backdrop' : '');
+			break;
+		case 'Episode':
+			$embyItem['type'] = 'tv';
+			$embyItem['title'] = $item['SeriesName'];
+			$embyItem['secondaryTitle'] = '';
+			$embyItem['summary'] = '';
+			$embyItem['ratingKey'] = $item['Id'];
+			$embyItem['thumb'] = (isset($item['SeriesId']) ? $item['SeriesId'] : $item['Id']);
+			$embyItem['key'] = (isset($item['SeriesId']) ? $item['SeriesId'] : $item['Id']) . "-list";
+			$embyItem['nowPlayingThumb'] = isset($item['ParentThumbItemId']) ? $item['ParentThumbItemId'] : (isset($item['ParentBackdropItemId']) ? $item['ParentBackdropItemId'] : false);
+			$embyItem['nowPlayingKey'] = isset($item['ParentThumbItemId']) ? $item['ParentThumbItemId'] . '-np' : (isset($item['ParentBackdropItemId']) ? $item['ParentBackdropItemId'] . '-np' : false);
+			$embyItem['metadataKey'] = $item['Id'];
+			$embyItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? 'Thumb' : (isset($item['ParentBackdropImageTags'][0]) ? 'Backdrop' : '');
+			$embyItem['nowPlayingTitle'] = @$item['SeriesName'] . ' - ' . @$item['Name'];
+			$embyItem['nowPlayingBottom'] = 'S' . @$item['ParentIndexNumber'] . ' · E' . @$item['IndexNumber'];
+			break;
+		case 'MusicAlbum':
+		case 'Audio':
+			$embyItem['type'] = 'music';
+			$embyItem['title'] = $item['Name'];
+			$embyItem['secondaryTitle'] = '';
+			$embyItem['summary'] = '';
+			$embyItem['ratingKey'] = $item['Id'];
+			$embyItem['thumb'] = $item['Id'];
+			$embyItem['key'] = $item['Id'] . "-list";
+			$embyItem['nowPlayingThumb'] = (isset($item['AlbumId']) ? $item['AlbumId'] : @$item['ParentBackdropItemId']);
+			$embyItem['nowPlayingKey'] = $item['Id'] . "-np";
+			$embyItem['metadataKey'] = isset($item['AlbumId']) ? $item['AlbumId'] : $item['Id'];
+			$embyItem['nowPlayingImageType'] = (isset($item['ParentBackdropItemId']) ? "Primary" : "Backdrop");
+			$embyItem['nowPlayingTitle'] = @$item['AlbumArtist'] . ' - ' . @$item['Name'];
+			$embyItem['nowPlayingBottom'] = @$item['Album'];
+			break;
+		case 'Movie':
+			$embyItem['type'] = 'movie';
+			$embyItem['title'] = $item['Name'];
+			$embyItem['secondaryTitle'] = '';
+			$embyItem['summary'] = '';
+			$embyItem['ratingKey'] = $item['Id'];
+			$embyItem['thumb'] = $item['Id'];
+			$embyItem['key'] = $item['Id'] . "-list";
+			$embyItem['nowPlayingThumb'] = $item['Id'];
+			$embyItem['nowPlayingKey'] = $item['Id'] . "-np";
+			$embyItem['metadataKey'] = $item['Id'];
+			$embyItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? "Thumb" : (isset($item['BackdropImageTags']) ? "Backdrop" : false);
+			$embyItem['nowPlayingTitle'] = @$item['Name'];
+			$embyItem['nowPlayingBottom'] = @$item['ProductionYear'];
+			break;
+		case 'Video':
+			$embyItem['type'] = 'video';
+			$embyItem['title'] = $item['Name'];
+			$embyItem['secondaryTitle'] = '';
+			$embyItem['summary'] = '';
+			$embyItem['ratingKey'] = $item['Id'];
+			$embyItem['thumb'] = $item['Id'];
+			$embyItem['key'] = $item['Id'] . "-list";
+			$embyItem['nowPlayingThumb'] = $item['Id'];
+			$embyItem['nowPlayingKey'] = $item['Id'] . "-np";
+			$embyItem['metadataKey'] = $item['Id'];
+			$embyItem['nowPlayingImageType'] = isset($item['ImageTags']['Thumb']) ? "Thumb" : (isset($item['BackdropImageTags']) ? "Backdrop" : false);
+			$embyItem['nowPlayingTitle'] = @$item['Name'];
+			$embyItem['nowPlayingBottom'] = @$item['ProductionYear'];
+			break;
+		default:
+			return false;
+	}
+	$embyItem['uid'] = $item['Id'];
+	$embyItem['imageType'] = (isset($item['ImageTags']['Primary']) ? "Primary" : false);
+	$embyItem['elapsed'] = isset($itemDetails['PlayState']['PositionTicks']) && $itemDetails['PlayState']['PositionTicks'] !== '0' ? (int)$itemDetails['PlayState']['PositionTicks'] : null;
+	$embyItem['duration'] = isset($itemDetails['NowPlayingItem']['RunTimeTicks']) ? (int)$itemDetails['NowPlayingItem']['RunTimeTicks'] : (int)(isset($item['RunTimeTicks']) ? $item['RunTimeTicks'] : '');
+	$embyItem['watched'] = ($embyItem['elapsed'] && $embyItem['duration'] ? floor(($embyItem['elapsed'] / $embyItem['duration']) * 100) : 0);
+	$embyItem['transcoded'] = isset($itemDetails['TranscodingInfo']['CompletionPercentage']) ? floor((int)$itemDetails['TranscodingInfo']['CompletionPercentage']) : 100;
+	$embyItem['stream'] = @$itemDetails['PlayState']['PlayMethod'];
+	$embyItem['id'] = $item['ServerId'];
+	$embyItem['session'] = @$itemDetails['DeviceId'];
+	$embyItem['bandwidth'] = isset($itemDetails['TranscodingInfo']['Bitrate']) ? $itemDetails['TranscodingInfo']['Bitrate'] / 1000 : '';
+	$embyItem['bandwidthType'] = 'wan';
+	$embyItem['sessionType'] = (@$itemDetails['PlayState']['PlayMethod'] == 'Transcode') ? 'Transcoding' : 'Direct Playing';
+	$embyItem['state'] = ((@(string)$itemDetails['PlayState']['IsPaused'] == '1') ? "pause" : "play");
+	$embyItem['user'] = ($GLOBALS['homepageShowStreamNames'] && qualifyRequest($GLOBALS['homepageShowStreamNamesAuth'])) ? @(string)$itemDetails['UserName'] : "";
+	$embyItem['userThumb'] = '';
+	$embyItem['userAddress'] = (isset($itemDetails['RemoteEndPoint']) ? $itemDetails['RemoteEndPoint'] : "x.x.x.x");
+	$embyItem['address'] = $GLOBALS['embyTabURL'] ? rtrim($GLOBALS['embyTabURL'], '/') . "/web/#!/itemdetails.html?id=" . $embyItem['uid'] : "https://app.emby.media/#!/itemdetails.html?id=" . $embyItem['uid'] . "&serverId=" . $embyItem['id'];
+	$embyItem['nowPlayingOriginalImage'] = 'api/?v1/image&source=emby&type=' . $embyItem['nowPlayingImageType'] . '&img=' . $embyItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $embyItem['nowPlayingKey'] . '$' . randString();
+	$embyItem['originalImage'] = 'api/?v1/image&source=emby&type=' . $embyItem['imageType'] . '&img=' . $embyItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $embyItem['key'] . '$' . randString();
+	$embyItem['openTab'] = $GLOBALS['embyTabURL'] && $GLOBALS['embyTabName'] ? true : false;
+	$embyItem['tabName'] = $GLOBALS['embyTabName'] ? $GLOBALS['embyTabName'] : '';
+	// Stream info
+	$embyItem['userStream'] = array(
+		'platform' => @(string)$itemDetails['Client'],
+		'product' => @(string)$itemDetails['Client'],
+		'device' => @(string)$itemDetails['DeviceName'],
+		'stream' => @$itemDetails['PlayState']['PlayMethod'],
+		'videoResolution' => isset($itemDetails['NowPlayingItem']['MediaStreams'][0]['Width']) ? $itemDetails['NowPlayingItem']['MediaStreams'][0]['Width'] : '',
+		'throttled' => false,
+		'sourceVideoCodec' => isset($itemDetails['NowPlayingItem']['MediaStreams'][0]) ? $itemDetails['NowPlayingItem']['MediaStreams'][0]['Codec'] : '',
+		'videoCodec' => @$itemDetails['TranscodingInfo']['VideoCodec'],
+		'audioCodec' => @$itemDetails['TranscodingInfo']['AudioCodec'],
+		'sourceAudioCodec' => isset($itemDetails['NowPlayingItem']['MediaStreams'][1]) ? $itemDetails['NowPlayingItem']['MediaStreams'][1]['Codec'] : (isset($itemDetails['NowPlayingItem']['MediaStreams'][0]) ? $itemDetails['NowPlayingItem']['MediaStreams'][0]['Codec'] : ''),
+		'videoDecision' => streamType(@$itemDetails['PlayState']['PlayMethod']),
+		'audioDecision' => streamType(@$itemDetails['PlayState']['PlayMethod']),
+		'container' => isset($itemDetails['NowPlayingItem']['Container']) ? $itemDetails['NowPlayingItem']['Container'] : '',
+		'audioChannels' => @$itemDetails['TranscodingInfo']['AudioChannels']
+	);
+	// Genre catch all
+	if ($item['Genres']) {
+		$genres = array();
+		foreach ($item['Genres'] as $genre) {
+			$genres[] = $genre;
+		}
+	}
+	// Actor catch all
+	if ($item['People']) {
+		$actors = array();
+		foreach ($item['People'] as $key => $value) {
+			if (@$value['PrimaryImageTag'] && @$value['Role']) {
+				if (file_exists($cacheDirectory . (string)$value['Id'] . '-cast.jpg')) {
+					$actorImage = $cacheDirectoryWeb . (string)$value['Id'] . '-cast.jpg';
+				}
+				if (file_exists($cacheDirectory . (string)$value['Id'] . '-cast.jpg') && (time() - 604800) > filemtime($cacheDirectory . (string)$value['Id'] . '-cast.jpg') || !file_exists($cacheDirectory . (string)$value['Id'] . '-cast.jpg')) {
+					$actorImage = 'api/?v1/image&source=emby&type=Primary&img=' . (string)$value['Id'] . '&height=' . $actorHeight . '&width=' . $actorWidth . '&key=' . (string)$value['Id'] . '-cast';
+				}
+				$actors[] = array(
+					'name' => (string)$value['Name'],
+					'role' => (string)$value['Role'],
+					'thumb' => $actorImage
+				);
+			}
+		}
+	}
+	// Metadata information
+	$embyItem['metadata'] = array(
+		'guid' => $item['Id'],
+		'summary' => @(string)$item['Overview'],
+		'rating' => @(string)$item['CommunityRating'],
+		'duration' => @(string)$item['RunTimeTicks'],
+		'originallyAvailableAt' => @(string)$item['PremiereDate'],
+		'year' => (string)isset($item['ProductionYear']) ? $item['ProductionYear'] : '',
+		//'studio' => (string)$item['studio'],
+		'tagline' => @(string)$item['Taglines'][0],
+		'genres' => ($item['Genres']) ? $genres : '',
+		'actors' => ($item['People']) ? $actors : ''
+	);
+	if (file_exists($cacheDirectory . $embyItem['nowPlayingKey'] . '.jpg')) {
+		$embyItem['nowPlayingImageURL'] = $cacheDirectoryWeb . $embyItem['nowPlayingKey'] . '.jpg';
+	}
+	if (file_exists($cacheDirectory . $embyItem['key'] . '.jpg')) {
+		$embyItem['imageURL'] = $cacheDirectoryWeb . $embyItem['key'] . '.jpg';
+	}
+	if (file_exists($cacheDirectory . $embyItem['nowPlayingKey'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $embyItem['nowPlayingKey'] . '.jpg') || !file_exists($cacheDirectory . $embyItem['nowPlayingKey'] . '.jpg')) {
+		$embyItem['nowPlayingImageURL'] = 'api/?v1/image&source=emby&type=' . $embyItem['nowPlayingImageType'] . '&img=' . $embyItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $embyItem['nowPlayingKey'] . '';
+	}
+	if (file_exists($cacheDirectory . $embyItem['key'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $embyItem['key'] . '.jpg') || !file_exists($cacheDirectory . $embyItem['key'] . '.jpg')) {
+		$embyItem['imageURL'] = 'api/?v1/image&source=emby&type=' . $embyItem['imageType'] . '&img=' . $embyItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $embyItem['key'] . '';
+	}
+	if (!$embyItem['nowPlayingThumb']) {
+		$embyItem['nowPlayingOriginalImage'] = $embyItem['nowPlayingImageURL'] = "plugins/images/cache/no-np.png";
+		$embyItem['nowPlayingKey'] = "no-np";
+	}
+	if (!$embyItem['thumb']) {
+		$embyItem['originalImage'] = $embyItem['imageURL'] = "plugins/images/cache/no-list.png";
+		$embyItem['key'] = "no-list";
+	}
+	if (isset($useImage)) {
+		$embyItem['useImage'] = $useImage;
+	}
+	return $embyItem;
+}
+
+function getCacheImageSize($type)
+{
+	switch ($type) {
+		case 'height':
+		case 'h':
+			return 300 * $GLOBALS['cacheImageSize'];
+			break;
+		case 'width':
+		case 'w':
+			return 200 * $GLOBALS['cacheImageSize'];
+			break;
+		case 'nowPlayingHeight':
+		case 'nph':
+			return 675 * $GLOBALS['cacheImageSize'];
+			break;
+		case 'nowPlayingWidth':
+		case 'npw':
+			return 1200 * $GLOBALS['cacheImageSize'];
+			break;
+	}
+}
+
+function resolvePlexItem($item)
+{
+	// Static Height & Width
+	$height = getCacheImageSize('h');
+	$width = getCacheImageSize('w');
+	$nowPlayingHeight = getCacheImageSize('nph');
+	$nowPlayingWidth = getCacheImageSize('npw');
+	// Cache Directories
+	$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+	$cacheDirectoryWeb = 'plugins/images/cache/';
+	// Types
+	switch ($item['type']) {
+		case 'season':
+			$plexItem['type'] = 'tv';
+			$plexItem['title'] = (string)$item['parentTitle'];
+			$plexItem['secondaryTitle'] = (string)$item['title'];
+			$plexItem['summary'] = (string)$item['parentSummary'];
+			$plexItem['ratingKey'] = (string)$item['parentRatingKey'];
+			$plexItem['thumb'] = (string)$item['thumb'];
+			$plexItem['key'] = (string)$item['ratingKey'] . "-list";
+			$plexItem['nowPlayingThumb'] = (string)$item['art'];
+			$plexItem['nowPlayingKey'] = (string)$item['ratingKey'] . "-np";
+			$plexItem['metadataKey'] = (string)$item['parentRatingKey'];
+			break;
+		case 'episode':
+			$plexItem['type'] = 'tv';
+			$plexItem['title'] = (string)$item['grandparentTitle'];
+			$plexItem['secondaryTitle'] = (string)$item['parentTitle'];
+			$plexItem['summary'] = (string)$item['title'];
+			$plexItem['ratingKey'] = (string)$item['parentRatingKey'];
+			$plexItem['thumb'] = ($item['parentThumb'] ? (string)$item['parentThumb'] : (string)$item['grandparentThumb']);
+			$plexItem['key'] = (string)$item['ratingKey'] . "-list";
+			$plexItem['nowPlayingThumb'] = (string)$item['grandparentArt'];
+			$plexItem['nowPlayingKey'] = (string)$item['grandparentRatingKey'] . "-np";
+			$plexItem['nowPlayingTitle'] = (string)$item['grandparentTitle'] . ' - ' . (string)$item['title'];
+			$plexItem['nowPlayingBottom'] = 'S' . (string)$item['parentIndex'] . ' · E' . (string)$item['index'];
+			$plexItem['metadataKey'] = (string)$item['grandparentRatingKey'];
+			break;
+		case 'clip':
+			$useImage = (isset($item['live']) ? "plugins/images/cache/livetv.png" : null);
+			$plexItem['type'] = 'clip';
+			$plexItem['title'] = (isset($item['live']) ? 'Live TV' : (string)$item['title']);
+			$plexItem['secondaryTitle'] = '';
+			$plexItem['summary'] = (string)$item['summary'];
+			$plexItem['ratingKey'] = (string)$item['parentRatingKey'];
+			$plexItem['thumb'] = (string)$item['thumb'];
+			$plexItem['key'] = (string)$item['ratingKey'] . "-list";
+			$plexItem['nowPlayingThumb'] = (string)$item['art'];
+			$plexItem['nowPlayingKey'] = isset($item['ratingKey']) ? (string)$item['ratingKey'] . "-np" : (isset($item['live']) ? "livetv.png" : ":)");
+			$plexItem['nowPlayingTitle'] = $plexItem['title'];
+			$plexItem['nowPlayingBottom'] = isset($item['extraType']) ? "Trailer" : (isset($item['live']) ? "Live TV" : ":)");
+			break;
+		case 'album':
+		case 'track':
+			$plexItem['type'] = 'music';
+			$plexItem['title'] = (string)$item['parentTitle'];
+			$plexItem['secondaryTitle'] = (string)$item['title'];
+			$plexItem['summary'] = (string)$item['title'];
+			$plexItem['ratingKey'] = (string)$item['parentRatingKey'];
+			$plexItem['thumb'] = (string)$item['thumb'];
+			$plexItem['key'] = (string)$item['ratingKey'] . "-list";
+			$plexItem['nowPlayingThumb'] = ($item['parentThumb']) ? (string)$item['parentThumb'] : (string)$item['art'];
+			$plexItem['nowPlayingKey'] = (string)$item['parentRatingKey'] . "-np";
+			$plexItem['nowPlayingTitle'] = (string)$item['grandparentTitle'] . ' - ' . (string)$item['title'];
+			$plexItem['nowPlayingBottom'] = (string)$item['parentTitle'];
+			$plexItem['metadataKey'] = isset($item['grandparentRatingKey']) ? (string)$item['grandparentRatingKey'] : (string)$item['parentRatingKey'];
+			break;
+		default:
+			$plexItem['type'] = 'movie';
+			$plexItem['title'] = (string)$item['title'];
+			$plexItem['secondaryTitle'] = (string)$item['year'];
+			$plexItem['summary'] = (string)$item['summary'];
+			$plexItem['ratingKey'] = (string)$item['ratingKey'];
+			$plexItem['thumb'] = (string)$item['thumb'];
+			$plexItem['key'] = (string)$item['ratingKey'] . "-list";
+			$plexItem['nowPlayingThumb'] = (string)$item['art'];
+			$plexItem['nowPlayingKey'] = (string)$item['ratingKey'] . "-np";
+			$plexItem['nowPlayingTitle'] = (string)$item['title'];
+			$plexItem['nowPlayingBottom'] = (string)$item['year'];
+			$plexItem['metadataKey'] = (string)$item['ratingKey'];
+	}
+	$plexItem['uid'] = (string)$item['ratingKey'];
+	$plexItem['elapsed'] = isset($item['viewOffset']) && $item['viewOffset'] !== '0' ? (int)$item['viewOffset'] : null;
+	$plexItem['duration'] = isset($item['duration']) ? (int)$item['duration'] : (int)$item->Media['duration'];
+	$plexItem['watched'] = ($plexItem['elapsed'] && $plexItem['duration'] ? floor(($plexItem['elapsed'] / $plexItem['duration']) * 100) : 0);
+	$plexItem['transcoded'] = isset($item->TranscodeSession['progress']) ? floor((int)$item->TranscodeSession['progress'] - $plexItem['watched']) : '';
+	$plexItem['stream'] = isset($item->Media->Part->Stream['decision']) ? (string)$item->Media->Part->Stream['decision'] : '';
+	$plexItem['id'] = str_replace('"', '', (string)$item->Player['machineIdentifier']);
+	$plexItem['session'] = (string)$item->Session['id'];
+	$plexItem['bandwidth'] = (string)$item->Session['bandwidth'];
+	$plexItem['bandwidthType'] = (string)$item->Session['location'];
+	$plexItem['sessionType'] = isset($item->TranscodeSession['progress']) ? 'Transcoding' : 'Direct Playing';
+	$plexItem['state'] = (((string)$item->Player['state'] == "paused") ? "pause" : "play");
+	$plexItem['user'] = ($GLOBALS['homepageShowStreamNames'] && qualifyRequest($GLOBALS['homepageShowStreamNamesAuth'])) ? (string)$item->User['title'] : "";
+	$plexItem['userThumb'] = ($GLOBALS['homepageShowStreamNames'] && qualifyRequest($GLOBALS['homepageShowStreamNamesAuth'])) ? (string)$item->User['thumb'] : "";
+	$plexItem['userAddress'] = ($GLOBALS['homepageShowStreamNames'] && qualifyRequest($GLOBALS['homepageShowStreamNamesAuth'])) ? (string)$item->Player['address'] : "x.x.x.x";
+	$plexItem['address'] = $GLOBALS['plexTabURL'] ? $GLOBALS['plexTabURL'] . "/web/index.html#!/server/" . $GLOBALS['plexID'] . "/details?key=/library/metadata/" . $item['ratingKey'] : "https://app.plex.tv/web/app#!/server/" . $GLOBALS['plexID'] . "/details?key=/library/metadata/" . $item['ratingKey'];
+	$plexItem['nowPlayingOriginalImage'] = 'api/?v1/image&source=plex&img=' . $plexItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $plexItem['nowPlayingKey'] . '$' . randString();
+	$plexItem['originalImage'] = 'api/?v1/image&source=plex&img=' . $plexItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $plexItem['key'] . '$' . randString();
+	$plexItem['openTab'] = $GLOBALS['plexTabURL'] && $GLOBALS['plexTabName'] ? true : false;
+	$plexItem['tabName'] = $GLOBALS['plexTabName'] ? $GLOBALS['plexTabName'] : '';
+	// Stream info
+	$plexItem['userStream'] = array(
+		'platform' => (string)$item->Player['platform'],
+		'product' => (string)$item->Player['product'],
+		'device' => (string)$item->Player['device'],
+		'stream' => (string)$item->Media->Part['decision'] . ($item->TranscodeSession['throttled'] == '1' ? ' (Throttled)' : ''),
+		'videoResolution' => (string)$item->Media['videoResolution'],
+		'throttled' => ($item->TranscodeSession['throttled'] == 1) ? true : false,
+		'sourceVideoCodec' => (string)$item->TranscodeSession['sourceVideoCodec'],
+		'videoCodec' => (string)$item->TranscodeSession['videoCodec'],
+		'audioCodec' => (string)$item->TranscodeSession['audioCodec'],
+		'sourceAudioCodec' => (string)$item->TranscodeSession['sourceAudioCodec'],
+		'videoDecision' => streamType((string)$item->TranscodeSession['videoDecision']),
+		'audioDecision' => streamType((string)$item->TranscodeSession['audioDecision']),
+		'container' => (string)$item->TranscodeSession['container'],
+		'audioChannels' => (string)$item->TranscodeSession['audioChannels']
+	);
+	// Genre catch all
+	if ($item->Genre) {
+		$genres = array();
+		foreach ($item->Genre as $key => $value) {
+			$genres[] = (string)$value['tag'];
+		}
+	}
+	// Actor catch all
+	if ($item->Role) {
+		$actors = array();
+		foreach ($item->Role as $key => $value) {
+			if ($value['thumb']) {
+				$actors[] = array(
+					'name' => (string)$value['tag'],
+					'role' => (string)$value['role'],
+					'thumb' => (string)$value['thumb']
+				);
+			}
+		}
+	}
+	// Metadata information
+	$plexItem['metadata'] = array(
+		'guid' => (string)$item['guid'],
+		'summary' => (string)$item['summary'],
+		'rating' => (string)$item['rating'],
+		'duration' => (string)$item['duration'],
+		'originallyAvailableAt' => (string)$item['originallyAvailableAt'],
+		'year' => (string)$item['year'],
+		'studio' => (string)$item['studio'],
+		'tagline' => (string)$item['tagline'],
+		'genres' => ($item->Genre) ? $genres : '',
+		'actors' => ($item->Role) ? $actors : ''
+	);
+	if (file_exists($cacheDirectory . $plexItem['nowPlayingKey'] . '.jpg')) {
+		$plexItem['nowPlayingImageURL'] = $cacheDirectoryWeb . $plexItem['nowPlayingKey'] . '.jpg';
+	}
+	if (file_exists($cacheDirectory . $plexItem['key'] . '.jpg')) {
+		$plexItem['imageURL'] = $cacheDirectoryWeb . $plexItem['key'] . '.jpg';
+	}
+	if (file_exists($cacheDirectory . $plexItem['nowPlayingKey'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $plexItem['nowPlayingKey'] . '.jpg') || !file_exists($cacheDirectory . $plexItem['nowPlayingKey'] . '.jpg')) {
+		$plexItem['nowPlayingImageURL'] = 'api/?v1/image&source=plex&img=' . $plexItem['nowPlayingThumb'] . '&height=' . $nowPlayingHeight . '&width=' . $nowPlayingWidth . '&key=' . $plexItem['nowPlayingKey'] . '';
+	}
+	if (file_exists($cacheDirectory . $plexItem['key'] . '.jpg') && (time() - 604800) > filemtime($cacheDirectory . $plexItem['key'] . '.jpg') || !file_exists($cacheDirectory . $plexItem['key'] . '.jpg')) {
+		$plexItem['imageURL'] = 'api/?v1/image&source=plex&img=' . $plexItem['thumb'] . '&height=' . $height . '&width=' . $width . '&key=' . $plexItem['key'] . '';
+	}
+	if (!$plexItem['nowPlayingThumb']) {
+		$plexItem['nowPlayingOriginalImage'] = $plexItem['nowPlayingImageURL'] = "plugins/images/cache/no-np.png";
+		$plexItem['nowPlayingKey'] = "no-np";
+	}
+	if (!$plexItem['thumb']) {
+		$plexItem['originalImage'] = $plexItem['imageURL'] = "plugins/images/cache/no-list.png";
+		$plexItem['key'] = "no-list";
+	}
+	if (isset($useImage)) {
+		$plexItem['useImage'] = $useImage;
+	}
+	return $plexItem;
+}
+
+function plexConnect($action, $key = null)
+{
+	if ($GLOBALS['homepagePlexEnabled'] && !empty($GLOBALS['plexURL']) && !empty($GLOBALS['plexToken']) && !empty($GLOBALS['plexID'] && qualifyRequest($GLOBALS['homepagePlexAuth']))) {
+		$url = qualifyURL($GLOBALS['plexURL']);
+		$ignore = array();
+		$resolve = true;
+		switch ($action) {
+			case 'streams':
+				$url = $url . "/status/sessions?X-Plex-Token=" . $GLOBALS['plexToken'];
+				break;
+			case 'libraries':
+				$url = $url . "/library/sections?X-Plex-Token=" . $GLOBALS['plexToken'];
+				$resolve = false;
+				break;
+			case 'recent':
+				$url = $url . "/library/recentlyAdded?X-Plex-Token=" . $GLOBALS['plexToken'] . "&limit=" . $GLOBALS['homepageRecentLimit'];
+				break;
+			case 'metadata':
+				$url = $url . "/library/metadata/" . $key . "?X-Plex-Token=" . $GLOBALS['plexToken'];
+				break;
+			case 'playlists':
+				$url = $url . "/playlists?X-Plex-Token=" . $GLOBALS['plexToken'];
+				break;
+			case 'search':
+				$url = $url . "/search?query=" . rawurlencode($key) . "&X-Plex-Token=" . $GLOBALS['plexToken'];
+				$ignore = array('artist', 'episode');
+				break;
+			default:
+				# code...
+				break;
+		}
+		try {
+			$options = (localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			libxml_use_internal_errors(true);
+			if ($response->success) {
+				$items = array();
+				$plex = simplexml_load_string($response->body);
+				foreach ($plex as $child) {
+					if (!in_array($child['type'], $ignore) && isset($child['librarySectionID'])) {
+						$items[] = resolvePlexItem($child);
+					}
+				}
+				$api['content'] = ($resolve) ? $items : $plex;
+				$api['plexID'] = $GLOBALS['plexID'];
+				$api['showNames'] = true;
+				$api['group'] = '1';
+				return $api;
+			}
+		} catch (Requests_Exception $e) {
+			writeLog('error', 'Plex Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+	}
+	return false;
+}
+
+function getPlexPlaylists()
+{
+	if ($GLOBALS['homepagePlexEnabled'] && !empty($GLOBALS['plexURL']) && !empty($GLOBALS['plexToken']) && !empty($GLOBALS['plexID'] && qualifyRequest($GLOBALS['homepagePlexAuth']) && qualifyRequest($GLOBALS['homepagePlexPlaylistAuth']) && $GLOBALS['homepagePlexPlaylist'])) {
+		$url = qualifyURL($GLOBALS['plexURL']);
+		$url = $url . "/playlists?X-Plex-Token=" . $GLOBALS['plexToken'];
+		try {
+			$options = (localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			libxml_use_internal_errors(true);
+			if ($response->success) {
+				$items = array();
+				$plex = simplexml_load_string($response->body);
+				foreach ($plex as $child) {
+					if ($child['playlistType'] == "video" && strpos(strtolower($child['title']), 'private') === false) {
+						$playlistTitleClean = preg_replace("/(\W)+/", "", (string)$child['title']);
+						$playlistURL = qualifyURL($GLOBALS['plexURL']);
+						$playlistURL = $playlistURL . $child['key'] . "?X-Plex-Token=" . $GLOBALS['plexToken'];
+						$options = (localURL($url)) ? array('verify' => false) : array();
+						$playlistResponse = Requests::get($playlistURL, array(), $options);
+						if ($playlistResponse->success) {
+							$playlistResponse = simplexml_load_string($playlistResponse->body);
+							$items[$playlistTitleClean]['title'] = (string)$child['title'];
+							foreach ($playlistResponse->Video as $playlistItem) {
+								$items[$playlistTitleClean][] = resolvePlexItem($playlistItem);
+							}
+						}
+					}
+				}
+				$api['content'] = $items;
+				$api['plexID'] = $GLOBALS['plexID'];
+				$api['showNames'] = true;
+				$api['group'] = '1';
+				return $api;
+			}
+		} catch (Requests_Exception $e) {
+			writeLog('error', 'Plex Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+	}
+	return false;
+}
+
+function embyConnect($action, $key = null, $skip = false)
+{
+	if ($GLOBALS['homepageEmbyEnabled'] && !empty($GLOBALS['embyURL']) && !empty($GLOBALS['embyToken']) && qualifyRequest($GLOBALS['homepageEmbyAuth'])) {
+		$url = qualifyURL($GLOBALS['embyURL']);
+		switch ($action) {
+			case 'streams':
+				$url = $url . '/Sessions?api_key=' . $GLOBALS['embyToken'];
+				break;
+			case 'recent':
+				$username = false;
+				if (isset($GLOBALS['organizrUser']['username'])) {
+					$username = strtolower($GLOBALS['organizrUser']['username']);
+				}
+				// Get A User
+				$userIds = $url . "/Users?api_key=" . $GLOBALS['embyToken'];
+				$showPlayed = true;
+				try {
+					$options = (localURL($userIds)) ? array('verify' => false) : array();
+					$response = Requests::get($userIds, array(), $options);
+					if ($response->success) {
+						$emby = json_decode($response->body, true);
+						foreach ($emby as $value) { // Scan for admin user
+							if (isset($value['Policy']) && isset($value['Policy']['IsAdministrator']) && $value['Policy']['IsAdministrator']) {
+								$userId = $value['Id'];
+							}
+							if ($username && strtolower($value['Name']) == $username) {
+								$userId = $value['Id'];
+								$showPlayed = false;
+								break;
+							}
+						}
+						$url = $url . '/Users/' . $userId . '/Items/Latest?EnableImages=false&Limit=' . $GLOBALS['homepageRecentLimit'] . '&api_key=' . $GLOBALS['embyToken'] . ($showPlayed ? '' : '&IsPlayed=false');
+					}
+				} catch (Requests_Exception $e) {
+					writeLog('error', 'Emby Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				};
+				break;
+			case 'metadata':
+				$skip = true;
+				break;
+			default:
+				# code...
+				break;
+		}
+		if ($skip && $key) {
+			$items[] = resolveEmbyItem(array('Id' => $key));
+			$api['content'] = $items;
+			return $api;
+		}
+		try {
+			$options = (localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$items = array();
+				$emby = json_decode($response->body, true);
+				foreach ($emby as $child) {
+					if (isset($child['NowPlayingItem']) || isset($child['Name'])) {
+						$items[] = resolveEmbyItem($child);
+					}
+				}
+				$api['content'] = array_filter($items);
+				return $api;
+			}
+		} catch (Requests_Exception $e) {
+			writeLog('error', 'Emby Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+	}
+	return false;
+}
+
+function sabnzbdConnect()
+{
+	if ($GLOBALS['homepageSabnzbdEnabled'] && !empty($GLOBALS['sabnzbdURL']) && !empty($GLOBALS['sabnzbdToken']) && qualifyRequest($GLOBALS['homepageSabnzbdAuth'])) {
+		$url = qualifyURL($GLOBALS['sabnzbdURL']);
+		$url = $url . '/api?mode=queue&output=json&apikey=' . $GLOBALS['sabnzbdToken'];
+		try {
+			$options = (localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$api['content']['queueItems'] = json_decode($response->body, true);
+			}
+		} catch (Requests_Exception $e) {
+			writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+		$url = qualifyURL($GLOBALS['sabnzbdURL']);
+		$url = $url . '/api?mode=history&output=json&limit=100&apikey=' . $GLOBALS['sabnzbdToken'];
+		try {
+			$options = (localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$api['content']['historyItems'] = json_decode($response->body, true);
+			}
+		} catch (Requests_Exception $e) {
+			writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		return $api;
+	}
+	return false;
+}
+
+function nzbgetConnect()
+{
+	if ($GLOBALS['homepageNzbgetEnabled'] && !empty($GLOBALS['nzbgetURL']) && qualifyRequest($GLOBALS['homepageNzbgetAuth'])) {
+		$url = qualifyURL($GLOBALS['nzbgetURL']);
+		if (!empty($GLOBALS['nzbgetUsername']) && !empty($GLOBALS['nzbgetPassword'])) {
+			$url = $url . '/' . $GLOBALS['nzbgetUsername'] . ':' . decrypt($GLOBALS['nzbgetPassword']) . '/jsonrpc/listgroups';
+		} else {
+			$url = $url . '/jsonrpc/listgroups';
+		}
+		try {
+			$options = (localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$api['content']['queueItems'] = json_decode($response->body, true);
+			}
+		} catch (Requests_Exception $e) {
+			writeLog('error', 'NZBGet Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+		$url = qualifyURL($GLOBALS['nzbgetURL']);
+		if (!empty($GLOBALS['nzbgetUsername']) && !empty($GLOBALS['nzbgetPassword'])) {
+			$url = $url . '/' . $GLOBALS['nzbgetUsername'] . ':' . decrypt($GLOBALS['nzbgetPassword']) . '/jsonrpc/history';
+		} else {
+			$url = $url . '/jsonrpc/history';
+		}
+		try {
+			$options = (localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$api['content']['historyItems'] = json_decode($response->body, true);
+			}
+		} catch (Requests_Exception $e) {
+			writeLog('error', 'NZBGet Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		return $api;
+	}
+	return false;
+}
+
+function transmissionConnect()
+{
+	if ($GLOBALS['homepageTransmissionEnabled'] && !empty($GLOBALS['transmissionURL']) && qualifyRequest($GLOBALS['homepageTransmissionAuth'])) {
+		$digest = qualifyURL($GLOBALS['transmissionURL'], true);
+		$passwordInclude = ($GLOBALS['transmissionUsername'] != '' && $GLOBALS['transmissionPassword'] != '') ? $GLOBALS['transmissionUsername'] . ':' . decrypt($GLOBALS['transmissionPassword']) . "@" : '';
+		$url = $digest['scheme'] . '://' . $passwordInclude . $digest['host'] . $digest['port'] . $digest['path'] . '/rpc';
+		try {
+			$options = (localURL($GLOBALS['transmissionURL'])) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->headers['x-transmission-session-id']) {
+				$headers = array(
+					'X-Transmission-Session-Id' => $response->headers['x-transmission-session-id'],
+					'Content-Type' => 'application/json'
+				);
+				$data = array(
+					'method' => 'torrent-get',
+					'arguments' => array(
+						'fields' => array(
+							"id", "name", "totalSize", "eta", "isFinished", "isStalled", "percentDone", "rateDownload", "status", "downloadDir", "errorString"
+						),
+					),
+					'tags' => ''
+				);
+				$response = Requests::post($url, $headers, json_encode($data), $options);
+				if ($response->success) {
+					$torrentList = json_decode($response->body, true)['arguments']['torrents'];
+					if ($GLOBALS['transmissionHideSeeding'] || $GLOBALS['transmissionHideCompleted']) {
+						$filter = array();
+						$torrents['arguments']['torrents'] = array();
+						if ($GLOBALS['transmissionHideSeeding']) {
+							array_push($filter, 6, 5);
+						}
+						if ($GLOBALS['transmissionHideCompleted']) {
+							array_push($filter, 0);
+						}
+						foreach ($torrentList as $key => $value) {
+							if (!in_array($value['status'], $filter)) {
+								$torrents['arguments']['torrents'][] = $value;
+							}
+						}
+					} else {
+						$torrents = json_decode($response->body, true);
+					}
+					$api['content']['queueItems'] = $torrents;
+					$api['content']['historyItems'] = false;
+				}
+			} else {
+				writeLog('error', 'Transmission Connect Function - Error: Could not get session ID', 'SYSTEM');
+			}
+		} catch (Requests_Exception $e) {
+			writeLog('error', 'Transmission Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		return $api;
+	}
+	return false;
+}
+
+function rTorrentStatus($completed, $state, $status)
+{
+	if ($completed && $state && $status == 'seed') {
+		$state = 'Seeding';
+	} elseif (!$completed && !$state && $status == 'leech') {
+		$state = 'Stopped';
+	} elseif (!$completed && $state && $status == 'leech') {
+		$state = 'Downloading';
+	} elseif ($completed && !$state && $status == 'seed') {
+		$state = 'Finished';
+	}
+	return ($state) ? $state : $status;
+}
+
+function rTorrentConnect()
+{
+	if ($GLOBALS['homepagerTorrentEnabled'] && !empty($GLOBALS['rTorrentURL']) && qualifyRequest($GLOBALS['homepagerTorrentAuth'])) {
+		try {
+			$torrents = array();
+			$digest = qualifyURL($GLOBALS['rTorrentURL'], true);
+			$passwordInclude = ($GLOBALS['rTorrentUsername'] != '' && $GLOBALS['rTorrentPassword'] != '') ? $GLOBALS['rTorrentUsername'] . ':' . decrypt($GLOBALS['rTorrentPassword']) . "@" : '';
+			$extraPath = (strpos($GLOBALS['rTorrentURL'], '.php') !== false) ? '' : '/RPC2';
+			$url = $digest['scheme'] . '://' . $passwordInclude . $digest['host'] . $digest['port'] . $digest['path'] . $extraPath;
+			$options = (localURL($url)) ? array('verify' => false) : array();
+			$data = xmlrpc_encode_request("d.multicall2", array(
+				"",
+				"main",
+				"d.name=",
+				"d.base_path=",
+				"d.up.total=",
+				"d.size_bytes=",
+				"d.down.total=",
+				"d.completed_bytes=",
+				"d.connection_current=",
+				"d.down.rate=",
+				"d.up.rate=",
+				"d.timestamp.started=",
+				"d.state=",
+				"d.group.name=",
+				"d.hash=",
+				"d.complete=",
+				"d.ratio=",
+				"d.chunk_size=",
+				"f.size_bytes=",
+				"f.size_chunks=",
+				"f.completed_chunks=",
+				"d.custom=",
+				"d.custom1=",
+				"d.custom2=",
+				"d.custom3=",
+				"d.custom4=",
+				"d.custom5=",
+			), array());
+			$response = Requests::post($url, array(), $data, $options);
+			if ($response->success) {
+				$torrentList = xmlrpc_decode(str_replace('i8>', 'string>', $response->body));
+				foreach ($torrentList as $key => $value) {
+					$tempStatus = rTorrentStatus($value[13], $value[10], $value[6]);
+					if ($tempStatus == 'Seeding' && $GLOBALS['rTorrentHideSeeding']) {
+						//do nothing
+					} elseif ($tempStatus == 'Finished' && $GLOBALS['rTorrentHideCompleted']) {
+						//do nothing
+					} else {
+						$torrents[$key] = array(
+							'name' => $value[0],
+							'base' => $value[1],
+							'upTotal' => $value[2],
+							'size' => $value[3],
+							'downTotal' => $value[4],
+							'downloaded' => $value[5],
+							'connectionState' => $value[6],
+							'leech' => $value[7],
+							'seed' => $value[8],
+							'date' => $value[9],
+							'state' => ($value[10]) ? 'on' : 'off',
+							'group' => $value[11],
+							'hash' => $value[12],
+							'complete' => ($value[13]) ? 'yes' : 'no',
+							'ratio' => $value[14],
+							'label' => $value[20],
+							'status' => $tempStatus,
+							'temp' => $value[16] . ' - ' . $value[17] . ' - ' . $value[18],
+							'custom' => $value[19] . ' - ' . $value[20] . ' - ' . $value[21],
+							'custom2' => $value[22] . ' - ' . $value[23] . ' - ' . $value[24],
+						);
+					}
+				}
+				if (count($torrents) !== 0) {
+					usort($torrents, function ($a, $b) {
+						$direction = substr($GLOBALS['rTorrentSortOrder'], -1);
+						$sort = substr($GLOBALS['rTorrentSortOrder'], 0, strlen($GLOBALS['rTorrentSortOrder']) - 1);
+						switch ($direction) {
+							case 'a':
+								return $a[$sort] <=> $b[$sort];
+								break;
+							case 'd':
+								return $b[$sort] <=> $a[$sort];
+								break;
+							default:
+								return $b['date'] <=> $a['date'];
+						}
+					});
+				}
+				$api['content']['queueItems'] = $torrents;
+				$api['content']['historyItems'] = false;
+			}
+		} catch
+		(Requests_Exception $e) {
+			writeLog('error', 'rTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		return $api;
+	}
+	return false;
+}
+
+function qBittorrentConnect()
+{
+	if ($GLOBALS['homepageqBittorrentEnabled'] && !empty($GLOBALS['qBittorrentURL']) && qualifyRequest($GLOBALS['homepageqBittorrentAuth'])) {
+		$digest = qualifyURL($GLOBALS['qBittorrentURL'], true);
+		$data = array('username' => $GLOBALS['qBittorrentUsername'], 'password' => decrypt($GLOBALS['qBittorrentPassword']));
+		$url = $digest['scheme'] . '://' . $digest['host'] . $digest['port'] . $digest['path'] . '/login';
+		try {
+			$options = (localURL($GLOBALS['qBittorrentURL'])) ? array('verify' => false) : array();
+			$response = Requests::post($url, array(), $data, $options);
+			$reflection = new ReflectionClass($response->cookies);
+			$cookie = $reflection->getProperty("cookies");
+			$cookie->setAccessible(true);
+			$cookie = $cookie->getValue($response->cookies);
+			if ($cookie) {
+				$headers = array(
+					'Cookie' => 'SID=' . $cookie['SID']->value
+				);
+				$reverse = $GLOBALS['qBittorrentReverseSorting'] ? 'true' : 'false';
+				$url = $digest['scheme'] . '://' . $digest['host'] . $digest['port'] . $digest['path'] . '/query/torrents?sort=' . $GLOBALS['qBittorrentSortOrder'] . '&reverse=' . $reverse;
+				$response = Requests::get($url, $headers, $options);
+				if ($response) {
+					$torrentList = json_decode($response->body, true);
+					if ($GLOBALS['qBittorrentHideSeeding'] || $GLOBALS['qBittorrentHideCompleted']) {
+						$filter = array();
+						$torrents['arguments']['torrents'] = array();
+						if ($GLOBALS['qBittorrentHideSeeding']) {
+							array_push($filter, 'uploading', 'stalledUP', 'queuedUP');
+						}
+						if ($GLOBALS['qBittorrentHideCompleted']) {
+							array_push($filter, 'pausedUP');
+						}
+						foreach ($torrentList as $key => $value) {
+							if (!in_array($value['state'], $filter)) {
+								$torrents['arguments']['torrents'][] = $value;
+							}
+						}
+					} else {
+						$torrents['arguments']['torrents'] = json_decode($response->body, true);
+					}
+					$api['content']['queueItems'] = $torrents;
+					$api['content']['historyItems'] = false;
+				}
+			} else {
+				writeLog('error', 'qBittorrent Connect Function - Error: Could not get session ID', 'SYSTEM');
+			}
+		} catch (Requests_Exception $e) {
+			writeLog('error', 'qBittorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		return $api;
+	}
+	return false;
+}
+
+function delugeStatus($queued, $status, $state)
+{
+	if ($queued == '-1' && $state == '100' && ($status == 'Seeding' || $status == 'Queued' || $status == 'Paused')) {
+		$state = 'Seeding';
+	} elseif ($state !== '100') {
+		$state = 'Downloading';
+	} else {
+		$state = 'Finished';
+	}
+	return ($state) ? $state : $status;
+}
+
+function delugeConnect()
+{
+	if ($GLOBALS['homepageDelugeEnabled'] && !empty($GLOBALS['delugeURL']) && !empty($GLOBALS['delugePassword']) && qualifyRequest($GLOBALS['homepageDelugeAuth'])) {
+		try {
+			$deluge = new deluge($GLOBALS['delugeURL'], decrypt($GLOBALS['delugePassword']));
+			$torrents = $deluge->getTorrents(null, 'comment, download_payload_rate, eta, hash, is_finished, is_seed, message, name, paused, progress, queue, state, total_size, upload_payload_rate');
+			foreach ($torrents as $key => $value) {
+				$tempStatus = delugeStatus($value->queue, $value->state, $value->progress);
+				if ($tempStatus == 'Seeding' && $GLOBALS['delugeHideSeeding']) {
+					//do nothing
+				} elseif ($tempStatus == 'Finished' && $GLOBALS['delugeHideCompleted']) {
+					//do nothing
+				} else {
+					$api['content']['queueItems'][] = $value;
+				}
+			}
+			$api['content']['queueItems'] = (empty($api['content']['queueItems'])) ? [] : $api['content']['queueItems'];
+			$api['content']['historyItems'] = false;
+		} catch (Excecption $e) {
+			writeLog('error', 'Deluge Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		}
+	}
+	$api['content'] = isset($api['content']) ? $api['content'] : false;
+	return $api;
+}
+
+function getCalendar()
+{
+	$startDate = date('Y-m-d', strtotime("-" . $GLOBALS['calendarStart'] . " days"));
+	$endDate = date('Y-m-d', strtotime("+" . $GLOBALS['calendarEnd'] . " days"));
+	$icalCalendarSources = array();
+	$calendarItems = array();
+	// SONARR CONNECT
+	if ($GLOBALS['homepageSonarrEnabled'] && qualifyRequest($GLOBALS['homepageSonarrAuth']) && !empty($GLOBALS['sonarrURL']) && !empty($GLOBALS['sonarrToken'])) {
+		$sonarrs = array();
+		$sonarrURLList = explode(',', $GLOBALS['sonarrURL']);
+		$sonarrTokenList = explode(',', $GLOBALS['sonarrToken']);
+		if (count($sonarrURLList) == count($sonarrTokenList)) {
+			foreach ($sonarrURLList as $key => $value) {
+				$sonarrs[$key] = array(
+					'url' => $value,
+					'token' => $sonarrTokenList[$key]
+				);
+			}
+			foreach ($sonarrs as $key => $value) {
+				try {
+					$sonarr = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
+					$sonarr = $sonarr->getCalendar($startDate, $endDate, $GLOBALS['sonarrUnmonitored']);
+					$result = json_decode($sonarr, true);
+					$sonarrCalendar = (array_key_exists('error', $result)) ? '' : getSonarrCalendar($sonarr, $key);;
+				} catch (Exception $e) {
+					writeLog('error', 'Sonarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				}
+				if (!empty($sonarrCalendar)) {
+					$calendarItems = array_merge($calendarItems, $sonarrCalendar);
+				}
+			}
+		}
+	}
+	// LIDARR CONNECT
+	if ($GLOBALS['homepageLidarrEnabled'] && qualifyRequest($GLOBALS['homepageLidarrAuth']) && !empty($GLOBALS['lidarrURL']) && !empty($GLOBALS['lidarrToken'])) {
+		$lidarrs = array();
+		$lidarrURLList = explode(',', $GLOBALS['lidarrURL']);
+		$lidarrTokenList = explode(',', $GLOBALS['lidarrToken']);
+		if (count($lidarrURLList) == count($lidarrTokenList)) {
+			foreach ($lidarrURLList as $key => $value) {
+				$lidarrs[$key] = array(
+					'url' => $value,
+					'token' => $lidarrTokenList[$key]
+				);
+			}
+			foreach ($lidarrs as $key => $value) {
+				try {
+					$lidarr = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token'], true);
+					$lidarr = $lidarr->getCalendar($startDate, $endDate);
+					$result = json_decode($lidarr, true);
+					$lidarrCalendar = (array_key_exists('error', $result)) ? '' : getLidarrCalendar($lidarr, $key);;
+				} catch (Exception $e) {
+					writeLog('error', 'Lidarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				}
+				if (!empty($lidarrCalendar)) {
+					$calendarItems = array_merge($calendarItems, $lidarrCalendar);
+				}
+			}
+		}
+	}
+	// RADARR CONNECT
+	if ($GLOBALS['homepageRadarrEnabled'] && qualifyRequest($GLOBALS['homepageRadarrAuth']) && !empty($GLOBALS['radarrURL']) && !empty($GLOBALS['radarrToken'])) {
+		$radarrs = array();
+		$radarrURLList = explode(',', $GLOBALS['radarrURL']);
+		$radarrTokenList = explode(',', $GLOBALS['radarrToken']);
+		if (count($radarrURLList) == count($radarrTokenList)) {
+			foreach ($radarrURLList as $key => $value) {
+				$radarrs[$key] = array(
+					'url' => $value,
+					'token' => $radarrTokenList[$key]
+				);
+			}
+			foreach ($radarrs as $key => $value) {
+				try {
+					$radarr = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
+					$radarr = $radarr->getCalendar($startDate, $endDate);
+					$result = json_decode($radarr, true);
+					$radarrCalendar = (array_key_exists('error', $result)) ? '' : getRadarrCalendar($radarr, $key, $value['url']);
+				} catch (Exception $e) {
+					writeLog('error', 'Radarr Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				}
+				if (!empty($radarrCalendar)) {
+					$calendarItems = array_merge($calendarItems, $radarrCalendar);
+				}
+			}
+		}
+	}
+	// SICKRAGE/BEARD/MEDUSA CONNECT
+	if ($GLOBALS['homepageSickrageEnabled'] && qualifyRequest($GLOBALS['homepageSickrageAuth']) && !empty($GLOBALS['sickrageURL']) && !empty($GLOBALS['sickrageToken'])) {
+		$sicks = array();
+		$sickURLList = explode(',', $GLOBALS['sickrageURL']);
+		$sickTokenList = explode(',', $GLOBALS['sickrageToken']);
+		if (count($sickURLList) == count($sickTokenList)) {
+			foreach ($sickURLList as $key => $value) {
+				$sicks[$key] = array(
+					'url' => $value,
+					'token' => $sickTokenList[$key]
+				);
+			}
+			foreach ($sicks as $key => $value) {
+				try {
+					$sickrage = new Kryptonit3\SickRage\SickRage($value['url'], $value['token']);
+					$sickrageFuture = getSickrageCalendarWanted($sickrage->future(), $key);
+					$sickrageHistory = getSickrageCalendarHistory($sickrage->history("100", "downloaded"), $key);
+					if (!empty($sickrageFuture)) {
+						$calendarItems = array_merge($calendarItems, $sickrageFuture);
+					}
+					if (!empty($sickrageHistory)) {
+						$calendarItems = array_merge($calendarItems, $sickrageHistory);
+					}
+				} catch (Exception $e) {
+					writeLog('error', 'Sickrage Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				}
+			}
+		}
+	}
+	// COUCHPOTATO CONNECT
+	if ($GLOBALS['homepageCouchpotatoEnabled'] && qualifyRequest($GLOBALS['homepageCouchpotatoAuth']) && !empty($GLOBALS['couchpotatoURL']) && !empty($GLOBALS['couchpotatoToken'])) {
+		$couchs = array();
+		$couchpotatoURLList = explode(',', $GLOBALS['couchpotatoURL']);
+		$couchpotatoTokenList = explode(',', $GLOBALS['couchpotatoToken']);
+		if (count($couchpotatoURLList) == count($couchpotatoTokenList)) {
+			foreach ($couchpotatoURLList as $key => $value) {
+				$couchs[$key] = array(
+					'url' => $value,
+					'token' => $couchpotatoTokenList[$key]
+				);
+			}
+			foreach ($couchs as $key => $value) {
+				try {
+					$couchpotato = new Kryptonit3\CouchPotato\CouchPotato($value['url'], $value['token']);
+					$couchCalendar = getCouchCalendar($couchpotato->getMediaList(), $key);
+					if (!empty($couchCalendar)) {
+						$calendarItems = array_merge($calendarItems, $couchCalendar);
+					}
+				} catch (Exception $e) {
+					writeLog('error', 'Sickrage Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				}
+			}
+		}
+	}
+	// iCal URL
+	if ($GLOBALS['homepageCalendarEnabled'] && qualifyRequest($GLOBALS['homepageCalendarAuth']) && !empty($GLOBALS['calendariCal'])) {
+		$calendars = array();
+		$calendarURLList = explode(',', $GLOBALS['calendariCal']);
+		$icalEvents = array();
+		foreach ($calendarURLList as $key => $value) {
+			$icsEvents = getIcsEventsAsArray($value);
+			if (isset($icsEvents) && !empty($icsEvents)) {
+				$timeZone = isset($icsEvents [1] ['X-WR-TIMEZONE']) ? trim($icsEvents[1]['X-WR-TIMEZONE']) : date_default_timezone_get();
+				unset($icsEvents [1]);
+				foreach ($icsEvents as $icsEvent) {
+					if ((isset($icsEvent['DTSTART']) || isset($icsEvent['DTSTART;VALUE=DATE'])) && (isset($icsEvent['DTEND']) || isset($icsEvent['DTEND;VALUE=DATE'])) && isset($icsEvent['SUMMARY'])) {
+						/* Getting start date and time */
+						$start = isset($icsEvent ['DTSTART;VALUE=DATE']) ? $icsEvent ['DTSTART;VALUE=DATE'] : $icsEvent ['DTSTART'];
+						/* Converting to datetime and apply the timezone to get proper date time */
+						$startDt = new DateTime ($start);
+						$startDt->setTimeZone(new DateTimezone ($timeZone));
+						$startDate = $startDt->format(DateTime::ATOM);
+						/* Getting end date with time */
+						$end = isset($icsEvent ['DTEND;VALUE=DATE']) ? $icsEvent ['DTEND;VALUE=DATE'] : $icsEvent ['DTEND'];
+						$endDt = new DateTime ($end);
+						$endDate = $endDt->format(DateTime::ATOM);
+						if (new DateTime() < $endDt) {
+							$extraClass = 'text-info';
+						} else {
+							$extraClass = 'text-success';
+						}
+						/* Getting the name of event */
+						$eventName = $icsEvent['SUMMARY'];
+						$icalEvents[] = array(
+							'title' => $eventName,
+							'imagetype' => 'calendar-o text-warning text-custom-calendar ' . $extraClass,
+							'className' => 'bg-calendar calendar-item bg-custom-calendar',
+							'start' => $startDate,
+							'end' => $endDate
+						);
+					}
+				}
+			}
+		}
+		$calendarSources['ical'] = $icalEvents;
+	}
+	$calendarSources['events'] = $calendarItems;
+	return ($calendarSources) ? $calendarSources : false;
+}
+
+function getSonarrCalendar($array, $number)
+{
+	$array = json_decode($array, true);
+	$gotCalendar = array();
+	$i = 0;
+	foreach ($array as $child) {
+		$i++;
+		$seriesName = $child['series']['title'];
+		$seriesID = $child['series']['tvdbId'];
+		$episodeID = $child['series']['tvdbId'];
+		$monitored = $child['monitored'];
+		if (!isset($episodeID)) {
+			$episodeID = "";
+		}
+		//$episodeName = htmlentities($child['title'], ENT_QUOTES);
+		$episodeAirDate = $child['airDateUtc'];
+		$episodeAirDate = strtotime($episodeAirDate);
+		$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
+		if (new DateTime() < new DateTime($episodeAirDate)) {
+			$unaired = true;
+		}
+		if ($child['episodeNumber'] == "1") {
+			$episodePremier = "true";
+		} else {
+			$episodePremier = "false";
+			$date = new DateTime($episodeAirDate);
+			$date->add(new DateInterval("PT1S"));
+			$date->format(DateTime::ATOM);
+			$child['airDateUtc'] = gmdate('Y-m-d\TH:i:s\Z', strtotime($date->format(DateTime::ATOM)));
+		}
+		$downloaded = $child['hasFile'];
+		if ($downloaded == "0" && isset($unaired) && $episodePremier == "true") {
+			$downloaded = "text-primary animated flash";
+		} elseif ($downloaded == "0" && isset($unaired) && $monitored == "0") {
+			$downloaded = "text-dark";
+		} elseif ($downloaded == "0" && isset($unaired)) {
+			$downloaded = "text-info";
+		} elseif ($downloaded == "1") {
+			$downloaded = "text-success";
+		} else {
+			$downloaded = "text-danger";
+		}
+		$fanart = "/plugins/images/cache/no-np.png";
+		foreach ($child['series']['images'] as $image) {
+			if ($image['coverType'] == "fanart") {
+				$fanart = $image['url'];
+			}
+		}
+		if ($fanart !== "/plugins/images/cache/no-np.png") {
+			$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+			$imageURL = $fanart;
+			$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+			$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+			if (!file_exists($cacheFile)) {
+				cacheImage($imageURL, $seriesID);
+				unset($imageURL);
+				unset($cacheFile);
+			}
+		}
+		$bottomTitle = 'S' . sprintf("%02d", $child['seasonNumber']) . 'E' . sprintf("%02d", $child['episodeNumber']) . ' - ' . $child['title'];
+		$details = array(
+			"seasonCount" => $child['series']['seasonCount'],
+			"status" => $child['series']['status'],
+			"topTitle" => $seriesName,
+			"bottomTitle" => $bottomTitle,
+			"overview" => isset($child['overview']) ? $child['overview'] : '',
+			"runtime" => $child['series']['runtime'],
+			"image" => $fanart,
+			"ratings" => $child['series']['ratings']['value'],
+			"videoQuality" => $child["hasFile"] ? $child['episodeFile']['quality']['quality']['name'] : "unknown",
+			"audioChannels" => $child["hasFile"] && isset($child['episodeFile']['mediaInfo']) ? $child['episodeFile']['mediaInfo']['audioChannels'] : "unknown",
+			"audioCodec" => $child["hasFile"] && isset($child['episodeFile']['mediaInfo']) ? $child['episodeFile']['mediaInfo']['audioCodec'] : "unknown",
+			"videoCodec" => $child["hasFile"] && isset($child['episodeFile']['mediaInfo']) ? $child['episodeFile']['mediaInfo']['videoCodec'] : "unknown",
+			"size" => $child["hasFile"] ? $child['episodeFile']['size'] : "unknown",
+			"genres" => $child['series']['genres'],
+		);
+		array_push($gotCalendar, array(
+			"id" => "Sonarr-" . $number . "-" . $i,
+			"title" => $seriesName,
+			"start" => $child['airDateUtc'],
+			"className" => "bg-calendar calendar-item tvID--" . $episodeID,
+			"imagetype" => "tv " . $downloaded,
+			"imagetypeFilter" => "tv",
+			"downloadFilter" => $downloaded,
+			"bgColor" => str_replace('text', 'bg', $downloaded),
+			"details" => $details
+		));
+	}
+	if ($i != 0) {
+		return $gotCalendar;
+	}
+	return false;
+}
+
+function getLidarrCalendar($array, $number)
+{
+	$array = json_decode($array, true);
+	$gotCalendar = array();
+	$i = 0;
+	foreach ($array as $child) {
+		$i++;
+		$albumName = $child['title'];
+		$artistName = $child['artist']['artistName'];
+		$albumID = '';
+		$releaseDate = $child['releaseDate'];
+		$releaseDate = strtotime($releaseDate);
+		$releaseDate = date("Y-m-d H:i:s", $releaseDate);
+		if (new DateTime() < new DateTime($releaseDate)) {
+			$unaired = true;
+		}
+		$downloaded = (isset($child['statistics']['percentOfEpisodes']) && $child['statistics']['percentOfEpisodes'] !== '100.0') ? '0' : '1';
+		if ($downloaded == "0" && isset($unaired)) {
+			$downloaded = "text-info";
+		} elseif ($downloaded == "1") {
+			$downloaded = "text-success";
+		} else {
+			$downloaded = "text-danger";
+		}
+		$fanart = "/plugins/images/cache/no-np.png";
+		foreach ($child['artist']['images'] as $image) {
+			if ($image['coverType'] == "fanart") {
+				$fanart = str_replace('http://', 'https://', $image['url']);
+			}
+		}
+		$details = array(
+			"seasonCount" => '',
+			"status" => '',
+			"topTitle" => $albumName,
+			"bottomTitle" => $artistName,
+			"overview" => isset($child['artist']['overview']) ? $child['artist']['overview'] : '',
+			"runtime" => '',
+			"image" => $fanart,
+			"ratings" => $child['artist']['ratings']['value'],
+			"videoQuality" => "unknown",
+			"audioChannels" => "unknown",
+			"audioCodec" => "unknown",
+			"videoCodec" => "unknown",
+			"size" => "unknown",
+			"genres" => $child['genres'],
+		);
+		array_push($gotCalendar, array(
+			"id" => "Lidarr-" . $number . "-" . $i,
+			"title" => $artistName,
+			"start" => $child['releaseDate'],
+			"className" => "bg-calendar calendar-item musicID--",
+			"imagetype" => "music " . $downloaded,
+			"imagetypeFilter" => "music",
+			"downloadFilter" => $downloaded,
+			"bgColor" => str_replace('text', 'bg', $downloaded),
+			"details" => $details
+		));
+	}
+	if ($i != 0) {
+		return $gotCalendar;
+	}
+	return false;
+}
+
+function getRadarrCalendar($array, $number, $url)
+{
+	$array = json_decode($array, true);
+	$gotCalendar = array();
+	$i = 0;
+	foreach ($array as $child) {
+		if (isset($child['physicalRelease'])) {
+			$i++;
+			$movieName = $child['title'];
+			$movieID = $child['tmdbId'];
+			if (!isset($movieID)) {
+				$movieID = "";
+			}
+			$physicalRelease = $child['physicalRelease'];
+			$physicalRelease = strtotime($physicalRelease);
+			$physicalRelease = date("Y-m-d", $physicalRelease);
+			if (new DateTime() < new DateTime($physicalRelease)) {
+				$notReleased = "true";
+			} else {
+				$notReleased = "false";
+			}
+			$downloaded = $child['hasFile'];
+			if ($downloaded == "0" && $notReleased == "true") {
+				$downloaded = "text-info";
+			} elseif ($downloaded == "1") {
+				$downloaded = "text-success";
+			} else {
+				$downloaded = "text-danger";
+			}
+			$banner = "/plugins/images/cache/no-np.png";
+			foreach ($child['images'] as $image) {
+				if ($image['coverType'] == "banner" || $image['coverType'] == "fanart") {
+					$url = rtrim($url, '/'); //remove trailing slash
+					$imageUrl = $image['url'];
+					$urlParts = explode("/", $url);
+					$imageParts = explode("/", $image['url']);
+					if ($imageParts[1] == end($urlParts)) {
+						unset($imageParts[1]);
+						$imageUrl = implode("/", $imageParts);
+					}
+					$banner = $url . $imageUrl;
+				}
+			}
+			if ($banner !== "/plugins/images/cache/no-np.png") {
+				$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+				$imageURL = $banner;
+				$cacheFile = $cacheDirectory . $movieID . '.jpg';
+				$banner = 'plugins/images/cache/' . $movieID . '.jpg';
+				if (!file_exists($cacheFile)) {
+					cacheImage($imageURL, $movieID);
+					unset($imageURL);
+					unset($cacheFile);
+				}
+			}
+			$alternativeTitles = "";
+			foreach ($child['alternativeTitles'] as $alternative) {
+				$alternativeTitles .= $alternative['title'] . ', ';
+			}
+			$alternativeTitles = empty($child['alternativeTitles']) ? "" : substr($alternativeTitles, 0, -2);
+			$details = array(
+				"topTitle" => $movieName,
+				"bottomTitle" => $alternativeTitles,
+				"status" => $child['status'],
+				"overview" => $child['overview'],
+				"runtime" => $child['runtime'],
+				"image" => $banner,
+				"ratings" => $child['ratings']['value'],
+				"videoQuality" => $child["hasFile"] ? @$child['movieFile']['quality']['quality']['name'] : "unknown",
+				"audioChannels" => $child["hasFile"] ? @$child['movieFile']['mediaInfo']['audioChannels'] : "unknown",
+				"audioCodec" => $child["hasFile"] ? @$child['movieFile']['mediaInfo']['audioFormat'] : "unknown",
+				"videoCodec" => $child["hasFile"] ? @$child['movieFile']['mediaInfo']['videoCodec'] : "unknown",
+				"size" => $child["hasFile"] ? @$child['movieFile']['size'] : "unknown",
+				"genres" => $child['genres'],
+			);
+			array_push($gotCalendar, array(
+				"id" => "Radarr-" . $number . "-" . $i,
+				"title" => $movieName,
+				"start" => $physicalRelease,
+				"className" => "bg-calendar movieID--" . $movieID,
+				"imagetype" => "film " . $downloaded,
+				"imagetypeFilter" => "film",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details
+			));
+		}
+	}
+	if ($i != 0) {
+		return $gotCalendar;
+	}
+	return false;
+}
+
+function getCouchCalendar($array, $number)
+{
+	$api = json_decode($array, true);
+	$gotCalendar = array();
+	$i = 0;
+	foreach ($api['movies'] as $child) {
+		if ($child['status'] == "active" || $child['status'] == "done") {
+			$i++;
+			$movieName = $child['info']['original_title'];
+			$movieID = $child['info']['tmdb_id'];
+			if (!isset($movieID)) {
+				$movieID = "";
+			}
+			$physicalRelease = (isset($child['info']['released']) ? $child['info']['released'] : null);
+			$backupRelease = (isset($child['info']['release_date']['theater']) ? $child['info']['release_date']['theater'] : null);
+			$physicalRelease = (isset($physicalRelease) ? $physicalRelease : $backupRelease);
+			$physicalRelease = strtotime($physicalRelease);
+			$physicalRelease = date("Y-m-d", $physicalRelease);
+			if (new DateTime() < new DateTime($physicalRelease)) {
+				$notReleased = "true";
+			} else {
+				$notReleased = "false";
+			}
+			$downloaded = ($child['status'] == "active") ? "0" : "1";
+			if ($downloaded == "0" && $notReleased == "true") {
+				$downloaded = "text-info";
+			} elseif ($downloaded == "1") {
+				$downloaded = "text-success";
+			} else {
+				$downloaded = "text-danger";
+			}
+			if (!empty($child['info']['images']['backdrop_original'])) {
+				$banner = $child['info']['images']['backdrop_original'][0];
+			} elseif (!empty($child['info']['images']['backdrop'])) {
+				$banner = $child['info']['images']['backdrop_original'][0];
+			} else {
+				$banner = "/plugins/images/cache/no-np.png";
+			}
+			if ($banner !== "/plugins/images/cache/no-np.png") {
+				$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+				$imageURL = $banner;
+				$cacheFile = $cacheDirectory . $movieID . '.jpg';
+				$banner = 'plugins/images/cache/' . $movieID . '.jpg';
+				if (!file_exists($cacheFile)) {
+					cacheImage($imageURL, $movieID);
+					unset($imageURL);
+					unset($cacheFile);
+				}
+			}
+			$hasFile = (!empty($child['releases']) && !empty($child['releases'][0]['files']['movie']));
+			$details = array(
+				"topTitle" => $movieName,
+				"bottomTitle" => $child['info']['tagline'],
+				"status" => $child['status'],
+				"overview" => $child['info']['plot'],
+				"runtime" => $child['info']['runtime'],
+				"image" => $banner,
+				"ratings" => isset($child['info']['rating']['imdb'][0]) ? $child['info']['rating']['imdb'][0] : '',
+				"videoQuality" => $hasFile ? $child['releases'][0]['quality'] : "unknown",
+				"audioChannels" => "",
+				"audioCodec" => "",
+				"videoCodec" => "",
+				"genres" => $child['info']['genres'],
+			);
+			array_push($gotCalendar, array(
+				"id" => "CouchPotato-" . $number . "-" . $i,
+				"title" => $movieName,
+				"start" => $physicalRelease,
+				"className" => "bg-calendar calendar-item movieID--" . $movieID,
+				"imagetype" => "film " . $downloaded,
+				"imagetypeFilter" => "film",
+				"downloadFilter" => $downloaded,
+				"bgColor" => str_replace('text', 'bg', $downloaded),
+				"details" => $details
+			));
+		}
+	}
+	if ($i != 0) {
+		return $gotCalendar;
+	}
+	return false;
+}
+
+function getSickrageCalendarWanted($array, $number)
+{
+	$array = json_decode($array, true);
+	$gotCalendar = array();
+	$i = 0;
+	foreach ($array['data']['missed'] as $child) {
+		$i++;
+		$seriesName = $child['show_name'];
+		$seriesID = $child['tvdbid'];
+		$episodeID = $child['tvdbid'];
+		$episodeAirDate = $child['airdate'];
+		$episodeAirDateTime = explode(" ", $child['airs']);
+		$episodeAirDateTime = date("H:i:s", strtotime($episodeAirDateTime[1] . $episodeAirDateTime[2]));
+		$episodeAirDate = strtotime($episodeAirDate . $episodeAirDateTime);
+		$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
+		if (new DateTime() < new DateTime($episodeAirDate)) {
+			$unaired = true;
+		}
+		$downloaded = "0";
+		if ($downloaded == "0" && isset($unaired)) {
+			$downloaded = "text-info";
+		} elseif ($downloaded == "1") {
+			$downloaded = "text-success";
+		} else {
+			$downloaded = "text-danger";
+		}
+		$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']) . ' - ' . $child['ep_name'];
+		$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+		$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+		$fanart = "/plugins/images/cache/no-np.png";
+		if (file_exists($cacheFile)) {
+			$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+			unset($cacheFile);
+		}
+		$details = array(
+			"seasonCount" => "",
+			"status" => $child['show_status'],
+			"topTitle" => $seriesName,
+			"bottomTitle" => $bottomTitle,
+			"overview" => isset($child['ep_plot']) ? $child['ep_plot'] : '',
+			"runtime" => "",
+			"image" => $fanart,
+			"ratings" => "",
+			"videoQuality" => isset($child["quality"]) ? $child["quality"] : "",
+			"audioChannels" => "",
+			"audioCodec" => "",
+			"videoCodec" => "",
+			"size" => "",
+			"genres" => "",
+		);
+		array_push($gotCalendar, array(
+			"id" => "Sick-" . $number . "-Miss-" . $i,
+			"title" => $seriesName,
+			"start" => $episodeAirDate,
+			"className" => "bg-calendar calendar-item tvID--" . $episodeID,
+			"imagetype" => "tv " . $downloaded,
+			"imagetypeFilter" => "tv",
+			"downloadFilter" => $downloaded,
+			"bgColor" => str_replace('text', 'bg', $downloaded),
+			"details" => $details,
+		));
+	}
+	foreach ($array['data']['today'] as $child) {
+		$i++;
+		$seriesName = $child['show_name'];
+		$seriesID = $child['tvdbid'];
+		$episodeID = $child['tvdbid'];
+		$episodeAirDate = $child['airdate'];
+		$episodeAirDateTime = explode(" ", $child['airs']);
+		$episodeAirDateTime = date("H:i:s", strtotime($episodeAirDateTime[1] . $episodeAirDateTime[2]));
+		$episodeAirDate = strtotime($episodeAirDate . $episodeAirDateTime);
+		$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
+		if (new DateTime() < new DateTime($episodeAirDate)) {
+			$unaired = true;
+		}
+		$downloaded = "0";
+		if ($downloaded == "0" && isset($unaired)) {
+			$downloaded = "text-info";
+		} elseif ($downloaded == "1") {
+			$downloaded = "text-success";
+		} else {
+			$downloaded = "text-danger";
+		}
+		$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']) . ' - ' . $child['ep_name'];
+		$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+		$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+		$fanart = "/plugins/images/cache/no-np.png";
+		if (file_exists($cacheFile)) {
+			$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+			unset($cacheFile);
+		}
+		$details = array(
+			"seasonCount" => "",
+			"status" => $child['show_status'],
+			"topTitle" => $seriesName,
+			"bottomTitle" => $bottomTitle,
+			"overview" => isset($child['ep_plot']) ? $child['ep_plot'] : '',
+			"runtime" => "",
+			"image" => $fanart,
+			"ratings" => "",
+			"videoQuality" => isset($child["quality"]) ? $child["quality"] : "",
+			"audioChannels" => "",
+			"audioCodec" => "",
+			"videoCodec" => "",
+			"size" => "",
+			"genres" => "",
+		);
+		array_push($gotCalendar, array(
+			"id" => "Sick-" . $number . "-Today-" . $i,
+			"title" => $seriesName,
+			"start" => $episodeAirDate,
+			"className" => "bg-calendar calendar-item tvID--" . $episodeID,
+			"imagetype" => "tv " . $downloaded,
+			"imagetypeFilter" => "tv",
+			"downloadFilter" => $downloaded,
+			"bgColor" => str_replace('text', 'bg', $downloaded),
+			"details" => $details,
+		));
+	}
+	foreach ($array['data']['soon'] as $child) {
+		$i++;
+		$seriesName = $child['show_name'];
+		$seriesID = $child['tvdbid'];
+		$episodeID = $child['tvdbid'];
+		$episodeAirDate = $child['airdate'];
+		$episodeAirDateTime = explode(" ", $child['airs']);
+		$episodeAirDateTime = date("H:i:s", strtotime($episodeAirDateTime[1] . $episodeAirDateTime[2]));
+		$episodeAirDate = strtotime($episodeAirDate . $episodeAirDateTime);
+		$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
+		if (new DateTime() < new DateTime($episodeAirDate)) {
+			$unaired = true;
+		}
+		$downloaded = "0";
+		if ($downloaded == "0" && isset($unaired)) {
+			$downloaded = "text-info";
+		} elseif ($downloaded == "1") {
+			$downloaded = "text-success";
+		} else {
+			$downloaded = "text-danger";
+		}
+		$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']) . ' - ' . $child['ep_name'];
+		$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+		$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+		$fanart = "/plugins/images/cache/no-np.png";
+		if (file_exists($cacheFile)) {
+			$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+			unset($cacheFile);
+		}
+		$details = array(
+			"seasonCount" => "",
+			"status" => $child['show_status'],
+			"topTitle" => $seriesName,
+			"bottomTitle" => $bottomTitle,
+			"overview" => isset($child['ep_plot']) ? $child['ep_plot'] : '',
+			"runtime" => "",
+			"image" => $fanart,
+			"ratings" => "",
+			"videoQuality" => isset($child["quality"]) ? $child["quality"] : "",
+			"audioChannels" => "",
+			"audioCodec" => "",
+			"videoCodec" => "",
+			"size" => "",
+			"genres" => "",
+		);
+		array_push($gotCalendar, array(
+			"id" => "Sick-" . $number . "-Soon-" . $i,
+			"title" => $seriesName,
+			"start" => $episodeAirDate,
+			"className" => "bg-calendar calendar-item tvID--" . $episodeID,
+			"imagetype" => "tv " . $downloaded,
+			"imagetypeFilter" => "tv",
+			"downloadFilter" => $downloaded,
+			"bgColor" => str_replace('text', 'bg', $downloaded),
+			"details" => $details,
+		));
+	}
+	foreach ($array['data']['later'] as $child) {
+		$i++;
+		$seriesName = $child['show_name'];
+		$seriesID = $child['tvdbid'];
+		$episodeID = $child['tvdbid'];
+		$episodeAirDate = $child['airdate'];
+		$episodeAirDateTime = explode(" ", $child['airs']);
+		$episodeAirDateTime = date("H:i:s", strtotime($episodeAirDateTime[1] . $episodeAirDateTime[2]));
+		$episodeAirDate = strtotime($episodeAirDate . $episodeAirDateTime);
+		$episodeAirDate = date("Y-m-d H:i:s", $episodeAirDate);
+		if (new DateTime() < new DateTime($episodeAirDate)) {
+			$unaired = true;
+		}
+		$downloaded = "0";
+		if ($downloaded == "0" && isset($unaired)) {
+			$downloaded = "text-info";
+		} elseif ($downloaded == "1") {
+			$downloaded = "text-success";
+		} else {
+			$downloaded = "text-danger";
+		}
+		$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']) . ' - ' . $child['ep_name'];
+		$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+		$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+		$fanart = "/plugins/images/cache/no-np.png";
+		if (file_exists($cacheFile)) {
+			$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+			unset($cacheFile);
+		}
+		$details = array(
+			"seasonCount" => "",
+			"status" => $child['show_status'],
+			"topTitle" => $seriesName,
+			"bottomTitle" => $bottomTitle,
+			"overview" => isset($child['ep_plot']) ? $child['ep_plot'] : '',
+			"runtime" => "",
+			"image" => $fanart,
+			"ratings" => "",
+			"videoQuality" => isset($child["quality"]) ? $child["quality"] : "",
+			"audioChannels" => "",
+			"audioCodec" => "",
+			"videoCodec" => "",
+			"size" => "",
+			"genres" => "",
+		);
+		array_push($gotCalendar, array(
+			"id" => "Sick-" . $number . "-Later-" . $i,
+			"title" => $seriesName,
+			"start" => $episodeAirDate,
+			"className" => "bg-calendar calendar-item tvID--" . $episodeID,
+			"imagetype" => "tv " . $downloaded,
+			"imagetypeFilter" => "tv",
+			"downloadFilter" => $downloaded,
+			"bgColor" => str_replace('text', 'bg', $downloaded),
+			"details" => $details,
+		));
+	}
+	if ($i != 0) {
+		return $gotCalendar;
+	}
+	return false;
+}
+
+function getSickrageCalendarHistory($array, $number)
+{
+	$array = json_decode($array, true);
+	$gotCalendar = array();
+	$i = 0;
+	foreach ($array['data'] as $child) {
+		$i++;
+		$seriesName = $child['show_name'];
+		$seriesID = $child['tvdbid'];
+		$episodeID = $child['tvdbid'];
+		$episodeAirDate = $child['date'];
+		$downloaded = "text-success";
+		$bottomTitle = 'S' . sprintf("%02d", $child['season']) . 'E' . sprintf("%02d", $child['episode']);
+		$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+		$cacheFile = $cacheDirectory . $seriesID . '.jpg';
+		$fanart = "/plugins/images/cache/no-np.png";
+		if (file_exists($cacheFile)) {
+			$fanart = 'plugins/images/cache/' . $seriesID . '.jpg';
+			unset($cacheFile);
+		}
+		$details = array(
+			"seasonCount" => "",
+			"status" => $child['status'],
+			"topTitle" => $seriesName,
+			"bottomTitle" => $bottomTitle,
+			"overview" => '',
+			"runtime" => isset($child['series']['runtime']) ? $child['series']['runtime'] : 30,
+			"image" => $fanart,
+			"ratings" => isset($child['series']['ratings']['value']) ? $child['series']['ratings']['value'] : "unknown",
+			"videoQuality" => isset($child["quality"]) ? $child['quality'] : "unknown",
+			"audioChannels" => "",
+			"audioCodec" => "",
+			"videoCodec" => "",
+			"size" => "",
+			"genres" => "",
+		);
+		array_push($gotCalendar, array(
+			"id" => "Sick-" . $number . "-History-" . $i,
+			"title" => $seriesName,
+			"start" => $episodeAirDate,
+			"className" => "bg-calendar calendar-item tvID--" . $episodeID,
+			"imagetype" => "tv " . $downloaded,
+			"imagetypeFilter" => "tv",
+			"downloadFilter" => $downloaded,
+			"bgColor" => str_replace('text', 'bg', $downloaded),
+			"details" => $details,
+		));
+	}
+	if ($i != 0) {
+		return $gotCalendar;
+	}
+	return false;
+}
+
+function ombiAPI($array)
+{
+	return ombiAction($array['data']['id'], $array['data']['action'], $array['data']['type'], $array['data']);
+}
+
+function ombiImport($type = null)
+{
+	if (!empty($GLOBALS['ombiURL']) && !empty($GLOBALS['ombiToken']) && !empty($type)) {
+		try {
+			$url = qualifyURL($GLOBALS['ombiURL']);
+			$headers = array(
+				"Accept" => "application/json",
+				"Content-Type" => "application/json",
+				"Apikey" => $GLOBALS['ombiToken']
+			);
+			$options = (localURL($url)) ? array('verify' => false) : array();
+			switch ($type) {
+				case 'emby':
+				case 'emby_local':
+				case 'emby_connect':
+				case 'emby_all':
+					$response = Requests::post($url . "/api/v1/Job/embyuserimporter", $headers, $options);
+					break;
+				case 'plex':
+					$response = Requests::post($url . "/api/v1/Job/plexuserimporter", $headers, $options);
+					break;
+				default:
+					break;
+			}
+			if ($response->success) {
+				writeLog('success', 'OMBI Connect Function - Ran User Import', 'SYSTEM');
+				return true;
+			} else {
+				writeLog('error', 'OMBI Connect Function - Error: Connection Unsuccessful', 'SYSTEM');
+				return false;
+			}
+		} catch (Requests_Exception $e) {
+			writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			return false;
+		};
+	}
+	return false;
+}
+
+function ombiAction($id, $action, $type, $fullArray = null)
+{
+	if ($GLOBALS['homepageOmbiEnabled'] && !empty($GLOBALS['ombiURL']) && !empty($GLOBALS['ombiToken']) && qualifyRequest($GLOBALS['homepageOmbiAuth'])) {
+		$url = qualifyURL($GLOBALS['ombiURL']);
+		$headers = array(
+			"Accept" => "application/json",
+			"Content-Type" => "application/json",
+			"Apikey" => $GLOBALS['ombiToken']
+		);
+		$data = array(
+			'id' => $id,
+		);
+		switch ($type) {
+			case 'season':
+			case 'tv':
+				$type = 'tv';
+				$add = array(
+					'tvDbId' => $id,
+					'requestAll' => ombiTVDefault('all'),
+					'latestSeason' => ombiTVDefault('last'),
+					'firstSeason' => ombiTVDefault('first')
+				);
+				break;
+			default:
+				$type = 'movie';
+				$add = array("theMovieDbId" => (int)$id);
+				break;
+		}
+		$success['head'] = $headers;
+		$success['act'] = $action;
+		$success['data'] = $data;
+		$success['add'] = $add;
+		$success['type'] = $type;
+		try {
+			$options = (localURL($url)) ? array('verify' => false) : array();
+			switch ($action) {
+				case 'add':
+					if (isset($_COOKIE['Auth'])) {
+						$headers = array(
+							"Accept" => "application/json",
+							"Content-Type" => "application/json",
+							"Authorization" => "Bearer " . $_COOKIE['Auth']
+						);
+						$success['head'] = $headers;
+					} else {
+						return false;
+					}
+					$response = Requests::post($url . "/api/v1/Request/" . $type, $headers, json_encode($add), $options);
+					break;
+				default:
+					if (qualifyRequest(1)) {
+						switch ($action) {
+							case 'approve':
+								$response = Requests::post($url . "/api/v1/Request/" . $type . "/approve", $headers, json_encode($data), $options);
+								break;
+							case 'available':
+								$response = Requests::post($url . "/api/v1/Request/" . $type . "/available", $headers, json_encode($data), $options);
+								break;
+							case 'unavailable':
+								$response = Requests::post($url . "/api/v1/Request/" . $type . "/unavailable", $headers, json_encode($data), $options);
+								break;
+							case 'deny':
+								$response = Requests::put($url . "/api/v1/Request/" . $type . "/deny", $headers, json_encode($data), $options);
+								break;
+							case 'delete':
+								$response = Requests::delete($url . "/api/v1/Request/" . $type . "/" . $id, $headers, $options);
+								break;
+							default:
+								return false;
+						}
+					}
+					break;
+			}
+			$success['api'] = $response;
+			$success['bd'] = $response->body;
+			$success['hd'] = $response->headers;
+			if ($response->success) {
+				$success['ok'] = true;
+			}
+		} catch (Requests_Exception $e) {
+			writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+	}
+	return isset($success['ok']) ? $success : false;
+}
+
+function getOmbiRequests($type = "both")
+{
+	if ($GLOBALS['homepageOmbiEnabled'] && !empty($GLOBALS['ombiURL']) && !empty($GLOBALS['ombiToken']) && qualifyRequest($GLOBALS['homepageOmbiAuth'])) {
+		$url = qualifyURL($GLOBALS['ombiURL']);
+		$headers = array(
+			"Accept" => "application/json",
+			"Apikey" => $GLOBALS['ombiToken'],
+		);
+		$requests = array();
+		try {
+			$options = (localURL($url)) ? array('verify' => false) : array();
+			switch ($type) {
+				case 'movie':
+					$movie = Requests::get($url . "/api/v1/Request/movie", $headers, $options);
+					break;
+				case 'tv':
+					$tv = Requests::get($url . "/api/v1/Request/tv", $headers, $options);
+					break;
+				default:
+					$movie = Requests::get($url . "/api/v1/Request/movie", $headers, $options);
+					$tv = Requests::get($url . "/api/v1/Request/tv", $headers, $options);
+					break;
+			}
+			if ($movie->success || $tv->success) {
+				if (isset($movie)) {
+					$movie = json_decode($movie->body, true);
+					//$movie = array_reverse($movie);
+					foreach ($movie as $key => $value) {
+						$requests[] = array(
+							'test' => $value,
+							'id' => $value['theMovieDbId'],
+							'title' => $value['title'],
+							'overview' => $value['overview'],
+							'poster' => (isset($value['posterPath']) && $value['posterPath'] !== '') ? 'https://image.tmdb.org/t/p/w300/' . $value['posterPath'] : '',
+							'background' => (isset($value['background']) && $value['background'] !== '') ? 'https://image.tmdb.org/t/p/w1280/' . $value['background'] : '',
+							'approved' => $value['approved'],
+							'available' => $value['available'],
+							'denied' => $value['denied'],
+							'deniedReason' => $value['deniedReason'],
+							'user' => $value['requestedUser']['userName'],
+							'request_id' => $value['id'],
+							'request_date' => $value['requestedDate'],
+							'release_date' => $value['releaseDate'],
+							'type' => 'movie',
+							'icon' => 'mdi mdi-filmstrip',
+							'color' => 'palette-Deep-Purple-900 bg white',
+						);
+					}
+				}
+				if (isset($tv) && (is_array($tv) || is_object($tv))) {
+					$tv = json_decode($tv->body, true);
+					foreach ($tv as $key => $value) {
+						if (is_array($value['childRequests'][0])) {
+							$requests[] = array(
+								'test' => $value,
+								'id' => $value['tvDbId'],
+								'title' => $value['title'],
+								'overview' => $value['overview'],
+								'poster' => $value['posterPath'],
+								'background' => (isset($value['background']) && $value['background'] !== '') ? 'https://image.tmdb.org/t/p/w1280/' . $value['background'] : '',
+								'approved' => $value['childRequests'][0]['approved'],
+								'available' => $value['childRequests'][0]['available'],
+								'denied' => $value['childRequests'][0]['denied'],
+								'deniedReason' => $value['childRequests'][0]['deniedReason'],
+								'user' => $value['childRequests'][0]['requestedUser']['userName'],
+								'request_id' => $value['id'],
+								'request_date' => $value['childRequests'][0]['requestedDate'],
+								'release_date' => $value['releaseDate'],
+								'type' => 'tv',
+								'icon' => 'mdi mdi-television',
+								'color' => 'grayish-blue-bg',
+							);
+						}
+					}
+				}
+				//sort here
+				usort($requests, function ($item1, $item2) {
+					if ($item1['request_date'] == $item2['request_date']) {
+						return 0;
+					}
+					return $item1['request_date'] > $item2['request_date'] ? -1 : 1;
+				});
+			}
+		} catch (Requests_Exception $e) {
+			writeLog('error', 'OMBI Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+	}
+	$api['content'] = isset($requests) ? $requests : false;
+	return $api;
+}
+
+function testAPIConnection($array)
+{
+	switch ($array['data']['action']) {
+		case 'ombi':
+			if (!empty($GLOBALS['ombiURL']) && !empty($GLOBALS['ombiToken'])) {
+				$url = qualifyURL($GLOBALS['ombiURL']);
+				$url = $url . "/api/v1/Status/info";
+				$headers = array(
+					"Accept" => "application/json",
+					"Content-Type" => "application/json",
+					"Apikey" => $GLOBALS['ombiToken']
+				);
+				try {
+					$options = (localURL($url)) ? array('verify' => false) : array();
+					$response = Requests::get($url, $headers, $options);
+					if ($response->success) {
+						return true;
+					} else {
+						return $response->body;
+					}
+				} catch (Requests_Exception $e) {
+					return $e->getMessage();
+				};
+			} else {
+				return 'URL and/or Token not setup';
+			}
+			break;
+		case 'plex':
+			if (!empty($GLOBALS['plexURL']) && !empty($GLOBALS['plexToken'])) {
+				$url = qualifyURL($GLOBALS['plexURL']);
+				$url = $url . "/?X-Plex-Token=" . $GLOBALS['plexToken'];
+				try {
+					$options = (localURL($url)) ? array('verify' => false) : array();
+					$response = Requests::get($url, array(), $options);
+					libxml_use_internal_errors(true);
+					if ($response->success) {
+						return true;
+					}
+				} catch (Requests_Exception $e) {
+					return $e->getMessage();
+				};
+			} else {
+				return 'URL and/or Token not setup';
+			}
+			break;
+		case 'emby':
+			break;
+		case 'sonarr':
+			if (!empty($GLOBALS['sonarrURL']) && !empty($GLOBALS['sonarrToken'])) {
+				$sonarrs = array();
+				$sonarrURLList = explode(',', $GLOBALS['sonarrURL']);
+				$sonarrTokenList = explode(',', $GLOBALS['sonarrToken']);
+				if (count($sonarrURLList) == count($sonarrTokenList)) {
+					foreach ($sonarrURLList as $key => $value) {
+						$sonarrs[$key] = array(
+							'url' => $value,
+							'token' => $sonarrTokenList[$key]
+						);
+					}
+					foreach ($sonarrs as $key => $value) {
+						try {
+							$sonarr = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
+							$result = json_decode($sonarr->getSystemStatus(), true);
+							return (array_key_exists('error', $result)) ? $result['error']['msg'] : true;
+						} catch (Exception $e) {
+							return strip($e->getMessage());
+						}
+					}
+				}
+			} else {
+				return 'URL/s and/or Token/s not setup';
+			}
+			break;
+		case 'lidarr':
+			if (!empty($GLOBALS['lidarrURL']) && !empty($GLOBALS['lidarrToken'])) {
+				$sonarrs = array();
+				$sonarrURLList = explode(',', $GLOBALS['lidarrURL']);
+				$sonarrTokenList = explode(',', $GLOBALS['lidarrToken']);
+				if (count($sonarrURLList) == count($sonarrTokenList)) {
+					foreach ($sonarrURLList as $key => $value) {
+						$sonarrs[$key] = array(
+							'url' => $value,
+							'token' => $sonarrTokenList[$key]
+						);
+					}
+					foreach ($sonarrs as $key => $value) {
+						try {
+							$sonarr = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token'], true);
+							$result = json_decode($sonarr->getSystemStatus(), true);
+							return (array_key_exists('error', $result)) ? $result['error']['msg'] : true;
+						} catch (Exception $e) {
+							return $e->getMessage();
+						}
+					}
+				}
+			} else {
+				return 'URL/s and/or Token/s not setup';
+			}
+			break;
+		case 'radarr':
+			if (!empty($GLOBALS['radarrURL']) && !empty($GLOBALS['radarrToken'])) {
+				$sonarrs = array();
+				$sonarrURLList = explode(',', $GLOBALS['radarrURL']);
+				$sonarrTokenList = explode(',', $GLOBALS['radarrToken']);
+				if (count($sonarrURLList) == count($sonarrTokenList)) {
+					foreach ($sonarrURLList as $key => $value) {
+						$sonarrs[$key] = array(
+							'url' => $value,
+							'token' => $sonarrTokenList[$key]
+						);
+					}
+					foreach ($sonarrs as $key => $value) {
+						try {
+							$sonarr = new Kryptonit3\Sonarr\Sonarr($value['url'], $value['token']);
+							$result = json_decode($sonarr->getSystemStatus(), true);
+							return (array_key_exists('error', $result)) ? $result['error']['msg'] : true;
+						} catch (Exception $e) {
+							return $e->getMessage();
+						}
+					}
+				}
+			} else {
+				return 'URL/s and/or Token/s not setup';
+			}
+			break;
+		case 'sabnzbd':
+			if (!empty($GLOBALS['sabnzbdURL']) && !empty($GLOBALS['sabnzbdToken'])) {
+				$url = qualifyURL($GLOBALS['sabnzbdURL']);
+				$url = $url . '/api?mode=queue&output=json&apikey=' . $GLOBALS['sabnzbdToken'];
+				try {
+					$options = (localURL($url)) ? array('verify' => false) : array();
+					$response = Requests::get($url, array(), $options);
+					if ($response->success) {
+						return true;
+					}
+				} catch (Requests_Exception $e) {
+					return $e->getMessage();
+				};
+			} else {
+				return 'URL and/or Token not setup';
+			}
+			break;
+		case 'nzbget':
+			if (!empty($GLOBALS['nzbgetURL'])) {
+				$url = qualifyURL($GLOBALS['nzbgetURL']);
+				if (!empty($GLOBALS['nzbgetUsername']) && !empty($GLOBALS['nzbgetPassword'])) {
+					$url = $url . '/' . $GLOBALS['nzbgetUsername'] . ':' . decrypt($GLOBALS['nzbgetPassword']) . '/jsonrpc/listgroups';
+				} else {
+					$url = $url . '/jsonrpc/listgroups';
+				}
+				try {
+					$options = (localURL($url)) ? array('verify' => false) : array();
+					$response = Requests::get($url, array(), $options);
+					if ($response->success) {
+						return true;
+					}
+				} catch (Requests_Exception $e) {
+					return $e->getMessage();
+				};
+			} else {
+				return 'URL and/or Username/Password not setup';
+			}
+			break;
+		case 'deluge':
+			if (!empty($GLOBALS['delugeURL']) && !empty($GLOBALS['delugePassword'])) {
+				try {
+					$deluge = new deluge($GLOBALS['delugeURL'], decrypt($GLOBALS['delugePassword']));
+					$torrents = $deluge->getTorrents(null, 'comment, download_payload_rate, eta, hash, is_finished, is_seed, message, name, paused, progress, queue, state, total_size, upload_payload_rate');
+					return true;
+				} catch (Exception $e) {
+					return $e->getMessage();
+				}
+			} else {
+				return 'URL and/or Password not setup';
+			}
+			break;
+		case 'rtorrent':
+			if (!empty($GLOBALS['rTorrentURL'])) {
+				try {
+					$digest = qualifyURL($GLOBALS['rTorrentURL'], true);
+					$passwordInclude = ($GLOBALS['rTorrentUsername'] != '' && $GLOBALS['rTorrentPassword'] != '') ? $GLOBALS['rTorrentUsername'] . ':' . decrypt($GLOBALS['rTorrentPassword']) . "@" : '';
+					$extraPath = (strpos($GLOBALS['rTorrentURL'], '.php') !== false) ? '' : '/RPC2';
+					$url = $digest['scheme'] . '://' . $passwordInclude . $digest['host'] . $digest['port'] . $digest['path'] . $extraPath;
+					$options = (localURL($url)) ? array('verify' => false) : array();
+					$data = xmlrpc_encode_request("system.listMethods", null);
+					$response = Requests::post($url, array(), $data, $options);
+					if ($response->success) {
+						$methods = xmlrpc_decode(str_replace('i8>', 'i4>', $response->body));
+						if (count($methods) !== 0) {
+							return true;
+						}
+					}
+					return false;
+				} catch
+				(Requests_Exception $e) {
+					writeLog('error', 'rTorrent Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+					return $e->getMessage();
+				};
+			}
+			break;
+		default :
+			return false;
+	}
+	return false;
+}

+ 2241 - 0
api/functions/homepage-functions.php

@@ -0,0 +1,2241 @@
+<?php
+//homepage order
+function homepageOrder()
+{
+	$homepageOrder = array(
+		"homepageOrdercustomhtml" => $GLOBALS['homepageOrdercustomhtml'],
+		"homepageOrdercustomhtmlTwo" => $GLOBALS['homepageOrdercustomhtmlTwo'],
+		"homepageOrdernzbget" => $GLOBALS['homepageOrdernzbget'],
+		"homepageOrdersabnzbd" => $GLOBALS['homepageOrdersabnzbd'],
+		"homepageOrderplexnowplaying" => $GLOBALS['homepageOrderplexnowplaying'],
+		"homepageOrderplexrecent" => $GLOBALS['homepageOrderplexrecent'],
+		"homepageOrderplexplaylist" => $GLOBALS['homepageOrderplexplaylist'],
+		"homepageOrderembynowplaying" => $GLOBALS['homepageOrderembynowplaying'],
+		"homepageOrderembyrecent" => $GLOBALS['homepageOrderembyrecent'],
+		"homepageOrderombi" => $GLOBALS['homepageOrderombi'],
+		"homepageOrdercalendar" => $GLOBALS['homepageOrdercalendar'],
+		"homepageOrdertransmission" => $GLOBALS['homepageOrdertransmission'],
+		"homepageOrderqBittorrent" => $GLOBALS['homepageOrderqBittorrent'],
+		"homepageOrderdeluge" => $GLOBALS['homepageOrderdeluge'],
+		"homepageOrderrTorrent" => $GLOBALS['homepageOrderrTorrent'],
+		"homepageOrderdownloader" => $GLOBALS['homepageOrderdownloader'],
+	);
+	asort($homepageOrder);
+	return $homepageOrder;
+}
+
+function buildHomepage()
+{
+	$homepageOrder = homepageOrder();
+	$homepageBuilt = '';
+	foreach ($homepageOrder as $key => $value) {
+		$homepageBuilt .= buildHomepageItem($key);
+	}
+	return $homepageBuilt;
+}
+
+function buildHomepageItem($homepageItem)
+{
+	$item = '<div id="' . $homepageItem . '">';
+	switch ($homepageItem) {
+		case 'homepageOrdercustomhtml':
+			if ($GLOBALS['homepagCustomHTMLoneEnabled'] && qualifyRequest($GLOBALS['homepagCustomHTMLoneAuth'])) {
+				$item .= ($GLOBALS['customHTMLone'] !== '') ? $GLOBALS['customHTMLone'] : '';
+			}
+			break;
+		case 'homepageOrdercustomhtmlTwo':
+			if ($GLOBALS['homepagCustomHTMLtwoEnabled'] && qualifyRequest($GLOBALS['homepagCustomHTMLtwoAuth'])) {
+				$item .= ($GLOBALS['customHTMLtwo'] !== '') ? $GLOBALS['customHTMLtwo'] : '';
+			}
+			break;
+		case 'homepageOrdernotice':
+			break;
+		case 'homepageOrdernoticeguest':
+			break;
+		case 'homepageOrderqBittorrent':
+			if ($GLOBALS['homepageqBittorrentEnabled'] && qualifyRequest($GLOBALS['homepageqBittorrentAuth'])) {
+				if ($GLOBALS['qBittorrentCombine']) {
+					$item .= '
+	                <script>
+	                // homepageOrderqBittorrent
+	                buildDownloaderCombined(\'qBittorrent\');
+	                homepageDownloader("qBittorrent", "' . $GLOBALS['homepageDownloadRefresh'] . '");
+	                // End homepageOrderqBittorrent
+	                </script>
+	                ';
+				} else {
+					$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+					$item .= '
+	                <script>
+	                // homepageOrderqBittorrent
+	                $("#' . $homepageItem . '").html(buildDownloader("qBittorrent"));
+	                homepageDownloader("qBittorrent", "' . $GLOBALS['homepageDownloadRefresh'] . '");
+	                // End homepageOrderqBittorrent
+	                </script>
+	                ';
+				}
+			}
+			break;
+		case 'homepageOrderrTorrent':
+			if ($GLOBALS['homepagerTorrentEnabled'] && qualifyRequest($GLOBALS['homepagerTorrentAuth'])) {
+				if ($GLOBALS['rTorrentCombine']) {
+					$item .= '
+	                <script>
+	                // homepageOrderrTorrent
+	                buildDownloaderCombined(\'rTorrent\');
+	                homepageDownloader("rTorrent", "' . $GLOBALS['homepageDownloadRefresh'] . '");
+	                // End homepageOrderrTorrent
+	                </script>
+	                ';
+				} else {
+					$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+					$item .= '
+	                <script>
+	                // homepageOrderrTorrent
+	                $("#' . $homepageItem . '").html(buildDownloader("rTorrent"));
+	                homepageDownloader("rTorrent", "' . $GLOBALS['homepageDownloadRefresh'] . '");
+	                // End homepageOrderrTorrent
+	                </script>
+	                ';
+				}
+			}
+			break;
+		case 'homepageOrderdeluge':
+			if ($GLOBALS['homepageDelugeEnabled'] && qualifyRequest($GLOBALS['homepageDelugeAuth'])) {
+				if ($GLOBALS['delugeCombine']) {
+					$item .= '
+					<script>
+					// Deluge
+					buildDownloaderCombined(\'deluge\');
+					homepageDownloader("deluge", "' . $GLOBALS['homepageDownloadRefresh'] . '");
+					// End Deluge
+					</script>
+					';
+				} else {
+					$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+					$item .= '
+					<script>
+					// Deluge
+					$("#' . $homepageItem . '").html(buildDownloader("deluge"));
+					homepageDownloader("deluge", "' . $GLOBALS['homepageDownloadRefresh'] . '");
+					// End Deluge
+					</script>
+					';
+				}
+			}
+			break;
+		case 'homepageOrdertransmission':
+			if ($GLOBALS['homepageTransmissionEnabled'] && qualifyRequest($GLOBALS['homepageTransmissionAuth'])) {
+				if ($GLOBALS['transmissionCombine']) {
+					$item .= '
+					<script>
+					// Transmission
+					buildDownloaderCombined(\'transmission\');
+					homepageDownloader("transmission", "' . $GLOBALS['homepageDownloadRefresh'] . '");
+					// End Transmission
+					</script>
+					';
+				} else {
+					$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+					$item .= '
+					<script>
+					// Transmission
+					$("#' . $homepageItem . '").html(buildDownloader("transmission"));
+					homepageDownloader("transmission", "' . $GLOBALS['homepageDownloadRefresh'] . '");
+					// End Transmission
+					</script>
+					';
+				}
+			}
+			break;
+		case 'homepageOrdernzbget':
+			if ($GLOBALS['homepageNzbgetEnabled'] && qualifyRequest($GLOBALS['homepageNzbgetAuth'])) {
+				if ($GLOBALS['nzbgetCombine']) {
+					$item .= '
+					<script>
+					// NZBGet
+					buildDownloaderCombined(\'nzbget\');
+					homepageDownloader("nzbget", "' . $GLOBALS['homepageDownloadRefresh'] . '");
+					// End NZBGet
+					</script>
+					';
+				} else {
+					$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+					$item .= '
+					<script>
+					// NZBGet
+					$("#' . $homepageItem . '").html(buildDownloader("nzbget"));
+					homepageDownloader("nzbget", "' . $GLOBALS['homepageDownloadRefresh'] . '");
+					// End NZBGet
+					</script>
+					';
+				}
+			}
+			break;
+		case 'homepageOrdersabnzbd':
+			if ($GLOBALS['homepageSabnzbdEnabled'] && qualifyRequest($GLOBALS['homepageSabnzbdAuth'])) {
+				if ($GLOBALS['sabnzbdCombine']) {
+					$item .= '
+					<script>
+					// SabNZBd
+					buildDownloaderCombined(\'sabnzbd\');
+					homepageDownloader("sabnzbd", "' . $GLOBALS['homepageDownloadRefresh'] . '");
+					// End SabNZBd
+					</script>
+					';
+				} else {
+					$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Download Queue...</h2></div>';
+					$item .= '
+					<script>
+					// SabNZBd
+					$("#' . $homepageItem . '").html(buildDownloader("sabnzbd"));
+					homepageDownloader("sabnzbd", "' . $GLOBALS['homepageDownloadRefresh'] . '");
+					// End SabNZBd
+					</script>
+					';
+				}
+			}
+			break;
+		case 'homepageOrderplexnowplaying':
+			if ($GLOBALS['homepagePlexStreams']) {
+				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Now Playing...</h2></div>';
+				$item .= '
+				<script>
+				// Plex Stream
+				homepageStream("plex", "' . $GLOBALS['homepageStreamRefresh'] . '");
+				// End Plex Stream
+				</script>
+				';
+			}
+			break;
+		case 'homepageOrderplexrecent':
+			if ($GLOBALS['homepagePlexRecent']) {
+				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Recent...</h2></div>';
+				$item .= '
+				<script>
+				// Plex Recent
+				homepageRecent("plex", "' . $GLOBALS['homepageRecentRefresh'] . '");
+				// End Plex Recent
+				</script>
+				';
+			}
+			break;
+		case 'homepageOrderplexplaylist':
+			if ($GLOBALS['homepagePlexPlaylist']) {
+				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Playlists...</h2></div>';
+				$item .= '
+				<script>
+				// Plex Playlist
+				homepagePlaylist("plex");
+				// End Plex Playlist
+				</script>
+				';
+			}
+			break;
+		case 'homepageOrderembynowplaying':
+			if ($GLOBALS['homepageEmbyStreams']) {
+				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Now Playing...</h2></div>';
+				$item .= '
+				<script>
+				// Emby Stream
+				homepageStream("emby", "' . $GLOBALS['homepageStreamRefresh'] . '");
+				// End Emby Stream
+				</script>
+				';
+			}
+			break;
+		case 'homepageOrderembyrecent':
+			if ($GLOBALS['homepageEmbyRecent']) {
+				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Recent...</h2></div>';
+				$item .= '
+				<script>
+				// Emby Recent
+				homepageRecent("emby", "' . $GLOBALS['homepageRecentRefresh'] . '");
+				// End Emby Recent
+				</script>
+				';
+			}
+			break;
+		case 'homepageOrderombi':
+			if ($GLOBALS['homepageOmbiEnabled']) {
+				$item .= '<div class="white-box"><h2 class="text-center" lang="en">Loading Requests...</h2></div>';
+				$item .= '
+				<script>
+				// Ombi Requests
+				homepageRequests("' . $GLOBALS['ombiRefresh'] . '");
+				// End Ombi Requests
+				</script>
+				';
+			}
+			break;
+		case 'homepageOrdercalendar':
+			if ($GLOBALS['homepageSonarrEnabled'] && qualifyRequest($GLOBALS['homepageSonarrAuth']) || ($GLOBALS['homepageRadarrEnabled'] && qualifyRequest($GLOBALS['homepageRadarrAuth'])) || ($GLOBALS['homepageSickrageEnabled'] && qualifyRequest($GLOBALS['homepageSickrageAuth'])) || ($GLOBALS['homepageCouchpotatoEnabled'] && qualifyRequest($GLOBALS['homepageCouchpotatoAuth'])) || ($GLOBALS['homepageCalendarEnabled'] && qualifyRequest($GLOBALS['homepageCalendarAuth']) && $GLOBALS['calendariCal'] !== '')) {
+				$item .= '
+				<div id="calendar" class="fc fc-ltr m-b-30"></div>
+				<script>
+				// Calendar
+				homepageCalendar("' . $GLOBALS['calendarRefresh'] . '");
+				// End Calendar
+				</script>
+				';
+			}
+			break;
+		default:
+			# code...
+			break;
+	}
+	return $item . '</div>';
+}
+
+function getHomepageList()
+{
+	$groups = groupSelect();
+	$ombiTvOptions = array(
+		array(
+			'name' => 'All Seasons',
+			'value' => 'all'
+		),
+		array(
+			'name' => 'First Season Only',
+			'value' => 'first'
+		),
+		array(
+			'name' => 'Last Season Only',
+			'value' => 'last'
+		),
+	);
+	$mediaServers = array(
+		array(
+			'name' => 'N/A',
+			'value' => ''
+		),
+		array(
+			'name' => 'Plex',
+			'value' => 'plex'
+		),
+		array(
+			'name' => 'Emby [Not Available]',
+			'value' => 'emby'
+		)
+	);
+	$limit = array(
+		array(
+			'name' => '1 Item',
+			'value' => '1'
+		),
+		array(
+			'name' => '2 Items',
+			'value' => '2'
+		),
+		array(
+			'name' => '3 Items',
+			'value' => '3'
+		),
+		array(
+			'name' => '4 Items',
+			'value' => '4'
+		),
+		array(
+			'name' => '5 Items',
+			'value' => '5'
+		),
+		array(
+			'name' => '6 Items',
+			'value' => '6'
+		),
+		array(
+			'name' => '7 Items',
+			'value' => '7'
+		),
+		array(
+			'name' => '8 Items',
+			'value' => '8'
+		),
+		array(
+			'name' => 'Unlimited',
+			'value' => '1000'
+		),
+	);
+	$day = array(
+		array(
+			'name' => 'Sunday',
+			'value' => '0'
+		),
+		array(
+			'name' => 'Monday',
+			'value' => '1'
+		),
+		array(
+			'name' => 'Tueday',
+			'value' => '2'
+		),
+		array(
+			'name' => 'Wednesday',
+			'value' => '3'
+		),
+		array(
+			'name' => 'Thursday',
+			'value' => '4'
+		),
+		array(
+			'name' => 'Friday',
+			'value' => '5'
+		),
+		array(
+			'name' => 'Saturday',
+			'value' => '6'
+		)
+	);
+	$calendarDefault = array(
+		array(
+			'name' => 'Month',
+			'value' => 'month'
+		),
+		array(
+			'name' => 'Day',
+			'value' => 'basicDay'
+		),
+		array(
+			'name' => 'Week',
+			'value' => 'basicWeek'
+		),
+		array(
+			'name' => 'List',
+			'value' => 'list'
+		)
+	);
+	$timeFormat = array(
+		array(
+			'name' => '6p',
+			'value' => 'h(:mm)t'
+		),
+		array(
+			'name' => '6:00p',
+			'value' => 'h:mmt'
+		),
+		array(
+			'name' => '6:00',
+			'value' => 'h:mm'
+		),
+		array(
+			'name' => '18',
+			'value' => 'H(:mm)'
+		),
+		array(
+			'name' => '18:00',
+			'value' => 'H:mm'
+		)
+	);
+	$rTorrentSortOptions = array(
+		array(
+			'name' => 'Date Desc',
+			'value' => 'dated'
+		),
+		array(
+			'name' => 'Date Asc',
+			'value' => 'datea'
+		),
+		array(
+			'name' => 'Hash Desc',
+			'value' => 'hashd'
+		),
+		array(
+			'name' => 'Hash Asc',
+			'value' => 'hasha'
+		),
+		array(
+			'name' => 'Name Desc',
+			'value' => 'named'
+		),
+		array(
+			'name' => 'Name Asc',
+			'value' => 'namea'
+		),
+		array(
+			'name' => 'Size Desc',
+			'value' => 'sized'
+		),
+		array(
+			'name' => 'Size Asc',
+			'value' => 'sizea'
+		),
+		array(
+			'name' => 'Label Desc',
+			'value' => 'labeld'
+		),
+		array(
+			'name' => 'Label Asc',
+			'value' => 'labela'
+		),
+		array(
+			'name' => 'Status Desc',
+			'value' => 'statusd'
+		),
+		array(
+			'name' => 'Status Asc',
+			'value' => 'statusa'
+		),
+	);
+	$qBittorrentSortOptions = array(
+		array(
+			'name' => 'Hash',
+			'value' => 'hash'
+		),
+		array(
+			'name' => 'Name',
+			'value' => 'name'
+		),
+		array(
+			'name' => 'Size',
+			'value' => 'size'
+		),
+		array(
+			'name' => 'Progress',
+			'value' => 'progress'
+		),
+		array(
+			'name' => 'Download Speed',
+			'value' => 'dlspeed'
+		),
+		array(
+			'name' => 'Upload Speed',
+			'value' => 'upspeed'
+		),
+		array(
+			'name' => 'Priority',
+			'value' => 'priority'
+		),
+		array(
+			'name' => 'Number of Seeds',
+			'value' => 'num_seeds'
+		),
+		array(
+			'name' => 'Number of Seeds in Swarm',
+			'value' => 'num_complete'
+		),
+		array(
+			'name' => 'Number of Leechers',
+			'value' => 'num_leechs'
+		),
+		array(
+			'name' => 'Number of Leechers in Swarm',
+			'value' => 'num_incomplete'
+		),
+		array(
+			'name' => 'Ratio',
+			'value' => 'ratio'
+		),
+		array(
+			'name' => 'ETA',
+			'value' => 'eta'
+		),
+		array(
+			'name' => 'State',
+			'value' => 'state'
+		),
+		array(
+			'name' => 'Category',
+			'value' => 'category'
+		)
+	);
+	$xmlStatus = (extension_loaded('xmlrpc')) ? 'Installed' : 'Not Installed';
+	return array(array(
+		'name' => 'Calendar',
+		'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
+		'image' => 'plugins/images/tabs/calendar.png',
+		'category' => 'HOMEPAGE',
+		'settings' => array(
+			'Enable' => array(
+				array(
+					'type' => 'switch',
+					'name' => 'homepageCalendarEnabled',
+					'label' => 'Enable iCal',
+					'value' => $GLOBALS['homepageCalendarEnabled']
+				),
+				array(
+					'type' => 'select',
+					'name' => 'homepageCalendarAuth',
+					'label' => 'Minimum Authentication',
+					'value' => $GLOBALS['homepageCalendarAuth'],
+					'options' => $groups
+				),
+				array(
+					'type' => 'input',
+					'name' => 'calendariCal',
+					'label' => 'iCal URL\'s',
+					'value' => $GLOBALS['calendariCal'],
+					'placeholder' => 'separate by comma\'s'
+				),
+			),
+			'Misc Options' => array(
+				array(
+					'type' => 'number',
+					'name' => 'calendarStart',
+					'label' => '# of Days Before',
+					'value' => $GLOBALS['calendarStart'],
+					'placeholder' => ''
+				),
+				array(
+					'type' => 'number',
+					'name' => 'calendarEnd',
+					'label' => '# of Days After',
+					'value' => $GLOBALS['calendarEnd'],
+					'placeholder' => ''
+				),
+				array(
+					'type' => 'select',
+					'name' => 'calendarFirstDay',
+					'label' => 'Start Day',
+					'value' => $GLOBALS['calendarFirstDay'],
+					'options' => $day
+				),
+				array(
+					'type' => 'select',
+					'name' => 'calendarDefault',
+					'label' => 'Default View',
+					'value' => $GLOBALS['calendarDefault'],
+					'options' => $calendarDefault
+				),
+				array(
+					'type' => 'select',
+					'name' => 'calendarTimeFormat',
+					'label' => 'Time Format',
+					'value' => $GLOBALS['calendarTimeFormat'],
+					'options' => $timeFormat
+				),
+				array(
+					'type' => 'select',
+					'name' => 'calendarLimit',
+					'label' => 'Items Per Day',
+					'value' => $GLOBALS['calendarLimit'],
+					'options' => $limit
+				),
+				array(
+					'type' => 'select',
+					'name' => 'calendarRefresh',
+					'label' => 'Refresh Seconds',
+					'value' => $GLOBALS['calendarRefresh'],
+					'options' => optionTime()
+				)
+			),
+		)
+	),
+		array(
+			'name' => 'Plex',
+			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
+			'image' => 'plugins/images/tabs/plex.png',
+			'category' => 'Media Server',
+			//'license' => $GLOBALS['license'],
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepagePlexEnabled',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepagePlexEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepagePlexAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $GLOBALS['homepagePlexAuth'],
+						'options' => $groups
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'plexURL',
+						'label' => 'URL',
+						'value' => $GLOBALS['plexURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'plexToken',
+						'label' => 'Token',
+						'value' => $GLOBALS['plexToken']
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'plexID',
+						'label' => 'Plex Machine',
+						'value' => $GLOBALS['plexID']
+					)
+				),
+				'Active Streams' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepagePlexStreams',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepagePlexStreams']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepagePlexStreamsAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $GLOBALS['homepagePlexStreamsAuth'],
+						'options' => $groups
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'homepageShowStreamNames',
+						'label' => 'User Information',
+						'value' => $GLOBALS['homepageShowStreamNames']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageShowStreamNamesAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $GLOBALS['homepageShowStreamNamesAuth'],
+						'options' => $groups
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageStreamRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $GLOBALS['homepageStreamRefresh'],
+						'options' => optionTime()
+					),
+				),
+				'Recent Items' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepagePlexRecent',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepagePlexRecent']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepagePlexRecentAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $GLOBALS['homepagePlexRecentAuth'],
+						'options' => $groups
+					),
+					array(
+						'type' => 'number',
+						'name' => 'homepageRecentLimit',
+						'label' => 'Item Limit',
+						'value' => $GLOBALS['homepageRecentLimit'],
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageRecentRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $GLOBALS['homepageRecentRefresh'],
+						'options' => optionTime()
+					),
+				),
+				'Media Search' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'mediaSearch',
+						'label' => 'Enable',
+						'value' => $GLOBALS['mediaSearch']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'mediaSearchAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $GLOBALS['mediaSearchAuth'],
+						'options' => $groups
+					),
+					array(
+						'type' => 'select',
+						'name' => 'mediaSearchType',
+						'label' => 'Media Server',
+						'value' => $GLOBALS['mediaSearchType'],
+						'options' => $mediaServers
+					),
+				),
+				'Playlists' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepagePlexPlaylist',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepagePlexPlaylist']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepagePlexPlaylistAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $GLOBALS['homepagePlexPlaylistAuth'],
+						'options' => $groups
+					),
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'input',
+						'name' => 'plexTabName',
+						'label' => 'Plex Tab Name',
+						'value' => $GLOBALS['plexTabName'],
+						'placeholder' => 'Only use if you have Plex in a reverse proxy'
+					),
+					array(
+						'type' => 'input',
+						'name' => 'plexTabURL',
+						'label' => 'Plex Tab WAN URL',
+						'value' => $GLOBALS['plexTabURL'],
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'select',
+						'name' => 'cacheImageSize',
+						'label' => 'Image Cache Size',
+						'value' => $GLOBALS['cacheImageSize'],
+						'options' => array(
+							array(
+								'name' => 'Low',
+								'value' => '.5'
+							),
+							array(
+								'name' => '1x',
+								'value' => '1'
+							),
+							array(
+								'name' => '2x',
+								'value' => '2'
+							),
+							array(
+								'name' => '3x',
+								'value' => '3'
+							)
+						)
+					)
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'plex\')"'
+					),
+				)
+			)
+		),
+		array(
+			'name' => 'Emby',
+			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
+			'image' => 'plugins/images/tabs/emby.png',
+			'category' => 'Media Server',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageEmbyEnabled',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepageEmbyEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageEmbyAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $GLOBALS['homepageEmbyAuth'],
+						'options' => $groups
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'embyURL',
+						'label' => 'URL',
+						'value' => $GLOBALS['embyURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'embyToken',
+						'label' => 'Token',
+						'value' => $GLOBALS['embyToken']
+					)
+				),
+				'Active Streams' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageEmbyStreams',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepageEmbyStreams']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageEmbyStreamsAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $GLOBALS['homepageEmbyStreamsAuth'],
+						'options' => $groups
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'homepageShowStreamNames',
+						'label' => 'User Information',
+						'value' => $GLOBALS['homepageShowStreamNames']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageShowStreamNamesAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $GLOBALS['homepageShowStreamNamesAuth'],
+						'options' => $groups
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageStreamRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $GLOBALS['homepageStreamRefresh'],
+						'options' => optionTime()
+					),
+				),
+				'Recent Items' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageEmbyRecent',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepageEmbyRecent']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageEmbyRecentAuth',
+						'label' => 'Minimum Authorization',
+						'value' => $GLOBALS['homepageEmbyRecentAuth'],
+						'options' => $groups
+					),
+					array(
+						'type' => 'number',
+						'name' => 'homepageRecentLimit',
+						'label' => 'Item Limit',
+						'value' => $GLOBALS['homepageRecentLimit'],
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageRecentRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $GLOBALS['homepageRecentRefresh'],
+						'options' => optionTime()
+					),
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'input',
+						'name' => 'embyTabName',
+						'label' => 'Emby Tab Name',
+						'value' => $GLOBALS['embyTabName'],
+						'placeholder' => 'Only use if you have Emby in a reverse proxy'
+					),
+					array(
+						'type' => 'input',
+						'name' => 'embyTabURL',
+						'label' => 'Emby Tab WAN URL',
+						'value' => $GLOBALS['embyTabURL'],
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'select',
+						'name' => 'cacheImageSize',
+						'label' => 'Image Cache Size',
+						'value' => $GLOBALS['cacheImageSize'],
+						'options' => array(
+							array(
+								'name' => 'Low',
+								'value' => '.5'
+							),
+							array(
+								'name' => '1x',
+								'value' => '1'
+							),
+							array(
+								'name' => '2x',
+								'value' => '2'
+							),
+							array(
+								'name' => '3x',
+								'value' => '3'
+							)
+						)
+					)
+				)
+			)
+		),
+		array(
+			'name' => 'SabNZBD',
+			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
+			'image' => 'plugins/images/tabs/sabnzbd.png',
+			'category' => 'Downloader',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageSabnzbdEnabled',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepageSabnzbdEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageSabnzbdAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $GLOBALS['homepageSabnzbdAuth'],
+						'options' => $groups
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'sabnzbdURL',
+						'label' => 'URL',
+						'value' => $GLOBALS['sabnzbdURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'sabnzbdToken',
+						'label' => 'Token',
+						'value' => $GLOBALS['sabnzbdToken']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'select',
+						'name' => 'homepageDownloadRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $GLOBALS['homepageDownloadRefresh'],
+						'options' => optionTime()
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'sabnzbdCombine',
+						'label' => 'Add to Combined Downloader',
+						'value' => $GLOBALS['sabnzbdCombine']
+					),
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'sabnzbd\')"'
+					),
+				)
+			)
+		),
+		array(
+			'name' => 'NZBGet',
+			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
+			'image' => 'plugins/images/tabs/nzbget.png',
+			'category' => 'Downloader',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageNzbgetEnabled',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepageNzbgetEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageNzbgetAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $GLOBALS['homepageNzbgetAuth'],
+						'options' => $groups
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'nzbgetURL',
+						'label' => 'URL',
+						'value' => $GLOBALS['nzbgetURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'input',
+						'name' => 'nzbgetUsername',
+						'label' => 'Username',
+						'value' => $GLOBALS['nzbgetUsername']
+					),
+					array(
+						'type' => 'password',
+						'name' => 'nzbgetPassword',
+						'label' => 'Password',
+						'value' => $GLOBALS['nzbgetPassword']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'select',
+						'name' => 'homepageDownloadRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $GLOBALS['homepageDownloadRefresh'],
+						'options' => optionTime()
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'nzbgetCombine',
+						'label' => 'Add to Combined Downloader',
+						'value' => $GLOBALS['nzbgetCombine']
+					),
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'nzbget\')"'
+					),
+				)
+			)
+		),
+		array(
+			'name' => 'Transmission',
+			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
+			'image' => 'plugins/images/tabs/transmission.png',
+			'category' => 'Downloader',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageTransmissionEnabled',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepageTransmissionEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageTransmissionAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $GLOBALS['homepageTransmissionAuth'],
+						'options' => $groups
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'transmissionURL',
+						'label' => 'URL',
+						'value' => $GLOBALS['transmissionURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'input',
+						'name' => 'transmissionUsername',
+						'label' => 'Username',
+						'value' => $GLOBALS['transmissionUsername']
+					),
+					array(
+						'type' => 'password',
+						'name' => 'transmissionPassword',
+						'label' => 'Password',
+						'value' => $GLOBALS['transmissionPassword']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'transmissionHideSeeding',
+						'label' => 'Hide Seeding',
+						'value' => $GLOBALS['transmissionHideSeeding']
+					), array(
+						'type' => 'switch',
+						'name' => 'transmissionHideCompleted',
+						'label' => 'Hide Completed',
+						'value' => $GLOBALS['transmissionHideCompleted']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageDownloadRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $GLOBALS['homepageDownloadRefresh'],
+						'options' => optionTime()
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'transmissionCombine',
+						'label' => 'Add to Combined Downloader',
+						'value' => $GLOBALS['transmissionCombine']
+					),
+				)
+			)
+		),
+		array(
+			'name' => 'qBittorrent',
+			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
+			'image' => 'plugins/images/tabs/qBittorrent.png',
+			'category' => 'Downloader',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageqBittorrentEnabled',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepageqBittorrentEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageqBittorrentAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $GLOBALS['homepageqBittorrentAuth'],
+						'options' => $groups
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'qBittorrentURL',
+						'label' => 'URL',
+						'value' => $GLOBALS['qBittorrentURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'input',
+						'name' => 'qBittorrentUsername',
+						'label' => 'Username',
+						'value' => $GLOBALS['qBittorrentUsername']
+					),
+					array(
+						'type' => 'password',
+						'name' => 'qBittorrentPassword',
+						'label' => 'Password',
+						'value' => $GLOBALS['qBittorrentPassword']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'qBittorrentHideSeeding',
+						'label' => 'Hide Seeding',
+						'value' => $GLOBALS['qBittorrentHideSeeding']
+					), array(
+						'type' => 'switch',
+						'name' => 'qBittorrentHideCompleted',
+						'label' => 'Hide Completed',
+						'value' => $GLOBALS['qBittorrentHideCompleted']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'qBittorrentSortOrder',
+						'label' => 'Order',
+						'value' => $GLOBALS['qBittorrentSortOrder'],
+						'options' => $qBittorrentSortOptions
+					), array(
+						'type' => 'switch',
+						'name' => 'qBittorrentReverseSorting',
+						'label' => 'Reverse Sorting',
+						'value' => $GLOBALS['qBittorrentReverseSorting']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageDownloadRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $GLOBALS['homepageDownloadRefresh'],
+						'options' => optionTime()
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'qBittorrentCombine',
+						'label' => 'Add to Combined Downloader',
+						'value' => $GLOBALS['qBittorrentCombine']
+					),
+				)
+			)
+		),
+		array(
+			'name' => 'rTorrent',
+			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
+			'image' => 'plugins/images/tabs/rTorrent.png',
+			'category' => 'Downloader',
+			'settings' => array(
+				'FYI' => array(
+					array(
+						'type' => 'html',
+						'label' => '',
+						'override' => 12,
+						'html' => '
+						<div class="row">
+						    <div class="col-lg-12">
+						        <div class="panel panel-info">
+						            <div class="panel-heading">
+						                <span lang="en">This module requires XMLRPC</span>
+						            </div>
+						            <div class="panel-wrapper collapse in" aria-expanded="true">
+						                <div class="panel-body">
+						                    <span lang="en">Status: [ <b>' . $xmlStatus . '</b> ]</span>
+						                </div>
+						            </div>
+						        </div>
+						    </div>
+						</div>
+						'
+					)
+				),
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepagerTorrentEnabled',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepagerTorrentEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepagerTorrentAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $GLOBALS['homepagerTorrentAuth'],
+						'options' => $groups
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'rTorrentURL',
+						'label' => 'URL',
+						'value' => $GLOBALS['rTorrentURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'input',
+						'name' => 'rTorrentUsername',
+						'label' => 'Username',
+						'value' => $GLOBALS['rTorrentUsername']
+					),
+					array(
+						'type' => 'password',
+						'name' => 'rTorrentPassword',
+						'label' => 'Password',
+						'value' => $GLOBALS['rTorrentPassword']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'rTorrentHideSeeding',
+						'label' => 'Hide Seeding',
+						'value' => $GLOBALS['rTorrentHideSeeding']
+					), array(
+						'type' => 'switch',
+						'name' => 'rTorrentHideCompleted',
+						'label' => 'Hide Completed',
+						'value' => $GLOBALS['rTorrentHideCompleted']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'rTorrentSortOrder',
+						'label' => 'Order',
+						'value' => $GLOBALS['rTorrentSortOrder'],
+						'options' => $rTorrentSortOptions
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageDownloadRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $GLOBALS['homepageDownloadRefresh'],
+						'options' => optionTime()
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'rTorrentCombine',
+						'label' => 'Add to Combined Downloader',
+						'value' => $GLOBALS['rTorrentCombine']
+					),
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'rtorrent\')"'
+					),
+				)
+			)
+		),
+		array(
+			'name' => 'Deluge',
+			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
+			'image' => 'plugins/images/tabs/deluge.png',
+			'category' => 'Downloader',
+			'settings' => array(
+				'custom' => '
+				<div class="row">
+                    <div class="col-lg-12">
+                        <div class="panel panel-info">
+                            <div class="panel-heading">
+								<span lang="en">Notice</span>
+                            </div>
+                            <div class="panel-wrapper collapse in" aria-expanded="true">
+                                <div class="panel-body">
+									<ul class="list-icons">
+                                        <li><i class="fa fa-chevron-right text-danger"></i> <a href="https://github.com/idlesign/deluge-webapi/tree/master/dist" target="_blank">Download Plugin</a></li>
+                                        <li><i class="fa fa-chevron-right text-danger"></i> Open Deluge Web UI, go to "Preferences -> Plugins -> Install plugin" and choose egg file.</li>
+                                        <li><i class="fa fa-chevron-right text-danger"></i> Activate WebAPI plugin </li>
+                                    </ul>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+				</div>
+				',
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageDelugeEnabled',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepageDelugeEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageDelugeAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $GLOBALS['homepageDelugeAuth'],
+						'options' => $groups
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'delugeURL',
+						'label' => 'URL',
+						'value' => $GLOBALS['delugeURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password',
+						'name' => 'delugePassword',
+						'label' => 'Password',
+						'value' => $GLOBALS['delugePassword']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'delugeHideSeeding',
+						'label' => 'Hide Seeding',
+						'value' => $GLOBALS['delugeHideSeeding']
+					), array(
+						'type' => 'switch',
+						'name' => 'delugeHideCompleted',
+						'label' => 'Hide Completed',
+						'value' => $GLOBALS['delugeHideCompleted']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageDownloadRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $GLOBALS['homepageDownloadRefresh'],
+						'options' => optionTime()
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'delugeCombine',
+						'label' => 'Add to Combined Downloader',
+						'value' => $GLOBALS['delugeCombine']
+					),
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'deluge\')"'
+					),
+				)
+			)
+		),
+		array(
+			'name' => 'Sonarr',
+			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
+			'image' => 'plugins/images/tabs/sonarr.png',
+			'category' => 'PVR',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageSonarrEnabled',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepageSonarrEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageSonarrAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $GLOBALS['homepageSonarrAuth'],
+						'options' => $groups
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'sonarrURL',
+						'label' => 'URL',
+						'value' => $GLOBALS['sonarrURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'sonarrToken',
+						'label' => 'Token',
+						'value' => $GLOBALS['sonarrToken']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'number',
+						'name' => 'calendarStart',
+						'label' => '# of Days Before',
+						'value' => $GLOBALS['calendarStart'],
+						'placeholder' => ''
+					),
+					array(
+						'type' => 'number',
+						'name' => 'calendarEnd',
+						'label' => '# of Days After',
+						'value' => $GLOBALS['calendarEnd'],
+						'placeholder' => ''
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarFirstDay',
+						'label' => 'Start Day',
+						'value' => $GLOBALS['calendarFirstDay'],
+						'options' => $day
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarDefault',
+						'label' => 'Default View',
+						'value' => $GLOBALS['calendarDefault'],
+						'options' => $calendarDefault
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarTimeFormat',
+						'label' => 'Time Format',
+						'value' => $GLOBALS['calendarTimeFormat'],
+						'options' => $timeFormat
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarLimit',
+						'label' => 'Items Per Day',
+						'value' => $GLOBALS['calendarLimit'],
+						'options' => $limit
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $GLOBALS['calendarRefresh'],
+						'options' => optionTime()
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'sonarrUnmonitored',
+						'label' => 'Show Unmonitored',
+						'value' => $GLOBALS['sonarrUnmonitored']
+					)
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'sonarr\')"'
+					),
+				)
+			)
+		),
+		array(
+			'name' => 'Lidarr',
+			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
+			'image' => 'plugins/images/tabs/lidarr.png',
+			'category' => 'PMR',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageLidarrEnabled',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepageLidarrEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageLidarrAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $GLOBALS['homepageLidarrAuth'],
+						'options' => $groups
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'lidarrURL',
+						'label' => 'URL',
+						'value' => $GLOBALS['lidarrURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'lidarrToken',
+						'label' => 'Token',
+						'value' => $GLOBALS['lidarrToken']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'number',
+						'name' => 'calendarStart',
+						'label' => '# of Days Before',
+						'value' => $GLOBALS['calendarStart'],
+						'placeholder' => ''
+					),
+					array(
+						'type' => 'number',
+						'name' => 'calendarEnd',
+						'label' => '# of Days After',
+						'value' => $GLOBALS['calendarEnd'],
+						'placeholder' => ''
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarFirstDay',
+						'label' => 'Start Day',
+						'value' => $GLOBALS['calendarFirstDay'],
+						'options' => $day
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarDefault',
+						'label' => 'Default View',
+						'value' => $GLOBALS['calendarDefault'],
+						'options' => $calendarDefault
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarTimeFormat',
+						'label' => 'Time Format',
+						'value' => $GLOBALS['calendarTimeFormat'],
+						'options' => $timeFormat
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarLimit',
+						'label' => 'Items Per Day',
+						'value' => $GLOBALS['calendarLimit'],
+						'options' => $limit
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $GLOBALS['calendarRefresh'],
+						'options' => optionTime()
+					),
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'lidarr\')"'
+					),
+				)
+			)
+		),
+		array(
+			'name' => 'Radarr',
+			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
+			'image' => 'plugins/images/tabs/radarr.png',
+			'category' => 'PVR',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageRadarrEnabled',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepageRadarrEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageRadarrAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $GLOBALS['homepageRadarrAuth'],
+						'options' => $groups
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'radarrURL',
+						'label' => 'URL',
+						'value' => $GLOBALS['radarrURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'radarrToken',
+						'label' => 'Token',
+						'value' => $GLOBALS['radarrToken']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'number',
+						'name' => 'calendarStart',
+						'label' => '# of Days Before',
+						'value' => $GLOBALS['calendarStart'],
+						'placeholder' => ''
+					),
+					array(
+						'type' => 'number',
+						'name' => 'calendarEnd',
+						'label' => '# of Days After',
+						'value' => $GLOBALS['calendarEnd'],
+						'placeholder' => ''
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarFirstDay',
+						'label' => 'Start Day',
+						'value' => $GLOBALS['calendarFirstDay'],
+						'options' => $day
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarDefault',
+						'label' => 'Default View',
+						'value' => $GLOBALS['calendarDefault'],
+						'options' => $calendarDefault
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarTimeFormat',
+						'label' => 'Time Format',
+						'value' => $GLOBALS['calendarTimeFormat'],
+						'options' => $timeFormat
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarLimit',
+						'label' => 'Items Per Day',
+						'value' => $GLOBALS['calendarLimit'],
+						'options' => $limit
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $GLOBALS['calendarRefresh'],
+						'options' => optionTime()
+					)
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'radarr\')"'
+					),
+				)
+			)
+		),
+		array(
+			'name' => 'CouchPotato',
+			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
+			'image' => 'plugins/images/tabs/couchpotato.png',
+			'category' => 'PVR',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageCouchpotatoEnabled',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepageCouchpotatoEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageCouchpotatoAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $GLOBALS['homepageCouchpotatoAuth'],
+						'options' => $groups
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'couchpotatoURL',
+						'label' => 'URL',
+						'value' => $GLOBALS['couchpotatoURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'couchpotatoToken',
+						'label' => 'Token',
+						'value' => $GLOBALS['couchpotatoToken']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'select',
+						'name' => 'calendarFirstDay',
+						'label' => 'Start Day',
+						'value' => $GLOBALS['calendarFirstDay'],
+						'options' => $day
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarDefault',
+						'label' => 'Default View',
+						'value' => $GLOBALS['calendarDefault'],
+						'options' => $calendarDefault
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarTimeFormat',
+						'label' => 'Time Format',
+						'value' => $GLOBALS['calendarTimeFormat'],
+						'options' => $timeFormat
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarLimit',
+						'label' => 'Items Per Day',
+						'value' => $GLOBALS['calendarLimit'],
+						'options' => $limit
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $GLOBALS['calendarRefresh'],
+						'options' => optionTime()
+					)
+				)
+			)
+		),
+		array(
+			'name' => 'SickRage',
+			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
+			'image' => 'plugins/images/tabs/sickrage.png',
+			'category' => 'PVR',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageSickrageEnabled',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepageSickrageEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageSickrageAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $GLOBALS['homepageSickrageAuth'],
+						'options' => $groups
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'sickrageURL',
+						'label' => 'URL',
+						'value' => $GLOBALS['sickrageURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'sickrageToken',
+						'label' => 'Token',
+						'value' => $GLOBALS['sickrageToken']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'select',
+						'name' => 'calendarFirstDay',
+						'label' => 'Start Day',
+						'value' => $GLOBALS['calendarFirstDay'],
+						'options' => $day
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarDefault',
+						'label' => 'Default View',
+						'value' => $GLOBALS['calendarDefault'],
+						'options' => $calendarDefault
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarTimeFormat',
+						'label' => 'Time Format',
+						'value' => $GLOBALS['calendarTimeFormat'],
+						'options' => $timeFormat
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarLimit',
+						'label' => 'Items Per Day',
+						'value' => $GLOBALS['calendarLimit'],
+						'options' => $limit
+					),
+					array(
+						'type' => 'select',
+						'name' => 'calendarRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $GLOBALS['calendarRefresh'],
+						'options' => optionTime()
+					)
+				)
+			)
+		),
+		array(
+			'name' => 'Ombi',
+			'enabled' => (strpos('personal', $GLOBALS['license']) !== false) ? true : false,
+			'image' => 'plugins/images/tabs/ombi.png',
+			'category' => 'Requests',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepageOmbiEnabled',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepageOmbiEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepageOmbiAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $GLOBALS['homepageOmbiAuth'],
+						'options' => $groups
+					)
+				),
+				'Connection' => array(
+					array(
+						'type' => 'input',
+						'name' => 'ombiURL',
+						'label' => 'URL',
+						'value' => $GLOBALS['ombiURL'],
+						'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+						'placeholder' => 'http(s)://hostname:port'
+					),
+					array(
+						'type' => 'password-alt',
+						'name' => 'ombiToken',
+						'label' => 'Token',
+						'value' => $GLOBALS['ombiToken']
+					)
+				),
+				'Misc Options' => array(
+					array(
+						'type' => 'select',
+						'name' => 'homepageOmbiRequestAuth',
+						'label' => 'Minimum Group to Request',
+						'value' => $GLOBALS['homepageOmbiRequestAuth'],
+						'options' => $groups
+					),
+					array(
+						'type' => 'select',
+						'name' => 'ombiTvDefault',
+						'label' => 'TV Show Default Request',
+						'value' => $GLOBALS['ombiTvDefault'],
+						'options' => $ombiTvOptions
+					),
+					array(
+						'type' => 'switch',
+						'name' => 'ombiLimitUser',
+						'label' => 'Limit to User',
+						'value' => $GLOBALS['ombiLimitUser']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'ombiRefresh',
+						'label' => 'Refresh Seconds',
+						'value' => $GLOBALS['ombiRefresh'],
+						'options' => optionTime()
+					)
+				),
+				'Test Connection' => array(
+					array(
+						'type' => 'blank',
+						'label' => 'Please Save before Testing'
+					),
+					array(
+						'type' => 'button',
+						'label' => '',
+						'icon' => 'fa fa-flask',
+						'class' => 'pull-right',
+						'text' => 'Test Connection',
+						'attr' => 'onclick="testAPIConnection(\'ombi\')"'
+					),
+				)
+			)
+		),
+		array(
+			'name' => 'CustomHTML-1',
+			'enabled' => (strpos('personal,business', $GLOBALS['license']) !== false) ? true : false,
+			'image' => 'plugins/images/tabs/custom1.png',
+			'category' => 'Custom',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepagCustomHTMLoneEnabled',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepagCustomHTMLoneEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepagCustomHTMLoneAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $GLOBALS['homepagCustomHTMLoneAuth'],
+						'options' => $groups
+					)
+				),
+				'Code' => array(
+					array(
+						'type' => 'textbox',
+						'name' => 'customHTMLone',
+						'class' => 'hidden customHTMLoneTextarea',
+						'label' => '',
+						'value' => $GLOBALS['customHTMLone'],
+					),
+					array(
+						'type' => 'html',
+						'override' => 12,
+						'label' => 'Custom HTML/JavaScript',
+						'html' => '<button type="button" class="hidden savecustomHTMLoneTextarea btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customHTMLoneEditor" style="height:300px">' . htmlentities($GLOBALS['customHTMLone']) . '</div>'
+					),
+				)
+			)
+		),
+		array(
+			'name' => 'CustomHTML-2',
+			'enabled' => (strpos('personal,business', $GLOBALS['license']) !== false) ? true : false,
+			'image' => 'plugins/images/tabs/custom2.png',
+			'category' => 'Custom',
+			'settings' => array(
+				'Enable' => array(
+					array(
+						'type' => 'switch',
+						'name' => 'homepagCustomHTMLtwoEnabled',
+						'label' => 'Enable',
+						'value' => $GLOBALS['homepagCustomHTMLtwoEnabled']
+					),
+					array(
+						'type' => 'select',
+						'name' => 'homepagCustomHTMLtwoAuth',
+						'label' => 'Minimum Authentication',
+						'value' => $GLOBALS['homepagCustomHTMLtwoAuth'],
+						'options' => $groups
+					)
+				),
+				'Code' => array(
+					array(
+						'type' => 'textbox',
+						'name' => 'customHTMLtwo',
+						'class' => 'hidden customHTMLtwoTextarea',
+						'label' => '',
+						'value' => $GLOBALS['customHTMLtwo'],
+					),
+					array(
+						'type' => 'html',
+						'override' => 12,
+						'label' => 'Custom HTML/JavaScript',
+						'html' => '<button type="button" class="hidden savecustomHTMLtwoTextarea btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customHTMLtwoEditor" style="height:300px">' . htmlentities($GLOBALS['customHTMLtwo']) . '</div>'
+					),
+				)
+			)
+		)
+	);
+}
+
+function buildHomepageSettings()
+{
+	$homepageOrder = homepageOrder();
+	$homepageList = '<h4>Drag Homepage Items to Order Them</h4><div id="homepage-items-sort" class="external-events">';
+	$inputList = '<form id="homepage-values" class="row">';
+	foreach ($homepageOrder as $key => $val) {
+		switch ($key) {
+			case 'homepageOrdercustomhtml':
+				$class = 'bg-info';
+				$image = 'plugins/images/tabs/custom1.png';
+				if (!$GLOBALS['homepagCustomHTMLoneEnabled']) {
+					$class .= ' faded';
+				}
+				break;
+			case 'homepageOrdercustomhtmlTwo':
+				$class = 'bg-info';
+				$image = 'plugins/images/tabs/custom2.png';
+				if (!$GLOBALS['homepagCustomHTMLtwoEnabled']) {
+					$class .= ' faded';
+				}
+				break;
+			case 'homepageOrdertransmission':
+				$class = 'bg-transmission';
+				$image = 'plugins/images/tabs/transmission.png';
+				if (!$GLOBALS['homepageTransmissionEnabled']) {
+					$class .= ' faded';
+				}
+				break;
+			case 'homepageOrdernzbget':
+				$class = 'bg-nzbget';
+				$image = 'plugins/images/tabs/nzbget.png';
+				if (!$GLOBALS['homepageNzbgetEnabled']) {
+					$class .= ' faded';
+				}
+				break;
+			case 'homepageOrdersabnzbd':
+				$class = 'bg-sab';
+				$image = 'plugins/images/tabs/sabnzbd.png';
+				if (!$GLOBALS['homepageSabnzbdEnabled']) {
+					$class .= ' faded';
+				}
+				break;
+			case 'homepageOrderdeluge':
+				$class = 'bg-deluge';
+				$image = 'plugins/images/tabs/deluge.png';
+				if (!$GLOBALS['homepageDelugeEnabled']) {
+					$class .= ' faded';
+				}
+				break;
+			case 'homepageOrderqBittorrent':
+				$class = 'bg-qbit';
+				$image = 'plugins/images/tabs/qBittorrent.png';
+				if (!$GLOBALS['homepageqBittorrentEnabled']) {
+					$class .= ' faded';
+				}
+				break;
+			case 'homepageOrderrTorrent':
+				$class = 'bg-qbit';
+				$image = 'plugins/images/tabs/rTorrent.png';
+				if (!$GLOBALS['homepagerTorrentEnabled']) {
+					$class .= ' faded';
+				}
+				break;
+			case 'homepageOrderplexnowplaying':
+			case 'homepageOrderplexrecent':
+			case 'homepageOrderplexplaylist':
+				$class = 'bg-plex';
+				$image = 'plugins/images/tabs/plex.png';
+				if (!$GLOBALS['homepagePlexEnabled']) {
+					$class .= ' faded';
+				}
+				break;
+			case 'homepageOrderembynowplaying':
+			case 'homepageOrderembyrecent':
+				$class = 'bg-emby';
+				$image = 'plugins/images/tabs/emby.png';
+				if (!$GLOBALS['homepageEmbyEnabled']) {
+					$class .= ' faded';
+				}
+				break;
+			case 'homepageOrderombi':
+				$class = 'bg-inverse';
+				$image = 'plugins/images/tabs/ombi.png';
+				if (!$GLOBALS['homepageOmbiEnabled']) {
+					$class .= ' faded';
+				}
+				break;
+			case 'homepageOrdercalendar':
+				$class = 'bg-primary';
+				$image = 'plugins/images/tabs/calendar.png';
+				if (!$GLOBALS['homepageSonarrEnabled'] && !$GLOBALS['homepageRadarrEnabled'] && !$GLOBALS['homepageSickrageEnabled'] && !$GLOBALS['homepageCouchpotatoEnabled']) {
+					$class .= ' faded';
+				}
+				break;
+			case 'homepageOrderdownloader':
+				$class = 'bg-inverse';
+				$image = 'plugins/images/tabs/downloader.png';
+				if (!$GLOBALS['sabnzbdCombine'] && !$GLOBALS['nzbgetCombine'] && !$GLOBALS['rTorrentCombine'] && !$GLOBALS['delugeCombine'] && !$GLOBALS['transmissionCombine'] && !$GLOBALS['qBittorrentCombine']) {
+					$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">
+			<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;
+}
+
+function ombiTVDefault($type)
+{
+	switch ($type) {
+		case 'all':
+			return ($type == $GLOBALS['ombiTvDefault']) ? true : false;
+		case 'first':
+			return ($type == $GLOBALS['ombiTvDefault']) ? true : false;
+		case 'last':
+			return ($type == $GLOBALS['ombiTvDefault']) ? true : false;
+		default:
+			return false;
+	}
+	return false;
+}

+ 59 - 0
api/functions/log-functions.php

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

+ 623 - 0
api/functions/normal-functions.php

@@ -0,0 +1,623 @@
+<?php
+// Print output all purrty
+function prettyPrint($v)
+{
+	$trace = debug_backtrace()[0];
+	echo '<pre style="white-space: pre; text-overflow: ellipsis; overflow: hidden; background-color: #f2f2f2; border: 2px solid black; border-radius: 5px; padding: 5px; margin: 5px;">' . $trace['file'] . ':' . $trace['line'] . ' ' . gettype($v) . "\n\n" . print_r($v, 1) . '</pre><br/>';
+}
+
+// Clean Directory string
+function cleanDirectory($path)
+{
+	$path = str_replace(array('/', '\\'), '/', $path);
+	if (substr($path, -1) != '/') {
+		$path = $path . '/';
+	}
+	if ($path[0] != '/' && $path[1] != ':') {
+		$path = '/' . $path;
+	}
+	return $path;
+}
+
+// Get Gravatar Email Image
+function gravatar($email = '')
+{
+	$email = md5(strtolower(trim($email)));
+	$gravurl = "https://www.gravatar.com/avatar/$email?s=100&d=mm";
+	return $gravurl;
+}
+
+function parseDomain($value, $force = false)
+{
+	$badDomains = array('ddns.net', 'ddnsking.com', '3utilities.com', 'bounceme.net', 'duckdns.org', 'freedynamicdns.net', 'freedynamicdns.org', 'gotdns.ch', 'hopto.org', 'myddns.me', 'myds.me', 'myftp.biz', 'myftp.org', 'myvnc.com', 'noip.com', 'onthewifi.com', 'redirectme.net', 'serveblog.net', 'servecounterstrike.com', 'serveftp.com', 'servegame.com', 'servehalflife.com', 'servehttp.com', 'serveirc.com', 'serveminecraft.net', 'servemp3.com', 'servepics.com', 'servequake.com', 'sytes.net', 'viewdns.net', 'webhop.me', 'zapto.org');
+	$Domain = $value;
+	$Port = strpos($Domain, ':');
+	if ($Port !== false) {
+		$Domain = substr($Domain, 0, $Port);
+		$value = $Domain;
+	}
+	$check = substr_count($Domain, '.');
+	if ($check >= 3) {
+		if (is_numeric($Domain[0])) {
+			$Domain = '';
+		} else {
+			if (in_array(strtolower(explode('.', $Domain)[2] . '.' . explode('.', $Domain)[3]), $badDomains)) {
+				$Domain = '.' . explode('.', $Domain)[0] . '.' . explode('.', $Domain)[1] . '.' . explode('.', $Domain)[2] . '.' . explode('.', $Domain)[3];
+			} else {
+				$Domain = '.' . explode('.', $Domain)[1] . '.' . explode('.', $Domain)[2] . '.' . explode('.', $Domain)[3];
+			}
+		}
+	} elseif ($check == 2) {
+		if (in_array(strtolower(explode('.', $Domain)[1] . '.' . explode('.', $Domain)[2]), $badDomains)) {
+			$Domain = '.' . explode('.', $Domain)[0] . '.' . explode('.', $Domain)[1] . '.' . explode('.', $Domain)[2];
+		} elseif (explode('.', $Domain)[0] == 'www') {
+			$Domain = '.' . explode('.', $Domain)[1] . '.' . explode('.', $Domain)[2];
+		} elseif (explode('.', $Domain)[1] == 'co') {
+			$Domain = '.' . explode('.', $Domain)[0] . '.' . explode('.', $Domain)[1] . '.' . explode('.', $Domain)[2];
+		} else {
+			$Domain = '.' . explode('.', $Domain)[1] . '.' . explode('.', $Domain)[2];
+		}
+	} elseif ($check == 1) {
+		$Domain = '.' . $Domain;
+	} else {
+		$Domain = '';
+	}
+	/*
+	if (is_numeric($Domain[0]) || strpos($Domain, '.') == false) {
+		$Domain = '';
+	} else {
+		if (substr($Domain, 0, 3) == 'www') {
+			$Domain = substr($Domain, 3, strlen($Domain) - 3);
+		} else {
+			$Domain = '.' . $Domain;
+		}
+	}
+	*/
+	return ($force) ? $value : $Domain;
+}
+
+// Cookie Custom Function
+function coookie($type, $name, $value = '', $days = -1, $http = true)
+{
+	if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == "https") {
+		$Secure = true;
+		$HTTPOnly = true;
+	} elseif (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') {
+		$Secure = true;
+		$HTTPOnly = true;
+	} else {
+		$Secure = false;
+		$HTTPOnly = false;
+	}
+	if (!$http) {
+		$HTTPOnly = false;
+	}
+	$Path = '/';
+	$Domain = parseDomain($_SERVER['HTTP_HOST']);
+	$DomainTest = parseDomain($_SERVER['HTTP_HOST'], true);
+	if ($type == 'set') {
+		$_COOKIE[$name] = $value;
+		header('Set-Cookie: ' . rawurlencode($name) . '=' . rawurlencode($value)
+			. (empty($days) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', time() + (86400 * $days)) . ' GMT')
+			. (empty($Path) ? '' : '; path=' . $Path)
+			. (empty($Domain) ? '' : '; domain=' . $Domain)
+			. (!$Secure ? '' : '; secure')
+			. (!$HTTPOnly ? '' : '; HttpOnly'), false);
+		header('Set-Cookie: ' . rawurlencode($name) . '=' . rawurlencode($value)
+			. (empty($days) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', time() + (86400 * $days)) . ' GMT')
+			. (empty($Path) ? '' : '; path=' . $Path)
+			. (empty($Domain) ? '' : '; domain=' . $DomainTest)
+			. (!$Secure ? '' : '; secure')
+			. (!$HTTPOnly ? '' : '; HttpOnly'), false);
+	} elseif ($type == 'delete') {
+		unset($_COOKIE[$name]);
+		header('Set-Cookie: ' . rawurlencode($name) . '=' . rawurlencode($value)
+			. (empty($days) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', time() - 3600) . ' GMT')
+			. (empty($Path) ? '' : '; path=' . $Path)
+			. (empty($Domain) ? '' : '; domain=' . $Domain)
+			. (!$Secure ? '' : '; secure')
+			. (!$HTTPOnly ? '' : '; HttpOnly'), false);
+		header('Set-Cookie: ' . rawurlencode($name) . '=' . rawurlencode($value)
+			. (empty($days) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', time() - 3600) . ' GMT')
+			. (empty($Path) ? '' : '; path=' . $Path)
+			. (empty($Domain) ? '' : '; domain=' . $DomainTest)
+			. (!$Secure ? '' : '; secure')
+			. (!$HTTPOnly ? '' : '; HttpOnly'), false);
+	}
+}
+
+function getOS()
+{
+	if (PHP_SHLIB_SUFFIX == "dll") {
+		return "win";
+	} else {
+		return "*nix";
+	}
+}
+
+if (!function_exists('getallheaders')) {
+	function getallheaders()
+	{
+		$headers = array();
+		foreach ($_SERVER as $name => $value) {
+			if (substr($name, 0, 5) == 'HTTP_') {
+				$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
+			}
+		}
+		return $headers;
+	}
+}
+function random_ascii_string($length)
+{
+	$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+	$charactersLength = strlen($characters);
+	$randomString = '';
+	for ($i = 0; $i < $length; $i++) {
+		$randomString .= $characters[rand(0, $charactersLength - 1)];
+	}
+	return $randomString;
+}
+
+// Generate Random string
+function randString($length = 10, $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ')
+{
+	$tmp = '';
+	for ($i = 0; $i < $length; $i++) {
+		$tmp .= substr(str_shuffle($chars), 0, 1);
+	}
+	return $tmp;
+}
+
+function isEncrypted($password)
+{
+	switch (strlen($password)) {
+		case '24':
+			return (strpos($password, '==') !== false) ? true : false;
+			break;
+		case '44':
+			return (substr($password, -1, 1) == '=') ? true : false;
+			break;
+		case '64':
+			return true;
+		case '88':
+			return (strpos($password, '==') !== false) ? true : false;
+			break;
+		case '108':
+			return (substr($password, -1, 1) == '=') ? true : false;
+			break;
+		default:
+			return false;
+	}
+}
+
+function encrypt($password, $key = null)
+{
+	$key = (isset($GLOBALS['organizrHash'])) ? $GLOBALS['organizrHash'] : $key;
+	return openssl_encrypt($password, 'AES-256-CBC', $key, 0, fillString($key, 16));
+}
+
+function decrypt($password, $key = null)
+{
+	if (empty($password)) {
+		return '';
+	}
+	$key = (isset($GLOBALS['organizrHash'])) ? $GLOBALS['organizrHash'] : $key;
+	return openssl_decrypt($password, 'AES-256-CBC', $key, 0, fillString($key, 16));
+}
+
+function fillString($string, $length)
+{
+	$filler = '0123456789abcdefghijklmnopqrstuvwxyz!@#$%^&*';
+	if (strlen($string) < $length) {
+		$diff = $length - strlen($string);
+		$filler = substr($filler, 0, $diff);
+		return $string . $filler;
+	} elseif (strlen($string) > $length) {
+		return substr($string, 0, $length);
+	} else {
+		return $string;
+	}
+}
+
+function userIP()
+{
+	if (isset($_SERVER['HTTP_CLIENT_IP'])) {
+		$ipaddress = $_SERVER['HTTP_CLIENT_IP'];
+	} elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+		$ipaddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
+	} elseif (isset($_SERVER['HTTP_X_FORWARDED'])) {
+		$ipaddress = $_SERVER['HTTP_X_FORWARDED'];
+	} elseif (isset($_SERVER['HTTP_FORWARDED_FOR'])) {
+		$ipaddress = $_SERVER['HTTP_FORWARDED_FOR'];
+	} elseif (isset($_SERVER['HTTP_FORWARDED'])) {
+		$ipaddress = $_SERVER['HTTP_FORWARDED'];
+	} elseif (isset($_SERVER['REMOTE_ADDR'])) {
+		$ipaddress = $_SERVER['REMOTE_ADDR'];
+	} else {
+		$ipaddress = 'UNKNOWN';
+	}
+	if (strpos($ipaddress, ',') !== false) {
+		list($first, $last) = explode(",", $ipaddress);
+		unset($last);
+		return $first;
+	} else {
+		return $ipaddress;
+	}
+}
+
+function arrayIP($string)
+{
+	if (strpos($string, ',') !== false) {
+		$result = explode(",", $string);
+	} else {
+		$result = array($string);
+	}
+	foreach ($result as &$ip) {
+		$ip = is_numeric(substr($ip, 0, 1)) ? $ip : gethostbyname($ip);
+	}
+	return $result;
+}
+
+function getCert()
+{
+	$url = 'http://curl.haxx.se/ca/cacert.pem';
+	$file = __DIR__ . DIRECTORY_SEPARATOR . 'cert' . DIRECTORY_SEPARATOR . 'cacert.pem';
+	if (!file_exists($file)) {
+		file_put_contents($file, fopen($url, 'r'));
+	} elseif (file_exists($file) && time() - 2592000 > filemtime($file)) {
+		file_put_contents($file, fopen($url, 'r'));
+	}
+	return $file;
+}
+
+function curl($curl, $url, $headers = array(), $data = array())
+{
+	// Initiate cURL
+	$curlReq = curl_init($url);
+	if (in_array(trim(strtoupper($curl)), ["GET", "POST", "PUT", "DELETE"])) {
+		curl_setopt($curlReq, CURLOPT_CUSTOMREQUEST, trim(strtoupper($curl)));
+	} else {
+		return null;
+	}
+	curl_setopt($curlReq, CURLOPT_RETURNTRANSFER, true);
+	curl_setopt($curlReq, CURLOPT_CAINFO, getCert());
+	curl_setopt($curlReq, CURLOPT_CONNECTTIMEOUT, 5);
+	if (localURL($url)) {
+		curl_setopt($curlReq, CURLOPT_SSL_VERIFYHOST, 0);
+		curl_setopt($curlReq, CURLOPT_SSL_VERIFYPEER, 0);
+	}
+	// Format Headers
+	$cHeaders = array();
+	foreach ($headers as $k => $v) {
+		$cHeaders[] = $k . ': ' . $v;
+	}
+	if (count($cHeaders)) {
+		curl_setopt($curlReq, CURLOPT_HTTPHEADER, $cHeaders);
+	}
+	// Format Data
+	switch (isset($headers['Content-Type']) ? $headers['Content-Type'] : '') {
+		case 'application/json':
+			curl_setopt($curlReq, CURLOPT_POSTFIELDS, json_encode($data));
+			break;
+		case 'application/x-www-form-urlencoded':
+			curl_setopt($curlReq, CURLOPT_POSTFIELDS, http_build_query($data));
+			break;
+		default:
+			$headers['Content-Type'] = 'application/x-www-form-urlencoded';
+			curl_setopt($curlReq, CURLOPT_POSTFIELDS, http_build_query($data));
+	}
+	// Execute
+	$result = curl_exec($curlReq);
+	$httpcode = curl_getinfo($curlReq);
+	// Close
+	curl_close($curlReq);
+	// Return
+	return array('content' => $result, 'http_code' => $httpcode);
+}
+
+function getHeaders($url)
+{
+	$ch = curl_init($url);
+	curl_setopt($ch, CURLOPT_NOBODY, true);
+	curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
+	curl_setopt($ch, CURLOPT_HEADER, false);
+	curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+	curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
+	curl_setopt($ch, CURLOPT_CAINFO, getCert());
+	if (localURL($url)) {
+		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
+		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
+	}
+	curl_exec($ch);
+	$headers = curl_getinfo($ch);
+	curl_close($ch);
+	return $headers;
+}
+
+function download($url, $path)
+{
+	ini_set('max_execution_time', 0);
+	set_time_limit(0);
+	$ch = curl_init($url);
+	curl_setopt($ch, CURLOPT_HEADER, 1);
+	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+	curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
+	curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
+	curl_setopt($ch, CURLOPT_CAINFO, getCert());
+	if (localURL($url)) {
+		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
+		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
+	}
+	$raw_file_data = curl_exec($ch);
+	curl_close($ch);
+	file_put_contents($path, $raw_file_data);
+	return (filesize($path) > 0) ? true : false;
+}
+
+function localURL($url)
+{
+	if (strpos($url, 'https') !== false) {
+		preg_match("/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/", $url, $result);
+		$result = (!empty($result) ? true : false);
+		return $result;
+	}
+	return false;
+}
+
+function array_filter_key(array $array, $callback)
+{
+	$matchedKeys = array_filter(array_keys($array), $callback);
+	return array_intersect_key($array, array_flip($matchedKeys));
+}
+
+function searchArray($array, $field, $value)
+{
+	foreach ($array as $key => $item) {
+		if ($item[$field] === $value)
+			return $key;
+	}
+	return false;
+}
+
+// Qualify URL
+function qualifyURL($url, $return = false)
+{
+	//local address?
+	if (substr($url, 0, 1) == "/") {
+		if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') {
+			$protocol = "https://";
+		} else {
+			$protocol = "http://";
+		}
+		$url = $protocol . getServer() . $url;
+	}
+	// Get Digest
+	$digest = parse_url($url);
+	// http/https
+	if (!isset($digest['scheme'])) {
+		if (isset($digest['port']) && in_array($digest['port'], array(80, 8080, 8096, 32400, 7878, 8989, 8182, 8081, 6789))) {
+			$scheme = 'http';
+		} else {
+			$scheme = 'https';
+		}
+	} else {
+		$scheme = $digest['scheme'];
+	}
+	// Host
+	$host = (isset($digest['host']) ? $digest['host'] : '');
+	// Port
+	$port = (isset($digest['port']) ? ':' . $digest['port'] : '');
+	// Path
+	$path = (isset($digest['path']) && $digest['path'] !== '/' ? $digest['path'] : '');
+	// Output
+	$array = array(
+		'scheme' => $scheme,
+		'host' => $host,
+		'port' => $port,
+		'path' => $path
+	);
+	return ($return) ? $array : $scheme . '://' . $host . $port . $path;
+}
+
+function getServerPath($over = false)
+{
+	if ($over) {
+		if ($GLOBALS['PHPMAILER-domain'] !== '') {
+			return $GLOBALS['PHPMAILER-domain'];
+		}
+	}
+	if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == "https") {
+		$protocol = "https://";
+	} elseif (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') {
+		$protocol = "https://";
+	} else {
+		$protocol = "http://";
+	}
+	$domain = '';
+	if (isset($_SERVER['SERVER_NAME']) && strpos($_SERVER['SERVER_NAME'], '.') !== false) {
+		$domain = $_SERVER['SERVER_NAME'];
+	} elseif (isset($_SERVER['HTTP_HOST'])) {
+		if (strpos($_SERVER['HTTP_HOST'], ':') !== false) {
+			$domain = explode(':', $_SERVER['HTTP_HOST'])[0];
+			$port = explode(':', $_SERVER['HTTP_HOST'])[1];
+			if ($port !== "80" && $port !== "443") {
+				$domain = $_SERVER['HTTP_HOST'];
+			}
+		} else {
+			$domain = $_SERVER['HTTP_HOST'];
+		}
+	}
+	$url = $protocol . $domain . str_replace("\\", "/", dirname($_SERVER['REQUEST_URI']));
+	if (strpos($url, '/api') !== false) {
+		$url = explode('/api', $url);
+		return $url[0] . '/';
+	} else {
+		return $url;
+	}
+}
+
+function get_browser_name()
+{
+	$user_agent = $_SERVER['HTTP_USER_AGENT'];
+	if (strpos($user_agent, 'Opera') || strpos($user_agent, 'OPR/')) {
+		return 'Opera';
+	} elseif (strpos($user_agent, 'Edge')) {
+		return 'Edge';
+	} elseif (strpos($user_agent, 'Chrome')) {
+		return 'Chrome';
+	} elseif (strpos($user_agent, 'Safari')) {
+		return 'Safari';
+	} elseif (strpos($user_agent, 'Firefox')) {
+		return 'Firefox';
+	} elseif (strpos($user_agent, 'MSIE') || strpos($user_agent, 'Trident/7')) {
+		return 'Internet Explorer';
+	}
+	return 'Other';
+}
+
+function getServer($over = false)
+{
+	if ($over) {
+		if ($GLOBALS['PHPMAILER-domain'] !== '') {
+			return $GLOBALS['PHPMAILER-domain'];
+		}
+	}
+	return isset($_SERVER["HTTP_HOST"]) ? $_SERVER["HTTP_HOST"] : $_SERVER["SERVER_NAME"];
+}
+
+/* Function is to get all the contents from ics and explode all the datas according to the events and its sections */
+function getIcsEventsAsArray($file)
+{
+	$icalString = file_get_contents_curl($file);
+	$icsDates = array();
+	/* Explode the ICs Data to get datas as array according to string ‘BEGIN:’ */
+	$icsData = explode("BEGIN:", $icalString);
+	/* Iterating the icsData value to make all the start end dates as sub array */
+	foreach ($icsData as $key => $value) {
+		$icsDatesMeta [$key] = explode("\n", $value);
+	}
+	/* Itearting the Ics Meta Value */
+	foreach ($icsDatesMeta as $key => $value) {
+		foreach ($value as $subKey => $subValue) {
+			/* to get ics events in proper order */
+			$icsDates = getICSDates($key, $subKey, $subValue, $icsDates);
+		}
+	}
+	return $icsDates;
+}
+
+/* funcion is to avaid the elements wich is not having the proper start, end  and summary informations */
+function getICSDates($key, $subKey, $subValue, $icsDates)
+{
+	if ($key != 0 && $subKey == 0) {
+		$icsDates [$key] ["BEGIN"] = $subValue;
+	} else {
+		$subValueArr = explode(":", $subValue, 2);
+		if (isset ($subValueArr [1])) {
+			$icsDates [$key] [$subValueArr [0]] = $subValueArr [1];
+		}
+	}
+	return $icsDates;
+}
+
+function file_get_contents_curl($url)
+{
+	$ch = curl_init();
+	curl_setopt($ch, CURLOPT_AUTOREFERER, true);
+	curl_setopt($ch, CURLOPT_HEADER, 0);
+	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+	curl_setopt($ch, CURLOPT_URL, $url);
+	curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+	curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
+	curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
+	$data = curl_exec($ch);
+	curl_close($ch);
+	return $data;
+}
+
+function getExtension($string)
+{
+	return preg_replace("#(.+)?\.(\w+)(\?.+)?#", "$2", $string);
+}
+
+function safe_json_encode($value, $options = 0, $depth = 512)
+{
+	$encoded = json_encode($value, $options, $depth);
+	if ($encoded === false && $value && json_last_error() == JSON_ERROR_UTF8) {
+		$encoded = json_encode(utf8ize($value), $options, $depth);
+	}
+	return $encoded;
+}
+
+function utf8ize($mixed)
+{
+	if (is_array($mixed)) {
+		foreach ($mixed as $key => $value) {
+			$mixed[$key] = utf8ize($value);
+		}
+	} elseif (is_string($mixed)) {
+		return mb_convert_encoding($mixed, "UTF-8", "UTF-8");
+	}
+	return $mixed;
+}
+
+function gen_uuid()
+{
+	return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
+		// 32 bits for "time_low"
+		mt_rand(0, 0xffff), mt_rand(0, 0xffff),
+		// 16 bits for "time_mid"
+		mt_rand(0, 0xffff),
+		// 16 bits for "time_hi_and_version",
+		// four most significant bits holds version number 4
+		mt_rand(0, 0x0fff) | 0x4000,
+		// 16 bits, 8 bits for "clk_seq_hi_res",
+		// 8 bits for "clk_seq_low",
+		// two most significant bits holds zero and one for variant DCE1.1
+		mt_rand(0, 0x3fff) | 0x8000,
+		// 48 bits for "node"
+		mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
+	);
+}
+
+function dbExtension($string)
+{
+	return (substr($string, -3) == '.db') ? $string : $string . '.db';
+}
+
+function localIPRanges()
+{
+	return array(
+		array(
+			'from' => '10.0.0.0',
+			'to' => '10.255.255.255'
+		),
+		array(
+			'from' => '172.16.0.0',
+			'to' => '172.31.255.255'
+		),
+		array(
+			'from' => '192.168.0.0',
+			'to' => '192.168.255.255'
+		),
+		array(
+			'from' => '127.0.0.1',
+			'to' => '127.255.255.255'
+		),
+	);
+}
+
+function isLocal($checkIP = null)
+{
+	$isLocal = false;
+	$userIP = ($checkIP) ? ip2long($checkIP) : ip2long(userIP());
+	$range = localIPRanges();
+	foreach ($range as $ip) {
+		$low = ip2long($ip['from']);
+		$high = ip2long($ip['to']);
+		if ($userIP <= $high && $low <= $userIP) {
+			$isLocal = true;
+		}
+	}
+	return $isLocal;
+}

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

@@ -0,0 +1,145 @@
+<?php
+function optionLimit()
+{
+	return array(
+		array(
+			'name' => '1 Item',
+			'value' => '1'
+		),
+		array(
+			'name' => '2 Items',
+			'value' => '2'
+		),
+		array(
+			'name' => '3 Items',
+			'value' => '3'
+		),
+		array(
+			'name' => '4 Items',
+			'value' => '4'
+		),
+		array(
+			'name' => '5 Items',
+			'value' => '5'
+		),
+		array(
+			'name' => '6 Items',
+			'value' => '6'
+		),
+		array(
+			'name' => '7 Items',
+			'value' => '7'
+		),
+		array(
+			'name' => '8 Items',
+			'value' => '8'
+		),
+		array(
+			'name' => 'Unlimited',
+			'value' => '1000'
+		),
+	);
+}
+
+function optionNotificationTypes()
+{
+	return array(
+		array(
+			'name' => 'Toastr',
+			'value' => 'toastr'
+		),
+		array(
+			'name' => 'Izi',
+			'value' => 'izi'
+		),
+		array(
+			'name' => 'Alertify',
+			'value' => 'alertify'
+		),
+		array(
+			'name' => 'Noty',
+			'value' => 'noty'
+		),
+	);
+}
+
+function optionNotificationPositions()
+{
+	return array(
+		array(
+			'name' => 'Bottom Right',
+			'value' => 'br'
+		),
+		array(
+			'name' => 'Bottom Left',
+			'value' => 'bl'
+		),
+		array(
+			'name' => 'Bottom Center',
+			'value' => 'bc'
+		),
+		array(
+			'name' => 'Top Right',
+			'value' => 'tr'
+		),
+		array(
+			'name' => 'Top Left',
+			'value' => 'tl'
+		),
+		array(
+			'name' => 'Top Center',
+			'value' => 'tc'
+		),
+		array(
+			'name' => 'Center',
+			'value' => 'c'
+		),
+	);
+}
+
+function optionTime()
+{
+	return array(
+		array(
+			'name' => '5',
+			'value' => '5000'
+		),
+		array(
+			'name' => '10',
+			'value' => '10000'
+		),
+		array(
+			'name' => '15',
+			'value' => '15000'
+		),
+		array(
+			'name' => '30',
+			'value' => '30000'
+		),
+		array(
+			'name' => '60 [1 Minute]',
+			'value' => '60000'
+		),
+		array(
+			'name' => '300 [5 Minutes]',
+			'value' => '300000'
+		),
+		array(
+			'name' => '600 [10 Minutes]',
+			'value' => '600000'
+		),
+		array(
+			'name' => '900 [15 Minutes]',
+			'value' => '900000'
+		),
+		array(
+			'name' => '1800 [30 Minutes]',
+			'value' => '1800000'
+		),
+		array(
+			'name' => '3600 [1 Hour]',
+			'value' => '3600000'
+		),
+	);
+	
+}

+ 1983 - 0
api/functions/organizr-functions.php

@@ -0,0 +1,1983 @@
+<?php
+function organizrSpecialSettings()
+{
+	$refreshSearch = "Refresh";
+	$tautulliSearch = "tautulli_token";
+	$tautulli = array_filter($_COOKIE, function ($k) use ($tautulliSearch) {
+		return stripos($k, $tautulliSearch) !== false;
+	}, ARRAY_FILTER_USE_KEY);
+	return array(
+		'homepage' => array(
+			'refresh' => array_filter($GLOBALS, function ($k) use ($refreshSearch) {
+				return stripos($k, $refreshSearch) !== false;
+			}, ARRAY_FILTER_USE_KEY),
+			'search' => array(
+				'enabled' => (qualifyRequest($GLOBALS['mediaSearchAuth']) && $GLOBALS['mediaSearch'] == true && $GLOBALS['plexToken']) ? true : false,
+				'type' => $GLOBALS['mediaSearchType'],
+			),
+			'ombi' => array(
+				'enabled' => (qualifyRequest($GLOBALS['homepageOmbiAuth']) && qualifyRequest($GLOBALS['homepageOmbiRequestAuth']) && $GLOBALS['homepageOmbiEnabled'] == true && $GLOBALS['ssoOmbi'] && isset($_COOKIE['Auth'])) ? true : false,
+				'authView' => (qualifyRequest($GLOBALS['homepageOmbiAuth'])) ? true : false,
+				'authRequest' => (qualifyRequest($GLOBALS['homepageOmbiRequestAuth'])) ? true : false,
+				'sso' => ($GLOBALS['ssoOmbi']) ? true : false,
+				'cookie' => (isset($_COOKIE['Auth'])) ? true : false,
+			),
+			'options' => array(
+				'alternateHomepageHeaders' => $GLOBALS['alternateHomepageHeaders'],
+			)
+		),
+		'sso' => array(
+			'misc' => array(
+				'oAuthLogin' => isset($_COOKIE['oAuth']) ? true : false,
+				'rememberMe' => $GLOBALS['rememberMe'],
+				'rememberMeDays' => $GLOBALS['rememberMeDays']
+			),
+			'plex' => array(
+				'enabled' => ($GLOBALS['ssoPlex']) ? true : false,
+				'cookie' => isset($_COOKIE['mpt']) ? true : false,
+				'machineID' => (strlen($GLOBALS['plexID']) == 40) ? true : false,
+				'token' => ($GLOBALS['plexToken'] !== '') ? true : false,
+				'oAuthEnabled' => ($GLOBALS['plexoAuth']) ? true : false,
+				'backend' => ($GLOBALS['authBackend'] == 'plex') ? true : false,
+			),
+			'ombi' => array(
+				'enabled' => ($GLOBALS['ssoOmbi']) ? true : false,
+				'cookie' => isset($_COOKIE['Auth']) ? true : false,
+				'url' => ($GLOBALS['ombiURL'] !== '') ? $GLOBALS['ombiURL'] : false,
+				'api' => ($GLOBALS['ombiToken'] !== '') ? true : false,
+			),
+			'tautulli' => array(
+				'enabled' => ($GLOBALS['ssoTautulli']) ? true : false,
+				'cookie' => !empty($tautulli) ? true : false,
+				'url' => ($GLOBALS['tautulliURL'] !== '') ? $GLOBALS['tautulliURL'] : false,
+			),
+		),
+		'ping' => array(
+			'onlineSound' => $GLOBALS['pingOnlineSound'],
+			'offlineSound' => $GLOBALS['pingOfflineSound'],
+			'statusSounds' => $GLOBALS['statusSounds'],
+			'auth' => $GLOBALS['pingAuth'],
+			'authMessage' => $GLOBALS['pingAuthMessage'],
+			'authMs' => $GLOBALS['pingAuthMs'],
+			'ms' => $GLOBALS['pingMs'],
+			'adminRefresh' => $GLOBALS['adminPingRefresh'],
+			'everyoneRefresh' => $GLOBALS['otherPingRefresh'],
+		),
+		'notifications' => array(
+			'backbone' => $GLOBALS['notificationBackbone'],
+			'position' => $GLOBALS['notificationPosition']
+		),
+		'lockout' => array(
+			'enabled' => $GLOBALS['lockoutSystem'],
+			'timer' => $GLOBALS['lockoutTimeout'],
+			'minGroup' => $GLOBALS['lockoutMinAuth'],
+			'maxGroup' => $GLOBALS['lockoutMaxAuth']
+		),
+		'user' => array(
+			'agent' => isset($_SERVER ['HTTP_USER_AGENT']) ? $_SERVER ['HTTP_USER_AGENT'] : null,
+			'oAuthLogin' => isset($_COOKIE['oAuth']) ? true : false,
+			'local' => (isLocal()) ? true : false
+		),
+		'login' => array(
+			'rememberMe' => $GLOBALS['rememberMe'],
+			'rememberMeDays' => $GLOBALS['rememberMeDays'],
+		),
+		'misc' => array(
+			'installedPlugins' => $GLOBALS['installedPlugins'],
+			'installedThemes' => $GLOBALS['installedThemes'],
+			'return' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : false,
+			'authDebug' => $GLOBALS['authDebug'],
+			'minimalLoginScreen' => $GLOBALS['minimalLoginScreen'],
+			'unsortedTabs' => $GLOBALS['unsortedTabs'],
+			'authBackend' => $GLOBALS['authBackend'],
+			'newMessageSound' => (isset($GLOBALS['CHAT-newMessageSound-include'])) ? $GLOBALS['CHAT-newMessageSound-include'] : '',
+			'uuid' => $GLOBALS['uuid'],
+			'docker' => $GLOBALS['docker']
+		)
+	);
+}
+
+function wizardConfig($array)
+{
+	foreach ($array['data'] as $items) {
+		foreach ($items as $key => $value) {
+			if ($key == 'name') {
+				$newKey = $value;
+			}
+			if ($key == 'value') {
+				$newValue = $value;
+			}
+			if (isset($newKey) && isset($newValue)) {
+				$$newKey = $newValue;
+			}
+		}
+	}
+	$location = cleanDirectory($location);
+	$dbName = dbExtension($dbName);
+	$configVersion = $GLOBALS['installedVersion'];
+	$configArray = array(
+		'dbName' => $dbName,
+		'dbLocation' => $location,
+		'license' => $license,
+		'organizrHash' => $hashKey,
+		'organizrAPI' => $api,
+		'registrationPassword' => $registrationPassword,
+	);
+	// Create Config
+	$GLOBALS['dbLocation'] = $location;
+	$GLOBALS['dbName'] = $dbName;
+	if (createConfig($configArray)) {
+		// Call DB Create
+		if (createDB($location, $dbName)) {
+			// Add in first user
+			if (createFirstAdmin($location, $dbName, $username, $password, $email)) {
+				if (createToken($username, $email, gravatar($email), 'Admin', 0, $hashKey, 1)) {
+					return true;
+				} else {
+					return 'token';
+				}
+			} else {
+				return 'admin';
+			}
+		} else {
+			return 'db';
+		}
+	} else {
+		return 'config';
+	}
+	return false;
+}
+
+function register($array)
+{
+	// Grab username and password from login form
+	foreach ($array['data'] as $items) {
+		foreach ($items as $key => $value) {
+			if ($key == 'name') {
+				$newKey = $value;
+			}
+			if ($key == 'value') {
+				$newValue = $value;
+			}
+			if (isset($newKey) && isset($newValue)) {
+				$$newKey = $newValue;
+			}
+		}
+	}
+	if ($registrationPassword == $GLOBALS['registrationPassword']) {
+		$defaults = defaultUserGroup();
+		writeLog('success', 'Registration Function - Registration Password Verified', $username);
+		if (createUser($username, $password, $defaults, $email)) {
+			writeLog('success', 'Registration Function - A User has registered', $username);
+			if (createToken($username, $email, gravatar($email), $defaults['group'], $defaults['group_id'], $GLOBALS['organizrHash'], $GLOBALS['rememberMeDays'])) {
+				writeLoginLog($username, 'success');
+				writeLog('success', 'Login Function - A User has logged in', $username);
+				return true;
+			}
+		} else {
+			writeLog('error', 'Registration Function - An error occured', $username);
+			return 'username taken';
+		}
+	} else {
+		writeLog('warning', 'Registration Function - Wrong Password', $username);
+		return 'mismatch';
+	}
+}
+
+function removeFile($array)
+{
+	$filePath = $array['data']['path'];
+	$fileName = $array['data']['name'];
+	if (file_exists($filePath)) {
+		if (unlink($filePath)) {
+			writeLog('success', 'Log Management Function - Log: ' . $fileName . ' has been purged/deleted', 'SYSTEM');
+			return true;
+		} else {
+			writeLog('error', 'Log Management Function - Log: ' . $fileName . ' - Error Occured', 'SYSTEM');
+			return false;
+		}
+	} else {
+		writeLog('error', 'Log Management Function - Log: ' . $fileName . ' does not exist', 'SYSTEM');
+		return false;
+	}
+}
+
+function recover($array)
+{
+	$email = $array['data']['email'];
+	$newPassword = randString(10);
+	try {
+		$connect = new Dibi\Connection([
+			'driver' => 'sqlite3',
+			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
+		]);
+		$isUser = $connect->fetch('SELECT * FROM users WHERE email = ? COLLATE NOCASE', $email);
+		if ($isUser) {
+			$connect->query('
+                UPDATE users SET', [
+				'password' => password_hash($newPassword, PASSWORD_BCRYPT)
+			], '
+                WHERE email=? COLLATE NOCASE', $email);
+			if ($GLOBALS['PHPMAILER-enabled']) {
+				$emailTemplate = array(
+					'type' => 'reset',
+					'body' => $GLOBALS['PHPMAILER-emailTemplateResetPassword'],
+					'subject' => $GLOBALS['PHPMAILER-emailTemplateResetPasswordSubject'],
+					'user' => $isUser['username'],
+					'password' => $newPassword,
+					'inviteCode' => null,
+				);
+				$emailTemplate = phpmEmailTemplate($emailTemplate);
+				$sendEmail = array(
+					'to' => $email,
+					'user' => $isUser['username'],
+					'subject' => $emailTemplate['subject'],
+					'body' => phpmBuildEmail($emailTemplate),
+				);
+				phpmSendEmail($sendEmail);
+			}
+			writeLog('success', 'User Management Function - User: ' . $isUser['username'] . '\'s password was reset', $isUser['username']);
+			return true;
+		} else {
+			writeLog('error', 'User Management Function - Error - User: ' . $email . ' An error Occured', $email);
+			return 'an error occured';
+		}
+	} catch (Dibi\Exception $e) {
+		writeLog('error', 'User Management Function - Error - User: ' . $email . ' An error Occured', $email);
+		return 'an error occured';
+	}
+}
+
+function unlock($array)
+{
+	if ($array['data']['password'] == '') {
+		return 'Password Not Set';
+	}
+	try {
+		$connect = new Dibi\Connection([
+			'driver' => 'sqlite3',
+			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
+		]);
+		$result = $connect->fetch('SELECT * FROM users WHERE id = ?', $GLOBALS['organizrUser']['userID']);
+		if (!password_verify($array['data']['password'], $result['password'])) {
+			return 'Password Incorrect';
+		}
+		$connect->query('
+            UPDATE users SET', [
+			'locked' => ''
+		], '
+            WHERE id=?', $GLOBALS['organizrUser']['userID']);
+		writeLog('success', 'User Lockout Function - User: ' . $GLOBALS['organizrUser']['username'] . '\'s account unlocked', $GLOBALS['organizrUser']['username']);
+		return true;
+	} catch (Dibi\Exception $e) {
+		writeLog('error', 'User Management Function - Error - User: ' . $GLOBALS['organizrUser']['username'] . ' An error Occured', $GLOBALS['organizrUser']['username']);
+		return 'an error occured';
+	}
+}
+
+function lock()
+{
+	if ($GLOBALS['organizrUser']['userID'] == '999') {
+		return 'Not Allowed on Guest';
+	}
+	try {
+		$connect = new Dibi\Connection([
+			'driver' => 'sqlite3',
+			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
+		]);
+		$connect->query('
+            UPDATE users SET', [
+			'locked' => '1'
+		], '
+            WHERE id=?', $GLOBALS['organizrUser']['userID']);
+		writeLog('success', 'User Lockout Function - User: ' . $GLOBALS['organizrUser']['username'] . '\'s account unlocked', $GLOBALS['organizrUser']['username']);
+		return true;
+	} catch (Dibi\Exception $e) {
+		writeLog('error', 'User Management Function - Error - User: ' . $GLOBALS['organizrUser']['username'] . ' An error Occured', $GLOBALS['organizrUser']['username']);
+		return 'an error occured';
+	}
+}
+
+function editUser($array)
+{
+	if ($array['data']['username'] == '' && $array['data']['username'] == '') {
+		return 'Username/email not set';
+	}
+	try {
+		$connect = new Dibi\Connection([
+			'driver' => 'sqlite3',
+			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
+		]);
+		if (!usernameTakenExcept($array['data']['username'], $array['data']['email'], $GLOBALS['organizrUser']['userID'])) {
+			$connect->query('
+                UPDATE users SET', [
+				'username' => $array['data']['username'],
+				'email' => $array['data']['email'],
+				'image' => gravatar($array['data']['email']),
+			], '
+                WHERE id=?', $GLOBALS['organizrUser']['userID']);
+			if (!empty($array['data']['password'])) {
+				$connect->query('
+                    UPDATE users SET', [
+					'password' => password_hash($array['data']['password'], PASSWORD_BCRYPT)
+				], '
+                    WHERE id=?', $GLOBALS['organizrUser']['userID']);
+			}
+			writeLog('success', 'User Management Function - User: ' . $array['data']['username'] . '\'s info was changed', $GLOBALS['organizrUser']['username']);
+			return true;
+		} else {
+			return 'Username/Email Already Taken';
+		}
+	} catch (Dibi\Exception $e) {
+		writeLog('error', 'User Management Function - Error - User: ' . $array['data']['username'] . ' An error Occured', $GLOBALS['organizrUser']['username']);
+		return 'an error occured';
+	}
+}
+
+function clearTautulliTokens()
+{
+	foreach (array_keys($_COOKIE) as $k => $v) {
+		if (strpos($v, 'tautulli') !== false) {
+			coookie('delete', $v);
+		}
+	}
+}
+
+function logout()
+{
+	coookie('delete', $GLOBALS['cookieName']);
+	coookie('delete', 'mpt');
+	coookie('delete', 'Auth');
+	coookie('delete', 'oAuth');
+	clearTautulliTokens();
+	revokeToken(array('data' => array('token' => $GLOBALS['organizrUser']['token'])));
+	$GLOBALS['organizrUser'] = false;
+	return true;
+}
+
+function qualifyRequest($accessLevelNeeded)
+{
+	if (getUserLevel() <= $accessLevelNeeded && getUserLevel() !== null) {
+		return true;
+	} else {
+		return false;
+	}
+}
+
+function getUserLevel()
+{
+	// Grab token
+	//$requesterToken = isset(getallheaders()['Token']) ? getallheaders()['Token'] : false;
+	$requesterToken = isset(getallheaders()['Token']) ? getallheaders()['Token'] : (isset($_GET['apikey']) ? $_GET['apikey'] : false);
+	// Check token or API key
+	// If API key, return 0 for admin
+	if (strlen($requesterToken) == 20 && $requesterToken == $GLOBALS['organizrAPI']) {
+		//DO API CHECK
+		return 0;
+	} elseif (isset($GLOBALS['organizrUser'])) {
+		return $GLOBALS['organizrUser']['groupID'];
+	}
+	// All else fails?  return guest id
+	return 999;
+}
+
+function organizrStatus()
+{
+	$status = array();
+	$dependenciesActive = array();
+	$dependenciesInactive = array();
+	$extensions = array("PDO_SQLITE", "PDO", "SQLITE3", "zip", "cURL", "openssl", "simplexml", "json", "session");
+	$functions = array("hash", "fopen", "fsockopen", "fwrite", "fclose", "readfile");
+	foreach ($extensions as $check) {
+		if (extension_loaded($check)) {
+			array_push($dependenciesActive, $check);
+		} else {
+			array_push($dependenciesInactive, $check);
+		}
+	}
+	foreach ($functions as $check) {
+		if (function_exists($check)) {
+			array_push($dependenciesActive, $check);
+		} else {
+			array_push($dependenciesInactive, $check);
+		}
+	}
+	if (!file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
+		$status['status'] = "wizard";//wizard - ok for test
+	}
+	if (count($dependenciesInactive) > 0 || !is_writable(dirname(__DIR__, 2)) || !(version_compare(PHP_VERSION, $GLOBALS['minimumPHP']) >= 0)) {
+		$status['status'] = "dependencies";
+	}
+	$status['status'] = (!empty($status['status'])) ? $status['status'] : $status['status'] = "ok";
+	$status['writable'] = is_writable(dirname(__DIR__, 2)) ? 'yes' : 'no';
+	$status['minVersion'] = (version_compare(PHP_VERSION, $GLOBALS['minimumPHP']) >= 0) ? 'yes' : 'no';
+	$status['dependenciesActive'] = $dependenciesActive;
+	$status['dependenciesInactive'] = $dependenciesInactive;
+	$status['version'] = $GLOBALS['installedVersion'];
+	$status['os'] = getOS();
+	$status['php'] = phpversion();
+	return $status;
+}
+
+function getSettingsMain()
+{
+	return array(
+		'Github' => array(
+			array(
+				'type' => 'select',
+				'name' => 'branch',
+				'label' => 'Branch',
+				'value' => $GLOBALS['branch'],
+				'options' => getBranches()
+			),
+			array(
+				'type' => 'button',
+				'label' => 'Force Install Branch',
+				'class' => 'updateNow',
+				'icon' => 'fa fa-download',
+				'text' => 'Retrieve',
+				'attr' => ($GLOBALS['docker']) ? 'title="You can just restart your docker to update"' : '',
+				'help' => ($GLOBALS['docker']) ? 'Since you are using the Official Docker image, You can just restart your docker to update' : 'This will re-download all of the source files for Organizr'
+			)
+		),
+		'API' => array(
+			array(
+				'type' => 'password-alt',
+				'name' => 'organizrAPI',
+				'label' => 'Organizr API',
+				'value' => $GLOBALS['organizrAPI']
+			),
+			array(
+				'type' => 'button',
+				'label' => 'Generate New API Key',
+				'class' => 'newAPIKey',
+				'icon' => 'fa fa-refresh',
+				'text' => 'Generate'
+			)
+		),
+		'Authentication' => array(
+			array(
+				'type' => 'select',
+				'name' => 'authType',
+				'id' => 'authSelect',
+				'label' => 'Authentication Type',
+				'value' => $GLOBALS['authType'],
+				'options' => getAuthTypes()
+			),
+			array(
+				'type' => 'select',
+				'name' => 'authBackend',
+				'id' => 'authBackendSelect',
+				'label' => 'Authentication Backend',
+				'class' => 'backendAuth switchAuth',
+				'value' => $GLOBALS['authBackend'],
+				'options' => getAuthBackends()
+			),
+			array(
+				'type' => 'password-alt',
+				'name' => 'plexToken',
+				'class' => 'plexAuth switchAuth',
+				'label' => 'Plex Token',
+				'value' => $GLOBALS['plexToken'],
+				'placeholder' => 'Use Get Token Button'
+			),
+			array(
+				'type' => 'button',
+				'label' => 'Get Plex Token',
+				'class' => 'popup-with-form getPlexTokenAuth plexAuth switchAuth',
+				'icon' => 'fa fa-ticket',
+				'text' => 'Retrieve',
+				'href' => '#auth-plex-token-form',
+				'attr' => 'data-effect="mfp-3d-unfold"'
+			),
+			array(
+				'type' => 'password-alt',
+				'name' => 'plexID',
+				'class' => 'plexAuth switchAuth',
+				'label' => 'Plex Machine',
+				'value' => $GLOBALS['plexID'],
+				'placeholder' => 'Use Get Plex Machine Button'
+			),
+			array(
+				'type' => 'button',
+				'label' => 'Get Plex Machine',
+				'class' => 'popup-with-form getPlexMachineAuth plexAuth switchAuth',
+				'icon' => 'fa fa-id-badge',
+				'text' => 'Retrieve',
+				'href' => '#auth-plex-machine-form',
+				'attr' => 'data-effect="mfp-3d-unfold"'
+			),
+			array(
+				'type' => 'input',
+				'name' => 'plexAdmin',
+				'label' => 'Admin Username',
+				'class' => 'plexAuth switchAuth',
+				'value' => $GLOBALS['plexAdmin'],
+				'placeholder' => 'Admin username for Plex'
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'plexoAuth',
+				'label' => 'Enable Plex oAuth',
+				'class' => 'plexAuth switchAuth',
+				'value' => $GLOBALS['plexoAuth']
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'plexStrictFriends',
+				'label' => 'Strict Plex Friends ',
+				'class' => 'plexAuth switchAuth',
+				'value' => $GLOBALS['plexStrictFriends'],
+				'help' => 'Enabling this will only allow Friends that have shares to the Machine ID entered above to login, Having this disabled will allow all Friends on your Friends list to login'
+			),
+			array(
+				'type' => 'input',
+				'name' => 'authBackendHost',
+				'class' => 'ldapAuth ftpAuth switchAuth',
+				'label' => 'Host Address',
+				'value' => $GLOBALS['authBackendHost'],
+				'placeholder' => 'http{s) | ftp(s) | ldap(s)://hostname:port'
+			),
+			array(
+				'type' => 'input',
+				'name' => 'authBaseDN',
+				'class' => 'ldapAuth switchAuth',
+				'label' => 'Host Base DN',
+				'value' => $GLOBALS['authBaseDN'],
+				'placeholder' => 'cn=%s,dc=sub,dc=domain,dc=com'
+			),
+			array(
+				'type' => 'input',
+				'name' => 'embyURL',
+				'class' => 'embyAuth switchAuth',
+				'label' => 'Emby URL',
+				'value' => $GLOBALS['embyURL'],
+				'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+				'placeholder' => 'http(s)://hostname:port'
+			),
+			array(
+				'type' => 'password-alt',
+				'name' => 'embyToken',
+				'class' => 'embyAuth switchAuth',
+				'label' => 'Emby Token',
+				'value' => $GLOBALS['embyToken'],
+				'placeholder' => ''
+			),
+			/*array(
+				'type' => 'button',
+				'label' => 'Send Test',
+				'class' => 'phpmSendTestEmail',
+				'icon' => 'fa fa-paper-plane',
+				'text' => 'Send'
+			)*/
+		),
+		'Security' => array(
+			array(
+				'type' => 'number',
+				'name' => 'lockoutTimeout',
+				'label' => 'Inactivity Timer [Minutes]',
+				'value' => $GLOBALS['lockoutTimeout'],
+				'placeholder' => ''
+			),
+			array(
+				'type' => 'select',
+				'name' => 'lockoutMinAuth',
+				'label' => 'Lockout Groups From',
+				'value' => $GLOBALS['lockoutMinAuth'],
+				'options' => groupSelect()
+			),
+			array(
+				'type' => 'select',
+				'name' => 'lockoutMaxAuth',
+				'label' => 'Lockout Groups To',
+				'value' => $GLOBALS['lockoutMaxAuth'],
+				'options' => groupSelect()
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'lockoutSystem',
+				'label' => 'Inactivity Lock',
+				'value' => $GLOBALS['lockoutSystem']
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'authDebug',
+				'label' => 'Nginx Auth Debug',
+				'help' => 'Important! Do not keep this enabled for too long as this opens up Authentication while testing.',
+				'value' => $GLOBALS['authDebug'],
+				'class' => 'authDebug'
+			)
+		),
+		'Login' => array(
+			array(
+				'type' => 'password-alt',
+				'name' => 'registrationPassword',
+				'label' => 'Registration Password',
+				'help' => 'Sets the password for the Registration form on the login screen',
+				'value' => $GLOBALS['registrationPassword'],
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'hideRegistration',
+				'label' => 'Hide Registration',
+				'help' => 'Enable this to hide the Registration button on the login screen',
+				'value' => $GLOBALS['hideRegistration'],
+			),
+			array(
+				'type' => 'number',
+				'name' => 'rememberMeDays',
+				'label' => 'Remember Me Length',
+				'help' => 'Number of days cookies and tokens will be valid for',
+				'value' => $GLOBALS['rememberMeDays'],
+				'placeholder' => '',
+				'attr' => 'min="1"'
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'rememberMe',
+				'label' => 'Remember Me',
+				'help' => 'Default status of Remember Me button on login screen',
+				'value' => $GLOBALS['rememberMe'],
+			),
+		),
+		'Ping' => array(
+			array(
+				'type' => 'select',
+				'name' => 'pingAuth',
+				'label' => 'Minimum Authentication',
+				'value' => $GLOBALS['pingAuth'],
+				'options' => groupSelect()
+			),
+			array(
+				'type' => 'select',
+				'name' => 'pingAuthMessage',
+				'label' => 'Minimum Authentication for Message and Sound',
+				'value' => $GLOBALS['pingAuthMessage'],
+				'options' => groupSelect()
+			),
+			array(
+				'type' => 'select',
+				'name' => 'pingOnlineSound',
+				'label' => 'Online Sound',
+				'value' => $GLOBALS['pingOnlineSound'],
+				'options' => getSounds()
+			),
+			array(
+				'type' => 'select',
+				'name' => 'pingOfflineSound',
+				'label' => 'Offline Sound',
+				'value' => $GLOBALS['pingOfflineSound'],
+				'options' => getSounds()
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'pingMs',
+				'label' => 'Show Ping Time',
+				'value' => $GLOBALS['pingMs']
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'statusSounds',
+				'label' => 'Enable Notify Sounds',
+				'value' => $GLOBALS['statusSounds'],
+				'help' => 'Will play a sound if the server goes down and will play sound if comes back up.',
+			),
+			array(
+				'type' => 'select',
+				'name' => 'pingAuthMs',
+				'label' => 'Minimum Authentication for Time Display',
+				'value' => $GLOBALS['pingAuthMs'],
+				'options' => groupSelect()
+			),
+			array(
+				'type' => 'select',
+				'name' => 'adminPingRefresh',
+				'label' => 'Admin Refresh Seconds',
+				'value' => $GLOBALS['adminPingRefresh'],
+				'options' => optionTime()
+			),
+			array(
+				'type' => 'select',
+				'name' => 'otherPingRefresh',
+				'label' => 'Everyone Refresh Seconds',
+				'value' => $GLOBALS['otherPingRefresh'],
+				'options' => optionTime()
+			),
+		)
+	);
+}
+
+function getSSO()
+{
+	return array(
+		'FYI' => array(
+			array(
+				'type' => 'html',
+				'label' => 'Important Information',
+				'override' => 12,
+				'html' => '
+				<div class="row">
+						    <div class="col-lg-12">
+						        <div class="panel panel-info">
+						            <div class="panel-heading">
+						                <span lang="en">Notice</span>
+						            </div>
+						            <div class="panel-wrapper collapse in" aria-expanded="true">
+						                <div class="panel-body">
+						                    <span lang="en">This is not the same as database authentication - i.e. Plex Authentication | Emby Authentication | FTP Authentication<br/>Click Main on the sub-menu above.</span>
+						                </div>
+						            </div>
+						        </div>
+						    </div>
+						</div>
+				'
+			)
+		),
+		'Plex' => array(
+			array(
+				'type' => 'password-alt',
+				'name' => 'plexToken',
+				'label' => 'Plex Token',
+				'value' => $GLOBALS['plexToken'],
+				'placeholder' => 'Use Get Token Button'
+			),
+			array(
+				'type' => 'button',
+				'label' => 'Get Plex Token',
+				'class' => 'popup-with-form getPlexTokenSSO',
+				'icon' => 'fa fa-ticket',
+				'text' => 'Retrieve',
+				'href' => '#sso-plex-token-form',
+				'attr' => 'data-effect="mfp-3d-unfold"'
+			),
+			array(
+				'type' => 'password-alt',
+				'name' => 'plexID',
+				'label' => 'Plex Machine',
+				'value' => $GLOBALS['plexID'],
+				'placeholder' => 'Use Get Plex Machine Button'
+			),
+			array(
+				'type' => 'button',
+				'label' => 'Get Plex Machine',
+				'class' => 'popup-with-form getPlexMachineSSO',
+				'icon' => 'fa fa-id-badge',
+				'text' => 'Retrieve',
+				'href' => '#sso-plex-machine-form',
+				'attr' => 'data-effect="mfp-3d-unfold"'
+			),
+			array(
+				'type' => 'input',
+				'name' => 'plexAdmin',
+				'label' => 'Admin Username',
+				'value' => $GLOBALS['plexAdmin'],
+				'placeholder' => 'Admin username for Plex'
+			),
+			array(
+				'type' => 'blank',
+				'label' => ''
+			),
+			array(
+				'type' => 'html',
+				'label' => 'Plex Note',
+				'html' => '<span lang="en">Please make sure both Token and Machine are filled in</span>'
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'ssoPlex',
+				'label' => 'Enable',
+				'value' => $GLOBALS['ssoPlex']
+			)
+		),
+		'Ombi' => array(
+			array(
+				'type' => 'input',
+				'name' => 'ombiURL',
+				'label' => 'Ombi URL',
+				'value' => $GLOBALS['ombiURL'],
+				'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+				'placeholder' => 'http(s)://hostname:port'
+			),
+			array(
+				'type' => 'password-alt',
+				'name' => 'ombiToken',
+				'label' => 'Token',
+				'value' => $GLOBALS['ombiToken']
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'ssoOmbi',
+				'label' => 'Enable',
+				'value' => $GLOBALS['ssoOmbi']
+			)
+		),
+		'Tautulli' => array(
+			array(
+				'type' => 'input',
+				'name' => 'tautulliURL',
+				'label' => 'Tautulli URL',
+				'value' => $GLOBALS['tautulliURL'],
+				'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
+				'placeholder' => 'http(s)://hostname:port'
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'ssoTautulli',
+				'label' => 'Enable',
+				'value' => $GLOBALS['ssoTautulli']
+			)
+		)
+	);
+}
+
+function loadAppearance()
+{
+	$appearance = array();
+	$appearance['logo'] = $GLOBALS['logo'];
+	$appearance['title'] = $GLOBALS['title'];
+	$appearance['useLogo'] = $GLOBALS['useLogo'];
+	$appearance['headerColor'] = $GLOBALS['headerColor'];
+	$appearance['headerTextColor'] = $GLOBALS['headerTextColor'];
+	$appearance['sidebarColor'] = $GLOBALS['sidebarColor'];
+	$appearance['headerTextColor'] = $GLOBALS['headerTextColor'];
+	$appearance['sidebarTextColor'] = $GLOBALS['sidebarTextColor'];
+	$appearance['accentColor'] = $GLOBALS['accentColor'];
+	$appearance['accentTextColor'] = $GLOBALS['accentTextColor'];
+	$appearance['buttonColor'] = $GLOBALS['buttonColor'];
+	$appearance['buttonTextColor'] = $GLOBALS['buttonTextColor'];
+	$appearance['buttonTextHoverColor'] = $GLOBALS['buttonTextHoverColor'];
+	$appearance['buttonHoverColor'] = $GLOBALS['buttonHoverColor'];
+	$appearance['loginWallpaper'] = $GLOBALS['loginWallpaper'];
+	$appearance['customCss'] = $GLOBALS['customCss'];
+	$appearance['customThemeCss'] = $GLOBALS['customThemeCss'];
+	$appearance['customJava'] = $GLOBALS['customJava'];
+	$appearance['customThemeJava'] = $GLOBALS['customThemeJava'];
+	return $appearance;
+}
+
+function getCustomizeAppearance()
+{
+	if (file_exists(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.php')) {
+		return array(
+			'Top Bar' => array(
+				array(
+					'type' => 'input',
+					'name' => 'logo',
+					'label' => 'Logo',
+					'value' => $GLOBALS['logo']
+				),
+				array(
+					'type' => 'input',
+					'name' => 'title',
+					'label' => 'Title',
+					'value' => $GLOBALS['title']
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'useLogo',
+					'label' => 'Use Logo instead of Title',
+					'value' => $GLOBALS['useLogo']
+				)
+			),
+			'Login Page' => array(
+				array(
+					'type' => 'input',
+					'name' => 'loginWallpaper',
+					'label' => 'Login Wallpaper',
+					'value' => $GLOBALS['loginWallpaper']
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'minimalLoginScreen',
+					'label' => 'Minimal Login Screen',
+					'value' => $GLOBALS['minimalLoginScreen']
+				)
+			),
+			'Options' => array(
+				array(
+					'type' => 'switch',
+					'name' => 'alternateHomepageHeaders',
+					'label' => 'Alternate Homepage Titles',
+					'value' => $GLOBALS['alternateHomepageHeaders']
+				),
+				array(
+					'type' => 'select',
+					'name' => 'unsortedTabs',
+					'label' => 'Unsorted Tab Placement',
+					'value' => $GLOBALS['unsortedTabs'],
+					'options' => array(
+						array(
+							'name' => 'Top',
+							'value' => 'top'
+						),
+						array(
+							'name' => 'Bottom',
+							'value' => 'bottom'
+						)
+					)
+				)
+			),
+			'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>
+					                </div>
+					            </div>
+					        </div>
+					    </div>
+					</div>
+					',
+				),
+				array(
+					'type' => 'blank',
+					'label' => ''
+				),
+				array(
+					'type' => 'input',
+					'name' => 'headerColor',
+					'label' => 'Nav Bar Color',
+					'value' => $GLOBALS['headerColor'],
+					'class' => 'pick-a-color',
+					'attr' => 'data-original="' . $GLOBALS['headerColor'] . '"'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'headerTextColor',
+					'label' => 'Nav Bar Text Color',
+					'value' => $GLOBALS['headerTextColor'],
+					'class' => 'pick-a-color',
+					'attr' => 'data-original="' . $GLOBALS['headerTextColor'] . '"'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'sidebarColor',
+					'label' => 'Side Bar Color',
+					'value' => $GLOBALS['sidebarColor'],
+					'class' => 'pick-a-color',
+					'attr' => 'data-original="' . $GLOBALS['sidebarColor'] . '"'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'sidebarTextColor',
+					'label' => 'Side Bar Text Color',
+					'value' => $GLOBALS['sidebarTextColor'],
+					'class' => 'pick-a-color',
+					'attr' => 'data-original="' . $GLOBALS['sidebarTextColor'] . '"'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'accentColor',
+					'label' => 'Accent Color',
+					'value' => $GLOBALS['accentColor'],
+					'class' => 'pick-a-color',
+					'attr' => 'data-original="' . $GLOBALS['accentColor'] . '"'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'accentTextColor',
+					'label' => 'Accent Text Color',
+					'value' => $GLOBALS['accentTextColor'],
+					'class' => 'pick-a-color',
+					'attr' => 'data-original="' . $GLOBALS['accentTextColor'] . '"'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'buttonColor',
+					'label' => 'Button Color',
+					'value' => $GLOBALS['buttonColor'],
+					'class' => 'pick-a-color',
+					'attr' => 'data-original="' . $GLOBALS['buttonColor'] . '"'
+				),
+				array(
+					'type' => 'input',
+					'name' => 'buttonTextColor',
+					'label' => 'Button Text Color',
+					'value' => $GLOBALS['buttonTextColor'],
+					'class' => 'pick-a-color',
+					'attr' => 'data-original="' . $GLOBALS['buttonTextColor'] . '"'
+				),/*
+                array(
+                    'type' => 'input',
+                    'name' => 'buttonHoverColor',
+                    'label' => 'Button Hover Color',
+                    'value' => $GLOBALS['buttonHoverColor'],
+                    'class' => 'pick-a-color',
+                    'disabled' => true
+                ),
+                array(
+                    'type' => 'input',
+                    'name' => 'buttonTextHoverColor',
+                    'label' => 'Button Hover Text Color',
+                    'value' => $GLOBALS['buttonTextHoverColor'],
+                    'class' => 'pick-a-color',
+                    'disabled' => true
+                ),*/
+				array(
+					'type' => 'select',
+					'name' => 'theme',
+					'label' => 'Theme',
+					'class' => 'themeChanger',
+					'value' => $GLOBALS['theme'],
+					'options' => getThemes()
+				),
+				array(
+					'type' => 'select',
+					'name' => 'style',
+					'label' => 'Style',
+					'class' => 'styleChanger',
+					'value' => $GLOBALS['style'],
+					'options' => array(
+						array(
+							'name' => 'Light',
+							'value' => 'light'
+						),
+						array(
+							'name' => 'Dark',
+							'value' => 'dark'
+						),
+						array(
+							'name' => 'Horizontal',
+							'value' => 'horizontal'
+						)
+					)
+				)
+			),
+			'Notifications' => array(
+				array(
+					'type' => 'select',
+					'name' => 'notificationBackbone',
+					'class' => 'notifyChanger',
+					'label' => 'Type',
+					'value' => $GLOBALS['notificationBackbone'],
+					'options' => optionNotificationTypes()
+				),
+				array(
+					'type' => 'select',
+					'name' => 'notificationPosition',
+					'class' => 'notifyPositionChanger',
+					'label' => 'Position',
+					'value' => $GLOBALS['notificationPosition'],
+					'options' => optionNotificationPositions()
+				),
+				array(
+					'type' => 'html',
+					'label' => 'Test Message',
+					'html' => '
+					<div class="btn-group m-r-10 dropup">
+						<button aria-expanded="false" data-toggle="dropdown" class="btn btn-info btn-outline dropdown-toggle waves-effect waves-light" type="button">
+							<i class="fa fa-comment m-r-5"></i>
+							<span>Test </span>
+						</button>
+						<ul role="menu" class="dropdown-menu">
+							<li><a onclick="message(\'Test Message\',\'This is a success Message\',activeInfo.settings.notifications.position,\'#FFF\',\'success\',\'5000\');">Success</a></li>
+							<li><a onclick="message(\'Test Message\',\'This is a info Message\',activeInfo.settings.notifications.position,\'#FFF\',\'info\',\'5000\');">Info</a></li>
+							<li><a onclick="message(\'Test Message\',\'This is a warning Message\',activeInfo.settings.notifications.position,\'#FFF\',\'warning\',\'5000\');">Warning</a></li>
+							<li><a onclick="message(\'Test Message\',\'This is a error Message\',activeInfo.settings.notifications.position,\'#FFF\',\'error\',\'5000\');">Error</a></li>
+						</ul>
+					</div>
+					'
+				)
+			),
+			'FavIcon' => array(
+				array(
+					'type' => 'textbox',
+					'name' => 'favIcon',
+					'class' => '',
+					'label' => 'Fav Icon Code',
+					'value' => $GLOBALS['favIcon'],
+					'placeholder' => 'Paste Contents from https://realfavicongenerator.net/',
+					'attr' => 'rows="10"',
+				),
+				array(
+					'type' => 'html',
+					'label' => 'Instructions',
+					'html' => '
+					<div class="panel panel-default">
+						<div class="panel-heading">
+							<a href="https://realfavicongenerator.net/" target="_blank"><span class="label label-info m-l-5">Visit FavIcon Site</span></a>
+						</div>
+						<div class="panel-wrapper collapse in">
+							<div class="panel-body">
+								<ul class="list-icons">
+									<li lang="en"><i class="fa fa-caret-right text-info"></i> Click [Select your Favicon picture]</li>
+									<li lang="en"><i class="fa fa-caret-right text-info"></i> Choose your image to use</li>
+									<li lang="en"><i class="fa fa-caret-right text-info"></i> Edit settings to your liking</li>
+									<li lang="en"><i class="fa fa-caret-right text-info"></i> At bottom of page on [Favicon Generator Options] under [Path] choose [I cannot or I do not want to place favicon files at the root of my web site.]</li>
+									<li lang="en"><i class="fa fa-caret-right text-info"></i> Enter this path <code>plugins/images/faviconCustom</code></li>
+									<li lang="en"><i class="fa fa-caret-right text-info"></i> Click [Generate your Favicons and HTML code]</li>
+									<li lang="en"><i class="fa fa-caret-right text-info"></i> Download and unzip file and place in <code>plugins/images/faviconCustom</code></li>
+									<li lang="en"><i class="fa fa-caret-right text-info"></i> Copy code and paste inside left box</li>
+								</ul>
+							</div>
+						</div>
+					</div>
+					'
+				),
+			),
+			'Custom CSS' => array(
+				array(
+					'type' => 'html',
+					'override' => 12,
+					'label' => 'Custom CSS [Can replace colors from above]',
+					'html' => '<button type="button" class="hidden saveCss btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customCSSEditor" style="height:300px">' . $GLOBALS['customCss'] . '</div>'
+				),
+				array(
+					'type' => 'textbox',
+					'name' => 'customCss',
+					'class' => 'hidden cssTextarea',
+					'label' => '',
+					'value' => $GLOBALS['customCss'],
+					'placeholder' => 'No <style> tags needed',
+					'attr' => 'rows="10"',
+				),
+			),
+			'Theme CSS' => array(
+				array(
+					'type' => 'html',
+					'override' => 12,
+					'label' => 'Theme CSS [Can replace colors from above]',
+					'html' => '<button type="button" class="hidden saveCssTheme btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customThemeCSSEditor" style="height:300px">' . $GLOBALS['customThemeCss'] . '</div>'
+				),
+				array(
+					'type' => 'textbox',
+					'name' => 'customThemeCss',
+					'class' => 'hidden cssThemeTextarea',
+					'label' => '',
+					'value' => $GLOBALS['customThemeCss'],
+					'placeholder' => 'No <style> tags needed',
+					'attr' => 'rows="10"',
+				),
+			),
+			'Custom Javascript' => array(
+				array(
+					'type' => 'html',
+					'override' => 12,
+					'label' => 'Custom Javascript',
+					'html' => '<button type="button" class="hidden saveJava btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customJavaEditor" style="height:300px">' . $GLOBALS['customJava'] . '</div>'
+				),
+				array(
+					'type' => 'textbox',
+					'name' => 'customJava',
+					'class' => 'hidden javaTextarea',
+					'label' => '',
+					'value' => $GLOBALS['customJava'],
+					'placeholder' => 'No <script> tags needed',
+					'attr' => 'rows="10"',
+				),
+			),
+			'Theme Javascript' => array(
+				array(
+					'type' => 'html',
+					'override' => 12,
+					'label' => 'Theme Javascript',
+					'html' => '<button type="button" class="hidden saveJavaTheme btn btn-info btn-circle pull-right m-r-5 m-l-10"><i class="fa fa-save"></i> </button><div id="customThemeJavaEditor" style="height:300px">' . $GLOBALS['customThemeJava'] . '</div>'
+				),
+				array(
+					'type' => 'textbox',
+					'name' => 'customThemeJava',
+					'class' => 'hidden javaThemeTextarea',
+					'label' => '',
+					'value' => $GLOBALS['customThemeJava'],
+					'placeholder' => 'No <script> tags needed',
+					'attr' => 'rows="10"',
+				),
+			),
+		);
+	}
+}
+
+function editAppearance($array)
+{
+	switch ($array['data']['value']) {
+		case 'true':
+			$array['data']['value'] = (bool)true;
+			break;
+		case 'false':
+			$array['data']['value'] = (bool)false;
+			break;
+		default:
+			$array['data']['value'] = $array['data']['value'];
+	}
+	//return gettype($array['data']['value']).' - '.$array['data']['value'];
+	switch ($array['data']['action']) {
+		case 'editCustomizeAppearance':
+			$newItem = array(
+				$array['data']['name'] => $array['data']['value']
+			);
+			return (updateConfig($newItem)) ? true : false;
+			break;
+		default:
+			# code...
+			break;
+	}
+}
+
+function updateConfigMultiple($array)
+{
+	return (updateConfig($array['data']['payload'])) ? true : false;
+}
+
+function updateConfigMultipleForm($array)
+{
+	$newItem = array();
+	foreach ($array['data']['payload'] as $k => $v) {
+		switch ($v['value']) {
+			case 'true':
+				$v['value'] = (bool)true;
+				break;
+			case 'false':
+				$v['value'] = (bool)false;
+				break;
+			default:
+				$v['value'] = $v['value'];
+		}
+		// Hash
+		if ($v['type'] == 'password') {
+			if (isEncrypted($v['value']) || $v['value'] == '') {
+				$v['value'] = $v['value'];
+			} else {
+				$v['value'] = encrypt($v['value']);
+			}
+		}
+		$newItem[$v['name']] = $v['value'];
+	}
+	//return $newItem;
+	return (updateConfig($newItem)) ? true : false;
+}
+
+function updateConfigItem($array)
+{
+	switch ($array['data']['value']) {
+		case 'true':
+			$array['data']['value'] = (bool)true;
+			break;
+		case 'false':
+			$array['data']['value'] = (bool)false;
+			break;
+		default:
+			$array['data']['value'] = $array['data']['value'];
+	}
+	// Hash
+	if ($array['data']['type'] == 'password') {
+		$array['data']['value'] = encrypt($array['data']['value']);
+	}
+	$newItem = array(
+		$array['data']['name'] => $array['data']['value']
+	);
+	return (updateConfig($newItem)) ? true : false;
+}
+
+function getPlugins()
+{
+	if (file_exists(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.php')) {
+		$pluginList = [];
+		foreach ($GLOBALS['plugins'] as $plugin) {
+			foreach ($plugin as $key => $value) {
+				if (strpos($value['license'], $GLOBALS['license']) !== false) {
+					$plugin[$key]['enabled'] = $GLOBALS[$value['configPrefix'] . '-enabled'];
+					$pluginList[$key] = $plugin[$key];
+				}
+			}
+		}
+		return $pluginList;
+	}
+	return false;
+}
+
+function editPlugins($array)
+{
+	switch ($array['data']['action']) {
+		case 'enable':
+			$newItem = array(
+				$array['data']['configName'] => true
+			);
+			writeLog('success', 'Plugin Function -  Enabled Plugin [' . $_POST['data']['name'] . ']', $GLOBALS['organizrUser']['username']);
+			return (updateConfig($newItem)) ? true : false;
+			break;
+		case 'disable':
+			$newItem = array(
+				$array['data']['configName'] => false
+			);
+			writeLog('success', 'Plugin Function -  Disabled Plugin [' . $_POST['data']['name'] . ']', $GLOBALS['organizrUser']['username']);
+			return (updateConfig($newItem)) ? true : false;
+			break;
+		default:
+			# code...
+			break;
+	}
+}
+
+function auth()
+{
+	$debug = $GLOBALS['authDebug']; // CAREFUL WHEN SETTING TO TRUE AS THIS OPENS AUTH UP
+	$ban = isset($_GET['ban']) ? strtoupper($_GET['ban']) : "";
+	$whitelist = isset($_GET['whitelist']) ? $_GET['whitelist'] : false;
+	$blacklist = isset($_GET['blacklist']) ? $_GET['blacklist'] : false;
+	$group = isset($_GET['group']) ? (int)$_GET['group'] : (int)0;
+	$currentIP = userIP();
+	$unlocked = ($GLOBALS['organizrUser']['locked'] == '1') ? false : true;
+	if (isset($GLOBALS['organizrUser'])) {
+		$currentUser = $GLOBALS['organizrUser']['username'];
+		$currentGroup = $GLOBALS['organizrUser']['groupID'];
+	} else {
+		$currentUser = 'Guest';
+		$currentGroup = getUserLevel();
+	}
+	$userInfo = "User: $currentUser | Group: $currentGroup | IP: $currentIP | Requesting Access to Group $group | Result: ";
+	if ($whitelist) {
+		if (in_array($currentIP, arrayIP($whitelist))) {
+			!$debug ? exit(http_response_code(200)) : die("$userInfo Whitelist Authorized");
+		}
+	}
+	if ($blacklist) {
+		if (in_array($currentIP, arrayIP($blacklist))) {
+			!$debug ? exit(http_response_code(401)) : die("$userInfo Blacklisted");
+		}
+	}
+	if ($group !== null) {
+		if (qualifyRequest($group) && $unlocked) {
+			!$debug ? exit(http_response_code(200)) : die("$userInfo Authorized");
+		} else {
+			!$debug ? exit(http_response_code(401)) : die("$userInfo Not Authorized");
+		}
+	} else {
+		!$debug ? exit(http_response_code(401)) : die("Not Authorized Due To No Parameters Set");
+	}
+}
+
+function logoOrText()
+{
+	if ($GLOBALS['useLogo'] == false) {
+		return '<h1>' . $GLOBALS['title'] . '</h1>';
+	} else {
+		return '<img class="loginLogo" src="' . $GLOBALS['logo'] . '" alt="Home" />';
+	}
+}
+
+function showLogin()
+{
+	if ($GLOBALS['hideRegistration'] == false) {
+		return '<p><span lang="en">Don\'t have an account?</span><a href="#" class="text-primary m-l-5 to-register"><b lang="en">Sign Up</b></a></p>';
+	}
+}
+
+function showoAuth()
+{
+	$buttons = '';
+	if ($GLOBALS['plexoAuth']) {
+		$buttons .= '<a href="javascript:void(0)" onclick="oAuthStart(\'plex\')" class="btn btn-lg btn-block text-uppercase waves-effect waves-light bg-plex text-muted" data-toggle="tooltip" title="" data-original-title="Login with Plex"> <span>Login with Plex Account</span><i aria-hidden="true" class="mdi mdi-plex m-l-5"></i> </a>';
+	}
+	return ($buttons) ? '
+		<div class="row">
+            <div class="col-xs-12 col-sm-12 col-md-12 m-t-10 text-center">
+                <div class="social">' . $buttons . '</div>
+            </div>
+        </div>
+	' : '';
+}
+
+function getImages()
+{
+	$dirname = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'tabs' . DIRECTORY_SEPARATOR;
+	$path = 'plugins/images/tabs/';
+	$images = scandir($dirname);
+	$ignore = array(".", "..", "._.DS_Store", ".DS_Store", ".pydio_id");
+	$allIcons = array();
+	foreach ($images as $image) {
+		if (!in_array($image, $ignore)) {
+			$allIcons[] = $path . $image;
+		}
+	}
+	return $allIcons;
+}
+
+function imageSelect($form)
+{
+	$i = 1;
+	$images = getImages();
+	$return = '<select class="form-control tabIconImageList" id="' . $form . '-chooseImage" name="chooseImage"><option lang="en">Select or type Icon</option>';
+	foreach ($images as $image) {
+		$i++;
+		$return .= '<option value="' . $image . '">' . basename($image) . '</option>';
+	}
+	return $return . '</select>';
+}
+
+function editImages()
+{
+	$array = array();
+	$postCheck = array_filter($_POST);
+	$filesCheck = array_filter($_FILES);
+	if (!empty($postCheck)) {
+		if ($_POST['data']['action'] == 'deleteImage') {
+			if (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . $_POST['data']['imagePath'])) {
+				writeLog('success', 'Image Manager Function -  Deleted Image [' . $_POST['data']['imageName'] . ']', $GLOBALS['organizrUser']['username']);
+				return (unlink(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . $_POST['data']['imagePath'])) ? true : false;
+			}
+		}
+	}
+	if (!empty($filesCheck)) {
+		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 . 'tabs' . DIRECTORY_SEPARATOR;
+		$targetFile = $targetPath . $_FILES['file']['name'];
+		return (move_uploaded_file($tempFile, $targetFile)) ? true : false;
+	}
+	return false;
+}
+
+function getThemes()
+{
+	$themes = array();
+	foreach (glob(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'css' . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . "*.css") as $filename) {
+		$themes[] = array(
+			'name' => preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($filename)),
+			'value' => preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($filename))
+		);
+	}
+	return $themes;
+}
+
+function getSounds()
+{
+	$sounds = array();
+	foreach (glob(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'sounds' . DIRECTORY_SEPARATOR . 'default' . DIRECTORY_SEPARATOR . "*.mp3") as $filename) {
+		$sounds[] = array(
+			'name' => preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($filename)),
+			'value' => preg_replace('/\\.[^.\\s]{3,4}$/', '', 'plugins/sounds/default/' . basename($filename) . '.mp3')
+		);
+	}
+	foreach (glob(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'sounds' . DIRECTORY_SEPARATOR . 'custom' . DIRECTORY_SEPARATOR . "*.mp3") as $filename) {
+		$sounds[] = array(
+			'name' => preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($filename)),
+			'value' => preg_replace('/\\.[^.\\s]{3,4}$/', '', 'plugins/sounds/custom/' . basename($filename) . '.mp3')
+		);
+	}
+	return $sounds;
+}
+
+function getBranches()
+{
+	return array(
+		array(
+			'name' => 'Develop',
+			'value' => 'v2-develop'
+		),
+		array(
+			'name' => 'Master',
+			'value' => 'v2-master'
+		)
+	);
+}
+
+function getAuthTypes()
+{
+	return array(
+		array(
+			'name' => 'Organizr DB',
+			'value' => 'internal'
+		),
+		array(
+			'name' => 'Organizr DB + Backend',
+			'value' => 'both'
+		),
+		array(
+			'name' => 'Backend Only',
+			'value' => 'external'
+		)
+	);
+}
+
+function getAuthBackends()
+{
+	$backendOptions = array();
+	$backendOptions[] = array(
+		'name' => 'Choose Backend',
+		'value' => false,
+		'disabled' => true
+	);
+	foreach (array_filter(get_defined_functions()['user'], function ($v) {
+		return strpos($v, 'plugin_auth_') === 0;
+	}) as $value) {
+		$name = str_replace('plugin_auth_', '', $value);
+		if (strpos($name, 'disabled') === false) {
+			$backendOptions[] = array(
+				'name' => ucwords(str_replace('_', ' ', $name)),
+				'value' => $name
+			);
+		} else {
+			$backendOptions[] = array(
+				'name' => $value(),
+				'value' => 'none',
+				'disabled' => true,
+			);
+		}
+	}
+	ksort($backendOptions);
+	return $backendOptions;
+}
+
+function wizardPath($array)
+{
+	$path = $array['data']['path'];
+	if (file_exists($path)) {
+		if (is_writable($path)) {
+			return true;
+		}
+	} else {
+		if (is_writable(dirname($path, 1))) {
+			if (mkdir($path, 0760, true)) {
+				return true;
+			}
+		}
+	}
+	return 'permissions';
+}
+
+function groupSelect()
+{
+	$groups = allGroups();
+	$select = array();
+	foreach ($groups as $key => $value) {
+		$select[] = array(
+			'name' => $value['group'],
+			'value' => $value['group_id']
+		);
+	}
+	return $select;
+}
+
+function getImage()
+{
+	$refresh = false;
+	$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+	if (!file_exists($cacheDirectory)) {
+		mkdir($cacheDirectory, 0777, true);
+	}
+	@$image_url = $_GET['img'];
+	@$key = $_GET['key'];
+	@$image_height = $_GET['height'];
+	@$image_width = $_GET['width'];
+	@$source = $_GET['source'];
+	@$itemType = $_GET['type'];
+	if (strpos($key, '$') !== false) {
+		$key = explode('$', $key)[0];
+		$refresh = true;
+	}
+	switch ($source) {
+		case 'plex':
+			$plexAddress = qualifyURL($GLOBALS['plexURL']);
+			$image_src = $plexAddress . '/photo/:/transcode?height=' . $image_height . '&width=' . $image_width . '&upscale=1&url=' . $image_url . '&X-Plex-Token=' . $GLOBALS['plexToken'];
+			break;
+		case 'emby':
+			$embyAddress = qualifyURL($GLOBALS['embyURL']);
+			$imgParams = array();
+			if (isset($_GET['height'])) {
+				$imgParams['height'] = 'maxHeight=' . $_GET['height'];
+			}
+			if (isset($_GET['width'])) {
+				$imgParams['width'] = 'maxWidth=' . $_GET['width'];
+			}
+			$image_src = $embyAddress . '/Items/' . $image_url . '/Images/' . $itemType . '?' . implode('&', $imgParams);
+			break;
+		default:
+			# code...
+			break;
+	}
+	if (isset($image_url) && isset($image_height) && isset($image_width) && isset($image_src)) {
+		$cachefile = $cacheDirectory . $key . '.jpg';
+		$cachetime = 604800;
+		// Serve from the cache if it is younger than $cachetime
+		if (file_exists($cachefile) && time() - $cachetime < filemtime($cachefile) && $refresh == false) {
+			header("Content-type: image/jpeg");
+			//@readfile($cachefile);
+			echo @curl('get', $cachefile)['content'];
+			exit;
+		}
+		ob_start(); // Start the output buffer
+		header('Content-type: image/jpeg');
+		//@readfile($image_src);
+		echo @curl('get', $image_src)['content'];
+		// Cache the output to a file
+		$fp = fopen($cachefile, 'wb');
+		fwrite($fp, ob_get_contents());
+		fclose($fp);
+		ob_end_flush(); // Send the output to the browser
+		die();
+	} else {
+		die("Invalid Request");
+	}
+}
+
+function cacheImage($url, $name)
+{
+	$cacheDirectory = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;
+	if (!file_exists($cacheDirectory)) {
+		mkdir($cacheDirectory, 0777, true);
+	}
+	$cachefile = $cacheDirectory . $name . '.jpg';
+	@copy($url, $cachefile);
+}
+
+function downloader($array)
+{
+	switch ($array['data']['source']) {
+		case 'sabnzbd':
+			switch ($array['data']['action']) {
+				case 'resume':
+				case 'pause':
+					sabnzbdAction($array['data']['action'], $array['data']['target']);
+					break;
+				default:
+					# code...
+					break;
+			}
+			break;
+		case 'nzbget':
+			break;
+		default:
+			# code...
+			break;
+	}
+}
+
+function sabnzbdAction($action = null, $target = null)
+{
+	if ($GLOBALS['homepageSabnzbdEnabled'] && !empty($GLOBALS['sabnzbdURL']) && !empty($GLOBALS['sabnzbdToken']) && qualifyRequest($GLOBALS['homepageSabnzbdAuth'])) {
+		$url = qualifyURL($GLOBALS['sabnzbdURL']);
+		switch ($action) {
+			case 'pause':
+				$id = ($target !== '' && $target !== 'main' && isset($target)) ? 'mode=queue&name=pause&value=' . $target . '&' : 'mode=pause';
+				$url = $url . '/api?' . $id . '&output=json&apikey=' . $GLOBALS['sabnzbdToken'];
+				break;
+			case 'resume':
+				$id = ($target !== '' && $target !== 'main' && isset($target)) ? 'mode=queue&name=resume&value=' . $target . '&' : 'mode=resume';
+				$url = $url . '/api?' . $id . '&output=json&apikey=' . $GLOBALS['sabnzbdToken'];
+				break;
+			default:
+				# code...
+				break;
+		}
+		try {
+			$options = (localURL($url)) ? array('verify' => false) : array();
+			$response = Requests::get($url, array(), $options);
+			if ($response->success) {
+				$api['content'] = json_decode($response->body, true);
+			}
+		} catch (Requests_Exception $e) {
+			writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		};
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		return $api;
+	}
+}
+
+// Deluge API isn't working ATM - will get with dev.
+function delugeAction($action = null, $target = null)
+{
+	if ($GLOBALS['homepageDelugeEnabled'] && !empty($GLOBALS['delugeURL']) && !empty($GLOBALS['delugePassword']) && qualifyRequest($GLOBALS['homepageDelugeAuth'])) {
+		$url = qualifyURL($GLOBALS['delugeURL']);
+		try {
+			$deluge = new deluge($GLOBALS['delugeURL'], decrypt($GLOBALS['delugePassword']));
+			switch ($action) {
+				case 'pause':
+					$torrents = $deluge->pauseTorrent($target);
+					break;
+				case 'pauseAll':
+					$torrents = $deluge->pauseAllTorrents();
+					break;
+				case 'resume':
+					$torrents = $deluge->resumeTorrent($target);
+					break;
+				case 'resumeAll':
+					$torrents = $deluge->resumeAllTorrents();
+					break;
+				default:
+					# code...
+					break;
+			}
+			$api['content'] = $torrents;
+		} catch (Excecption $e) {
+			writeLog('error', 'Deluge Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+		}
+		$api['content'] = isset($api['content']) ? $api['content'] : false;
+		return $api;
+	}
+	return false;
+}
+
+function getOrgUsers()
+{
+	$result = allUsers();
+	if (is_array($result) || is_object($result)) {
+		foreach ($result['users'] as $k => $v) {
+			$return[$v['username']] = $v['email'];
+		}
+		return $return;
+	}
+}
+
+function convertPlexName($user, $type)
+{
+	$array = userList('plex');
+	switch ($type) {
+		case "username":
+		case "u":
+			$plexUser = array_search($user, $array['users']);
+			break;
+		case "id":
+			if (array_key_exists(strtolower($user), $array['users'])) {
+				$plexUser = $array['users'][strtolower($user)];
+			}
+			break;
+		default:
+			$plexUser = false;
+	}
+	return (!empty($plexUser) ? $plexUser : null);
+}
+
+function userList($type = null)
+{
+	switch ($type) {
+		case 'plex':
+			if (!empty($GLOBALS['plexToken']) && !empty($GLOBALS['plexID'])) {
+				$url = 'https://plex.tv/api/servers/' . $GLOBALS['plexID'] . '/shared_servers';
+				try {
+					$headers = array(
+						"Accept" => "application/json",
+						"X-Plex-Token" => $GLOBALS['plexToken']
+					);
+					$response = Requests::get($url, $headers, array());
+					libxml_use_internal_errors(true);
+					if ($response->success) {
+						$libraryList = array();
+						$plex = simplexml_load_string($response->body);
+						foreach ($plex->SharedServer as $child) {
+							if (!empty($child['username'])) {
+								$username = (string)strtolower($child['username']);
+								$email = (string)strtolower($child['email']);
+								$libraryList['users'][$username] = (string)$child['id'];
+								$libraryList['emails'][$email] = (string)$child['id'];
+								$libraryList['both'][$username] = $email;
+							}
+						}
+						$libraryList = array_change_key_case($libraryList, CASE_LOWER);
+						return $libraryList;
+					}
+				} catch (Requests_Exception $e) {
+					writeLog('error', 'Plex Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				};
+			}
+			break;
+		default:
+			# code...
+			break;
+	}
+	return false;
+}
+
+function libraryList($type = null)
+{
+	switch ($type) {
+		case 'plex':
+			if (!empty($GLOBALS['plexToken']) && !empty($GLOBALS['plexID'])) {
+				$url = 'https://plex.tv/api/servers/' . $GLOBALS['plexID'];
+				try {
+					$headers = array(
+						"Accept" => "application/json",
+						"X-Plex-Token" => $GLOBALS['plexToken']
+					);
+					$response = Requests::get($url, $headers, array());
+					libxml_use_internal_errors(true);
+					if ($response->success) {
+						$libraryList = array();
+						$plex = simplexml_load_string($response->body);
+						foreach ($plex->Server->Section as $child) {
+							$libraryList['libraries'][(string)$child['title']] = (string)$child['id'];
+						}
+						$libraryList = array_change_key_case($libraryList, CASE_LOWER);
+						return $libraryList;
+					}
+				} catch (Requests_Exception $e) {
+					writeLog('error', 'Plex Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+				};
+			}
+			break;
+		default:
+			# code...
+			break;
+	}
+	return false;
+}
+
+function plexJoinAPI($array)
+{
+	return plexJoin($array['data']['username'], $array['data']['email'], $array['data']['password']);
+}
+
+function plexJoin($username, $email, $password)
+{
+	try {
+		$url = 'https://plex.tv/users.json';
+		$headers = array(
+			'Accept' => 'application/json',
+			'Content-Type' => 'application/x-www-form-urlencoded',
+			'X-Plex-Product' => 'Organizr',
+			'X-Plex-Version' => '2.0',
+			'X-Plex-Client-Identifier' => $GLOBALS['uuid'],
+		);
+		$data = array(
+			'user[email]' => $email,
+			'user[username]' => $username,
+			'user[password]' => $password,
+		);
+		$response = Requests::post($url, $headers, $data, array());
+		$json = json_decode($response->body, true);
+		$errors = (!empty($json['errors']) ? true : false);
+		$success = (!empty($json['user']) ? true : false);
+		//Use This for later
+		$usernameError = (!empty($json['errors']['username']) ? $json['errors']['username'][0] : false);
+		$emailError = (!empty($json['errors']['email']) ? $json['errors']['email'][0] : false);
+		$passwordError = (!empty($json['errors']['password']) ? $json['errors']['password'][0] : false);
+		$errorMessage = "";
+		if ($errors) {
+			if ($usernameError) {
+				$errorMessage .= "[Username Error: " . $usernameError . "]";
+			}
+			if ($emailError) {
+				$errorMessage .= "[Email Error: " . $emailError . "]";
+			}
+			if ($passwordError) {
+				$errorMessage .= "[Password Error: " . $passwordError . "]";
+			}
+		}
+		return (!empty($success) && empty($errors) ? true : $errorMessage);
+	} catch (Requests_Exception $e) {
+		writeLog('error', 'Plex.TV Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+	};
+	return false;
+}
+
+function checkFrame($array, $url)
+{
+	if (array_key_exists("x-frame-options", $array)) {
+		if ($array['x-frame-options'] == "deny") {
+			return false;
+		} elseif ($array['x-frame-options'] == "sameorgin") {
+			$digest = parse_url($url);
+			$host = (isset($digest['host']) ? $digest['host'] : '');
+			if (getServer() == $host) {
+				return true;
+			} else {
+				return false;
+			}
+		}
+	} else {
+		if (!$array) {
+			return false;
+		}
+		return true;
+	}
+}
+
+function frameTest($url)
+{
+	$array = array_change_key_case(get_headers(qualifyURL($url), 1));
+	$url = qualifyURL($url);
+	if (checkFrame($array, $url)) {
+		return true;
+	} else {
+		return false;
+	}
+}
+
+function ping($pings)
+{
+	if (qualifyRequest($GLOBALS['pingAuth'])) {
+		$type = gettype($pings);
+		$ping = new Ping("");
+		$ping->setTtl(128);
+		$ping->setTimeout(2);
+		switch ($type) {
+			case "array":
+				$results = [];
+				foreach ($pings as $k => $v) {
+					if (strpos($v, ':') !== false) {
+						$domain = explode(':', $v)[0];
+						$port = explode(':', $v)[1];
+						$ping->setHost($domain);
+						$ping->setPort($port);
+						$latency = $ping->ping('fsockopen');
+					} else {
+						$ping->setHost($v);
+						$latency = $ping->ping();
+					}
+					if ($latency || $latency === 0) {
+						$results[$v] = $latency;
+					} else {
+						$results[$v] = false;
+					}
+				}
+				break;
+			case "string":
+				if (strpos($pings, ':') !== false) {
+					$domain = explode(':', $pings)[0];
+					$port = explode(':', $pings)[1];
+					$ping->setHost($domain);
+					$ping->setPort($port);
+					$latency = $ping->ping('fsockopen');
+				} else {
+					$ping->setHost($pings);
+					$latency = $ping->ping();
+				}
+				if ($latency || $latency === 0) {
+					$results = $latency;
+				} else {
+					$results = false;
+				}
+				break;
+		}
+		return $results;
+	}
+	return false;
+}
+
+function guestHash($start, $end)
+{
+	$ip = $_SERVER['REMOTE_ADDR'];
+	$ip = md5($ip);
+	return substr($ip, $start, $end);
+}
+
+function importUserButtons()
+{
+	$emptyButtons = '
+		<div class="col-md-12">
+            <div class="white-box bg-org">
+                <h3 class="box-title m-0" lang="en">Currently User import is available for Plex only.</h3> </div>
+        </div>
+	';
+	$buttons = '';
+	if (!empty($GLOBALS['plexToken'])) {
+		$buttons .= '<button class="btn bg-plex text-muted waves-effect waves-light importUsersButton" onclick="importUsers(\'plex\')" type="button"><span class="btn-label"><i class="mdi mdi-plex"></i></span><span lang="en">Import Plex Users</span></button>';
+	}
+	return ($buttons !== '') ? $buttons : $emptyButtons;
+}
+
+function settingsDocker()
+{
+	$type = ($GLOBALS['docker']) ? 'Official Docker' : 'Native';
+	return '<li><div class="bg-info"><i class="mdi mdi-flag mdi-24px text-white"></i></div><span class="text-muted hidden-xs" lang="en">Install Type</span> ' . $type . '</li>';
+}

+ 328 - 0
api/functions/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);
+	}
+}

+ 81 - 0
api/functions/plugin-functions.php

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

+ 89 - 0
api/functions/sso-functions.php

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

+ 109 - 0
api/functions/static-globals.php

@@ -0,0 +1,109 @@
+<?php
+// ===================================
+// Organizr Version
+$GLOBALS['installedVersion'] = '2.0.0-beta.950';
+// ===================================
+// Quick php Version check
+$GLOBALS['minimumPHP'] = '7.0.0';
+if (!(version_compare(PHP_VERSION, $GLOBALS['minimumPHP']) >= 0)) {
+	die('Organizr needs PHP Version: ' . $GLOBALS['minimumPHP'] . '<br/> You have PHP Version: ' . PHP_VERSION);
+}
+// Set GLOBALS from config file
+$GLOBALS['userConfigPath'] = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.php';
+$GLOBALS['defaultConfigPath'] = dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'default.php';
+$GLOBALS['currentTime'] = gmdate("Y-m-d\TH:i:s\Z");
+$GLOBALS['docker'] = (file_exists(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'Docker.txt')) ? true : false;
+// Quick function for plugins
+function pluginFiles($type)
+{
+	$files = '';
+	switch ($type) {
+		case 'js':
+			foreach (glob(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . "*.js") as $filename) {
+				$files .= '<script src="api/plugins/js/' . basename($filename) . '?v=' . $GLOBALS['installedVersion'] . '" defer="true"></script>';
+			}
+			break;
+		case 'css':
+			foreach (glob(dirname(__DIR__, 1) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'css' . DIRECTORY_SEPARATOR . "*.js") as $filename) {
+				$files .= '<link href="api/plugins/css/' . basename($filename) . $GLOBALS['installedVersion'] . '" rel="stylesheet">';
+			}
+			break;
+		default:
+			break;
+	}
+	return $files;
+}
+
+function loadConfigOnce($path = null)
+{
+	$path = ($path) ? $path : $GLOBALS['userConfigPath'];
+	if (!is_file($path)) {
+		return null;
+	} else {
+		return (array)call_user_func(function () use ($path) {
+			return include($path);
+		});
+	}
+}
+
+function favIcons()
+{
+	$favicon = '
+	<link rel="apple-touch-icon" sizes="180x180" href="plugins/images/favicon/apple-touch-icon.png">
+	<link rel="icon" type="image/png" sizes="32x32" href="plugins/images/favicon/favicon-32x32.png">
+	<link rel="icon" type="image/png" sizes="16x16" href="plugins/images/favicon/favicon-16x16.png">
+	<link rel="manifest" href="plugins/images/favicon/site.webmanifest">
+	<link rel="mask-icon" href="plugins/images/favicon/safari-pinned-tab.svg" color="#5bbad5">
+	<link rel="shortcut icon" href="plugins/images/favicon/favicon.ico">
+	<meta name="msapplication-TileColor" content="#da532c">
+	<meta name="msapplication-TileImage" content="plugins/images/favicon/mstile-144x144.png">
+	<meta name="msapplication-config" content="plugins/images/favicon/browserconfig.xml">
+	<meta name="theme-color" content="#ffffff">
+	';
+	if (file_exists($GLOBALS['userConfigPath'])) {
+		$config = loadConfigOnce($GLOBALS['userConfigPath']);
+		if (isset($config['favIcon'])) {
+			if ($config['favIcon'] !== '') {
+				$favicon = $config['favIcon'];
+			}
+		}
+	}
+	return $favicon;
+}
+
+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' => matchBrackets(basename($filename))
+			);
+		}
+	}
+	usort($files, function ($a, $b) {
+		return $a['language'] <=> $b['language'];
+	});
+	return ($encode) ? json_encode($files) : $files;
+}
+
+function matchBrackets($text, $brackets = 's')
+{
+	switch ($brackets) {
+		case 's':
+		case 'square':
+			$pattern = '#\[(.*?)\]#';
+			break;
+		case 'c':
+		case 'curly':
+			$pattern = '#\((.*?)\)#';
+			break;
+		default:
+			return null;
+	}
+	preg_match($pattern, $text, $match);
+	return $match[1];
+}

+ 81 - 0
api/functions/theme-functions.php

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

+ 173 - 0
api/functions/token-functions.php

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

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

@@ -0,0 +1,184 @@
+<?php
+// Upgrade the installation
+function upgradeInstall($branch = 'v2-master', $stage)
+{
+	ini_set('max_execution_time', 0);
+	set_time_limit(0);
+	$url = 'https://github.com/causefx/Organizr/archive/' . $branch . '.zip';
+	$file = "upgrade.zip";
+	$source = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'upgrade' . DIRECTORY_SEPARATOR . 'Organizr-' . str_replace('v2', '2', $branch) . DIRECTORY_SEPARATOR;
+	$cleanup = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . "upgrade" . DIRECTORY_SEPARATOR;
+	$destination = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR;
+	switch ($stage) {
+		case '1':
+			writeLog('success', 'Update Function -  Started Upgrade Process', $GLOBALS['organizrUser']['username']);
+			if (downloadFile($url, $file)) {
+				writeLog('success', 'Update Function -  Downloaded Update File for Branch: ' . $branch, $GLOBALS['organizrUser']['username']);
+				return true;
+			} else {
+				writeLog('error', 'Update Function -  Downloaded Update File Failed  for Branch: ' . $branch, $GLOBALS['organizrUser']['username']);
+				return false;
+			}
+			break;
+		case '2':
+			if (unzipFile($file)) {
+				writeLog('success', 'Update Function -  Unzipped Update File for Branch: ' . $branch, $GLOBALS['organizrUser']['username']);
+				return true;
+			} else {
+				writeLog('error', 'Update Function -  Unzip Failed for Branch: ' . $branch, $GLOBALS['organizrUser']['username']);
+				return false;
+			}
+			break;
+		case '3':
+			if (rcopy($source, $destination)) {
+				writeLog('success', 'Update Function -  Overwrited Files using Updated Files from Branch: ' . $branch, $GLOBALS['organizrUser']['username']);
+				$updateComplete = $GLOBALS['dbLocation'] . 'completed.txt';
+				if (!file_exists($updateComplete)) {
+					touch($updateComplete);
+				}
+				return true;
+			} else {
+				writeLog('error', 'Update Function -  Overwrite Failed for Branch: ' . $branch, $GLOBALS['organizrUser']['username']);
+				return false;
+			}
+			break;
+		case '4':
+			if (rrmdir($cleanup)) {
+				writeLog('success', 'Update Function -  Deleted Update Files from Branch: ' . $branch, $GLOBALS['organizrUser']['username']);
+				writeLog('success', 'Update Function -  Update Completed', $GLOBALS['organizrUser']['username']);
+				return true;
+			} else {
+				writeLog('error', 'Update Function -  Removal of Update Files Failed for Branch: ' . $branch, $GLOBALS['organizrUser']['username']);
+				return false;
+			}
+			break;
+		default:
+			return false;
+			break;
+	}
+	return false;
+}
+
+function downloadFile($url, $path)
+{
+	ini_set('max_execution_time', 0);
+	set_time_limit(0);
+	$folderPath = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . "upgrade" . DIRECTORY_SEPARATOR;
+	if (!mkdir($folderPath)) {
+		//writeLog("error", "organizr could not create upgrade folder");
+	}
+	$newfname = $folderPath . $path;
+	$file = fopen($url, 'rb');
+	if ($file) {
+		$newf = fopen($newfname, 'wb');
+		if ($newf) {
+			while (!feof($file)) {
+				fwrite($newf, fread($file, 1024 * 8), 1024 * 8);
+			}
+		}
+	} else {
+		//writeLog("error", "organizr could not download $url");
+	}
+	if ($file) {
+		fclose($file);
+		//writeLog("success", "organizr finished downloading the github zip file");
+	} else {
+		//writeLog("error", "organizr could not download the github zip file");
+	}
+	if ($newf) {
+		fclose($newf);
+		//writeLog("success", "organizr created upgrade zip file from github zip file");
+	} else {
+		//writeLog("error", "organizr could not create upgrade zip file from github zip file");
+	}
+	return true;
+}
+
+function downloadFileToPath($from, $to, $path)
+{
+	ini_set('max_execution_time', 0);
+	set_time_limit(0);
+	if (@!mkdir($path, 0777, true)) {
+		//writeLog("error", "organizr could not create upgrade folder");
+	}
+	$file = fopen($from, 'rb');
+	if ($file) {
+		$newf = fopen($to, 'wb');
+		if ($newf) {
+			while (!feof($file)) {
+				fwrite($newf, fread($file, 1024 * 8), 1024 * 8);
+			}
+		}
+	} else {
+		//writeLog("error", "organizr could not download $url");
+	}
+	if ($file) {
+		fclose($file);
+		//writeLog("success", "organizr finished downloading the github zip file");
+	} else {
+		//writeLog("error", "organizr could not download the github zip file");
+	}
+	if ($newf) {
+		fclose($newf);
+		//writeLog("success", "organizr created upgrade zip file from github zip file");
+	} else {
+		//writeLog("error", "organizr could not create upgrade zip file from github zip file");
+	}
+	return true;
+}
+
+function unzipFile($zipFile)
+{
+	ini_set('max_execution_time', 0);
+	set_time_limit(0);
+	$zip = new ZipArchive;
+	$extractPath = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . "upgrade/";
+	if ($zip->open($extractPath . $zipFile) != "true") {
+		//writeLog("error", "organizr could not unzip upgrade.zip");
+	} else {
+		//writeLog("success", "organizr unzipped upgrade.zip");
+	}
+	/* Extract Zip File */
+	$zip->extractTo($extractPath);
+	$zip->close();
+	return true;
+}
+
+// Function to remove folders and files
+function rrmdir($dir)
+{
+	ini_set('max_execution_time', 0);
+	set_time_limit(0);
+	if (is_dir($dir)) {
+		$files = scandir($dir);
+		foreach ($files as $file) {
+			if ($file != "." && $file != "..") {
+				rrmdir("$dir/$file");
+			}
+		}
+		rmdir($dir);
+	} elseif (file_exists($dir)) {
+		unlink($dir);
+	}
+	return true;
+}
+
+// Function to Copy folders and files
+function rcopy($src, $dst)
+{
+	ini_set('max_execution_time', 0);
+	set_time_limit(0);
+	if (is_dir($src)) {
+		if (!file_exists($dst)) : mkdir($dst);
+		endif;
+		$files = scandir($src);
+		foreach ($files as $file) {
+			if ($file != "." && $file != "..") {
+				rcopy("$src/$file", "$dst/$file");
+			}
+		}
+	} elseif (file_exists($src)) {
+		copy($src, $dst);
+	}
+	return true;
+}

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

@@ -0,0 +1,53 @@
+<?php
+function upgradeCheck()
+{
+	if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
+		$tempLock = $GLOBALS['dbLocation'] . 'DBLOCK.txt';
+		$updateComplete = $GLOBALS['dbLocation'] . 'completed.txt';
+		$cleanup = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . "upgrade" . DIRECTORY_SEPARATOR;
+		if (file_exists($updateComplete)) {
+			@unlink($updateComplete);
+			@rrmdir($cleanup);
+		}
+		if (file_exists($tempLock)) {
+			die('upgrading');
+		}
+		$updateDB = false;
+		$updateSuccess = true;
+		$compare = new Composer\Semver\Comparator;
+		$oldVer = $GLOBALS['configVersion'];
+		// Upgrade check start for version below
+		$versionCheck = '2.0.0-beta-200';
+		if ($compare->lessThan($oldVer, $versionCheck)) {
+			$updateDB = true;
+			$oldVer = $versionCheck;
+		}
+		// End Upgrade check start for version above
+		// Upgrade check start for version below
+		$versionCheck = '2.0.0-beta-500';
+		if ($compare->lessThan($oldVer, $versionCheck)) {
+			$updateDB = true;
+			$oldVer = $versionCheck;
+		}
+		// End Upgrade check start for version above
+		$versionCheck = '2.0.0-beta-800';
+		if ($compare->lessThan($oldVer, $versionCheck)) {
+			$updateDB = true;
+			$oldVer = $versionCheck;
+		}
+		// End Upgrade check start for version above
+		if ($updateDB == true) {
+			//return 'Upgraded Needed - Current Version '.$oldVer.' - New Version: '.$versionCheck;
+			// Upgrade database to latest version
+			$updateSuccess = (updateDB($oldVer)) ? true : false;
+		}
+		// Update config.php version if different to the installed version
+		if ($updateSuccess && $GLOBALS['installedVersion'] !== $GLOBALS['configVersion']) {
+			updateConfig(array('apply_CONFIG_VERSION' => $GLOBALS['installedVersion']));
+		}
+		if ($updateSuccess == false) {
+			die('Database update failed - Please manually check logs and fix - Then reload this page');
+		}
+		return true;
+	}
+}

+ 1297 - 0
api/index.php

@@ -0,0 +1,1297 @@
+<?php
+$generationTime = -microtime(true);
+//include functions
+require_once 'functions.php';
+//Set result array
+$result = array();
+//Get request method
+$method = $_SERVER['REQUEST_METHOD'];
+$pretty = isset($_GET['pretty']) ? true : false;
+reset($_GET);
+$function = (key($_GET) ? str_replace("/", "_", key($_GET)) : false);
+//Exit if $function is blank
+if ($function === false) {
+	$result['status'] = "error";
+	$result['statusText'] = "No API Path Supplied";
+	exit(json_encode($result));
+}
+$result['request'] = key($_GET);
+$result['params'] = $_POST;
+switch ($function) {
+	case 'v1_settings_page':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = $pageSettings;
+					writeLog('success', 'Admin Function -  Accessed Settings Page', $GLOBALS['organizrUser']['username']);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+					writeLog('error', 'Admin Function -  Tried to access Settings Page', $GLOBALS['organizrUser']['username']);
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_homepage_page':
+		switch ($method) {
+			case 'GET':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = $pageHomepage;
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_settings_plugins':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = $pageSettingsPlugins;
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_settings_tab_editor_homepage':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = $pageSettingsTabEditorHomepage;
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_settings_tab_editor_homepage_order':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = $pageSettingsTabEditorHomepageOrder;
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_settings_homepage_list':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = getHomepageList();
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = editPlugins($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_settings_plugins_list':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = getPlugins();
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = editPlugins($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_settings_settings_logs':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = $pageSettingsSettingsLogs;
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_settings_settings_sso':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = $pageSettingsSettingsSSO;
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_settings_settings_main':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = $pageSettingsSettingsMain;
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_settings_customize_appearance':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = $pageSettingsCustomizeAppearance;
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = editAppearance($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_remove_file':
+		switch ($method) {
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = removeFile($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_update_config':
+		switch ($method) {
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = updateConfigItem($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_update_config_multiple':
+		switch ($method) {
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = updateConfigMultiple($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_update_config_multiple_form':
+		switch ($method) {
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = updateConfigMultipleForm($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_homepage_connect':
+		switch ($method) {
+			case 'POST':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = homepageConnect($_POST);
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_ping_list':
+		switch ($method) {
+			case 'POST':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = ping($_POST['data']['pingList']);
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_test_api_connection':
+		switch ($method) {
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = testAPIConnection($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_settings_tab_editor_tabs':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = $pageSettingsTabEditorTabs;
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = editTabs($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_settings_tab_editor_categories':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = $pageSettingsTabEditorCategories;
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = editCategories($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_settings_user_manage_users':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = $pageSettingsUserManageUsers;
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = adminEditUser($_POST);
+				} elseif (qualifyRequest(998)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = editUser($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_manage_user':
+		switch ($method) {
+			case 'POST':
+				if (qualifyRequest(998)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = editUser($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_settings_user_manage_groups':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = $pageSettingsUserManageGroups;
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = adminEditGroup($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_settings_image_manager_view':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = $pageSettingsImageManager;
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = editImages();
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_wizard_page':
+		switch ($method) {
+			case 'GET':
+				if (!file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = $pageWizard;
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'Wizard has already been run';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_dependencies_page':
+		switch ($method) {
+			case 'GET':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = $pageDependencies;
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_wizard_config':
+		switch ($method) {
+			case 'POST':
+				if (!file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = wizardConfig($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'Wizard has already been run';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_wizard_path':
+		switch ($method) {
+			case 'POST':
+				if (!file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = wizardPath($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'Wizard has already been run';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_login':
+		switch ($method) {
+			case 'POST':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = login($_POST);
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_register':
+		switch ($method) {
+			case 'POST':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = register($_POST);
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_recover':
+		switch ($method) {
+			case 'POST':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = recover($_POST);
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_unlock':
+		switch ($method) {
+			case 'POST':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = unlock($_POST);
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_lock':
+		switch ($method) {
+			case 'POST':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = lock();
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_test_iframe':
+		switch ($method) {
+			case 'POST':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = frameTest($_POST['data']['url']);
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_upgrade':
+	case 'v1_update':
+	case 'v1_force':
+		switch ($method) {
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = upgradeInstall($_POST['data']['branch'], $_POST['data']['stage']);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_login_page':
+		switch ($method) {
+			case 'GET':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = $pageLogin;
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_lockscreen':
+		switch ($method) {
+			case 'GET':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = $pageLockScreen;
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_login_log':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = getLog('loginLog');
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_organizr_log':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = getLog('org');
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_user_list':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = allUsers();
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_tab_list':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = allTabs();
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_image_list':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = getImages();
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_customize_appearance':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = getCustomizeAppearance();
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_sso':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = getSSO();
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_settings_main':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = getSettingsMain();
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_plugin_install':
+		switch ($method) {
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = installPlugin($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_plugin_remove':
+		switch ($method) {
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = removePlugin($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_theme_install':
+		switch ($method) {
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = installTheme($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_theme_remove':
+		switch ($method) {
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = removeTheme($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_user_edit':
+		switch ($method) {
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = adminEditUser($_POST);
+				} elseif (qualifyRequest(998)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = editUser($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_2fa_create':
+		switch ($method) {
+			case 'POST':
+				if (qualifyRequest(998)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = create2FA($_POST['data']['type']);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_2fa_save':
+		switch ($method) {
+			case 'POST':
+				if (qualifyRequest(998)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = save2FA($_POST['data']['secret'], $_POST['data']['type']);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_2fa_verify':
+		switch ($method) {
+			case 'POST':
+				if (qualifyRequest(998)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = verify2FA($_POST['data']['secret'], $_POST['data']['code'], $_POST['data']['type']);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_2fa_remove':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(998)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = remove2FA();
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_logout':
+		switch ($method) {
+			case 'GET':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = logout();
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_launch_organizr':
+		switch ($method) {
+			case 'GET':
+				$pluginSearch = '-enabled';
+				$pluginInclude = '-include';
+				$status = array();
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$status['status'] = organizrStatus();
+				$result['appearance'] = loadAppearance();
+				$status['user'] = $GLOBALS['organizrUser'];
+				$status['categories'] = loadTabs()['categories'];
+				$status['tabs'] = loadTabs()['tabs'];
+				$status['plugins'] = array_filter($GLOBALS, function ($k) use ($pluginSearch) {
+					return stripos($k, $pluginSearch) !== false;
+				}, ARRAY_FILTER_USE_KEY);
+				$status['plugins']['includes'] = array_filter($GLOBALS, function ($k) use ($pluginInclude) {
+					return stripos($k, $pluginInclude) !== false;
+				}, ARRAY_FILTER_USE_KEY);
+				$result['data'] = $status;
+				$result['branch'] = $GLOBALS['branch'];
+				$result['theme'] = $GLOBALS['theme'];
+				$result['style'] = $GLOBALS['style'];
+				$result['version'] = $GLOBALS['installedVersion'];
+				$result['sso'] = array(
+					'myPlexAccessToken' => isset($_COOKIE['mpt']) ? $_COOKIE['mpt'] : false,
+					'id_token' => isset($_COOKIE['Auth']) ? $_COOKIE['Auth'] : false
+				);
+				$result['settings'] = organizrSpecialSettings();
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_auth':
+		switch ($method) {
+			case 'GET':
+				auth();
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_plugin':
+		switch ($method) {
+			case 'POST':
+			case 'GET':
+				// Include all plugin api Calls
+				foreach (glob(__DIR__ . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'api' . DIRECTORY_SEPARATOR . "*.php") as $filename) {
+					require_once $filename;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_image':
+		switch ($method) {
+			case 'GET':
+				getImage();
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_downloader':
+		switch ($method) {
+			case 'POST':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = downloader($_POST);
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_import_users':
+		switch ($method) {
+			case 'POST':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = importUsersType($_POST);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_ombi':
+		switch ($method) {
+			case 'POST':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = ombiAPI($_POST);
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_plex_join':
+		switch ($method) {
+			case 'POST':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = plexJoinAPI($_POST);
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_token_revoke':
+		switch ($method) {
+			case 'POST':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = revokeToken($_POST);
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_update_db_manual':
+		switch ($method) {
+			case 'GET':
+				if (qualifyRequest(1)) {
+					$result['status'] = 'success';
+					$result['statusText'] = 'success';
+					$result['data'] = updateDB($GLOBALS['installedVersion']);
+				} else {
+					$result['status'] = 'error';
+					$result['statusText'] = 'API/Token invalid or not set';
+					$result['data'] = null;
+				}
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_version':
+		switch ($method) {
+			case 'GET':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = $GLOBALS['installedVersion'];
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	case 'v1_ping':
+		switch ($method) {
+			case 'GET':
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = 'pong';
+				break;
+			default:
+				$result['status'] = 'error';
+				$result['statusText'] = 'The function requested is not defined for method: ' . $method;
+				break;
+		}
+		break;
+	default:
+		//No Function Available
+		$result['status'] = 'error';
+		$result['statusText'] = 'function requested is not defined';
+		break;
+}
+//Set Default Result
+if (!$result) {
+	$result['status'] = "error";
+	$result['error'] = "An error has occurred";
+}
+$result['generationDate'] = $GLOBALS['currentTime'];
+$generationTime += microtime(true);
+$result['generationTime'] = (sprintf('%f', $generationTime) * 1000) . 'ms';
+//return JSON array
+if ($pretty) {
+	echo '<pre>' . safe_json_encode($result, JSON_PRETTY_PRINT) . '</pre>';
+} else {
+	exit(safe_json_encode($result, JSON_HEX_QUOT | JSON_HEX_TAG));
+}
+

+ 75 - 0
api/pages/dependencies.php

@@ -0,0 +1,75 @@
+<?php
+
+$pageDependencies = '
+<script>
+</script>
+<div class="container-fluid">
+    <div class="row bg-title">
+        <div class="col-lg-3 col-md-4 col-sm-4 col-xs-12">
+            <h4 class="page-title" lang="en">Organizr Dependency Check</h4>
+        </div>
+        <!-- /.col-lg-12 -->
+    </div>
+    <!--.row-->
+    <div class="row">
+        <div class="col-lg-8">
+            <div class="panel panel-danger">
+                <div class="panel-heading"> <i class="ti-alert fa-fw"></i> <span lang="en">Dependencies Missing</span>
+                    <div class="pull-right"><a href="#" data-perform="panel-collapse"><i class="ti-minus"></i></a> <a href="#" data-perform="panel-dismiss"><i class="ti-close"></i></a> </div>
+                </div>
+                <div class="panel-wrapper collapse in" aria-expanded="true">
+                    <div class="panel-body">
+                        <ul class="common-list" id="depenency-info"></ul>
+                    </div>
+                </div>
+            </div>
+        </div>
+		<div class="col-lg-4">
+            <div class="panel panel-info">
+                <div class="panel-heading"> <i class="ti-alert fa-fw"></i> <span lang="en">PHP Version Check</span>
+                    <div class="pull-right"><a href="#" data-perform="panel-collapse"><i class="ti-minus"></i></a> <a href="#" data-perform="panel-dismiss"><i class="ti-close"></i></a> </div>
+                </div>
+                <div class="panel-wrapper collapse in" aria-expanded="true">
+                    <table class="table table-hover">
+                        <tbody>
+                            <tr>
+                                <td id="php-version-check" lang="en">Loading...</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+        </div>
+		<div class="col-lg-4">
+            <div class="panel panel-info">
+                <div class="panel-heading"> <i class="ti-alert fa-fw"></i> <span lang="en">Web Folder</span>
+                    <div class="pull-right"><a href="#" data-perform="panel-collapse"><i class="ti-minus"></i></a> <a href="#" data-perform="panel-dismiss"><i class="ti-close"></i></a> </div>
+                </div>
+                <div class="panel-wrapper collapse in" aria-expanded="true">
+                    <table class="table table-hover">
+                        <tbody>
+                            <tr>
+                                <td>'.dirname(__DIR__,2).'</td>
+                            </tr>
+                            <tr>
+                                <td id="web-folder" lang="en">Loading...</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+        </div>
+        <div class="col-lg-4">
+            <div class="panel panel-info">
+                <div class="panel-heading"> <i class="ti-alert fa-fw"></i> <span lang="en">Browser Information</span>
+                    <div class="pull-right"><a href="#" data-perform="panel-collapse"><i class="ti-minus"></i></a> <a href="#" data-perform="panel-dismiss"><i class="ti-close"></i></a> </div>
+                </div>
+                <div class="panel-wrapper collapse in" id="browser-info" aria-expanded="true"></div>
+            </div>
+        </div>
+
+    </div>
+    <!--./row-->
+</div>
+<!-- /.container-fluid -->
+';

+ 129 - 0
api/pages/homepage.php

@@ -0,0 +1,129 @@
+<?php
+if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
+	$pageHomepage = '
+<script>
+!function($) {
+    "use strict";
+    var CalendarApp = function() {
+        this.$body = $("body");
+        this.$calendar = $("#calendar"),
+        this.$event = ("#calendar-events div.calendar-events"),
+        this.$categoryForm = $("#add-new-event form"),
+        this.$extEvents = $("#calendar-events"),
+        this.$modal = $("#my-event"),
+        this.$saveCategoryBtn = $(".save-category"),
+        this.$calendarObj = null
+    };
+    /* Initializing */
+    CalendarApp.prototype.init = function() {
+        /*  Initialize the calendar  */
+        var date = new Date();
+        var d = date.getDate();
+        var m = date.getMonth();
+        var y = date.getFullYear();
+        var form = "";
+        var today = new Date($.now());
+
+        var $this = this;
+        $this.$calendarObj = $this.$calendar.fullCalendar({
+        	customButtons: {
+			    filterCalendar: {
+			      text: \'Filter\',
+			      click: function() {
+			        $(\'#calendar-filter-modal\').modal(\'show\');
+			      }
+			    }
+			  },
+            defaultView: (activeInfo.mobile) ? "list" : "' . $GLOBALS['calendarDefault'] . '",
+            firstDay: "' . $GLOBALS['calendarFirstDay'] . '",
+            timeFormat: "' . $GLOBALS['calendarTimeFormat'] . '",
+            handleWindowResize: true,
+            header: {
+               left: "prev,next,today",
+               center: "title",
+               right: (activeInfo.mobile) ? "filterCalendar" : "filterCalendar,month,basicWeek,basicDay,list",
+            },
+            views: {
+               basicDay: { buttonText: window.lang.translate("Day"), eventLimit: ' . $GLOBALS['calendarLimit'] . ' },
+               basicWeek: { buttonText: window.lang.translate("Week"), eventLimit: ' . $GLOBALS['calendarLimit'] . ' },
+               month: { buttonText: window.lang.translate("Month"), eventLimit: ' . $GLOBALS['calendarLimit'] . ' },
+               list: { buttonText: window.lang.translate("List"), duration: {days: 15} },
+            },
+            timezone: "local",
+            editable: false,
+            navLinks: true, // can click day/week names to navigate views
+            droppable: false, // this allows things to be dropped onto the calendar !!!
+            selectable: false,
+            height: "auto",
+            eventRender: function eventRender( event, element, view ) {
+                if (typeof filter !== "undefined" && filterDownload !== "undefined") {
+                    if(filter === "all" && filterDownload === "all"){
+                        return (event.imagetypeFilter === event.imagetypeFilter && event.downloadFilter === event.downloadFilter);
+                    }else if(filter !== "all" && filterDownload !== "all"){
+                        return filter === event.imagetypeFilter && filterDownload === event.downloadFilter;
+                    }else if(filter !== "all" && filterDownload === "all"){
+                        return filter === event.imagetypeFilter && event.downloadFilter === event.downloadFilter;
+                    }else if(filter === "all" && filterDownload !== "all"){
+                        return event.imagetypeFilter === event.imagetypeFilter && filterDownload === event.downloadFilter;
+                    }
+                }else {
+                    return event.imagetypeFilter === event.imagetypeFilter && event.downloadFilter === event.downloadFilter;
+                }
+            },
+        });
+    },
+   //init CalendarApp
+    $.CalendarApp = new CalendarApp, $.CalendarApp.Constructor = CalendarApp
+}(window.jQuery),
+//initializing CalendarApp
+function($) {
+    "use strict";
+    $.CalendarApp.init()
+}(window.jQuery);
+</script>
+<div class="container-fluid p-t-30" id="homepage-items">
+    ' . buildHomepage() . '
+</div>
+<div id="open-youtube" class="white-popup mfp-with-anim mfp-hide">
+    <div class="col-md-8 col-md-offset-2 youtube-div">  </div>
+</div>
+<!-- /.container-fluid -->
+<!--  modal content -->
+<div id="calendar-filter-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true" style="display: none;">
+    <div class="modal-dialog modal-sm">
+        <div class="modal-content">
+            <div class="modal-header">
+                <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+                <h4 class="modal-title" id="mySmallModalLabel" lane="en">Filter Calendar</h4> </div>
+            <div class="modal-body">
+            	<div class="row">
+                    
+                    <div class="col-md-12">
+                        <label class="control-label" lang="en">Choose Media Type</label>
+                        <select class="form-control form-white" data-placeholder="Choose media type" id="choose-calender-filter">
+                            <option value="all" lang="en">All</option>
+                            <option value="tv" lang="en">TV</option>
+                            <option value="film" lang="en">Movie</option>
+                            <option value="music" lang="en">Music</option>
+                        </select>
+                    </div>
+                    <div class="col-md-12">
+                        <label class="control-label" lang="en">Choose Media Status</label>
+                        <select class="form-control form-white" data-placeholder="Choose media status" id="choose-calender-filter-status">
+                            <option value="all" lang="en">All</option>
+                            <option value="text-success" lang="en">Downloaded</option>
+                            <option value="text-info" lang="en">Unaired</option>
+                            <option value="text-danger" lang="en">Missing</option>
+                            <option value="text-primary animated flash" lang="en">Premier</option>
+                        </select>
+                    </div>
+                </div>
+			</div>
+        </div>
+        <!-- /.modal-content -->
+    </div>
+    <!-- /.modal-dialog -->
+</div>
+<!-- /.modal -->
+';
+}

+ 32 - 0
api/pages/lockscreen.php

@@ -0,0 +1,32 @@
+<?php
+if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
+	$pageLockScreen = '
+<script>
+</script>
+<section id="lockScreen" class="lock-screen" oncontextmenu="return false;" onkeydown="blockDev">
+  <div class="login-box">
+    <div class="white-box">
+      <form class="form-horizontal form-material" id="form-lockscreen" onsubmit="return false;">
+        <div class="form-group">
+          <div class="col-xs-12 text-center">
+            <div class="user-thumb text-center"> <img alt="thumbnail" class="img-circle" width="100" src="' . $GLOBALS['organizrUser']['image'] . '">
+              <h3>' . $GLOBALS['organizrUser']['username'] . '</h3>
+            </div>
+          </div>
+        </div>
+        <div class="form-group ">
+          <div class="col-xs-12">
+            <input id="unlockPassword" name="password" class="form-control" type="password" required="" placeholder="password" lang="en" autofocus>
+          </div>
+        </div>
+        <div class="form-group text-center">
+          <div class="col-xs-12">
+            <button class="btn btn-info btn-lg btn-block text-uppercase waves-effect waves-light unlockButton" type="submit" lang="en">Unlock</button>
+          </div>
+        </div>
+      </form>
+    </div>
+  </div>
+</section>
+';
+}

+ 127 - 0
api/pages/login.php

@@ -0,0 +1,127 @@
+<?php
+$pageLogin = '
+<script>
+if(activeInfo.settings.login.rememberMe){
+	$(\'#checkbox-login\').prop(\'checked\',true);
+}
+</script>
+<section id="wrapper" class="login-register">
+  <div class="login-box login-sidebar animated slideInRight">
+    <div class="white-box">
+      <form class="form-horizontal" id="loginform" onsubmit="return false;">
+        <a href="javascript:void(0)" class="text-center db visible-xs" id="login-logo">' . logoOrText() . '</a>
+        <div id="oAuth-div" class="form-group hidden">
+          <div class="col-xs-12">
+            <div class="panel panel-success animated tada">
+                <div class="panel-heading">oAuth Successful - Please wait...</div>
+            </div>
+          </div>
+        </div>
+		<div id="tfa-div" class="form-group hidden">
+          <div class="col-xs-12">
+            <div class="panel panel-warning animated tada">
+                <div class="panel-heading"> 2FA
+                    <div class="pull-right"><a href="#" data-perform="panel-collapse"><i class="ti-minus"></i></a> <a href="#" data-perform="panel-dismiss"><i class="ti-close"></i></a> </div>
+                </div>
+                <div class="panel-wrapper collapse in" aria-expanded="true">
+                    <div class="panel-body">
+	                    <div class="input-group" style="width: 100%;">
+	                        <div class="input-group-addon hidden-xs"><i class="ti-lock"></i></div>
+	                        <input type="text" class="form-control tfa-input" name="tfaCode" placeholder="Code" autocomplete="off" autocorrect="off" autocapitalize="off" maxlength="6" spellcheck="false" autofocus="">
+	                    </div>
+                    </div>
+                </div>
+            </div>
+          </div>
+        </div>
+        <div class="form-group m-t-40">
+          <div class="col-xs-12">
+            <input id="login-username-Input" class="form-control" name="username" type="text" required="" placeholder="Username" lang="en" autofocus>
+          </div>
+        </div>
+        <div class="form-group">
+          <div class="col-xs-12">
+            <input id="login-password-Input" class="form-control" name="password" type="password" required="" placeholder="Password" lang="en">
+          </div>
+        </div>
+        <div class="form-group">
+          <div class="col-md-12">
+            <div class="checkbox checkbox-primary pull-left p-t-0 remember-me">
+              <input id="checkbox-login" name="remember" type="checkbox">
+              <label for="checkbox-login" lang="en">Remember Me</label>
+            </div>
+        	</div>
+        </div>
+        <div class="form-group text-center m-t-20 m-b-0">
+          <div class="col-xs-12">
+            <button class="btn btn-info btn-lg btn-block text-uppercase waves-effect waves-light login-button" type="submit" lang="en">Login</button>
+          </div>
+        </div>
+		' . showoAuth() . '
+        <div class="form-group m-b-0">
+          <div class="col-sm-12 text-center">
+          	<input id="oAuth-Input" class="form-control" name="oAuth" type="hidden">
+            <input id="oAuthType-Input" class="form-control" name="oAuthType" type="hidden">
+            ' . showLogin() . '
+          </div>
+        </div>
+      </form>
+      <form class="form-horizontal form-material hidden" id="registerForm" onsubmit="return false;">
+        <div class="form-group m-t-40">
+          <div class="col-xs-12">
+            <input class="form-control" type="text" name="registrationPassword" required="" placeholder="Registration Password" lang="en" autofocus>
+          </div>
+        </div>
+        <div class="form-group">
+          <div class="col-xs-12">
+            <input class="form-control" name="username" type="text" required="" placeholder="Username" lang="en">
+          </div>
+        </div>
+        <div class="form-group">
+          <div class="col-xs-12">
+            <input class="form-control" name="email" type="text" required="" placeholder="Email" lang="en">
+          </div>
+        </div>
+        <div class="form-group">
+          <div class="col-xs-12">
+            <input class="form-control" name="password" type="password" required="" placeholder="Password" lang="en">
+          </div>
+        </div>
+        <div class="form-group text-center m-t-20">
+          <div class="col-xs-12">
+            <button class="btn btn-info btn-lg btn-block text-uppercase waves-effect waves-light register-button" type="submit" lang="en">Register</button>
+          </div>
+        </div>
+        <div class="form-group text-center m-t-20">
+          <div class="col-xs-12">
+            <button id="leave-registration" class="btn btn-primary btn-lg btn-block text-uppercase waves-effect waves-light" type="button" lang="en">Go Back</button>
+          </div>
+        </div>
+      </form>
+      <form class="form-horizontal" id="recoverform" onsubmit="return false;">
+        <div class="form-group ">
+          <div class="col-xs-12">
+            <h3 lang="en">Recover Password</h3>
+            <p class="text-muted" lang="en">Enter your Email and instructions will be sent to you!</p>
+          </div>
+        </div>
+        <div class="form-group ">
+          <div class="col-xs-12">
+            <input id="recover-input" class="form-control" name="email" type="text" placeholder="Email" lang="en" required>
+          </div>
+        </div>
+        <div class="form-group text-center m-t-20">
+          <div class="col-xs-12">
+            <button class="btn btn-primary btn-lg btn-block text-uppercase waves-effect waves-light reset-button" type="submit" lang="en">Reset</button>
+          </div>
+        </div>
+        <div class="form-group text-center m-t-20">
+          <div class="col-xs-12">
+            <button id="leave-recover" class="btn btn-primary btn-lg btn-block text-uppercase waves-effect waves-light" type="button" lang="en">Go Back</button>
+          </div>
+        </div>
+      </form>
+    </div>
+  </div>
+</section>
+';

+ 18 - 0
api/pages/settings-customize-appearance.php

@@ -0,0 +1,18 @@
+<?php
+$pageSettingsCustomizeAppearance = '
+<script>
+	buildCustomizeAppearance();
+</script>
+<div class="panel bg-org panel-info">
+    <div class="panel-heading">
+		<span lang="en">Customize Appearance</span>
+		<button type="button" id="customize-appearance-reload" class="btn btn-primary btn-circle pull-right reload hidden m-r-5"><i class="fa fa-spin fa-refresh"></i> </button>
+		<button id="customize-appearance-form-save" onclick="submitSettingsForm(\'customize-appearance-form\')" class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right hidden animated loop-animation rubberBand" type="button"><span class="btn-label"><i class="fa fa-save"></i></span><span lang="en">Save</span></button>
+	</div>
+    <div class="panel-wrapper collapse in" aria-expanded="true">
+        <div class="panel-body bg-org">
+            <form id="customize-appearance-form" class="addFormTick" onsbumit="return false;"></form>
+        </div>
+    </div>
+</div>
+';

+ 34 - 0
api/pages/settings-image-manager.php

@@ -0,0 +1,34 @@
+<?php
+
+$pageSettingsImageManager = '
+<script>
+	buildImageManagerView();
+    var myDropzone = new Dropzone("#new-image-form", {
+      url: "api/?v1/settings/image/manager/view",
+      init: function() {
+        this.on("complete", function(file) {
+            buildImageManagerView();
+            //$.magnificPopup.close();
+        });
+      }
+    });
+</script>
+<div class="panel bg-org panel-info">
+    <div class="panel-heading">
+		<span lang="en">View Images</span>
+        <button type="button" class="btn btn-success btn-circle pull-right popup-with-form m-r-5" href="#new-image-form" data-effect="mfp-3d-unfold"><i class="fa fa-upload"></i> </button>
+	</div>
+    <div class="panel-wrapper collapse in" aria-expanded="true">
+        <div class="panel-body bg-org" >
+        <div class="row el-element-overlay m-b-40" id="settings-image-manager-list"></div>
+        </div>
+    </div>
+</div>
+<form action="#" id="new-image-form" class="mfp-hide white-popup-block mfp-with-anim dropzone" enctype="multipart/form-data">
+    <h1 lang="en">Upload Image</h1>
+    <div class="fallback">
+        <input name="file" type="file" multiple />
+    </div>
+    <div class="clearfix"></div>
+</form>
+';

+ 14 - 0
api/pages/settings-plugins.php

@@ -0,0 +1,14 @@
+<?php
+if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
+	$pageSettingsPlugins = '
+<script>
+	buildPlugins();
+</script>
+<div id="main-plugin-area"></div>
+<form id="about-plugin-form" class="mfp-hide white-popup-block mfp-with-anim">
+    <h2 id="about-plugin-title">Loading...</h2>
+    <div class="clearfix"></div>
+    <div id="about-plugin-body" class=""></div>
+</form>
+';
+}

+ 129 - 0
api/pages/settings-settings-logs.php

@@ -0,0 +1,129 @@
+<?php
+if(file_exists('config'.DIRECTORY_SEPARATOR.'config.php')){
+    $pageSettingsSettingsLogs = '
+    <script>
+    $(document).on("click", ".swapLog", function(e) {
+        var log = $(this).attr(\'data-name\')+\'Div\';
+        $(\'.logTable\').addClass(\'hidden\');
+        $(\'.\'+log).addClass(\'show\').removeClass(\'hidden\');
+    	$(\'.swapLog\').removeClass(\'active\');
+    	$(this).addClass(\'active\');
+    });
+    </script>
+    <div class="btn-group m-b-20 pull-left">
+        <button type="button" class="btn btn-default btn-outline waves-effect bg-org swapLog active" data-name="loginLog" data-path="'.$GLOBALS['organizrLoginLog'].'" lang="en">Login Log</button>
+        <button type="button" class="btn btn-default btn-outline waves-effect bg-org swapLog" data-name="orgLog" data-path="'.$GLOBALS['organizrLog'].'" lang="en">Organizr Log</button>
+    </div>
+    <button class="btn btn-danger btn-sm waves-effect waves-light pull-right purgeLog" type="button"><span class="btn-label"><i class="fa fa-trash"></i></span>Purge Log</button>
+    <div class="clearfix"></div>
+    <div class="white-box bg-org logTable loginLogDiv">
+        <h3 class="box-title m-b-0" lang="en">Login Logs</h3>
+        <div class="table-responsive">
+            <table id="loginLogTable" class="table table-striped">
+                <thead>
+                    <tr>
+                        <th lang="en">Date</th>
+                        <th lang="en">Username</th>
+                        <th lang="en">IP Address</th>
+                        <th lang="en">Type</th>
+                    </tr>
+                </thead>
+    			<tfoot>
+                    <tr>
+                        <th lang="en">Date</th>
+                        <th lang="en">Username</th>
+                        <th lang="en">IP Address</th>
+                        <th lang="en">Type</th>
+                    </tr>
+                </tfoot>
+                <tbody></tbody>
+            </table>
+        </div>
+    </div>
+    <div class="white-box bg-org logTable orgLogDiv hidden">
+        <h3 class="box-title m-b-0" lang="en">Organizr Logs</h3>
+        <div class="table-responsive">
+            <table id="organizrLogTable" class="table table-striped">
+                <thead>
+                    <tr>
+                        <th lang="en">Date</th>
+                        <th lang="en">Username</th>
+                        <th lang="en">IP Address</th>
+                        <th lang="en">Message</th>
+                        <th lang="en">Type</th>
+                    </tr>
+                </thead>
+                <tfoot>
+                    <tr>
+                        <th lang="en">Date</th>
+                        <th lang="en">Username</th>
+                        <th lang="en">IP Address</th>
+                        <th lang="en">Message</th>
+                        <th lang="en">Type</th>
+                    </tr>
+                </tfoot>
+                <tbody></tbody>
+            </table>
+        </div>
+    </div>
+    <!-- /.container-fluid -->
+    <script>
+    //$.fn.dataTable.moment(\'DD-MMM-Y HH:mm:ss\');
+    $("#loginLogTable").DataTable( {
+            "ajax": "api/?v1/login_log",
+            "columns": [
+                { data: \'utc_date\',
+                    render: function ( data, type, row ) {
+                        if ( type === \'display\' || type === \'filter\' ) {
+                            var m = moment.tz(data, activeInfo.timezone);
+                            return moment(m).format(\'LLL\');
+                        }
+                        return data;
+                    }
+                },
+                { "data": "username" },
+                { "data": "ip" },
+                { data: \'auth_type\',
+                    render: function ( data, type, row ) {
+                        if ( type === \'display\' || type === \'filter\' ) {
+                            return logIcon(data);
+                        }
+                        return logIcon(data);
+                    }
+                }
+            ],
+            "order": [[ 0, \'desc\' ]],
+    } );
+    $("#organizrLogTable").DataTable( {
+            "ajax": "api/?v1/organizr_log",
+                "columns": [
+                { data: \'utc_date\',
+                    render: function ( data, type, row ) {
+                        // If display or filter data is requested, format the date
+                        if ( type === \'display\' || type === \'filter\' ) {
+                            var m = moment.tz(data, activeInfo.timezone);
+                            return moment(m).format(\'LLL\');
+                        }
+
+                    // Otherwise the data type requested (`type`) is type detection or
+                    // sorting data, for which we want to use the integer, so just return
+                    // that, unaltered
+                    return data;}
+                    },
+                { "data": "username" },
+                { "data": "ip" },
+                { "data": "message" },
+                { data: \'type\',
+                    render: function ( data, type, row ) {
+                        if ( type === \'display\' || type === \'filter\' ) {
+                            return logIcon(data);
+                        }
+                        return logIcon(data);
+                    }
+                }
+            ],
+            "order": [[ 0, \'desc\' ]],
+    } );
+    </script>
+    ';
+}

+ 48 - 0
api/pages/settings-settings-main.php

@@ -0,0 +1,48 @@
+<?php
+$pageSettingsSettingsMain = '
+<script>
+	buildSettingsMain();
+</script>
+<div class="panel bg-org panel-info">
+    <div class="panel-heading">
+		<span lang="en">Organizr Settings</span>
+		<button id="settings-main-form-save" onclick="submitSettingsForm(\'settings-main-form\')" class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right hidden animated loop-animation rubberBand" type="button"><span class="btn-label"><i class="fa fa-save"></i></span><span lang="en">Save</span></button>
+	</div>
+    <div class="panel-wrapper collapse in" aria-expanded="true">
+        <div class="panel-body bg-org">
+            <form id="settings-main-form" class="addFormTick" onsbumit="return false;"></form>
+        </div>
+    </div>
+</div>
+<form id="auth-plex-token-form" class="mfp-hide white-popup-block mfp-with-anim">
+    <h1 lang="en">Get Plex Token</h1>
+    <div class="panel authPlexTokenHeader">
+        <div class="panel-heading authPlexTokenMessage" lang="en">Enter Plex Details</div>
+    </div>
+    <fieldset style="border:0;">
+        <div class="form-group">
+            <label class="control-label" for="auth-plex-token-form-username" lang="en">Plex Username</label>
+            <input type="text" class="form-control" id="auth-plex-token-form-username" name="username" required="" autofocus>
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="auth-plex-token-form-password" lang="en">Plex Password</label>
+            <input type="password" class="form-control" id="auth-plex-token-form-password" name="password"  required="">
+        </div>
+    </fieldset>
+    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none getauthPlexToken" type="button"><span class="btn-label"><i class="fa fa-ticket"></i></span><span lang="en">Grab It</span></button>
+    <div class="clearfix"></div>
+</form>
+<form id="auth-plex-machine-form" class="mfp-hide white-popup-block mfp-with-anim">
+    <h1 lang="en">Get Plex Machine</h1>
+    <div class="panel authPlexMachineHeader">
+        <div class="panel-heading authPlexMachineMessage" lang="en"></div>
+    </div>
+    <fieldset style="border:0;">
+        <div class="form-group">
+            <label class="control-label" for="auth-plex-machine-form-machine" lang="en">Plex Machine</label>
+            <div class="authPlexMachineListing"></div>
+        </div>
+    </fieldset>
+    <div class="clearfix"></div>
+</form>
+';

+ 48 - 0
api/pages/settings-settings-sso.php

@@ -0,0 +1,48 @@
+<?php
+$pageSettingsSettingsSSO = '
+<script>
+	buildSSO();
+</script>
+<div class="panel bg-org panel-info">
+    <div class="panel-heading">
+		<span lang="en">Single Sign-On</span>
+		<button id="sso-form-save" onclick="submitSettingsForm(\'sso-form\')" class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right hidden animated loop-animation rubberBand" type="button"><span class="btn-label"><i class="fa fa-save"></i></span><span lang="en">Save</span></button>
+	</div>
+    <div class="panel-wrapper collapse in" aria-expanded="true">
+        <div class="panel-body bg-org">
+            <form id="sso-form" class="addFormTick" onsbumit="return false;"></form>
+        </div>
+    </div>
+</div>
+<form id="sso-plex-token-form" class="mfp-hide white-popup-block mfp-with-anim">
+    <h1 lang="en">Get Plex Token</h1>
+    <div class="panel ssoPlexTokenHeader">
+        <div class="panel-heading ssoPlexTokenMessage" lang="en">Enter Plex Details</div>
+    </div>
+    <fieldset style="border:0;">
+        <div class="form-group">
+            <label class="control-label" for="sso-plex-token-form-username" lang="en">Plex Username</label>
+            <input type="text" class="form-control" id="sso-plex-token-form-username" name="username" required="" autofocus>
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="sso-plex-token-form-password" lang="en">Plex Password</label>
+            <input type="password" class="form-control" id="sso-plex-token-form-password" name="password"  required="">
+        </div>
+    </fieldset>
+    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none getSSOPlexToken" type="button"><span class="btn-label"><i class="fa fa-ticket"></i></span><span lang="en">Grab It</span></button>
+    <div class="clearfix"></div>
+</form>
+<form id="sso-plex-machine-form" class="mfp-hide white-popup-block mfp-with-anim">
+    <h1 lang="en">Get Plex Machine</h1>
+    <div class="panel ssoPlexMachineHeader">
+        <div class="panel-heading ssoPlexMachineMessage" lang="en"></div>
+    </div>
+    <fieldset style="border:0;">
+        <div class="form-group">
+            <label class="control-label" for="sso-plex-machine-form-machine" lang="en">Plex Machine</label>
+            <div class="ssoPlexMachineListing"></div>
+        </div>
+    </fieldset>
+    <div class="clearfix"></div>
+</form>
+';

+ 84 - 0
api/pages/settings-tab-editor-categories.php

@@ -0,0 +1,84 @@
+<?php
+
+$pageSettingsTabEditorCategories = '
+<script>
+buildCategoryEditor();
+$( \'#categoryEditorTable\' ).sortable({
+    stop: function () {
+        var inputs = $(\'input.order\');
+        var nbElems = inputs.length;
+        $(\'input.order\').each(function(idx) {
+            $(this).val(idx + 1);
+        });
+        submitCategoryOrder();
+    }
+});
+</script>
+<div class="panel bg-org panel-info">
+    <div class="panel-heading">
+        <span lang="en">Category Editor</span>
+        <button type="button" class="btn btn-success btn-circle pull-right popup-with-form m-r-5" href="#new-category-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
+    </div>
+    <div class="table-responsive">
+        <form id="submit-categories-form" onsubmit="return false;">
+            <table class="table table-hover manage-u-table">
+                <thead>
+                    <tr>
+                        <th width="70" class="text-center">#</th>
+                        <th lang="en">NAME</th>
+                        <th lang="en" style="text-align:center">TABS</th>
+                        <th lang="en" style="text-align:center">DEFAULT</th>
+                        <th lang="en" style="text-align:center">EDIT</th>
+                        <th lang="en" style="text-align:center">DELETE</th>
+                    </tr>
+                </thead>
+                <tbody id="categoryEditorTable"></tbody>
+            </table>
+        </form>
+    </div>
+</div>
+<form id="new-category-form" class="mfp-hide white-popup-block mfp-with-anim">
+    <h1 lang="en">Add New Category</h1>
+    <fieldset style="border:0;">
+        <div class="form-group">
+            <label class="control-label" for="new-category-form-inputNameNew" lang="en">Category Name</label>
+            <input type="text" class="form-control" id="new-category-form-inputNameNew" name="name" required="" autofocus>
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="new-category-form-inputImageNew" lang="en">Category Image</label>
+            <input type="text" class="form-control" id="new-category-form-inputImageNew" name="image"  required="">
+        </div>
+    </fieldset>
+    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none addNewCategory" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Add Category</span></button>
+    <div class="clearfix"></div>
+</form>
+<form id="edit-category-form" class="mfp-hide white-popup-block mfp-with-anim">
+    <input type="hidden" name="id" value="">
+    <h1 lang="en">Edit Category</h1>
+    <fieldset style="border:0;">
+        <div class="form-group">
+            <label class="control-label" for="edit-category-form-inputName" lang="en">Category Name</label>
+            <input type="text" class="form-control" id="edit-category-form-inputName" name="name" required="" autofocus>
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="edit-category-form-inputImage" lang="en">Category Image</label>
+            <div class="panel panel-info">
+                <div class="panel-heading">
+                    <span lang="en">Image Legend</span>
+                    <div class="pull-right"><a href="#" data-perform="panel-collapse"><i class="ti-plus"></i></a></div>
+                </div>
+                <div class="panel-wrapper collapse" aria-expanded="false">
+                    <div class="panel-body">
+                        <p lang="en">You may use an image or icon in this field</p>
+                        <p lang="en">For images, use the following format:</p><code>url::path/to/image</code>
+                        <p lang="en">For icons, use the following format:</p><code>icon-type::icon-name</code> i.e. <code>fontawesome::home</code>
+                    </div>
+                </div>
+            </div>
+            <input type="text" class="form-control" id="edit-category-form-inputImage" name="image"  required="">
+        </div>
+    </fieldset>
+    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none editCategory" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Edit Category</span></button>
+    <div class="clearfix"></div>
+</form>
+';

+ 50 - 0
api/pages/settings-tab-editor-homepage-order.php

@@ -0,0 +1,50 @@
+<?php
+if (file_exists('config' . DIRECTORY_SEPARATOR . 'config.php')) {
+	$pageSettingsTabEditorHomepageOrder = '
+<script>
+    $("#homepage-items-sort").sortable({
+	    placeholder:    "sort-placeholder col-md-3 col-xs-12 clearfix",
+	    forcePlaceholderSize: true,
+	    start: function( e, ui ){
+	        ui.item.data( "start-pos", ui.item.index()+1 );
+	    },
+	    change: function( e, ui ){
+	        var seq,
+	        startPos = ui.item.data( "start-pos" ),
+	        $index,
+	        correction;
+	        correction = startPos <= ui.placeholder.index() ? 0 : 1;
+	        ui.item.parent().find( "div.sort-homepage").each( function( idx, el ){
+	            var $this = $( el ),
+	            $index = $this.index();
+	            if ( ( $index+1 >= startPos && correction === 0) || ($index+1 <= startPos && correction === 1 ) ){
+	                $index = $index + correction;
+	                $this.find( ".ordinal-position").text( $index);
+	                link = $this.find( ".ordinal-position" ).attr("data-link");
+	                $("#homepage-values [name="+link+"]").val($index);
+	                $("#homepage-values [name="+link+"]").attr("data-changed", "true");
+	            }
+	        });
+	        seq = ui.item.parent().find( "div.sort-placeholder.col-md-3").index() + correction;
+	        ui.item.find( ".ordinal-position" ).text( seq );
+	        newlink = ui.item.find( ".ordinal-position" ).attr("data-link");
+	        $("#homepage-values [name="+newlink+"]").val(seq);
+	        $("#homepage-values [name="+newlink+"]").attr("data-changed", "true");
+	        $("#submitHomepageOrder-save").removeClass("hidden");
+	    }
+    });
+</script>
+<div class="panel bg-org panel-info">
+    <div class="panel-heading">
+		<span lang="en">Homepage Order</span>
+        <button id="submitHomepageOrder-save" type="button" class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right animated loop-animation rubberBand hidden" onclick="submitHomepageOrder()" ><span class="btn-label"><i class="fa fa-save"></i></span><span lang="en">Save</span></button>
+	</div>
+    <div class="panel-wrapper collapse in" aria-expanded="true">
+        <div class="panel-body bg-org" >
+        <div class="row el-element-overlay m-b-40" id="settings-homepage-order">' . buildHomepageSettings() . '</div>
+        </div>
+    </div>
+</div>
+
+';
+}

+ 19 - 0
api/pages/settings-tab-editor-homepage.php

@@ -0,0 +1,19 @@
+<?php
+
+$pageSettingsTabEditorHomepage = '
+<script>
+	buildHomepage();
+</script>
+<div class="panel bg-org panel-info">
+    <div class="panel-heading">
+		<span lang="en">Homepage Items</span>
+        <!-- <button type="button" class="btn btn-success btn-circle pull-right popup-with-form m-r-5" href="#new-image-form" data-effect="mfp-3d-unfold"><i class="fa fa-upload"></i> </button> -->
+	</div>
+    <div class="panel-wrapper collapse in" aria-expanded="true">
+        <div class="panel-body bg-org" >
+        <div class="row el-element-overlay m-b-40" id="settings-homepage-list"></div>
+        </div>
+    </div>
+</div>
+
+';

+ 151 - 0
api/pages/settings-tab-editor-tabs.php

@@ -0,0 +1,151 @@
+<?php
+$pageSettingsTabEditorTabs = '
+<script>
+buildTabEditor();
+$( \'#tabEditorTable\' ).sortable({
+    stop: function () {
+        var inputs = $(\'input.order\');
+        var nbElems = inputs.length;
+        $(\'input.order\').each(function(idx) {
+            $(this).val(idx + 1);
+        });
+        submitTabOrder();
+    }
+});
+$(".tabIconImageList").select2({
+	templateResult: formatImage,
+	templateSelection: formatImage,
+});
+allIcons().success(function(data) {
+    $(".tabIconIconList").select2({
+		data: data,
+		templateResult: formatIcon,
+		templateSelection: formatIcon,
+	});
+});
+
+</script>
+<div class="panel bg-org panel-info">
+    <div class="panel-heading">
+        <span lang="en">Tab Editor</span>
+        <button type="button" class="btn btn-info btn-circle pull-right popup-with-form m-r-5" href="#new-tab-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
+        <button type="button" class="btn btn-info btn-circle pull-right m-r-5 help-modal" data-modal="tabs"><i class="fa fa-question-circle"></i> </button>
+    </div>
+    <div class="table-responsive">
+        <form id="submit-tabs-form" onsubmit="return false;">
+            <table class="table table-hover manage-u-table">
+                <thead>
+                    <tr>
+                        <th width="70" class="text-center">#</th>
+                        <th lang="en">NAME</th>
+                        <th lang="en">CATEGORY</th>
+                        <th lang="en">GROUP</th>
+                        <th lang="en">TYPE</th>
+                        <th lang="en" style="text-align:center">DEFAULT</th>
+                        <th lang="en" style="text-align:center">ACTIVE</th>
+                        <th lang="en" style="text-align:center">SPLASH</th>
+                        <th lang="en" style="text-align:center">PING</th>
+                        <th lang="en" style="text-align:center">PRELOAD</th>
+                        <th lang="en" style="text-align:center">EDIT</th>
+                        <th lang="en" style="text-align:center">DELETE</th>
+                    </tr>
+                </thead>
+                <tbody id="tabEditorTable"></tbody>
+            </table>
+        </form>
+    </div>
+</div>
+<form id="new-tab-form" class="mfp-hide white-popup-block mfp-with-anim">
+    <h1 lang="en">Add New Tab</h1>
+    <fieldset style="border:0;">
+    	<div class="alert alert-success alert-dismissable tabTestMessage hidden">
+        	<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
+        	<span lang="en">Tab can be set as iFrame</span>
+        </div>
+        <div class="alert alert-danger alert-dismissable tabTestMessage hidden">
+        	<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
+        	<span lang="en">Please set tab as [New Window] on next screen</span>
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="new-tab-form-inputNameNew" lang="en">Tab Name</label>
+            <input type="text" class="form-control" id="new-tab-form-inputNameNew" name="tabName" required="" autofocus>
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="new-tab-form-inputURLNew" lang="en">Tab URL</label>
+            <input type="text" class="form-control" id="new-tab-form-inputURLNew" name="tabURL"  required="">
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="new-tab-form-inputURLLocalNew" lang="en">Tab Local URL</label>
+            <input type="text" class="form-control" id="new-tab-form-inputURLLocalNew" name="tabLocalURL">
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="new-tab-form-inputPingURLNew" lang="en">Ping URL</label>
+            <input type="text" class="form-control" id="new-tab-form-inputPingURLNew" name="pingURL"  placeholder="host/ip:port">
+        </div>
+        <div class="row">
+	        <div class="form-group col-lg-6">
+	            <label class="control-label" for="new-tab-form-chooseImage" lang="en">Choose Image</label>
+	            ' . imageSelect("new-tab-form") . '
+	        </div>
+	        <div class="form-group col-lg-6">
+	            <label class="control-label" for="new-tab-form-chooseIcon" lang="en">Choose Icon</label>
+				<select class="form-control tabIconIconList" id="new-tab-form-chooseIcon" name="chooseIcon"><option lang="en">Select or type Icon</option></select>
+	        </div>
+	    </div>
+        <div class="form-group">
+            <label class="control-label" for="new-tab-form-inputImageNew" lang="en">Tab Image</label>
+            <input type="text" class="form-control" id="new-tab-form-inputImageNew" name="tabImage"  required="">
+        </div>
+    </fieldset>
+    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light row b-none testTab" type="button"><span class="btn-label"><i class="fa fa-flask"></i></span><span lang="en">Test Tab</span></button>
+    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none addNewTab" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Add Tab</span></button>
+    <div class="clearfix"></div>
+</form>
+<form id="edit-tab-form" class="mfp-hide white-popup-block mfp-with-anim">
+    <input type="hidden" name="id" value="x">
+    <h1 lang="en">Edit Tab</h1>
+    <fieldset style="border:0;">
+    	<div class="alert alert-success alert-dismissable tabEditTestMessage hidden">
+        	<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
+        	<span lang="en">Tab can be set as iFrame</span>
+        </div>
+        <div class="alert alert-danger alert-dismissable tabEditTestMessage hidden">
+        	<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
+        	<span lang="en">Please set tab as [New Window] on next screen</span>
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="edit-tab-form-inputName" lang="en">Tab Name</label>
+            <input type="text" class="form-control" id="edit-tab-form-inputName" name="tabName" required="" autofocus>
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="edit-tab-form-inputURL" lang="en">Tab URL</label>
+            <input type="text" class="form-control" id="edit-tab-form-inputURL" name="tabURL"  required="">
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="edit-tab-form-inputLocalURL" lang="en">Tab Local URL</label>
+            <input type="text" class="form-control" id="edit-tab-form-inputLocalURL" name="tabLocalURL">
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="edit-tab-form-pingURL" lang="en">Ping URL</label>
+            <input type="text" class="form-control" id="edit-tab-form-pingURL" name="pingURL" placeholder="host/ip:port">
+        </div>
+        <div class="row">
+	        <div class="form-group col-lg-6">
+	            <label class="control-label" for="edit-tab-form-chooseImage" lang="en">Choose Image</label>
+	            ' . imageSelect("edit-tab-form") . '
+	        </div>
+	        <div class="form-group col-lg-6">
+	            <label class="control-label" for="edit-tab-form-chooseIcon" lang="en">Choose Icon</label>
+				<select class="form-control tabIconIconList" id="edit-tab-form-chooseIcon" name="chooseIcon"><option lang="en">Select or type Icon</option></select>
+	        </div>
+	    </div>
+        <div class="form-group">
+            <label class="control-label" for="edit-tab-form-inputImage" lang="en">Tab Image</label>
+            <input type="text" class="form-control" id="edit-tab-form-inputImage" name="tabImage"  required="">
+        </div>
+    </fieldset>
+    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light row b-none testEditTab" type="button"><span class="btn-label"><i class="fa fa-flask"></i></span><span lang="en">Test Tab</span></button>
+    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none editTab" type="button"><span class="btn-label"><i class="fa fa-check"></i></span><span lang="en">Edit Tab</span></button>
+    <div class="clearfix"></div>
+</form>
+';

+ 16 - 0
api/pages/settings-template.php

@@ -0,0 +1,16 @@
+<?php
+
+$pageSettingsTemplate = '
+<script>
+	//buildCustomizeAppearance();
+</script>
+<div class="panel bg-org panel-info">
+    <div class="panel-heading">
+		<span lang="en">Template</span>
+	</div>
+    <div class="panel-wrapper collapse in" aria-expanded="true">
+        <div class="panel-body bg-org">
+        </div>
+    </div>
+</div>
+';

+ 60 - 0
api/pages/settings-user-manage-groups.php

@@ -0,0 +1,60 @@
+<?php
+
+$pageSettingsUserManageGroups = '
+<script>
+    buildGroupManagement();
+</script>
+<div class="panel bg-org panel-info">
+    <div class="panel-heading">
+        <span lang="en">MANAGE GROUPS</span>
+        <button type="button" class="btn btn-info btn-circle pull-right popup-with-form" href="#new-group-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
+    </div>
+    <div class="table-responsive">
+        <table class="table table-hover manage-u-table">
+            <thead>
+                <tr>
+                    <th width="70" class="text-center">#</th>
+                    <th lang="en">GROUP NAME</th>
+                    <th lang="en">USERS</th>
+                    <th lang="en">DEFAULT</th>
+                    <th lang="en">EDIT</th>
+                    <th lang="en">DELETE</th>
+                </tr>
+            </thead>
+            <tbody id="manageGroupTable"></tbody>
+        </table>
+    </div>
+</div>
+<form id="new-group-form" class="mfp-hide white-popup-block mfp-with-anim">
+    <input type="hidden" id="newGroupID" name="groupID" value="0">
+    <input type="hidden" name="groupDefault" value="0" required="">
+    <h1 lang="en">Add New Group</h1>
+    <fieldset style="border:0;">
+        <div class="form-group">
+            <label class="control-label" for="new-group-form-inputName" lang="en">Group Name</label>
+            <input type="text" class="form-control" id="new-group-form-inputName" name="groupName" required="" autofocus> </div>
+        <div class="form-group">
+            <label class="control-label" for="new-group-form-inputImage" lang="en">Group Image</label>
+            <input type="text" class="form-control" id="new-group-form-inputImage" name="groupImage" required=""> </div>
+    </fieldset>
+    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none addNewGroup" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Add Group</span></button>
+    <div class="clearfix"></div>
+</form>
+<form id="edit-group-form" class="mfp-hide white-popup-block mfp-with-anim">
+    <input type="hidden" name="id" value="x">
+    <input type="hidden" name="oldGroupName" value="x">
+    <h1 lang="en">Edit Group</h1>
+    <fieldset style="border:0;">
+        <div class="form-group">
+            <label class="control-label" for="edit-group-form-inputEditGroupName" lang="en">Group Name</label>
+            <input type="text" class="form-control" id="edit-group-form-inputEditGroupName" name="groupName" required="" autofocus>
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="edit-group-form-inputEditGroupImage" lang="en">Group Image</label>
+            <input type="text" class="form-control" id="edit-group-form-inputEditGroupImage" name="groupImage"  required="">
+        </div>
+    </fieldset>
+    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none editGroup" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Edit Group</span></button>
+    <div class="clearfix"></div>
+</form>
+';

+ 72 - 0
api/pages/settings-user-manage-users.php

@@ -0,0 +1,72 @@
+<?php
+
+$pageSettingsUserManageUsers = '
+<script>
+    buildUserManagement();
+</script>
+<div class="panel bg-org panel-info">
+    <div class="panel-heading">
+        <span lang="en">MANAGE USERS</span>
+        <button type="button" class="btn btn-info btn-circle pull-right popup-with-form" href="#new-user-form" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
+    </div>
+    <div class="table-responsive">
+        <table class="table table-hover manage-u-table">
+            <thead>
+                <tr>
+                    <th width="70" class="text-center">#</th>
+                    <th lang="en">NAME & EMAIL</th>
+                    <th lang="en">ADDED</th>
+                    <th lang="en">GROUP</th>
+                    <th lang="en">EDIT</th>
+                    <th lang="en">EMAIL</th>
+                    <th lang="en">DELETE</th>
+                </tr>
+            </thead>
+            <tbody id="manageUserTable"></tbody>
+        </table>
+    </div>
+</div>
+<form id="new-user-form" class="mfp-hide white-popup-block mfp-with-anim">
+    <h1 lang="en">Add New User</h1>
+    <fieldset style="border:0;">
+        <div class="form-group">
+            <label class="control-label" for="new-user-form-inputUsername" lang="en">Username</label>
+            <input type="text" class="form-control" id="new-user-form-inputUsername" name="username" required="" autofocus>
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="new-user-form-inputEmail" lang="en">Email</label>
+            <input type="email" class="form-control" id="new-user-form-inputEmail" name="email"  required="">
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="new-user-form-inputPassword" lang="en">Password</label>
+            <input type="password" class="form-control" id="new-user-form-inputPassword" name="password"  required="">
+        </div>
+    </fieldset>
+    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none addNewUser" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Add User</span></button>
+    <div class="clearfix"></div>
+</form>
+<form id="edit-user-form" class="mfp-hide white-popup-block mfp-with-anim">
+    <input type="hidden" name="id" value="">
+    <h1 lang="en">Edit User</h1>
+    <fieldset style="border:0;">
+        <div class="form-group">
+            <label class="control-label" for="edit-user-form-inputUsername" lang="en">Username</label>
+            <input type="text" class="form-control" id="edit-user-form-inputUsername" name="username" required="" autofocus>
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="edit-user-form-inputEmail" lang="en">Email</label>
+            <input type="text" class="form-control" id="edit-user-form-inputEmail" name="email" required="" autofocus>
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="edit-user-form-inputPassword" lang="en">Password</label>
+            <input type="password" class="form-control" id="edit-user-form-inputPassword" name="password"  required="">
+        </div>
+        <div class="form-group">
+            <label class="control-label" for="edit-user-form-inputPassword2" lang="en">Password Again</label>
+            <input type="password" class="form-control" id="edit-user-form-inputPassword2" name="password2"  required="">
+        </div>
+    </fieldset>
+    <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none editUserAdmin" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Edit User</span></button>
+    <div class="clearfix"></div>
+</form>
+';

+ 332 - 0
api/pages/settings.php

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

+ 401 - 0
api/pages/wizard.php

@@ -0,0 +1,401 @@
+<?php
+$pageWizard = '
+<script>
+    (function() {
+        $(\'#adminValidator\').wizard({
+            onInit: function() {
+                $(\'#validation\').formValidation({
+                    framework: \'bootstrap\',
+                    fields: {
+                        username: {
+                            validators: {
+                                notEmpty: {
+                                    message: \'The username is required\'
+                                },
+                                stringLength: {
+                                    min: 3,
+                                    max: 30,
+                                    message: \'The username must be more than 2 and less than 30 characters long\'
+                                },
+                                regexp: {
+                                    regexp: /^[a-zA-Z0-9_\.]+$/,
+                                    message: \'The username can only consist of alphabetical, number, dot and underscore\'
+                                }
+                            }
+                        },
+                        license: {
+                            validators: {
+                                regexp: {
+                                    regexp: /^[a-zA-Z0-9_\.]+$/,
+                                    message: \'Please choose a license\'
+                                }
+                            }
+                        },
+                        email: {
+                            validators: {
+                                notEmpty: {
+                                    message: \'The email address is required\'
+                                },
+                                emailAddress: {
+                                    message: \'The input is not a valid email address\'
+                                }
+                            }
+                        },
+                        hashKey: {
+                            validators: {
+                                notEmpty: {
+                                    message: \'The hash key is required\'
+                                },
+                                stringLength: {
+                                    min: 3,
+                                    max: 30,
+                                    message: \'The hash key must be more than 2 and less than 30 characters long\'
+                                }
+                            }
+                        },
+                        location: {
+                            validators: {
+                                notEmpty: {
+                                    message: \'The database location is required\'
+                                }
+                            }
+                        },
+                        dbName: {
+                            validators: {
+                                notEmpty: {
+                                    message: \'The Database Name is required\'
+                                },
+                                stringLength: {
+                                    min: 2,
+                                    max: 30,
+                                    message: \'The Database Name must be more than 1 and less than 30 characters long\'
+                                },
+                                regexp: {
+                                    regexp: /^[a-zA-Z0-9_\.]+$/,
+                                    message: \'The Database Name can only consist of alphabetical, number, dot and underscore\'
+                                }
+                            }
+                        },
+                        api: {
+                            validators: {
+                                notEmpty: {
+                                    message: \'The API Key is required\'
+                                },
+                                stringLength: {
+                                    min: 20,
+                                    max: 20,
+                                    message: \'The API Key must be 20 characters long\'
+                                }
+                            }
+                        },
+                        registrationPassword: {
+                            validators: {
+                                notEmpty: {
+                                    message: \'The registration password is required\'
+                                }
+                            }
+                        },
+                        password: {
+                            validators: {
+                                notEmpty: {
+                                    message: \'The password is required\'
+                                },
+                                different: {
+                                    field: \'username\',
+                                    message: \'The password cannot be the same as username\'
+                                }
+                            }
+                        }
+                    }
+                });
+            },
+            validator: function() {
+                var fv = $(\'#validation\').data(\'formValidation\');
+                var $this = $(this);
+                // Validate the container
+                fv.validateContainer($this);
+                var isValidStep = fv.isValidContainer($this);
+                if (isValidStep === false || isValidStep === null) {
+                    return false;
+                }
+                return true;
+            },
+            onFinish: function() {
+                //$(\'#validation\').submit();
+                var post = $( \'#validation\' ).serializeArray();
+                console.log( post );
+                organizrAPI(\'POST\',\'api/?v1/wizard_config\',post).success(function(data) {
+            		var html = JSON.parse(data);
+                    if(html.data == true){
+                        location.reload();
+                    }else if(html.data == \'token\'){
+                        message("",window.lang.translate(\'Could not create Token\'),activeInfo.settings.notifications.position,"#FFF","error","3500");
+						console.error(\'Organizr Function: Could not create Token\');
+					}else if(html.data == \'db\'){
+						message("",window.lang.translate(\'Could not create DB - check permissions\'),activeInfo.settings.notifications.position,"#FFF","error","3500");
+						console.error(\'Organizr Function: Could not create DB - check permissions\');
+					}else if(html.data == \'admin\'){
+						message("",window.lang.translate(\'Could not create admin acct\'),activeInfo.settings.notifications.position,"#FFF","error","3500");
+						console.error(\'Organizr Function: Could not create admin acct\');
+					}else if(html.data == \'config\'){
+						message("",window.lang.translate(\'Could not create config files - check permissions\'),activeInfo.settings.notifications.position,"#FFF","error","3500");
+						console.error(\'Organizr Function: Could not create config files - check permissions\');
+					}else{
+						message("",window.lang.translate(\'Sign-up Error Occurred\'),activeInfo.settings.notifications.position,"#FFF","error","3500");
+                        console.error(\'Organizr Function: Sign-up Error Occurred\');
+                    }
+            	}).fail(function(xhr) {
+            		console.error("Organizr Function: Connection Failed");
+            	});
+            }
+        });
+        generateAPI();
+        $( ".wizardInput" ).focusout(function() {
+            var value = $(this).val();
+            var name = $(this).attr(\'name\');
+            if (typeof value !== \'undefined\' && typeof name !== \'undefined\') {
+                $(\'#verify-\'+name).text(value);
+            }
+        });
+    })();
+</script>
+<div class="container-fluid">
+    <div class="row bg-title">
+        <div class="col-lg-3 col-md-4 col-sm-4 col-xs-12">
+            <h4 class="page-title">Organizr Setup Wizard</h4>
+        </div>
+        <!-- /.col-lg-12 -->
+    </div>
+    <!--.row-->
+    <div class="row">
+        <div class="col-sm-12">
+            <div class="white-box">
+                <h3 class="box-title m-b-0" lang="en">Admin Creation</h3>
+                <div class="wizard" id="adminValidator">
+                    <ul class="wizard-steps" role="tablist">
+                        <li class="active" role="tab">
+                            <h4><span><i class="ti-direction"></i></span><item lang="en">Install Type</item></h4>
+                        </li>
+                        <li role="tab">
+                            <h4><span><i class="ti-user"></i></span><item lang="en">Admin Info</item></h4>
+                        </li>
+                        <li role="tab">
+                            <h4><span><i class="ti-key"></i></span><item lang="en">Security</item></h4>
+                        </li>
+                        <li role="tab">
+                            <h4><span><i class="ti-server"></i></span><item lang="en">Database</item></h4>
+                        </li>
+                        <li role="tab">
+                            <h4><span><i class="ti-check"></i></span><item lang="en">Verify</item></h4>
+                        </li>
+                    </ul>
+                    <form class="form-horizontal" id="validation" name="validation" onsubmit="return false;">
+                        <div class="wizard-content">
+                            <div class="wizard-pane active" role="tabpanel">
+	                            <div class="panel panel-info">
+                                    <div class="panel-heading">
+                                        <i class="ti-alert fa-fw"></i> <span lang="en">Notice</span>
+                                        <div class="pull-right"><a href="#" data-perform="panel-collapse"><i class="ti-minus"></i></a> <a href="#" data-perform="panel-dismiss"><i class="ti-close"></i></a> </div>
+                                    </div>
+                                    <div class="panel-wrapper collapse in" aria-expanded="true">
+                                        <div class="panel-body">
+                                            <p lang="en">Personal has everything unlocked - no restrictions</p>
+                                            <p lang="en">Business has Media items hidden [Plex, Emby etc...]</p>
+                                        </div>
+                                    </div>
+                                </div>
+                                <div class="form-group">
+                                    <label for="license" lang="en">Install Type</label>
+                                    <div class="input-group">
+                                        <div class="input-group-addon"><i class="ti-direction"></i></div>
+                                        <select name="license" class="form-control wizardInput" id="form-license">
+                                            <option lang="en">Choose License</option>
+                                            <option lang="en" value="personal">Personal</option>
+                                            <option lang="en" value="business">Business</option>
+                                        </select>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="wizard-pane" role="tabpanel">
+                                <div class="panel panel-info">
+                                    <div class="panel-heading">
+                                        <i class="ti-alert fa-fw"></i> <span lang="en">Notice</span>
+                                        <div class="pull-right"><a href="#" data-perform="panel-collapse"><i class="ti-minus"></i></a> <a href="#" data-perform="panel-dismiss"><i class="ti-close"></i></a> </div>
+                                    </div>
+                                    <div class="panel-wrapper collapse in" aria-expanded="true">
+                                        <div class="panel-body">
+                                            <p lang="en">If using Plex or Emby - It is suggested that you use the username and email of the Admin account.</p>
+                                        </div>
+                                    </div>
+                                </div>
+                                <div class="form-group">
+                                    <label for="username" lang="en">Username</label>
+                                    <div class="input-group">
+                                        <div class="input-group-addon"><i class="ti-user"></i></div>
+                                        <input type="text" class="form-control wizardInput" name="username" id="form-username">
+                                    </div>
+                                </div>
+                                <div class="form-group">
+                                    <label for="email" lang="en">Email</label>
+                                    <div class="input-group">
+                                        <div class="input-group-addon"><i class="ti-email"></i></div>
+                                        <input type="text" class="form-control wizardInput" name="email" id="form-email">
+                                    </div>
+                                </div>
+                                <div class="form-group">
+                                    <label for="passwrod" lang="en">Password</label>
+                                    <div class="input-group">
+                                        <div class="input-group-addon"><i class="ti-lock"></i></div>
+                                        <input type="password" class="form-control wizardInput" name="password" id="form-password">
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="wizard-pane" role="tabpanel">
+                                <div class="panel panel-info">
+                                    <div class="panel-heading">
+                                        <i class="ti-alert fa-fw"></i> <span lang="en">Notice</span>
+                                        <div class="pull-right"><a href="#" data-perform="panel-collapse"><i class="ti-minus"></i></a> <a href="#" data-perform="panel-dismiss"><i class="ti-close"></i></a> </div>
+                                    </div>
+                                    <div class="panel-wrapper collapse in" aria-expanded="true">
+                                        <div class="panel-body">
+                                            <p lang="en">The Hash Key will be used to decrypt all passwords etc... on the server. {User-Generated]</p>
+                                            <p lang="en">The Registration Password will lockout the registration field with this password. {User-Generated]</p>
+                                            <p lang="en">The API Key will be used for all calls to organizr for the UI. [Auto-Generated]</p>
+                                        </div>
+                                    </div>
+                                </div>
+                                <div class="form-group">
+                                    <label for="key" lang="en">Hash Key</label>
+                                    <div class="input-group">
+                                        <div class="input-group-addon"><i class="ti-key"></i></div>
+                                        <input type="password" class="form-control wizardInput" name="hashKey" id="form-hashKey">
+                                    </div>
+                                </div>
+                                <div class="form-group">
+                                    <label for="key" lang="en">Registration Password</label>
+                                    <div class="input-group">
+                                        <div class="input-group-addon"><i class="ti-key"></i></div>
+                                        <input type="password" class="form-control wizardInput" name="registrationPassword" id="form-registrationPassword">
+                                    </div>
+                                </div>
+                                <div class="form-group">
+                                    <label for="key" lang="en">API Key</label>
+                                    <div class="input-group">
+                                        <div class="input-group-addon"><i class="ti-key"></i></div>
+                                        <input type="password" class="form-control wizardInput disabled" name="api" id="form-api">
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="wizard-pane" role="tabpanel">
+                                <div class="panel panel-danger">
+                                    <div class="panel-heading">
+                                        <i class="ti-alert fa-fw"></i> <span lang="en">Attention</span>
+                                        <div class="pull-right"><a href="#" data-perform="panel-collapse"><i class="ti-minus"></i></a> <a href="#" data-perform="panel-dismiss"><i class="ti-close"></i></a> </div>
+                                    </div>
+                                    <div class="panel-wrapper collapse in" aria-expanded="true">
+                                        <div class="panel-body">
+                                            <p lang="en">The Database will contain sensitive information.  Please place in directory outside of root Web Directory.</p>
+                                            <p lang="en">Suggested Directory: <code>' . dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'db</code> <a class="btn default btn-outline clipboard p-5" data-clipboard-text="' . dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'db" href="javascript:void(0);"><i class="ti-clipboard"></i></a></p>
+                                            <p lang="en">Current Directory: <code>' . dirname(__DIR__, 2) . '</code> <a class="btn default btn-outline clipboard p-5" data-clipboard-text="' . dirname(__DIR__, 2) . '" href="javascript:void(0);"><i class="ti-clipboard"></i></a></p>
+                                            <p lang="en">Parent Directory: <code>' . dirname(__DIR__, 3) . '</code> <a class="btn default btn-outline clipboard p-5" data-clipboard-text="' . dirname(__DIR__, 3) . '" href="javascript:void(0);"><i class="ti-clipboard"></i></a></p>
+                                        </div>
+                                    </div>
+                                </div>
+                                <div class="form-group">
+                                    <label for="dbName" lang="en">Database Name</label>
+                                    <div class="input-group">
+                                        <div class="input-group-addon"><i class="ti-server"></i></div>
+                                        <input type="text" class="form-control wizardInput" name="dbName" id="form-dbName" placeholder="orgDBname">
+                                    </div>
+                                </div>
+                                <div class="form-group">
+                                    <label for="location" lang="en">Database Location</label>
+                                    <div class="input-group">
+                                        <div class="input-group-addon"><i class="ti-server"></i></div>
+                                        <input type="text" class="form-control wizardInput" name="location" id="form-location" placeholder="Enter path or copy from above">
+                                        <span class="input-group-btn"><button class="btn btn-info testPath" lang="en" type="button">Test / Create Path</button></span>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="wizard-pane" role="tabpanel">
+                                <div class="row">
+                                    <div class="col-md-6">
+                                        <div class="form-group">
+                                            <label class="control-label col-md-3" lang="en">License:</label>
+                                            <div class="col-md-9">
+                                                <p class="form-control-static" id="verify-license"></p>
+                                            </div>
+                                        </div>
+                                        <div class="form-group">
+                                            <label class="control-label col-md-3" lang="en">Username:</label>
+                                            <div class="col-md-9">
+                                                <p class="form-control-static" id="verify-username"></p>
+                                            </div>
+                                        </div>
+                                        <div class="form-group">
+                                            <label class="control-label col-md-3" lang="en">Email:</label>
+                                            <div class="col-md-9">
+                                                <p class="form-control-static" id="verify-email"></p>
+                                            </div>
+                                        </div>
+                                        <div class="form-group">
+                                            <label class="control-label col-md-3" lang="en">Password:</label>
+                                            <div class="col-md-9">
+                                                <p class="form-control-static">
+                                                    <a class="mytooltip" href="javascript:void(0)"> <span lang="en">Hover to show </span><span class="tooltip-content5"><span class="tooltip-text3"><span class="tooltip-inner2" id="verify-password"></span></span></span></a>
+                                                </p>
+                                            </div>
+                                        </div>
+                                    </div>
+                                    <div class="col-md-6">
+                                        <div class="form-group">
+                                            <label class="control-label col-md-3" lang="en">Hash Key:</label>
+                                            <div class="col-md-9">
+                                                <p class="form-control-static">
+                                                    <a class="mytooltip" href="javascript:void(0)"> <span lang="en">Hover to show </span><span class="tooltip-content5"><span class="tooltip-text3"><span class="tooltip-inner2" id="verify-hashKey">pass</span></span></span></a>
+                                                </p>
+                                            </div>
+                                        </div>
+                                        <div class="form-group">
+                                            <label class="control-label col-md-3" lang="en">Registration Password:</label>
+                                            <div class="col-md-9">
+                                                <p class="form-control-static">
+                                                    <a class="mytooltip" href="javascript:void(0)"> <span lang="en">Hover to show </span><span class="tooltip-content5"><span class="tooltip-text3"><span class="tooltip-inner2" id="verify-registrationPassword">pass</span></span></span></a>
+                                                </p>
+                                            </div>
+                                        </div>
+                                        <div class="form-group">
+                                            <label class="control-label col-md-3" lang="en">API Key:</label>
+                                            <div class="col-md-9">
+                                                <p class="form-control-static">
+                                                    <a class="mytooltip" href="javascript:void(0)"> <span lang="en">Hover to show </span><span class="tooltip-content5"><span class="tooltip-text3"><span class="tooltip-inner2" id="verify-api">pass</span></span></span></a>
+                                                </p>
+                                            </div>
+                                        </div>
+                                        <div class="form-group">
+                                            <label class="control-label col-md-3" lang="en">Database Location:</label>
+                                            <div class="col-md-9">
+                                                <p class="form-control-static" id="verify-location">  </p>
+                                            </div>
+                                        </div>
+                                        <div class="form-group">
+                                            <label class="control-label col-md-3" lang="en">Database Name:</label>
+                                            <div class="col-md-9">
+                                                <p class="form-control-static" id="verify-dbName">  </p>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                                <!--/row-->
+                            </div>
+                        </div>
+                    </form>
+                </div>
+            </div>
+        </div>
+
+    </div>
+    <!--./row-->
+</div>
+<!-- /.container-fluid -->
+';

+ 59 - 0
api/plugins/api/chat.php

@@ -0,0 +1,59 @@
+<?php
+if (isset($_POST['data']['plugin'])) {
+	switch ($_POST['data']['plugin']) {
+		case 'chat/settings/get':
+			if (qualifyRequest(1)) {
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = chatGetSettings();
+			} else {
+				$result['status'] = 'error';
+				$result['statusText'] = 'API/Token invalid or not set';
+				$result['data'] = null;
+			}
+			break;
+		case 'chat/message':
+			if (qualifyRequest($GLOBALS['CHAT-Auth-include'])) {
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = sendChatMessage($_POST);
+			} else {
+				$result['status'] = 'error';
+				$result['statusText'] = 'API/Token invalid or not set';
+				$result['data'] = null;
+			}
+			break;
+		default:
+			//DO NOTHING!!!
+			break;
+	}
+}
+if (isset($_GET['plugin']) && $_GET['plugin'] == 'chat' && isset($_GET['cmd'])) {
+	switch ($_GET['cmd']) {
+		case 'chat/settings/get':
+			if (qualifyRequest(1)) {
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = chatGetSettings();
+			} else {
+				$result['status'] = 'error';
+				$result['statusText'] = 'API/Token invalid or not set';
+				$result['data'] = null;
+			}
+			break;
+		case 'chat/message':
+			if (qualifyRequest($GLOBALS['CHAT-Auth-include'])) {
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = getChatMessage();
+			} else {
+				$result['status'] = 'error';
+				$result['statusText'] = 'API/Token invalid or not set';
+				$result['data'] = null;
+			}
+			break;
+		default:
+			//Do NOTHING!
+			break;
+	}
+}

+ 42 - 0
api/plugins/api/invites.php

@@ -0,0 +1,42 @@
+<?php
+if (isset($_POST['data']['plugin'])) {
+	switch ($_POST['data']['plugin']) {
+		case 'Invites/settings/get':
+			if (qualifyRequest(1)) {
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = invitesGetSettings();
+			} else {
+				$result['status'] = 'error';
+				$result['statusText'] = 'API/Token invalid or not set';
+				$result['data'] = null;
+			}
+			break;
+		case 'Invites/codes':
+			$result['status'] = 'success';
+			$result['statusText'] = 'success';
+			$result['data'] = inviteCodes($_POST);
+			break;
+		default:
+			//DO NOTHING!!
+			break;
+	}
+}
+if (isset($_GET['plugin']) && $_GET['plugin'] == 'Invites' && isset($_GET['cmd'])) {
+	switch ($_GET['cmd']) {
+		case 'Invites/settings/get':
+			if (qualifyRequest(1)) {
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = invitesGetSettings();
+			} else {
+				$result['status'] = 'error';
+				$result['statusText'] = 'API/Token invalid or not set';
+				$result['data'] = null;
+			}
+			break;
+		default:
+			//Do NOTHING!
+			break;
+	}
+}

+ 70 - 0
api/plugins/api/php-mailer.php

@@ -0,0 +1,70 @@
+<?php
+if (isset($_POST['data']['plugin'])) {
+	switch ($_POST['data']['plugin']) {
+		case 'PHPMailer/settings/get':
+			if (qualifyRequest(1)) {
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = phpmGetSettings();
+			} else {
+				$result['status'] = 'error';
+				$result['statusText'] = 'API/Token invalid or not set';
+				$result['data'] = null;
+			}
+			break;
+		case 'PHPMailer/send/test':
+			if (qualifyRequest(1)) {
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = phpmSendTestEmail();
+			} else {
+				$result['status'] = 'error';
+				$result['statusText'] = 'API/Token invalid or not set';
+				$result['data'] = null;
+			}
+			break;
+		case 'PHPMailer/send/email':
+			if (qualifyRequest(1)) {
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = phpmAdminSendEmail();
+			} else {
+				$result['status'] = 'error';
+				$result['statusText'] = 'API/Token invalid or not set';
+				$result['data'] = null;
+			}
+			break;
+		case 'PHPMailer/users/get':
+			if (qualifyRequest(1)) {
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = getEmails();
+			} else {
+				$result['status'] = 'error';
+				$result['statusText'] = 'API/Token invalid or not set';
+				$result['data'] = null;
+			}
+			break;
+		default:
+			//DO NOTHING!!
+			break;
+	}
+}
+if (isset($_GET['plugin']) && $_GET['plugin'] == 'PHPMailer' && isset($_GET['cmd'])) {
+	switch ($_GET['cmd']) {
+		case 'PHPMailer/settings/get':
+			if (qualifyRequest(1)) {
+				$result['status'] = 'success';
+				$result['statusText'] = 'success';
+				$result['data'] = phpmGetSettings();
+			} else {
+				$result['status'] = 'error';
+				$result['statusText'] = 'API/Token invalid or not set';
+				$result['data'] = null;
+			}
+			break;
+		default:
+			//Do NOTHING!
+			break;
+	}
+}

+ 19 - 0
api/plugins/api/speedTest.php

@@ -0,0 +1,19 @@
+<?php
+if (isset($_POST['data']['plugin'])) {
+    switch ($_POST['data']['plugin']) {
+        case 'SpeedTest/settings/get':
+            if (qualifyRequest(1)) {
+                $result['status'] = 'success';
+                $result['statusText'] = 'success';
+                $result['data'] = speedTestGetSettings();
+            } else {
+                $result['status'] = 'error';
+                $result['statusText'] = 'API/Token invalid or not set';
+                $result['data'] = null;
+            }
+            break;
+        default:
+            //DO NOTHING!!
+            break;
+    }
+}

+ 164 - 0
api/plugins/chat.php

@@ -0,0 +1,164 @@
+<?php
+// PLUGIN INFORMATION
+$GLOBALS['plugins'][]['chat'] = array( // Plugin Name
+	'name' => 'Chat', // Plugin Name
+	'author' => 'CauseFX', // Who wrote the plugin
+	'category' => 'Utilities', // One to Two Word Description
+	'link' => '', // Link to plugin info
+	'license' => 'personal,business', // License Type use , for multiple
+	//'fileName'=>'php-mailer.php',
+	//'configFile'=>'php-mailer.php',
+	//'apiFile'=>'php-mailer.php',
+	'idPrefix' => 'CHAT', // html element id prefix
+	'configPrefix' => 'CHAT', // config file prefix for array items without the hypen
+	'version' => '1.0.0', // SemVer of plugin
+	'image' => 'plugins/images/chat.png', // 1:1 non transparent image for plugin
+	'settings' => true, // does plugin need a settings page? true or false
+	'homepage' => false // Is plugin for use on homepage? true or false
+);
+// INCLUDE/REQUIRE FILES
+// PLUGIN FUNCTIONS
+/* GET CHAT SETTINGS */
+function chatGetSettings()
+{
+	return 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://dashboard.pusher.com/accounts/sign_up" target="_blank">Signup for Pusher [FREE]</a></li>
+                                        <li><i class="fa fa-chevron-right text-danger"></i> Create an App called whatever you like and choose a cluster (Close to you)</li>
+                                        <li><i class="fa fa-chevron-right text-danger"></i> Frontend (JQuery) - Backend (PHP)</li>
+                                        <li><i class="fa fa-chevron-right text-danger"></i> Click the overview tab on top left</li>
+                                        <li><i class="fa fa-chevron-right text-danger"></i> Copy and paste the 4 values into Organizr</li>
+                                        <li><i class="fa fa-chevron-right text-danger"></i> Save and reload!</li>
+                                    </ul>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+				</div>
+				',
+		'Options' => array(
+			array(
+				'type' => 'select',
+				'name' => 'CHAT-Auth-include',
+				'label' => 'Minimum Authentication',
+				'value' => $GLOBALS['CHAT-Auth-include'],
+				'options' => groupSelect()
+			),
+			array(
+				'type' => 'number',
+				'name' => 'CHAT-messageLoadLimit',
+				'label' => '# of Previous Messages',
+				'value' => $GLOBALS['CHAT-messageLoadLimit'],
+				'placeholder' => ''
+			),
+			array(
+				'type' => 'select',
+				'name' => 'CHAT-userRefreshTimeout',
+				'label' => 'Refresh Seconds',
+				'value' => $GLOBALS['CHAT-userRefreshTimeout'],
+				'options' => optionTime()
+			),
+			array(
+				'type' => 'select',
+				'name' => 'CHAT-newMessageSound-include',
+				'label' => 'Message Sound',
+				'value' => $GLOBALS['CHAT-newMessageSound-include'],
+				'options' => getSounds()
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'CHAT-useSSL',
+				'label' => 'Use Pusher SSL',
+				'help' => 'If messages get stuck sending, please turn this option off.',
+				'value' => $GLOBALS['CHAT-useSSL']
+			)
+		),
+		'Connection' => array(
+			array(
+				'type' => 'password-alt',
+				'name' => 'CHAT-authKey-include',
+				'label' => 'Auth Key',
+				'value' => $GLOBALS['CHAT-authKey-include']
+			),
+			array(
+				'type' => 'password-alt',
+				'name' => 'CHAT-secret',
+				'label' => 'API Secret',
+				'value' => $GLOBALS['CHAT-secret']
+			),
+			array(
+				'type' => 'input',
+				'name' => 'CHAT-appID-include',
+				'label' => 'App ID',
+				'value' => $GLOBALS['CHAT-appID-include']
+			),
+			array(
+				'type' => 'input',
+				'name' => 'CHAT-cluster-include',
+				'label' => 'App Cluster',
+				'value' => $GLOBALS['CHAT-cluster-include']
+			),
+		)
+	);
+}
+
+function sendChatMessage($array)
+{
+	$message = isset($array['data']['message']) ? $array['data']['message'] : null;
+	$message = htmlspecialchars($message, ENT_QUOTES);
+	$now = date("Y-m-d H:i:s");
+	$currentIP = userIP();
+	try {
+		$connect = new Dibi\Connection([
+			'driver' => 'sqlite3',
+			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
+		]);
+		$newMessage = [
+			'username' => $GLOBALS['organizrUser']['username'],
+			'gravatar' => $GLOBALS['organizrUser']['image'],
+			'uid' => $GLOBALS['organizrUser']['uid'],
+			'date' => $now,
+			'ip' => $currentIP,
+			'message' => $message
+		];
+		$connect->query('INSERT INTO [chatroom]', $newMessage);
+		$options = array(
+			'cluster' => $GLOBALS['CHAT-cluster-include'],
+			'useTLS' => $GLOBALS['CHAT-useSSL']
+		);
+		$pusher = new Pusher\Pusher(
+			$GLOBALS['CHAT-authKey-include'],
+			$GLOBALS['CHAT-secret'],
+			$GLOBALS['CHAT-appID-include'],
+			$options
+		);
+		$pusher->trigger('org_channel', 'my-event', $newMessage);
+		return true;
+	} catch (Dibi\Exception $e) {
+		return $e;
+	}
+}
+
+function getChatMessage()
+{
+	try {
+		$connect = new Dibi\Connection([
+			'driver' => 'sqlite3',
+			'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
+		]);
+		$all = $connect->fetchAll('SELECT `username`, `gravatar`, `uid`, `date`, `message` FROM chatroom LIMIT ' . $GLOBALS['CHAT-messageLoadLimit']);
+		return $all;
+	} catch (Dibi\Exception $e) {
+		return false;
+	}
+	
+}

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

@@ -0,0 +1,13 @@
+<?php
+return array(
+	'CHAT-enabled' => false,
+	'CHAT-Auth-include' => '1',
+	'CHAT-authKey-include' => '',
+	'CHAT-appID-include' => '',
+	'CHAT-cluster-include' => '',
+	'CHAT-secret' => '',
+	'CHAT-messageLoadLimit' => '200',
+	'CHAT-userRefreshTimeout' => '60000',
+	'CHAT-newMessageSound-include' => 'plugins/sounds/default/newmessage.mp3',
+	'CHAT-useSSL' => false
+);

+ 6 - 0
api/plugins/config/invites.php

@@ -0,0 +1,6 @@
+<?php
+return array(
+    'INVITES-enabled' => false,
+    'INVITES-type-include' => 'plex',
+    'INVITES-plexLibraries' => '',
+);

+ 47 - 0
api/plugins/config/php-mailer.php

@@ -0,0 +1,47 @@
+<?php
+return array(
+	'PHPMAILER-enabled' => false,
+	'PHPMAILER-smtpHost' => '',
+	'PHPMAILER-smtpHostPort' => '',
+	'PHPMAILER-smtpHostAuth' => true,
+	'PHPMAILER-smtpHostUsername' => '',
+	'PHPMAILER-smtpHostPassword' => '',
+	'PHPMAILER-smtpHostSenderName' => 'Organizr',
+	'PHPMAILER-smtpHostSenderEmail' => 'no-reply@Organizr.tld',
+	'PHPMAILER-smtpHostType' => 'tls',
+	'PHPMAILER-domain' => '',
+	'PHPMAILER-template' => 'default',
+	'PHPMAILER-logo' => 'https://raw.githubusercontent.com/causefx/Organizr/v2-develop/plugins/images/organizr/logo-wide.png',
+	'PHPMAILER-emailTemplateResetPassword' => '
+	<h2>Hey there {user}!</h2><br />
+	Looks like you forgot your password.  Well, I got you...  Here is your new password: {password}<br />
+	If you want to change it once you log in, you can.  Head over to my website: {domain}<br />
+	',
+	'PHPMAILER-emailTemplateResetPasswordSubject' => 'Password Reset',
+	'PHPMAILER-emailTemplateInviteUser' => '
+	<h2>Hey there {user}!</h2><br />
+	Here is the invite code to join my cool media server: {inviteCode}<br/>
+	Head over to my website and enter the code to join: {domain}<br />
+	',
+	'PHPMAILER-emailTemplateInviteUserSubject' => 'You have been invited to join my server',
+	'PHPMAILER-emailTemplateRegisterUser' => '
+	<h2>Hey there {user}!</h2><br />
+	Welcome to my site.<br/>
+	If you need anything, please let me know.<br />
+	',
+	'PHPMAILER-emailTemplateRegisterUserSubject' => 'Thank you For Registering',
+	'PHPMAILER-emailTemplateRegisterUserEnabled' => true,
+	'PHPMAILER-emailTemplateCustom-include-One' => '',
+	'PHPMAILER-emailTemplateCustom-include-OneName' => 'Template #1',
+	'PHPMAILER-emailTemplateCustom-include-OneSubject' => '',
+	'PHPMAILER-emailTemplateCustom-include-Two' => '',
+	'PHPMAILER-emailTemplateCustom-include-TwoName' => 'Template #2',
+	'PHPMAILER-emailTemplateCustom-include-TwoSubject' => '',
+	'PHPMAILER-emailTemplateCustom-include-Three' => '',
+	'PHPMAILER-emailTemplateCustom-include-ThreeName' => 'Template #3',
+	'PHPMAILER-emailTemplateCustom-include-ThreeSubject' => '',
+	'PHPMAILER-emailTemplateCustom-include-Four' => '',
+	'PHPMAILER-emailTemplateCustom-include-FourName' => 'Template #4',
+	'PHPMAILER-emailTemplateCustom-include-FourSubject' => '',
+	'PHPMAILER-verifyCert' => true,
+);

+ 6 - 0
api/plugins/config/speedTest.php

@@ -0,0 +1,6 @@
+<?php
+return array(
+    'SPEEDTEST-enabled' => false,
+    'SPEEDTEST-Auth-include' => '1',
+    'SPEEDTEST-option2-include' => '',
+);

+ 318 - 0
api/plugins/invites.php

@@ -0,0 +1,318 @@
+<?php
+// PLUGIN INFORMATION
+$GLOBALS['plugins'][]['Invites'] = array( // Plugin Name
+	'name' => 'Invites', // Plugin Name
+	'author' => 'CauseFX', // Who wrote the plugin
+	'category' => 'Management', // One to Two Word Description
+	'link' => 'https://github.com/PHPMailer/PHPMailer', // Link to plugin info
+	'license' => 'personal', // License Type use , for multiple
+	//'fileName'=>'php-mailer.php',
+	//'configFile'=>'php-mailer.php',
+	//'apiFile'=>'php-mailer.php',
+	'idPrefix' => 'INVITES', // html element id prefix
+	'configPrefix' => 'INVITES', // config file prefix for array items without the hypen
+	'version' => '1.0.0', // SemVer of plugin
+	'image' => 'plugins/images/invites.png', // 1:1 non transparent image for plugin
+	'settings' => true, // does plugin need a settings page? true or false
+	'homepage' => false // Is plugin for use on homepage? true or false
+);
+// INCLUDE/REQUIRE FILES
+// PLUGIN FUNCTIONS
+function inviteCodes($array)
+{
+	$action = isset($array['data']['action']) ? $array['data']['action'] : null;
+	$code = isset($array['data']['code']) ? $array['data']['code'] : null;
+	$usedBy = isset($array['data']['usedby']) ? $array['data']['usedby'] : null;
+	$username = isset($array['data']['username']) ? $array['data']['username'] : null;
+	$email = isset($array['data']['email']) ? $array['data']['email'] : null;
+	$id = isset($array['data']['id']) ? $array['data']['id'] : null;
+	$now = date("Y-m-d H:i:s");
+	$currentIP = userIP();
+	switch ($action) {
+		case "check":
+			try {
+				$connect = new Dibi\Connection([
+					'driver' => 'sqlite3',
+					'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
+				]);
+				$all = $connect->fetch('SELECT * FROM invites WHERE valid = "Yes" AND code = ?', $code);
+				return ($all) ? true : false;
+			} catch (Dibi\Exception $e) {
+				return false;
+			}
+			break;
+		case "use":
+			try {
+				if (inviteCodes(array('data' => array('action' => 'check', 'code' => $code)))) {
+					$connect = new Dibi\Connection([
+						'driver' => 'sqlite3',
+						'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
+					]);
+					$connect->query('
+	                	UPDATE invites SET', [
+						'valid' => 'No',
+						'usedby' => $usedBy,
+						'dateused' => $now,
+						'ip' => $currentIP,
+					], '
+	                	WHERE code=?', $code);
+					writeLog('success', 'Invite Management Function -  Invite Used [' . $code . ']', 'SYSTEM');
+					return inviteAction($usedBy, 'share', $GLOBALS['INVITES-type-include']);
+				} else {
+					return false;
+				}
+			} catch (Dibi\Exception $e) {
+				return false;
+			}/*
+            if(ENABLEMAIL){
+                if (!isset($GLOBALS['USER'])) {
+                    require_once("user.php");
+                    $GLOBALS['USER'] = new User('registration_callback');
+                }
+                $emailTemplate = array(
+                    'type' => 'mass',
+                    'body' => 'The user: {user} has reddemed the code: {inviteCode} his IP Address was '.$currentIP,
+                    'subject' => 'Invite Code '.$code.' Has Been Used',
+                    'user' => $usedBy,
+                    'password' => null,
+                    'inviteCode' => $code,
+                );
+                $emailTemplate = emailTemplate($emailTemplate);
+                $subject = $emailTemplate['subject'];
+                $body = buildEmail($emailTemplate);
+                sendEmail($GLOBALS['USER']->adminEmail, "Admin", $subject, $body);
+            }*/
+			break;
+		default:
+			if (qualifyRequest(1)) {
+				switch ($action) {
+					case "create":
+						try {
+							$connect = new Dibi\Connection([
+								'driver' => 'sqlite3',
+								'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
+							]);
+							$newCode = [
+								'code' => $code,
+								'email' => $email,
+								'username' => $username,
+								'valid' => 'Yes',
+								'type' => $GLOBALS['INVITES-type-include'],
+							];
+							$connect->query('INSERT INTO [invites]', $newCode);
+							writeLog('success', 'Invite Management Function -  Added Invite [' . $code . ']', $GLOBALS['organizrUser']['username']);
+							if ($GLOBALS['PHPMAILER-enabled']) {
+								$emailTemplate = array(
+									'type' => 'invite',
+									'body' => $GLOBALS['PHPMAILER-emailTemplateInviteUser'],
+									'subject' => $GLOBALS['PHPMAILER-emailTemplateInviteUserSubject'],
+									'user' => $username,
+									'password' => null,
+									'inviteCode' => $code,
+								);
+								$emailTemplate = phpmEmailTemplate($emailTemplate);
+								$sendEmail = array(
+									'to' => $email,
+									'subject' => $emailTemplate['subject'],
+									'body' => phpmBuildEmail($emailTemplate),
+								);
+								phpmSendEmail($sendEmail);
+							}
+							return true;
+						} catch (Dibi\Exception $e) {
+							writeLog('error', 'Invite Management Function  -  Error [' . $e . ']', 'SYSTEM');
+							return false;
+						}
+						break;
+					case "get":
+						try {
+							$connect = new Dibi\Connection([
+								'driver' => 'sqlite3',
+								'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
+							]);
+							$invites = $connect->fetchAll('SELECT * FROM invites');
+							return $invites;
+						} catch (Dibi\Exception $e) {
+							writeLog('error', 'Invite Management Function  -  Error [' . $e . ']', 'SYSTEM');
+							return false;
+						}
+						break;
+					case "delete":
+						try {
+							$connect = new Dibi\Connection([
+								'driver' => 'sqlite3',
+								'database' => $GLOBALS['dbLocation'] . $GLOBALS['dbName'],
+							]);
+							$connect->query('DELETE FROM invites WHERE id = ?', $id);
+							return true;
+						} catch (Dibi\Exception $e) {
+							writeLog('error', 'Invite Management Function  -  Error [' . $e . ']', 'SYSTEM');
+							return false;
+						}
+						break;
+					default:
+						return false;
+				}
+			}
+	}
+}
+
+/* GET PHPMAILER SETTINGS */
+function invitesGetSettings()
+{
+	if ($GLOBALS['plexID'] !== '' && $GLOBALS['plexToken'] !== '' && $GLOBALS['INVITES-type-include'] !== '') {
+		$loop = libraryList($GLOBALS['INVITES-type-include'])['libraries'];
+		foreach ($loop as $key => $value) {
+			$libraryList[] = array(
+				'name' => $key,
+				'value' => $value
+			);
+		}
+	} else {
+		$libraryList = array(
+			array(
+				'name' => 'Refresh page to update List',
+				'value' => '',
+				'disabled' => true,
+			),
+		);
+	}
+	return array(
+		'Backend' => array(
+			array(
+				'type' => 'select',
+				'name' => 'INVITES-type-include',
+				'label' => 'Media Server',
+				'value' => $GLOBALS['INVITES-type-include'],
+				'options' => array(
+					array(
+						'name' => 'N/A',
+						'value' => 'n/a'
+					),
+					array(
+						'name' => 'Plex',
+						'value' => 'plex'
+					),
+					array(
+						'name' => 'Emby [Not Ready]',
+						'value' => 'emby'
+					)
+				)
+			)
+		),
+		'Plex Settings' => array(
+			array(
+				'type' => 'password-alt',
+				'name' => 'plexToken',
+				'label' => 'Plex Token',
+				'value' => $GLOBALS['plexToken'],
+				'placeholder' => 'Use Get Token Button'
+			),
+			array(
+				'type' => 'password-alt',
+				'name' => 'plexID',
+				'label' => 'Plex Machine',
+				'value' => $GLOBALS['plexID'],
+				'placeholder' => 'Use Get Plex Machine Button'
+			),
+			array(
+				'type' => 'select2',
+				'class' => 'select2-multiple invite-select',
+				'name' => 'INVITES-plexLibraries',
+				'label' => 'Libraries',
+				'value' => $GLOBALS['INVITES-plexLibraries'],
+				'options' => $libraryList
+			)
+		),
+		'Emby Settings' => array(),
+		'FYI' => array(
+			array(
+				'type' => 'html',
+				'label' => 'Note',
+				'html' => 'After enabling for the first time, please reload the page - Menu is located under User menu on top right'
+			)
+		)
+	);
+}
+
+function inviteAction($username, $action = null, $type = null)
+{
+	if ($action == null) {
+		return false;
+	}
+	switch ($type) {
+		case 'plex':
+			if (!empty($GLOBALS['plexToken']) && !empty($GLOBALS['plexID'])) {
+				$url = "https://plex.tv/api/servers/" . $GLOBALS['plexID'] . "/shared_servers/";
+				if ($GLOBALS['INVITES-plexLibraries'] !== "") {
+					$libraries = explode(',', $GLOBALS['INVITES-plexLibraries']);
+				} else {
+					$libraries = '';
+				}
+				$headers = array(
+					"Accept" => "application/json",
+					"Content-Type" => "application/json",
+					"X-Plex-Token" => $GLOBALS['plexToken']
+				);
+				$data = array(
+					"server_id" => $GLOBALS['plexID'],
+					"shared_server" => array(
+						"library_section_ids" => $libraries,
+						"invited_email" => $username
+					)
+				);
+				try {
+					switch ($action) {
+						case 'share':
+							$response = Requests::post($url, $headers, json_encode($data), array());
+							break;
+						case 'unshare':
+							$id = (is_numeric($username) ? $username : convertPlexName($username, "id"));
+							$url = $url . $id;
+							$response = Requests::delete($url, $headers, array());
+							break;
+						default:
+							return false;
+							break;
+					}
+					if ($response->success) {
+						writeLog('success', 'Plex Invite Function - Plex User now has access to system', $username);
+						return true;
+					} else {
+						switch ($response->status_code) {
+							case 400:
+								writeLog('error', 'Plex Invite Function - Plex User already has access', $username);
+								return false;
+								break;
+							case 401:
+								writeLog('error', 'Plex Invite Function - Incorrect Token', 'SYSTEM');
+								return false;
+								break;
+							case 404:
+								writeLog('error', 'Plex Invite Function - Libraries not setup correct [' . $GLOBALS['INVITES-plexLibraries'] . ']', 'SYSTEM');
+								return false;
+								break;
+							default:
+								writeLog('error', 'Plex Invite Function - An error occurred [' . $response->status_code . ']', $username);
+								return false;
+								break;
+						}
+					}
+				} catch (Requests_Exception $e) {
+					writeLog('error', 'Plex Invite Function - Error: ' . $e->getMessage(), 'SYSTEM');
+					return false;
+				};
+			} else {
+				writeLog('error', 'Plex Invite Function - Plex Token/ID not set', 'SYSTEM');
+				return false;
+			}
+			break;
+		case 'emby':
+			# code...
+			break;
+		default:
+			return false;
+			break;
+	}
+	return false;
+}

+ 207 - 0
api/plugins/js/chat.js

@@ -0,0 +1,207 @@
+// FUNCTIONS FOR CHAT
+chatLaunch()
+function chatLaunch(){
+    if(typeof activeInfo == 'undefined'){
+        setTimeout(function () {
+            chatLaunch();
+        }, 1000);
+    }else{
+        if(activeInfo.plugins["CHAT-enabled"] == true && activeInfo.plugins.includes["CHAT-authKey-include"] !== '' && activeInfo.plugins.includes["CHAT-appID-include"] !== '' && activeInfo.plugins.includes["CHAT-cluster-include"] !== ''){
+            if (activeInfo.user.groupID <= activeInfo.plugins.includes["CHAT-Auth-include"]) {
+                var menuList = `<li><a class=""  onclick="tabActions(event,'chat','plugin');chatEntry();"><i class="fa fa-comments-o fa-fw"></i> <span lang="en">Chat</span><small class="chat-counter label label-rouded label-info pull-right hidden">0</small></a></li>`;
+				var htmlDOM = `
+                <div id="container-plugin-chat" class="plugin-container hidden">
+                    <div class="chat-main-box bg-org">
+                        <!-- .chat-left-panel -->
+                        <div class="chat-left-aside">
+                            <div class="open-panel"><i class="ti-angle-right"></i></div>
+                            <div class="chat-left-inner bg-org"><ul class="chatonline style-none "></ul></div>
+                        </div>
+                        <!-- .chat-left-panel -->
+                        <!-- .chat-right-panel -->
+                        <div class="chat-right-aside">
+                            <div class="chat-box">
+                                <ul class="chat-list p-t-30"></ul>
+                                <div class="row send-chat-box">
+                                    <div class="col-sm-12">
+                                        <textarea class="form-control chat-input-send" placeholder="Type your message"></textarea>
+                                        <div class="custom-send">
+                                            <button type="button" class="btn btn-info btn-lg custom-send-button"><i class="fa fa-paper-plane fa-2x"></i> </button>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        <!-- .chat-right-panel -->
+                    </div>
+                </div>
+		    	`;
+				$('.append-menu').after(menuList);
+	            $('.plugin-listing').append(htmlDOM);
+	            pageLoad();
+                // Enable pusher logging - don't include this in production
+                //Pusher.logToConsole = true;
+                // Add API Key & cluster here to make the connection
+                var pusher = new Pusher(activeInfo.plugins.includes["CHAT-authKey-include"], {
+                    cluster: activeInfo.plugins.includes["CHAT-cluster-include"],
+                    encrypted: true
+                });
+                // Enter a unique channel you wish your users to be subscribed in.
+                var channel = pusher.subscribe('org_channel');
+                // bind the server event to get the response data and append it to the message div
+                channel.bind('my-event',
+                    function(data) {
+                        formatMessage(data);
+                        $('.chat-list').append(formatMessage(data));
+                        $('.custom-send').html('<button type="button" class="btn btn-info btn-lg custom-send-button"><i class="fa fa-paper-plane fa-2x"></i> </button>');
+                        $(".chat-list").scrollTop($(".chat-list")[0].scrollHeight);
+                        if($('#container-plugin-chat').hasClass('hidden')){
+                            var chatSound =  new Audio(activeInfo.plugins.includes["CHAT-newMessageSound-include"]);
+                            chatSound.play();
+                            message(data.username,data.message,activeInfo.settings.notifications.position,"#FFF","success","20000");
+                            $('.profile-image').addClass('animated loop-animation rubberBand');
+                            $('.chat-counter').removeClass('hidden').html(parseInt($('.chat-counter').text()) + 1);
+                        }
+                    });
+                // check if the user is subscribed to the above channel
+                channel.bind('pusher:subscription_succeeded', function(members) {
+                    console.log('Chat Websocket Connected!');
+                    console.log('Connecting to Organizr Chat DB');
+                    getMessagesAndUsers(activeInfo.settings.homepage.refresh["CHAT-userRefreshTimeout"], true);
+                });
+                /*jslint browser: true*/
+                /*global $, jQuery, alert*/
+                $(document).ready(function () {
+                    "use strict";
+                    $('.chat-left-inner > .chatonline').slimScroll({
+                        height: '100%',
+                        position: 'right',
+                        size: "0px",
+                        color: '#dcdcdc'
+
+                    });
+                    $('.chat-list').slimScroll({
+                        height: '100%',
+                        position: 'right',
+                        size: "0px",
+                        color: '#dcdcdc',
+                        start: 'bottom',
+                    });
+                    $(".open-panel").on("click", function () {
+                        $(".chat-left-aside").toggleClass("open-pnl");
+                        $(".open-panel i").toggleClass("ti-angle-left");
+                    });
+                });
+			}
+        }
+    }
+}
+$(document).on('click', '#CHAT-settings-button', function() {
+    var post = {
+        plugin:'chat/settings/get', // used for switch case in your API call
+    };
+    ajaxloader(".content-wrap","in");
+    organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
+        var response = JSON.parse(data);
+        $('#CHAT-settings-items').html(buildFormGroup(response.data));
+    }).fail(function(xhr) {
+        console.error("Organizr Function: API Connection Failed");
+    });
+    ajaxloader();
+});
+//Chat functions!
+$(document).on('keypress', '.chat-input-send', function(ev) {
+    var keycode = (ev.keyCode ? ev.keyCode : ev.which);
+    if (keycode == '13') {
+        ev.preventDefault();
+        $('.custom-send-button').click();
+    }
+});
+// Send the Message enter by User
+$('body').on('click', '.custom-send-button', function(e) {
+    e.preventDefault();
+    var message = $('.chat-input-send').val();
+    // Validate Name field
+    if (message !== '') {
+        var post = {
+            plugin:'chat/message',
+            message:message
+        };
+        organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
+            // Nada yet
+        }).fail(function(xhr) {
+            console.error("Organizr Function: API Connection Failed");
+        });
+        // Clear the message input field
+        $('.chat-input-send').val('');
+        // Show a loading image while sending
+        $('.custom-send').html('<button type="button" class="btn btn-info btn-lg custom-send-button" disabled><i class="fa fa-spinner fa-pulse fa-2x"></i> </button>');
+    }
+});
+function formatMessage(msg){
+    var className = 'odd';
+    if(msg.username == activeInfo.user.username){
+        if(activeInfo.user.username == 'Guest' && activeInfo.user.uid !== msg.uid){
+            className = '';
+        }
+    }else{
+        className = '';
+    }
+    return `
+        <li class="`+className+`">
+            <div class="chat-image"> <img alt="male" src="`+msg.gravatar+`"> </div>
+            <div class="chat-body">
+                <div class="chat-text">
+                    <h4>`+msg.username+`</h4>
+                    <p> `+msg.message+` </p> <b>`+moment.utc(msg.date, "YYYY-MM-DD hh:mm").local().format('LLL')+`</b> </div>
+            </div>
+        </li>
+    `;
+}
+function formatUsers(array){
+    var users = {};
+    var userList = '';
+    array.reverse();
+    $.each(array, function (i, v){
+        if(!users.hasOwnProperty(v.username)){
+            users[v.username] = {
+                'last':v.date,
+                'gravatar':v.gravatar
+            }
+        }
+    });
+    $.each(users, function (i, v) {
+        userList += `
+            <li>
+                <a href="javascript:void(0)"><img src="`+v.gravatar+`" alt="user-img" class="img-circle"> <span>`+i+`<small class="text-success">`+moment.utc(v.last, "YYYY-MM-DD hh:mm[Z]").local().fromNow()+`</small></span></a>
+            </li>
+        `;
+    });
+    userList += '<li class="p-20"></li>';
+    return userList;
+}
+function chatEntry(){
+    $(".chat-list").scrollTop($(".chat-list")[0].scrollHeight);
+    $('.chat-input-send').focus();
+    $('.chat-counter').addClass('hidden').html('0');
+}
+function getMessagesAndUsers(timeout, initial = false){
+    var timeout = (typeof timeout !== 'undefined') ? timeout : activeInfo.settings.homepage.refresh["CHAT-userRefreshTimeout"];
+    organizrAPI('GET','api/?v1/plugin&plugin=chat&cmd=chat/message').success(function(data) {
+        var response = JSON.parse(data);
+        if(initial == true){
+            $.each(response.data, function (i, v){
+                $('.chat-list').append(formatMessage(v));
+            });
+        }
+        $('.chatonline').html(formatUsers(response.data));
+    }).fail(function(xhr) {
+        console.error("Organizr Function: API Connection Failed");
+    });
+    var timeoutTitle = 'ChatUserList';
+    if(typeof timeouts[timeoutTitle] !== 'undefined'){ clearTimeout(timeouts[timeoutTitle]); }
+    timeouts[timeoutTitle] = setTimeout(function(){ getMessagesAndUsers(timeout, false); }, timeout);
+}
+$(document).on('click', '.profile-pic', function(e) {
+    $('.profile-image').removeClass('animated loop-animation rubberBand');
+});

+ 449 - 0
api/plugins/js/invites.js

@@ -0,0 +1,449 @@
+/* PHP MAILER JS FILE */
+/*
+$(document).on('click', '#PHPMAILER-settings-button', function() {
+	var post = {
+        plugin:'PHPMailer/settings/get', // used for switch case in your API call
+        api:'api/?v1/plugin', // API Endpoint will always be this for custom plugin API calls
+        name:$(this).attr('data-plugin-name'),
+        configName:$(this).attr('data-config-name'),
+        messageTitle:'', // Send succees message title (top line)
+        messageBody:'Disabled '+$(this).attr('data-plugin-name'), // Send succees message body (bottom line)
+        error:'Organizr Function: API Connection Failed' // conole error message
+    };
+	var callbacks = $.Callbacks(); // init callbacks var
+    //callbacks.add(  ); // add function to callback to be fired after API call
+    //settingsAPI(post,callbacks); // exec API call
+    //ajaxloader(".content-wrap","in");
+    //setTimeout(function(){ buildPlugins();ajaxloader(); }, 3000);
+});
+*/
+
+// FUNCTIONS
+inviteLaunch()
+function inviteLaunch(){
+    if(typeof activeInfo == 'undefined'){
+        setTimeout(function () {
+            inviteLaunch();
+        }, 1000);
+    }else{
+        var menuList = '';
+    	var htmlDOM = `
+    	<div id="invite-area" class="white-popup mfp-with-anim mfp-hide">
+    		<div class="col-md-10 col-md-offset-1">
+    			<div class="invite-div"></div>
+    		</div>
+    	</div>
+    	`;
+        if(activeInfo.plugins["INVITES-enabled"] == true){
+            if (activeInfo.user.loggedin === true && activeInfo.user.groupID <= 1) {
+                menuList = `<li><a class="inline-popups inviteModal" href="#invite-area" data-effect="mfp-zoom-out"><i class="fa fa-ticket fa-fw"></i> <span lang="en">Manage Invites</span></a></li>`;
+                htmlDOM += `
+            	<div id="new-invite-area" class="white-popup mfp-with-anim mfp-hide">
+            		<div class="col-md-10 col-md-offset-1">
+                        <div class="col-md-12">
+                            <div class="panel panel-info m-b-0">
+                                <div class="panel-heading" lang="en">New Invite</div>
+                                <div class="panel-wrapper collapse in" aria-expanded="true">
+                                    <div class="panel-body">
+
+                                        <form id="new-invite-form">
+                                            <fieldset style="border:0;">
+                                            <div class="form-group">
+                                                <label class="control-label" for="new-invite-form-inputUsername" lang="en">Name or Username</label>
+                                                <input type="text" class="form-control" id="new-invite-form-inputUsername" name="username" required="" autofocus="">
+                                            </div>
+                                            <div class="form-group">
+                                                <label class="control-label" for="new-invite-form-inputEmail" lang="en">Email</label>
+                                                <input type="text" class="form-control" id="new-invite-form-inputEmail" name="email" required="" autofocus="">
+                                            </div>
+                                            </fieldset>
+                                            <button class="btn btn-sm btn-info btn-rounded waves-effect waves-light pull-right row b-none" onclick="createNewInvite();" type="button"><span class="btn-label"><i class="fa fa-plus"></i></span><span lang="en">Create/Send Invite</span></button>
+                                            <div class="clearfix"></div>
+                                        </form>
+
+
+
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        <div class="clearfix"></div>
+            		</div>
+            	</div>`;
+            }else if (activeInfo.user.loggedin === false){
+                menuList = `<li><a class="inline-popups inviteModal" href="#invite-area" data-effect="mfp-zoom-out"><i class="fa fa-ticket fa-fw"></i> <span lang="en">Use Invite Code</span></a></li>`;
+            }
+            $('.append-menu').after(menuList);
+            $('.organizr-area').after(htmlDOM);
+            pageLoad();
+            getInvite();
+        }
+    }
+}
+function joinPlex(){
+    var username = $('#invitePlexJoinUsername');
+    var email = $('#invitePlexJoinEmail');
+    var password = $('#invitePlexJoinPassword');
+    if(username.val() == ''){
+        username.focus();
+        message('Invite Error',' Please Enter Username',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+    }else if(email.val() == ''){
+        email.focus();
+        message('Invite Error',' Please Enter Email',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+    }else if(password.val() == ''){
+        password.focus();
+        message('Invite Error',' Please Enter Passowrd',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+    }
+    if(email.val() !== '' && username.val() !== '' && password.val() !== ''){
+        organizrAPI('POST','api/?v1/plex/join',{username:username.val(), email:email.val(), password:password.val()}).success(function(data) {
+    		var response = JSON.parse(data);
+            if(response.data === true){
+                $('.invite-step-3-plex-no').toggleClass('hidden');
+                $('.invite-step-3-plex-yes').toggleClass('hidden');
+                message('Invite Function',' User Created',activeInfo.settings.notifications.position,'#FFF','success','5000');
+                $('#inviteUsernameInvite').val(username.val());
+                hasPlexUsername();
+            }else{
+                message('Invite Error',' '+response.data,activeInfo.settings.notifications.position,'#FFF','warning','5000');
+            }
+    	}).fail(function(xhr) {
+    		console.error("Organizr Function: API Connection Failed");
+    	});
+    }
+}
+function inviteHasAccount(type,value){
+    switch (type) {
+        case 'plex':
+            if(value){
+                $('.invite-step-2').toggleClass('hidden');
+                $('.invite-step-3-plex-yes').toggleClass('hidden');
+            }else{
+                $('.invite-step-2').toggleClass('hidden');
+                $('.invite-step-3-plex-no').toggleClass('hidden');
+            }
+            break;
+        default:
+        alert(type+' is not set up yet');
+    }
+}
+function hasPlexUsername(){
+    var code = $('#inviteCodeInput').val().toUpperCase();
+    var username = $('#inviteUsernameInvite');
+    if(username.val() == ''){
+        username.focus();
+        message('Invite Error',' Please Enter Username',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+    }else{
+        var post = {
+            plugin:'Invites/codes',
+            action:'use',
+            code:code,
+            usedby:username.val()
+        };
+        ajaxloader(".content-wrap","in");
+        organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
+            var response = JSON.parse(data);
+            if(response.data === true){
+                $('.invite-step-3-plex-yes').toggleClass('hidden');
+                $('.invite-step-4-plex-accept').toggleClass('hidden');
+                if(local('get', 'invite')){
+            		local('remove', 'invite');
+            	}
+            }else{
+                message('Invite Error',' Code Incorrect',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+            }
+            ajaxloader();;
+        }).fail(function(xhr) {
+            console.error("Organizr Function: API Connection Failed");
+            ajaxloader();
+        });
+    }
+}
+function verifyInvite(){
+    var code = $('#inviteCodeInput').val().toUpperCase();
+    var post = {
+        plugin:'Invites/codes',
+        action:'check',
+        code:code
+    };
+    ajaxloader(".content-wrap","in");
+    organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
+        var response = JSON.parse(data);
+        if(response.data === true){
+            $('.invite-step-1').toggleClass('hidden');
+            $('.invite-step-2').toggleClass('hidden');
+        }else{
+            message('Invite Error',' Code Incorrect',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+        }
+        if(local('get', 'invite')){
+            local('remove', 'invite');
+        }
+        ajaxloader();;
+    }).fail(function(xhr) {
+        console.error("Organizr Function: API Connection Failed");
+        ajaxloader();
+    });
+}
+function getInvite(invite=null){
+	if(invite){
+		local('set','invite',invite);
+	}
+	if($.urlParam('invite') !== null){
+		local('set','invite',$.urlParam('invite'));
+	}
+	if(local('get', 'invite')){
+		//show error page
+		$('.inviteModal').trigger('click');
+        $('#inviteCodeInput').val(local('get', 'invite'));
+		window.history.pushState({}, document.title, "./" );
+        local('remove', 'invite');
+	}
+
+}
+function createNewInvite(){
+    var username = $('#new-invite-form-inputUsername');
+    var email = $('#new-invite-form-inputEmail');
+    if(username.val() == ''){
+        username.focus();
+        message('Invite Error',' Please Enter Username',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+    }else if(email.val() == ''){
+        email.focus();
+        message('Invite Error',' Please Enter Email',activeInfo.settings.notifications.position,'#FFF','warning','5000');
+    }
+
+    if(email.val() !== '' && username.val() !== ''){
+        var post = {
+            plugin:'Invites/codes',
+            action:'create',
+            code:createRandomString(6).toUpperCase(),
+            email:email.val(),
+            username:username.val(),
+        };
+        ajaxloader(".content-wrap","in");
+        organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
+            var response = JSON.parse(data);
+            $.magnificPopup.close();
+            ajaxloader();
+            message('Invite',' Invite Created',activeInfo.settings.notifications.position,'#FFF','success','5000');
+        }).fail(function(xhr) {
+            console.error("Organizr Function: API Connection Failed");
+            ajaxloader();
+            message('Invite Error',' An Error Occured',activeInfo.settings.notifications.position,'#FFF','error','5000');
+        });
+    }
+
+}
+function deleteInvite(id){
+    var post = {
+        plugin:'Invites/codes',
+        action:'delete',
+        id:id,
+    };
+    ajaxloader(".content-wrap","in");
+    organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
+        var response = JSON.parse(data);
+        $('#inviteItem-'+id).remove();
+        //$.magnificPopup.close();
+        ajaxloader();
+        message('Invite',' Invite Deleted',activeInfo.settings.notifications.position,'#FFF','success','5000');
+    }).fail(function(xhr) {
+        console.error("Organizr Function: API Connection Failed");
+        ajaxloader();
+        message('Invite Error',' An Error Occured',activeInfo.settings.notifications.position,'#FFF','error','5000');
+    });
+
+}
+// EVENTS and LISTENERS
+function buildInvites(array){
+    if(array.length == 0){
+		return '<h2 class="text-center" lang="en">No Invites</h2>';
+	}
+    var invites = '';
+	$.each(array, function(i,v) {
+        v.dateused = (v.dateused) ? v.dateused : '-';
+        v.usedby = (v.usedby) ? v.usedby : '-';
+        v.ip = (v.ip) ? v.ip : '-';
+        invites += `
+        <tr id="inviteItem-`+v.id+`">
+            <td class="text-center">`+v.id+`</td>
+            <td>`+v.username+`</td>
+            <td>`+v.email+`</td>
+            <td>`+v.code+`</td>
+            <td>`+v.date+`</td>
+            <td>`+v.dateused+`</td>
+            <td>`+v.usedby+`</td>
+            <td>`+v.ip+`</td>
+            <td>`+v.valid+`</td>
+            <td><button type="button" class="btn btn-danger btn-outline btn-circle btn-lg m-r-5" onclick="deleteInvite('`+v.id+`');"><i class="ti-trash"></i></button></td>
+        </tr>
+        `;
+    });
+    return invites;
+}
+$(document).on('click', '.inviteModal', function() {
+    var htmlDOM = '';
+    if (activeInfo.user.loggedin === true && activeInfo.user.groupID <= 1) {
+        var post = {
+            plugin:'Invites/codes',
+            action:'get',
+        };
+        ajaxloader(".content-wrap","in");
+        organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
+            var response = JSON.parse(data);
+            var htmlDOM = '';
+            htmlDOM = `
+            <div class="col-md-12">
+                <div class="panel bg-org panel-info">
+                    <div class="panel-heading">
+                        <span lang="en">Manage Invites</span>
+                        <button type="button" class="btn btn-info btn-circle pull-right popup-with-form" href="#new-invite-area" data-effect="mfp-3d-unfold"><i class="fa fa-plus"></i> </button>
+                    </div>
+                    <div class="table-responsive">
+                        <table class="table table-hover manage-u-table">
+                            <thead>
+                                <tr>
+                                    <th width="70" class="text-center">#</th>
+                                    <th lang="en">USERNAME</th>
+                                    <th lang="en">EMAIL</th>
+                                    <th lang="en">INVITE CODE</th>
+                                    <th lang="en">DATE SENT</th>
+                                    <th lang="en">DATE USED</th>
+                                    <th lang="en">USED BY</th>
+                                    <th lang="en">IP ADDRESS</th>
+                                    <th lang="en">VALID</th>
+                                    <th lang="en">DELETE</th>
+                                </tr>
+                            </thead>
+                            <tbody id="manageInviteTable">
+                                `+buildInvites(response.data)+`
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
+            </div>
+            <div class="clearfix"></div>
+            `;
+            $('.invite-div').html(htmlDOM);
+        }).fail(function(xhr) {
+            console.error("Organizr Function: API Connection Failed");
+        });
+        ajaxloader();
+    }else if (activeInfo.user.loggedin === false){
+        htmlDOM = `
+        <div class="col-md-12">
+            <div class="panel panel-info m-b-0">
+                <div class="panel-heading" lang="en">Use Invite Code</div>
+                <div class="panel-wrapper collapse in" aria-expanded="true">
+                    <div class="panel-body">
+                        <div class="form-group invite-step-1">
+                            <div class="input-group" style="width: 100%;">
+                                <div class="input-group-addon hidden-xs"><i class="ti-lock"></i></div>
+                                <input type="text" class="form-control text-uppercase" id="inviteCodeInput" placeholder="Code" autocomplete="off" autocorrect="off" autocapitalize="off" maxlength="6" spellcheck="false" autofocus="" required="">
+                            </div>
+                            <br />
+                            <button class="btn btn-block btn-info" onclick="verifyInvite();">Verify</button>
+
+                        </div>
+                        <div class="form-group invite-step-2 hidden">
+
+
+                            <div class="row">
+                                <h2 class="text-center" lang="en">Do you have a `+activeInfo.plugins.includes["INVITES-type-include"].toUpperCase()+` account?</h2>
+                                <div class="col-lg-6">
+                                    <button class="btn btn-block btn-info m-b-10" onclick="inviteHasAccount('`+activeInfo.plugins.includes["INVITES-type-include"]+`',true);" lang="en">Yes</button>
+                                </div>
+                                <div class="col-lg-6">
+                                    <button class="btn btn-block btn-primary m-b-10" onclick="inviteHasAccount('`+activeInfo.plugins.includes["INVITES-type-include"]+`',false);" lang="en">No</button>
+                                </div>
+                            </div>
+
+                        </div>
+                        <div class="form-group invite-step-3-plex-yes hidden">
+                            <div class="input-group" style="width: 100%;">
+                                <div class="input-group-addon hidden-xs"><i class="ti-user"></i></div>
+                                <input type="text" class="form-control" id="inviteUsernameInvite" placeholder="Plex Username" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus="" required="">
+                            </div>
+                            <br />
+                            <button class="btn btn-block btn-info" onclick="hasPlexUsername();">Submit</button>
+                        </div>
+                        <div class="form-group invite-step-3-plex-no hidden">
+                            <div class="input-group" style="width: 100%;">
+                                <div class="input-group-addon hidden-xs"><i class="ti-user"></i></div>
+                                <input type="text" class="form-control" id="invitePlexJoinUsername" lang="en" placeholder="Username" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus="" required="">
+                            </div>
+                            <div class="input-group" style="width: 100%;">
+                                <div class="input-group-addon hidden-xs"><i class="ti-email"></i></div>
+                                <input type="text" class="form-control" id="invitePlexJoinEmail" lang="en" placeholder="E-Mail" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" required="">
+                            </div>
+                            <div class="input-group" style="width: 100%;">
+                                <div class="input-group-addon hidden-xs"><i class="ti-user"></i></div>
+                                <input type="password" class="form-control" id="invitePlexJoinPassword" lang="en" placeholder="Password" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"  required="">
+                            </div>
+                            <br />
+                            <button class="btn btn-block btn-info" onclick="joinPlex();">Submit</button>
+                        </div>
+                        <div class="form-group invite-step-3-emby-yes hidden">
+                        </div>
+                        <div class="form-group invite-step-3-emby-no hidden">
+                        </div>
+                        <div class="form-group invite-step-4-plex-accept hidden">
+                            <h4 class="" lang="en">You have been invited.  Please goto <a href="https://plex.tv" target="_blank">PLEX.TV</a> and login to accept the invite.  Once you have done that, you may head back here and login with your credentials.</h4>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="clearfix"></div>
+        `;
+        $('.invite-div').html(htmlDOM);
+    }
+});
+
+// CHANGE CUSTOMIZE Options
+$(document).on('change asColorPicker::close', '#INVITES-settings-page1 :input', function(e) {
+    var input = $(this);
+    switch ($(this).attr('type')) {
+        case 'switch':
+        case 'checkbox':
+            var value = $(this).prop("checked") ? true : false;
+            break;
+        default:
+            var value = $(this).val().toString();
+    }
+	var post = {
+        api:'api/?v1/update/config',
+        name:$(this).attr("name"),
+        type:$(this).attr("data-type"),
+        value:value,
+        messageTitle:'',
+        messageBody:'Updated Value for '+$(this).parent().parent().find('label').text(),
+        error:'Organizr Function: API Connection Failed'
+    };
+	var callbacks = $.Callbacks();
+    //callbacks.add( buildCustomizeAppearance );
+    settingsAPI(post,callbacks);
+    //disable button then renable
+    $('#INVITES-settings-page :input').prop('disabled', 'true');
+    setTimeout(
+        function(){
+            $('#INVITES-settings-page :input').prop('disabled', null);
+            input.emulateTab();
+        },
+        2000
+    );
+
+});
+$(document).on('click', '#INVITES-settings-button', function() {
+    var post = {
+        plugin:'Invites/settings/get', // used for switch case in your API call
+    };
+    ajaxloader(".content-wrap","in");
+    organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
+        var response = JSON.parse(data);
+        $('#INVITES-settings-items').html(buildFormGroup(response.data));
+        $(".invite-select").select2();
+        $('.selectpicker').selectpicker();
+    }).fail(function(xhr) {
+        console.error("Organizr Function: API Connection Failed");
+    });
+    ajaxloader();
+});

+ 319 - 0
api/plugins/js/php-mailer.js

@@ -0,0 +1,319 @@
+/* PHP MAILER JS FILE */
+/*
+$(document).on('click', '#PHPMAILER-settings-button', function() {
+	var post = {
+        plugin:'PHPMailer/settings/get', // used for switch case in your API call
+        api:'api/?v1/plugin', // API Endpoint will always be this for custom plugin API calls
+        name:$(this).attr('data-plugin-name'),
+        configName:$(this).attr('data-config-name'),
+        messageTitle:'', // Send succees message title (top line)
+        messageBody:'Disabled '+$(this).attr('data-plugin-name'), // Send succees message body (bottom line)
+        error:'Organizr Function: API Connection Failed' // conole error message
+    };
+	var callbacks = $.Callbacks(); // init callbacks var
+    //callbacks.add(  ); // add function to callback to be fired after API call
+    //settingsAPI(post,callbacks); // exec API call
+    //ajaxloader(".content-wrap","in");
+    //setTimeout(function(){ buildPlugins();ajaxloader(); }, 3000);
+});
+*/
+
+// FUNCTIONS
+phpmLaunch();
+function phpmLaunch(){
+    if(typeof activeInfo == 'undefined'){
+        setTimeout(function () {
+            phpmLaunch();
+        }, 1000);
+    }else{
+        if(activeInfo.plugins["PHPMAILER-enabled"] == true){
+            if (activeInfo.user.loggedin === true && activeInfo.user.groupID <= 1) {
+                var menuList = `<li><a class="inline-popups emailModal" href="#email-area" data-effect="mfp-zoom-out"><i class="fa fa-envelope fa-fw"></i> <span lang="en">E-Mail Center</span></a></li>`;
+                var htmlDOM = `
+            	<div id="email-area" class="white-popup mfp-with-anim mfp-hide">
+            		<div class="col-md-10 col-md-offset-1">
+            			<div class="email-div"></div>
+            		</div>
+            	</div>
+            	`;
+                $('.organizr-area').after(htmlDOM);
+                $('.append-menu').after(menuList);
+                pageLoad();
+            }
+        }
+    }
+}
+function sendMail(){
+    var to = $('#sendEmailToInput').val();
+    var subject = $('#sendEmailSubjectInput').val();
+    var body = tinyMCE.get('sendEmail').getContent();
+    if(to == ''){
+        messageSingle('','Please Enter Email',activeInfo.settings.notifications.position,'#FFF','error','5000');
+    }else if(subject == ''){
+        messageSingle('','Please Enter Subject',activeInfo.settings.notifications.position,'#FFF','error','5000');
+    }else if(body == ''){
+        messageSingle('','Please Enter Body',activeInfo.settings.notifications.position,'#FFF','error','5000');
+    }
+    if(to !== '' && subject !== '' && body !== ''){
+        var post = {
+            plugin:'PHPMailer/send/email', // used for switch case in your API call
+            bcc:to,
+            subject:subject,
+            body:body
+        };
+        ajaxloader(".content-wrap","in");
+        organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
+            var response = JSON.parse(data);
+            if(response.data == true){
+                $.magnificPopup.close();
+                messageSingle('',window.lang.translate('Email Sent Successful'),activeInfo.settings.notifications.position,'#FFF','success','5000');
+            }else{
+                messageSingle('',response.data,activeInfo.settings.notifications.position,'#FFF','error','5000');
+            }
+        }).fail(function(xhr) {
+            console.error("Organizr Function: API Connection Failed");
+        });
+        ajaxloader();
+    }
+}
+function buildUserList(array){
+    var users = '';
+    var htmlDOM = '';
+	$.each(array, function(i,v) {
+        users += '<option value="'+v+'">'+i+'</option>';
+    });
+    htmlDOM = `
+    <select multiple id="email-user-list" name="email-user-list[]">`+users+`</select>
+    <div class="button-box m-t-20">
+        <a id="select-all-users-list" class="btn btn-danger btn-outline" href="#">select all</a>
+        <a id="deselect-all-users-list" class="btn btn-info btn-outline" href="#">deselect all</a>
+        <a id="minimize-users-list" class="btn btn-primary btn-outline" href="#">minimize</a>
+    </div>`;
+    return htmlDOM;
+}
+function buildEmailModal(){
+    var htmlDOM = `
+    <div class="row">
+        <div class="col-md-12">
+            <div class="panel panel-info m-0">
+                <div class="panel-heading">
+                    <span lang="en">Email Users</span>
+                    <div class="btn-group pull-right">
+
+						<button class="btn btn-info waves-effect waves-light loadUserList" type="button">
+							<i class="fa fa-user"></i>
+						</button>
+                        <button class="btn btn-info waves-effect waves-light" type="button" onclick="$('.mce-i-template').trigger('click');">
+							<i class="fa fa-files-o"></i>
+						</button>
+                        <button class="btn btn-info waves-effect waves-light unhide-user-list hidden" type="button">
+							<i class="fa fa-eye"></i>
+						</button>
+						<button class="btn btn-info waves-effect waves-light" onclick="sendMail();"><i class="fa fa-paper-plane"></i></button>
+
+	                </div>
+                </div>
+                <div class="panel-wrapper collapse in main-email-panel" aria-expanded="true">
+                    <div class="panel-body">
+                        <div class="form-body">
+                            <div class="row">
+                                <div class="col-md-6">
+                                    <div class="form-group">
+                                        <label class="control-label" lang="en">To:</label>
+                                        <input type="text" id="sendEmailToInput" class="form-control"></div>
+                                </div>
+                                <div class="col-md-6">
+                                    <div class="form-group">
+                                        <label class="control-label" lang="en">Subject</label>
+                                        <input type="text" id="sendEmailSubjectInput" class="form-control"></div>
+                                </div>
+                                <div class="col-md-12" id="user-list-div">
+
+
+                                </div>
+                            </div>
+                            <!--/row-->
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <textarea id="sendEmail" name="area"></textarea>
+    `;
+    $('.email-div').html(htmlDOM);
+    if ($("#sendEmail").length > 0) {
+        var templates = [];
+        if(activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-One"] !== ''){
+            templates.push(
+                {
+                    title: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-OneName"],
+                    description: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-OneSubject"],
+                    content: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-One"],
+                }
+            )
+        }
+        if(activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Two"] !== ''){
+            templates.push(
+                {
+                    title: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-TwoName"],
+                    description: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-TwoSubject"],
+                    content: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Two"],
+                }
+            )
+        }
+        if(activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Three"] !== ''){
+            templates.push(
+                {
+                    title: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-ThreeName"],
+                    description: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-ThreeSubject"],
+                    content: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Three"],
+                }
+            )
+        }
+        if(activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Four"] !== ''){
+            templates.push(
+                {
+                    title: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-FourName"],
+                    description: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-FourSubject"],
+                    content: activeInfo.plugins.includes["PHPMAILER-emailTemplateCustom-include-Four"],
+                }
+            )
+        }
+        tinymce.init({
+            selector: "textarea#sendEmail",
+            theme: "modern",
+            height: 300,
+            plugins: [
+                "advlist autolink link image lists charmap print preview hr anchor pagebreak spellchecker", "searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking", "save table contextmenu directionality emoticons template paste textcolor"
+            ],
+            toolbar: "insertfile template undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image | print preview media fullpage | forecolor backcolor",
+            templates: templates,
+            init_instance_callback: function (editor) {
+                editor.on('BeforeSetContent', function (e) {
+                    //tinyMCE.get('sendEmail').execCommand('selectAll');
+                    //tinyMCE.get('sendEmail').execCommand('delete');
+                    $.each(e.target.settings.templates, function(i,v) {
+                        if($.trim(v.content) == $.trim(e.content)){
+                            $('#sendEmailSubjectInput').val(v.description);
+                        }
+                    });
+                });
+              }
+        });
+    }
+
+}
+// EVENTS and LISTENERS
+$(document).on("change", "#email-user-list", function () {
+    $('#sendEmailToInput').val($('#email-user-list').val());
+});
+$(document).on('click', '.loadUserList', function() {
+    var post = {
+        plugin:'PHPMailer/users/get', // used for switch case in your API call
+    };
+    ajaxloader(".content-wrap","in");
+    organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
+        var response = JSON.parse(data);
+        $('#user-list-div').html(buildUserList(response.data));
+        $('#email-user-list').multiSelect();
+    }).fail(function(xhr) {
+        console.error("Organizr Function: API Connection Failed");
+    });
+    ajaxloader();
+});
+$(document).on("click", ".emailModal", function(e) {
+    buildEmailModal();
+});
+$(document).on("click", ".show-login", function(e) {
+    setTimeout(addForgotPassword, 1000);
+});
+$(document).on("click", "#select-all-users-list", function(e) {
+    $('#email-user-list').multiSelect('select_all');
+    return false;
+});
+$(document).on("click", "#deselect-all-users-list", function(e) {
+    $('#email-user-list').multiSelect('deselect_all');
+    return false;
+});
+$(document).on("click", "#minimize-users-list, .unhide-user-list", function(e) {
+    $('.main-email-panel').toggleClass('hidden');
+    $('.loadUserList').toggleClass('hidden');
+    $('.unhide-user-list').toggleClass('hidden');
+    return false;
+});
+function addForgotPassword(){
+    var item = '';
+    if(activeInfo.plugins["PHPMAILER-enabled"] == true){
+        if (activeInfo.user.loggedin === false) {
+            item = `<a href="javascript:void(0)" id="to-recover" class="text-dark pull-right"><i class="fa fa-lock m-r-5"></i> <span lang="en">Forgot pwd?</span></a>`;
+            $('.remember-me').after(item);
+        }
+    }
+}
+// CHANGE CUSTOMIZE Options
+$(document).on('change asColorPicker::close', '#PHPMAILER-settings-page1 :input', function(e) {
+    var input = $(this);
+    switch ($(this).attr('type')) {
+        case 'switch':
+        case 'checkbox':
+            var value = $(this).prop("checked") ? true : false;
+            break;
+        default:
+            var value = $(this).val();
+    }
+	var post = {
+        api:'api/?v1/update/config',
+        name:$(this).attr("name"),
+        type:$(this).attr("data-type"),
+        value:value,
+        messageTitle:'',
+        messageBody:'Updated Value for '+$(this).parent().parent().find('label').text(),
+        error:'Organizr Function: API Connection Failed'
+    };
+	var callbacks = $.Callbacks();
+    //callbacks.add( buildCustomizeAppearance );
+    settingsAPI(post,callbacks);
+    //disable button then renable
+    $('#PHPMAILER-settings-page :input').prop('disabled', 'true');
+    setTimeout(
+        function(){
+            $('#PHPMAILER-settings-page :input').prop('disabled', null);
+            input.emulateTab();
+        },
+        2000
+    );
+
+});
+$(document).on('click', '#PHPMAILER-settings-button', function() {
+    var post = {
+        plugin:'PHPMailer/settings/get', // used for switch case in your API call
+    };
+    ajaxloader(".content-wrap","in");
+    organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
+        var response = JSON.parse(data);
+        $('#PHPMAILER-settings-items').html(buildFormGroup(response.data));
+    }).fail(function(xhr) {
+        console.error("Organizr Function: API Connection Failed");
+    });
+    ajaxloader();
+});
+// SEND TEST EMAIL
+$(document).on('click', '.phpmSendTestEmail', function() {
+    messageSingle('',window.lang.translate('Sending Test E-Mail'),activeInfo.settings.notifications.position,'#FFF','info','5000');
+    var post = {
+        plugin:'PHPMailer/send/test', // used for switch case in your API call
+    };
+    ajaxloader(".content-wrap","in");
+    organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
+        var response = JSON.parse(data);
+        if(response.data == true){
+            messageSingle('',window.lang.translate('Email Test Successful'),activeInfo.settings.notifications.position,'#FFF','success','5000');
+        }else{
+            messageSingle('',response.data,activeInfo.settings.notifications.position,'#FFF','error','5000');
+        }
+    }).fail(function(xhr) {
+        console.error("Organizr Function: API Connection Failed");
+    });
+    ajaxloader();
+});

+ 192 - 0
api/plugins/js/speedTest.js

@@ -0,0 +1,192 @@
+/* PHP MAILER JS FILE */
+/*
+$(document).on('click', '#PHPMAILER-settings-button', function() {
+	var post = {
+        plugin:'PHPMailer/settings/get', // used for switch case in your API call
+        api:'api/?v1/plugin', // API Endpoint will always be this for custom plugin API calls
+        name:$(this).attr('data-plugin-name'),
+        configName:$(this).attr('data-config-name'),
+        messageTitle:'', // Send succees message title (top line)
+        messageBody:'Disabled '+$(this).attr('data-plugin-name'), // Send succees message body (bottom line)
+        error:'Organizr Function: API Connection Failed' // conole error message
+    };
+	var callbacks = $.Callbacks(); // init callbacks var
+    //callbacks.add(  ); // add function to callback to be fired after API call
+    //settingsAPI(post,callbacks); // exec API call
+    //ajaxloader(".content-wrap","in");
+    //setTimeout(function(){ buildPlugins();ajaxloader(); }, 3000);
+});
+*/
+function clamp(num, min, max) {
+  return num <= min ? min : num >= max ? max : num;
+}
+function I(id){return document.getElementById(id);}
+var w=null; //speedtest worker
+function startStop(){
+	if(w!=null){
+		//speedtest is running, abort
+		w.postMessage('abort');
+		w=null;
+		$('#speedTestButtonText').text('Start');
+		initUI();
+	}else{
+		//test is not running, begin
+		w=new Worker('api/plugins/misc/speedTest/speedtest_worker.min.js');
+		w.postMessage('start'); //Add optional parameters as a JSON object to this command
+		$('#speedTestButtonText').text('Running');
+		w.onmessage=function(e){
+			var data=e.data.split(';');
+			var status=Number(data[0]);
+			if(status>=4){
+				//test completed
+				$('#speedTestButtonText').text('Re-Test');
+				w=null;
+			}
+			var downloadText = Math.ceil((data[1]/1000)*1000);
+			var downloadPercent = clamp(Math.ceil(((data[1]/1000)*100)/5)*5, 0,100);
+			var uploadText = Math.ceil((data[2]/1000)*1000);
+			var uploadPercent = clamp(Math.ceil(((data[2]/1000)*100)/5)*5, 0,100);
+			I("ip").textContent=data[4];
+			I("dlText").textContent=(status==1&&data[1]==0)?"...":Math.ceil(data[1]);
+			I("ulText").textContent=(status==3&&data[2]==0)?"...":Math.ceil(data[2]);
+			I("pingText").textContent=Math.ceil(data[3]);
+			I("jitText").textContent=Math.ceil(data[5]);
+			var prog=(Number(data[6])*2+Number(data[7])*2+Number(data[8]))/5;
+			I("progress").style.width=(100*prog)+"%";
+			$('#downloadPercent').attr('class', 'css-bar css-bar-'+downloadPercent+' css-bar-lg css-bar-default').attr('data-label', downloadText+'Mbps');
+			$('#uploadPercent').attr('class', 'css-bar css-bar-'+uploadPercent+' css-bar-lg css-bar-warning pull-right').attr('data-label', uploadText+'Mbps');
+		};
+	}
+}
+//poll the status from the worker every 200ms (this will also update the UI)
+setInterval(function(){
+	if(w) w.postMessage('status');
+},200);
+//function to (re)initialize UI
+function initUI(){
+	I("dlText").textContent="";
+	I("ulText").textContent="";
+	I("pingText").textContent="";
+	I("jitText").textContent="";
+	I("ip").textContent="";
+	I("progress").style.width="";
+	$('#downloadPercent').attr('class', 'css-bar css-bar-0 css-bar-lg css-bar-default').attr('data-label', '0Mbps');
+	$('#uploadPercent').attr('class', 'css-bar css-bar-0 css-bar-lg css-bar-warning pull-right').attr('data-label', '0Mbps');
+}
+// FUNCTIONS
+speedTestLaunch()
+function speedTestLaunch(){
+    if(typeof activeInfo == 'undefined'){
+        setTimeout(function () {
+            speedTestLaunch();
+        }, 1000);
+    }else{
+        if(activeInfo.plugins["SPEEDTEST-enabled"] == true){
+            if (activeInfo.user.groupID <= activeInfo.plugins.includes["SPEEDTEST-Auth-include"]) {
+                var menuList = `<li><a class="inline-popups speedTestModal" href="#speedtest-area" data-effect="mfp-zoom-out"><i class="fa fa-rocket fa-fw"></i> <span lang="en">Test Server Speed</span></a></li>`;
+				var htmlDOM = `
+		    	<div id="speedtest-area" class="white-popup mfp-with-anim mfp-hide">
+		    		<div class="col-md-4 col-md-offset-4">
+						<div class="panel bg-org panel-info">
+							<div class="panel-heading">
+								<span lang="en">Test Speed to Server</span>
+								<button id="startStopBtn" onclick="startStop()" class="btn btn-info waves-effect waves-light pull-right"><span lang="en" id="speedTestButtonText">Start</span> <i class="fa fa-rocket m-l-5"></i></button>
+							</div>
+							<div class="panel-body">
+								<div id="test">
+									<div class="row hidden-xs">
+										<div class="col-md-6 col-xs-6"><div id="downloadPercent" data-label="0Mbps" style="font-size: 15px;"></div></div>
+										<div class="col-md-6 col-xs-6"><div id="uploadPercent" data-label="0Mbps" style="font-size: 15px;"></div></div>
+									</div>
+									<div class="progress progress-sm">
+										<div id="progress" class="progress-bar progress-bar-info active progress-bar-striped" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
+											<span class="sr-only">0% Complete (success)</span>
+										</div>
+									</div>
+				                    <div class="white-box m-b-0">
+				                        <div class="user-btm-box">
+				                            <div class="col-md-3 col-xs-6 p-l-0 p-r-0 text-center">
+				                                <p class="text-success"><i class="ti-download fa-2x"></i></p>
+				                                <h1 id="dlText"></h1>
+												<h4 class="">Mbps</h4>
+											</div>
+				                            <div class="col-md-3 col-xs-6 p-l-0 p-r-0 text-center">
+				                                <p class="text-warning"><i class="ti-upload fa-2x"></i></p>
+				                                <h1 id="ulText"></h1>
+												<h4 class="">Mbps</h4>
+											</div>
+				                            <div class="col-md-3 col-xs-6 p-l-0 p-r-0 text-center">
+				                                <p class="text-purple"><i class="ti-direction-alt fa-2x"></i></p>
+				                                <h1 id="pingText"></h1>
+												<h4 class="">ms</h4>
+											</div>
+				                            <div class="col-md-3 col-xs-6 p-l-0 p-r-0 text-center">
+				                                <p class="text-info"><i class="ti-pulse fa-2x"></i></p>
+				                                <h1 id="jitText"></h1>
+												<h4 class="">ms</h4>
+											</div>
+				                        </div>
+				                    </div>
+								</div>
+								<script type="text/javascript">initUI();</script>
+							</div>
+							<div class="panel-footer"> IP Address: <span id="ip"></span> </div>
+						</div>
+		    		</div>
+		    	</div>
+		    	`;
+				$('.append-menu').after(menuList);
+	            $('.organizr-area').after(htmlDOM);
+	            pageLoad();
+			}
+        }
+    }
+}
+
+// CHANGE CUSTOMIZE Options
+$(document).on('change asColorPicker::close', '#SPEEDTEST-settings-page1 :input', function(e) {
+    var input = $(this);
+    switch ($(this).attr('type')) {
+        case 'switch':
+        case 'checkbox':
+            var value = $(this).prop("checked") ? true : false;
+            break;
+        default:
+            var value = $(this).val().toString();
+    }
+	var post = {
+        api:'api/?v1/update/config',
+        name:$(this).attr("name"),
+        type:$(this).attr("data-type"),
+        value:value,
+        messageTitle:'',
+        messageBody:'Updated Value for '+$(this).parent().parent().find('label').text(),
+        error:'Organizr Function: API Connection Failed'
+    };
+	var callbacks = $.Callbacks();
+    //callbacks.add( buildCustomizeAppearance );
+    settingsAPI(post,callbacks);
+    //disable button then renable
+    $('#SPEEDTEST-settings-page :input').prop('disabled', 'true');
+    setTimeout(
+        function(){
+            $('#SPEEDTEST-settings-page :input').prop('disabled', null);
+            input.emulateTab();
+        },
+        2000
+    );
+
+});
+$(document).on('click', '#SPEEDTEST-settings-button', function() {
+    var post = {
+        plugin:'SpeedTest/settings/get', // used for switch case in your API call
+    };
+    ajaxloader(".content-wrap","in");
+    organizrAPI('POST','api/?v1/plugin',post).success(function(data) {
+        var response = JSON.parse(data);
+        $('#SPEEDTEST-settings-items').html(buildFormGroup(response.data));
+    }).fail(function(xhr) {
+        console.error("Organizr Function: API Connection Failed");
+    });
+    ajaxloader();
+});

+ 1218 - 0
api/plugins/misc/emailTemplates/default.php

@@ -0,0 +1,1218 @@
+<?php
+switch ($extra) {
+	case 'invite':
+		$button = '
+        <table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnButtonBlock" style="min-width:100%;">
+            <tbody class="mcnButtonBlockOuter">
+                <tr>
+                    <td style="padding-top:0; padding-right:18px; padding-bottom:18px; padding-left:18px;" valign="top" align="center" class="mcnButtonBlockInner">
+                        <table border="0" cellpadding="0" cellspacing="0" class="mcnButtonContentContainer" style="border-collapse: separate !important;border-radius: 10px;background-color: #AD80FD;">
+                            <tbody>
+                                <tr>
+                                    <td align="center" valign="middle" class="mcnButtonContent" style="font-family: Helvetica; font-size: 18px; padding: 18px;">
+                                        <a class="mcnButton " title="Button Text" href="' . getServerPath(true) . '?invite=' . $email['inviteCode'] . '" target="_self" style="font-weight: bold;letter-spacing: -0.5px;line-height: 100%;text-align: center;text-decoration: none;color: #FFFFFF;">Use Invite Code</a>
+                                    </td>
+                                </tr>
+                            </tbody>
+                        </table>
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+        ';
+		break;
+	case 'reset':
+		$button = '
+        <table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnButtonBlock" style="min-width:100%;">
+            <tbody class="mcnButtonBlockOuter">
+                <tr>
+                    <td style="padding-top:0; padding-right:18px; padding-bottom:18px; padding-left:18px;" valign="top" align="center" class="mcnButtonBlockInner">
+                        <table border="0" cellpadding="0" cellspacing="0" class="mcnButtonContentContainer" style="border-collapse: separate !important;border-radius: 10px;background-color: #AD80FD;">
+                            <tbody>
+                                <tr>
+                                    <td align="center" valign="middle" class="mcnButtonContent" style="font-family: Helvetica; font-size: 18px; padding: 18px;">
+                                        <a class="mcnButton " title="Reset Password" href="' . getServerPath(true) . '" target="_self" style="font-weight: bold;letter-spacing: -0.5px;line-height: 100%;text-align: center;text-decoration: none;color: #FFFFFF;">Goto My Site</a>
+                                    </td>
+                                </tr>
+                            </tbody>
+                        </table>
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+        ';
+		break;
+	default:
+		$button = null;
+		break;
+}
+$info = '
+<table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnBoxedTextBlock" style="min-width:100%;">
+    <!--[if gte mso 9]>
+<table align="center" border="0" cellspacing="0" cellpadding="0" width="100%">
+<![endif]-->
+    <tbody class="mcnBoxedTextBlockOuter">
+        <tr>
+            <td valign="top" class="mcnBoxedTextBlockInner">
+
+                <!--[if gte mso 9]>
+<td align="center" valign="top" ">
+<![endif]-->
+                <table align="left" border="0" cellpadding="0" cellspacing="0" width="100%" style="min-width:100%;" class="mcnBoxedTextContentContainer">
+                    <tbody>
+                        <tr>
+
+                            <td style="padding-top:9px; padding-left:18px; padding-bottom:9px; padding-right:18px;">
+
+                                <table border="0" cellspacing="0" class="mcnTextContentContainer" width="100%" style="min-width: 100% !important;background-color: #F7F7F7;border: 1px none;">
+                                    <tbody>
+                                        <tr>
+                                            <td valign="top" class="mcnTextContent" style="padding: 18px;">
+                                                <h3 style="text-align:center;">' . getServerPath(true) . '</h3>
+                                            </td>
+                                        </tr>
+                                    </tbody>
+                                </table>
+                            </td>
+                        </tr>
+                    </tbody>
+                </table>
+                <!--[if gte mso 9]>
+</td>
+<![endif]-->
+
+                <!--[if gte mso 9]>
+</tr>
+</table>
+<![endif]-->
+            </td>
+        </tr>
+    </tbody>
+</table>
+';
+$email = '
+<!doctype html>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
+
+<head>
+    <!-- NAME: EDUCATE -->
+    <!--[if gte mso 15]>
+		<xml>
+			<o:OfficeDocumentSettings>
+			<o:AllowPNG/>
+			<o:PixelsPerInch>96</o:PixelsPerInch>
+			</o:OfficeDocumentSettings>
+		</xml>
+		<![endif]-->
+    <meta charset="UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>*EMAIL*</title>
+
+    <style type="text/css">
+        p {
+            margin: 10px 0;
+            padding: 0;
+        }
+
+        table {
+            border-collapse: collapse;
+        }
+
+        h1,
+        h2,
+        h3,
+        h4,
+        h5,
+        h6 {
+            display: block;
+            margin: 0;
+            padding: 0;
+        }
+
+        img,
+        a img {
+            border: 0;
+            height: auto;
+            outline: none;
+            text-decoration: none;
+        }
+
+        body,
+        #bodyTable,
+        #bodyCell {
+            height: 100%;
+            margin: 0;
+            padding: 0;
+            width: 100%;
+        }
+
+        .mcnPreviewText {
+            display: none !important;
+        }
+
+        #outlook a {
+            padding: 0;
+        }
+
+        img {
+            -ms-interpolation-mode: bicubic;
+        }
+
+        table {
+            mso-table-lspace: 0pt;
+            mso-table-rspace: 0pt;
+        }
+
+        .ReadMsgBody {
+            width: 100%;
+        }
+
+        .ExternalClass {
+            width: 100%;
+        }
+
+        p,
+        a,
+        li,
+        td,
+        blockquote {
+            mso-line-height-rule: exactly;
+        }
+
+        a[href^=tel],
+        a[href^=sms] {
+            color: inherit;
+            cursor: default;
+            text-decoration: none;
+        }
+
+        p,
+        a,
+        li,
+        td,
+        body,
+        table,
+        blockquote {
+            -ms-text-size-adjust: 100%;
+            -webkit-text-size-adjust: 100%;
+        }
+
+        .ExternalClass,
+        .ExternalClass p,
+        .ExternalClass td,
+        .ExternalClass div,
+        .ExternalClass span,
+        .ExternalClass font {
+            line-height: 100%;
+        }
+
+        a[x-apple-data-detectors] {
+            color: inherit !important;
+            text-decoration: none !important;
+            font-size: inherit !important;
+            font-family: inherit !important;
+            font-weight: inherit !important;
+            line-height: inherit !important;
+        }
+
+        .templateContainer {
+            max-width: 600px !important;
+        }
+
+        a.mcnButton {
+            display: block;
+        }
+
+        .mcnImage {
+            vertical-align: bottom;
+        }
+
+        .mcnTextContent {
+            word-break: break-word;
+        }
+
+        .mcnTextContent img {
+            height: auto !important;
+        }
+
+        .mcnDividerBlock {
+            table-layout: fixed !important;
+        }
+        /*
+	@tab Page
+	@section Heading 1
+	@style heading 1
+	*/
+
+        h1 {
+            /*@editable*/
+            color: #222222;
+            /*@editable*/
+            font-family: Helvetica;
+            /*@editable*/
+            font-size: 40px;
+            /*@editable*/
+            font-style: normal;
+            /*@editable*/
+            font-weight: bold;
+            /*@editable*/
+            line-height: 150%;
+            /*@editable*/
+            letter-spacing: normal;
+            /*@editable*/
+            text-align: left;
+        }
+        /*
+	@tab Page
+	@section Heading 2
+	@style heading 2
+	*/
+
+        h2 {
+            /*@editable*/
+            color: #222222;
+            /*@editable*/
+            font-family: Helvetica;
+            /*@editable*/
+            font-size: 28px;
+            /*@editable*/
+            font-style: normal;
+            /*@editable*/
+            font-weight: bold;
+            /*@editable*/
+            line-height: 150%;
+            /*@editable*/
+            letter-spacing: normal;
+            /*@editable*/
+            text-align: left;
+        }
+        /*
+	@tab Page
+	@section Heading 3
+	@style heading 3
+	*/
+
+        h3 {
+            /*@editable*/
+            color: #444444;
+            /*@editable*/
+            font-family: Helvetica;
+            /*@editable*/
+            font-size: 22px;
+            /*@editable*/
+            font-style: normal;
+            /*@editable*/
+            font-weight: bold;
+            /*@editable*/
+            line-height: 150%;
+            /*@editable*/
+            letter-spacing: normal;
+            /*@editable*/
+            text-align: left;
+        }
+        /*
+	@tab Page
+	@section Heading 4
+	@style heading 4
+	*/
+
+        h4 {
+            /*@editable*/
+            color: #999999;
+            /*@editable*/
+            font-family: Georgia;
+            /*@editable*/
+            font-size: 20px;
+            /*@editable*/
+            font-style: italic;
+            /*@editable*/
+            font-weight: normal;
+            /*@editable*/
+            line-height: 125%;
+            /*@editable*/
+            letter-spacing: normal;
+            /*@editable*/
+            text-align: left;
+        }
+        /*
+	@tab Header
+	@section Header Container Style
+	*/
+
+        #templateHeader {
+            /*@editable*/
+            background-color: #333333;
+            /*@editable*/
+            background-image: none;
+            /*@editable*/
+            background-repeat: no-repeat;
+            /*@editable*/
+            background-position: center;
+            /*@editable*/
+            background-size: cover;
+            /*@editable*/
+            border-top: 0;
+            /*@editable*/
+            border-bottom: 0;
+            /*@editable*/
+            padding-top: 10px;
+            /*@editable*/
+            padding-bottom: 10px;
+        }
+        /*
+	@tab Header
+	@section Header Interior Style
+	*/
+
+        .headerContainer {
+            /*@editable*/
+            background-color: #transparent;
+            /*@editable*/
+            background-image: none;
+            /*@editable*/
+            background-repeat: no-repeat;
+            /*@editable*/
+            background-position: center;
+            /*@editable*/
+            background-size: cover;
+            /*@editable*/
+            border-top: 0;
+            /*@editable*/
+            border-bottom: 0;
+            /*@editable*/
+            padding-top: 0;
+            /*@editable*/
+            padding-bottom: 0;
+        }
+        /*
+	@tab Header
+	@section Header Text
+	*/
+
+        .headerContainer .mcnTextContent,
+        .headerContainer .mcnTextContent p {
+            /*@editable*/
+            color: #808080;
+            /*@editable*/
+            font-family: Helvetica;
+            /*@editable*/
+            font-size: 16px;
+            /*@editable*/
+            line-height: 150%;
+            /*@editable*/
+            text-align: left;
+        }
+        /*
+	@tab Header
+	@section Header Link
+	*/
+
+        .headerContainer .mcnTextContent a,
+        .headerContainer .mcnTextContent p a {
+            /*@editable*/
+            color: #00ADD8;
+            /*@editable*/
+            font-weight: normal;
+            /*@editable*/
+            text-decoration: underline;
+        }
+        /*
+	@tab Body
+	@section Body Container Style
+	*/
+
+        #templateBody {
+            /*@editable*/
+            background-color: #ffffff;
+            /*@editable*/
+            background-image: none;
+            /*@editable*/
+            background-repeat: no-repeat;
+            /*@editable*/
+            background-position: center;
+            /*@editable*/
+            background-size: cover;
+            /*@editable*/
+            border-top: 0;
+            /*@editable*/
+            border-bottom: 0;
+            /*@editable*/
+            padding-top: 27px;
+            /*@editable*/
+            padding-bottom: 63px;
+        }
+        /*
+	@tab Body
+	@section Body Interior Style
+	*/
+
+        .bodyContainer {
+            /*@editable*/
+            background-color: #transparent;
+            /*@editable*/
+            background-image: none;
+            /*@editable*/
+            background-repeat: no-repeat;
+            /*@editable*/
+            background-position: center;
+            /*@editable*/
+            background-size: cover;
+            /*@editable*/
+            border-top: 0;
+            /*@editable*/
+            border-bottom: 0;
+            /*@editable*/
+            padding-top: 0;
+            /*@editable*/
+            padding-bottom: 0;
+        }
+        /*
+	@tab Body
+	@section Body Text
+	*/
+
+        .bodyContainer .mcnTextContent,
+        .bodyContainer .mcnTextContent p {
+            /*@editable*/
+            color: #808080;
+            /*@editable*/
+            font-family: Helvetica;
+            /*@editable*/
+            font-size: 16px;
+            /*@editable*/
+            line-height: 150%;
+            /*@editable*/
+            text-align: left;
+        }
+        /*
+	@tab Body
+	@section Body Link
+	*/
+
+        .bodyContainer .mcnTextContent a,
+        .bodyContainer .mcnTextContent p a {
+            /*@editable*/
+            color: #00ADD8;
+            /*@editable*/
+            font-weight: normal;
+            /*@editable*/
+            text-decoration: underline;
+        }
+        /*
+	@tab Footer
+	@section Footer Style
+	*/
+
+        #templateFooter {
+            /*@editable*/
+            background-color: #333333;
+            /*@editable*/
+            background-image: none;
+            /*@editable*/
+            background-repeat: no-repeat;
+            /*@editable*/
+            background-position: center;
+            /*@editable*/
+            background-size: cover;
+            /*@editable*/
+            border-top: 0;
+            /*@editable*/
+            border-bottom: 0;
+            /*@editable*/
+            padding-top: 0px;
+            /*@editable*/
+            padding-bottom: 0px;
+        }
+        /*
+	@tab Footer
+	@section Footer Interior Style
+	*/
+
+        .footerContainer {
+            /*@editable*/
+            background-color: #transparent;
+            /*@editable*/
+            background-image: none;
+            /*@editable*/
+            background-repeat: no-repeat;
+            /*@editable*/
+            background-position: center;
+            /*@editable*/
+            background-size: cover;
+            /*@editable*/
+            border-top: 0;
+            /*@editable*/
+            border-bottom: 0;
+            /*@editable*/
+            padding-top: 0;
+            /*@editable*/
+            padding-bottom: 0;
+        }
+        /*
+	@tab Footer
+	@section Footer Text
+	*/
+
+        .footerContainer .mcnTextContent,
+        .footerContainer .mcnTextContent p {
+            /*@editable*/
+            color: #FFFFFF;
+            /*@editable*/
+            font-family: Helvetica;
+            /*@editable*/
+            font-size: 12px;
+            /*@editable*/
+            line-height: 150%;
+            /*@editable*/
+            text-align: center;
+        }
+        /*
+	@tab Footer
+	@section Footer Link
+	*/
+
+        .footerContainer .mcnTextContent a,
+        .footerContainer .mcnTextContent p a {
+            /*@editable*/
+            color: #FFFFFF;
+            /*@editable*/
+            font-weight: normal;
+            /*@editable*/
+            text-decoration: underline;
+        }
+
+        @media only screen and (min-width:768px) {
+            .templateContainer {
+                width: 600px !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            body,
+            table,
+            td,
+            p,
+            a,
+            li,
+            blockquote {
+                -webkit-text-size-adjust: none !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            body {
+                width: 100% !important;
+                min-width: 100% !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            .mcnImage {
+                width: 100% !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            .mcnCartContainer,
+            .mcnCaptionTopContent,
+            .mcnRecContentContainer,
+            .mcnCaptionBottomContent,
+            .mcnTextContentContainer,
+            .mcnBoxedTextContentContainer,
+            .mcnImageGroupContentContainer,
+            .mcnCaptionLeftTextContentContainer,
+            .mcnCaptionRightTextContentContainer,
+            .mcnCaptionLeftImageContentContainer,
+            .mcnCaptionRightImageContentContainer,
+            .mcnImageCardLeftTextContentContainer,
+            .mcnImageCardRightTextContentContainer {
+                max-width: 100% !important;
+                width: 100% !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            .mcnBoxedTextContentContainer {
+                min-width: 100% !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            .mcnImageGroupContent {
+                padding: 9px !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            .mcnCaptionLeftContentOuter .mcnTextContent,
+            .mcnCaptionRightContentOuter .mcnTextContent {
+                padding-top: 9px !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            .mcnImageCardTopImageContent,
+            .mcnCaptionBlockInner .mcnCaptionTopContent:last-child .mcnTextContent {
+                padding-top: 18px !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            .mcnImageCardBottomImageContent {
+                padding-bottom: 9px !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            .mcnImageGroupBlockInner {
+                padding-top: 0 !important;
+                padding-bottom: 0 !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            .mcnImageGroupBlockOuter {
+                padding-top: 9px !important;
+                padding-bottom: 9px !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            .mcnTextContent,
+            .mcnBoxedTextContentColumn {
+                padding-right: 18px !important;
+                padding-left: 18px !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            .mcnImageCardLeftImageContent,
+            .mcnImageCardRightImageContent {
+                padding-right: 18px !important;
+                padding-bottom: 0 !important;
+                padding-left: 18px !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            .mcpreview-image-uploader {
+                display: none !important;
+                width: 100% !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            /*
+	@tab Mobile Styles
+	@section Heading 1
+	@tip Make the first-level headings larger in size for better readability on small screens.
+	*/
+            h1 {
+                /*@editable*/
+                font-size: 30px !important;
+                /*@editable*/
+                line-height: 125% !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            /*
+	@tab Mobile Styles
+	@section Heading 2
+	@tip Make the second-level headings larger in size for better readability on small screens.
+	*/
+            h2 {
+                /*@editable*/
+                font-size: 26px !important;
+                /*@editable*/
+                line-height: 125% !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            /*
+	@tab Mobile Styles
+	@section Heading 3
+	@tip Make the third-level headings larger in size for better readability on small screens.
+	*/
+            h3 {
+                /*@editable*/
+                font-size: 20px !important;
+                /*@editable*/
+                line-height: 150% !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            /*
+	@tab Mobile Styles
+	@section Heading 4
+	@tip Make the fourth-level headings larger in size for better readability on small screens.
+	*/
+            h4 {
+                /*@editable*/
+                font-size: 18px !important;
+                /*@editable*/
+                line-height: 150% !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            /*
+	@tab Mobile Styles
+	@section Boxed Text
+	@tip Make the boxed text larger in size for better readability on small screens. We recommend a font size of at least 16px.
+	*/
+            .mcnBoxedTextContentContainer .mcnTextContent,
+            .mcnBoxedTextContentContainer .mcnTextContent p {
+                /*@editable*/
+                font-size: 14px !important;
+                /*@editable*/
+                line-height: 150% !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            /*
+	@tab Mobile Styles
+	@section Header Text
+	@tip Make the header text larger in size for better readability on small screens.
+	*/
+            .headerContainer .mcnTextContent,
+            .headerContainer .mcnTextContent p {
+                /*@editable*/
+                font-size: 16px !important;
+                /*@editable*/
+                line-height: 150% !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            /*
+	@tab Mobile Styles
+	@section Body Text
+	@tip Make the body text larger in size for better readability on small screens. We recommend a font size of at least 16px.
+	*/
+            .bodyContainer .mcnTextContent,
+            .bodyContainer .mcnTextContent p {
+                /*@editable*/
+                font-size: 16px !important;
+                /*@editable*/
+                line-height: 150% !important;
+            }
+        }
+
+        @media only screen and (max-width: 480px) {
+            /*
+	@tab Mobile Styles
+	@section Footer Text
+	@tip Make the footer content text larger in size for better readability on small screens.
+	*/
+            .footerContainer .mcnTextContent,
+            .footerContainer .mcnTextContent p {
+                /*@editable*/
+                font-size: 14px !important;
+                /*@editable*/
+                line-height: 150% !important;
+            }
+        }
+    </style>
+</head>
+
+<body>
+    <!--*|IF:MC_PREVIEW_TEXT|*-->
+    <!--[if !gte mso 9]><!----><span class="mcnPreviewText" style="display:none; font-size:0px; line-height:0px; max-height:0px; max-width:0px; opacity:0; overflow:hidden; visibility:hidden; mso-hide:all;"></span>
+    <!--<![endif]-->
+    <!--*|END:IF|*-->
+    <center>
+        <table align="center" border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="bodyTable">
+            <tr>
+                <td align="center" valign="top" id="bodyCell">
+                    <!-- BEGIN TEMPLATE // -->
+                    <table border="0" cellpadding="0" cellspacing="0" width="100%">
+                        <tr>
+                            <td align="center" valign="top" id="templateHeader" data-template-container>
+                                <!--[if (gte mso 9)|(IE)]>
+									<table align="center" border="0" cellspacing="0" cellpadding="0" width="600" style="width:600px;">
+									<tr>
+									<td align="center" valign="top" width="600" style="width:600px;">
+									<![endif]-->
+                                <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" class="templateContainer">
+                                    <tr>
+                                        <td valign="top" class="headerContainer">
+                                            <table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnImageBlock" style="min-width:100%;">
+                                                <tbody class="mcnImageBlockOuter">
+                                                    <tr>
+                                                        <td valign="top" style="padding:9px" class="mcnImageBlockInner">
+                                                            <table align="left" width="100%" border="0" cellpadding="0" cellspacing="0" class="mcnImageContentContainer" style="min-width:100%;">
+                                                                <tbody>
+                                                                    <tr>
+                                                                        <td class="mcnImageContent" valign="top" style="padding-right: 9px; padding-left: 9px; padding-top: 0; padding-bottom: 0; text-align:center;">
+
+                                                                            <img align="center" alt="" src="' . $GLOBALS['PHPMAILER-logo'] . '" width="564" style="max-width:700px; padding-bottom: 0; display: inline !important; vertical-align: bottom;" class="mcnImage">
+
+                                                                        </td>
+                                                                    </tr>
+                                                                </tbody>
+                                                            </table>
+                                                        </td>
+                                                    </tr>
+                                                </tbody>
+                                            </table>
+                                        </td>
+                                    </tr>
+                                </table>
+                                <!--[if (gte mso 9)|(IE)]>
+									</td>
+									</tr>
+									</table>
+									<![endif]-->
+                            </td>
+                        </tr>
+                        <tr>
+                            <td align="center" valign="top" id="templateBody" data-template-container>
+                                <!--[if (gte mso 9)|(IE)]>
+									<table align="center" border="0" cellspacing="0" cellpadding="0" width="600" style="width:600px;">
+									<tr>
+									<td align="center" valign="top" width="600" style="width:600px;">
+									<![endif]-->
+                                <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" class="templateContainer">
+                                    <tr>
+                                        <td valign="top" class="bodyContainer">
+                                            <table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnTextBlock" style="min-width:100%;">
+                                                <tbody class="mcnTextBlockOuter">
+                                                    <tr>
+                                                        <td valign="top" class="mcnTextBlockInner" style="padding-top:9px;">
+                                                            <!--[if mso]>
+				<table align="left" border="0" cellspacing="0" cellpadding="0" width="100%" style="width:100%;">
+				<tr>
+				<![endif]-->
+
+                                                            <!--[if mso]>
+				<td valign="top" width="600" style="width:600px;">
+				<![endif]-->
+                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" style="max-width:100%; min-width:100%;" width="100%" class="mcnTextContentContainer">
+                                                                <tbody>
+                                                                    <tr>
+
+                                                                        <td valign="top" class="mcnTextContent" style="padding-top:0; padding-right:18px; padding-bottom:9px; padding-left:18px;">
+
+                                                                            ' . $body . '
+
+                                                                        </td>
+                                                                    </tr>
+                                                                </tbody>
+                                                            </table>
+                                                            <!--[if mso]>
+				</td>
+				<![endif]-->
+
+                                                            <!--[if mso]>
+				</tr>
+				</table>
+				<![endif]-->
+                                                        </td>
+                                                    </tr>
+                                                </tbody>
+                                            </table>
+
+                                            ' . $button . $info . '
+                                        </td>
+                                    </tr>
+                                </table>
+                                <!--[if (gte mso 9)|(IE)]>
+									</td>
+									</tr>
+									</table>
+									<![endif]-->
+                            </td>
+                        </tr>
+                        <tr>
+                            <td align="center" valign="top" id="templateFooter" data-template-container>
+                                <!--[if (gte mso 9)|(IE)]>
+									<table align="center" border="0" cellspacing="0" cellpadding="0" width="600" style="width:600px;">
+									<tr>
+									<td align="center" valign="top" width="600" style="width:600px;">
+									<![endif]-->
+                                <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" class="templateContainer">
+                                    <tr>
+                                        <td valign="top" class="footerContainer">
+                                            <table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnFollowBlock" style="min-width:100%;">
+                                                <tbody class="mcnFollowBlockOuter">
+                                                    <tr>
+                                                        <td align="center" valign="top" style="padding:9px" class="mcnFollowBlockInner">
+                                                            <table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnFollowContentContainer" style="min-width:100%;">
+                                                                <tbody>
+                                                                    <tr>
+                                                                        <td align="center" style="padding-left:9px;padding-right:9px;">
+                                                                            <table border="0" cellpadding="0" cellspacing="0" width="100%" style="min-width: 100%; border: 1px none;" class="mcnFollowContent">
+                                                                                <tbody>
+                                                                                    <tr>
+                                                                                        <td align="center" valign="top" style="padding-top:9px; padding-right:9px; padding-left:9px;">
+                                                                                            <table align="center" border="0" cellpadding="0" cellspacing="0">
+                                                                                                <tbody>
+                                                                                                    <tr>
+                                                                                                        <td align="center" valign="top">
+                                                                                                            <!--[if mso]>
+                                    <table align="center" border="0" cellspacing="0" cellpadding="0">
+                                    <tr>
+                                    <![endif]-->
+
+                                                                                                            <!--[if mso]>
+                                        <td align="center" valign="top">
+                                        <![endif]-->
+
+                                                                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" style="display:inline;">
+                                                                                                                <tbody>
+                                                                                                                    <tr>
+                                                                                                                        <td valign="top" style="padding-right:10px; padding-bottom:9px;" class="mcnFollowContentItemContainer">
+                                                                                                                            <table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnFollowContentItem">
+                                                                                                                                <tbody>
+                                                                                                                                    <tr>
+                                                                                                                                        <td align="left" valign="middle" style="padding-top:5px; padding-right:10px; padding-bottom:5px; padding-left:9px;">
+                                                                                                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" width="">
+                                                                                                                                                <tbody>
+                                                                                                                                                    <tr>
+
+                                                                                                                                                        <td align="center" valign="middle" width="24" class="mcnFollowIconContent">
+                                                                                                                                                            <a href="https://github.com/causefx/organizr" target="_blank"><img src="https://cdn-images.mailchimp.com/icons/social-block-v2/color-github-48.png" style="display:block;" height="24" width="24" class=""></a>
+                                                                                                                                                        </td>
+
+                                                                                                                                                        <td align="left" valign="middle" class="mcnFollowTextContent" style="padding-left:5px;">
+                                                                                                                                                            <a href="https://github.com/causefx/organizr" target="" style="font-family: Helvetica;font-size: 12px;text-decoration: none;color: #FFFFFF;font-weight: bold;">GitHub</a>
+                                                                                                                                                        </td>
+
+                                                                                                                                                    </tr>
+                                                                                                                                                </tbody>
+                                                                                                                                            </table>
+                                                                                                                                        </td>
+                                                                                                                                    </tr>
+                                                                                                                                </tbody>
+                                                                                                                            </table>
+                                                                                                                        </td>
+                                                                                                                    </tr>
+                                                                                                                </tbody>
+                                                                                                            </table>
+
+                                                                                                            <!--[if mso]>
+                                        </td>
+                                        <![endif]-->
+
+                                                                                                            <!--[if mso]>
+                                        <td align="center" valign="top">
+                                        <![endif]-->
+
+                                                                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" style="display:inline;">
+                                                                                                                <tbody>
+                                                                                                                    <tr>
+                                                                                                                        <td valign="top" style="padding-right:10px; padding-bottom:9px;" class="mcnFollowContentItemContainer">
+                                                                                                                            <table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnFollowContentItem">
+                                                                                                                                <tbody>
+                                                                                                                                    <tr>
+                                                                                                                                        <td align="left" valign="middle" style="padding-top:5px; padding-right:10px; padding-bottom:5px; padding-left:9px;">
+                                                                                                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" width="">
+                                                                                                                                                <tbody>
+                                                                                                                                                    <tr>
+
+                                                                                                                                                        <td align="center" valign="middle" width="24" class="mcnFollowIconContent">
+                                                                                                                                                            <a href="https://www.reddit.com/r/organizr" target="_blank"><img src="https://cdn-images.mailchimp.com/icons/social-block-v2/color-reddit-48.png" style="display:block;" height="24" width="24" class=""></a>
+                                                                                                                                                        </td>
+
+                                                                                                                                                        <td align="left" valign="middle" class="mcnFollowTextContent" style="padding-left:5px;">
+                                                                                                                                                            <a href="https://www.reddit.com/r/organizr" target="" style="font-family: Helvetica;font-size: 12px;text-decoration: none;color: #FFFFFF;font-weight: bold;">Reddit</a>
+                                                                                                                                                        </td>
+
+                                                                                                                                                    </tr>
+                                                                                                                                                </tbody>
+                                                                                                                                            </table>
+                                                                                                                                        </td>
+                                                                                                                                    </tr>
+                                                                                                                                </tbody>
+                                                                                                                            </table>
+                                                                                                                        </td>
+                                                                                                                    </tr>
+                                                                                                                </tbody>
+                                                                                                            </table>
+
+                                                                                                            <!--[if mso]>
+                                        </td>
+                                        <![endif]-->
+
+                                                                                                            <!--[if mso]>
+                                        <td align="center" valign="top">
+                                        <![endif]-->
+
+                                                                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" style="display:inline;">
+                                                                                                                <tbody>
+                                                                                                                    <tr>
+                                                                                                                        <td valign="top" style="padding-right:10px; padding-bottom:9px;" class="mcnFollowContentItemContainer">
+                                                                                                                            <table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnFollowContentItem">
+                                                                                                                                <tbody>
+                                                                                                                                    <tr>
+                                                                                                                                        <td align="left" valign="middle" style="padding-top:5px; padding-right:10px; padding-bottom:5px; padding-left:9px;">
+                                                                                                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" width="">
+                                                                                                                                                <tbody>
+                                                                                                                                                    <tr>
+
+                                                                                                                                                        <td align="center" valign="middle" width="24" class="mcnFollowIconContent">
+                                                                                                                                                            <a href="https://organizr.app" target="_blank"><img src="https://cdn-images.mailchimp.com/icons/social-block-v2/color-link-48.png" style="display:block;" height="24" width="24" class=""></a>
+                                                                                                                                                        </td>
+
+                                                                                                                                                        <td align="left" valign="middle" class="mcnFollowTextContent" style="padding-left:5px;">
+                                                                                                                                                            <a href="https://organizr.app" target="" style="font-family: Helvetica;font-size: 12px;text-decoration: none;color: #FFFFFF;font-weight: bold;">Website</a>
+                                                                                                                                                        </td>
+
+                                                                                                                                                    </tr>
+                                                                                                                                                </tbody>
+                                                                                                                                            </table>
+                                                                                                                                        </td>
+                                                                                                                                    </tr>
+                                                                                                                                </tbody>
+                                                                                                                            </table>
+                                                                                                                        </td>
+                                                                                                                    </tr>
+                                                                                                                </tbody>
+                                                                                                            </table>
+
+                                                                                                            <!--[if mso]>
+                                        </td>
+                                        <![endif]-->
+
+                                                                                                            <!--[if mso]>
+                                        <td align="center" valign="top">
+                                        <![endif]-->
+
+                                                                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" style="display:inline;">
+                                                                                                                <tbody>
+                                                                                                                    <tr>
+                                                                                                                        <td valign="top" style="padding-right:0; padding-bottom:9px;" class="mcnFollowContentItemContainer">
+                                                                                                                            <table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnFollowContentItem">
+                                                                                                                                <tbody>
+                                                                                                                                    <tr>
+                                                                                                                                        <td align="left" valign="middle" style="padding-top:5px; padding-right:10px; padding-bottom:5px; padding-left:9px;">
+                                                                                                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" width="">
+                                                                                                                                                <tbody>
+                                                                                                                                                    <tr>
+
+                                                                                                                                                        <td align="center" valign="middle" width="24" class="mcnFollowIconContent">
+                                                                                                                                                            <a href="https://discord.gg/TrNtY7N" target="_blank"><img src="https://cdn-images.mailchimp.com/icons/social-block-v2/color-link-48.png" style="display:block;" height="24" width="24" class=""></a>
+                                                                                                                                                        </td>
+
+                                                                                                                                                        <td align="left" valign="middle" class="mcnFollowTextContent" style="padding-left:5px;">
+                                                                                                                                                            <a href="https://discord.gg/TrNtY7N" target="" style="font-family: Helvetica;font-size: 12px;text-decoration: none;color: #FFFFFF;font-weight: bold;">Discord</a>
+                                                                                                                                                        </td>
+
+                                                                                                                                                    </tr>
+                                                                                                                                                </tbody>
+                                                                                                                                            </table>
+                                                                                                                                        </td>
+                                                                                                                                    </tr>
+                                                                                                                                </tbody>
+                                                                                                                            </table>
+                                                                                                                        </td>
+                                                                                                                    </tr>
+                                                                                                                </tbody>
+                                                                                                            </table>
+
+                                                                                                            <!--[if mso]>
+                                        </td>
+                                        <![endif]-->
+
+                                                                                                            <!--[if mso]>
+                                    </tr>
+                                    </table>
+                                    <![endif]-->
+                                                                                                        </td>
+                                                                                                    </tr>
+                                                                                                </tbody>
+                                                                                            </table>
+                                                                                        </td>
+                                                                                    </tr>
+                                                                                </tbody>
+                                                                            </table>
+                                                                        </td>
+                                                                    </tr>
+                                                                </tbody>
+                                                            </table>
+
+                                                        </td>
+                                                    </tr>
+                                                </tbody>
+                                            </table>
+                                            <table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnDividerBlock" style="min-width:100%;">
+                                                <tbody class="mcnDividerBlockOuter">
+                                                    <tr>
+                                                        <td class="mcnDividerBlockInner" style="min-width: 100%; padding: 0px 18px;">
+                                                            <table class="mcnDividerContent" border="0" cellpadding="0" cellspacing="0" width="100%" style="min-width: 100%;border-top: 2px solid #505050;">
+                                                                <tbody>
+                                                                    <tr>
+                                                                        <td>
+                                                                            <span></span>
+                                                                        </td>
+                                                                    </tr>
+                                                                </tbody>
+                                                            </table>
+                                                            <!--
+                <td class="mcnDividerBlockInner" style="padding: 18px;">
+                <hr class="mcnDividerContent" style="border-bottom-color:none; border-left-color:none; border-right-color:none; border-bottom-width:0; border-left-width:0; border-right-width:0; margin-top:0; margin-right:0; margin-bottom:0; margin-left:0;" />
+-->
+                                                        </td>
+                                                    </tr>
+                                                </tbody>
+                                            </table>
+                                            <table border="0" cellpadding="0" cellspacing="0" width="100%" class="mcnTextBlock" style="min-width:100%;">
+                                                <tbody class="mcnTextBlockOuter">
+                                                    <tr>
+                                                        <td valign="top" class="mcnTextBlockInner" style="padding-top:9px;">
+                                                            <!--[if mso]>
+				<table align="left" border="0" cellspacing="0" cellpadding="0" width="100%" style="width:100%;">
+				<tr>
+				<![endif]-->
+
+                                                            <!--[if mso]>
+				<td valign="top" width="600" style="width:600px;">
+				<![endif]-->
+                                                            <table align="left" border="0" cellpadding="0" cellspacing="0" style="max-width:100%; min-width:100%;" width="100%" class="mcnTextContentContainer">
+                                                                <tbody>
+                                                                    <tr>
+
+                                                                        <td valign="top" class="mcnTextContent" style="padding-top:0; padding-right:18px; padding-bottom:9px; padding-left:18px;">
+
+                                                                            <em>Powered By: Organizr</em>
+                                                                        </td>
+                                                                    </tr>
+                                                                </tbody>
+                                                            </table>
+                                                            <!--[if mso]>
+				</td>
+				<![endif]-->
+
+                                                            <!--[if mso]>
+				</tr>
+				</table>
+				<![endif]-->
+                                                        </td>
+                                                    </tr>
+                                                </tbody>
+                                            </table>
+                                        </td>
+                                    </tr>
+                                </table>
+                                <!--[if (gte mso 9)|(IE)]>
+									</td>
+									</tr>
+									</table>
+									<![endif]-->
+                            </td>
+                        </tr>
+                    </table>
+                    <!-- // END TEMPLATE -->
+                </td>
+            </tr>
+        </table>
+    </center>
+</body>
+
+</html>
+';

+ 129 - 0
api/plugins/misc/emailTemplates/gray.php

@@ -0,0 +1,129 @@
+<?php
+switch ($extra) {
+	case 'invite':
+		$button = '
+		<tr>
+			<td class="divider sideborder" style="font-family:\'Helvetica\', Arial, sans-serif;color:#73747C;padding:0;text-align:left;border-left:1px solid #e9e9e9;border-right:1px solid #e9e9e9;background-color:#ffffff;">
+				<table border="0" class="table-basic full-width" style="border-collapse:collapse;border:0px solid #000;width:100%;">
+					<tbody>
+						<tr>
+							<td style="font-family: \'Helvetica\', Arial, sans-serif; color: #73747C; padding: 0; text-align: left;" width="50%">
+								<hr style="margin-top: 10px; height: 1px; overflow: hidden; border: 0; border-bottom: 1px solid #e9e9e9;">
+							</td>
+							<td style="font-family: \'Helvetica\', Arial, sans-serif; color: #73747C; padding: 0; text-align: left;" width="175">
+								<a class="button orange nowrap" href="' . getServerPath(true) . '?invite=' . $email['inviteCode'] . '" rel="noopener noreferrer" style="text-decoration:none;color:#ffffff;white-space:nowrap;display:inline-block;padding:6px 15px 6px 15px;font-size:16px;border-radius:3px;background-color:#E74D39;margin:0 15px;">Use Invite Code</a>
+							</td>
+							<td style="font-family: \'Helvetica\', Arial, sans-serif; color: #73747C; padding: 0; text-align: left;" width="50%">
+								<hr style="margin-top: 10px; height: 1px; overflow: hidden; border: 0; border-bottom: 1px solid #e9e9e9;">
+							</td>
+						</tr>
+					</tbody>
+				</table>
+			</td>
+		</tr>
+        ';
+		break;
+	case 'reset':
+		$button = '
+		<tr>
+			<td class="divider sideborder" style="font-family:\'Helvetica\', Arial, sans-serif;color:#73747C;padding:0;text-align:left;border-left:1px solid #e9e9e9;border-right:1px solid #e9e9e9;background-color:#ffffff;">
+				<table border="0" class="table-basic full-width" style="border-collapse:collapse;border:0px solid #000;width:100%;">
+					<tbody>
+						<tr>
+							<td style="font-family: \'Helvetica\', Arial, sans-serif; color: #73747C; padding: 0; text-align: left;" width="50%">
+								<hr style="margin-top: 10px; height: 1px; overflow: hidden; border: 0; border-bottom: 1px solid #e9e9e9;">
+							</td>
+							<td style="font-family: \'Helvetica\', Arial, sans-serif; color: #73747C; padding: 0; text-align: left;" width="175">
+								<a class="button orange nowrap" href="' . getServerPath(true) . '" rel="noopener noreferrer" style="text-decoration:none;color:#ffffff;white-space:nowrap;display:inline-block;padding:6px 15px 6px 15px;font-size:16px;border-radius:3px;background-color:#E74D39;margin:0 15px;">Goto My Site</a>
+							</td>
+							<td style="font-family: \'Helvetica\', Arial, sans-serif; color: #73747C; padding: 0; text-align: left;" width="50%">
+								<hr style="margin-top: 10px; height: 1px; overflow: hidden; border: 0; border-bottom: 1px solid #e9e9e9;">
+							</td>
+						</tr>
+					</tbody>
+				</table>
+			</td>
+		</tr>
+        ';
+		break;
+	default:
+		$button = null;
+		break;
+}
+$email = '
+ <!DOCTYPE html>
+ <html>
+ <head>
+ 	<meta content="width=device-width" name="viewport">
+ 	<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
+ 	<title>Email</title>
+ </head>
+ 
+ 
+ 
+ 
+ <body style="margin:0;padding:0;width:100%;background-color:#f8f8f8;font-size:16px;line-height:22px;-webkit-font-smoothing:antialiased;">
+	<table border="0" class="table-basic full-width" style="border-collapse:collapse;border:0px solid #000;width:100%;">
+		<tbody>
+			<tr>
+				<td style="font-family: \'Helvetica\', Arial, sans-serif; color: #73747C; padding: 0; text-align: left;">
+					<table align="center" border="0" class="table-basic wrapper-width" style="border-collapse:collapse;border:0px solid #000;width:592px;">
+						<tbody>
+							<tr>
+								<td style="font-family: \'Helvetica\', Arial, sans-serif; color: #73747C; padding: 0; text-align: left;">
+									<table border="0" class="table-basic full-width" style="border-collapse:collapse;border:0px solid #000;width:100%;">
+										<tbody>
+											<tr>
+												<td class="topbar" style="font-family:\'Helvetica\', Arial, sans-serif;color:#73747C;padding:20px 0 10px 0;text-align:left;"></td>
+											</tr>
+											<tr>
+												<td class="header side-border" style="font-family:\'Helvetica\', Arial, sans-serif;color:#a3a4a8;padding:50px 32px 32px 32px;text-align:left;background-color:#33363D;border-top-left-radius:4px;border-top-right-radius:4px;">
+													<h3 style="margin: 10px 0 20px 0;">' . $subject . '</h3>
+												</td>
+											</tr>
+											<tr>
+												<td style="font-family: \'Helvetica\', Arial, sans-serif; color: #73747C; padding: 0; text-align: left;">
+													<table border="0" class="table-basic full-width" style="border-collapse:collapse;border:0px solid #000;width:100%;">
+														<tbody>
+															<tr>
+																<td class="body sideborder" style="font-family:\'Helvetica\', Arial, sans-serif;color:#73747C;padding:32px;text-align:left;border-left:1px solid #e9e9e9;border-right:1px solid #e9e9e9;background-color:#ffffff;font-size:15px;">
+																	<p style="margin: 10px 0 20px 0;">' . $body . '</p><br>
+																</td>
+															</tr>
+														</tbody>
+													</table>
+												</td>
+											</tr>
+											' . $button . '
+											<tr>
+												<td class="very-bottom" style="font-family:\'Helvetica\', Arial, sans-serif;color:#73747C;padding:0;text-align:left;font-size:14px;">
+													<table border="0" cellspacing="0" class="full-width" style="width: 100%;">
+														<tbody>
+															<tr>
+																<td class="round-bottom" style="font-family:\'Helvetica\', Arial, sans-serif;color:#73747C;padding:32px;text-align:left;border:1px solid #e9e9e9;border-top:0;border-radius:0 0 10px 10px;background-color:#ffffff;">
+																	<table border="0" class="table-basic full-width" style="border-collapse:collapse;border:0px solid #000;width:100%;">
+																		<tbody>
+																			<tr>
+																				<td style="font-family: \'Helvetica\', Arial, sans-serif; color: #73747C; padding: 0; text-align: left;" width="50%"><b>Powered By:</b> Organizr</td>
+																			</tr>
+																		</tbody>
+																	</table>
+																</td>
+															</tr>
+														</tbody>
+													</table>
+												</td>
+											</tr>
+										</tbody>
+									</table>
+								</td>
+							</tr>
+						</tbody>
+					</table>
+				</td>
+			</tr>
+		</tbody>
+	</table>
+</body>
+ </html>
+';

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

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

Fichier diff supprimé car celui-ci est trop grand
+ 42 - 0
api/plugins/misc/emailTemplates/plehex.php


+ 7 - 0
api/plugins/misc/speedTest/empty.php

@@ -0,0 +1,7 @@
+<?php
+header( "HTTP/1.1 200 OK" );
+header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
+header("Cache-Control: post-check=0, pre-check=0", false);
+header("Pragma: no-cache");
+header("Connection: keep-alive");
+?>

+ 27 - 0
api/plugins/misc/speedTest/garbage.php

@@ -0,0 +1,27 @@
+<?php
+// Disable Compression
+@ini_set('zlib.output_compression', 'Off');
+@ini_set('output_buffering', 'Off');
+@ini_set('output_handler', '');
+// Headers
+header('HTTP/1.1 200 OK');
+// Download follows...
+header('Content-Description: File Transfer');
+header('Content-Type: application/octet-stream');
+header('Content-Disposition: attachment; filename=random.dat');
+header('Content-Transfer-Encoding: binary');
+// Never cache me
+header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
+header('Cache-Control: post-check=0, pre-check=0', false);
+header('Pragma: no-cache');
+// Generate data
+$data=openssl_random_pseudo_bytes(1048576);
+// Deliver chunks of 1048576 bytes
+$chunks=isset($_GET['ckSize']) ? intval($_GET['ckSize']) : 4;
+if(empty($chunks)){$chunks = 4;}
+if($chunks>100){$chunks = 100;}
+for($i=0;$i<$chunks;$i++){
+    echo $data;
+    flush();
+}
+?>

+ 99 - 0
api/plugins/misc/speedTest/getIP.php

@@ -0,0 +1,99 @@
+<?php
+
+$ip = "";
+header('Content-Type: text/plain; charset=utf-8');
+if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
+    $ip = $_SERVER['HTTP_CLIENT_IP'];
+} elseif (!empty($_SERVER['X-Real-IP'])) {
+    $ip = $_SERVER['X-Real-IP'];
+} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+    $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
+} else {
+    $ip = $_SERVER['REMOTE_ADDR'];
+}
+
+$ip = preg_replace("/^::ffff:/", "", $ip);
+
+if (strpos($ip, '::1') !== false) {
+    echo $ip . " - localhost ipv6 access";
+    die();
+}
+if (strpos($ip, '127.0.0') !== false) {
+    echo $ip . " - localhost ipv4 access";
+    die();
+}
+
+/**
+ * Optimized algorithm from http://www.codexworld.com
+ *
+ * @param float $latitudeFrom
+ * @param float $longitudeFrom
+ * @param float $latitudeTo
+ * @param float $longitudeTo
+ *
+ * @return float [km]
+ */
+function distance($latitudeFrom, $longitudeFrom, $latitudeTo, $longitudeTo) {
+    $rad = M_PI / 180;
+    $theta = $longitudeFrom - $longitudeTo;
+    $dist = sin($latitudeFrom * $rad) * sin($latitudeTo * $rad) + cos($latitudeFrom * $rad) * cos($latitudeTo * $rad) * cos($theta * $rad);
+    return acos($dist) / $rad * 60 * 1.853;
+}
+
+if (isset($_GET["isp"])) {
+    $isp = "";
+    $ip = (strpos($ip, ',') !== false) ? explode(', ',$ip)[1] : $ip;
+    try {
+        $json = @file_get_contents("https://ipinfo.io/" . $ip . "/json");
+        $details = json_decode($json, true);
+        if (is_array($details) || is_object($details)){
+            if (array_key_exists("org", $details))
+                $isp .= $details["org"];
+            else
+                $isp .= "Unknown ISP";
+            if (array_key_exists("country", $details))
+                $isp .= ", " . $details["country"];
+            $clientLoc = NULL;
+            $serverLoc = NULL;
+            if (array_key_exists("loc", $details))
+                $clientLoc = $details["loc"];
+            if (isset($_GET["distance"])) {
+                if ($clientLoc) {
+                    $json = file_get_contents("https://ipinfo.io/json");
+                    $details = json_decode($json, true);
+                    if (array_key_exists("loc", $details))
+                        $serverLoc = $details["loc"];
+                    if ($serverLoc) {
+                        try {
+                            $clientLoc = explode(",", $clientLoc);
+                            $serverLoc = explode(",", $serverLoc);
+                            $dist = distance($clientLoc[0], $clientLoc[1], $serverLoc[0], $serverLoc[1]);
+                            if ($_GET["distance"] == "mi") {
+                                $dist /= 1.609344;
+                                $dist = round($dist, -1);
+                                if ($dist < 15)
+                                    $dist = "<15";
+                                $isp .= " (" . $dist . " mi)";
+                            }else if ($_GET["distance"] == "km") {
+                                $dist = round($dist, -1);
+                                if ($dist < 20)
+                                    $dist = "<20";
+                                $isp .= " (" . $dist . " km)";
+                            }
+                        } catch (Exception $e) {
+
+                        }
+                    }
+                }
+            }
+        }else{
+            $isp = "Unknown ISP";
+        }
+    } catch (Exception $ex) {
+        $isp = "Unknown ISP";
+    }
+    echo $ip . " - " . $isp;
+} else {
+    echo $ip;
+}
+?>

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
api/plugins/misc/speedTest/speedtest_worker.min.js


+ 73 - 0
api/plugins/misc/speedTest/telemetry.php

@@ -0,0 +1,73 @@
+<?php
+include_once('telemetry_settings.php');
+
+$ip=($_SERVER['REMOTE_ADDR']);
+$ua=($_SERVER['HTTP_USER_AGENT']);
+$lang=""; if(isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) $lang=($_SERVER['HTTP_ACCEPT_LANGUAGE']);
+$dl=($_POST["dl"]);
+$ul=($_POST["ul"]);
+$ping=($_POST["ping"]);
+$jitter=($_POST["jitter"]);
+$log=($_POST["log"]);
+
+if($db_type=="mysql"){
+    $conn = new mysqli($MySql_hostname, $MySql_username, $MySql_password, $MySql_databasename) or die("1");
+    $stmt = $conn->prepare("INSERT INTO speedtest_users (ip,ua,lang,dl,ul,ping,jitter,log) VALUES (?,?,?,?,?,?,?,?)") or die("2");
+    $stmt->bind_param("ssssssss",$ip,$ua,$lang,$dl,$ul,$ping,$jitter,$log) or die("3");
+    $stmt->execute() or die("4");
+    $stmt->close() or die("5");
+    $conn->close() or die("6");
+
+}elseif($db_type=="sqlite"){
+    $conn = new PDO("sqlite:$Sqlite_db_file") or die("1");
+    $conn->exec("
+        CREATE TABLE IF NOT EXISTS `speedtest_users` (
+        `id`    INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+        `timestamp`     timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+        `ip`    text NOT NULL,
+        `ua`    text NOT NULL,
+        `lang`  text NOT NULL,
+        `dl`    text,
+        `ul`    text,
+        `ping`  text,
+        `jitter`        text,
+        `log`   longtext
+        );
+    ");
+    $stmt = $conn->prepare("INSERT INTO speedtest_users (ip,ua,lang,dl,ul,ping,jitter,log) VALUES (?,?,?,?,?,?,?,?)") or die("2");
+    $stmt->execute(array($ip,$ua,$lang,$dl,$ul,$ping,$jitter,$log)) or die("3");
+    $conn = null;
+}elseif($db_type=="postgresql"){
+    // Prepare connection parameters for db connection
+    $conn_host = "host=$PostgreSql_hostname";
+    $conn_db = "dbname=$PostgreSql_databasename";
+    $conn_user = "user=$PostgreSql_username";
+    $conn_password = "password=$PostgreSql_password";
+    // Create db connection
+    $conn = new PDO("pgsql:$conn_host;$conn_db;$conn_user;$conn_password") or die("1");
+    $stmt = $conn->prepare("INSERT INTO speedtest_users (ip,ua,lang,dl,ul,ping,jitter,log) VALUES (?,?,?,?,?,?,?,?)") or die("2");
+    $stmt->execute(array($ip,$ua,$lang,$dl,$ul,$ping,$jitter,$log)) or die("3");
+    $conn = null;
+}
+elseif($db_type=="csv"){
+    // Prepare the csv formatted string
+    date_default_timezone_set($timezone);
+    $date = date('Y-m-d H:i:s');
+    $str = '"' . $date . '",';
+    $str .= '"' . $ip . '",';
+    $str .= '"' . $ua . '",';
+    $str .= '"' . $dl . '",';
+    $str .= '"' . $ul . '",';
+    $str .= '"' . $ping . '",';
+    $str .= '"' . $jitter . '"' . "\n";
+
+    // Set header if this is a new file
+    if (!file_exists($Csv_File)) {
+        $header = '"date","ip","ua","download","upload","ping","jitter"' . "\n";
+        file_put_contents($Csv_File, $header, FILE_APPEND);
+    }
+
+    // Writting line to file
+    file_put_contents($Csv_File, $str, FILE_APPEND);
+}
+?>

+ 24 - 0
api/plugins/misc/speedTest/telemetry_settings.php

@@ -0,0 +1,24 @@
+<?php
+
+$db_type="mysql"; //Type of db: "mysql", "sqlite" or "postgresql" or "csv"
+
+// Sqlite3 settings
+$Sqlite_db_file = "../telemetry.sql";
+
+// Mysql settings
+$MySql_username="USERNAME";
+$MySql_password="PASSWORD";
+$MySql_hostname="DB_HOSTNAME";
+$MySql_databasename="DB_NAME";
+
+// Postgresql settings
+$PostgreSql_username="USERNAME";
+$PostgreSql_password="PASSWORD";
+$PostgreSql_hostname="DB_HOSTNAME";
+$PostgreSql_databasename="DB_NAME";
+
+// CSV settings
+$Csv_File="reports.csv";
+$timezone='UTC';
+
+?>

+ 501 - 0
api/plugins/php-mailer.php

@@ -0,0 +1,501 @@
+<?php
+/** @noinspection PhpUnusedLocalVariableInspection */
+// PLUGIN INFORMATION
+$GLOBALS['plugins'][]['PHP Mailer'] = array( // Plugin Name
+	'name' => 'PHP Mailer', // Plugin Name
+	'author' => 'PHP Mailer', // Who wrote the plugin
+	'category' => 'Mail', // One to Two Word Description
+	'link' => 'https://github.com/PHPMailer/PHPMailer', // Link to plugin info
+	'license' => 'personal,business', // License Type use , for multiple
+	//'fileName'=>'php-mailer.php',
+	//'configFile'=>'php-mailer.php',
+	//'apiFile'=>'php-mailer.php',
+	'idPrefix' => 'PHPMAILER', // html element id prefix
+	'configPrefix' => 'PHPMAILER', // config file prefix for array items without the hyphen
+	'version' => '1.0.0', // SemVer of plugin
+	'image' => 'plugins/images/php-mailer.png', // 1:1 non transparent image for plugin
+	'settings' => true, // does plugin need a settings page? true or false
+	'homepage' => false // Is plugin for use on homepage? true or false
+);
+// INCLUDE/REQUIRE FILES
+// PLUGIN FUNCTIONS
+function getEmails()
+{
+	if ($GLOBALS['authBackend']) {
+		if ($GLOBALS['authBackend'] == 'plex') {
+			$type = 'plex';
+		}
+	} else {
+		$type = 'none';
+	}
+	if ($type == 'plex') {
+		$emails = array_merge(userList('plex')['both'], getOrgUsers());
+	} elseif ($type == 'emby') {
+		$emails = getOrgUsers();
+	} else {
+		$emails = getOrgUsers();
+	}
+	return $emails;
+}
+
+function getTemplates()
+{
+	foreach (glob(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'api' . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'misc' . DIRECTORY_SEPARATOR . 'emailTemplates' . DIRECTORY_SEPARATOR . "*.php") as $filename) {
+		$templates[] = array(
+			'name' => preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($filename)),
+			'value' => preg_replace('/\\.[^.\\s]{3,4}$/', '', basename($filename))
+		);
+	}
+	return $templates;
+}
+
+function phpmEmailTemplate($emailTemplate)
+{
+	$variables = [
+		'{user}' => $emailTemplate['user'],
+		'{domain}' => getServerPath(true),
+		'{password}' => $emailTemplate['password'],
+		'{inviteCode}' => $emailTemplate['inviteCode'],
+		'{fullDomain}' => getServerPath(true),
+		'{title}' => $GLOBALS['title'],
+	];
+	$emailTemplate['body'] = strtr($emailTemplate['body'], $variables);
+	$emailTemplate['subject'] = strtr($emailTemplate['subject'], $variables);
+	return $emailTemplate;
+}
+
+function phpmBuildEmail($email)
+{
+	/** @noinspection PhpUnusedLocalVariableInspection */
+	$subject = (isset($email['subject'])) ? $email['subject'] : 'Message from Server';
+	$body = (isset($email['body'])) ? $email['body'] : 'Message Error Occurred';
+	$type = (isset($email['type'])) ? $email['type'] : 'No Type';
+	switch ($type) {
+		case 'invite':
+			$extra = 'invite';
+			break;
+		case 'reset':
+			$extra = 'reset';
+			break;
+		default:
+			$extra = null;
+			break;
+	}
+	include('misc/emailTemplates/' . $GLOBALS['PHPMAILER-template'] . '.php');
+	return $email;
+}
+
+function phpmAdminSendEmail()
+{
+	if ($GLOBALS['PHPMAILER-enabled']) {
+		$emailTemplate = array(
+			'type' => 'admin',
+			'body' => $_POST['data']['body'],
+			'subject' => $_POST['data']['subject'],
+			'user' => null,
+			'password' => null,
+			'inviteCode' => null,
+		);
+		$emailTemplate = phpmEmailTemplate($emailTemplate);
+		$sendEmail = array(
+			'bcc' => $_POST['data']['bcc'],
+			'subject' => $emailTemplate['subject'],
+			'body' => phpmBuildEmail($emailTemplate),
+		);
+		return phpmSendEmail($sendEmail);
+	}
+	return false;
+}
+
+function phpmSendTestEmail()
+{
+	$emailTemplate = array(
+		'type' => 'test',
+		'body' => 'This is just a test email.',
+		'subject' => 'Test E-Mail',
+		'user' => null,
+		'password' => null,
+		'inviteCode' => null,
+	);
+	$emailTemplate = phpmEmailTemplate($emailTemplate);
+	try {
+		$mail = new PHPMailer\PHPMailer\PHPMailer(true);
+		$mail->isSMTP();
+		//$mail->SMTPDebug = 3;
+		$mail->Host = $GLOBALS['PHPMAILER-smtpHost'];
+		$mail->Port = $GLOBALS['PHPMAILER-smtpHostPort'];
+		if ($GLOBALS['PHPMAILER-smtpHostType'] !== 'n/a') {
+			$mail->SMTPSecure = $GLOBALS['PHPMAILER-smtpHostType'];
+		}
+		$mail->SMTPAuth = $GLOBALS['PHPMAILER-smtpHostAuth'];
+		$mail->Username = $GLOBALS['PHPMAILER-smtpHostUsername'];
+		$mail->Password = decrypt($GLOBALS['PHPMAILER-smtpHostPassword']);
+		$mail->SMTPOptions = array(
+			'ssl' => [
+				'verify_peer' => $GLOBALS['PHPMAILER-verifyCert'],
+				'verify_depth' => 3,
+				'allow_self_signed' => true,
+				'peer_name' => $GLOBALS['PHPMAILER-smtpHost'],
+				'cafile' => getCert(),
+			],
+		);
+		$mail->setFrom($GLOBALS['PHPMAILER-smtpHostSenderEmail'], $GLOBALS['PHPMAILER-smtpHostSenderName']);
+		$mail->addReplyTo($GLOBALS['PHPMAILER-smtpHostSenderEmail'], $GLOBALS['PHPMAILER-smtpHostSenderName']);
+		$mail->isHTML(true);
+		$mail->addAddress($GLOBALS['organizrUser']['email'], $GLOBALS['organizrUser']['username']);
+		$mail->Subject = $emailTemplate['subject'];
+		$mail->Body = phpmBuildEmail($emailTemplate);
+		$mail->send();
+		writeLog('success', 'Mail Function -  E-Mail Test Sent', $GLOBALS['organizrUser']['username']);
+		return true;
+	} catch (PHPMailer\PHPMailer\Exception $e) {
+		writeLog('error', 'Mail Function -  E-Mail Test Failed[' . $mail->ErrorInfo . ']', $GLOBALS['organizrUser']['username']);
+		return $e->errorMessage();
+	}
+	return false;
+}
+
+function phpmSendEmail($emailInfo)
+{
+	$to = isset($emailInfo['to']) ? $emailInfo['to'] : null;
+	$cc = isset($emailInfo['cc']) ? $emailInfo['cc'] : null;
+	$bcc = isset($emailInfo['bcc']) ? $emailInfo['bcc'] : null;
+	$subject = isset($emailInfo['subject']) ? $emailInfo['subject'] : null;
+	$body = isset($emailInfo['body']) ? $emailInfo['body'] : null;
+	$username = isset($emailInfo['user']) ? $emailInfo['user'] : 'Organizr User';
+	try {
+		$mail = new PHPMailer\PHPMailer\PHPMailer(true);
+		$mail->isSMTP();
+		//$mail->SMTPDebug = 3;
+		$mail->Host = $GLOBALS['PHPMAILER-smtpHost'];
+		$mail->Port = $GLOBALS['PHPMAILER-smtpHostPort'];
+		if ($GLOBALS['PHPMAILER-smtpHostType'] !== 'n/a') {
+			$mail->SMTPSecure = $GLOBALS['PHPMAILER-smtpHostType'];
+		}
+		$mail->SMTPAuth = $GLOBALS['PHPMAILER-smtpHostAuth'];
+		$mail->Username = $GLOBALS['PHPMAILER-smtpHostUsername'];
+		$mail->Password = decrypt($GLOBALS['PHPMAILER-smtpHostPassword']);
+		$mail->SMTPOptions = array(
+			'ssl' => [
+				'verify_peer' => $GLOBALS['PHPMAILER-verifyCert'],
+				'verify_depth' => 3,
+				'allow_self_signed' => true,
+				'peer_name' => $GLOBALS['PHPMAILER-smtpHost'],
+				'cafile' => getCert(),
+			],
+		);
+		$mail->setFrom($GLOBALS['PHPMAILER-smtpHostSenderEmail'], $GLOBALS['PHPMAILER-smtpHostSenderName']);
+		$mail->addReplyTo($GLOBALS['PHPMAILER-smtpHostSenderEmail'], $GLOBALS['PHPMAILER-smtpHostSenderName']);
+		$mail->isHTML(true);
+		if ($to) {
+			$mail->addAddress($to, $username);
+		}
+		if ($cc) {
+			$mail->addCC($cc);
+		}
+		if ($bcc) {
+			if (strpos($bcc, ',') === false) {
+				$mail->addBCC($bcc);
+			} else {
+				$allEmails = explode(",", $bcc);
+				foreach ($allEmails as $gotEmail) {
+					$mail->addBCC($gotEmail);
+				}
+			}
+		}
+		$mail->Subject = $subject;
+		$mail->Body = $body;
+		$mail->send();
+		//writeLog('success', 'Mail Function -  E-Mail Test Sent', $GLOBALS['organizrUser']['username']);
+		return true;
+	} catch (PHPMailer\PHPMailer\Exception $e) {
+		writeLog('error', 'Mail Function -  E-Mail Test Failed[' . $mail->ErrorInfo . ']', $GLOBALS['organizrUser']['username']);
+		return $e->errorMessage();
+	}
+	return false;
+}
+
+/* GET PHPMAILER SETTINGS */
+function phpmGetSettings()
+{
+	return array(
+		'Host' => array(
+			array(
+				'type' => 'input',
+				'name' => 'PHPMAILER-smtpHost',
+				'label' => 'SMTP Host',
+				'value' => $GLOBALS['PHPMAILER-smtpHost']
+			),
+			array(
+				'type' => 'input',
+				'name' => 'PHPMAILER-smtpHostPort',
+				'label' => 'SMTP Port',
+				'value' => $GLOBALS['PHPMAILER-smtpHostPort']
+			)
+		),
+		'Authentication' => array(
+			array(
+				'type' => 'input',
+				'name' => 'PHPMAILER-smtpHostUsername',
+				'label' => 'Username',
+				'value' => $GLOBALS['PHPMAILER-smtpHostUsername']
+			),
+			array(
+				'type' => 'password',
+				'name' => 'PHPMAILER-smtpHostPassword',
+				'label' => 'Password',
+				'value' => $GLOBALS['PHPMAILER-smtpHostPassword']
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'PHPMAILER-smtpHostAuth',
+				'label' => 'Authentication',
+				'value' => $GLOBALS['PHPMAILER-smtpHostAuth']
+			),
+			array(
+				'type' => 'select',
+				'name' => 'PHPMAILER-smtpHostType',
+				'label' => 'Authentication Type',
+				'value' => $GLOBALS['PHPMAILER-smtpHostType'],
+				'options' => array(
+					array(
+						'name' => 'tls',
+						'value' => 'tls'
+					),
+					array(
+						'name' => 'ssl',
+						'value' => 'ssl'
+					),
+					array(
+						'name' => 'off',
+						'value' => 'n/a'
+					)
+				)
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'PHPMAILER-verifyCert',
+				'label' => 'Verify Certificate',
+				'value' => $GLOBALS['PHPMAILER-verifyCert']
+			),
+		),
+		'Sender Information' => array(
+			array(
+				'type' => 'input',
+				'name' => 'PHPMAILER-smtpHostSenderName',
+				'label' => 'Sender Name',
+				'value' => $GLOBALS['PHPMAILER-smtpHostSenderName']
+			),
+			array(
+				'type' => 'input',
+				'name' => 'PHPMAILER-smtpHostSenderEmail',
+				'label' => 'Sender Email',
+				'value' => $GLOBALS['PHPMAILER-smtpHostSenderEmail'],
+				'placeholder' => 'i.e. same as username'
+			)
+		),
+		'Test & Options' => array(
+			array(
+				'type' => 'button',
+				'label' => 'Send Test',
+				'class' => 'phpmSendTestEmail',
+				'icon' => 'fa fa-paper-plane',
+				'text' => 'Send'
+			),
+			array(
+				'type' => 'input',
+				'name' => 'PHPMAILER-domain',
+				'label' => 'Domain Link Override',
+				'value' => $GLOBALS['PHPMAILER-domain'],
+				'placeholder' => 'https://domain.com/',
+			),
+			array(
+				'type' => 'select',
+				'name' => 'PHPMAILER-template',
+				'label' => 'Theme',
+				'value' => $GLOBALS['PHPMAILER-template'],
+				'options' => getTemplates()
+			),
+			array(
+				'type' => 'input',
+				'name' => 'PHPMAILER-logo',
+				'label' => 'WAN Logo URL',
+				'value' => $GLOBALS['PHPMAILER-logo'],
+				'placeholder' => 'Full URL',
+			),
+			array(
+				'type' => 'switch',
+				'name' => 'PHPMAILER-emailTemplateRegisterUserEnabled',
+				'label' => 'Send Welcome E-Mail',
+				'value' => $GLOBALS['PHPMAILER-emailTemplateRegisterUserEnabled'],
+			),
+		),
+		'Templates' => array(
+			array(
+				'type' => 'accordion',
+				'label' => 'Edit Template',
+				'id' => 'customEmailTemplates',
+				'override' => 12,
+				'options' => array(
+					array(
+						'id' => 'PHPMAILER-emailTemplateRegisterUserForm',
+						'header' => 'New Registration',
+						'body' => array(
+							array(
+								'type' => 'input',
+								'name' => 'PHPMAILER-emailTemplateRegisterUserSubject',
+								'smallLabel' => 'Subject',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateRegisterUserSubject'],
+							),
+							array(
+								'type' => 'textbox',
+								'name' => 'PHPMAILER-emailTemplateRegisterUser',
+								'smallLabel' => 'Body',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateRegisterUser'],
+								'attr' => 'rows="10"',
+							)
+						)
+					),
+					array(
+						'id' => 'PHPMAILER-emailTemplateResetPasswordForm',
+						'header' => 'Reset Password',
+						'body' => array(
+							array(
+								'type' => 'input',
+								'name' => 'PHPMAILER-emailTemplateResetPasswordSubject',
+								'smallLabel' => 'Subject',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateResetPasswordSubject'],
+							),
+							array(
+								'type' => 'textbox',
+								'name' => 'PHPMAILER-emailTemplateResetPassword',
+								'smallLabel' => 'Body',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateResetPassword'],
+								'attr' => 'rows="10"',
+							)
+						)
+					),
+					array(
+						'id' => 'PHPMAILER-emailTemplateInviteUserForm',
+						'header' => 'Invite User',
+						'body' => array(
+							array(
+								'type' => 'input',
+								'name' => 'PHPMAILER-emailTemplateInviteUserSubject',
+								'smallLabel' => 'Subject',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateInviteUserSubject'],
+							),
+							array(
+								'type' => 'textbox',
+								'name' => 'PHPMAILER-emailTemplateInviteUser',
+								'smallLabel' => 'Body',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateInviteUser'],
+								'attr' => 'rows="10"',
+							)
+						)
+					),
+					array(
+						'id' => 'PHPMAILER-emailTemplateCustom-include-OneForm',
+						'header' => $GLOBALS['PHPMAILER-emailTemplateCustom-include-OneName'],
+						'body' => array(
+							array(
+								'type' => 'input',
+								'name' => 'PHPMAILER-emailTemplateCustom-include-OneName',
+								'smallLabel' => 'Name',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateCustom-include-OneName'],
+							),
+							array(
+								'type' => 'input',
+								'name' => 'PHPMAILER-emailTemplateCustom-include-OneSubject',
+								'smallLabel' => 'Subject',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateCustom-include-OneSubject'],
+							),
+							array(
+								'type' => 'textbox',
+								'name' => 'PHPMAILER-emailTemplateCustom-include-One',
+								'smallLabel' => 'Body',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateCustom-include-One'],
+								'attr' => 'rows="10"',
+							)
+						)
+					),
+					array(
+						'id' => 'PHPMAILER-emailTemplateCustom-include-TwoForm',
+						'header' => $GLOBALS['PHPMAILER-emailTemplateCustom-include-TwoName'],
+						'body' => array(
+							array(
+								'type' => 'input',
+								'name' => 'PHPMAILER-emailTemplateCustom-include-TwoName',
+								'smallLabel' => 'Name',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateCustom-include-TwoName'],
+							),
+							array(
+								'type' => 'input',
+								'name' => 'PHPMAILER-emailTemplateCustom-include-TwoSubject',
+								'smallLabel' => 'Subject',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateCustom-include-TwoSubject'],
+							),
+							array(
+								'type' => 'textbox',
+								'name' => 'PHPMAILER-emailTemplateCustom-include-Two',
+								'smallLabel' => 'Body',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateCustom-include-Two'],
+								'attr' => 'rows="10"',
+							)
+						)
+					),
+					array(
+						'id' => 'PHPMAILER-emailTemplateCustom-include-ThreeForm',
+						'header' => $GLOBALS['PHPMAILER-emailTemplateCustom-include-ThreeName'],
+						'body' => array(
+							array(
+								'type' => 'input',
+								'name' => 'PHPMAILER-emailTemplateCustom-include-ThreeName',
+								'smallLabel' => 'Name',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateCustom-include-ThreeName'],
+							),
+							array(
+								'type' => 'input',
+								'name' => 'PHPMAILER-emailTemplateCustom-include-ThreeSubject',
+								'smallLabel' => 'Subject',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateCustom-include-ThreeSubject'],
+							),
+							array(
+								'type' => 'textbox',
+								'name' => 'PHPMAILER-emailTemplateCustom-include-Three',
+								'smallLabel' => 'Body',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateCustom-include-Three'],
+								'attr' => 'rows="10"',
+							)
+						)
+					),
+					array(
+						'id' => 'PHPMAILER-emailTemplateCustom-include-FourForm',
+						'header' => $GLOBALS['PHPMAILER-emailTemplateCustom-include-FourName'],
+						'body' => array(
+							array(
+								'type' => 'input',
+								'name' => 'PHPMAILER-emailTemplateCustom-include-FourName',
+								'smallLabel' => 'Name',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateCustom-include-FourName'],
+							),
+							array(
+								'type' => 'input',
+								'name' => 'PHPMAILER-emailTemplateCustom-include-FourSubject',
+								'smallLabel' => 'Subject',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateCustom-include-FourSubject'],
+							),
+							array(
+								'type' => 'textbox',
+								'name' => 'PHPMAILER-emailTemplateCustom-include-Four',
+								'smallLabel' => 'Body',
+								'value' => $GLOBALS['PHPMAILER-emailTemplateCustom-include-Four'],
+								'attr' => 'rows="10"',
+							)
+						)
+					),
+				)
+			)
+		)
+	);
+}

+ 38 - 0
api/plugins/speedTest.php

@@ -0,0 +1,38 @@
+<?php
+
+// PLUGIN INFORMATION
+$GLOBALS['plugins'][]['SpeedTest'] = array( // Plugin Name
+    'name'=>'SpeedTest', // Plugin Name
+    'author'=>'CauseFX', // Who wrote the plugin
+    'category'=>'Utilities', // One to Two Word Description
+    'link'=>'https://github.com/PHPMailer/PHPMailer', // Link to plugin info
+    'license'=>'personal,business', // License Type use , for multiple
+    //'fileName'=>'php-mailer.php',
+    //'configFile'=>'php-mailer.php',
+    //'apiFile'=>'php-mailer.php',
+    'idPrefix'=>'SPEEDTEST', // html element id prefix
+    'configPrefix'=>'SPEEDTEST', // config file prefix for array items without the hypen
+    'version'=>'1.0.0', // SemVer of plugin
+    'image'=>'plugins/images/speedtest.png', // 1:1 non transparent image for plugin
+    'settings'=>true, // does plugin need a settings page? true or false
+    'homepage'=>false // Is plugin for use on homepage? true or false
+);
+// INCLUDE/REQUIRE FILES
+
+// PLUGIN FUNCTIONS
+
+/* GET PHPMAILER SETTINGS */
+function speedTestGetSettings()
+{
+    return array(
+        'Options' => array(
+            array(
+                'type' => 'select',
+                'name' => 'SPEEDTEST-Auth-include',
+                'label' => 'Minimum Authentication',
+                'value' => $GLOBALS['SPEEDTEST-Auth-include'],
+                'options' => groupSelect()
+            )
+        )
+    );
+}

+ 7 - 0
api/vendor/autoload.php

@@ -0,0 +1,7 @@
+<?php
+
+// autoload.php @generated by Composer
+
+require_once __DIR__ . '/composer/autoload_real.php';
+
+return ComposerAutoloaderInitcbdc783d76f8e7563dcce7d8af053ecb::getLoader();

+ 445 - 0
api/vendor/composer/ClassLoader.php

@@ -0,0 +1,445 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ *     $loader = new \Composer\Autoload\ClassLoader();
+ *
+ *     // register classes with namespaces
+ *     $loader->add('Symfony\Component', __DIR__.'/component');
+ *     $loader->add('Symfony',           __DIR__.'/framework');
+ *
+ *     // activate the autoloader
+ *     $loader->register();
+ *
+ *     // to enable searching the include path (eg. for PEAR packages)
+ *     $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @see    http://www.php-fig.org/psr/psr-0/
+ * @see    http://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+    // PSR-4
+    private $prefixLengthsPsr4 = array();
+    private $prefixDirsPsr4 = array();
+    private $fallbackDirsPsr4 = array();
+
+    // PSR-0
+    private $prefixesPsr0 = array();
+    private $fallbackDirsPsr0 = array();
+
+    private $useIncludePath = false;
+    private $classMap = array();
+    private $classMapAuthoritative = false;
+    private $missingClasses = array();
+    private $apcuPrefix;
+
+    public function getPrefixes()
+    {
+        if (!empty($this->prefixesPsr0)) {
+            return call_user_func_array('array_merge', $this->prefixesPsr0);
+        }
+
+        return array();
+    }
+
+    public function getPrefixesPsr4()
+    {
+        return $this->prefixDirsPsr4;
+    }
+
+    public function getFallbackDirs()
+    {
+        return $this->fallbackDirsPsr0;
+    }
+
+    public function getFallbackDirsPsr4()
+    {
+        return $this->fallbackDirsPsr4;
+    }
+
+    public function getClassMap()
+    {
+        return $this->classMap;
+    }
+
+    /**
+     * @param array $classMap Class to filename map
+     */
+    public function addClassMap(array $classMap)
+    {
+        if ($this->classMap) {
+            $this->classMap = array_merge($this->classMap, $classMap);
+        } else {
+            $this->classMap = $classMap;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix, either
+     * appending or prepending to the ones previously set for this prefix.
+     *
+     * @param string       $prefix  The prefix
+     * @param array|string $paths   The PSR-0 root directories
+     * @param bool         $prepend Whether to prepend the directories
+     */
+    public function add($prefix, $paths, $prepend = false)
+    {
+        if (!$prefix) {
+            if ($prepend) {
+                $this->fallbackDirsPsr0 = array_merge(
+                    (array) $paths,
+                    $this->fallbackDirsPsr0
+                );
+            } else {
+                $this->fallbackDirsPsr0 = array_merge(
+                    $this->fallbackDirsPsr0,
+                    (array) $paths
+                );
+            }
+
+            return;
+        }
+
+        $first = $prefix[0];
+        if (!isset($this->prefixesPsr0[$first][$prefix])) {
+            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+
+            return;
+        }
+        if ($prepend) {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                (array) $paths,
+                $this->prefixesPsr0[$first][$prefix]
+            );
+        } else {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                $this->prefixesPsr0[$first][$prefix],
+                (array) $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace, either
+     * appending or prepending to the ones previously set for this namespace.
+     *
+     * @param string       $prefix  The prefix/namespace, with trailing '\\'
+     * @param array|string $paths   The PSR-4 base directories
+     * @param bool         $prepend Whether to prepend the directories
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function addPsr4($prefix, $paths, $prepend = false)
+    {
+        if (!$prefix) {
+            // Register directories for the root namespace.
+            if ($prepend) {
+                $this->fallbackDirsPsr4 = array_merge(
+                    (array) $paths,
+                    $this->fallbackDirsPsr4
+                );
+            } else {
+                $this->fallbackDirsPsr4 = array_merge(
+                    $this->fallbackDirsPsr4,
+                    (array) $paths
+                );
+            }
+        } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+            // Register directories for a new namespace.
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+        } elseif ($prepend) {
+            // Prepend directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                (array) $paths,
+                $this->prefixDirsPsr4[$prefix]
+            );
+        } else {
+            // Append directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                $this->prefixDirsPsr4[$prefix],
+                (array) $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix,
+     * replacing any others previously set for this prefix.
+     *
+     * @param string       $prefix The prefix
+     * @param array|string $paths  The PSR-0 base directories
+     */
+    public function set($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr0 = (array) $paths;
+        } else {
+            $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace,
+     * replacing any others previously set for this namespace.
+     *
+     * @param string       $prefix The prefix/namespace, with trailing '\\'
+     * @param array|string $paths  The PSR-4 base directories
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function setPsr4($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr4 = (array) $paths;
+        } else {
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Turns on searching the include path for class files.
+     *
+     * @param bool $useIncludePath
+     */
+    public function setUseIncludePath($useIncludePath)
+    {
+        $this->useIncludePath = $useIncludePath;
+    }
+
+    /**
+     * Can be used to check if the autoloader uses the include path to check
+     * for classes.
+     *
+     * @return bool
+     */
+    public function getUseIncludePath()
+    {
+        return $this->useIncludePath;
+    }
+
+    /**
+     * Turns off searching the prefix and fallback directories for classes
+     * that have not been registered with the class map.
+     *
+     * @param bool $classMapAuthoritative
+     */
+    public function setClassMapAuthoritative($classMapAuthoritative)
+    {
+        $this->classMapAuthoritative = $classMapAuthoritative;
+    }
+
+    /**
+     * Should class lookup fail if not found in the current class map?
+     *
+     * @return bool
+     */
+    public function isClassMapAuthoritative()
+    {
+        return $this->classMapAuthoritative;
+    }
+
+    /**
+     * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+     *
+     * @param string|null $apcuPrefix
+     */
+    public function setApcuPrefix($apcuPrefix)
+    {
+        $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null;
+    }
+
+    /**
+     * The APCu prefix in use, or null if APCu caching is not enabled.
+     *
+     * @return string|null
+     */
+    public function getApcuPrefix()
+    {
+        return $this->apcuPrefix;
+    }
+
+    /**
+     * Registers this instance as an autoloader.
+     *
+     * @param bool $prepend Whether to prepend the autoloader or not
+     */
+    public function register($prepend = false)
+    {
+        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+    }
+
+    /**
+     * Unregisters this instance as an autoloader.
+     */
+    public function unregister()
+    {
+        spl_autoload_unregister(array($this, 'loadClass'));
+    }
+
+    /**
+     * Loads the given class or interface.
+     *
+     * @param  string    $class The name of the class
+     * @return bool|null True if loaded, null otherwise
+     */
+    public function loadClass($class)
+    {
+        if ($file = $this->findFile($class)) {
+            includeFile($file);
+
+            return true;
+        }
+    }
+
+    /**
+     * Finds the path to the file where the class is defined.
+     *
+     * @param string $class The name of the class
+     *
+     * @return string|false The path if found, false otherwise
+     */
+    public function findFile($class)
+    {
+        // class map lookup
+        if (isset($this->classMap[$class])) {
+            return $this->classMap[$class];
+        }
+        if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+            return false;
+        }
+        if (null !== $this->apcuPrefix) {
+            $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+            if ($hit) {
+                return $file;
+            }
+        }
+
+        $file = $this->findFileWithExtension($class, '.php');
+
+        // Search for Hack files if we are running on HHVM
+        if (false === $file && defined('HHVM_VERSION')) {
+            $file = $this->findFileWithExtension($class, '.hh');
+        }
+
+        if (null !== $this->apcuPrefix) {
+            apcu_add($this->apcuPrefix.$class, $file);
+        }
+
+        if (false === $file) {
+            // Remember that this class does not exist.
+            $this->missingClasses[$class] = true;
+        }
+
+        return $file;
+    }
+
+    private function findFileWithExtension($class, $ext)
+    {
+        // PSR-4 lookup
+        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+        $first = $class[0];
+        if (isset($this->prefixLengthsPsr4[$first])) {
+            $subPath = $class;
+            while (false !== $lastPos = strrpos($subPath, '\\')) {
+                $subPath = substr($subPath, 0, $lastPos);
+                $search = $subPath.'\\';
+                if (isset($this->prefixDirsPsr4[$search])) {
+                    foreach ($this->prefixDirsPsr4[$search] as $dir) {
+                        $length = $this->prefixLengthsPsr4[$first][$search];
+                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-4 fallback dirs
+        foreach ($this->fallbackDirsPsr4 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 lookup
+        if (false !== $pos = strrpos($class, '\\')) {
+            // namespaced class name
+            $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+        } else {
+            // PEAR-like class name
+            $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+        }
+
+        if (isset($this->prefixesPsr0[$first])) {
+            foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+                if (0 === strpos($class, $prefix)) {
+                    foreach ($dirs as $dir) {
+                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-0 fallback dirs
+        foreach ($this->fallbackDirsPsr0 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 include paths.
+        if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+            return $file;
+        }
+
+        return false;
+    }
+}
+
+/**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ */
+function includeFile($file)
+{
+    include $file;
+}

+ 21 - 0
api/vendor/composer/LICENSE

@@ -0,0 +1,21 @@
+
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+

+ 64 - 0
api/vendor/composer/autoload_classmap.php

@@ -0,0 +1,64 @@
+<?php
+
+// autoload_classmap.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+    'Dibi\\Bridges\\Nette\\DibiExtension22' => $vendorDir . '/dibi/dibi/src/Dibi/Bridges/Nette/DibiExtension22.php',
+    'Dibi\\Bridges\\Tracy\\Panel' => $vendorDir . '/dibi/dibi/src/Dibi/Bridges/Tracy/Panel.php',
+    'Dibi\\Connection' => $vendorDir . '/dibi/dibi/src/Dibi/Connection.php',
+    'Dibi\\ConstraintViolationException' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',
+    'Dibi\\DataSource' => $vendorDir . '/dibi/dibi/src/Dibi/DataSource.php',
+    'Dibi\\DateTime' => $vendorDir . '/dibi/dibi/src/Dibi/DateTime.php',
+    'Dibi\\Driver' => $vendorDir . '/dibi/dibi/src/Dibi/interfaces.php',
+    'Dibi\\DriverException' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',
+    'Dibi\\Drivers\\FirebirdDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/FirebirdDriver.php',
+    'Dibi\\Drivers\\MsSqlDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/MsSqlDriver.php',
+    'Dibi\\Drivers\\MsSqlReflector' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/MsSqlReflector.php',
+    'Dibi\\Drivers\\MySqlDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/MySqlDriver.php',
+    'Dibi\\Drivers\\MySqlReflector' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/MySqlReflector.php',
+    'Dibi\\Drivers\\MySqliDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/MySqliDriver.php',
+    'Dibi\\Drivers\\OdbcDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/OdbcDriver.php',
+    'Dibi\\Drivers\\OracleDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/OracleDriver.php',
+    'Dibi\\Drivers\\PdoDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/PdoDriver.php',
+    'Dibi\\Drivers\\PostgreDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/PostgreDriver.php',
+    'Dibi\\Drivers\\Sqlite3Driver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/Sqlite3Driver.php',
+    'Dibi\\Drivers\\SqliteReflector' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/SqliteReflector.php',
+    'Dibi\\Drivers\\SqlsrvDriver' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/SqlsrvDriver.php',
+    'Dibi\\Drivers\\SqlsrvReflector' => $vendorDir . '/dibi/dibi/src/Dibi/Drivers/SqlsrvReflector.php',
+    'Dibi\\Event' => $vendorDir . '/dibi/dibi/src/Dibi/Event.php',
+    'Dibi\\Exception' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',
+    'Dibi\\Expression' => $vendorDir . '/dibi/dibi/src/Dibi/Expression.php',
+    'Dibi\\Fluent' => $vendorDir . '/dibi/dibi/src/Dibi/Fluent.php',
+    'Dibi\\ForeignKeyConstraintViolationException' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',
+    'Dibi\\HashMap' => $vendorDir . '/dibi/dibi/src/Dibi/HashMap.php',
+    'Dibi\\HashMapBase' => $vendorDir . '/dibi/dibi/src/Dibi/HashMap.php',
+    'Dibi\\Helpers' => $vendorDir . '/dibi/dibi/src/Dibi/Helpers.php',
+    'Dibi\\IDataSource' => $vendorDir . '/dibi/dibi/src/Dibi/interfaces.php',
+    'Dibi\\Literal' => $vendorDir . '/dibi/dibi/src/Dibi/Literal.php',
+    'Dibi\\Loggers\\FileLogger' => $vendorDir . '/dibi/dibi/src/Dibi/Loggers/FileLogger.php',
+    'Dibi\\Loggers\\FirePhpLogger' => $vendorDir . '/dibi/dibi/src/Dibi/Loggers/FirePhpLogger.php',
+    'Dibi\\NotImplementedException' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',
+    'Dibi\\NotNullConstraintViolationException' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',
+    'Dibi\\NotSupportedException' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',
+    'Dibi\\PcreException' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',
+    'Dibi\\ProcedureException' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',
+    'Dibi\\Reflection\\Column' => $vendorDir . '/dibi/dibi/src/Dibi/Reflection/Column.php',
+    'Dibi\\Reflection\\Database' => $vendorDir . '/dibi/dibi/src/Dibi/Reflection/Database.php',
+    'Dibi\\Reflection\\ForeignKey' => $vendorDir . '/dibi/dibi/src/Dibi/Reflection/ForeignKey.php',
+    'Dibi\\Reflection\\Index' => $vendorDir . '/dibi/dibi/src/Dibi/Reflection/Index.php',
+    'Dibi\\Reflection\\Result' => $vendorDir . '/dibi/dibi/src/Dibi/Reflection/Result.php',
+    'Dibi\\Reflection\\Table' => $vendorDir . '/dibi/dibi/src/Dibi/Reflection/Table.php',
+    'Dibi\\Reflector' => $vendorDir . '/dibi/dibi/src/Dibi/interfaces.php',
+    'Dibi\\Result' => $vendorDir . '/dibi/dibi/src/Dibi/Result.php',
+    'Dibi\\ResultDriver' => $vendorDir . '/dibi/dibi/src/Dibi/interfaces.php',
+    'Dibi\\ResultIterator' => $vendorDir . '/dibi/dibi/src/Dibi/ResultIterator.php',
+    'Dibi\\Row' => $vendorDir . '/dibi/dibi/src/Dibi/Row.php',
+    'Dibi\\Strict' => $vendorDir . '/dibi/dibi/src/Dibi/Strict.php',
+    'Dibi\\Translator' => $vendorDir . '/dibi/dibi/src/Dibi/Translator.php',
+    'Dibi\\Type' => $vendorDir . '/dibi/dibi/src/Dibi/Type.php',
+    'Dibi\\UniqueConstraintViolationException' => $vendorDir . '/dibi/dibi/src/Dibi/exceptions.php',
+    'dibi' => $vendorDir . '/dibi/dibi/src/Dibi/dibi.php',
+);

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

@@ -0,0 +1,17 @@
+<?php
+
+// autoload_files.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+    '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
+    'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
+    'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php',
+    '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
+    '3109cb1a231dcd04bee1f9f620d46975' => $vendorDir . '/paragonie/sodium_compat/autoload.php',
+    'bd9634f2d41831496de0d3dfe4c94881' => $vendorDir . '/symfony/polyfill-php56/bootstrap.php',
+    '0097ca414fcb37c7130ac24b05f485f8' => $vendorDir . '/dibi/dibi/src/loader.php',
+    '98ac7ebbcd4b271b4f101d3af2543920' => $vendorDir . '/layershifter/tld-extract/src/static.php',
+);

+ 10 - 0
api/vendor/composer/autoload_namespaces.php

@@ -0,0 +1,10 @@
+<?php
+
+// autoload_namespaces.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+    'Requests' => array($vendorDir . '/rmccue/requests/library'),
+);

+ 31 - 0
api/vendor/composer/autoload_psr4.php

@@ -0,0 +1,31 @@
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+    'TrueBV\\' => array($vendorDir . '/true/punycode/src'),
+    'Symfony\\Polyfill\\Util\\' => array($vendorDir . '/symfony/polyfill-util'),
+    'Symfony\\Polyfill\\Php56\\' => array($vendorDir . '/symfony/polyfill-php56'),
+    'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
+    'Pusher\\' => array($vendorDir . '/pusher/pusher-php-server/src'),
+    'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
+    'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'),
+    'PragmaRX\\Google2FA\\Tests\\' => array($vendorDir . '/pragmarx/google2fa/tests'),
+    'PragmaRX\\Google2FA\\' => array($vendorDir . '/pragmarx/google2fa/src'),
+    'ParagonIE\\ConstantTime\\' => array($vendorDir . '/paragonie/constant_time_encoding/src'),
+    'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
+    'Lcobucci\\JWT\\' => array($vendorDir . '/lcobucci/jwt/src'),
+    'LayerShifter\\TLDSupport\\' => array($vendorDir . '/layershifter/tld-support/src'),
+    'LayerShifter\\TLDExtract\\' => array($vendorDir . '/layershifter/tld-extract/src'),
+    'LayerShifter\\TLDDatabase\\' => array($vendorDir . '/layershifter/tld-database/src'),
+    'Kryptonit3\\Sonarr\\' => array($vendorDir . '/kryptonit3/sonarr/src'),
+    'Kryptonit3\\SickRage\\' => array($vendorDir . '/kryptonit3/sickrage/src'),
+    'Kryptonit3\\CouchPotato\\' => array($vendorDir . '/kryptonit3/couchpotato/src'),
+    'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
+    'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
+    'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
+    'Composer\\Semver\\' => array($vendorDir . '/composer/semver/src'),
+);

+ 70 - 0
api/vendor/composer/autoload_real.php

@@ -0,0 +1,70 @@
+<?php
+
+// autoload_real.php @generated by Composer
+
+class ComposerAutoloaderInitcbdc783d76f8e7563dcce7d8af053ecb
+{
+    private static $loader;
+
+    public static function loadClassLoader($class)
+    {
+        if ('Composer\Autoload\ClassLoader' === $class) {
+            require __DIR__ . '/ClassLoader.php';
+        }
+    }
+
+    public static function getLoader()
+    {
+        if (null !== self::$loader) {
+            return self::$loader;
+        }
+
+        spl_autoload_register(array('ComposerAutoloaderInitcbdc783d76f8e7563dcce7d8af053ecb', 'loadClassLoader'), true, true);
+        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
+        spl_autoload_unregister(array('ComposerAutoloaderInitcbdc783d76f8e7563dcce7d8af053ecb', 'loadClassLoader'));
+
+        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
+        if ($useStaticLoader) {
+            require_once __DIR__ . '/autoload_static.php';
+
+            call_user_func(\Composer\Autoload\ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb::getInitializer($loader));
+        } else {
+            $map = require __DIR__ . '/autoload_namespaces.php';
+            foreach ($map as $namespace => $path) {
+                $loader->set($namespace, $path);
+            }
+
+            $map = require __DIR__ . '/autoload_psr4.php';
+            foreach ($map as $namespace => $path) {
+                $loader->setPsr4($namespace, $path);
+            }
+
+            $classMap = require __DIR__ . '/autoload_classmap.php';
+            if ($classMap) {
+                $loader->addClassMap($classMap);
+            }
+        }
+
+        $loader->register(true);
+
+        if ($useStaticLoader) {
+            $includeFiles = Composer\Autoload\ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb::$files;
+        } else {
+            $includeFiles = require __DIR__ . '/autoload_files.php';
+        }
+        foreach ($includeFiles as $fileIdentifier => $file) {
+            composerRequirecbdc783d76f8e7563dcce7d8af053ecb($fileIdentifier, $file);
+        }
+
+        return $loader;
+    }
+}
+
+function composerRequirecbdc783d76f8e7563dcce7d8af053ecb($fileIdentifier, $file)
+{
+    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+        require $file;
+
+        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+    }
+}

+ 235 - 0
api/vendor/composer/autoload_static.php

@@ -0,0 +1,235 @@
+<?php
+
+// autoload_static.php @generated by Composer
+
+namespace Composer\Autoload;
+
+class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
+{
+    public static $files = array (
+        '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
+        'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
+        'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php',
+        '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
+        '3109cb1a231dcd04bee1f9f620d46975' => __DIR__ . '/..' . '/paragonie/sodium_compat/autoload.php',
+        'bd9634f2d41831496de0d3dfe4c94881' => __DIR__ . '/..' . '/symfony/polyfill-php56/bootstrap.php',
+        '0097ca414fcb37c7130ac24b05f485f8' => __DIR__ . '/..' . '/dibi/dibi/src/loader.php',
+        '98ac7ebbcd4b271b4f101d3af2543920' => __DIR__ . '/..' . '/layershifter/tld-extract/src/static.php',
+    );
+
+    public static $prefixLengthsPsr4 = array (
+        'T' => 
+        array (
+            'TrueBV\\' => 7,
+        ),
+        'S' => 
+        array (
+            'Symfony\\Polyfill\\Util\\' => 22,
+            'Symfony\\Polyfill\\Php56\\' => 23,
+            'Symfony\\Polyfill\\Mbstring\\' => 26,
+        ),
+        'P' => 
+        array (
+            'Pusher\\' => 7,
+            'Psr\\Log\\' => 8,
+            'Psr\\Http\\Message\\' => 17,
+            'PragmaRX\\Google2FA\\Tests\\' => 25,
+            'PragmaRX\\Google2FA\\' => 19,
+            'ParagonIE\\ConstantTime\\' => 23,
+            'PHPMailer\\PHPMailer\\' => 20,
+        ),
+        'L' => 
+        array (
+            'Lcobucci\\JWT\\' => 13,
+            'LayerShifter\\TLDSupport\\' => 24,
+            'LayerShifter\\TLDExtract\\' => 24,
+            'LayerShifter\\TLDDatabase\\' => 25,
+        ),
+        'K' => 
+        array (
+            'Kryptonit3\\Sonarr\\' => 18,
+            'Kryptonit3\\SickRage\\' => 20,
+            'Kryptonit3\\CouchPotato\\' => 23,
+        ),
+        'G' => 
+        array (
+            'GuzzleHttp\\Psr7\\' => 16,
+            'GuzzleHttp\\Promise\\' => 19,
+            'GuzzleHttp\\' => 11,
+        ),
+        'C' => 
+        array (
+            'Composer\\Semver\\' => 16,
+        ),
+    );
+
+    public static $prefixDirsPsr4 = array (
+        'TrueBV\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/true/punycode/src',
+        ),
+        'Symfony\\Polyfill\\Util\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/symfony/polyfill-util',
+        ),
+        'Symfony\\Polyfill\\Php56\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/symfony/polyfill-php56',
+        ),
+        'Symfony\\Polyfill\\Mbstring\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
+        ),
+        'Pusher\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/pusher/pusher-php-server/src',
+        ),
+        'Psr\\Log\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
+        ),
+        'Psr\\Http\\Message\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/psr/http-message/src',
+        ),
+        'PragmaRX\\Google2FA\\Tests\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/pragmarx/google2fa/tests',
+        ),
+        'PragmaRX\\Google2FA\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/pragmarx/google2fa/src',
+        ),
+        'ParagonIE\\ConstantTime\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src',
+        ),
+        'PHPMailer\\PHPMailer\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/phpmailer/phpmailer/src',
+        ),
+        'Lcobucci\\JWT\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/lcobucci/jwt/src',
+        ),
+        'LayerShifter\\TLDSupport\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/layershifter/tld-support/src',
+        ),
+        'LayerShifter\\TLDExtract\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/layershifter/tld-extract/src',
+        ),
+        'LayerShifter\\TLDDatabase\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/layershifter/tld-database/src',
+        ),
+        'Kryptonit3\\Sonarr\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/kryptonit3/sonarr/src',
+        ),
+        'Kryptonit3\\SickRage\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/kryptonit3/sickrage/src',
+        ),
+        'Kryptonit3\\CouchPotato\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/kryptonit3/couchpotato/src',
+        ),
+        'GuzzleHttp\\Psr7\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
+        ),
+        'GuzzleHttp\\Promise\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/guzzlehttp/promises/src',
+        ),
+        'GuzzleHttp\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
+        ),
+        'Composer\\Semver\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/composer/semver/src',
+        ),
+    );
+
+    public static $prefixesPsr0 = array (
+        'R' => 
+        array (
+            'Requests' => 
+            array (
+                0 => __DIR__ . '/..' . '/rmccue/requests/library',
+            ),
+        ),
+    );
+
+    public static $classMap = array (
+        'Dibi\\Bridges\\Nette\\DibiExtension22' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Bridges/Nette/DibiExtension22.php',
+        'Dibi\\Bridges\\Tracy\\Panel' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Bridges/Tracy/Panel.php',
+        'Dibi\\Connection' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Connection.php',
+        'Dibi\\ConstraintViolationException' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',
+        'Dibi\\DataSource' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/DataSource.php',
+        'Dibi\\DateTime' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/DateTime.php',
+        'Dibi\\Driver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/interfaces.php',
+        'Dibi\\DriverException' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',
+        'Dibi\\Drivers\\FirebirdDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/FirebirdDriver.php',
+        'Dibi\\Drivers\\MsSqlDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/MsSqlDriver.php',
+        'Dibi\\Drivers\\MsSqlReflector' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/MsSqlReflector.php',
+        'Dibi\\Drivers\\MySqlDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/MySqlDriver.php',
+        'Dibi\\Drivers\\MySqlReflector' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/MySqlReflector.php',
+        'Dibi\\Drivers\\MySqliDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/MySqliDriver.php',
+        'Dibi\\Drivers\\OdbcDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/OdbcDriver.php',
+        'Dibi\\Drivers\\OracleDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/OracleDriver.php',
+        'Dibi\\Drivers\\PdoDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/PdoDriver.php',
+        'Dibi\\Drivers\\PostgreDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/PostgreDriver.php',
+        'Dibi\\Drivers\\Sqlite3Driver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/Sqlite3Driver.php',
+        'Dibi\\Drivers\\SqliteReflector' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/SqliteReflector.php',
+        'Dibi\\Drivers\\SqlsrvDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/SqlsrvDriver.php',
+        'Dibi\\Drivers\\SqlsrvReflector' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Drivers/SqlsrvReflector.php',
+        'Dibi\\Event' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Event.php',
+        'Dibi\\Exception' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',
+        'Dibi\\Expression' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Expression.php',
+        'Dibi\\Fluent' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Fluent.php',
+        'Dibi\\ForeignKeyConstraintViolationException' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',
+        'Dibi\\HashMap' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/HashMap.php',
+        'Dibi\\HashMapBase' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/HashMap.php',
+        'Dibi\\Helpers' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Helpers.php',
+        'Dibi\\IDataSource' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/interfaces.php',
+        'Dibi\\Literal' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Literal.php',
+        'Dibi\\Loggers\\FileLogger' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Loggers/FileLogger.php',
+        'Dibi\\Loggers\\FirePhpLogger' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Loggers/FirePhpLogger.php',
+        'Dibi\\NotImplementedException' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',
+        'Dibi\\NotNullConstraintViolationException' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',
+        'Dibi\\NotSupportedException' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',
+        'Dibi\\PcreException' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',
+        'Dibi\\ProcedureException' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',
+        'Dibi\\Reflection\\Column' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Reflection/Column.php',
+        'Dibi\\Reflection\\Database' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Reflection/Database.php',
+        'Dibi\\Reflection\\ForeignKey' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Reflection/ForeignKey.php',
+        'Dibi\\Reflection\\Index' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Reflection/Index.php',
+        'Dibi\\Reflection\\Result' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Reflection/Result.php',
+        'Dibi\\Reflection\\Table' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Reflection/Table.php',
+        'Dibi\\Reflector' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/interfaces.php',
+        'Dibi\\Result' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Result.php',
+        'Dibi\\ResultDriver' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/interfaces.php',
+        'Dibi\\ResultIterator' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/ResultIterator.php',
+        'Dibi\\Row' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Row.php',
+        'Dibi\\Strict' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Strict.php',
+        'Dibi\\Translator' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Translator.php',
+        'Dibi\\Type' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/Type.php',
+        'Dibi\\UniqueConstraintViolationException' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/exceptions.php',
+        'dibi' => __DIR__ . '/..' . '/dibi/dibi/src/Dibi/dibi.php',
+    );
+
+    public static function getInitializer(ClassLoader $loader)
+    {
+        return \Closure::bind(function () use ($loader) {
+            $loader->prefixLengthsPsr4 = ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb::$prefixDirsPsr4;
+            $loader->prefixesPsr0 = ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb::$prefixesPsr0;
+            $loader->classMap = ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb::$classMap;
+
+        }, null, ClassLoader::class);
+    }
+}

+ 1402 - 0
api/vendor/composer/installed.json

@@ -0,0 +1,1402 @@
+[
+    {
+        "name": "lcobucci/jwt",
+        "version": "3.2.2",
+        "version_normalized": "3.2.2.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/lcobucci/jwt.git",
+            "reference": "0b5930be73582369e10c4d4bb7a12bac927a203c"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/lcobucci/jwt/zipball/0b5930be73582369e10c4d4bb7a12bac927a203c",
+            "reference": "0b5930be73582369e10c4d4bb7a12bac927a203c",
+            "shasum": ""
+        },
+        "require": {
+            "ext-openssl": "*",
+            "php": ">=5.5"
+        },
+        "require-dev": {
+            "mdanter/ecc": "~0.3.1",
+            "mikey179/vfsstream": "~1.5",
+            "phpmd/phpmd": "~2.2",
+            "phpunit/php-invoker": "~1.1",
+            "phpunit/phpunit": "~4.5",
+            "squizlabs/php_codesniffer": "~2.3"
+        },
+        "suggest": {
+            "mdanter/ecc": "Required to use Elliptic Curves based algorithms."
+        },
+        "time": "2017-09-01T08:23:26+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "3.1-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Lcobucci\\JWT\\": "src"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "BSD-3-Clause"
+        ],
+        "authors": [
+            {
+                "name": "Luís Otávio Cobucci Oblonczyk",
+                "email": "lcobucci@gmail.com",
+                "role": "Developer"
+            }
+        ],
+        "description": "A simple library to work with JSON Web Token and JSON Web Signature",
+        "keywords": [
+            "JWS",
+            "jwt"
+        ]
+    },
+    {
+        "name": "composer/semver",
+        "version": "1.4.2",
+        "version_normalized": "1.4.2.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/composer/semver.git",
+            "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573",
+            "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573",
+            "shasum": ""
+        },
+        "require": {
+            "php": "^5.3.2 || ^7.0"
+        },
+        "require-dev": {
+            "phpunit/phpunit": "^4.5 || ^5.0.5",
+            "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0"
+        },
+        "time": "2016-08-30T16:08:34+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.x-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Composer\\Semver\\": "src"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nils Adermann",
+                "email": "naderman@naderman.de",
+                "homepage": "http://www.naderman.de"
+            },
+            {
+                "name": "Jordi Boggiano",
+                "email": "j.boggiano@seld.be",
+                "homepage": "http://seld.be"
+            },
+            {
+                "name": "Rob Bast",
+                "email": "rob.bast@gmail.com",
+                "homepage": "http://robbast.nl"
+            }
+        ],
+        "description": "Semver library that offers utilities, version constraint parsing and validation.",
+        "keywords": [
+            "semantic",
+            "semver",
+            "validation",
+            "versioning"
+        ]
+    },
+    {
+        "name": "phpmailer/phpmailer",
+        "version": "v6.0.3",
+        "version_normalized": "6.0.3.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/PHPMailer/PHPMailer.git",
+            "reference": "44d49bab5ab1fef721d3ee07e75dc0865ddf4cc6"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/44d49bab5ab1fef721d3ee07e75dc0865ddf4cc6",
+            "reference": "44d49bab5ab1fef721d3ee07e75dc0865ddf4cc6",
+            "shasum": ""
+        },
+        "require": {
+            "ext-ctype": "*",
+            "ext-filter": "*",
+            "php": ">=5.5.0"
+        },
+        "require-dev": {
+            "doctrine/annotations": "1.2.*",
+            "friendsofphp/php-cs-fixer": "^2.2",
+            "phpdocumentor/phpdocumentor": "2.*",
+            "phpunit/phpunit": "^4.8 || ^5.7",
+            "zendframework/zend-eventmanager": "3.0.*",
+            "zendframework/zend-i18n": "2.7.3",
+            "zendframework/zend-serializer": "2.7.*"
+        },
+        "suggest": {
+            "ext-mbstring": "Needed to send email in multibyte encoding charset",
+            "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
+            "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
+            "psr/log": "For optional PSR-3 debug logging",
+            "stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication",
+            "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)"
+        },
+        "time": "2018-01-05T13:19:58+00:00",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "PHPMailer\\PHPMailer\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "LGPL-2.1"
+        ],
+        "authors": [
+            {
+                "name": "Jim Jagielski",
+                "email": "jimjag@gmail.com"
+            },
+            {
+                "name": "Marcus Bointon",
+                "email": "phpmailer@synchromedia.co.uk"
+            },
+            {
+                "name": "Andy Prevost",
+                "email": "codeworxtech@users.sourceforge.net"
+            },
+            {
+                "name": "Brent R. Matzelle"
+            }
+        ],
+        "description": "PHPMailer is a full-featured email creation and transfer class for PHP"
+    },
+    {
+        "name": "rmccue/requests",
+        "version": "v1.7.0",
+        "version_normalized": "1.7.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/rmccue/Requests.git",
+            "reference": "87932f52ffad70504d93f04f15690cf16a089546"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/rmccue/Requests/zipball/87932f52ffad70504d93f04f15690cf16a089546",
+            "reference": "87932f52ffad70504d93f04f15690cf16a089546",
+            "shasum": ""
+        },
+        "require": {
+            "php": ">=5.2"
+        },
+        "require-dev": {
+            "requests/test-server": "dev-master"
+        },
+        "time": "2016-10-13T00:11:37+00:00",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-0": {
+                "Requests": "library/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "ISC"
+        ],
+        "authors": [
+            {
+                "name": "Ryan McCue",
+                "homepage": "http://ryanmccue.info"
+            }
+        ],
+        "description": "A HTTP library written in PHP, for human beings.",
+        "homepage": "http://github.com/rmccue/Requests",
+        "keywords": [
+            "curl",
+            "fsockopen",
+            "http",
+            "idna",
+            "ipv6",
+            "iri",
+            "sockets"
+        ]
+    },
+    {
+        "name": "guzzlehttp/promises",
+        "version": "v1.3.1",
+        "version_normalized": "1.3.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/guzzle/promises.git",
+            "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
+            "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
+            "shasum": ""
+        },
+        "require": {
+            "php": ">=5.5.0"
+        },
+        "require-dev": {
+            "phpunit/phpunit": "^4.0"
+        },
+        "time": "2016-12-20T10:07:11+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.4-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "GuzzleHttp\\Promise\\": "src/"
+            },
+            "files": [
+                "src/functions_include.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Michael Dowling",
+                "email": "mtdowling@gmail.com",
+                "homepage": "https://github.com/mtdowling"
+            }
+        ],
+        "description": "Guzzle promises library",
+        "keywords": [
+            "promise"
+        ]
+    },
+    {
+        "name": "psr/http-message",
+        "version": "1.0.1",
+        "version_normalized": "1.0.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/php-fig/http-message.git",
+            "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+            "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+            "shasum": ""
+        },
+        "require": {
+            "php": ">=5.3.0"
+        },
+        "time": "2016-08-06T14:39:51+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.0.x-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Psr\\Http\\Message\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "PHP-FIG",
+                "homepage": "http://www.php-fig.org/"
+            }
+        ],
+        "description": "Common interface for HTTP messages",
+        "homepage": "https://github.com/php-fig/http-message",
+        "keywords": [
+            "http",
+            "http-message",
+            "psr",
+            "psr-7",
+            "request",
+            "response"
+        ]
+    },
+    {
+        "name": "guzzlehttp/psr7",
+        "version": "1.4.2",
+        "version_normalized": "1.4.2.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/guzzle/psr7.git",
+            "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
+            "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c",
+            "shasum": ""
+        },
+        "require": {
+            "php": ">=5.4.0",
+            "psr/http-message": "~1.0"
+        },
+        "provide": {
+            "psr/http-message-implementation": "1.0"
+        },
+        "require-dev": {
+            "phpunit/phpunit": "~4.0"
+        },
+        "time": "2017-03-20T17:10:46+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.4-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "GuzzleHttp\\Psr7\\": "src/"
+            },
+            "files": [
+                "src/functions_include.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Michael Dowling",
+                "email": "mtdowling@gmail.com",
+                "homepage": "https://github.com/mtdowling"
+            },
+            {
+                "name": "Tobias Schultze",
+                "homepage": "https://github.com/Tobion"
+            }
+        ],
+        "description": "PSR-7 message implementation that also provides common utility methods",
+        "keywords": [
+            "http",
+            "message",
+            "request",
+            "response",
+            "stream",
+            "uri",
+            "url"
+        ]
+    },
+    {
+        "name": "kryptonit3/sonarr",
+        "version": "1.0.6.1",
+        "version_normalized": "1.0.6.1",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/Kryptonit3/Sonarr.git",
+            "reference": "e30c5c783a837270bcef81571ca9b95909c52e5e"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/Kryptonit3/Sonarr/zipball/e30c5c783a837270bcef81571ca9b95909c52e5e",
+            "reference": "e30c5c783a837270bcef81571ca9b95909c52e5e",
+            "shasum": ""
+        },
+        "require": {
+            "guzzlehttp/guzzle": "^6.1"
+        },
+        "time": "2017-06-30T01:25:49+00:00",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Kryptonit3\\Sonarr\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Jesse Szypulski",
+                "email": "jesse.szypulski@gmail.com"
+            }
+        ],
+        "description": "PHP Sonarr API Wrapper"
+    },
+    {
+        "name": "kryptonit3/couchpotato",
+        "version": "1.0.0",
+        "version_normalized": "1.0.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/Kryptonit3/CouchPotato.git",
+            "reference": "7a1fc892f70f120f74ff005850e923a0f1566376"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/Kryptonit3/CouchPotato/zipball/7a1fc892f70f120f74ff005850e923a0f1566376",
+            "reference": "7a1fc892f70f120f74ff005850e923a0f1566376",
+            "shasum": ""
+        },
+        "require": {
+            "guzzlehttp/guzzle": "^6.1"
+        },
+        "time": "2016-02-06T22:02:39+00:00",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Kryptonit3\\CouchPotato\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Jesse Szypulski",
+                "email": "jesse.szypulski@gmail.com"
+            }
+        ],
+        "description": "PHP Wrapper for CouchPotato API"
+    },
+    {
+        "name": "kryptonit3/sickrage",
+        "version": "1.0.1",
+        "version_normalized": "1.0.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/Kryptonit3/SickRage.git",
+            "reference": "441a293b5c219c3cdd1ebebd2bcf4518598f84aa"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/Kryptonit3/SickRage/zipball/441a293b5c219c3cdd1ebebd2bcf4518598f84aa",
+            "reference": "441a293b5c219c3cdd1ebebd2bcf4518598f84aa",
+            "shasum": ""
+        },
+        "require": {
+            "guzzlehttp/guzzle": "^6.1"
+        },
+        "time": "2016-08-08T00:57:46+00:00",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Kryptonit3\\SickRage\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Jesse Szypulski",
+                "email": "jesse.szypulski@gmail.com"
+            }
+        ],
+        "description": "PHP Wrapper for SickRage / SickBeard API"
+    },
+    {
+        "name": "paragonie/random_compat",
+        "version": "v9.99.99",
+        "version_normalized": "9.99.99.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/paragonie/random_compat.git",
+            "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
+            "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
+            "shasum": ""
+        },
+        "require": {
+            "php": "^7"
+        },
+        "require-dev": {
+            "phpunit/phpunit": "4.*|5.*",
+            "vimeo/psalm": "^1"
+        },
+        "suggest": {
+            "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
+        },
+        "time": "2018-07-02T15:55:56+00:00",
+        "type": "library",
+        "installation-source": "dist",
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Paragon Initiative Enterprises",
+                "email": "security@paragonie.com",
+                "homepage": "https://paragonie.com"
+            }
+        ],
+        "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
+        "keywords": [
+            "csprng",
+            "polyfill",
+            "pseudorandom",
+            "random"
+        ]
+    },
+    {
+        "name": "paragonie/sodium_compat",
+        "version": "v1.6.4",
+        "version_normalized": "1.6.4.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/paragonie/sodium_compat.git",
+            "reference": "3f2fd07977541b4d630ea0365ad0eceddee5179c"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/3f2fd07977541b4d630ea0365ad0eceddee5179c",
+            "reference": "3f2fd07977541b4d630ea0365ad0eceddee5179c",
+            "shasum": ""
+        },
+        "require": {
+            "paragonie/random_compat": ">=1",
+            "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7"
+        },
+        "require-dev": {
+            "phpunit/phpunit": "^3|^4|^5"
+        },
+        "suggest": {
+            "ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.",
+            "ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security."
+        },
+        "time": "2018-08-29T22:02:48+00:00",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "files": [
+                "autoload.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "ISC"
+        ],
+        "authors": [
+            {
+                "name": "Paragon Initiative Enterprises",
+                "email": "security@paragonie.com"
+            },
+            {
+                "name": "Frank Denis",
+                "email": "jedisct1@pureftpd.org"
+            }
+        ],
+        "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists",
+        "keywords": [
+            "Authentication",
+            "BLAKE2b",
+            "ChaCha20",
+            "ChaCha20-Poly1305",
+            "Chapoly",
+            "Curve25519",
+            "Ed25519",
+            "EdDSA",
+            "Edwards-curve Digital Signature Algorithm",
+            "Elliptic Curve Diffie-Hellman",
+            "Poly1305",
+            "Pure-PHP cryptography",
+            "RFC 7748",
+            "RFC 8032",
+            "Salpoly",
+            "Salsa20",
+            "X25519",
+            "XChaCha20-Poly1305",
+            "XSalsa20-Poly1305",
+            "Xchacha20",
+            "Xsalsa20",
+            "aead",
+            "cryptography",
+            "ecdh",
+            "elliptic curve",
+            "elliptic curve cryptography",
+            "encryption",
+            "libsodium",
+            "php",
+            "public-key cryptography",
+            "secret-key cryptography",
+            "side-channel resistant"
+        ]
+    },
+    {
+        "name": "psr/log",
+        "version": "1.0.2",
+        "version_normalized": "1.0.2.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/php-fig/log.git",
+            "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
+            "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
+            "shasum": ""
+        },
+        "require": {
+            "php": ">=5.3.0"
+        },
+        "time": "2016-10-10T12:19:37+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.0.x-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Psr\\Log\\": "Psr/Log/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "PHP-FIG",
+                "homepage": "http://www.php-fig.org/"
+            }
+        ],
+        "description": "Common interface for logging libraries",
+        "homepage": "https://github.com/php-fig/log",
+        "keywords": [
+            "log",
+            "psr",
+            "psr-3"
+        ]
+    },
+    {
+        "name": "pusher/pusher-php-server",
+        "version": "v3.2.0",
+        "version_normalized": "3.2.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/pusher/pusher-http-php.git",
+            "reference": "4da4d208fe13fd2ff71ede5063a3456f23f38cf7"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/4da4d208fe13fd2ff71ede5063a3456f23f38cf7",
+            "reference": "4da4d208fe13fd2ff71ede5063a3456f23f38cf7",
+            "shasum": ""
+        },
+        "require": {
+            "ext-curl": "*",
+            "paragonie/sodium_compat": "^1.6",
+            "php": "^5.4 || ^7.0",
+            "psr/log": "^1.0"
+        },
+        "require-dev": {
+            "phpunit/phpunit": "^4.8 || ^5.7"
+        },
+        "time": "2018-08-13T14:57:32+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "3.0-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Pusher\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "description": "Library for interacting with the Pusher REST API",
+        "keywords": [
+            "events",
+            "messaging",
+            "php-pusher-server",
+            "publish",
+            "push",
+            "pusher",
+            "real time",
+            "real-time",
+            "realtime",
+            "rest",
+            "trigger"
+        ]
+    },
+    {
+        "name": "symfony/polyfill-util",
+        "version": "v1.9.0",
+        "version_normalized": "1.9.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/polyfill-util.git",
+            "reference": "8e15d04ba3440984d23e7964b2ee1d25c8de1581"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/8e15d04ba3440984d23e7964b2ee1d25c8de1581",
+            "reference": "8e15d04ba3440984d23e7964b2ee1d25c8de1581",
+            "shasum": ""
+        },
+        "require": {
+            "php": ">=5.3.3"
+        },
+        "time": "2018-08-06T14:22:27+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.9-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Polyfill\\Util\\": ""
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Symfony utilities for portability of PHP codes",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "compat",
+            "compatibility",
+            "polyfill",
+            "shim"
+        ]
+    },
+    {
+        "name": "symfony/polyfill-php56",
+        "version": "v1.9.0",
+        "version_normalized": "1.9.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/polyfill-php56.git",
+            "reference": "7b4fc009172cc0196535b0328bd1226284a28000"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/7b4fc009172cc0196535b0328bd1226284a28000",
+            "reference": "7b4fc009172cc0196535b0328bd1226284a28000",
+            "shasum": ""
+        },
+        "require": {
+            "php": ">=5.3.3",
+            "symfony/polyfill-util": "~1.0"
+        },
+        "time": "2018-08-06T14:22:27+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.9-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Polyfill\\Php56\\": ""
+            },
+            "files": [
+                "bootstrap.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "compatibility",
+            "polyfill",
+            "portable",
+            "shim"
+        ]
+    },
+    {
+        "name": "paragonie/constant_time_encoding",
+        "version": "v2.2.2",
+        "version_normalized": "2.2.2.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/paragonie/constant_time_encoding.git",
+            "reference": "eccf915f45f911bfb189d1d1638d940ec6ee6e33"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/eccf915f45f911bfb189d1d1638d940ec6ee6e33",
+            "reference": "eccf915f45f911bfb189d1d1638d940ec6ee6e33",
+            "shasum": ""
+        },
+        "require": {
+            "php": "^7"
+        },
+        "require-dev": {
+            "phpunit/phpunit": "^6|^7",
+            "vimeo/psalm": "^1"
+        },
+        "time": "2018-03-10T19:47:49+00:00",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "ParagonIE\\ConstantTime\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Paragon Initiative Enterprises",
+                "email": "security@paragonie.com",
+                "homepage": "https://paragonie.com",
+                "role": "Maintainer"
+            },
+            {
+                "name": "Steve 'Sc00bz' Thomas",
+                "email": "steve@tobtu.com",
+                "homepage": "https://www.tobtu.com",
+                "role": "Original Developer"
+            }
+        ],
+        "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
+        "keywords": [
+            "base16",
+            "base32",
+            "base32_decode",
+            "base32_encode",
+            "base64",
+            "base64_decode",
+            "base64_encode",
+            "bin2hex",
+            "encoding",
+            "hex",
+            "hex2bin",
+            "rfc4648"
+        ]
+    },
+    {
+        "name": "pragmarx/google2fa",
+        "version": "v3.0.3",
+        "version_normalized": "3.0.3.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/antonioribeiro/google2fa.git",
+            "reference": "6949226739e4424f40031e6f1c96b1fd64047335"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/6949226739e4424f40031e6f1c96b1fd64047335",
+            "reference": "6949226739e4424f40031e6f1c96b1fd64047335",
+            "shasum": ""
+        },
+        "require": {
+            "paragonie/constant_time_encoding": "~1.0|~2.0",
+            "paragonie/random_compat": ">=1",
+            "php": ">=5.4",
+            "symfony/polyfill-php56": "~1.2"
+        },
+        "require-dev": {
+            "bacon/bacon-qr-code": "~1.0",
+            "phpunit/phpunit": "~4|~5|~6"
+        },
+        "suggest": {
+            "bacon/bacon-qr-code": "Required to generate inline QR Codes."
+        },
+        "time": "2018-08-29T13:28:06+00:00",
+        "type": "library",
+        "extra": {
+            "component": "package",
+            "branch-alias": {
+                "dev-master": "2.0-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "PragmaRX\\Google2FA\\": "src/",
+                "PragmaRX\\Google2FA\\Tests\\": "tests/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Antonio Carlos Ribeiro",
+                "email": "acr@antoniocarlosribeiro.com",
+                "role": "Creator & Designer"
+            }
+        ],
+        "description": "A One Time Password Authentication package, compatible with Google Authenticator.",
+        "keywords": [
+            "2fa",
+            "Authentication",
+            "Two Factor Authentication",
+            "google2fa",
+            "laravel"
+        ]
+    },
+    {
+        "name": "guzzlehttp/guzzle",
+        "version": "6.3.3",
+        "version_normalized": "6.3.3.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/guzzle/guzzle.git",
+            "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba",
+            "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba",
+            "shasum": ""
+        },
+        "require": {
+            "guzzlehttp/promises": "^1.0",
+            "guzzlehttp/psr7": "^1.4",
+            "php": ">=5.5"
+        },
+        "require-dev": {
+            "ext-curl": "*",
+            "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
+            "psr/log": "^1.0"
+        },
+        "suggest": {
+            "psr/log": "Required for using the Log middleware"
+        },
+        "time": "2018-04-22T15:46:56+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "6.3-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "files": [
+                "src/functions_include.php"
+            ],
+            "psr-4": {
+                "GuzzleHttp\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Michael Dowling",
+                "email": "mtdowling@gmail.com",
+                "homepage": "https://github.com/mtdowling"
+            }
+        ],
+        "description": "Guzzle is a PHP HTTP client library",
+        "homepage": "http://guzzlephp.org/",
+        "keywords": [
+            "client",
+            "curl",
+            "framework",
+            "http",
+            "http client",
+            "rest",
+            "web service"
+        ]
+    },
+    {
+        "name": "dibi/dibi",
+        "version": "v3.2.3",
+        "version_normalized": "3.2.3.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/dg/dibi.git",
+            "reference": "906e1f240742124e49562cace271916e1b08c991"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/dg/dibi/zipball/906e1f240742124e49562cace271916e1b08c991",
+            "reference": "906e1f240742124e49562cace271916e1b08c991",
+            "shasum": ""
+        },
+        "require": {
+            "php": ">=5.4.4"
+        },
+        "replace": {
+            "dg/dibi": "*"
+        },
+        "require-dev": {
+            "nette/tester": "~1.7",
+            "tracy/tracy": "~2.2"
+        },
+        "time": "2018-09-17T11:47:10+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "3.2-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "classmap": [
+                "src/"
+            ],
+            "files": [
+                "src/loader.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "BSD-3-Clause",
+            "GPL-2.0",
+            "GPL-3.0"
+        ],
+        "authors": [
+            {
+                "name": "David Grudl",
+                "homepage": "https://davidgrudl.com"
+            }
+        ],
+        "description": "Dibi is Database Abstraction Library for PHP",
+        "homepage": "https://dibiphp.com",
+        "keywords": [
+            "access",
+            "database",
+            "dbal",
+            "mssql",
+            "mysql",
+            "odbc",
+            "oracle",
+            "pdo",
+            "postgresql",
+            "sqlite",
+            "sqlsrv"
+        ]
+    },
+    {
+        "name": "symfony/polyfill-mbstring",
+        "version": "v1.10.0",
+        "version_normalized": "1.10.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/symfony/polyfill-mbstring.git",
+            "reference": "c79c051f5b3a46be09205c73b80b346e4153e494"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494",
+            "reference": "c79c051f5b3a46be09205c73b80b346e4153e494",
+            "shasum": ""
+        },
+        "require": {
+            "php": ">=5.3.3"
+        },
+        "suggest": {
+            "ext-mbstring": "For best performance"
+        },
+        "time": "2018-09-21T13:07:52+00:00",
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "1.9-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Symfony\\Polyfill\\Mbstring\\": ""
+            },
+            "files": [
+                "bootstrap.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Nicolas Grekas",
+                "email": "p@tchwork.com"
+            },
+            {
+                "name": "Symfony Community",
+                "homepage": "https://symfony.com/contributors"
+            }
+        ],
+        "description": "Symfony polyfill for the Mbstring extension",
+        "homepage": "https://symfony.com",
+        "keywords": [
+            "compatibility",
+            "mbstring",
+            "polyfill",
+            "portable",
+            "shim"
+        ]
+    },
+    {
+        "name": "true/punycode",
+        "version": "v2.1.1",
+        "version_normalized": "2.1.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/true/php-punycode.git",
+            "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/true/php-punycode/zipball/a4d0c11a36dd7f4e7cd7096076cab6d3378a071e",
+            "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e",
+            "shasum": ""
+        },
+        "require": {
+            "php": ">=5.3.0",
+            "symfony/polyfill-mbstring": "^1.3"
+        },
+        "require-dev": {
+            "phpunit/phpunit": "~4.7",
+            "squizlabs/php_codesniffer": "~2.0"
+        },
+        "time": "2016-11-16T10:37:54+00:00",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "TrueBV\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Renan Gonçalves",
+                "email": "renan.saddam@gmail.com"
+            }
+        ],
+        "description": "A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA)",
+        "homepage": "https://github.com/true/php-punycode",
+        "keywords": [
+            "idna",
+            "punycode"
+        ]
+    },
+    {
+        "name": "layershifter/tld-support",
+        "version": "1.1.1",
+        "version_normalized": "1.1.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/layershifter/TLDSupport.git",
+            "reference": "9b9eb2350db82239289f4e6eb250a2fe4865b0db"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/layershifter/TLDSupport/zipball/9b9eb2350db82239289f4e6eb250a2fe4865b0db",
+            "reference": "9b9eb2350db82239289f4e6eb250a2fe4865b0db",
+            "shasum": ""
+        },
+        "require": {
+            "php": ">=5.5",
+            "symfony/polyfill-mbstring": "^1.2"
+        },
+        "require-dev": {
+            "codeclimate/php-test-reporter": "dev-master",
+            "phpmd/phpmd": "@stable",
+            "phpunit/phpunit": "4.8.*",
+            "squizlabs/php_codesniffer": "2.*"
+        },
+        "time": "2016-06-18T12:28:04+00:00",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "LayerShifter\\TLDSupport\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "Apache-2.0"
+        ],
+        "authors": [
+            {
+                "name": "Alexander Fedyashov",
+                "email": "a@fedyashov.com"
+            }
+        ],
+        "description": "Support package for TLDDatabase and TLDExtract"
+    },
+    {
+        "name": "layershifter/tld-database",
+        "version": "1.0.64",
+        "version_normalized": "1.0.64.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/layershifter/TLDDatabase.git",
+            "reference": "16d9b7cd195eda57c08c64f75e9dbc175320e0cc"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/layershifter/TLDDatabase/zipball/16d9b7cd195eda57c08c64f75e9dbc175320e0cc",
+            "reference": "16d9b7cd195eda57c08c64f75e9dbc175320e0cc",
+            "shasum": ""
+        },
+        "require": {
+            "ext-curl": "*",
+            "layershifter/tld-support": "^1.0.0",
+            "php": "^5.5.0 || ^7.0"
+        },
+        "require-dev": {
+            "codeclimate/php-test-reporter": "dev-master",
+            "mikey179/vfsstream": "^1.6",
+            "phpmd/phpmd": "@stable",
+            "phpunit/phpunit": "^4.8 || ^5.0",
+            "squizlabs/php_codesniffer": "~2.0"
+        },
+        "time": "2018-11-09T09:12:04+00:00",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "LayerShifter\\TLDDatabase\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "Apache-2.0"
+        ],
+        "authors": [
+            {
+                "name": "Alexander Fedyashov",
+                "email": "a@fedyashov.com"
+            }
+        ],
+        "description": "Database abstraction for Public Suffix List",
+        "keywords": [
+            "PSL",
+            "Public Suffix List",
+            "domain database",
+            "tld database"
+        ]
+    },
+    {
+        "name": "layershifter/tld-extract",
+        "version": "2.0.0",
+        "version_normalized": "2.0.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/layershifter/TLDExtract.git",
+            "reference": "236bfa0fd253e70f8db10f896e788e12b58b4a41"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/layershifter/TLDExtract/zipball/236bfa0fd253e70f8db10f896e788e12b58b4a41",
+            "reference": "236bfa0fd253e70f8db10f896e788e12b58b4a41",
+            "shasum": ""
+        },
+        "require": {
+            "layershifter/tld-database": "^1.0",
+            "layershifter/tld-support": "^1.1",
+            "php": "^5.5.0 || ^7.0",
+            "true/punycode": "^2.1.1"
+        },
+        "require-dev": {
+            "phpmd/phpmd": "@stable",
+            "phpunit/phpunit": "^4.8 || ^5.0",
+            "squizlabs/php_codesniffer": "~2.0"
+        },
+        "time": "2018-09-28T11:11:20+00:00",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "files": [
+                "src/static.php"
+            ],
+            "psr-4": {
+                "LayerShifter\\TLDExtract\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "Apache-2.0"
+        ],
+        "authors": [
+            {
+                "name": "Alexander Fedyashov",
+                "email": "a@fedyashov.com"
+            }
+        ],
+        "description": "TLDExtract, library for extracting parts of domain, e.q. domain parser",
+        "keywords": [
+            "TLDExtract",
+            "domain parser"
+        ]
+    }
+]

+ 66 - 0
api/vendor/composer/semver/CHANGELOG.md

@@ -0,0 +1,66 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+This project adheres to [Semantic Versioning](http://semver.org/).
+
+### [1.4.2] 2016-08-30
+
+  * Fixed: collapsing of complex constraints lead to buggy constraints
+
+### [1.4.1] 2016-06-02
+
+  * Changed: branch-like requirements no longer strip build metadata - [composer/semver#38](https://github.com/composer/semver/pull/38).
+
+### [1.4.0] 2016-03-30
+
+  * Added: getters on MultiConstraint - [composer/semver#35](https://github.com/composer/semver/pull/35).
+
+### [1.3.0] 2016-02-25
+
+  * Fixed: stability parsing - [composer/composer#1234](https://github.com/composer/composer/issues/4889).
+  * Changed: collapse contiguous constraints when possible.
+
+### [1.2.0] 2015-11-10
+
+  * Changed: allow multiple numerical identifiers in 'pre-release' version part.
+  * Changed: add more 'v' prefix support.
+
+### [1.1.0] 2015-11-03
+
+  * Changed: dropped redundant `test` namespace.
+  * Changed: minor adjustment in datetime parsing normalization.
+  * Changed: `ConstraintInterface` relaxed, setPrettyString is not required anymore.
+  * Changed: `AbstractConstraint` marked deprecated, will be removed in 2.0.
+  * Changed: `Constraint` is now extensible.
+
+### [1.0.0] 2015-09-21
+
+  * Break: `VersionConstraint` renamed to `Constraint`.
+  * Break: `SpecificConstraint` renamed to `AbstractConstraint`.
+  * Break: `LinkConstraintInterface` renamed to `ConstraintInterface`.
+  * Break: `VersionParser::parseNameVersionPairs` was removed.
+  * Changed: `VersionParser::parseConstraints` allows (but ignores) build metadata now.
+  * Changed: `VersionParser::parseConstraints` allows (but ignores) prefixing numeric versions with a 'v' now.
+  * Changed: Fixed namespace(s) of test files.
+  * Changed: `Comparator::compare` no longer throws `InvalidArgumentException`.
+  * Changed: `Constraint` now throws `InvalidArgumentException`.
+
+### [0.1.0] 2015-07-23
+
+  * Added: `Composer\Semver\Comparator`, various methods to compare versions.
+  * Added: various documents such as README.md, LICENSE, etc.
+  * Added: configuration files for Git, Travis, php-cs-fixer, phpunit.
+  * Break: the following namespaces were renamed:
+    - Namespace: `Composer\Package\Version` -> `Composer\Semver`
+    - Namespace: `Composer\Package\LinkConstraint` -> `Composer\Semver\Constraint`
+    - Namespace: `Composer\Test\Package\Version` -> `Composer\Test\Semver`
+    - Namespace: `Composer\Test\Package\LinkConstraint` -> `Composer\Test\Semver\Constraint`
+  * Changed: code style using php-cs-fixer.
+
+[1.4.1]: https://github.com/composer/semver/compare/1.4.0...1.4.1
+[1.4.0]: https://github.com/composer/semver/compare/1.3.0...1.4.0
+[1.3.0]: https://github.com/composer/semver/compare/1.2.0...1.3.0
+[1.2.0]: https://github.com/composer/semver/compare/1.1.0...1.2.0
+[1.1.0]: https://github.com/composer/semver/compare/1.0.0...1.1.0
+[1.0.0]: https://github.com/composer/semver/compare/0.1.0...1.0.0
+[0.1.0]: https://github.com/composer/semver/compare/5e0b9a4da...0.1.0

+ 19 - 0
api/vendor/composer/semver/LICENSE

@@ -0,0 +1,19 @@
+Copyright (C) 2015 Composer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 70 - 0
api/vendor/composer/semver/README.md

@@ -0,0 +1,70 @@
+composer/semver
+===============
+
+Semver library that offers utilities, version constraint parsing and validation.
+
+Originally written as part of [composer/composer](https://github.com/composer/composer),
+now extracted and made available as a stand-alone library.
+
+[![Build Status](https://travis-ci.org/composer/semver.svg?branch=master)](https://travis-ci.org/composer/semver)
+
+
+Installation
+------------
+
+Install the latest version with:
+
+```bash
+$ composer require composer/semver
+```
+
+
+Requirements
+------------
+
+* PHP 5.3.2 is required but using the latest version of PHP is highly recommended.
+
+
+Version Comparison
+------------------
+
+For details on how versions are compared, refer to the [Versions](https://getcomposer.org/doc/articles/versions.md)
+article in the documentation section of the [getcomposer.org](https://getcomposer.org) website.
+
+
+Basic usage
+-----------
+
+### Comparator
+
+The `Composer\Semver\Comparator` class provides the following methods for comparing versions:
+
+* greaterThan($v1, $v2)
+* greaterThanOrEqualTo($v1, $v2)
+* lessThan($v1, $v2)
+* lessThanOrEqualTo($v1, $v2)
+* equalTo($v1, $v2)
+* notEqualTo($v1, $v2)
+
+Each function takes two version strings as arguments. For example:
+
+```php
+use Composer\Semver\Comparator;
+
+Comparator::greaterThan('1.25.0', '1.24.0'); // 1.25.0 > 1.24.0
+```
+
+### Semver
+
+The `Composer\Semver\Semver` class provides the following methods:
+
+* satisfies($version, $constraints)
+* satisfiedBy(array $versions, $constraint)
+* sort($versions)
+* rsort($versions)
+
+
+License
+-------
+
+composer/semver is licensed under the MIT License, see the LICENSE file for details.

+ 58 - 0
api/vendor/composer/semver/composer.json

@@ -0,0 +1,58 @@
+{
+    "name": "composer/semver",
+    "description": "Semver library that offers utilities, version constraint parsing and validation.",
+    "type": "library",
+    "license": "MIT",
+    "keywords": [
+        "semver",
+        "semantic",
+        "versioning",
+        "validation"
+    ],
+    "authors": [
+        {
+            "name": "Nils Adermann",
+            "email": "naderman@naderman.de",
+            "homepage": "http://www.naderman.de"
+        },
+        {
+            "name": "Jordi Boggiano",
+            "email": "j.boggiano@seld.be",
+            "homepage": "http://seld.be"
+        },
+        {
+            "name": "Rob Bast",
+            "email": "rob.bast@gmail.com",
+            "homepage": "http://robbast.nl"
+        }
+    ],
+    "support": {
+        "irc": "irc://irc.freenode.org/composer",
+        "issues": "https://github.com/composer/semver/issues"
+    },
+    "require": {
+        "php": "^5.3.2 || ^7.0"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^4.5 || ^5.0.5",
+        "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0"
+    },
+    "autoload": {
+        "psr-4": {
+            "Composer\\Semver\\": "src"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "Composer\\Semver\\": "tests"
+        }
+    },
+    "extra": {
+        "branch-alias": {
+            "dev-master": "1.x-dev"
+        }
+    },
+    "scripts": {
+        "test": "phpunit"
+    }
+}

+ 111 - 0
api/vendor/composer/semver/src/Comparator.php

@@ -0,0 +1,111 @@
+<?php
+
+/*
+ * This file is part of composer/semver.
+ *
+ * (c) Composer <https://github.com/composer>
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Composer\Semver;
+
+use Composer\Semver\Constraint\Constraint;
+
+class Comparator
+{
+    /**
+     * Evaluates the expression: $version1 > $version2.
+     *
+     * @param string $version1
+     * @param string $version2
+     *
+     * @return bool
+     */
+    public static function greaterThan($version1, $version2)
+    {
+        return self::compare($version1, '>', $version2);
+    }
+
+    /**
+     * Evaluates the expression: $version1 >= $version2.
+     *
+     * @param string $version1
+     * @param string $version2
+     *
+     * @return bool
+     */
+    public static function greaterThanOrEqualTo($version1, $version2)
+    {
+        return self::compare($version1, '>=', $version2);
+    }
+
+    /**
+     * Evaluates the expression: $version1 < $version2.
+     *
+     * @param string $version1
+     * @param string $version2
+     *
+     * @return bool
+     */
+    public static function lessThan($version1, $version2)
+    {
+        return self::compare($version1, '<', $version2);
+    }
+
+    /**
+     * Evaluates the expression: $version1 <= $version2.
+     *
+     * @param string $version1
+     * @param string $version2
+     *
+     * @return bool
+     */
+    public static function lessThanOrEqualTo($version1, $version2)
+    {
+        return self::compare($version1, '<=', $version2);
+    }
+
+    /**
+     * Evaluates the expression: $version1 == $version2.
+     *
+     * @param string $version1
+     * @param string $version2
+     *
+     * @return bool
+     */
+    public static function equalTo($version1, $version2)
+    {
+        return self::compare($version1, '==', $version2);
+    }
+
+    /**
+     * Evaluates the expression: $version1 != $version2.
+     *
+     * @param string $version1
+     * @param string $version2
+     *
+     * @return bool
+     */
+    public static function notEqualTo($version1, $version2)
+    {
+        return self::compare($version1, '!=', $version2);
+    }
+
+    /**
+     * Evaluates the expression: $version1 $operator $version2.
+     *
+     * @param string $version1
+     * @param string $operator
+     * @param string $version2
+     *
+     * @return bool
+     */
+    public static function compare($version1, $operator, $version2)
+    {
+        $constraint = new Constraint($operator, $version2);
+
+        return $constraint->matches(new Constraint('==', $version1));
+    }
+}

+ 63 - 0
api/vendor/composer/semver/src/Constraint/AbstractConstraint.php

@@ -0,0 +1,63 @@
+<?php
+
+/*
+ * This file is part of composer/semver.
+ *
+ * (c) Composer <https://github.com/composer>
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Composer\Semver\Constraint;
+
+trigger_error('The ' . __CLASS__ . ' abstract class is deprecated, there is no replacement for it, it will be removed in the next major version.', E_USER_DEPRECATED);
+
+/**
+ * Base constraint class.
+ */
+abstract class AbstractConstraint implements ConstraintInterface
+{
+    /** @var string */
+    protected $prettyString;
+
+    /**
+     * @param ConstraintInterface $provider
+     *
+     * @return bool
+     */
+    public function matches(ConstraintInterface $provider)
+    {
+        if ($provider instanceof $this) {
+            // see note at bottom of this class declaration
+            return $this->matchSpecific($provider);
+        }
+
+        // turn matching around to find a match
+        return $provider->matches($this);
+    }
+
+    /**
+     * @param string $prettyString
+     */
+    public function setPrettyString($prettyString)
+    {
+        $this->prettyString = $prettyString;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPrettyString()
+    {
+        if ($this->prettyString) {
+            return $this->prettyString;
+        }
+
+        return $this->__toString();
+    }
+
+    // implementations must implement a method of this format:
+    // not declared abstract here because type hinting violates parameter coherence (TODO right word?)
+    // public function matchSpecific(<SpecificConstraintType> $provider);
+}

+ 219 - 0
api/vendor/composer/semver/src/Constraint/Constraint.php

@@ -0,0 +1,219 @@
+<?php
+
+/*
+ * This file is part of composer/semver.
+ *
+ * (c) Composer <https://github.com/composer>
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Composer\Semver\Constraint;
+
+/**
+ * Defines a constraint.
+ */
+class Constraint implements ConstraintInterface
+{
+    /* operator integer values */
+    const OP_EQ = 0;
+    const OP_LT = 1;
+    const OP_LE = 2;
+    const OP_GT = 3;
+    const OP_GE = 4;
+    const OP_NE = 5;
+
+    /**
+     * Operator to integer translation table.
+     *
+     * @var array
+     */
+    private static $transOpStr = array(
+        '=' => self::OP_EQ,
+        '==' => self::OP_EQ,
+        '<' => self::OP_LT,
+        '<=' => self::OP_LE,
+        '>' => self::OP_GT,
+        '>=' => self::OP_GE,
+        '<>' => self::OP_NE,
+        '!=' => self::OP_NE,
+    );
+
+    /**
+     * Integer to operator translation table.
+     *
+     * @var array
+     */
+    private static $transOpInt = array(
+        self::OP_EQ => '==',
+        self::OP_LT => '<',
+        self::OP_LE => '<=',
+        self::OP_GT => '>',
+        self::OP_GE => '>=',
+        self::OP_NE => '!=',
+    );
+
+    /** @var string */
+    protected $operator;
+
+    /** @var string */
+    protected $version;
+
+    /** @var string */
+    protected $prettyString;
+
+    /**
+     * @param ConstraintInterface $provider
+     *
+     * @return bool
+     */
+    public function matches(ConstraintInterface $provider)
+    {
+        if ($provider instanceof $this) {
+            return $this->matchSpecific($provider);
+        }
+
+        // turn matching around to find a match
+        return $provider->matches($this);
+    }
+
+    /**
+     * @param string $prettyString
+     */
+    public function setPrettyString($prettyString)
+    {
+        $this->prettyString = $prettyString;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPrettyString()
+    {
+        if ($this->prettyString) {
+            return $this->prettyString;
+        }
+
+        return $this->__toString();
+    }
+
+    /**
+     * Get all supported comparison operators.
+     *
+     * @return array
+     */
+    public static function getSupportedOperators()
+    {
+        return array_keys(self::$transOpStr);
+    }
+
+    /**
+     * Sets operator and version to compare with.
+     *
+     * @param string $operator
+     * @param string $version
+     *
+     * @throws \InvalidArgumentException if invalid operator is given.
+     */
+    public function __construct($operator, $version)
+    {
+        if (!isset(self::$transOpStr[$operator])) {
+            throw new \InvalidArgumentException(sprintf(
+                'Invalid operator "%s" given, expected one of: %s',
+                $operator,
+                implode(', ', self::getSupportedOperators())
+            ));
+        }
+
+        $this->operator = self::$transOpStr[$operator];
+        $this->version = $version;
+    }
+
+    /**
+     * @param string $a
+     * @param string $b
+     * @param string $operator
+     * @param bool $compareBranches
+     *
+     * @throws \InvalidArgumentException if invalid operator is given.
+     *
+     * @return bool
+     */
+    public function versionCompare($a, $b, $operator, $compareBranches = false)
+    {
+        if (!isset(self::$transOpStr[$operator])) {
+            throw new \InvalidArgumentException(sprintf(
+                'Invalid operator "%s" given, expected one of: %s',
+                $operator,
+                implode(', ', self::getSupportedOperators())
+            ));
+        }
+
+        $aIsBranch = 'dev-' === substr($a, 0, 4);
+        $bIsBranch = 'dev-' === substr($b, 0, 4);
+
+        if ($aIsBranch && $bIsBranch) {
+            return $operator === '==' && $a === $b;
+        }
+
+        // when branches are not comparable, we make sure dev branches never match anything
+        if (!$compareBranches && ($aIsBranch || $bIsBranch)) {
+            return false;
+        }
+
+        return version_compare($a, $b, $operator);
+    }
+
+    /**
+     * @param Constraint $provider
+     * @param bool $compareBranches
+     *
+     * @return bool
+     */
+    public function matchSpecific(Constraint $provider, $compareBranches = false)
+    {
+        $noEqualOp = str_replace('=', '', self::$transOpInt[$this->operator]);
+        $providerNoEqualOp = str_replace('=', '', self::$transOpInt[$provider->operator]);
+
+        $isEqualOp = self::OP_EQ === $this->operator;
+        $isNonEqualOp = self::OP_NE === $this->operator;
+        $isProviderEqualOp = self::OP_EQ === $provider->operator;
+        $isProviderNonEqualOp = self::OP_NE === $provider->operator;
+
+        // '!=' operator is match when other operator is not '==' operator or version is not match
+        // these kinds of comparisons always have a solution
+        if ($isNonEqualOp || $isProviderNonEqualOp) {
+            return !$isEqualOp && !$isProviderEqualOp
+                || $this->versionCompare($provider->version, $this->version, '!=', $compareBranches);
+        }
+
+        // an example for the condition is <= 2.0 & < 1.0
+        // these kinds of comparisons always have a solution
+        if ($this->operator !== self::OP_EQ && $noEqualOp === $providerNoEqualOp) {
+            return true;
+        }
+
+        if ($this->versionCompare($provider->version, $this->version, self::$transOpInt[$this->operator], $compareBranches)) {
+            // special case, e.g. require >= 1.0 and provide < 1.0
+            // 1.0 >= 1.0 but 1.0 is outside of the provided interval
+            if ($provider->version === $this->version
+                && self::$transOpInt[$provider->operator] === $providerNoEqualOp
+                && self::$transOpInt[$this->operator] !== $noEqualOp) {
+                return false;
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * @return string
+     */
+    public function __toString()
+    {
+        return self::$transOpInt[$this->operator] . ' ' . $this->version;
+    }
+}

+ 32 - 0
api/vendor/composer/semver/src/Constraint/ConstraintInterface.php

@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of composer/semver.
+ *
+ * (c) Composer <https://github.com/composer>
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Composer\Semver\Constraint;
+
+interface ConstraintInterface
+{
+    /**
+     * @param ConstraintInterface $provider
+     *
+     * @return bool
+     */
+    public function matches(ConstraintInterface $provider);
+
+    /**
+     * @return string
+     */
+    public function getPrettyString();
+
+    /**
+     * @return string
+     */
+    public function __toString();
+}

+ 59 - 0
api/vendor/composer/semver/src/Constraint/EmptyConstraint.php

@@ -0,0 +1,59 @@
+<?php
+
+/*
+ * This file is part of composer/semver.
+ *
+ * (c) Composer <https://github.com/composer>
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Composer\Semver\Constraint;
+
+/**
+ * Defines the absence of a constraint.
+ */
+class EmptyConstraint implements ConstraintInterface
+{
+    /** @var string */
+    protected $prettyString;
+
+    /**
+     * @param ConstraintInterface $provider
+     *
+     * @return bool
+     */
+    public function matches(ConstraintInterface $provider)
+    {
+        return true;
+    }
+
+    /**
+     * @param $prettyString
+     */
+    public function setPrettyString($prettyString)
+    {
+        $this->prettyString = $prettyString;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPrettyString()
+    {
+        if ($this->prettyString) {
+            return $this->prettyString;
+        }
+
+        return $this->__toString();
+    }
+
+    /**
+     * @return string
+     */
+    public function __toString()
+    {
+        return '[]';
+    }
+}

+ 120 - 0
api/vendor/composer/semver/src/Constraint/MultiConstraint.php

@@ -0,0 +1,120 @@
+<?php
+
+/*
+ * This file is part of composer/semver.
+ *
+ * (c) Composer <https://github.com/composer>
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Composer\Semver\Constraint;
+
+/**
+ * Defines a conjunctive or disjunctive set of constraints.
+ */
+class MultiConstraint implements ConstraintInterface
+{
+    /** @var ConstraintInterface[] */
+    protected $constraints;
+
+    /** @var string */
+    protected $prettyString;
+
+    /** @var bool */
+    protected $conjunctive;
+
+    /**
+     * @param ConstraintInterface[] $constraints A set of constraints
+     * @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive
+     */
+    public function __construct(array $constraints, $conjunctive = true)
+    {
+        $this->constraints = $constraints;
+        $this->conjunctive = $conjunctive;
+    }
+
+    /**
+     * @return ConstraintInterface[]
+     */
+    public function getConstraints()
+    {
+        return $this->constraints;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isConjunctive()
+    {
+        return $this->conjunctive;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isDisjunctive()
+    {
+        return !$this->conjunctive;
+    }
+
+    /**
+     * @param ConstraintInterface $provider
+     *
+     * @return bool
+     */
+    public function matches(ConstraintInterface $provider)
+    {
+        if (false === $this->conjunctive) {
+            foreach ($this->constraints as $constraint) {
+                if ($constraint->matches($provider)) {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        foreach ($this->constraints as $constraint) {
+            if (!$constraint->matches($provider)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * @param string $prettyString
+     */
+    public function setPrettyString($prettyString)
+    {
+        $this->prettyString = $prettyString;
+    }
+
+    /**
+     * @return string
+     */
+    public function getPrettyString()
+    {
+        if ($this->prettyString) {
+            return $this->prettyString;
+        }
+
+        return $this->__toString();
+    }
+
+    /**
+     * @return string
+     */
+    public function __toString()
+    {
+        $constraints = array();
+        foreach ($this->constraints as $constraint) {
+            $constraints[] = (string) $constraint;
+        }
+
+        return '[' . implode($this->conjunctive ? ' ' : ' || ', $constraints) . ']';
+    }
+}

+ 127 - 0
api/vendor/composer/semver/src/Semver.php

@@ -0,0 +1,127 @@
+<?php
+
+/*
+ * This file is part of composer/semver.
+ *
+ * (c) Composer <https://github.com/composer>
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Composer\Semver;
+
+use Composer\Semver\Constraint\Constraint;
+
+class Semver
+{
+    const SORT_ASC = 1;
+    const SORT_DESC = -1;
+
+    /** @var VersionParser */
+    private static $versionParser;
+
+    /**
+     * Determine if given version satisfies given constraints.
+     *
+     * @param string $version
+     * @param string $constraints
+     *
+     * @return bool
+     */
+    public static function satisfies($version, $constraints)
+    {
+        if (null === self::$versionParser) {
+            self::$versionParser = new VersionParser();
+        }
+
+        $versionParser = self::$versionParser;
+        $provider = new Constraint('==', $versionParser->normalize($version));
+        $constraints = $versionParser->parseConstraints($constraints);
+
+        return $constraints->matches($provider);
+    }
+
+    /**
+     * Return all versions that satisfy given constraints.
+     *
+     * @param array $versions
+     * @param string $constraints
+     *
+     * @return array
+     */
+    public static function satisfiedBy(array $versions, $constraints)
+    {
+        $versions = array_filter($versions, function ($version) use ($constraints) {
+            return Semver::satisfies($version, $constraints);
+        });
+
+        return array_values($versions);
+    }
+
+    /**
+     * Sort given array of versions.
+     *
+     * @param array $versions
+     *
+     * @return array
+     */
+    public static function sort(array $versions)
+    {
+        return self::usort($versions, self::SORT_ASC);
+    }
+
+    /**
+     * Sort given array of versions in reverse.
+     *
+     * @param array $versions
+     *
+     * @return array
+     */
+    public static function rsort(array $versions)
+    {
+        return self::usort($versions, self::SORT_DESC);
+    }
+
+    /**
+     * @param array $versions
+     * @param int $direction
+     *
+     * @return array
+     */
+    private static function usort(array $versions, $direction)
+    {
+        if (null === self::$versionParser) {
+            self::$versionParser = new VersionParser();
+        }
+
+        $versionParser = self::$versionParser;
+        $normalized = array();
+
+        // Normalize outside of usort() scope for minor performance increase.
+        // Creates an array of arrays: [[normalized, key], ...]
+        foreach ($versions as $key => $version) {
+            $normalized[] = array($versionParser->normalize($version), $key);
+        }
+
+        usort($normalized, function (array $left, array $right) use ($direction) {
+            if ($left[0] === $right[0]) {
+                return 0;
+            }
+
+            if (Comparator::lessThan($left[0], $right[0])) {
+                return -$direction;
+            }
+
+            return $direction;
+        });
+
+        // Recreate input array, using the original indexes which are now in sorted order.
+        $sorted = array();
+        foreach ($normalized as $item) {
+            $sorted[] = $versions[$item[1]];
+        }
+
+        return $sorted;
+    }
+}

+ 548 - 0
api/vendor/composer/semver/src/VersionParser.php

@@ -0,0 +1,548 @@
+<?php
+
+/*
+ * This file is part of composer/semver.
+ *
+ * (c) Composer <https://github.com/composer>
+ *
+ * For the full copyright and license information, please view
+ * the LICENSE file that was distributed with this source code.
+ */
+
+namespace Composer\Semver;
+
+use Composer\Semver\Constraint\ConstraintInterface;
+use Composer\Semver\Constraint\EmptyConstraint;
+use Composer\Semver\Constraint\MultiConstraint;
+use Composer\Semver\Constraint\Constraint;
+
+/**
+ * Version parser.
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class VersionParser
+{
+    /**
+     * Regex to match pre-release data (sort of).
+     *
+     * Due to backwards compatibility:
+     *   - Instead of enforcing hyphen, an underscore, dot or nothing at all are also accepted.
+     *   - Only stabilities as recognized by Composer are allowed to precede a numerical identifier.
+     *   - Numerical-only pre-release identifiers are not supported, see tests.
+     *
+     *                        |--------------|
+     * [major].[minor].[patch] -[pre-release] +[build-metadata]
+     *
+     * @var string
+     */
+    private static $modifierRegex = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*+)?)?([.-]?dev)?';
+
+    /** @var array */
+    private static $stabilities = array('stable', 'RC', 'beta', 'alpha', 'dev');
+
+    /**
+     * Returns the stability of a version.
+     *
+     * @param string $version
+     *
+     * @return string
+     */
+    public static function parseStability($version)
+    {
+        $version = preg_replace('{#.+$}i', '', $version);
+
+        if ('dev-' === substr($version, 0, 4) || '-dev' === substr($version, -4)) {
+            return 'dev';
+        }
+
+        preg_match('{' . self::$modifierRegex . '(?:\+.*)?$}i', strtolower($version), $match);
+        if (!empty($match[3])) {
+            return 'dev';
+        }
+
+        if (!empty($match[1])) {
+            if ('beta' === $match[1] || 'b' === $match[1]) {
+                return 'beta';
+            }
+            if ('alpha' === $match[1] || 'a' === $match[1]) {
+                return 'alpha';
+            }
+            if ('rc' === $match[1]) {
+                return 'RC';
+            }
+        }
+
+        return 'stable';
+    }
+
+    /**
+     * @param string $stability
+     *
+     * @return string
+     */
+    public static function normalizeStability($stability)
+    {
+        $stability = strtolower($stability);
+
+        return $stability === 'rc' ? 'RC' : $stability;
+    }
+
+    /**
+     * Normalizes a version string to be able to perform comparisons on it.
+     *
+     * @param string $version
+     * @param string $fullVersion optional complete version string to give more context
+     *
+     * @throws \UnexpectedValueException
+     *
+     * @return string
+     */
+    public function normalize($version, $fullVersion = null)
+    {
+        $version = trim($version);
+        if (null === $fullVersion) {
+            $fullVersion = $version;
+        }
+
+        // strip off aliasing
+        if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $version, $match)) {
+            $version = $match[1];
+        }
+
+        // match master-like branches
+        if (preg_match('{^(?:dev-)?(?:master|trunk|default)$}i', $version)) {
+            return '9999999-dev';
+        }
+
+        // if requirement is branch-like, use full name
+        if ('dev-' === strtolower(substr($version, 0, 4))) {
+            return 'dev-' . substr($version, 4);
+        }
+
+        // strip off build metadata
+        if (preg_match('{^([^,\s+]++)\+[^\s]++$}', $version, $match)) {
+            $version = $match[1];
+        }
+
+        // match classical versioning
+        if (preg_match('{^v?(\d{1,5})(\.\d++)?(\.\d++)?(\.\d++)?' . self::$modifierRegex . '$}i', $version, $matches)) {
+            $version = $matches[1]
+                . (!empty($matches[2]) ? $matches[2] : '.0')
+                . (!empty($matches[3]) ? $matches[3] : '.0')
+                . (!empty($matches[4]) ? $matches[4] : '.0');
+            $index = 5;
+        // match date(time) based versioning
+        } elseif (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3})?)' . self::$modifierRegex . '$}i', $version, $matches)) {
+            $version = preg_replace('{\D}', '.', $matches[1]);
+            $index = 2;
+        }
+
+        // add version modifiers if a version was matched
+        if (isset($index)) {
+            if (!empty($matches[$index])) {
+                if ('stable' === $matches[$index]) {
+                    return $version;
+                }
+                $version .= '-' . $this->expandStability($matches[$index]) . (!empty($matches[$index + 1]) ? ltrim($matches[$index + 1], '.-') : '');
+            }
+
+            if (!empty($matches[$index + 2])) {
+                $version .= '-dev';
+            }
+
+            return $version;
+        }
+
+        // match dev branches
+        if (preg_match('{(.*?)[.-]?dev$}i', $version, $match)) {
+            try {
+                return $this->normalizeBranch($match[1]);
+            } catch (\Exception $e) {
+            }
+        }
+
+        $extraMessage = '';
+        if (preg_match('{ +as +' . preg_quote($version) . '$}', $fullVersion)) {
+            $extraMessage = ' in "' . $fullVersion . '", the alias must be an exact version';
+        } elseif (preg_match('{^' . preg_quote($version) . ' +as +}', $fullVersion)) {
+            $extraMessage = ' in "' . $fullVersion . '", the alias source must be an exact version, if it is a branch name you should prefix it with dev-';
+        }
+
+        throw new \UnexpectedValueException('Invalid version string "' . $version . '"' . $extraMessage);
+    }
+
+    /**
+     * Extract numeric prefix from alias, if it is in numeric format, suitable for version comparison.
+     *
+     * @param string $branch Branch name (e.g. 2.1.x-dev)
+     *
+     * @return string|false Numeric prefix if present (e.g. 2.1.) or false
+     */
+    public function parseNumericAliasPrefix($branch)
+    {
+        if (preg_match('{^(?P<version>(\d++\\.)*\d++)(?:\.x)?-dev$}i', $branch, $matches)) {
+            return $matches['version'] . '.';
+        }
+
+        return false;
+    }
+
+    /**
+     * Normalizes a branch name to be able to perform comparisons on it.
+     *
+     * @param string $name
+     *
+     * @return string
+     */
+    public function normalizeBranch($name)
+    {
+        $name = trim($name);
+
+        if (in_array($name, array('master', 'trunk', 'default'))) {
+            return $this->normalize($name);
+        }
+
+        if (preg_match('{^v?(\d++)(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?$}i', $name, $matches)) {
+            $version = '';
+            for ($i = 1; $i < 5; ++$i) {
+                $version .= isset($matches[$i]) ? str_replace(array('*', 'X'), 'x', $matches[$i]) : '.x';
+            }
+
+            return str_replace('x', '9999999', $version) . '-dev';
+        }
+
+        return 'dev-' . $name;
+    }
+
+    /**
+     * Parses a constraint string into MultiConstraint and/or Constraint objects.
+     *
+     * @param string $constraints
+     *
+     * @return ConstraintInterface
+     */
+    public function parseConstraints($constraints)
+    {
+        $prettyConstraint = $constraints;
+
+        if (preg_match('{^([^,\s]*?)@(' . implode('|', self::$stabilities) . ')$}i', $constraints, $match)) {
+            $constraints = empty($match[1]) ? '*' : $match[1];
+        }
+
+        if (preg_match('{^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#.+$}i', $constraints, $match)) {
+            $constraints = $match[1];
+        }
+
+        $orConstraints = preg_split('{\s*\|\|?\s*}', trim($constraints));
+        $orGroups = array();
+        foreach ($orConstraints as $constraints) {
+            $andConstraints = preg_split('{(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)}', $constraints);
+            if (count($andConstraints) > 1) {
+                $constraintObjects = array();
+                foreach ($andConstraints as $constraint) {
+                    foreach ($this->parseConstraint($constraint) as $parsedConstraint) {
+                        $constraintObjects[] = $parsedConstraint;
+                    }
+                }
+            } else {
+                $constraintObjects = $this->parseConstraint($andConstraints[0]);
+            }
+
+            if (1 === count($constraintObjects)) {
+                $constraint = $constraintObjects[0];
+            } else {
+                $constraint = new MultiConstraint($constraintObjects);
+            }
+
+            $orGroups[] = $constraint;
+        }
+
+        if (1 === count($orGroups)) {
+            $constraint = $orGroups[0];
+        } elseif (2 === count($orGroups)
+            // parse the two OR groups and if they are contiguous we collapse
+            // them into one constraint
+            && $orGroups[0] instanceof MultiConstraint
+            && $orGroups[1] instanceof MultiConstraint
+            && 2 === count($orGroups[0]->getConstraints())
+            && 2 === count($orGroups[1]->getConstraints())
+            && ($a = (string) $orGroups[0])
+            && substr($a, 0, 3) === '[>=' && (false !== ($posA = strpos($a, '<', 4)))
+            && ($b = (string) $orGroups[1])
+            && substr($b, 0, 3) === '[>=' && (false !== ($posB = strpos($b, '<', 4)))
+            && substr($a, $posA + 2, -1) === substr($b, 4, $posB - 5)
+        ) {
+            $constraint = new MultiConstraint(array(
+                new Constraint('>=', substr($a, 4, $posA - 5)),
+                new Constraint('<', substr($b, $posB + 2, -1)),
+            ));
+        } else {
+            $constraint = new MultiConstraint($orGroups, false);
+        }
+
+        $constraint->setPrettyString($prettyConstraint);
+
+        return $constraint;
+    }
+
+    /**
+     * @param string $constraint
+     *
+     * @throws \UnexpectedValueException
+     *
+     * @return array
+     */
+    private function parseConstraint($constraint)
+    {
+        if (preg_match('{^([^,\s]+?)@(' . implode('|', self::$stabilities) . ')$}i', $constraint, $match)) {
+            $constraint = $match[1];
+            if ($match[2] !== 'stable') {
+                $stabilityModifier = $match[2];
+            }
+        }
+
+        if (preg_match('{^v?[xX*](\.[xX*])*$}i', $constraint)) {
+            return array(new EmptyConstraint());
+        }
+
+        $versionRegex = 'v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.(\d++))?' . self::$modifierRegex . '(?:\+[^\s]+)?';
+
+        // Tilde Range
+        //
+        // Like wildcard constraints, unsuffixed tilde constraints say that they must be greater than the previous
+        // version, to ensure that unstable instances of the current version are allowed. However, if a stability
+        // suffix is added to the constraint, then a >= match on the current version is used instead.
+        if (preg_match('{^~>?' . $versionRegex . '$}i', $constraint, $matches)) {
+            if (substr($constraint, 0, 2) === '~>') {
+                throw new \UnexpectedValueException(
+                    'Could not parse version constraint ' . $constraint . ': ' .
+                    'Invalid operator "~>", you probably meant to use the "~" operator'
+                );
+            }
+
+            // Work out which position in the version we are operating at
+            if (isset($matches[4]) && '' !== $matches[4]) {
+                $position = 4;
+            } elseif (isset($matches[3]) && '' !== $matches[3]) {
+                $position = 3;
+            } elseif (isset($matches[2]) && '' !== $matches[2]) {
+                $position = 2;
+            } else {
+                $position = 1;
+            }
+
+            // Calculate the stability suffix
+            $stabilitySuffix = '';
+            if (!empty($matches[5])) {
+                $stabilitySuffix .= '-' . $this->expandStability($matches[5]) . (!empty($matches[6]) ? $matches[6] : '');
+            }
+
+            if (!empty($matches[7])) {
+                $stabilitySuffix .= '-dev';
+            }
+
+            if (!$stabilitySuffix) {
+                $stabilitySuffix = '-dev';
+            }
+
+            $lowVersion = $this->manipulateVersionString($matches, $position, 0) . $stabilitySuffix;
+            $lowerBound = new Constraint('>=', $lowVersion);
+
+            // For upper bound, we increment the position of one more significance,
+            // but highPosition = 0 would be illegal
+            $highPosition = max(1, $position - 1);
+            $highVersion = $this->manipulateVersionString($matches, $highPosition, 1) . '-dev';
+            $upperBound = new Constraint('<', $highVersion);
+
+            return array(
+                $lowerBound,
+                $upperBound,
+            );
+        }
+
+        // Caret Range
+        //
+        // Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch] tuple.
+        // In other words, this allows patch and minor updates for versions 1.0.0 and above, patch updates for
+        // versions 0.X >=0.1.0, and no updates for versions 0.0.X
+        if (preg_match('{^\^' . $versionRegex . '($)}i', $constraint, $matches)) {
+            // Work out which position in the version we are operating at
+            if ('0' !== $matches[1] || '' === $matches[2]) {
+                $position = 1;
+            } elseif ('0' !== $matches[2] || '' === $matches[3]) {
+                $position = 2;
+            } else {
+                $position = 3;
+            }
+
+            // Calculate the stability suffix
+            $stabilitySuffix = '';
+            if (empty($matches[5]) && empty($matches[7])) {
+                $stabilitySuffix .= '-dev';
+            }
+
+            $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1));
+            $lowerBound = new Constraint('>=', $lowVersion);
+
+            // For upper bound, we increment the position of one more significance,
+            // but highPosition = 0 would be illegal
+            $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev';
+            $upperBound = new Constraint('<', $highVersion);
+
+            return array(
+                $lowerBound,
+                $upperBound,
+            );
+        }
+
+        // X Range
+        //
+        // Any of X, x, or * may be used to "stand in" for one of the numeric values in the [major, minor, patch] tuple.
+        // A partial version range is treated as an X-Range, so the special character is in fact optional.
+        if (preg_match('{^v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.[xX*])++$}', $constraint, $matches)) {
+            if (isset($matches[3]) && '' !== $matches[3]) {
+                $position = 3;
+            } elseif (isset($matches[2]) && '' !== $matches[2]) {
+                $position = 2;
+            } else {
+                $position = 1;
+            }
+
+            $lowVersion = $this->manipulateVersionString($matches, $position) . '-dev';
+            $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev';
+
+            if ($lowVersion === '0.0.0.0-dev') {
+                return array(new Constraint('<', $highVersion));
+            }
+
+            return array(
+                new Constraint('>=', $lowVersion),
+                new Constraint('<', $highVersion),
+            );
+        }
+
+        // Hyphen Range
+        //
+        // Specifies an inclusive set. If a partial version is provided as the first version in the inclusive range,
+        // then the missing pieces are replaced with zeroes. If a partial version is provided as the second version in
+        // the inclusive range, then all versions that start with the supplied parts of the tuple are accepted, but
+        // nothing that would be greater than the provided tuple parts.
+        if (preg_match('{^(?P<from>' . $versionRegex . ') +- +(?P<to>' . $versionRegex . ')($)}i', $constraint, $matches)) {
+            // Calculate the stability suffix
+            $lowStabilitySuffix = '';
+            if (empty($matches[6]) && empty($matches[8])) {
+                $lowStabilitySuffix = '-dev';
+            }
+
+            $lowVersion = $this->normalize($matches['from']);
+            $lowerBound = new Constraint('>=', $lowVersion . $lowStabilitySuffix);
+
+            $empty = function ($x) {
+                return ($x === 0 || $x === '0') ? false : empty($x);
+            };
+
+            if ((!$empty($matches[11]) && !$empty($matches[12])) || !empty($matches[14]) || !empty($matches[16])) {
+                $highVersion = $this->normalize($matches['to']);
+                $upperBound = new Constraint('<=', $highVersion);
+            } else {
+                $highMatch = array('', $matches[10], $matches[11], $matches[12], $matches[13]);
+                $highVersion = $this->manipulateVersionString($highMatch, $empty($matches[11]) ? 1 : 2, 1) . '-dev';
+                $upperBound = new Constraint('<', $highVersion);
+            }
+
+            return array(
+                $lowerBound,
+                $upperBound,
+            );
+        }
+
+        // Basic Comparators
+        if (preg_match('{^(<>|!=|>=?|<=?|==?)?\s*(.*)}', $constraint, $matches)) {
+            try {
+                $version = $this->normalize($matches[2]);
+
+                if (!empty($stabilityModifier) && $this->parseStability($version) === 'stable') {
+                    $version .= '-' . $stabilityModifier;
+                } elseif ('<' === $matches[1] || '>=' === $matches[1]) {
+                    if (!preg_match('/-' . self::$modifierRegex . '$/', strtolower($matches[2]))) {
+                        if (substr($matches[2], 0, 4) !== 'dev-') {
+                            $version .= '-dev';
+                        }
+                    }
+                }
+
+                return array(new Constraint($matches[1] ?: '=', $version));
+            } catch (\Exception $e) {
+            }
+        }
+
+        $message = 'Could not parse version constraint ' . $constraint;
+        if (isset($e)) {
+            $message .= ': ' . $e->getMessage();
+        }
+
+        throw new \UnexpectedValueException($message);
+    }
+
+    /**
+     * Increment, decrement, or simply pad a version number.
+     *
+     * Support function for {@link parseConstraint()}
+     *
+     * @param array $matches Array with version parts in array indexes 1,2,3,4
+     * @param int $position 1,2,3,4 - which segment of the version to increment/decrement
+     * @param int $increment
+     * @param string $pad The string to pad version parts after $position
+     *
+     * @return string The new version
+     */
+    private function manipulateVersionString($matches, $position, $increment = 0, $pad = '0')
+    {
+        for ($i = 4; $i > 0; --$i) {
+            if ($i > $position) {
+                $matches[$i] = $pad;
+            } elseif ($i === $position && $increment) {
+                $matches[$i] += $increment;
+                // If $matches[$i] was 0, carry the decrement
+                if ($matches[$i] < 0) {
+                    $matches[$i] = $pad;
+                    --$position;
+
+                    // Return null on a carry overflow
+                    if ($i === 1) {
+                        return;
+                    }
+                }
+            }
+        }
+
+        return $matches[1] . '.' . $matches[2] . '.' . $matches[3] . '.' . $matches[4];
+    }
+
+    /**
+     * Expand shorthand stability string to long version.
+     *
+     * @param string $stability
+     *
+     * @return string
+     */
+    private function expandStability($stability)
+    {
+        $stability = strtolower($stability);
+
+        switch ($stability) {
+            case 'a':
+                return 'alpha';
+            case 'b':
+                return 'beta';
+            case 'p':
+            case 'pl':
+                return 'patch';
+            case 'rc':
+                return 'RC';
+            default:
+                return $stability;
+        }
+    }
+}

+ 32 - 0
api/vendor/dibi/dibi/composer.json

@@ -0,0 +1,32 @@
+{
+	"name": "dibi/dibi",
+	"description": "Dibi is Database Abstraction Library for PHP",
+	"keywords": ["database", "dbal", "mysql", "postgresql", "sqlite", "mssql", "sqlsrv", "oracle", "access", "pdo", "odbc"],
+	"homepage": "https://dibiphp.com",
+	"license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"],
+	"authors": [
+		{
+			"name": "David Grudl",
+			"homepage": "https://davidgrudl.com"
+		}
+	],
+	"require": {
+		"php": ">=5.4.4"
+	},
+	"require-dev": {
+		"tracy/tracy": "~2.2",
+		"nette/tester": "~1.7"
+	},
+	"replace": {
+		"dg/dibi": "*"
+	},
+	"autoload": {
+		"classmap": ["src/"],
+		"files": ["src/loader.php"]
+	},
+	"extra": {
+		"branch-alias": {
+			"dev-master": "3.2-dev"
+		}
+	}
+}

+ 31 - 0
api/vendor/dibi/dibi/contributing.md

@@ -0,0 +1,31 @@
+How to contribute & use the issue tracker
+=========================================
+
+Dibi welcomes your contributions. There are several ways to help out:
+
+* Create an issue on GitHub, if you have found a bug
+* Write test cases for open bug issues
+* Write fixes for open bug/feature issues, preferably with test cases included
+* Contribute to the documentation
+
+Issues
+------
+
+Please **do not use the issue tracker to ask questions**. We will be happy to help you
+on [Dibi forum](https://forum.dibiphp.com).
+
+A good bug report shouldn't leave others needing to chase you up for more
+information. Please try to be as detailed as possible in your report.
+
+**Feature requests** are welcome. But take a moment to find out whether your idea
+fits with the scope and aims of the project. It's up to *you* to make a strong
+case to convince the project's developers of the merits of this feature.
+
+Contributing
+------------
+
+The best way to propose a feature is to discuss your ideas on [Dibi forum](https://forum.dibiphp.com) before implementing them.
+
+Please do not fix whitespace, format code, or make a purely cosmetic patch.
+
+Thanks! :heart:

+ 163 - 0
api/vendor/dibi/dibi/examples/connecting-to-databases.php

@@ -0,0 +1,163 @@
+<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
+
+<h1>Connecting to Databases | Dibi</h1>
+
+<?php
+
+if (@!include __DIR__ . '/../vendor/autoload.php') {
+	die('Install packages using `composer install`');
+}
+
+
+// connects to SQlite using Dibi class
+echo '<p>Connecting to Sqlite: ';
+try {
+	dibi::connect([
+		'driver' => 'sqlite3',
+		'database' => 'data/sample.s3db',
+	]);
+	echo 'OK';
+} catch (Dibi\Exception $e) {
+	echo get_class($e), ': ', $e->getMessage(), "\n";
+}
+echo "</p>\n";
+
+
+// connects to SQlite using Dibi\Connection object
+echo '<p>Connecting to Sqlite: ';
+try {
+	$connection = new Dibi\Connection([
+		'driver' => 'sqlite3',
+		'database' => 'data/sample.s3db',
+	]);
+	echo 'OK';
+} catch (Dibi\Exception $e) {
+	echo get_class($e), ': ', $e->getMessage(), "\n";
+}
+echo "</p>\n";
+
+
+// connects to MySQL using DSN
+echo '<p>Connecting to MySQL: ';
+try {
+	dibi::connect('driver=mysql&host=localhost&username=root&password=xxx&database=test&charset=cp1250');
+	echo 'OK';
+} catch (Dibi\Exception $e) {
+	echo get_class($e), ': ', $e->getMessage(), "\n";
+}
+echo "</p>\n";
+
+
+// connects to MySQLi using array
+echo '<p>Connecting to MySQLi: ';
+try {
+	dibi::connect([
+		'driver' => 'mysqli',
+		'host' => 'localhost',
+		'username' => 'root',
+		'password' => 'xxx',
+		'database' => 'dibi',
+		'options' => [
+			MYSQLI_OPT_CONNECT_TIMEOUT => 30,
+		],
+		'flags' => MYSQLI_CLIENT_COMPRESS,
+	]);
+	echo 'OK';
+} catch (Dibi\Exception $e) {
+	echo get_class($e), ': ', $e->getMessage(), "\n";
+}
+echo "</p>\n";
+
+
+// connects to ODBC
+echo '<p>Connecting to ODBC: ';
+try {
+	dibi::connect([
+		'driver' => 'odbc',
+		'username' => 'root',
+		'password' => '***',
+		'dsn' => 'Driver={Microsoft Access Driver (*.mdb)};Dbq=' . __DIR__ . '/data/sample.mdb',
+	]);
+	echo 'OK';
+} catch (Dibi\Exception $e) {
+	echo get_class($e), ': ', $e->getMessage(), "\n";
+}
+echo "</p>\n";
+
+
+// connects to PostgreSql
+echo '<p>Connecting to PostgreSql: ';
+try {
+	dibi::connect([
+		'driver' => 'postgre',
+		'string' => 'host=localhost port=5432 dbname=mary',
+		'persistent' => true,
+	]);
+	echo 'OK';
+} catch (Dibi\Exception $e) {
+	echo get_class($e), ': ', $e->getMessage(), "\n";
+}
+echo "</p>\n";
+
+
+// connects to PDO
+echo '<p>Connecting to Sqlite via PDO: ';
+try {
+	dibi::connect([
+		'driver' => 'pdo',
+		'dsn' => 'sqlite::memory:',
+	]);
+	echo 'OK';
+} catch (Dibi\Exception $e) {
+	echo get_class($e), ': ', $e->getMessage(), "\n";
+}
+echo "</p>\n";
+
+
+// connects to MS SQL
+echo '<p>Connecting to MS SQL: ';
+try {
+	dibi::connect([
+		'driver' => 'mssql',
+		'host' => 'localhost',
+		'username' => 'root',
+		'password' => 'xxx',
+	]);
+	echo 'OK';
+} catch (Dibi\Exception $e) {
+	echo get_class($e), ': ', $e->getMessage(), "\n";
+}
+echo "</p>\n";
+
+
+// connects to SQLSRV
+echo '<p>Connecting to Microsoft SQL Server: ';
+try {
+	dibi::connect([
+		'driver' => 'sqlsrv',
+		'host' => '(local)',
+		'username' => 'Administrator',
+		'password' => 'xxx',
+		'database' => 'main',
+	]);
+	echo 'OK';
+} catch (Dibi\Exception $e) {
+	echo get_class($e), ': ', $e->getMessage(), "\n";
+}
+echo "</p>\n";
+
+
+// connects to Oracle
+echo '<p>Connecting to Oracle: ';
+try {
+	dibi::connect([
+		'driver' => 'oracle',
+		'username' => 'root',
+		'password' => 'xxx',
+		'database' => 'db',
+	]);
+	echo 'OK';
+} catch (Dibi\Exception $e) {
+	echo get_class($e), ': ', $e->getMessage(), "\n";
+}
+echo "</p>\n";

BIN
api/vendor/dibi/dibi/examples/data/arrow.png


BIN
api/vendor/dibi/dibi/examples/data/dibi-powered.gif


BIN
api/vendor/dibi/dibi/examples/data/sample.dump.sql.gz


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff