Browse Source

Merge pull request #1735 from causefx/v2-develop

V2 develop
causefx 4 years ago
parent
commit
a9a22244fa
100 changed files with 8762 additions and 1438 deletions
  1. 1 0
      .gitignore
  2. 35 0
      api/classes/logger.class.php
  3. 226 1149
      api/classes/organizr.class.php
  4. 3 1
      api/composer.json
  5. 285 1
      api/composer.lock
  6. 16 3
      api/config/default.php
  7. 0 1
      api/functions/auth-functions.php
  8. 4 2
      api/functions/backup-functions.php
  9. 390 8
      api/functions/log-functions.php
  10. 118 10
      api/functions/option-functions.php
  11. 19 0
      api/functions/organizr-functions.php
  12. 61 2
      api/functions/sso-functions.php
  13. 247 0
      api/functions/upgrade-functions.php
  14. 6 4
      api/homepage/plex.php
  15. 14 5
      api/homepage/sabnzbd.php
  16. 62 3
      api/homepage/tautulli.php
  17. 39 37
      api/homepage/utorrent.php
  18. 1 1
      api/pages/error.php
  19. 20 0
      api/pages/settings-plugins-disabled.php
  20. 25 0
      api/pages/settings-plugins-enabled.php
  21. 31 0
      api/pages/settings-plugins-settings.php
  22. 84 157
      api/pages/settings-settings-logs.php
  23. 47 4
      api/pages/settings.php
  24. 2 2
      api/plugins/bookmark/plugin.php
  25. 2 2
      api/plugins/chat/plugin.php
  26. 1 1
      api/plugins/healthChecks/plugin.php
  27. 4 4
      api/plugins/invites/api.php
  28. 5 1
      api/plugins/invites/config.php
  29. 9 4
      api/plugins/invites/main.js
  30. 133 21
      api/plugins/invites/plugin.php
  31. 2 2
      api/plugins/php-mailer/main.js
  32. 1 1
      api/plugins/php-mailer/plugin.php
  33. 2 2
      api/plugins/speedTest/plugin.php
  34. 9 4
      api/v2/routes/log.php
  35. 33 0
      api/v2/routes/plugins.php
  36. 10 0
      api/v2/routes/settings.php
  37. 90 0
      api/vendor/bcremer/line-reader/.github/workflows/build.yaml
  38. 6 0
      api/vendor/bcremer/line-reader/.gitignore
  39. 30 0
      api/vendor/bcremer/line-reader/CHANGELOG.md
  40. 20 0
      api/vendor/bcremer/line-reader/LICENSE
  41. 134 0
      api/vendor/bcremer/line-reader/README.md
  42. 32 0
      api/vendor/bcremer/line-reader/composer.json
  43. 11 0
      api/vendor/bcremer/line-reader/infection.json.dist
  44. 24 0
      api/vendor/bcremer/line-reader/phpunit.xml.dist
  45. 106 0
      api/vendor/bcremer/line-reader/src/LineReader.php
  46. 178 0
      api/vendor/bcremer/line-reader/tests/LineReaderTest.php
  47. 52 2
      api/vendor/composer/InstalledVersions.php
  48. 2 1
      api/vendor/composer/autoload_files.php
  49. 4 0
      api/vendor/composer/autoload_psr4.php
  50. 28 1
      api/vendor/composer/autoload_static.php
  51. 296 0
      api/vendor/composer/installed.json
  52. 52 2
      api/vendor/composer/installed.php
  53. 423 0
      api/vendor/monolog/monolog/CHANGELOG.md
  54. 19 0
      api/vendor/monolog/monolog/LICENSE
  55. 94 0
      api/vendor/monolog/monolog/README.md
  56. 58 0
      api/vendor/monolog/monolog/composer.json
  57. 16 0
      api/vendor/monolog/monolog/phpstan.neon.dist
  58. 239 0
      api/vendor/monolog/monolog/src/Monolog/ErrorHandler.php
  59. 78 0
      api/vendor/monolog/monolog/src/Monolog/Formatter/ChromePHPFormatter.php
  60. 89 0
      api/vendor/monolog/monolog/src/Monolog/Formatter/ElasticaFormatter.php
  61. 116 0
      api/vendor/monolog/monolog/src/Monolog/Formatter/FlowdockFormatter.php
  62. 88 0
      api/vendor/monolog/monolog/src/Monolog/Formatter/FluentdFormatter.php
  63. 36 0
      api/vendor/monolog/monolog/src/Monolog/Formatter/FormatterInterface.php
  64. 138 0
      api/vendor/monolog/monolog/src/Monolog/Formatter/GelfMessageFormatter.php
  65. 142 0
      api/vendor/monolog/monolog/src/Monolog/Formatter/HtmlFormatter.php
  66. 212 0
      api/vendor/monolog/monolog/src/Monolog/Formatter/JsonFormatter.php
  67. 181 0
      api/vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php
  68. 47 0
      api/vendor/monolog/monolog/src/Monolog/Formatter/LogglyFormatter.php
  69. 166 0
      api/vendor/monolog/monolog/src/Monolog/Formatter/LogstashFormatter.php
  70. 107 0
      api/vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php
  71. 180 0
      api/vendor/monolog/monolog/src/Monolog/Formatter/NormalizerFormatter.php
  72. 48 0
      api/vendor/monolog/monolog/src/Monolog/Formatter/ScalarFormatter.php
  73. 113 0
      api/vendor/monolog/monolog/src/Monolog/Formatter/WildfireFormatter.php
  74. 196 0
      api/vendor/monolog/monolog/src/Monolog/Handler/AbstractHandler.php
  75. 68 0
      api/vendor/monolog/monolog/src/Monolog/Handler/AbstractProcessingHandler.php
  76. 101 0
      api/vendor/monolog/monolog/src/Monolog/Handler/AbstractSyslogHandler.php
  77. 148 0
      api/vendor/monolog/monolog/src/Monolog/Handler/AmqpHandler.php
  78. 241 0
      api/vendor/monolog/monolog/src/Monolog/Handler/BrowserConsoleHandler.php
  79. 148 0
      api/vendor/monolog/monolog/src/Monolog/Handler/BufferHandler.php
  80. 212 0
      api/vendor/monolog/monolog/src/Monolog/Handler/ChromePHPHandler.php
  81. 72 0
      api/vendor/monolog/monolog/src/Monolog/Handler/CouchDBHandler.php
  82. 152 0
      api/vendor/monolog/monolog/src/Monolog/Handler/CubeHandler.php
  83. 57 0
      api/vendor/monolog/monolog/src/Monolog/Handler/Curl/Util.php
  84. 169 0
      api/vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php
  85. 45 0
      api/vendor/monolog/monolog/src/Monolog/Handler/DoctrineCouchDBHandler.php
  86. 108 0
      api/vendor/monolog/monolog/src/Monolog/Handler/DynamoDbHandler.php
  87. 128 0
      api/vendor/monolog/monolog/src/Monolog/Handler/ElasticSearchHandler.php
  88. 82 0
      api/vendor/monolog/monolog/src/Monolog/Handler/ErrorLogHandler.php
  89. 172 0
      api/vendor/monolog/monolog/src/Monolog/Handler/FilterHandler.php
  90. 28 0
      api/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ActivationStrategyInterface.php
  91. 59 0
      api/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ChannelLevelActivationStrategy.php
  92. 34 0
      api/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ErrorLevelActivationStrategy.php
  93. 207 0
      api/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossedHandler.php
  94. 195 0
      api/vendor/monolog/monolog/src/Monolog/Handler/FirePHPHandler.php
  95. 126 0
      api/vendor/monolog/monolog/src/Monolog/Handler/FleepHookHandler.php
  96. 128 0
      api/vendor/monolog/monolog/src/Monolog/Handler/FlowdockHandler.php
  97. 39 0
      api/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerInterface.php
  98. 63 0
      api/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerTrait.php
  99. 65 0
      api/vendor/monolog/monolog/src/Monolog/Handler/GelfHandler.php
  100. 117 0
      api/vendor/monolog/monolog/src/Monolog/Handler/GroupHandler.php

+ 1 - 0
.gitignore

@@ -60,6 +60,7 @@ php_errors.log
 # =========================
 # Organizr files
 # =========================
+organizr-*.log
 databaseLocation.ini.php
 homepageSettings.ini.php
 loginLog.json

+ 35 - 0
api/classes/logger.class.php

@@ -0,0 +1,35 @@
+<?php
+
+use Nekonomokochan\PhpJsonLogger\Logger;
+use Nekonomokochan\PhpJsonLogger\LoggerBuilder;
+
+class OrganizrLogger extends LoggerBuilder
+{
+	public $isReady;
+	
+	/**
+	 * @return boolean
+	 */
+	public function getReadyStatus(): bool
+	{
+		return $this->isReady;
+	}
+	
+	/**
+	 * @param boolean $readyStatus
+	 */
+	public function setReadyStatus(bool $readyStatus)
+	{
+		$this->isReady = $readyStatus;
+	}
+	
+	public function build(): Logger
+	{
+		if (!$this->isReady) {
+			$this->setChannel('Organizr');
+			$this->setLogLevel(self::DEBUG);
+			$this->setMaxFiles(1);
+		}
+		return new Logger($this);
+	}
+}

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


+ 3 - 1
api/composer.json

@@ -16,6 +16,8 @@
     "slim/psr7": "^1.1",
     "zircote/swagger-php": "^3.0",
     "bogstag/oauth2-trakt": "^1.0",
-    "paquettg/php-html-parser": "^3.1"
+    "paquettg/php-html-parser": "^3.1",
+    "nekonomokochan/php-json-logger": "^1.3",
+    "bcremer/line-reader": "^1.1"
   }
 }

+ 285 - 1
api/composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "db7f63e80bb05dd9f7e63091879075a6",
+    "content-hash": "0643757e58f97f0068f330f791e91ded",
     "packages": [
         {
             "name": "adldap2/adldap2",
@@ -71,6 +71,50 @@
             },
             "time": "2021-08-09T15:22:35+00:00"
         },
+        {
+            "name": "bcremer/line-reader",
+            "version": "1.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/bcremer/LineReader.git",
+                "reference": "3ec3e200577630f1e58d30b4c1c468b877d8d0a7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/bcremer/LineReader/zipball/3ec3e200577630f1e58d30b4c1c468b877d8d0a7",
+                "reference": "3ec3e200577630f1e58d30b4c1c468b877d8d0a7",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.3|^7.4|^8.0"
+            },
+            "require-dev": {
+                "infection/infection": "^0.18",
+                "phpunit/phpunit": "^9.4"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Bcremer\\LineReader\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Benjamin Cremer",
+                    "email": "bc@benjamin-cremer.de"
+                }
+            ],
+            "description": "Read large files line by line in a memory efficient (constant) way.",
+            "support": {
+                "issues": "https://github.com/bcremer/LineReader/issues",
+                "source": "https://github.com/bcremer/LineReader/tree/1.1.0"
+            },
+            "time": "2020-12-08T13:58:53+00:00"
+        },
         {
             "name": "bogstag/oauth2-trakt",
             "version": "v1.0.1",
@@ -973,6 +1017,92 @@
             },
             "time": "2020-10-28T02:03:40+00:00"
         },
+        {
+            "name": "monolog/monolog",
+            "version": "1.26.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Seldaek/monolog.git",
+                "reference": "c6b00f05152ae2c9b04a448f99c7590beb6042f5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c6b00f05152ae2c9b04a448f99c7590beb6042f5",
+                "reference": "c6b00f05152ae2c9b04a448f99c7590beb6042f5",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0",
+                "psr/log": "~1.0"
+            },
+            "provide": {
+                "psr/log-implementation": "1.0.0"
+            },
+            "require-dev": {
+                "aws/aws-sdk-php": "^2.4.9 || ^3.0",
+                "doctrine/couchdb": "~1.0@dev",
+                "graylog2/gelf-php": "~1.0",
+                "php-amqplib/php-amqplib": "~2.4",
+                "php-console/php-console": "^3.1.3",
+                "phpstan/phpstan": "^0.12.59",
+                "phpunit/phpunit": "~4.5",
+                "ruflin/elastica": ">=0.90 <3.0",
+                "sentry/sentry": "^0.13",
+                "swiftmailer/swiftmailer": "^5.3|^6.0"
+            },
+            "suggest": {
+                "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
+                "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+                "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
+                "ext-mongo": "Allow sending log messages to a MongoDB server",
+                "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
+                "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
+                "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
+                "php-console/php-console": "Allow sending log messages to Google Chrome",
+                "rollbar/rollbar": "Allow sending log messages to Rollbar",
+                "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
+                "sentry/sentry": "Allow sending log messages to a Sentry server"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Monolog\\": "src/Monolog"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be",
+                    "homepage": "http://seld.be"
+                }
+            ],
+            "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
+            "homepage": "http://github.com/Seldaek/monolog",
+            "keywords": [
+                "log",
+                "logging",
+                "psr-3"
+            ],
+            "support": {
+                "issues": "https://github.com/Seldaek/monolog/issues",
+                "source": "https://github.com/Seldaek/monolog/tree/1.26.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/Seldaek",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-05-28T08:32:12+00:00"
+        },
         {
             "name": "myclabs/php-enum",
             "version": "1.8.0",
@@ -1033,6 +1163,55 @@
             ],
             "time": "2021-02-15T16:11:48+00:00"
         },
+        {
+            "name": "nekonomokochan/php-json-logger",
+            "version": "v1.3.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/nekonomokochan/php-json-logger.git",
+                "reference": "6df126a82940a00d8ea2da6e0b7c58e3e57eb132"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/nekonomokochan/php-json-logger/zipball/6df126a82940a00d8ea2da6e0b7c58e3e57eb132",
+                "reference": "6df126a82940a00d8ea2da6e0b7c58e3e57eb132",
+                "shasum": ""
+            },
+            "require": {
+                "monolog/monolog": "^1.24",
+                "php": "~7.1",
+                "ramsey/uuid": "^3.8"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "^2.14",
+                "php": "~7.1",
+                "php-coveralls/php-coveralls": "^2.1",
+                "phpunit/phpcov": "^5.0",
+                "phpunit/phpunit": "^7.5"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Nekonomokochan\\PhpJsonLogger\\": "src/PhpJsonLogger"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "keitakn",
+                    "email": "keita.koga.work@gmail.com"
+                }
+            ],
+            "description": "LoggingLibrary for PHP. Output by JSON Format",
+            "support": {
+                "issues": "https://github.com/nekonomokochan/php-json-logger/issues",
+                "source": "https://github.com/nekonomokochan/php-json-logger/tree/feature/issue63"
+            },
+            "time": "2019-02-18T06:07:14+00:00"
+        },
         {
             "name": "nikic/fast-route",
             "version": "v1.3.0",
@@ -2173,6 +2352,111 @@
             },
             "time": "2019-03-08T08:55:37+00:00"
         },
+        {
+            "name": "ramsey/uuid",
+            "version": "3.9.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ramsey/uuid.git",
+                "reference": "ffa80ab953edd85d5b6c004f96181a538aad35a3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ramsey/uuid/zipball/ffa80ab953edd85d5b6c004f96181a538aad35a3",
+                "reference": "ffa80ab953edd85d5b6c004f96181a538aad35a3",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "paragonie/random_compat": "^1 | ^2 | ^9.99.99",
+                "php": "^5.4 | ^7.0 | ^8.0",
+                "symfony/polyfill-ctype": "^1.8"
+            },
+            "replace": {
+                "rhumsaa/uuid": "self.version"
+            },
+            "require-dev": {
+                "codeception/aspect-mock": "^1 | ^2",
+                "doctrine/annotations": "^1.2",
+                "goaop/framework": "1.0.0-alpha.2 | ^1 | >=2.1.0 <=2.3.2",
+                "mockery/mockery": "^0.9.11 | ^1",
+                "moontoast/math": "^1.1",
+                "nikic/php-parser": "<=4.5.0",
+                "paragonie/random-lib": "^2",
+                "php-mock/php-mock-phpunit": "^0.3 | ^1.1 | ^2.6",
+                "php-parallel-lint/php-parallel-lint": "^1.3",
+                "phpunit/phpunit": ">=4.8.36 <9.0.0 | >=9.3.0",
+                "squizlabs/php_codesniffer": "^3.5",
+                "yoast/phpunit-polyfills": "^1.0"
+            },
+            "suggest": {
+                "ext-ctype": "Provides support for PHP Ctype functions",
+                "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator",
+                "ext-openssl": "Provides the OpenSSL extension for use with the OpenSslGenerator",
+                "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator",
+                "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).",
+                "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter",
+                "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid",
+                "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type."
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Ramsey\\Uuid\\": "src/"
+                },
+                "files": [
+                    "src/functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ben Ramsey",
+                    "email": "ben@benramsey.com",
+                    "homepage": "https://benramsey.com"
+                },
+                {
+                    "name": "Marijn Huizendveld",
+                    "email": "marijn.huizendveld@gmail.com"
+                },
+                {
+                    "name": "Thibaud Fabre",
+                    "email": "thibaud@aztech.io"
+                }
+            ],
+            "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).",
+            "homepage": "https://github.com/ramsey/uuid",
+            "keywords": [
+                "guid",
+                "identifier",
+                "uuid"
+            ],
+            "support": {
+                "issues": "https://github.com/ramsey/uuid/issues",
+                "rss": "https://github.com/ramsey/uuid/releases.atom",
+                "source": "https://github.com/ramsey/uuid",
+                "wiki": "https://github.com/ramsey/uuid/wiki"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/ramsey",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-09-25T23:07:42+00:00"
+        },
         {
             "name": "rmccue/requests",
             "version": "v1.8.0",

+ 16 - 3
api/config/default.php

@@ -74,9 +74,13 @@ return [
 	'ssoPlex' => false,
 	'ssoOmbi' => false,
 	'ssoTautulli' => false,
+	'ssoTautulliAuth' => '4',
 	'ssoJellyfin' => false,
 	'ssoOverseerr' => false,
 	'ssoPetio' => false,
+	'ssoKomga' => false,
+	'ssoKomgaAuth' => '4',
+	'komgaURL' => '',
 	'sonarrURL' => '',
 	'sonarrUnmonitored' => false,
 	'sonarrToken' => '',
@@ -391,7 +395,7 @@ return [
 	'lockoutMinAuth' => '1',
 	'themeInstalled' => '',
 	'themeVersion' => '',
-	'installedPlugins' => '',
+	'installedPlugins' => [],
 	'installedThemes' => '',
 	'authDebug' => false,
 	'customJava' => '',
@@ -457,8 +461,10 @@ return [
 	'homepageTautulliEnabled' => false,
 	'homepageTautulliAuth' => '1',
 	'homepageTautulliLibraryAuth' => '1',
+	'homepageTautulliLibraryStatsExclude' => '',
 	'homepageTautulliViewsAuth' => '1',
 	'homepageTautulliMiscAuth' => '1',
+	'homepageTautulliViewingStatsExclude' => '',
 	'homepageTautulliRefresh' => '60000',
 	'tautulliApikey' => '',
 	'tautulliLibraries' => true,
@@ -570,7 +576,7 @@ return [
 	'netdataCustom' => '{}',
 	'homepageOctoprintEnabled' => false,
 	'homepageOctoprintAuth' => '1',
-	'homepageOctoprintRefresh' => 10000,
+	'homepageOctoprintRefresh' => '10000',
 	'octoprintURL' => '',
 	'octoprintToken' => '',
 	'octoprintHeaderToggle' => true,
@@ -605,5 +611,12 @@ return [
 	'easterEggs' => true,
 	'allowCollapsableSideMenu' => false,
 	'sideMenuCollapsed' => false,
-	'collapseSideMenuOnClick' => false
+	'collapseSideMenuOnClick' => false,
+	'logLevel' => 'INFO',
+	'maxLogFiles' => '7',
+	'logLiveUpdateRefresh' => '5000',
+	'logPageSize' => '50',
+	'includeDatabaseQueriesInDebug' => false,
+	'externalPluginMarketplaceRepos' => '',
+	'checkForPluginUpdate' => true
 ];

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

@@ -182,7 +182,6 @@ trait AuthFunctions
 			} else {
 				return false;
 			}
-			
 		} catch (Requests_Exception $e) {
 			$this->writeLog('success', 'Plex Token Check Function - Error: ' . $e->getMessage(), 'SYSTEM');
 		}

+ 4 - 2
api/functions/backup-functions.php

@@ -99,7 +99,8 @@ trait BackupFunctions
 		$totalFiles = 0;
 		$totalFileSize = 0;
 		foreach ($files as $file) {
-			if (file_exists($path . $file)) {
+			$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
+			if (file_exists($path . $file) && $ext == 'zip') {
 				$size = filesize($path . $file);
 				$totalFileSize = $totalFileSize + $size;
 				$totalFiles = $totalFiles + 1;
@@ -117,8 +118,9 @@ trait BackupFunctions
 		}
 		$fileList['total_files'] = $totalFiles;
 		$fileList['total_size'] = $this->human_filesize($totalFileSize, 2);
+		$fileList['files'] = array_reverse($fileList['files']);
 		$this->setAPIResponse('success', null, 200, array_reverse($fileList));
 		return array_reverse($fileList);
 	}
 	
-}
+}

+ 390 - 8
api/functions/log-functions.php

@@ -2,23 +2,405 @@
 
 trait LogFunctions
 {
-	public function info($msg, $username = null)
+	public function debug($msg, $context = [])
 	{
-		$this->writeLog('info', $msg, $username);
+		if ($this->logger) {
+			$this->logger->debug($msg, $context);
+		}
 	}
 	
-	public function error($msg, $username = null)
+	public function info($msg, $context = [])
 	{
-		$this->writeLog('error', $msg, $username);
+		if ($this->logger) {
+			$this->logger->info($msg, $context);
+		}
 	}
 	
-	public function warning($msg, $username = null)
+	public function notice($msg, $context = [])
 	{
-		$this->writeLog('warning', $msg, $username);
+		if ($this->logger) {
+			$this->logger->notice($msg, $context);
+		}
 	}
 	
-	public function debug($msg, $username = null)
+	public function warning($msg, $context = [])
 	{
-		$this->writeLog('debug', $msg, $username);
+		if ($this->logger) {
+			$this->logger->warning($msg, $context);
+		}
+	}
+	
+	public function error($msg, $context = [])
+	{
+		if ($this->logger) {
+			$this->logger->error($msg, $context);
+		}
+	}
+	
+	public function critical($msg, $context = [])
+	{
+		if ($this->logger) {
+			$this->logger->critical($msg, $context);
+		}
+	}
+	
+	public function alert($msg, $context = [])
+	{
+		if ($this->logger) {
+			$this->logger->alert($msg, $context);
+		}
+	}
+	
+	public function emergency($msg, $context = [])
+	{
+		if ($this->logger) {
+			$this->logger->emergency($msg, $context);
+		}
+	}
+	
+	public function setOrganizrLog()
+	{
+		if ($this->hasDB()) {
+			$logPath = $this->config['dbLocation'] . 'logs' . DIRECTORY_SEPARATOR;
+			return $logPath . 'organizr.log';
+		}
+		return false;
+	}
+	
+	public function readLog($file, $pageSize = 10, $offset = 0, $filter = 'NONE')
+	{
+		$combinedLogs = false;
+		if ($file == 'combined-logs') {
+			$combinedLogs = true;
+		}
+		if (file_exists($file) || $combinedLogs) {
+			$filter = strtoupper($filter);
+			switch ($filter) {
+				case 'DEBUG':
+				case 'INFO':
+				case 'NOTICE':
+				case 'WARNING':
+				case 'ERROR':
+				case 'CRITICAL':
+				case 'ALERT':
+				case 'EMERGENCY':
+					break;
+				case 'NONE':
+					$filter = null;
+					break;
+				default:
+					$filter = 'DEBUG';
+					break;
+			}
+			if ($combinedLogs) {
+				$logs = $this->getLogFiles();
+				$lines = [];
+				if ($logs) {
+					foreach ($logs as $log) {
+						if (file_exists($log)) {
+							$lineGenerator = Bcremer\LineReader\LineReader::readLinesBackwards($log);
+							$lines = array_merge($lines, iterator_to_array($lineGenerator));
+						}
+					}
+				}
+			} else {
+				$lineGenerator = Bcremer\LineReader\LineReader::readLinesBackwards($file);
+				$lines = iterator_to_array($lineGenerator);
+			}
+			if ($filter) {
+				$results = [];
+				foreach ($lines as $line) {
+					if (stripos($line, '"' . $filter . '"') !== false) {
+						$results[] = $line;
+					}
+				}
+				$lines = $results;
+			}
+			return $this->formatLogResults($lines, $pageSize, $offset);
+		}
+		return false;
+	}
+	
+	public function formatLogResults($lines, $pageSize, $offset)
+	{
+		$totalLines = count($lines);
+		$totalPages = $totalLines / $pageSize;
+		$results = array_slice($lines, $offset, $pageSize);
+		$lines = [];
+		foreach ($results as $line) {
+			$lines[] = json_decode($line, true);
+		}
+		return [
+			'pageInfo' => [
+				'results' => $totalLines,
+				'totalPages' => ceil($totalPages),
+				'pageSize' => $pageSize,
+				'page' => $offset >= $totalPages ? -1 : ceil($offset / $pageSize) + 1
+			],
+			'results' => $lines
+		];
+	}
+	
+	public function getLatestLogFile()
+	{
+		if ($this->log) {
+			if (isset($this->log)) {
+				$folder = $this->config['dbLocation'] . 'logs' . DIRECTORY_SEPARATOR;
+				$directoryIterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS);
+				$iteratorIterator = new RecursiveIteratorIterator($directoryIterator);
+				$files = [];
+				foreach ($iteratorIterator as $info) {
+					$files[] = $info->getPathname();
+				}
+				if (count($files) > 0) {
+					usort($files, function ($x, $y) {
+						preg_match('/[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/', $x, $xArray);
+						preg_match('/[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/', $y, $yArray);
+						return strtotime($xArray[0]) < strtotime($yArray[0]);
+					});
+					if (file_exists($files[0])) {
+						return $files[0];
+					}
+				}
+			}
+		}
+		return false;
+	}
+	
+	public function getLogFiles()
+	{
+		if ($this->log) {
+			if (isset($this->log)) {
+				$folder = $this->config['dbLocation'] . 'logs' . DIRECTORY_SEPARATOR;
+				$directoryIterator = new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS);
+				$iteratorIterator = new RecursiveIteratorIterator($directoryIterator);
+				$files = [];
+				foreach ($iteratorIterator as $info) {
+					$files[] = $info->getPathname();
+				}
+				if (count($files) > 0) {
+					usort($files, function ($x, $y) {
+						preg_match('/[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/', $x, $xArray);
+						preg_match('/[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/', $y, $yArray);
+						return strtotime($xArray[0]) < strtotime($yArray[0]);
+					});
+					return $files;
+				}
+			}
+		}
+		return false;
+	}
+	
+	public function setLoggerChannel($channel = 'Organizr', $username = null)
+	{
+		if ($this->hasDB()) {
+			$setLogger = false;
+			if ($this->logger) {
+				if ($channel) {
+					if (strtolower($this->logger->getChannel()) !== strtolower($channel)) {
+						$setLogger = true;
+					}
+				}
+				if ($username) {
+					if (strtolower($this->logger->getTraceId()) !== strtolower($channel)) {
+						$setLogger = true;
+					}
+				}
+			} else {
+				$setLogger = true;
+			}
+			if ($setLogger) {
+				$channel = $channel ?: 'Organizr';
+				$this->setupLogger($channel, $username);
+			}
+		}
+	}
+	
+	public function setupLogger($channel = 'Organizr', $username = null)
+	{
+		if (!$username) {
+			$username = $this->user['username'] ?? 'System';
+		}
+		$loggerBuilder = new OrganizrLogger();
+		$loggerBuilder->setReadyStatus($this->hasDB() && $this->log);
+		$loggerBuilder->setMaxFiles($this->config['maxLogFiles']);
+		$loggerBuilder->setFileName($this->tempLogIfNeeded());
+		$loggerBuilder->setTraceId($username);
+		$loggerBuilder->setChannel(ucwords(strtolower($channel)));
+		switch ($this->config['logLevel']) {
+			case 'DEBUG':
+				$logLevel = Nekonomokochan\PhpJsonLogger\LoggerBuilder::DEBUG;
+				break;
+			case 'INFO':
+				$logLevel = Nekonomokochan\PhpJsonLogger\LoggerBuilder::INFO;
+				break;
+			case 'NOTICE':
+				$logLevel = Nekonomokochan\PhpJsonLogger\LoggerBuilder::NOTICE;
+				break;
+			case 'ERROR':
+				$logLevel = Nekonomokochan\PhpJsonLogger\LoggerBuilder::ERROR;
+				break;
+			case 'CRITICAL':
+				$logLevel = Nekonomokochan\PhpJsonLogger\LoggerBuilder::CRITICAL;
+				break;
+			case 'ALERT':
+				$logLevel = Nekonomokochan\PhpJsonLogger\LoggerBuilder::ALERT;
+				break;
+			case 'EMERGENCY':
+				$logLevel = Nekonomokochan\PhpJsonLogger\LoggerBuilder::EMERGENCY;
+				break;
+			default:
+				$logLevel = Nekonomokochan\PhpJsonLogger\LoggerBuilder::WARNING;
+				break;
+		}
+		$loggerBuilder->setLogLevel($logLevel);
+		try {
+			$this->logger = $loggerBuilder->build();
+		} catch (Exception $e) {
+			// nothing so far
+			$this->logger = null;
+		}
+		/* setup:
+		set the log channel before you send log (You can set an optional Username (2nd Variable) | If user is logged already logged in, it will use their username):
+		$this->setLoggerChannel('Plex Homepage');
+		normal log:
+		$this->logger->info('test');
+		normal log with context ($context must be an array):
+		$this->logger->info('test', $context);
+		exception:
+		$this->logger->critical($exception, $context);
+		*/
+		
+	}
+	
+	public function tempLogIfNeeded()
+	{
+		if (!$this->log) {
+			return $this->root . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR . 'organizr-' . $this->randString() . '.log';
+		} else {
+			return $this->log;
+		}
+	}
+	
+	public function getLog($pageSize = 10, $offset = 0, $filter = 'NONE', $number = 0)
+	{
+		if ($this->log) {
+			if (isset($this->log)) {
+				if ($number !== 0) {
+					if ($number == 'all' || $number == 'combined-logs') {
+						$log = 'combined-logs';
+					} else {
+						$logs = $this->getLogFiles();
+						$log = $logs[$number] ?? $this->getLatestLogFile();
+					}
+				} else {
+					$log = $this->getLatestLogFile();
+				}
+				$readLog = $this->readLog($log, 1000, 0, $filter);
+				$this->setResponse(200, 'Results for log: ' . $log, $readLog);
+				return $readLog;
+			} else {
+				$this->setResponse(404, 'Log not found');
+				return false;
+			}
+		} else {
+			$this->setResponse(409, 'Logging not setup');
+			return false;
+		}
+	}
+	
+	public function purgeLog($number)
+	{
+		$this->setLoggerChannel('Logger');
+		$this->logger->debug('Starting log purge function');
+		if ($this->log) {
+			$this->logger->debug('Checking if log id exists');
+			if ($number !== 0) {
+				if ($number == 'all' || $number == 'combined-logs') {
+					$this->logger->debug('Cannot delete log [all] as it is not a real log');
+					$this->setResponse(409, 'Cannot delete log [all] as it is not a real log');
+					return false;
+				}
+				$logs = $this->getLogFiles();
+				$file = $logs[$number] ?? false;
+				if (!$file) {
+					$this->setResponse(404, 'Log not found');
+					return false;
+				}
+			} else {
+				$file = $this->getLatestLogFile();
+			}
+			preg_match('/[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/', $file, $log);
+			$log = $log[0];
+			$this->logger->debug('Checking if log exists');
+			if (file_exists($file)) {
+				$this->logger->debug('Log: ' . $log . ' does exist');
+				$this->logger->debug('Attempting to purge log: ' . $log);
+				if (unlink($file)) {
+					$this->logger->info('Log: ' . $log . ' has been purged/deleted');
+					$this->setResponse(200, 'Log purged');
+					return true;
+				} else {
+					$this->logger->warning('Log: ' . $log . ' could not be purged/deleted');
+					$this->setResponse(500, 'Log could not be purged');
+					return false;
+				}
+			} else {
+				$this->logger->debug('Log does not exist');
+				$this->setResponse(404, 'Log does not exist');
+				return false;
+			}
+		} else {
+			$this->setResponse(409, 'Logging not setup');
+			return false;
+		}
+	}
+	
+	public function logArray($context)
+	{
+		if (!is_array($context)) {
+			if (is_string($context)) {
+				return ['data' => $context];
+			} else {
+				$context = (string)$context;
+				return ['data' => $context];
+			}
+		} else {
+			return $context;
+		}
+	}
+	
+	function buildLogDropdown()
+	{
+		$logs = $this->getLogFiles();
+		//<select class='form-control settings-dropdown-box system-settings-menu'><option value=''>About</option></select>
+		if ($logs) {
+			if (count($logs) > 0) {
+				$options = '';
+				$i = 0;
+				foreach ($logs as $k => $log) {
+					$selected = $i == 0 ? 'selected' : '';
+					preg_match('/[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/', $log, $name);
+					$options .= '<option data-id="' . $k . '" value="api/v2/log/' . $k . '?filter=NONE&pageSize=1000&offset=0" ' . $selected . '>' . $name[0] . '</option>';
+					$i++;
+				}
+				return '<select class="form-control choose-organizr-log"><option data-id="all" value="api/v2/log/all?filter=NONE&pageSize=1000&offset=0">All</option>' . $options . '</select>';
+			}
+		}
+		return false;
+	}
+	
+	function buildFilterDropdown()
+	{
+		$dropdownItems = '<li><a href="javascript:toggleLogFilter(\'DEBUG\')"><span lang="en">Debug</span></a></li>';
+		$dropdownItems .= '<li><a href="javascript:toggleLogFilter(\'INFO\')"><span lang="en">Info</span></a></li>';
+		$dropdownItems .= '<li><a href="javascript:toggleLogFilter(\'NOTICE\')"><span lang="en">Notice</span></a></li>';
+		$dropdownItems .= '<li><a href="javascript:toggleLogFilter(\'WARNING\')"><span lang="en">Warning</span></a></li>';
+		$dropdownItems .= '<li><a href="javascript:toggleLogFilter(\'ERROR\')"><span lang="en">Error</span></a></li>';
+		$dropdownItems .= '<li><a href="javascript:toggleLogFilter(\'CRITICAL\')"><span lang="en">Critical</span></a></li>';
+		$dropdownItems .= '<li><a href="javascript:toggleLogFilter(\'ALERT\')"><span lang="en">Alert</span></a></li>';
+		$dropdownItems .= '<li><a href="javascript:toggleLogFilter(\'EMERGENCY\')"><span lang="en">Emergency</span></a></li>';
+		$dropdownItems .= '<li class="divider"></li><li><a href="javascript:toggleLogFilter(\'NONE\')"><span lang="en">None</span></a></li>';
+		return '<button aria-expanded="false" data-toggle="dropdown" class="btn btn-inverse dropdown-toggle waves-effect waves-light pull-right m-r-5 hidden-xs" type="button"> <span class="log-filter-text m-r-5" lang="en">NONE</span><i class="fa fa-filter m-r-5"></i></button><ul role="menu" class="dropdown-menu log-filter-dropdown pull-right">' . $dropdownItems . '</ul>';
 	}
 }

+ 118 - 10
api/functions/option-functions.php

@@ -30,6 +30,7 @@ trait OptionsFunction
 				];
 				break;
 			case 'auth':
+				$this->setGroupOptionsVariable();
 				$settingMerge = [
 					'type' => 'select',
 					'label' => 'Minimum Authentication',
@@ -53,11 +54,12 @@ trait OptionsFunction
 			case 'test':
 				$settingMerge = [
 					'type' => 'button',
-					'label' => '',
+					'label' => 'Test Connection',
 					'icon' => 'fa fa-flask',
 					'class' => 'pull-right',
 					'text' => 'Test Connection',
-					'attr' => 'onclick="testAPIConnection(\'' . $name . '\')"'
+					'attr' => 'onclick="testAPIConnection(\'' . $name . '\')"',
+					'help' => 'Remember! Please save before using the test button!'
 				];
 				break;
 			case 'url':
@@ -72,22 +74,22 @@ trait OptionsFunction
 				$settingMerge = [
 					'type' => 'select2',
 					'class' => 'select2-multiple',
-					'id' => $name . '-select',
+					'id' => $name . '-select-' . $this->random_ascii_string(6),
 					'label' => 'Multiple URL\'s',
 					'help' => 'Please make sure to use local IP address and port - You also may use local dns name too.',
 					'placeholder' => 'http(s)://hostname:port',
 					'options' => $this->makeOptionsFromValues($this->config[$name]),
-					'settings' => '{tags: true, selectOnClose: true, closeOnSelect: true}',
+					'settings' => '{tags: true, selectOnClose: true, closeOnSelect: true, allowClear: true}',
 				];
 				break;
 			case 'multiple':
 				$settingMerge = [
 					'type' => 'select2',
 					'class' => 'select2-multiple',
-					'id' => $name . '-select',
+					'id' => $name . '-select-' . $this->random_ascii_string(6),
 					'label' => 'Multiple Values\'s',
 					'options' => $this->makeOptionsFromValues($this->config[$name]),
-					'settings' => '{tags: true, selectOnClose: true, closeOnSelect: true}',
+					'settings' => '{tags: true, selectOnClose: true, closeOnSelect: true, allowClear: true}',
 				];
 				break;
 			case 'username':
@@ -108,6 +110,12 @@ trait OptionsFunction
 					'label' => 'Password',
 				];
 				break;
+			case 'passwordaltcopy':
+				$settingMerge = [
+					'type' => 'password-alt-copy',
+					'label' => 'Password',
+				];
+				break;
 			case 'apikey':
 			case 'token':
 				$settingMerge = [
@@ -120,10 +128,10 @@ trait OptionsFunction
 				$settingMerge = [
 					'type' => 'select2',
 					'class' => 'select2-multiple',
-					'id' => $name . '-select',
+					'id' => $name . '-select-' . $this->random_ascii_string(6),
 					'label' => 'Multiple API Key/Token\'s',
 					'options' => $this->makeOptionsFromValues($this->config[$name]),
-					'settings' => '{tags: true, theme: "default password-alt", selectOnClose: true, closeOnSelect: true}',
+					'settings' => '{tags: true, theme: "default", selectionCssClass: "password-alt", selectOnClose: true, closeOnSelect: true, allowClear: true}',
 				];
 				break;
 			case 'notice':
@@ -263,7 +271,7 @@ trait OptionsFunction
 				$settingMerge = [
 					'type' => 'select2',
 					'class' => 'select2-multiple',
-					'id' => $name . '-exclude-select',
+					'id' => $name . '-exclude-select-' . $this->random_ascii_string(6),
 					'label' => 'Libraries to Exclude',
 					'options' => $extras['options']
 				];
@@ -272,7 +280,7 @@ trait OptionsFunction
 				$settingMerge = [
 					'type' => 'select2',
 					'class' => 'select2-multiple',
-					'id' => $name . '-include-select',
+					'id' => $name . '-include-select-' . $this->random_ascii_string(6),
 					'label' => 'Libraries to Include',
 					'options' => $extras['options']
 				];
@@ -368,6 +376,14 @@ trait OptionsFunction
 					'options' => $this->limitOptions()
 				];
 				break;
+			case 'color':
+				$settingMerge = [
+					'type' => 'input',
+					'label' => 'Color',
+					'class' => 'pick-a-color-custom-options',
+					'attr' => 'data-original="' . $this->config[$name] . '"'
+				];
+				break;
 			default:
 				$settingMerge = [
 					'type' => strtolower($type),
@@ -406,6 +422,98 @@ trait OptionsFunction
 		return $formattedValues;
 	}
 	
+	public function logLevels()
+	{
+		return [
+			[
+				'name' => 'Debug',
+				'value' => 'DEBUG'
+			],
+			[
+				'name' => 'Info',
+				'value' => 'INFO'
+			],
+			[
+				'name' => 'Notice',
+				'value' => 'NOTICE'
+			],
+			[
+				'name' => 'Warning',
+				'value' => 'WARNING'
+			],
+			[
+				'name' => 'Error',
+				'value' => 'ERROR'
+			],
+			[
+				'name' => 'Critical',
+				'value' => 'CRITICAL'
+			],
+			[
+				'name' => 'Alert',
+				'value' => 'ALERT'
+			],
+			[
+				'name' => 'Emergency',
+				'value' => 'EMERGENCY'
+			]
+		];
+	}
+	
+	public function sandboxOptions()
+	{
+		return [
+			[
+				'name' => 'Allow Presentation',
+				'value' => 'allow-presentation'
+			],
+			[
+				'name' => 'Allow Forms',
+				'value' => 'allow-forms'
+			],
+			[
+				'name' => 'Allow Same Origin',
+				'value' => 'allow-same-origin'
+			],
+			[
+				'name' => 'Allow Orientation Lock',
+				'value' => 'allow-orientation-lock'
+			],
+			[
+				'name' => 'Allow Pointer Lock',
+				'value' => 'allow-pointer-lock'
+			],
+			[
+				'name' => 'Allow Scripts',
+				'value' => 'allow-scripts'
+			],
+			[
+				'name' => 'Allow Popups',
+				'value' => 'allow-popups'
+			],
+			[
+				'name' => 'Allow Popups To Escape Sandbox',
+				'value' => 'allow-popups-to-escape-sandbox'
+			],
+			[
+				'name' => 'Allow Modals',
+				'value' => 'allow-modals'
+			],
+			[
+				'name' => 'Allow Top Navigation',
+				'value' => 'allow-top-navigation'
+			],
+			[
+				'name' => 'Allow Top Navigation By User Activation',
+				'value' => 'allow-top-navigation-by-user-activation'
+			],
+			[
+				'name' => 'Allow Downloads',
+				'value' => 'allow-downloads'
+			],
+		];
+	}
+	
 	public function calendarLocaleOptions()
 	{
 		return [

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

@@ -709,6 +709,25 @@ trait OrganizrFunctions
 		$this->coookie('delete', 'jellyfin_credentials');
 	}
 	
+	public function clearKomgaToken()
+	{
+		if (isset($_COOKIE['komga_token'])) {
+			try {
+				$url = $this->qualifyURL($this->config['komgaURL']);
+				$options = $this->requestOptions($url, 60000, true, false);
+				$response = Requests::post($url . '/api/v1/users/logout', ['X-Auth-Token' => $_COOKIE['komga_token']], $options);
+				if ($response->success) {
+					$this->writeLog('success', 'Komga Token Function - Logged User out', 'SYSTEM');
+				} else {
+					$this->writeLog('error', 'Komga Token Function - Unable to Logged User out', 'SYSTEM');
+				}
+			} catch (Requests_Exception $e) {
+				$this->writeLog('error', 'Komga Token Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			}
+			$this->coookie('delete', 'komga_token');
+		}
+	}
+	
 	public function analyzeIP($ip)
 	{
 		if (strpos($ip, '/') !== false) {

+ 61 - 2
api/functions/sso-functions.php

@@ -8,6 +8,7 @@ trait SSOFunctions
 			'myPlexAccessToken' => $_COOKIE['mpt'] ?? false,
 			'id_token' => $_COOKIE['Auth'] ?? false,
 			'jellyfin_credentials' => $_COOKIE['jellyfin_credentials'] ?? false,
+			'komga_token' => $_COOKIE['komga_token'] ?? false
 		);
 		// Jellyfin cookie
 		foreach (array_keys($_COOKIE) as $k => $v) {
@@ -28,56 +29,114 @@ trait SSOFunctions
 			'ombi' => 'username',
 			'overseerr' => 'email',
 			'tautulli' => 'username',
-			'petio' => 'username'
+			'petio' => 'username',
+			'komga' => 'email'
 		);
 		return (gettype($userobj) == 'string') ? $userobj : $userobj[$map[$app]];
 	}
 	
 	public function ssoCheck($userobj, $password, $token = null)
 	{
+		$this->setCurrentUser(false);
+		$this->setLoggerChannel('Authentication', $userobj['username']);
+		$this->logger->debug('Starting SSO check function');
 		if ($this->config['ssoPlex'] && $token) {
+			$this->logger->debug('Setting Plex SSO cookie');
 			$this->coookie('set', 'mpt', $token, $this->config['rememberMeDays'], false);
 		}
 		if ($this->config['ssoOmbi']) {
+			$this->logger->debug('Starting Ombi SSO check function');
 			$fallback = ($this->config['ombiFallbackUser'] !== '' && $this->config['ombiFallbackPassword'] !== '');
 			$ombiToken = $this->getOmbiToken($this->getSSOUserFor('ombi', $userobj), $password, $token, $fallback);
 			if ($ombiToken) {
+				$this->logger->debug('Setting Ombi SSO cookie');
 				$this->coookie('set', 'Auth', $ombiToken, $this->config['rememberMeDays'], false);
+			} else {
+				$this->logger->debug('No Ombi token received from backend');
 			}
 		}
-		if ($this->config['ssoTautulli']) {
+		if ($this->config['ssoTautulli'] && $this->qualifyRequest($this->config['ssoTautulliAuth'])) {
+			$this->logger->debug('Starting Tautulli SSO check function');
 			$tautulliToken = $this->getTautulliToken($this->getSSOUserFor('tautulli', $userobj), $password, $token);
 			if ($tautulliToken) {
 				foreach ($tautulliToken as $key => $value) {
+					$this->logger->debug('Setting Tautulli SSO cookie');
 					$this->coookie('set', 'tautulli_token_' . $value['uuid'], $value['token'], $this->config['rememberMeDays'], true, $value['path']);
 				}
+			} else {
+				$this->logger->debug('No Tautulli token received from backend');
 			}
 		}
 		if ($this->config['ssoJellyfin']) {
+			$this->logger->debug('Starting Jellyfin SSO check function');
 			$jellyfinToken = $this->getJellyfinToken($this->getSSOUserFor('jellyfin', $userobj), $password);
 			if ($jellyfinToken) {
 				foreach ($jellyfinToken as $k => $v) {
+					$this->logger->debug('Setting Jellyfin SSO cookie');
 					$this->coookie('set', $k, $v, $this->config['rememberMeDays'], false);
 				}
+			} else {
+				$this->logger->debug('No Jellyfin token received from backend');
 			}
 		}
 		if ($this->config['ssoOverseerr']) {
+			$this->logger->debug('Starting Overseerr SSO check function');
 			$fallback = ($this->config['overseerrFallbackUser'] !== '' && $this->config['overseerrFallbackPassword'] !== '');
 			$overseerrToken = $this->getOverseerrToken($this->getSSOUserFor('overseerr', $userobj), $password, $token, $fallback);
 			if ($overseerrToken) {
+				$this->logger->debug('Setting Overseerr SSO cookie');
 				$this->coookie('set', 'connect.sid', $overseerrToken, $this->config['rememberMeDays'], false);
+			} else {
+				$this->logger->debug('No Overseerr token received from backend');
 			}
 		}
 		if ($this->config['ssoPetio']) {
+			$this->logger->debug('Starting Petio SSO check function');
 			$fallback = ($this->config['petioFallbackUser'] !== '' && $this->config['petioFallbackPassword'] !== '');
 			$petioToken = $this->getPetioToken($this->getSSOUserFor('petio', $userobj), $password, $token, $fallback);
 			if ($petioToken) {
+				$this->logger->debug('Setting Petio SSO cookie');
 				$this->coookie('set', 'petio_jwt', $petioToken, $this->config['rememberMeDays'], false);
+			} else {
+				$this->logger->debug('No Petio token received from backend');
+			}
+		}
+		if ($this->config['ssoKomga'] && $this->qualifyRequest($this->config['ssoKomgaAuth'])) {
+			$this->logger->debug('Starting Komga SSO check function');
+			$komga = $this->getKomgaToken($this->getSSOUserFor('komga', $userobj), $password);
+			if ($komga) {
+				$this->logger->debug('Setting Komga SSO cookie');
+				$this->coookie('set', 'komga_token', $komga, $this->config['rememberMeDays'], false);
+			} else {
+				$this->logger->debug('No Komga token received from backend');
 			}
 		}
 		return true;
 	}
 	
+	public function getKomgaToken($email, $password)
+	{
+		try {
+			$credentials = array('auth' => new Requests_Auth_Digest(array($email, $password)));
+			$url = $this->qualifyURL($this->config['komgaURL']);
+			$options = $this->requestOptions($url, 60000, true, false, $credentials);
+			$response = Requests::get($url . '/api/v1/users/me', ['X-Auth-Token' => 'organizrSSO'], $options);
+			if ($response->success) {
+				if ($response->headers['x-auth-token']) {
+					$this->writeLog('success', 'Komga Token Function - Grabbed token.', $email);
+					return $response->headers['x-auth-token'];
+				} else {
+					$this->writeLog('error', 'Komga Token Function - Komga did not return Token', $email);
+				}
+			} else {
+				$this->writeLog('error', 'Komga Token Function - Komga did not return Token', $email);
+			}
+		} catch (Requests_Exception $e) {
+			$this->writeLog('error', 'Komga Token Function - Error: ' . $e->getMessage(), $email);
+		}
+		return false;
+	}
+	
 	public function getJellyfinToken($username, $password)
 	{
 		$token = null;

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

@@ -2,6 +2,218 @@
 
 trait UpgradeFunctions
 {
+	public function upgradeCheck()
+	{
+		if ($this->hasDB()) {
+			$tempLock = $this->config['dbLocation'] . 'DBLOCK.txt';
+			$updateComplete = $this->config['dbLocation'] . 'completed.txt';
+			$cleanup = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'upgrade' . DIRECTORY_SEPARATOR;
+			if (file_exists($updateComplete)) {
+				@unlink($updateComplete);
+				@$this->rrmdir($cleanup);
+			}
+			if (file_exists($tempLock)) {
+				die($this->showHTML('Upgrading', 'Please wait...'));
+			}
+			$updateDB = false;
+			$updateSuccess = true;
+			$compare = new Composer\Semver\Comparator;
+			$oldVer = $this->config['configVersion'];
+			// Upgrade check start for version below
+			$versionCheck = '2.0.0-beta-200';
+			if ($compare->lessThan($oldVer, $versionCheck)) {
+				$updateDB = true;
+				$oldVer = $versionCheck;
+			}
+			// End Upgrade check start for version above
+			// Upgrade check start for version below
+			$versionCheck = '2.0.0-beta-500';
+			if ($compare->lessThan($oldVer, $versionCheck)) {
+				$updateDB = true;
+				$oldVer = $versionCheck;
+			}
+			// End Upgrade check start for version above
+			// Upgrade check start for version below
+			$versionCheck = '2.0.0-beta-800';
+			if ($compare->lessThan($oldVer, $versionCheck)) {
+				$updateDB = true;
+				$oldVer = $versionCheck;
+			}
+			// End Upgrade check start for version above
+			// Upgrade check start for version below
+			$versionCheck = '2.1.0';
+			if ($compare->lessThan($oldVer, $versionCheck)) {
+				$updateDB = false;
+				$oldVer = $versionCheck;
+				$this->upgradeToVersion($versionCheck);
+			}
+			// End Upgrade check start for version above
+			// Upgrade check start for version below
+			$versionCheck = '2.1.400';
+			if ($compare->lessThan($oldVer, $versionCheck)) {
+				$updateDB = false;
+				$oldVer = $versionCheck;
+				$this->upgradeToVersion($versionCheck);
+			}
+			// End Upgrade check start for version above
+			// Upgrade check start for version below
+			$versionCheck = '2.1.525';
+			if ($compare->lessThan($oldVer, $versionCheck)) {
+				$updateDB = false;
+				$oldVer = $versionCheck;
+				$this->upgradeToVersion($versionCheck);
+			}
+			// End Upgrade check start for version above
+			// Upgrade check start for version below
+			$versionCheck = '2.1.860';
+			if ($compare->lessThan($oldVer, $versionCheck)) {
+				$updateDB = false;
+				$oldVer = $versionCheck;
+				$this->upgradeToVersion($versionCheck);
+			}
+			// End Upgrade check start for version above
+			if ($updateDB == true) {
+				//return 'Upgraded Needed - Current Version '.$oldVer.' - New Version: '.$versionCheck;
+				// Upgrade database to latest version
+				$updateSuccess = $this->updateDB($oldVer);
+			}
+			// Update config.php version if different to the installed version
+			if ($updateSuccess && $this->version !== $this->config['configVersion']) {
+				$this->updateConfig(array('apply_CONFIG_VERSION' => $this->version));
+				$this->setLoggerChannel('Update');
+				$this->logger->debug('Updated config version to ' . $this->version);
+			}
+			if ($updateSuccess == false) {
+				die($this->showHTML('Database update failed', 'Please manually check logs and fix - Then reload this page'));
+			}
+			return true;
+		}
+	}
+	
+	public function addColumnToDatabase($table = '', $columnName = '', $definition = 'TEXT')
+	{
+		if ($table == '' || $columnName == '' || $definition == '') {
+			return false;
+		}
+		if ($this->hasDB()) {
+			$tableInfo = [
+				array(
+					'function' => 'fetchSingle',
+					'query' => array(
+						'SELECT COUNT(*) AS has_column FROM pragma_table_info(?) WHERE name=?',
+						$table,
+						$columnName
+					)
+				)
+			];
+			$query = $this->processQueries($tableInfo);
+			if (!$query) {
+				$columnAlter = [
+					array(
+						'function' => 'query',
+						'query' => ['ALTER TABLE ? ADD ? ' . $definition,
+							$table,
+							$columnName,
+						]
+					)
+				];
+				$AlterQuery = $this->processQueries($columnAlter);
+				if ($AlterQuery) {
+					$query = $this->processQueries($tableInfo);
+					if ($query) {
+						return true;
+					}
+				}
+			} else {
+				return true;
+			}
+		}
+		return false;
+	}
+	
+	public function updateDB($oldVerNum = false)
+	{
+		$tempLock = $this->config['dbLocation'] . 'DBLOCK.txt';
+		if (!file_exists($tempLock)) {
+			touch($tempLock);
+			$migrationDB = 'tempMigration.db';
+			$pathDigest = pathinfo($this->config['dbLocation'] . $this->config['dbName']);
+			if (file_exists($this->config['dbLocation'] . $migrationDB)) {
+				unlink($this->config['dbLocation'] . $migrationDB);
+			}
+			// Create Temp DB First
+			$this->connectOtherDB();
+			$backupDB = $pathDigest['dirname'] . '/' . $pathDigest['filename'] . '[' . date('Y-m-d_H-i-s') . ']' . ($oldVerNum ? '[' . $oldVerNum . ']' : '') . '.bak.db';
+			copy($this->config['dbLocation'] . $this->config['dbName'], $backupDB);
+			$success = $this->createDB($this->config['dbLocation'], true);
+			if ($success) {
+				$response = [
+					array(
+						'function' => 'fetchAll',
+						'query' => array(
+							'SELECT name FROM sqlite_master WHERE type="table"'
+						)
+					),
+				];
+				$tables = $this->processQueries($response);
+				foreach ($tables as $table) {
+					$response = [
+						array(
+							'function' => 'fetchAll',
+							'query' => array(
+								'SELECT * FROM ' . $table['name']
+							)
+						),
+					];
+					$data = $this->processQueries($response);
+					$this->writeLog('success', 'Update Function -  Grabbed Table data for Table: ' . $table['name'], 'Database');
+					foreach ($data as $row) {
+						$response = [
+							array(
+								'function' => 'query',
+								'query' => array(
+									'INSERT into ' . $table['name'],
+									$row
+								)
+							),
+						];
+						$this->processQueries($response, true);
+					}
+					$this->writeLog('success', 'Update Function -  Wrote Table data for Table: ' . $table['name'], 'Database');
+				}
+				$this->writeLog('success', 'Update Function -  All Table data converted - Starting Movement', 'Database');
+				$this->db->disconnect();
+				$this->otherDb->disconnect();
+				// Remove Current Database
+				if (file_exists($this->config['dbLocation'] . $migrationDB)) {
+					$oldFileSize = filesize($this->config['dbLocation'] . $this->config['dbName']);
+					$newFileSize = filesize($this->config['dbLocation'] . $migrationDB);
+					if ($newFileSize > 0) {
+						$this->writeLog('success', 'Update Function -  Table Size of new DB ok..', 'Database');
+						@unlink($this->config['dbLocation'] . $this->config['dbName']);
+						copy($this->config['dbLocation'] . $migrationDB, $this->config['dbLocation'] . $this->config['dbName']);
+						@unlink($this->config['dbLocation'] . $migrationDB);
+						$this->writeLog('success', 'Update Function -  Migrated Old Info to new Database', 'Database');
+						@unlink($tempLock);
+						return true;
+					} else {
+						$this->writeLog('error', 'Update Function -  Filesize is zero', 'Database');
+					}
+				} else {
+					$this->writeLog('error', 'Update Function -  Migration DB does not exist', 'Database');
+				}
+				@unlink($tempLock);
+				return false;
+				
+			} else {
+				$this->writeLog('error', 'Update Function -  Could not create migration DB', 'Database');
+			}
+			@unlink($tempLock);
+			return false;
+		}
+		return false;
+	}
+	
 	public function upgradeToVersion($version = '2.1.0')
 	{
 		switch ($version) {
@@ -12,6 +224,8 @@ trait UpgradeFunctions
 				$this->removeOldPluginDirectoriesAndFiles();
 			case '2.1.525':
 				$this->removeOldCustomHTML();
+			case '2.1.860':
+				$this->upgradeInstalledPluginsConfigItem();
 			default:
 				$this->setAPIResponse('success', 'Ran update function for version: ' . $version, 200);
 				return true;
@@ -50,6 +264,39 @@ trait UpgradeFunctions
 		return $this->processQueries($response);
 	}
 	
+	public function upgradeInstalledPluginsConfigItem()
+	{
+		$oldConfigItem = $this->config['installedPlugins'];
+		if (gettype($oldConfigItem) == 'string') {
+			if ((strpos($oldConfigItem, '|') !== false)) {
+				$newPlugins = [];
+				$plugins = explode('|', $oldConfigItem);
+				foreach ($plugins as $plugin) {
+					$info = explode(':', $plugin);
+					$newPlugins[$info[0]] = [
+						'name' => $info[0],
+						'version' => $info[1],
+						'repo' => 'organizr'
+					];
+				}
+			} else {
+				$newPlugins = [];
+				if ($oldConfigItem !== '') {
+					$info = explode(':', $oldConfigItem);
+					$newPlugins[$info[0]] = [
+						'name' => $info[0],
+						'version' => $info[1],
+						'repo' => 'https://github.com/Organizr/Organizr-Plugins'
+					];
+				}
+			}
+			$this->updateConfig(['installedPlugins' => $newPlugins]);
+		} elseif (gettype($oldConfigItem) == 'array') {
+			$this->updateConfig(['installedPlugins' => $oldConfigItem]);
+		}
+		return true;
+	}
+	
 	public function removeOldPluginDirectoriesAndFiles()
 	{
 		$folders = [

+ 6 - 4
api/homepage/plex.php

@@ -671,10 +671,12 @@ trait PlexHomepageItem
 	{
 		if ($this->config['tautulliURL'] && $this->config['tautulliApikey'] && $this->config['homepageUseCustomStreamNames']) {
 			$names = $this->getTautulliFriendlyNames(true);
-			if (json_encode($names) !== $this->config['homepageCustomStreamNames']) {
-				$this->updateConfig(array('homepageCustomStreamNames' => json_encode($names)));
-				$this->config['homepageCustomStreamNames'] = json_encode($names);
-				$this->debug('Updating Tautulli custom names config item', 'SYSTEM');
+			$names = json_encode($names);
+			if ($names !== $this->config['homepageCustomStreamNames']) {
+				$this->updateConfig(array('homepageCustomStreamNames' => $names));
+				$this->config['homepageCustomStreamNames'] = $names;
+				$this->setLoggerChannel('Tautulli');
+				$this->logger->debug('Updating Tautulli custom names config item', $names);
 			}
 		}
 	}

+ 14 - 5
api/homepage/sabnzbd.php

@@ -49,6 +49,8 @@ trait SabNZBdHomepageItem
 	
 	public function testConnectionSabNZBd()
 	{
+		$this->setLoggerChannel('Sabnzbd Homepage');
+		$this->logger->debug('Starting API Connection Test');
 		if (!empty($this->config['sabnzbdURL']) && !empty($this->config['sabnzbdToken'])) {
 			$url = $this->qualifyURL($this->config['sabnzbdURL']);
 			$url = $url . '/api?mode=queue&output=json&apikey=' . $this->config['sabnzbdToken'];
@@ -66,16 +68,20 @@ trait SabNZBdHomepageItem
 						$message = $data['error'];
 					}
 					$this->setAPIResponse($status, $message, $responseCode, $data);
+					$this->logger->debug('API Connection Test was successful');
 					return true;
 				} else {
 					$this->setAPIResponse('error', $response->body, 500);
+					$this->logger->debug('API Connection Test was unsuccessful');
 					return false;
 				}
 			} catch (Requests_Exception $e) {
+				$this->logger->critical($e, [$url]);
 				$this->setAPIResponse('error', $e->getMessage(), 500);
 				return false;
-			};
+			}
 		} else {
+			$this->logger->debug('URL and/or Token not setup');
 			$this->setAPIResponse('error', 'URL and/or Token not setup', 422);
 			return 'URL and/or Token not setup';
 		}
@@ -121,6 +127,7 @@ trait SabNZBdHomepageItem
 	
 	public function getSabNZBdHomepageQueue()
 	{
+		$this->setLoggerChannel('Sabnzbd Homepage');
 		if (!$this->homepageItemPermissions($this->sabNZBdHomepagePermissions('main'), true)) {
 			return false;
 		}
@@ -133,7 +140,7 @@ trait SabNZBdHomepageItem
 				$api['content']['queueItems'] = json_decode($response->body, true);
 			}
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->logger->critical($e, [$url]);
 			$this->setAPIResponse('error', $e->getMessage(), 500);
 			return false;
 		};
@@ -146,7 +153,7 @@ trait SabNZBdHomepageItem
 				$api['content']['historyItems'] = json_decode($response->body, true);
 			}
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->logger->critical($e, [$url]);
 			$this->setAPIResponse('error', $e->getMessage(), 500);
 			return false;
 		};
@@ -157,6 +164,7 @@ trait SabNZBdHomepageItem
 	
 	public function pauseSabNZBdQueue($target = null)
 	{
+		$this->setLoggerChannel('Sabnzbd Homepage');
 		if (!$this->homepageItemPermissions($this->sabNZBdHomepagePermissions('main'), true)) {
 			return false;
 		}
@@ -170,7 +178,7 @@ trait SabNZBdHomepageItem
 				$api['content'] = json_decode($response->body, true);
 			}
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->logger->critical($e, [$url]);
 			$this->setAPIResponse('error', $e->getMessage(), 500);
 			return false;
 		};
@@ -181,6 +189,7 @@ trait SabNZBdHomepageItem
 	
 	public function resumeSabNZBdQueue($target = null)
 	{
+		$this->setLoggerChannel('Sabnzbd Homepage');
 		if (!$this->homepageItemPermissions($this->sabNZBdHomepagePermissions('main'), true)) {
 			return false;
 		}
@@ -194,7 +203,7 @@ trait SabNZBdHomepageItem
 				$api['content'] = json_decode($response->body, true);
 			}
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'SabNZBd Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->logger->critical($e, [$url]);
 			$this->setAPIResponse('error', $e->getMessage(), 500);
 			return false;
 		};

+ 62 - 3
api/homepage/tautulli.php

@@ -14,6 +14,14 @@ trait TautulliHomepageItem
 		if ($infoOnly) {
 			return $homepageInformation;
 		}
+		$libraryList = [['name' => 'Refresh page to update List', 'value' => '', 'disabled' => true]];
+		if (!empty($this->config['tautulliApikey']) && !empty($this->config['tautulliURL'])) {
+			$libraryList = [];
+			$loop = $this->tautulliLibraryList('key')['libraries'];
+			foreach ($loop as $key => $value) {
+				$libraryList[] = ['name' => $key, 'value' => $value];
+			}
+		}
 		$homepageSettings = [
 			'debug' => true,
 			'settings' => [
@@ -41,6 +49,7 @@ trait TautulliHomepageItem
 				'Library Stats' => [
 					$this->settingsOption('switch', 'tautulliLibraries', ['label' => 'Libraries', 'help' => 'Shows/hides the card with library information.']),
 					$this->settingsOption('auth', 'homepageTautulliLibraryAuth'),
+					$this->settingsOption('plex-library-exclude', 'homepageTautulliLibraryStatsExclude', ['options' => $libraryList]),
 				],
 				'Viewing Stats' => [
 					$this->settingsOption('switch', 'tautulliPopularMovies', ['label' => 'Popular Movies', 'help' => 'Shows/hides the card with Popular Movie information.']),
@@ -48,6 +57,7 @@ trait TautulliHomepageItem
 					$this->settingsOption('switch', 'tautulliTopMovies', ['label' => 'Top Movies', 'help' => 'Shows/hides the card with Top Movies information.']),
 					$this->settingsOption('switch', 'tautulliTopTV', ['label' => 'Top TV', 'help' => 'Shows/hides the card with Top TV information.']),
 					$this->settingsOption('auth', 'homepageTautulliViewsAuth'),
+					$this->settingsOption('plex-library-exclude', 'homepageTautulliViewingStatsExclude', ['options' => $libraryList]),
 				],
 				'Misc Stats' => [
 					$this->settingsOption('switch', 'tautulliTopUsers', ['label' => 'Top Users', 'help' => 'Shows/hides the card with Top Users information.']),
@@ -66,6 +76,7 @@ trait TautulliHomepageItem
 	
 	public function testConnectionTautulli()
 	{
+		$this->setLoggerChannel('Tautulli Homepage');
 		if (empty($this->config['tautulliURL'])) {
 			$this->setAPIResponse('error', 'Tautulli URL is not defined', 422);
 			return false;
@@ -88,7 +99,7 @@ trait TautulliHomepageItem
 				return false;
 			}
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Tautulli Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->logger->critical($e, [$url]);
 			$this->setAPIResponse('error', $e->getMessage(), 500);
 			return false;
 		}
@@ -131,6 +142,7 @@ trait TautulliHomepageItem
 	
 	public function getTautulliHomepageData()
 	{
+		$this->setLoggerChannel('Tautulli Homepage');
 		if (!$this->homepageItemPermissions($this->tautulliHomepagePermissions('main'), true)) {
 			return false;
 		}
@@ -146,7 +158,18 @@ trait TautulliHomepageItem
 			$options = $this->requestOptions($this->config['tautulliURL'], $this->config['homepageTautulliRefresh'], $this->config['tautulliDisableCertCheck'], $this->config['tautulliUseCustomCertificate']);
 			$homestats = Requests::get($homestatsUrl, [], $options);
 			if ($homestats->success) {
+				$homepageTautulliViewingStatsExclude = explode(",",$this->config['homepageTautulliViewingStatsExclude']);
 				$homestats = json_decode($homestats->body, true);
+				foreach ($homestats['response']['data'] as $s => $stats) {
+					foreach ($stats['rows'] as $i => $v) {
+						if (array_key_exists('section_id', $v)) {
+							if (in_array($v['section_id'],$homepageTautulliViewingStatsExclude)) {
+								unset($homestats['response']['data'][$s]['rows'][$i]);
+							}
+						}
+					}
+				}
+				$homestats['response']['data'] = array_values($homestats['response']['data']);
 				$api['homestats'] = $homestats['response'];
 				// Cache art & thumb for first result in each tautulli API result
 				$categories = ['top_movies', 'top_tv', 'popular_movies', 'popular_tv'];
@@ -168,7 +191,16 @@ trait TautulliHomepageItem
 			$options = $this->requestOptions($this->config['tautulliURL'], $this->config['homepageTautulliRefresh'], $this->config['tautulliDisableCertCheck'], $this->config['tautulliUseCustomCertificate']);
 			$libstats = Requests::get($libstatsUrl, [], $options);
 			if ($libstats->success) {
+				$homepageTautulliLibraryStatsExclude = explode(",",$this->config['homepageTautulliLibraryStatsExclude']);
 				$libstats = json_decode($libstats->body, true);
+				foreach ($libstats['response']['data']['data'] as $i => $v) {
+					if (array_key_exists('section_id', $v)) {
+						if (in_array($v['section_id'],$homepageTautulliLibraryStatsExclude)) {
+							unset($libstats['response']['data']['data'][$i]);
+						}
+					}
+				}
+				$libstats['response']['data']['data'] = array_values($libstats['response']['data']['data']);
 				$api['libstats'] = $libstats['response']['data'];
 				$categories = ['movie.svg', 'show.svg', 'artist.svg'];
 				foreach ($categories as $cat) {
@@ -215,7 +247,7 @@ trait TautulliHomepageItem
 				}
 			}
 		} catch (Requests_Exception $e) {
-			$this->writeLog('error', 'Tautulli Connect Function - Error: ' . $e->getMessage(), 'SYSTEM');
+			$this->logger->critical($e, [$url]);
 			$this->setAPIResponse('error', $e->getMessage(), 500);
 			return false;
 		};
@@ -223,4 +255,31 @@ trait TautulliHomepageItem
 		$this->setAPIResponse('success', null, 200, $api);
 		return $api;
 	}
-}
+
+	public function tautulliLibraryList()
+	{
+		$url = $this->qualifyURL($this->config['tautulliURL']);
+		$apiURL = $url . '/api/v2?apikey=' . $this->config['tautulliApikey'];
+		if (!empty($this->config['tautulliApikey']) && !empty($this->config['tautulliURL'])) {
+			$liblistUrl = $apiURL . '&cmd=get_libraries';
+			$options = $this->requestOptions($this->config['tautulliURL'], $this->config['homepageTautulliRefresh'], $this->config['tautulliDisableCertCheck'], $this->config['tautulliUseCustomCertificate']);
+			try {
+				$liblist = Requests::get($liblistUrl, [], $options);
+				$libraryList = array();
+				if ($liblist->success) {
+					$liblist = json_decode($liblist->body, true);
+					foreach ($liblist['response']['data'] as $lib) {
+						$libraryList['libraries'][(string)$lib['section_name']] = (string)$lib["section_id"];
+					}
+					$libraryList = array_change_key_case($libraryList, CASE_LOWER);
+					return $libraryList;
+				}
+			} catch (Requests_Exception $e) {
+				$this->setAPIResponse('error', 'Tautulli Homepage Error - Unable to get list of libraries: '.$e->getMessage(), 500);
+				$this->writeLog('error', 'Tautulli Homepage Error - Unable to get list of libraries: ' . $e->getMessage(), 'SYSTEM');
+				return false;
+			};
+		}
+		return false;
+	}
+}

+ 39 - 37
api/homepage/utorrent.php

@@ -120,16 +120,18 @@ trait uTorrentHomepageItem
 			$dom = new PHPHtmlParser\Dom;
 			$dom->loadStr($response->body);
 			$id = $dom->getElementById('token')->text;
-			$uTorrentConfig = new stdClass();
-			$uTorrentConfig->uTorrentToken = $id;
+			$uTorrentConfig = array (
+				"uTorrentToken" => $id,
+				"uTorrentCookie" => "",
+			);
 			$reflection = new ReflectionClass($response->cookies);
 			$cookie = $reflection->getProperty("cookies");
 			$cookie->setAccessible(true);
 			$cookie = $cookie->getValue($response->cookies);
 			if ($cookie['GUID']) {
-				$uTorrentConfig->uTorrentCookie = $cookie['GUID']->value;
+				$uTorrentConfig['uTorrentCookie'] = $cookie['GUID']->value;
 			}
-			if ($uTorrentConfig->uTorrentToken || $uTorrentConfig->uTorrentCookie) {
+			if ($uTorrentConfig['uTorrentToken'] || $uTorrentConfig['uTorrentCookie']) {
 				$this->updateConfigItems($uTorrentConfig);
 			}
 			
@@ -182,39 +184,39 @@ trait uTorrentHomepageItem
 					} else if ($this->config['uTorrentHideCompleted'] && $Status == "Finished") {
 						// Do Nothing
 					} else {
-                                                $value = array(
-                                                        'Hash' => $keyArr[0],
-                                                        'TorrentStatus' => $keyArr[1],
-                                                        'Name' => $keyArr[2],
-                                                        'Size' => $keyArr[3],
-                                                        'Progress' => $keyArr[4],
-                                                        'Downloaded' => $keyArr[5],
-                                                        'Uploaded' => $keyArr[6],
-                                                        'Ratio' => $keyArr[7],
-                                                        'upSpeed' => $keyArr[8],
-                                                        'downSpeed' => $keyArr[9],
-                                                        'eta' => $keyArr[10],
-                                                        'Labels' => $keyArr[11],
-                                                        'PeersConnected' => $keyArr[12],
-                                                        'PeersInSwarm' => $keyArr[13],
-                                                        'SeedsConnected' => $keyArr[14],
-                                                        'SeedsInSwarm' => $keyArr[15],
-                                                        'Availability' => $keyArr[16],
-                                                        'TorrentQueueOrder' => $keyArr[17],
-                                                        'Remaining' => $keyArr[18],
-                                                        'DownloadUrl' => $keyArr[19],
-                                                        'RssFeedUrl' => $keyArr[20],
-                                                        'Message' => $keyArr[21],
-                                                        'StreamId' => $keyArr[22],
-                                                        'DateAdded' => $keyArr[23],
-                                                        'DateCompleted' => $keyArr[24],
-                                                        'AppUpdateUrl' => $keyArr[25],
-                                                        'RootDownloadPath' => $keyArr[26],
-                                                        'Unknown27' => $keyArr[27],
-                                                        'Unknown28' => $keyArr[28],
-                                                        'Status' => $Status,
-                                                        'Percent' => str_replace(' ', '', $matches['Percentage']),
-                                                );
+						$value = array(
+							'Hash' => $keyArr[0],
+							'TorrentStatus' => $keyArr[1],
+							'Name' => $keyArr[2],
+							'Size' => $keyArr[3],
+							'Progress' => $keyArr[4],
+							'Downloaded' => $keyArr[5],
+							'Uploaded' => $keyArr[6],
+							'Ratio' => $keyArr[7],
+							'upSpeed' => $keyArr[8],
+							'downSpeed' => $keyArr[9],
+							'eta' => $keyArr[10],
+							'Labels' => $keyArr[11],
+							'PeersConnected' => $keyArr[12],
+							'PeersInSwarm' => $keyArr[13],
+							'SeedsConnected' => $keyArr[14],
+							'SeedsInSwarm' => $keyArr[15],
+							'Availability' => $keyArr[16],
+							'TorrentQueueOrder' => $keyArr[17],
+							'Remaining' => $keyArr[18],
+							'DownloadUrl' => $keyArr[19],
+							'RssFeedUrl' => $keyArr[20],
+							'Message' => $keyArr[21],
+							'StreamId' => $keyArr[22],
+							'DateAdded' => $keyArr[23],
+							'DateCompleted' => $keyArr[24],
+							'AppUpdateUrl' => $keyArr[25],
+							'RootDownloadPath' => $keyArr[26],
+							'Unknown27' => $keyArr[27],
+							'Unknown28' => $keyArr[28],
+							'Status' => $Status,
+							'Percent' => str_replace(' ', '', $matches['Percentage']),
+						);
 						array_push($valueArray, $value);
 					}
 				}

+ 1 - 1
api/pages/error.php

@@ -14,7 +14,7 @@ function get_page_error($Organizr)
 	$errorDetails = $Organizr->errorCodes($error);
 	$redirect = $_GET['vars']['var2'] ?? null;
 	if ($redirect) {
-		$Organizr->debug($redirect);
+		$Organizr->logger->debug($redirect);
 	}
 	$GLOBALS['responseCode'] = 200;
 	return '

+ 20 - 0
api/pages/settings-plugins-disabled.php

@@ -0,0 +1,20 @@
+<?php
+$GLOBALS['organizrPages'][] = 'settings_plugins_disabled';
+function get_page_settings_plugins_disabled($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	return '
+<script>
+	buildPlugins("disabled");
+</script>
+<div id="disabled-plugin-area"></div>
+';
+}

+ 25 - 0
api/pages/settings-plugins-enabled.php

@@ -0,0 +1,25 @@
+<?php
+$GLOBALS['organizrPages'][] = 'settings_plugins_enabled';
+function get_page_settings_plugins_enabled($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	return '
+<script>
+	buildPlugins("enabled");
+</script>
+<div id="enabled-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>
+';
+}

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

@@ -0,0 +1,31 @@
+<?php
+$GLOBALS['organizrPages'][] = 'settings_plugins_settings';
+function get_page_settings_plugins_settings($Organizr)
+{
+	if (!$Organizr) {
+		$Organizr = new Organizr();
+	}
+	if ((!$Organizr->hasDB())) {
+		return false;
+	}
+	if (!$Organizr->qualifyRequest(1, true)) {
+		return false;
+	}
+	return '
+<script>
+	buildPluginsSettings();
+</script>
+<div class="panel bg-org panel-info">
+    <div class="panel-heading">
+		<span lang="en">Plugin Settings</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="plugin-settings-form-save" onclick="submitSettingsForm(\'plugin-settings-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="bg-org">
+            <form id="plugin-settings-form" class="addFormTick" onsubmit="return false;"></form>
+        </div>
+    </div>
+</div>
+';
+}

+ 84 - 157
api/pages/settings-settings-logs.php

@@ -11,165 +11,92 @@ function get_page_settings_settings_logs($Organizr)
 	if (!$Organizr->qualifyRequest(1, true)) {
 		return false;
 	}
+	$logsDropdown = $Organizr->buildLogDropdown();
+	$filterDropdown = $Organizr->buildFilterDropdown();
 	return '
-    <script>
-    $(document).on("click", ".swapLog", function(e) {
-    	switch ($(this).attr(\'data-name\')){
-    	case \'loginLog\':
-    		loginLogTable.ajax.reload(null, false);
-    	break;
-    	case \'orgLog\':
-    		organizrLogTable.ajax.reload(null, false);
-    	break;
-    	default:
-    		//nada
-    		//loginLogTable
-    	}
-        var log = $(this).attr(\'data-name\')+\'Div\';
-        $(\'.logTable\').addClass(\'hidden\');
-        $(\'.\'+log).addClass(\'show\').removeClass(\'hidden\');
-    	$(\'.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="' . $Organizr->organizrLoginLog . '" lang="en">Login Log</button>
-        <button type="button" class="btn btn-default btn-outline waves-effect bg-org swapLog" data-name="orgLog" data-path="' . $Organizr->organizrLog . '" lang="en">Organizr Log</button>
-    </div>
-    <button class="btn btn-danger btn-sm waves-effect waves-light pull-right purgeLog" type="button"><span class="btn-label"><i class="fa fa-trash"></i></span><span lang="en">Purge Log</span></button>
-    <div class="clearfix"></div>
-    <div class="white-box bg-org logTable loginLogDiv">
-        <h3 class="box-title m-b-0" lang="en">Login Logs</h3>
-        <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\');
-    $.fn.dataTable.ext.errMode = \'none\';
-    var loginLogTable = $("#loginLogTable")
-    .on( \'error.dt\', function ( e, settings, techNote, message ) {
-        console.log( \'An error has been reported by DataTables: \', message );
-        loginLogTable.draw();
-    } )
-    .DataTable( {
-    		"ajax": {
-				"url": "api/v2/log/login",
-				"dataSrc": function ( json ) {
-					return json.response.data;
+	<div class="btn-group m-b-20 pull-left">' . $logsDropdown . '</div>
+	<button class="btn btn-danger waves-effect waves-light pull-right purgeLog" type="button" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="Purge Log"><i class="fa fa-trash"></i></span></button>
+	<button onclick="organizrLogTable.clear().draw().ajax.reload(null, false)" class="btn btn-info waves-effect waves-light pull-right reloadLog m-r-5" type="button" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="Reload Log"><i class="fa fa-refresh"></i></span></button>
+	<button onclick="toggleKillOrganizrLiveUpdate(' . $Organizr->config['logLiveUpdateRefresh'] . ');" class="btn btn-primary waves-effect waves-light pull-right organizr-log-live-update m-r-5" type="button" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="Live Update"><i class="fa fa-clock-o"></i></span></button>
+	' . $filterDropdown . '
+	<div class="clearfix"></div>
+	<div class="white-box bg-org logTable orgLogDiv">
+		<h3 class="box-title m-b-0" lang="en">Organizr Logs</h3>
+		<div class="table-responsive">
+			<table id="organizrLogTable" class="table table-striped compact nowrap">
+				<thead>
+					<tr>
+						<th lang="en">Date</th>
+						<th lang="en">Severity</th>
+						<th lang="en">Function</th>
+						<th lang="en">Message</th>
+						<th lang="en">IP Address</th>
+						<th lang="en">User</th>
+						<th></th>
+					</tr>
+				</thead>
+				<tbody></tbody>
+			</table>
+		</div>
+	</div>
+	<!-- /.container-fluid -->
+	<script>
+	clearTimeout(timeouts[\'organizr-log\']);
+	$.fn.dataTable.ext.errMode = "none";
+	var organizrLogTable = $("#organizrLogTable")
+	.on("error.dt", function(e, settings, techNote, message) {
+		console.log("An error has been reported by DataTables: ", message);
+		organizrLogTable.draw();
+	})
+	.DataTable({
+		"ajax": {
+			"url": "api/v2/log/0?filter=NONE&pageSize=1000&offset=0",
+			"dataSrc": function(json) {
+				return json.response.data.results;
+			}
+		},
+		"deferRender": true,
+		"pageLength": ' . (int)$Organizr->config['logPageSize'] . ',
+		"columns": [{
+			data: "datetime",
+			render: function(data, type, row) {
+				if (type === "display" || type === "filter") {
+					var m = moment.tz(data + "Z", activeInfo.timezone);
+					return moment(m).format("LLL");
 				}
-			},
-            "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\',
-                    render: function ( data, type, row ) {
-                        return ipInfoSpan(data);
-                    }
-                },
-                { data: \'auth_type\',
-                    render: function ( data, type, row ) {
-                        if ( type === \'display\' || type === \'filter\' ) {
-                            return logIcon(data);
-                        }
-                        return logIcon(data);
-                    }
-                }
-            ],
-            "order": [[ 0, \'desc\' ]],
-    } );
-    var organizrLogTable = $("#organizrLogTable")
-    .on( \'error.dt\', function ( e, settings, techNote, message ) {
-        console.log( \'An error has been reported by DataTables: \', message );
-        organizrLogTable.draw();
-    } )
-    .DataTable( {
-            "ajax": {
-				"url": "api/v2/log/organizr",
-				"dataSrc": function ( json ) {
-					return json.response.data;
+				return data;
+			}
+		}, {
+			data: "log_level",
+			render: function(data, type, row) {
+				if (type === "display" || type === "filter") {
+					return logIcon(data);
 				}
+				return logIcon(data);
+			}
+		}, {
+			data: "channel"
+		}, {
+			data: "message"
+		}, {
+			data: "remote_ip_address",
+			"width": "5%",
+			render: function(data, type, row) {
+				return ipInfoSpan(data);
+			}
+		}, {
+			"data": "trace_id"
+		}, {
+			data: "context",
+			render: function(data, type, row) {
+				return logContext(row);
 			},
-                "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\',
-                    render: function ( data, type, row ) {
-                        return ipInfoSpan(data);
-                    }
-                },
-                { "data": "message" },
-                { data: \'type\',
-                    render: function ( data, type, row ) {
-                        if ( type === \'display\' || type === \'filter\' ) {
-                            return logIcon(data);
-                        }
-                        return logIcon(data);
-                    }
-                }
-            ],
-            "order": [[ 0, \'desc\' ]],
-    } );
-    </script>
-    ';
+			orderable: false
+		}, ],
+		"order": [
+			[0, "desc"]
+		],
+	})
+	</script>
+	';
 }

+ 47 - 4
api/pages/settings.php

@@ -11,7 +11,8 @@ function get_page_settings($Organizr)
 	if (!$Organizr->qualifyRequest(1, true)) {
 		return false;
 	}
-	$Organizr->writeLog('success', 'Admin Function -  Accessed Settings Page', $Organizr->user['username']);
+	$Organizr->setLoggerChannel('Organizr');
+	$Organizr->logger->info('Accessed admin settings page');
 	$systemMenus = $Organizr->systemMenuLists();
 	return $Organizr->pluginFiles('js', true) . $Organizr->loadJavascriptFile('js/Sortable.min.js') . '
 <script>
@@ -51,7 +52,7 @@ function get_page_settings($Organizr)
 						<li onclick="changeSettingsMenu(\'Settings::Customize\')" id="settings-main-customize-anchor"><a href="#settings-main-customize" class="sticon ti-paint-bucket"><span lang="en">Customize</span></a></li>
 						<li onclick="changeSettingsMenu(\'Settings::User Management\')" id="settings-main-user-management-anchor"><a href="#settings-main-user-management" class="sticon ti-user"><span lang="en">User Management</span></a></li>
 						<li onclick="changeSettingsMenu(\'Settings::Image Manager\');loadSettingsPage2(\'api/v2/page/settings_image_manager\',\'#settings-image-manager-view\',\'Image Viewer\');" id="settings-main-image-manager-anchor"><a href="#settings-main-image-manager" class="sticon ti-image"><span lang="en">Image Manager</span></a></li>
-						<li onclick="changeSettingsMenu(\'Settings::Plugins\');loadSettingsPage2(\'api/v2/page/settings_plugins\',\'#settings-main-plugins\',\'Plugins\');" id="settings-main-plugins-anchor"><a href="#settings-main-plugins" class="sticon ti-plug"><span lang="en">Plugins</span></a></li>
+						<li onclick="changeSettingsMenu(\'Settings::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>
@@ -104,7 +105,9 @@ function get_page_settings($Organizr)
 														<th lang="en" style="text-align:center">DELETE</th>
 													</tr>
 												</thead>
-												<tbody id="manageThemeTable"></tbody>
+												<tbody id="manageThemeTable">
+													<td class="text-center" colspan="12"><i class="fa fa-spin fa-spinner"></i></td>
+												</tbody>
 											</table>
 										</div>
 									</div>
@@ -143,7 +146,47 @@ function get_page_settings($Organizr)
 					</section>
 					<! -- PLUGINS -->
 					<section id="settings-main-plugins">
-						<h2 lang="en">Plugins</h2>
+						' . $systemMenus['plugins'] . '
+						<!-- Tab panes -->
+						<div class="tab-content">
+							<div role="tabpanel" class="tab-pane fade" id="settings-plugins-enabled">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-plugins-disabled">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-plugins-settings">
+								<h2 lang="en">Loading...</h2>
+								<div class="clearfix"></div>
+							</div>
+							<div role="tabpanel" class="tab-pane fade" id="settings-plugins-marketplace">
+								<div class="panel bg-org panel-info">
+									<div class="panel-heading">
+										<span lang="en">Plugin 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">PLUGIN</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="managePluginTable"><td class="text-center" colspan="12"><i class="fa fa-spin fa-spinner"></i></td></tbody>
+											</table>
+										</div>
+									</div>
+								</div>
+							</div>
+						</div>
 					</section>
 					<! -- SYSTEM SETTINGS -->
 					<section id="settings-main-system-settings">

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

@@ -1,13 +1,13 @@
 <?php
 // PLUGIN INFORMATION
-$GLOBALS['plugins'][]['Bookmark'] = array( // Plugin Name
+$GLOBALS['plugins']['Bookmark'] = array( // Plugin Name
 	'name' => 'Bookmark', // Plugin Name
 	'author' => 'leet1994', // Who wrote the plugin
 	'category' => 'Utilities', // One to Two Word Description
 	'link' => '', // Link to plugin info
 	'license' => 'personal,business', // License Type use , for multiple
 	'idPrefix' => 'BOOKMARK', // html element id prefix
-	'configPrefix' => 'BOOKMARK', // config file prefix for array items without the hypen
+	'configPrefix' => 'BOOKMARK', // config file prefix for array items without the hyphen
 	'dbPrefix' => 'BOOKMARK', // db prefix
 	'version' => '0.1.0', // SemVer of plugin
 	'image' => 'api/plugins/bookmark/logo.png', // 1:1 non transparent image for plugin

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

@@ -2,14 +2,14 @@
 // PLUGIN INFORMATION
 use Pusher\PusherException;
 
-$GLOBALS['plugins'][]['Chat'] = array( // Plugin Name
+$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
 	'idPrefix' => 'CHAT', // html element id prefix
-	'configPrefix' => 'CHAT', // config file prefix for array items without the hypen
+	'configPrefix' => 'CHAT', // config file prefix for array items without the hyphen
 	'version' => '1.0.0', // SemVer of plugin
 	'image' => 'api/plugins/chat/logo.png', // 1:1 non transparent image for plugin
 	'settings' => true, // does plugin need a settings modal?

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

@@ -1,6 +1,6 @@
 <?php
 // PLUGIN INFORMATION
-$GLOBALS['plugins'][]['HealthChecks'] = array( // Plugin Name
+$GLOBALS['plugins']['HealthChecks'] = array( // Plugin Name
 	'name' => 'HealthChecks', // Plugin Name
 	'author' => 'CauseFX', // Who wrote the plugin
 	'category' => 'Utilities', // One to Two Word Description

+ 4 - 4
api/plugins/invites/api.php

@@ -239,7 +239,7 @@ $app->get('/plugins/invites', function ($request, $response, $args) {
 	 */
 	$Invites = new Invites();
 	if ($Invites->checkRoute($request)) {
-		if ($Invites->qualifyRequest(1, true)) {
+		if ($Invites->qualifyRequest($Invites->config['INVITES-Auth-include'], true)) {
 			$GLOBALS['api']['response']['data'] = $Invites->_invitesPluginGetCodes();
 		}
 	}
@@ -265,7 +265,7 @@ $app->post('/plugins/invites', function ($request, $response, $args) {
 	 */
 	$Invites = new Invites();
 	if ($Invites->checkRoute($request)) {
-		if ($Invites->qualifyRequest(1, true)) {
+		if ($Invites->qualifyRequest($Invites->config['INVITES-Auth-include'], true)) {
 			$Invites->_invitesPluginCreateCode($Invites->apiData($request));
 		}
 	}
@@ -371,7 +371,7 @@ $app->delete('/plugins/invites/{code}', function ($request, $response, $args) {
 	 */
 	$Invites = new Invites();
 	if ($Invites->checkRoute($request)) {
-		if ($Invites->qualifyRequest(1, true)) {
+		if ($Invites->qualifyRequest($Invites->config['INVITES-Auth-include'], true)) {
 			$Invites->_invitesPluginDeleteCode($args['code']);
 		}
 	}
@@ -379,4 +379,4 @@ $app->delete('/plugins/invites/{code}', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
-});
+});

+ 5 - 1
api/plugins/invites/config.php

@@ -1,10 +1,14 @@
 <?php
 return array(
 	'INVITES-enabled' => false,
+	'INVITES-Auth-include' => '1',
+	'INVITES-dbVersion' => '1.0.0',
 	'INVITES-type-include' => 'plex',
 	'INVITES-plexLibraries' => '',
 	'INVITES-EmbyTemplate' => '',
 	'INVITES-plex-tv-labels' => '',
 	'INVITES-plex-music-labels' => '',
-	'INVITES-plex-movies-labels' => ''
+	'INVITES-plex-movies-labels' => '',
+	'INVITES-allow-delete-include' => false,
+	'INVITES-maximum-invites' => '0'
 );

+ 9 - 4
api/plugins/invites/main.js

@@ -13,7 +13,7 @@ function inviteLaunch(){
 	</div>
 	`;
 	if(activeInfo.plugins["INVITES-enabled"] == true){
-		if (activeInfo.user.loggedin === true && activeInfo.user.groupID <= 1) {
+		if (activeInfo.user.loggedin === true && activeInfo.user.groupID <= activeInfo.plugins.includes["INVITES-Auth-include"]) {
 			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">
@@ -301,8 +301,9 @@ function buildInvites(array){
 			<td>`+v.dateused+`</td>
 			<td>`+v.usedby+`</td>
 			<td>`+v.ip+`</td>
+			<td>`+v.invitedby+`</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.code+`','`+v.id+`');"><i class="ti-trash"></i></button></td>
+			<td class="deleteButton"><button type="button" class="btn btn-danger btn-outline btn-circle btn-lg m-r-5" onclick="deleteInvite('`+v.code+`','`+v.id+`');"><i class="ti-trash"></i></button></td>
 		</tr>
 		`;
 	});
@@ -310,7 +311,7 @@ function buildInvites(array){
 }
 $(document).on('click', '.inviteModal', function() {
 	var htmlDOM = '';
-	if (activeInfo.user.loggedin === true && activeInfo.user.groupID <= 1) {
+	if (activeInfo.user.loggedin === true && activeInfo.user.groupID <= activeInfo.plugins.includes["INVITES-Auth-include"]) {
 		ajaxloader(".content-wrap","in");
 		organizrAPI2('GET','api/v2/plugins/invites').success(function(data) {
 			var response = data.response;
@@ -334,8 +335,9 @@ $(document).on('click', '.inviteModal', function() {
 									<th lang="en">DATE USED</th>
 									<th lang="en">USED BY</th>
 									<th lang="en">IP ADDRESS</th>
+									<th lang="en">INVITED BY</th>
 									<th lang="en">VALID</th>
-									<th lang="en">DELETE</th>
+									<th lang="en" class="deleteButton">DELETE</th>
 								</tr>
 							</thead>
 							<tbody id="manageInviteTable">
@@ -348,6 +350,9 @@ $(document).on('click', '.inviteModal', function() {
 			<div class="clearfix"></div>
 			`;
 			$('.invite-div').html(htmlDOM);
+			if (activeInfo.plugins.includes["INVITES-allow-delete-include"] === false && activeInfo.user.groupID > 1) {
+				$('.deleteButton').hide();
+			}
 		}).fail(function(xhr) {
 			console.error("Organizr Function: API Connection Failed");
 		});

+ 133 - 21
api/plugins/invites/plugin.php

@@ -1,14 +1,14 @@
 <?php
 // PLUGIN INFORMATION
-$GLOBALS['plugins'][]['Invites'] = array( // Plugin Name
+$GLOBALS['plugins']['Invites'] = array( // Plugin Name
 	'name' => 'Invites', // Plugin Name
 	'author' => 'CauseFX', // Who wrote the plugin
 	'category' => 'Management', // One to Two Word Description
 	'link' => '', // Link to plugin info
 	'license' => 'personal', // License Type use , for multiple
 	'idPrefix' => 'INVITES', // html element id prefix
-	'configPrefix' => 'INVITES', // config file prefix for array items without the hypen
-	'version' => '1.0.0', // SemVer of plugin
+	'configPrefix' => 'INVITES', // config file prefix for array items without the hyphen
+	'version' => '1.1.0', // SemVer of plugin
 	'image' => 'api/plugins/invites/logo.png', // 1:1 non transparent image for plugin
 	'settings' => true, // does plugin need a settings modal?
 	'bind' => true, // use default bind to make settings page - true or false
@@ -18,14 +18,77 @@ $GLOBALS['plugins'][]['Invites'] = array( // Plugin Name
 
 class Invites extends Organizr
 {
+	public function __construct()
+	{
+		parent::__construct();
+		$this->_pluginUpgradeCheck();
+	}
+	
+	public function _pluginUpgradeCheck()
+	{
+		if ($this->hasDB()) {
+			$compare = new Composer\Semver\Comparator;
+			$oldVer = $this->config['INVITES-dbVersion'];
+			// Upgrade check start for version below
+			$versionCheck = '1.1.0';
+			if ($compare->lessThan($oldVer, $versionCheck)) {
+				$oldVer = $versionCheck;
+				$this->_pluginUpgradeToVersion($versionCheck);
+			}
+			// End Upgrade check start for version above
+			// Update config.php version if different to the installed version
+			if ($GLOBALS['plugins']['Invites']['version'] !== $this->config['INVITES-dbVersion']) {
+				$this->updateConfig(array('INVITES-dbVersion' => $oldVer));
+				$this->setLoggerChannel('Invites Plugin');
+				$this->logger->debug('Updated INVITES-dbVersion to ' . $oldVer);
+			}
+			return true;
+		}
+	}
+	
+	public function _pluginUpgradeToVersion($version = '1.1.0')
+	{
+		switch ($version) {
+			case '1.1.0':
+				$this->_addInvitedByColumnToDatabase();
+				break;
+		}
+		$this->setResponse(200, 'Ran plugin update function for version: ' . $version);
+		return true;
+	}
+	
+	public function _addInvitedByColumnToDatabase()
+	{
+		$addColumn = $this->addColumnToDatabase('invites', 'invitedby', 'TEXT');
+		$this->setLoggerChannel('Invites Plugin');
+		if ($addColumn) {
+			$this->logger->info('Updated Invites Database');
+		} else {
+			$this->logger->warning('Could not update Invites Database');
+		}
+	}
+	
 	public function _invitesPluginGetCodes()
 	{
-		$response = [
-			array(
-				'function' => 'fetchAll',
-				'query' => 'SELECT * FROM invites'
-			)
-		];
+		if ($this->qualifyRequest(1, false)) {
+			$response = [
+				array(
+					'function' => 'fetchAll',
+					'query' => 'SELECT * FROM invites'
+				)
+			];
+		} else {
+			$response = [
+				array(
+					'function' => 'fetchAll',
+					'query' => array (
+						'SELECT * FROM invites WHERE invitedby = ?',
+						$this->user['username']
+					)
+				)
+			];
+		}
+
 		return $this->processQueries($response);
 	}
 	
@@ -34,6 +97,14 @@ class Invites extends Organizr
 		$code = ($array['code']) ?? null;
 		$username = ($array['username']) ?? null;
 		$email = ($array['email']) ?? null;
+		$invites = $this->_invitesPluginGetCodes();
+		$inviteCount = count($invites);
+		if (!$this->qualifyRequest(1, false)) {
+			if ($this->config['INVITES-maximum-invites'] != 0 && $inviteCount >= $this->config['INVITES-maximum-invites']) {
+			$this->setAPIResponse('error', 'Maximum number of invites reached', 409);
+			return false;
+			}
+		}
 		if (!$code) {
 			$this->setAPIResponse('error', 'Code not supplied', 409);
 			return false;
@@ -52,6 +123,7 @@ class Invites extends Organizr
 			'username' => $username,
 			'valid' => 'Yes',
 			'type' => $this->config['INVITES-type-include'],
+			'invitedby' => $this->user['username'],
 		];
 		$response = [
 			array(
@@ -113,15 +185,33 @@ class Invites extends Organizr
 	
 	public function _invitesPluginDeleteCode($code)
 	{
-		$response = [
-			array(
-				'function' => 'fetch',
-				'query' => array(
-					'SELECT * FROM invites WHERE code = ? COLLATE NOCASE',
-					$code
+		if ($this->qualifyRequest(1, false)) {
+			$response = [
+				array(
+					'function' => 'fetch',
+					'query' => array(
+						'SELECT * FROM invites WHERE code = ? COLLATE NOCASE',
+						$code
+					)
 				)
-			)
-		];
+			];
+		} else {
+			if ($this->config['INVITES-allow-delete']) {
+				$response = [
+					array(
+						'function' => 'fetch',
+						'query' => array(
+							'SELECT * FROM invites WHERE invitedby = ? AND code = ? COLLATE NOCASE',
+							$this->user['username'],
+							$code
+						)
+					)
+				];
+			} else {
+				$this->setAPIResponse('error', 'You are not permitted to delete invites.', 409);
+				return false;
+			}
+		}
 		$info = $this->processQueries($response);
 		if (!$info) {
 			$this->setAPIResponse('error', 'Code not found', 404);
@@ -196,7 +286,7 @@ class Invites extends Organizr
 								$noLongerId = 0;
 								$libraries = explode(',', $this->config['INVITES-plexLibraries']);
 								foreach ($libraries as $child) {
-									if ($this->search_for_value($child, $libraryList)) {
+									if (!$this->search_for_value($child, $libraryList)) {
 										$libraryList['libraries']['No Longer Exists - ' . $noLongerId] = $child;
 										$noLongerId++;
 									}
@@ -258,7 +348,29 @@ class Invites extends Organizr
 							'value' => 'emby'
 						)
 					)
-				)
+				),
+				array(
+					'type' => 'select',
+					'name' => 'INVITES-Auth-include',
+					'label' => 'Minimum Authentication',
+					'value' => $this->config['INVITES-Auth-include'],
+					'options' => $this->groupSelect()
+				),
+				array(
+					'type' => 'switch',
+					'name' => 'INVITES-allow-delete-include',
+					'label' => 'Allow users to delete invites',
+					'help' => 'This must be disabled to enforce invitation limits.',
+					'value' => $this->config['INVITES-allow-delete-include']
+				),
+				array(
+					'type' => 'number',
+					'name' => 'INVITES-maximum-invites',
+					'label' => 'Maximum number of invites permitted for users.',
+					'help' => 'Set to 0 to disable the limit.',
+					'value' => $this->config['INVITES-maximum-invites'],
+					'placeholder' => '0'
+				),
 			),
 			'Plex Settings' => array(
 				array(
@@ -292,7 +404,7 @@ class Invites extends Organizr
 				array(
 					'type' => 'select2',
 					'class' => 'select2-multiple',
-					'id' => 'invite-select',
+					'id' => 'invite-select-' . $this->random_ascii_string(6),
 					'name' => 'INVITES-plexLibraries',
 					'label' => 'Libraries',
 					'value' => $this->config['INVITES-plexLibraries'],
@@ -483,5 +595,5 @@ class Invites extends Organizr
 		}
 		return (!empty($plexUser) ? $plexUser : null);
 	}
-	
+
 }

+ 2 - 2
api/plugins/php-mailer/main.js

@@ -230,7 +230,7 @@ $(document).on('click', '.phpmSendTestEmail', function() {
 	organizrAPI2('GET','api/v2/plugins/php-mailer/email/test').success(function(data) {
 		var response = data.response;
 		if(response.message !== null && response.message.indexOf('|||DEBUG|||') == 0){
-			messageSingle('',window.lang.translate('Press F11 to check Console for output'),activeInfo.settings.notifications.position,'#FFF','warning','5000');
+			messageSingle('',window.lang.translate('Press F12 to check Console for output'),activeInfo.settings.notifications.position,'#FFF','warning','5000');
 			console.warn(response.message);
 		}else if(response.result == 'success') {
 			messageSingle('',window.lang.translate('Email Test Successful'),activeInfo.settings.notifications.position,'#FFF','success','20000');
@@ -241,4 +241,4 @@ $(document).on('click', '.phpmSendTestEmail', function() {
 		OrganizrApiError(xhr, 'Mailer Error');
 	});
 	ajaxloader();
-});
+});

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

@@ -1,6 +1,6 @@
 <?php
 // PLUGIN INFORMATION
-$GLOBALS['plugins'][]['PHP Mailer'] = array( // Plugin Name
+$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

+ 2 - 2
api/plugins/speedTest/plugin.php

@@ -1,13 +1,13 @@
 <?php
 // PLUGIN INFORMATION
-$GLOBALS['plugins'][]['SpeedTest'] = array( // Plugin Name
+$GLOBALS['plugins']['SpeedTest'] = array( // Plugin Name
 	'name' => 'SpeedTest', // 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
 	'idPrefix' => 'SPEEDTEST', // html element id prefix
-	'configPrefix' => 'SPEEDTEST', // config file prefix for array items without the hypen
+	'configPrefix' => 'SPEEDTEST', // config file prefix for array items without the hyphen
 	'version' => '1.0.0', // SemVer of plugin
 	'image' => 'api/plugins/speedTest/logo.png', // 1:1 non transparent image for plugin
 	'settings' => true, // does plugin need a settings modal?

+ 9 - 4
api/v2/routes/log.php

@@ -1,9 +1,13 @@
 <?php
-$app->get('/log/{log}', function ($request, $response, $args) {
+$app->get('/log[/{number}]', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
 	if ($Organizr->checkRoute($request)) {
 		if ($Organizr->qualifyRequest(1, true)) {
-			$GLOBALS['api']['response']['data'] = $Organizr->getLog($args['log']);
+			$args['number'] = $args['number'] ?? 0;
+			$_GET['pageSize'] = $_GET['pageSize'] ?? 1000;
+			$_GET['offset'] = $_GET['offset'] ?? 0;
+			$_GET['filter'] = $_GET['filter'] ?? 'NONE';
+			$Organizr->getLog($_GET['pageSize'], $_GET['offset'], $_GET['filter'], $args['number']);
 		}
 	}
 	$response->getBody()->write(jsonE($GLOBALS['api']));
@@ -11,11 +15,12 @@ $app->get('/log/{log}', function ($request, $response, $args) {
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 });
-$app->delete('/log/{log}', function ($request, $response, $args) {
+$app->delete('/log[/{number}]', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
 	if ($Organizr->checkRoute($request)) {
 		if ($Organizr->qualifyRequest(1, true)) {
-			$Organizr->purgeLog($args['log']);
+			$args['number'] = $args['number'] ?? 0;
+			$Organizr->purgeLog($args['number']);
 		}
 	}
 	$response->getBody()->write(jsonE($GLOBALS['api']));

+ 33 - 0
api/v2/routes/plugins.php

@@ -14,6 +14,26 @@ $app->get('/plugins', function ($request, $response, $args) {
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 });
+$app->get('/plugins/disabled', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->checkRoute($request)) {
+		$GLOBALS['api']['response']['data'] = $Organizr->getPlugins('disabled');
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
+$app->get('/plugins/enabled', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->checkRoute($request)) {
+		$GLOBALS['api']['response']['data'] = $Organizr->getPlugins('enabled');
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
 $app->post('/plugins/manage/{plugin}', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
 	if ($Organizr->qualifyRequest(1, true)) {
@@ -33,4 +53,17 @@ $app->delete('/plugins/manage/{plugin}', function ($request, $response, $args) {
 	return $response
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
+});
+$app->get('/plugins/marketplace', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->checkRoute($request)) {
+		if ($Organizr->qualifyRequest(1, true)) {
+			$GLOBALS['api']['response']['data'] = $Organizr->getPluginsMarketplace();
+		}
+		
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
 });

+ 10 - 0
api/v2/routes/settings.php

@@ -9,6 +9,16 @@ $app->get('/settings/appearance', function ($request, $response, $args) {
 		->withHeader('Content-Type', 'application/json;charset=UTF-8')
 		->withStatus($GLOBALS['responseCode']);
 });
+$app->get('/settings/plugin', function ($request, $response, $args) {
+	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
+	if ($Organizr->qualifyRequest(1, true)) {
+		$GLOBALS['api']['response']['data'] = $Organizr->getPluginSettings();
+	}
+	$response->getBody()->write(jsonE($GLOBALS['api']));
+	return $response
+		->withHeader('Content-Type', 'application/json;charset=UTF-8')
+		->withStatus($GLOBALS['responseCode']);
+});
 $app->get('/settings/main', function ($request, $response, $args) {
 	$Organizr = ($request->getAttribute('Organizr')) ?? new Organizr();
 	if ($Organizr->qualifyRequest(1, true)) {

+ 90 - 0
api/vendor/bcremer/line-reader/.github/workflows/build.yaml

@@ -0,0 +1,90 @@
+
+# https://docs.github.com/en/actions
+
+name: Build
+
+on:
+  pull_request:
+  push:
+    branches:
+      - main
+
+jobs:
+  unit-tests:
+    name: Tests
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        php-version:
+          - 7.3
+          - 7.4
+          - 8.0
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      - name: Install PHP
+        uses: shivammathur/setup-php@v2
+        with:
+          coverage: none
+          php-version: ${{ matrix.php-version }}
+
+      - name: Determine composer cache directory
+        id: determine-composer-cache-directory
+        run: echo "::set-output name=directory::$(composer config cache-dir)"
+
+      - name: Cache dependencies installed with composer
+        uses: actions/cache@v2
+        with:
+          path: ${{ steps.determine-composer-cache-directory.outputs.directory }}
+          key: php-${{ matrix.php-version }}-composer-${{ hashFiles('composer.json') }}
+          restore-keys: php-${{ matrix.php-version }}-composer-
+
+      - name: Install dependencies
+        run: composer install --no-interaction --no-progress
+
+      - name: Run phpunit/phpunit
+        run: vendor/bin/phpunit
+
+  tests-with-coverage:
+    name: "Tests with coverage and PR Comments"
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        php-version:
+          - 7.4
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      - name: Install PHP
+        uses: shivammathur/setup-php@v2
+        with:
+          coverage: pcov
+          php-version: ${{ matrix.php-version }}
+
+      - name: Setup problem matchers for PHP
+        run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"
+
+      - name: Setup problem matchers for PHPUnit
+        run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
+
+      - name: Determine composer cache directory
+        id: determine-composer-cache-directory
+        run: echo "::set-output name=directory::$(composer config cache-dir)"
+
+      - name: Cache dependencies installed with composer
+        uses: actions/cache@v2
+        with:
+          path: ${{ steps.determine-composer-cache-directory.outputs.directory }}
+          key: php-${{ matrix.php-version }}-composer-${{ hashFiles('composer.json') }}
+          restore-keys: php-${{ matrix.php-version }}-composer-
+
+      - name: Install dependencies
+        run: composer install --no-interaction --no-progress
+
+      - name: Run phpunit/phpunit with code coverage
+        run: vendor/bin/phpunit --coverage-text --coverage-clover=clover.xml --coverage-xml=coverage/coverage.xml --log-junit=coverage/junit.xml
+
+      - name: Run infection
+        run: vendor/bin/infection --threads=4 --min-msi=81 --min-covered-msi=81 --coverage=coverage

+ 6 - 0
api/vendor/bcremer/line-reader/.gitignore

@@ -0,0 +1,6 @@
+vendor
+composer.lock
+clover.xml
+tests/testfile_*.txt
+infection-log.txt
+.phpunit.result.cache

+ 30 - 0
api/vendor/bcremer/line-reader/CHANGELOG.md

@@ -0,0 +1,30 @@
+## 1.1.0
+- Introduced support for PHP 7.4
+- Introduced support for PHP 8.0
+- Bumped minimum PHP Version to 7.3
+- Introduced github actions
+
+## 1.0.1
+
+- Introduced support for PHP 7.3
+
+## 1.0.0
+
+- First stable release
+- Bumped minimum PHP Version to 7.1
+- Introduced support for PHP 7.2
+- Adde scalar type hints
+
+## 0.2.0
+
+- Introduced support for PHP 7.1
+- Removed support for HHVM
+- Added humbug mutation testing
+- Fixed behaviour of trailing and leading newlines (Commit 95af4396e01a9294a9e82969192962220c1af0bd)
+- Exceptions are thrown even before the generator is read (Commit 54094f0c90772620ca24c636aaafb617d471bc68)
+
+## 0.1.0
+
+- Initial Release
+
+

+ 20 - 0
api/vendor/bcremer/line-reader/LICENSE

@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Benjamin Cremer
+
+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.

+ 134 - 0
api/vendor/bcremer/line-reader/README.md

@@ -0,0 +1,134 @@
+# LineReader
+
+[![Latest Version on Packagist][ico-version]][link-packagist]
+[![Software License][ico-license]](LICENSE.md)
+[![Build Status][ico-ghactions]][link-ghactions]
+
+LineReader is a library to read large files line by line in a memory efficient (constant) way.
+
+## Install
+
+Via Composer
+
+```bash
+$ composer require bcremer/line-reader
+```
+
+## Usage
+
+Given we have a textfile (`some/file.txt`) with lines like:
+
+```
+Line 1
+Line 2
+Line 3
+Line 4
+Line 5
+Line 6
+Line 7
+Line 8
+Line 9
+Line 10
+```
+
+Also let's assume the namespace is imported to keep the examples dense:
+
+```
+use Bcremer\LineReader\LineReader;
+```
+
+### Read forwards
+
+```php
+foreach (LineReader::readLines('some/file.txt') as $line) {
+    echo $line . "\n"
+}
+```
+
+The output will be:
+
+```
+Line 1
+Line 2
+Line 3
+Line 4
+Line 5
+...
+```
+
+To set an offset or a limit use the `\LimitIterator`:
+
+```php
+$lineGenerator = LineReader::readLines('some/file.txt');
+$lineGenerator = new \LimitIterator($lineGenerator, 2, 5);
+foreach ($lineGenerator as $line) {
+    echo $line . "\n"
+}
+```
+
+Will output line 3 to 7
+
+```
+Line 3
+Line 4
+Line 5
+Line 6
+Line 7
+```
+
+### Read backwards
+
+```php
+foreach (LineReader::readLinesBackwards('some/file.txt') as $line) {
+    echo $line;
+}
+```
+
+```
+Line 10
+Line 9
+Line 8
+Line 7
+Line 6
+...
+```
+
+Example: Read the last 5 lines in forward order:
+
+```php
+$lineGenerator = LineReader::readLinesBackwards('some/file.txt');
+$lineGenerator = new \LimitIterator($lineGenerator, 0, 5);
+
+$lines = array_reverse(iterator_to_array($lineGenerator));
+foreach ($line as $line) {
+    echo $line;
+}
+```
+
+```
+Line 6
+Line 7
+Line 8
+Line 9
+Line 10
+```
+
+## Testing
+
+```bash
+$ composer test
+```
+
+```bash
+$ TEST_MAX_LINES=200000 composer test
+```
+
+## License
+
+The MIT License (MIT). Please see [License File](LICENSE) for more information.
+
+[ico-version]: https://img.shields.io/packagist/v/bcremer/line-reader.svg?style=flat-square
+[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square
+[link-packagist]: https://packagist.org/packages/bcremer/line-reader
+[ico-ghactions]: https://github.com/bcremer/LineReader/workflows/Build/badge.svg
+[link-ghactions]: https://github.com/bcremer/LineReader/actions?query=workflow%3ABuild

+ 32 - 0
api/vendor/bcremer/line-reader/composer.json

@@ -0,0 +1,32 @@
+{
+    "name": "bcremer/line-reader",
+    "description": "Read large files line by line in a memory efficient (constant) way.",
+    "type": "library",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Benjamin Cremer",
+            "email": "bc@benjamin-cremer.de"
+        }
+    ],
+    "require": {
+        "php": "^7.3|^7.4|^8.0"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^9.4",
+        "infection/infection": "^0.18"
+    },
+    "autoload": {
+        "psr-4": {
+            "Bcremer\\LineReader\\": "src/"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "Bcremer\\LineReaderTests\\": "tests/"
+        }
+    },
+    "scripts": {
+        "test": "phpunit"
+    }
+}

+ 11 - 0
api/vendor/bcremer/line-reader/infection.json.dist

@@ -0,0 +1,11 @@
+{
+    "source": {
+        "directories": [
+            "src"
+        ]
+    },
+    "logs": {
+        "text": "infection-log.txt"
+    },
+    "timeout": 2
+}

+ 24 - 0
api/vendor/bcremer/line-reader/phpunit.xml.dist

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
+         bootstrap="vendor/autoload.php"
+         beStrictAboutChangesToGlobalState="true"
+         beStrictAboutCoversAnnotation="true"
+         beStrictAboutOutputDuringTests="true"
+         beStrictAboutResourceUsageDuringSmallTests="true"
+         beStrictAboutTodoAnnotatedTests="true"
+         enforceTimeLimit="true"
+         executionOrder="random"
+         failOnRisky="true"
+         failOnWarning="true"
+         colors="true"
+         verbose="true">
+    <coverage>
+        <include>
+            <directory suffix=".php">./src</directory>
+        </include>
+    </coverage>
+    <testsuite name="LineReader tests">
+        <directory>tests</directory>
+    </testsuite>
+</phpunit>

+ 106 - 0
api/vendor/bcremer/line-reader/src/LineReader.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace Bcremer\LineReader;
+
+final class LineReader
+{
+    /**
+     * Prevent instantiation
+     */
+    private function __construct() {}
+
+    /**
+     * @param string $filePath
+     * @return \Generator
+     * @throws \InvalidArgumentException if $filePath is not readable
+     */
+    public static function readLines(string $filePath): \Generator
+    {
+        if (!$fh = @fopen($filePath, 'r')) {
+            throw new \InvalidArgumentException('Cannot open file for reading: ' . $filePath);
+        }
+
+        return self::read($fh);
+    }
+
+    /**
+     * @param string $filePath
+     * @return \Generator
+     * @throws \InvalidArgumentException if $filePath is not readable
+     */
+    public static function readLinesBackwards(string $filePath): \Generator
+    {
+        if (!$fh = @fopen($filePath, 'r')) {
+            throw new \InvalidArgumentException('Cannot open file for reading: ' . $filePath);
+        }
+
+        $size = filesize($filePath);
+
+        return self::readBackwards($fh, $size);
+    }
+
+    /**
+     * @param resource $fh
+     * @return \Generator
+     */
+    private static function read($fh): \Generator
+    {
+        while (false !== $line = fgets($fh)) {
+            yield rtrim($line, "\n");
+        }
+
+        fclose($fh);
+    }
+
+    /**
+     * Read a file from the end using a buffer.
+     *
+     * This is way more efficient than using the naive method
+     * of reading the file backwards byte by byte looking for
+     * a newline character.
+     *
+     * @see http://stackoverflow.com/a/10494801/147634
+     * @param resource $fh
+     * @param int $pos
+     * @return \Generator
+     */
+    private static function readBackwards($fh, int $pos): \Generator
+    {
+        $buffer = null;
+        $bufferSize = 4096;
+
+        if ($pos === 0) {
+            return;
+        }
+
+        while (true) {
+            if (isset($buffer[1])) { // faster than count($buffer) > 1
+                yield array_pop($buffer);
+                continue;
+            }
+
+            if ($pos === 0) {
+                yield array_pop($buffer);
+                break;
+            }
+
+            if ($bufferSize > $pos) {
+                $bufferSize = $pos;
+                $pos = 0;
+            } else {
+                $pos -= $bufferSize;
+            }
+            fseek($fh, $pos);
+            $chunk = fread($fh, $bufferSize);
+            if ($buffer === null) {
+                // remove single trailing newline, rtrim cannot be used here
+                if (substr($chunk, -1) === "\n") {
+                    $chunk = substr($chunk, 0, -1);
+                }
+                $buffer = explode("\n", $chunk);
+            } else {
+                $buffer = explode("\n", $chunk . $buffer[0]);
+            }
+        }
+    }
+}

+ 178 - 0
api/vendor/bcremer/line-reader/tests/LineReaderTest.php

@@ -0,0 +1,178 @@
+<?php
+namespace Bcremer\LineReaderTests;
+
+use Bcremer\LineReader\LineReader;
+use PHPUnit\Framework\TestCase;
+
+class LineReaderTest extends TestCase
+{
+    private static $maxLines;
+    private static $testFile;
+
+    public static function setUpBeforeClass(): void
+    {
+        self::$maxLines = (int)getenv('TEST_MAX_LINES') ?: 10000;
+        self::$testFile = __DIR__.'/testfile_'.self::$maxLines.'.txt';
+
+        if (is_file(self::$testFile)) {
+            return;
+        }
+
+        $fh = fopen(self::$testFile, 'w');
+        for ($i = 1; $i <= self::$maxLines; $i++) {
+            fwrite($fh, "Line $i\n");
+        }
+        fclose($fh);
+    }
+
+    public function testReadLinesThrowsException(): void
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('Cannot open file for reading: /tmp/invalid-file.txt');
+
+        LineReader::readLines('/tmp/invalid-file.txt');
+    }
+
+    public function testReadLinesBackwardsThrowsException(): void
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('Cannot open file for reading: /tmp/invalid-file.txt');
+
+        LineReader::readLinesBackwards('/tmp/invalid-file.txt');
+    }
+
+    public function testReadsAllLines(): void
+    {
+        $result = LineReader::readLines(self::$testFile);
+
+        self::assertInstanceOf(\Traversable::class, $result);
+
+        $firstLine = 1;
+        $lastLine = self::$maxLines;
+        $lineCount = self::$maxLines;
+        $this->assertLines($result, $firstLine, $lastLine, $lineCount);
+    }
+
+    public function testReadsLinesByStartline(): void
+    {
+        $lineGenerator = LineReader::readLines(self::$testFile);
+        $lineGenerator = new \LimitIterator($lineGenerator, 50);
+
+        $firstLine = 51;
+        $lastLine = self::$maxLines;
+        $lineCount = self::$maxLines-50;
+        $this->assertLines($lineGenerator, $firstLine, $lastLine, $lineCount);
+    }
+
+    public function testReadsLinesByLimit(): void
+    {
+        $lineGenerator = LineReader::readLines(self::$testFile);
+        $lineGenerator = new \LimitIterator($lineGenerator, 50, 100);
+
+        $firstLine = 51;
+        $lastLine = 150;
+        $lineCount = 100;
+        $this->assertLines($lineGenerator, $firstLine, $lastLine, $lineCount);
+    }
+
+    public function testReadsLinesBackwards(): void
+    {
+        $lineGenerator = LineReader::readLinesBackwards(self::$testFile);
+
+        $firstLine = self::$maxLines;
+        $lastLine = 1;
+        $lineCount = self::$maxLines;
+        $this->assertLines($lineGenerator, $firstLine, $lastLine, $lineCount);
+    }
+
+    public function testReadsLinesBackwardsWithOffsetAndLimit(): void
+    {
+        $lineGenerator = LineReader::readLinesBackwards(self::$testFile);
+        $lineGenerator = new \LimitIterator($lineGenerator, 10, 50);
+
+        $firstLine = self::$maxLines-10;
+        $lastLine = self::$maxLines-59;
+        $lineCount = 50;
+        $this->assertLines($lineGenerator, $firstLine, $lastLine, $lineCount);
+    }
+
+    public function testEmptyFile(): void
+    {
+        $testFile = __DIR__.'/testfile_empty.txt';
+        $content = '';
+        file_put_contents($testFile, $content);
+
+        $lineGenerator = LineReader::readLines($testFile);
+        self::assertSame([], iterator_to_array($lineGenerator));
+
+        $lineGenerator = LineReader::readLinesBackwards($testFile);
+        self::assertSame([], iterator_to_array($lineGenerator));
+    }
+
+    public function testFileWithLeadingAndTrailingNewlines(): void
+    {
+        $testFile = __DIR__.'/testfile_space.txt';
+
+        $content = <<<CONTENT
+
+
+Line 1
+
+
+Line 4
+Line 5
+
+
+CONTENT;
+
+        file_put_contents($testFile, $content);
+
+        self::assertSame(
+            [
+                '',
+                '',
+                'Line 1',
+                '',
+                '',
+                'Line 4',
+                'Line 5',
+                '',
+            ],
+            iterator_to_array(LineReader::readLines($testFile))
+        );
+
+        self::assertSame(
+            [
+                '',
+                'Line 5',
+                'Line 4',
+                '',
+                '',
+                'Line 1',
+                '',
+                '',
+            ],
+            iterator_to_array(LineReader::readLinesBackwards($testFile))
+        );
+    }
+
+    /**
+     * Runs the generator and asserts on first, last and the total line count
+     *
+     * @param \Traversable $generator
+     */
+    private function assertLines(\Traversable $generator, string $firstLine, int $lastLine, int $lineCount): void
+    {
+        $count = 0;
+        $line = '';
+        foreach ($generator as $line) {
+            if ($count === 0) {
+                self::assertSame("Line $firstLine", $line, 'Expect first line');
+            }
+            $count++;
+        }
+
+        self::assertSame("Line $lastLine", $line, 'Expect last line');
+        self::assertSame($lineCount, $count, 'Expect total line count');
+    }
+}

+ 52 - 2
api/vendor/composer/InstalledVersions.php

@@ -29,7 +29,7 @@ private static $installed = array (
     'aliases' => 
     array (
     ),
-    'reference' => 'f571aa755d372fc8205704b3cda8e1585aab0e3a',
+    'reference' => '19b7b1e70d3dc94a63451008db2737eaaddf8cbf',
     'name' => '__root__',
   ),
   'versions' => 
@@ -41,7 +41,7 @@ private static $installed = array (
       'aliases' => 
       array (
       ),
-      'reference' => 'f571aa755d372fc8205704b3cda8e1585aab0e3a',
+      'reference' => '19b7b1e70d3dc94a63451008db2737eaaddf8cbf',
     ),
     'adldap2/adldap2' => 
     array (
@@ -52,6 +52,15 @@ private static $installed = array (
       ),
       'reference' => 'c2a8f72455d3438377d955fc0f4b9ed836b47463',
     ),
+    'bcremer/line-reader' => 
+    array (
+      'pretty_version' => '1.1.0',
+      'version' => '1.1.0.0',
+      'aliases' => 
+      array (
+      ),
+      'reference' => '3ec3e200577630f1e58d30b4c1c468b877d8d0a7',
+    ),
     'bogstag/oauth2-trakt' => 
     array (
       'pretty_version' => 'v1.0.1',
@@ -194,6 +203,15 @@ private static $installed = array (
       ),
       'reference' => 'badb01e62383430706433191b82506b6df24ad98',
     ),
+    'monolog/monolog' => 
+    array (
+      'pretty_version' => '1.26.1',
+      'version' => '1.26.1.0',
+      'aliases' => 
+      array (
+      ),
+      'reference' => 'c6b00f05152ae2c9b04a448f99c7590beb6042f5',
+    ),
     'myclabs/php-enum' => 
     array (
       'pretty_version' => '1.8.0',
@@ -203,6 +221,15 @@ private static $installed = array (
       ),
       'reference' => '46cf3d8498b095bd33727b13fd5707263af99421',
     ),
+    'nekonomokochan/php-json-logger' => 
+    array (
+      'pretty_version' => 'v1.3.1',
+      'version' => '1.3.1.0',
+      'aliases' => 
+      array (
+      ),
+      'reference' => '6df126a82940a00d8ea2da6e0b7c58e3e57eb132',
+    ),
     'nikic/fast-route' => 
     array (
       'pretty_version' => 'v1.3.0',
@@ -377,6 +404,13 @@ private static $installed = array (
       ),
       'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11',
     ),
+    'psr/log-implementation' => 
+    array (
+      'provided' => 
+      array (
+        0 => '1.0.0',
+      ),
+    ),
     'psr/simple-cache' => 
     array (
       'pretty_version' => '1.0.1',
@@ -404,6 +438,22 @@ private static $installed = array (
       ),
       'reference' => '120b605dfeb996808c31b6477290a714d356e822',
     ),
+    'ramsey/uuid' => 
+    array (
+      'pretty_version' => '3.9.6',
+      'version' => '3.9.6.0',
+      'aliases' => 
+      array (
+      ),
+      'reference' => 'ffa80ab953edd85d5b6c004f96181a538aad35a3',
+    ),
+    'rhumsaa/uuid' => 
+    array (
+      'replaced' => 
+      array (
+        0 => '3.9.6',
+      ),
+    ),
     'rmccue/requests' => 
     array (
       'pretty_version' => 'v1.8.0',

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

@@ -10,14 +10,15 @@ return array(
     'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php',
     'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
     '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
+    '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
     '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
     '25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
     '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
-    '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
     'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
     '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
     '253c157292f75eb38082b5acb06f3f01' => $vendorDir . '/nikic/fast-route/src/functions.php',
     '3109cb1a231dcd04bee1f9f620d46975' => $vendorDir . '/paragonie/sodium_compat/autoload.php',
+    'e39a8b23c42d4e1452234d762b03835a' => $vendorDir . '/ramsey/uuid/src/functions.php',
     'bd9634f2d41831496de0d3dfe4c94881' => $vendorDir . '/symfony/polyfill-php56/bootstrap.php',
     'fe62ba7e10580d903cc46d808b5961a4' => $vendorDir . '/tightenco/collect/src/Collect/Support/helpers.php',
     'caf31cc6ec7cf2241cb6f12c226c3846' => $vendorDir . '/tightenco/collect/src/Collect/Support/alias.php',

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

@@ -18,6 +18,7 @@ return array(
     'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'),
     'Slim\\Psr7\\' => array($vendorDir . '/slim/psr7/src'),
     'Slim\\' => array($vendorDir . '/slim/slim/Slim'),
+    'Ramsey\\Uuid\\' => array($vendorDir . '/ramsey/uuid/src'),
     'Pusher\\' => array($vendorDir . '/pusher/pusher-php-server/src'),
     'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
     'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
@@ -31,7 +32,9 @@ return array(
     'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
     'PHPHtmlParser\\' => array($vendorDir . '/paquettg/php-html-parser/src/PHPHtmlParser'),
     'OpenApi\\' => array($vendorDir . '/zircote/swagger-php/src'),
+    'Nekonomokochan\\PhpJsonLogger\\' => array($vendorDir . '/nekonomokochan/php-json-logger/src/PhpJsonLogger'),
     'MyCLabs\\Enum\\' => array($vendorDir . '/myclabs/php-enum/src'),
+    'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
     'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-client/src'),
     'Lcobucci\\JWT\\' => array($vendorDir . '/lcobucci/jwt/src'),
     'Kryptonit3\\Sonarr\\' => array($vendorDir . '/kryptonit3/sonarr/src'),
@@ -49,5 +52,6 @@ return array(
     'Doctrine\\Common\\Annotations\\' => array($vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations'),
     'Composer\\Semver\\' => array($vendorDir . '/composer/semver/src'),
     'Bogstag\\OAuth2\\Client\\' => array($vendorDir . '/bogstag/oauth2-trakt/src'),
+    'Bcremer\\LineReader\\' => array($vendorDir . '/bcremer/line-reader/src'),
     'Adldap\\' => array($vendorDir . '/adldap2/adldap2/src'),
 );

+ 28 - 1
api/vendor/composer/autoload_static.php

@@ -11,14 +11,15 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php',
         'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
         '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
+        '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
         '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
         '25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
         '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
-        '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
         'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
         '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
         '253c157292f75eb38082b5acb06f3f01' => __DIR__ . '/..' . '/nikic/fast-route/src/functions.php',
         '3109cb1a231dcd04bee1f9f620d46975' => __DIR__ . '/..' . '/paragonie/sodium_compat/autoload.php',
+        'e39a8b23c42d4e1452234d762b03835a' => __DIR__ . '/..' . '/ramsey/uuid/src/functions.php',
         'bd9634f2d41831496de0d3dfe4c94881' => __DIR__ . '/..' . '/symfony/polyfill-php56/bootstrap.php',
         'fe62ba7e10580d903cc46d808b5961a4' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/helpers.php',
         'caf31cc6ec7cf2241cb6f12c226c3846' => __DIR__ . '/..' . '/tightenco/collect/src/Collect/Support/alias.php',
@@ -44,6 +45,10 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
             'Slim\\Psr7\\' => 10,
             'Slim\\' => 5,
         ),
+        'R' => 
+        array (
+            'Ramsey\\Uuid\\' => 12,
+        ),
         'P' => 
         array (
             'Pusher\\' => 7,
@@ -63,9 +68,14 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         array (
             'OpenApi\\' => 8,
         ),
+        'N' => 
+        array (
+            'Nekonomokochan\\PhpJsonLogger\\' => 29,
+        ),
         'M' => 
         array (
             'MyCLabs\\Enum\\' => 13,
+            'Monolog\\' => 8,
         ),
         'L' => 
         array (
@@ -110,6 +120,7 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         'B' => 
         array (
             'Bogstag\\OAuth2\\Client\\' => 22,
+            'Bcremer\\LineReader\\' => 19,
         ),
         'A' => 
         array (
@@ -166,6 +177,10 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         array (
             0 => __DIR__ . '/..' . '/slim/slim/Slim',
         ),
+        'Ramsey\\Uuid\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/ramsey/uuid/src',
+        ),
         'Pusher\\' => 
         array (
             0 => __DIR__ . '/..' . '/pusher/pusher-php-server/src',
@@ -220,10 +235,18 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         array (
             0 => __DIR__ . '/..' . '/zircote/swagger-php/src',
         ),
+        'Nekonomokochan\\PhpJsonLogger\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/nekonomokochan/php-json-logger/src/PhpJsonLogger',
+        ),
         'MyCLabs\\Enum\\' => 
         array (
             0 => __DIR__ . '/..' . '/myclabs/php-enum/src',
         ),
+        'Monolog\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog',
+        ),
         'League\\OAuth2\\Client\\' => 
         array (
             0 => __DIR__ . '/..' . '/league/oauth2-client/src',
@@ -292,6 +315,10 @@ class ComposerStaticInitcbdc783d76f8e7563dcce7d8af053ecb
         array (
             0 => __DIR__ . '/..' . '/bogstag/oauth2-trakt/src',
         ),
+        'Bcremer\\LineReader\\' => 
+        array (
+            0 => __DIR__ . '/..' . '/bcremer/line-reader/src',
+        ),
         'Adldap\\' => 
         array (
             0 => __DIR__ . '/..' . '/adldap2/adldap2/src',

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

@@ -68,6 +68,53 @@
             },
             "install-path": "../adldap2/adldap2"
         },
+        {
+            "name": "bcremer/line-reader",
+            "version": "1.1.0",
+            "version_normalized": "1.1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/bcremer/LineReader.git",
+                "reference": "3ec3e200577630f1e58d30b4c1c468b877d8d0a7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/bcremer/LineReader/zipball/3ec3e200577630f1e58d30b4c1c468b877d8d0a7",
+                "reference": "3ec3e200577630f1e58d30b4c1c468b877d8d0a7",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.3|^7.4|^8.0"
+            },
+            "require-dev": {
+                "infection/infection": "^0.18",
+                "phpunit/phpunit": "^9.4"
+            },
+            "time": "2020-12-08T13:58:53+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Bcremer\\LineReader\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Benjamin Cremer",
+                    "email": "bc@benjamin-cremer.de"
+                }
+            ],
+            "description": "Read large files line by line in a memory efficient (constant) way.",
+            "support": {
+                "issues": "https://github.com/bcremer/LineReader/issues",
+                "source": "https://github.com/bcremer/LineReader/tree/1.1.0"
+            },
+            "install-path": "../bcremer/line-reader"
+        },
         {
             "name": "bogstag/oauth2-trakt",
             "version": "v1.0.1",
@@ -1015,6 +1062,95 @@
             },
             "install-path": "../league/oauth2-client"
         },
+        {
+            "name": "monolog/monolog",
+            "version": "1.26.1",
+            "version_normalized": "1.26.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Seldaek/monolog.git",
+                "reference": "c6b00f05152ae2c9b04a448f99c7590beb6042f5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c6b00f05152ae2c9b04a448f99c7590beb6042f5",
+                "reference": "c6b00f05152ae2c9b04a448f99c7590beb6042f5",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.0",
+                "psr/log": "~1.0"
+            },
+            "provide": {
+                "psr/log-implementation": "1.0.0"
+            },
+            "require-dev": {
+                "aws/aws-sdk-php": "^2.4.9 || ^3.0",
+                "doctrine/couchdb": "~1.0@dev",
+                "graylog2/gelf-php": "~1.0",
+                "php-amqplib/php-amqplib": "~2.4",
+                "php-console/php-console": "^3.1.3",
+                "phpstan/phpstan": "^0.12.59",
+                "phpunit/phpunit": "~4.5",
+                "ruflin/elastica": ">=0.90 <3.0",
+                "sentry/sentry": "^0.13",
+                "swiftmailer/swiftmailer": "^5.3|^6.0"
+            },
+            "suggest": {
+                "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
+                "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+                "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
+                "ext-mongo": "Allow sending log messages to a MongoDB server",
+                "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
+                "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
+                "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
+                "php-console/php-console": "Allow sending log messages to Google Chrome",
+                "rollbar/rollbar": "Allow sending log messages to Rollbar",
+                "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
+                "sentry/sentry": "Allow sending log messages to a Sentry server"
+            },
+            "time": "2021-05-28T08:32:12+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Monolog\\": "src/Monolog"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be",
+                    "homepage": "http://seld.be"
+                }
+            ],
+            "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
+            "homepage": "http://github.com/Seldaek/monolog",
+            "keywords": [
+                "log",
+                "logging",
+                "psr-3"
+            ],
+            "support": {
+                "issues": "https://github.com/Seldaek/monolog/issues",
+                "source": "https://github.com/Seldaek/monolog/tree/1.26.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/Seldaek",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../monolog/monolog"
+        },
         {
             "name": "myclabs/php-enum",
             "version": "1.8.0",
@@ -1078,6 +1214,58 @@
             ],
             "install-path": "../myclabs/php-enum"
         },
+        {
+            "name": "nekonomokochan/php-json-logger",
+            "version": "v1.3.1",
+            "version_normalized": "1.3.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/nekonomokochan/php-json-logger.git",
+                "reference": "6df126a82940a00d8ea2da6e0b7c58e3e57eb132"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/nekonomokochan/php-json-logger/zipball/6df126a82940a00d8ea2da6e0b7c58e3e57eb132",
+                "reference": "6df126a82940a00d8ea2da6e0b7c58e3e57eb132",
+                "shasum": ""
+            },
+            "require": {
+                "monolog/monolog": "^1.24",
+                "php": "~7.1",
+                "ramsey/uuid": "^3.8"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "^2.14",
+                "php": "~7.1",
+                "php-coveralls/php-coveralls": "^2.1",
+                "phpunit/phpcov": "^5.0",
+                "phpunit/phpunit": "^7.5"
+            },
+            "time": "2019-02-18T06:07:14+00:00",
+            "type": "library",
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Nekonomokochan\\PhpJsonLogger\\": "src/PhpJsonLogger"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "keitakn",
+                    "email": "keita.koga.work@gmail.com"
+                }
+            ],
+            "description": "LoggingLibrary for PHP. Output by JSON Format",
+            "support": {
+                "issues": "https://github.com/nekonomokochan/php-json-logger/issues",
+                "source": "https://github.com/nekonomokochan/php-json-logger/tree/feature/issue63"
+            },
+            "install-path": "../nekonomokochan/php-json-logger"
+        },
         {
             "name": "nikic/fast-route",
             "version": "v1.3.0",
@@ -2256,6 +2444,114 @@
             "description": "A polyfill for getallheaders.",
             "install-path": "../ralouphie/getallheaders"
         },
+        {
+            "name": "ramsey/uuid",
+            "version": "3.9.6",
+            "version_normalized": "3.9.6.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ramsey/uuid.git",
+                "reference": "ffa80ab953edd85d5b6c004f96181a538aad35a3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ramsey/uuid/zipball/ffa80ab953edd85d5b6c004f96181a538aad35a3",
+                "reference": "ffa80ab953edd85d5b6c004f96181a538aad35a3",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "paragonie/random_compat": "^1 | ^2 | ^9.99.99",
+                "php": "^5.4 | ^7.0 | ^8.0",
+                "symfony/polyfill-ctype": "^1.8"
+            },
+            "replace": {
+                "rhumsaa/uuid": "self.version"
+            },
+            "require-dev": {
+                "codeception/aspect-mock": "^1 | ^2",
+                "doctrine/annotations": "^1.2",
+                "goaop/framework": "1.0.0-alpha.2 | ^1 | >=2.1.0 <=2.3.2",
+                "mockery/mockery": "^0.9.11 | ^1",
+                "moontoast/math": "^1.1",
+                "nikic/php-parser": "<=4.5.0",
+                "paragonie/random-lib": "^2",
+                "php-mock/php-mock-phpunit": "^0.3 | ^1.1 | ^2.6",
+                "php-parallel-lint/php-parallel-lint": "^1.3",
+                "phpunit/phpunit": ">=4.8.36 <9.0.0 | >=9.3.0",
+                "squizlabs/php_codesniffer": "^3.5",
+                "yoast/phpunit-polyfills": "^1.0"
+            },
+            "suggest": {
+                "ext-ctype": "Provides support for PHP Ctype functions",
+                "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator",
+                "ext-openssl": "Provides the OpenSSL extension for use with the OpenSslGenerator",
+                "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator",
+                "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).",
+                "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter",
+                "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid",
+                "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type."
+            },
+            "time": "2021-09-25T23:07:42+00:00",
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.x-dev"
+                }
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Ramsey\\Uuid\\": "src/"
+                },
+                "files": [
+                    "src/functions.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ben Ramsey",
+                    "email": "ben@benramsey.com",
+                    "homepage": "https://benramsey.com"
+                },
+                {
+                    "name": "Marijn Huizendveld",
+                    "email": "marijn.huizendveld@gmail.com"
+                },
+                {
+                    "name": "Thibaud Fabre",
+                    "email": "thibaud@aztech.io"
+                }
+            ],
+            "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).",
+            "homepage": "https://github.com/ramsey/uuid",
+            "keywords": [
+                "guid",
+                "identifier",
+                "uuid"
+            ],
+            "support": {
+                "issues": "https://github.com/ramsey/uuid/issues",
+                "rss": "https://github.com/ramsey/uuid/releases.atom",
+                "source": "https://github.com/ramsey/uuid",
+                "wiki": "https://github.com/ramsey/uuid/wiki"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/ramsey",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid",
+                    "type": "tidelift"
+                }
+            ],
+            "install-path": "../ramsey/uuid"
+        },
         {
             "name": "rmccue/requests",
             "version": "v1.8.0",

+ 52 - 2
api/vendor/composer/installed.php

@@ -6,7 +6,7 @@
     'aliases' => 
     array (
     ),
-    'reference' => 'f571aa755d372fc8205704b3cda8e1585aab0e3a',
+    'reference' => '19b7b1e70d3dc94a63451008db2737eaaddf8cbf',
     'name' => '__root__',
   ),
   'versions' => 
@@ -18,7 +18,7 @@
       'aliases' => 
       array (
       ),
-      'reference' => 'f571aa755d372fc8205704b3cda8e1585aab0e3a',
+      'reference' => '19b7b1e70d3dc94a63451008db2737eaaddf8cbf',
     ),
     'adldap2/adldap2' => 
     array (
@@ -29,6 +29,15 @@
       ),
       'reference' => 'c2a8f72455d3438377d955fc0f4b9ed836b47463',
     ),
+    'bcremer/line-reader' => 
+    array (
+      'pretty_version' => '1.1.0',
+      'version' => '1.1.0.0',
+      'aliases' => 
+      array (
+      ),
+      'reference' => '3ec3e200577630f1e58d30b4c1c468b877d8d0a7',
+    ),
     'bogstag/oauth2-trakt' => 
     array (
       'pretty_version' => 'v1.0.1',
@@ -171,6 +180,15 @@
       ),
       'reference' => 'badb01e62383430706433191b82506b6df24ad98',
     ),
+    'monolog/monolog' => 
+    array (
+      'pretty_version' => '1.26.1',
+      'version' => '1.26.1.0',
+      'aliases' => 
+      array (
+      ),
+      'reference' => 'c6b00f05152ae2c9b04a448f99c7590beb6042f5',
+    ),
     'myclabs/php-enum' => 
     array (
       'pretty_version' => '1.8.0',
@@ -180,6 +198,15 @@
       ),
       'reference' => '46cf3d8498b095bd33727b13fd5707263af99421',
     ),
+    'nekonomokochan/php-json-logger' => 
+    array (
+      'pretty_version' => 'v1.3.1',
+      'version' => '1.3.1.0',
+      'aliases' => 
+      array (
+      ),
+      'reference' => '6df126a82940a00d8ea2da6e0b7c58e3e57eb132',
+    ),
     'nikic/fast-route' => 
     array (
       'pretty_version' => 'v1.3.0',
@@ -354,6 +381,13 @@
       ),
       'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11',
     ),
+    'psr/log-implementation' => 
+    array (
+      'provided' => 
+      array (
+        0 => '1.0.0',
+      ),
+    ),
     'psr/simple-cache' => 
     array (
       'pretty_version' => '1.0.1',
@@ -381,6 +415,22 @@
       ),
       'reference' => '120b605dfeb996808c31b6477290a714d356e822',
     ),
+    'ramsey/uuid' => 
+    array (
+      'pretty_version' => '3.9.6',
+      'version' => '3.9.6.0',
+      'aliases' => 
+      array (
+      ),
+      'reference' => 'ffa80ab953edd85d5b6c004f96181a538aad35a3',
+    ),
+    'rhumsaa/uuid' => 
+    array (
+      'replaced' => 
+      array (
+        0 => '3.9.6',
+      ),
+    ),
     'rmccue/requests' => 
     array (
       'pretty_version' => 'v1.8.0',

+ 423 - 0
api/vendor/monolog/monolog/CHANGELOG.md

@@ -0,0 +1,423 @@
+### 1.26.1 (2021-05-28)
+
+  * Fixed PHP 8.1 deprecation warning
+
+### 1.26.0 (2020-12-14)
+
+  * Added $dateFormat and $removeUsedContextFields arguments to PsrLogMessageProcessor (backport from 2.x)
+
+### 1.25.5 (2020-07-23)
+
+  * Fixed array access on null in RavenHandler
+  * Fixed unique_id in WebProcessor not being disableable
+
+### 1.25.4 (2020-05-22)
+
+  * Fixed GitProcessor type error when there is no git repo present
+  * Fixed normalization of SoapFault objects containing deeply nested objects as "detail"
+  * Fixed support for relative paths in RotatingFileHandler
+
+### 1.25.3 (2019-12-20)
+
+  * Fixed formatting of resources in JsonFormatter
+  * Fixed RedisHandler failing to use MULTI properly when passed a proxied Redis instance (e.g. in Symfony with lazy services)
+  * Fixed FilterHandler triggering a notice when handleBatch was filtering all records passed to it
+  * Fixed Turkish locale messing up the conversion of level names to their constant values
+
+### 1.25.2 (2019-11-13)
+
+  * Fixed normalization of Traversables to avoid traversing them as not all of them are rewindable
+  * Fixed setFormatter/getFormatter to forward to the nested handler in FilterHandler, FingersCrossedHandler, BufferHandler and SamplingHandler
+  * Fixed BrowserConsoleHandler formatting when using multiple styles
+  * Fixed normalization of exception codes to be always integers even for PDOException which have them as numeric strings
+  * Fixed normalization of SoapFault objects containing non-strings as "detail"
+  * Fixed json encoding across all handlers to always attempt recovery of non-UTF-8 strings instead of failing the whole encoding
+
+### 1.25.1 (2019-09-06)
+
+  * Fixed forward-compatible interfaces to be compatible with Monolog 1.x too.
+
+### 1.25.0 (2019-09-06)
+
+  * Deprecated SlackbotHandler, use SlackWebhookHandler or SlackHandler instead
+  * Deprecated RavenHandler, use sentry/sentry 2.x and their Sentry\Monolog\Handler instead
+  * Deprecated HipChatHandler, migrate to Slack and use SlackWebhookHandler or SlackHandler instead
+  * Added forward-compatible interfaces and traits FormattableHandlerInterface, FormattableHandlerTrait, ProcessableHandlerInterface, ProcessableHandlerTrait. If you use modern PHP and want to make code compatible with Monolog 1 and 2 this can help. You will have to require at least Monolog 1.25 though.
+  * Added support for RFC3164 (outdated BSD syslog protocol) to SyslogUdpHandler
+  * Fixed issue in GroupHandler and WhatFailureGroupHandler where setting multiple processors would duplicate records
+  * Fixed issue in SignalHandler restarting syscalls functionality
+  * Fixed normalizers handling of exception backtraces to avoid serializing arguments in some cases
+  * Fixed ZendMonitorHandler to work with the latest Zend Server versions
+  * Fixed ChromePHPHandler to avoid sending more data than latest Chrome versions allow in headers (4KB down from 256KB).
+
+### 1.24.0 (2018-11-05)
+
+  * BC Notice: If you are extending any of the Monolog's Formatters' `normalize` method, make sure you add the new `$depth = 0` argument to your function signature to avoid strict PHP warnings.
+  * Added a `ResettableInterface` in order to reset/reset/clear/flush handlers and processors
+  * Added a `ProcessorInterface` as an optional way to label a class as being a processor (mostly useful for autowiring dependency containers)
+  * Added a way to log signals being received using Monolog\SignalHandler
+  * Added ability to customize error handling at the Logger level using Logger::setExceptionHandler
+  * Added InsightOpsHandler to migrate users of the LogEntriesHandler
+  * Added protection to NormalizerHandler against circular and very deep structures, it now stops normalizing at a depth of 9
+  * Added capture of stack traces to ErrorHandler when logging PHP errors
+  * Added RavenHandler support for a `contexts` context or extra key to forward that to Sentry's contexts
+  * Added forwarding of context info to FluentdFormatter
+  * Added SocketHandler::setChunkSize to override the default chunk size in case you must send large log lines to rsyslog for example
+  * Added ability to extend/override BrowserConsoleHandler
+  * Added SlackWebhookHandler::getWebhookUrl and SlackHandler::getToken to enable class extensibility
+  * Added SwiftMailerHandler::getSubjectFormatter to enable class extensibility
+  * Dropped official support for HHVM in test builds
+  * Fixed normalization of exception traces when call_user_func is used to avoid serializing objects and the data they contain
+  * Fixed naming of fields in Slack handler, all field names are now capitalized in all cases
+  * Fixed HipChatHandler bug where slack dropped messages randomly
+  * Fixed normalization of objects in Slack handlers
+  * Fixed support for PHP7's Throwable in NewRelicHandler
+  * Fixed race bug when StreamHandler sometimes incorrectly reported it failed to create a directory
+  * Fixed table row styling issues in HtmlFormatter
+  * Fixed RavenHandler dropping the message when logging exception
+  * Fixed WhatFailureGroupHandler skipping processors when using handleBatch
+    and implement it where possible
+  * Fixed display of anonymous class names
+
+### 1.23.0 (2017-06-19)
+
+  * Improved SyslogUdpHandler's support for RFC5424 and added optional `$ident` argument
+  * Fixed GelfHandler truncation to be per field and not per message
+  * Fixed compatibility issue with PHP <5.3.6
+  * Fixed support for headless Chrome in ChromePHPHandler
+  * Fixed support for latest Aws SDK in DynamoDbHandler
+  * Fixed support for SwiftMailer 6.0+ in SwiftMailerHandler
+
+### 1.22.1 (2017-03-13)
+
+  * Fixed lots of minor issues in the new Slack integrations
+  * Fixed support for allowInlineLineBreaks in LineFormatter when formatting exception backtraces
+
+### 1.22.0 (2016-11-26)
+
+  * Added SlackbotHandler and SlackWebhookHandler to set up Slack integration more easily
+  * Added MercurialProcessor to add mercurial revision and branch names to log records
+  * Added support for AWS SDK v3 in DynamoDbHandler
+  * Fixed fatal errors occuring when normalizing generators that have been fully consumed
+  * Fixed RollbarHandler to include a level (rollbar level), monolog_level (original name), channel and datetime (unix)
+  * Fixed RollbarHandler not flushing records automatically, calling close() explicitly is not necessary anymore
+  * Fixed SyslogUdpHandler to avoid sending empty frames
+  * Fixed a few PHP 7.0 and 7.1 compatibility issues
+
+### 1.21.0 (2016-07-29)
+
+  * Break: Reverted the addition of $context when the ErrorHandler handles regular php errors from 1.20.0 as it was causing issues
+  * Added support for more formats in RotatingFileHandler::setFilenameFormat as long as they have Y, m and d in order
+  * Added ability to format the main line of text the SlackHandler sends by explictly setting a formatter on the handler
+  * Added information about SoapFault instances in NormalizerFormatter
+  * Added $handleOnlyReportedErrors option on ErrorHandler::registerErrorHandler (default true) to allow logging of all errors no matter the error_reporting level
+
+### 1.20.0 (2016-07-02)
+
+  * Added FingersCrossedHandler::activate() to manually trigger the handler regardless of the activation policy
+  * Added StreamHandler::getUrl to retrieve the stream's URL
+  * Added ability to override addRow/addTitle in HtmlFormatter
+  * Added the $context to context information when the ErrorHandler handles a regular php error
+  * Deprecated RotatingFileHandler::setFilenameFormat to only support 3 formats: Y, Y-m and Y-m-d
+  * Fixed WhatFailureGroupHandler to work with PHP7 throwables
+  * Fixed a few minor bugs
+
+### 1.19.0 (2016-04-12)
+
+  * Break: StreamHandler will not close streams automatically that it does not own. If you pass in a stream (not a path/url), then it will not close it for you. You can retrieve those using getStream() if needed
+  * Added DeduplicationHandler to remove duplicate records from notifications across multiple requests, useful for email or other notifications on errors
+  * Added ability to use `%message%` and other LineFormatter replacements in the subject line of emails sent with NativeMailHandler and SwiftMailerHandler
+  * Fixed HipChatHandler handling of long messages
+
+### 1.18.2 (2016-04-02)
+
+  * Fixed ElasticaFormatter to use more precise dates
+  * Fixed GelfMessageFormatter sending too long messages
+
+### 1.18.1 (2016-03-13)
+
+  * Fixed SlackHandler bug where slack dropped messages randomly
+  * Fixed RedisHandler issue when using with the PHPRedis extension
+  * Fixed AmqpHandler content-type being incorrectly set when using with the AMQP extension
+  * Fixed BrowserConsoleHandler regression
+
+### 1.18.0 (2016-03-01)
+
+  * Added optional reduction of timestamp precision via `Logger->useMicrosecondTimestamps(false)`, disabling it gets you a bit of performance boost but reduces the precision to the second instead of microsecond
+  * Added possibility to skip some extra stack frames in IntrospectionProcessor if you have some library wrapping Monolog that is always adding frames
+  * Added `Logger->withName` to clone a logger (keeping all handlers) with a new name
+  * Added FluentdFormatter for the Fluentd unix socket protocol
+  * Added HandlerWrapper base class to ease the creation of handler wrappers, just extend it and override as needed
+  * Added support for replacing context sub-keys using `%context.*%` in LineFormatter
+  * Added support for `payload` context value in RollbarHandler
+  * Added setRelease to RavenHandler to describe the application version, sent with every log
+  * Added support for `fingerprint` context value in RavenHandler
+  * Fixed JSON encoding errors that would gobble up the whole log record, we now handle those more gracefully by dropping chars as needed
+  * Fixed write timeouts in SocketHandler and derivatives, set to 10sec by default, lower it with `setWritingTimeout()`
+  * Fixed PHP7 compatibility with regard to Exception/Throwable handling in a few places
+
+### 1.17.2 (2015-10-14)
+
+  * Fixed ErrorHandler compatibility with non-Monolog PSR-3 loggers
+  * Fixed SlackHandler handling to use slack functionalities better
+  * Fixed SwiftMailerHandler bug when sending multiple emails they all had the same id
+  * Fixed 5.3 compatibility regression
+
+### 1.17.1 (2015-08-31)
+
+  * Fixed RollbarHandler triggering PHP notices
+
+### 1.17.0 (2015-08-30)
+
+  * Added support for `checksum` and `release` context/extra values in RavenHandler
+  * Added better support for exceptions in RollbarHandler
+  * Added UidProcessor::getUid
+  * Added support for showing the resource type in NormalizedFormatter
+  * Fixed IntrospectionProcessor triggering PHP notices
+
+### 1.16.0 (2015-08-09)
+
+  * Added IFTTTHandler to notify ifttt.com triggers
+  * Added Logger::setHandlers() to allow setting/replacing all handlers
+  * Added $capSize in RedisHandler to cap the log size
+  * Fixed StreamHandler creation of directory to only trigger when the first log write happens
+  * Fixed bug in the handling of curl failures
+  * Fixed duplicate logging of fatal errors when both error and fatal error handlers are registered in monolog's ErrorHandler
+  * Fixed missing fatal errors records with handlers that need to be closed to flush log records
+  * Fixed TagProcessor::addTags support for associative arrays
+
+### 1.15.0 (2015-07-12)
+
+  * Added addTags and setTags methods to change a TagProcessor
+  * Added automatic creation of directories if they are missing for a StreamHandler to open a log file
+  * Added retry functionality to Loggly, Cube and Mandrill handlers so they retry up to 5 times in case of network failure
+  * Fixed process exit code being incorrectly reset to 0 if ErrorHandler::registerExceptionHandler was used
+  * Fixed HTML/JS escaping in BrowserConsoleHandler
+  * Fixed JSON encoding errors being silently suppressed (PHP 5.5+ only)
+
+### 1.14.0 (2015-06-19)
+
+  * Added PHPConsoleHandler to send record to Chrome's PHP Console extension and library
+  * Added support for objects implementing __toString in the NormalizerFormatter
+  * Added support for HipChat's v2 API in HipChatHandler
+  * Added Logger::setTimezone() to initialize the timezone monolog should use in case date.timezone isn't correct for your app
+  * Added an option to send formatted message instead of the raw record on PushoverHandler via ->useFormattedMessage(true)
+  * Fixed curl errors being silently suppressed
+
+### 1.13.1 (2015-03-09)
+
+  * Fixed regression in HipChat requiring a new token to be created
+
+### 1.13.0 (2015-03-05)
+
+  * Added Registry::hasLogger to check for the presence of a logger instance
+  * Added context.user support to RavenHandler
+  * Added HipChat API v2 support in the HipChatHandler
+  * Added NativeMailerHandler::addParameter to pass params to the mail() process
+  * Added context data to SlackHandler when $includeContextAndExtra is true
+  * Added ability to customize the Swift_Message per-email in SwiftMailerHandler
+  * Fixed SwiftMailerHandler to lazily create message instances if a callback is provided
+  * Fixed serialization of INF and NaN values in Normalizer and LineFormatter
+
+### 1.12.0 (2014-12-29)
+
+  * Break: HandlerInterface::isHandling now receives a partial record containing only a level key. This was always the intent and does not break any Monolog handler but is strictly speaking a BC break and you should check if you relied on any other field in your own handlers.
+  * Added PsrHandler to forward records to another PSR-3 logger
+  * Added SamplingHandler to wrap around a handler and include only every Nth record
+  * Added MongoDBFormatter to support better storage with MongoDBHandler (it must be enabled manually for now)
+  * Added exception codes in the output of most formatters
+  * Added LineFormatter::includeStacktraces to enable exception stack traces in logs (uses more than one line)
+  * Added $useShortAttachment to SlackHandler to minify attachment size and $includeExtra to append extra data
+  * Added $host to HipChatHandler for users of private instances
+  * Added $transactionName to NewRelicHandler and support for a transaction_name context value
+  * Fixed MandrillHandler to avoid outputing API call responses
+  * Fixed some non-standard behaviors in SyslogUdpHandler
+
+### 1.11.0 (2014-09-30)
+
+  * Break: The NewRelicHandler extra and context data are now prefixed with extra_ and context_ to avoid clashes. Watch out if you have scripts reading those from the API and rely on names
+  * Added WhatFailureGroupHandler to suppress any exception coming from the wrapped handlers and avoid chain failures if a logging service fails
+  * Added MandrillHandler to send emails via the Mandrillapp.com API
+  * Added SlackHandler to log records to a Slack.com account
+  * Added FleepHookHandler to log records to a Fleep.io account
+  * Added LogglyHandler::addTag to allow adding tags to an existing handler
+  * Added $ignoreEmptyContextAndExtra to LineFormatter to avoid empty [] at the end
+  * Added $useLocking to StreamHandler and RotatingFileHandler to enable flock() while writing
+  * Added support for PhpAmqpLib in the AmqpHandler
+  * Added FingersCrossedHandler::clear and BufferHandler::clear to reset them between batches in long running jobs
+  * Added support for adding extra fields from $_SERVER in the WebProcessor
+  * Fixed support for non-string values in PrsLogMessageProcessor
+  * Fixed SwiftMailer messages being sent with the wrong date in long running scripts
+  * Fixed minor PHP 5.6 compatibility issues
+  * Fixed BufferHandler::close being called twice
+
+### 1.10.0 (2014-06-04)
+
+  * Added Logger::getHandlers() and Logger::getProcessors() methods
+  * Added $passthruLevel argument to FingersCrossedHandler to let it always pass some records through even if the trigger level is not reached
+  * Added support for extra data in NewRelicHandler
+  * Added $expandNewlines flag to the ErrorLogHandler to create multiple log entries when a message has multiple lines
+
+### 1.9.1 (2014-04-24)
+
+  * Fixed regression in RotatingFileHandler file permissions
+  * Fixed initialization of the BufferHandler to make sure it gets flushed after receiving records
+  * Fixed ChromePHPHandler and FirePHPHandler's activation strategies to be more conservative
+
+### 1.9.0 (2014-04-20)
+
+  * Added LogEntriesHandler to send logs to a LogEntries account
+  * Added $filePermissions to tweak file mode on StreamHandler and RotatingFileHandler
+  * Added $useFormatting flag to MemoryProcessor to make it send raw data in bytes
+  * Added support for table formatting in FirePHPHandler via the table context key
+  * Added a TagProcessor to add tags to records, and support for tags in RavenHandler
+  * Added $appendNewline flag to the JsonFormatter to enable using it when logging to files
+  * Added sound support to the PushoverHandler
+  * Fixed multi-threading support in StreamHandler
+  * Fixed empty headers issue when ChromePHPHandler received no records
+  * Fixed default format of the ErrorLogHandler
+
+### 1.8.0 (2014-03-23)
+
+  * Break: the LineFormatter now strips newlines by default because this was a bug, set $allowInlineLineBreaks to true if you need them
+  * Added BrowserConsoleHandler to send logs to any browser's console via console.log() injection in the output
+  * Added FilterHandler to filter records and only allow those of a given list of levels through to the wrapped handler
+  * Added FlowdockHandler to send logs to a Flowdock account
+  * Added RollbarHandler to send logs to a Rollbar account
+  * Added HtmlFormatter to send prettier log emails with colors for each log level
+  * Added GitProcessor to add the current branch/commit to extra record data
+  * Added a Monolog\Registry class to allow easier global access to pre-configured loggers
+  * Added support for the new official graylog2/gelf-php lib for GelfHandler, upgrade if you can by replacing the mlehner/gelf-php requirement
+  * Added support for HHVM
+  * Added support for Loggly batch uploads
+  * Added support for tweaking the content type and encoding in NativeMailerHandler
+  * Added $skipClassesPartials to tweak the ignored classes in the IntrospectionProcessor
+  * Fixed batch request support in GelfHandler
+
+### 1.7.0 (2013-11-14)
+
+  * Added ElasticSearchHandler to send logs to an Elastic Search server
+  * Added DynamoDbHandler and ScalarFormatter to send logs to Amazon's Dynamo DB
+  * Added SyslogUdpHandler to send logs to a remote syslogd server
+  * Added LogglyHandler to send logs to a Loggly account
+  * Added $level to IntrospectionProcessor so it only adds backtraces when needed
+  * Added $version to LogstashFormatter to allow using the new v1 Logstash format
+  * Added $appName to NewRelicHandler
+  * Added configuration of Pushover notification retries/expiry
+  * Added $maxColumnWidth to NativeMailerHandler to change the 70 chars default
+  * Added chainability to most setters for all handlers
+  * Fixed RavenHandler batch processing so it takes the message from the record with highest priority
+  * Fixed HipChatHandler batch processing so it sends all messages at once
+  * Fixed issues with eAccelerator
+  * Fixed and improved many small things
+
+### 1.6.0 (2013-07-29)
+
+  * Added HipChatHandler to send logs to a HipChat chat room
+  * Added ErrorLogHandler to send logs to PHP's error_log function
+  * Added NewRelicHandler to send logs to NewRelic's service
+  * Added Monolog\ErrorHandler helper class to register a Logger as exception/error/fatal handler
+  * Added ChannelLevelActivationStrategy for the FingersCrossedHandler to customize levels by channel
+  * Added stack traces output when normalizing exceptions (json output & co)
+  * Added Monolog\Logger::API constant (currently 1)
+  * Added support for ChromePHP's v4.0 extension
+  * Added support for message priorities in PushoverHandler, see $highPriorityLevel and $emergencyLevel
+  * Added support for sending messages to multiple users at once with the PushoverHandler
+  * Fixed RavenHandler's support for batch sending of messages (when behind a Buffer or FingersCrossedHandler)
+  * Fixed normalization of Traversables with very large data sets, only the first 1000 items are shown now
+  * Fixed issue in RotatingFileHandler when an open_basedir restriction is active
+  * Fixed minor issues in RavenHandler and bumped the API to Raven 0.5.0
+  * Fixed SyslogHandler issue when many were used concurrently with different facilities
+
+### 1.5.0 (2013-04-23)
+
+  * Added ProcessIdProcessor to inject the PID in log records
+  * Added UidProcessor to inject a unique identifier to all log records of one request/run
+  * Added support for previous exceptions in the LineFormatter exception serialization
+  * Added Monolog\Logger::getLevels() to get all available levels
+  * Fixed ChromePHPHandler so it avoids sending headers larger than Chrome can handle
+
+### 1.4.1 (2013-04-01)
+
+  * Fixed exception formatting in the LineFormatter to be more minimalistic
+  * Fixed RavenHandler's handling of context/extra data, requires Raven client >0.1.0
+  * Fixed log rotation in RotatingFileHandler to work with long running scripts spanning multiple days
+  * Fixed WebProcessor array access so it checks for data presence
+  * Fixed Buffer, Group and FingersCrossed handlers to make use of their processors
+
+### 1.4.0 (2013-02-13)
+
+  * Added RedisHandler to log to Redis via the Predis library or the phpredis extension
+  * Added ZendMonitorHandler to log to the Zend Server monitor
+  * Added the possibility to pass arrays of handlers and processors directly in the Logger constructor
+  * Added `$useSSL` option to the PushoverHandler which is enabled by default
+  * Fixed ChromePHPHandler and FirePHPHandler issue when multiple instances are used simultaneously
+  * Fixed header injection capability in the NativeMailHandler
+
+### 1.3.1 (2013-01-11)
+
+  * Fixed LogstashFormatter to be usable with stream handlers
+  * Fixed GelfMessageFormatter levels on Windows
+
+### 1.3.0 (2013-01-08)
+
+  * Added PSR-3 compliance, the `Monolog\Logger` class is now an instance of `Psr\Log\LoggerInterface`
+  * Added PsrLogMessageProcessor that you can selectively enable for full PSR-3 compliance
+  * Added LogstashFormatter (combine with SocketHandler or StreamHandler to send logs to Logstash)
+  * Added PushoverHandler to send mobile notifications
+  * Added CouchDBHandler and DoctrineCouchDBHandler
+  * Added RavenHandler to send data to Sentry servers
+  * Added support for the new MongoClient class in MongoDBHandler
+  * Added microsecond precision to log records' timestamps
+  * Added `$flushOnOverflow` param to BufferHandler to flush by batches instead of losing
+    the oldest entries
+  * Fixed normalization of objects with cyclic references
+
+### 1.2.1 (2012-08-29)
+
+  * Added new $logopts arg to SyslogHandler to provide custom openlog options
+  * Fixed fatal error in SyslogHandler
+
+### 1.2.0 (2012-08-18)
+
+  * Added AmqpHandler (for use with AMQP servers)
+  * Added CubeHandler
+  * Added NativeMailerHandler::addHeader() to send custom headers in mails
+  * Added the possibility to specify more than one recipient in NativeMailerHandler
+  * Added the possibility to specify float timeouts in SocketHandler
+  * Added NOTICE and EMERGENCY levels to conform with RFC 5424
+  * Fixed the log records to use the php default timezone instead of UTC
+  * Fixed BufferHandler not being flushed properly on PHP fatal errors
+  * Fixed normalization of exotic resource types
+  * Fixed the default format of the SyslogHandler to avoid duplicating datetimes in syslog
+
+### 1.1.0 (2012-04-23)
+
+  * Added Monolog\Logger::isHandling() to check if a handler will
+    handle the given log level
+  * Added ChromePHPHandler
+  * Added MongoDBHandler
+  * Added GelfHandler (for use with Graylog2 servers)
+  * Added SocketHandler (for use with syslog-ng for example)
+  * Added NormalizerFormatter
+  * Added the possibility to change the activation strategy of the FingersCrossedHandler
+  * Added possibility to show microseconds in logs
+  * Added `server` and `referer` to WebProcessor output
+
+### 1.0.2 (2011-10-24)
+
+  * Fixed bug in IE with large response headers and FirePHPHandler
+
+### 1.0.1 (2011-08-25)
+
+  * Added MemoryPeakUsageProcessor and MemoryUsageProcessor
+  * Added Monolog\Logger::getName() to get a logger's channel name
+
+### 1.0.0 (2011-07-06)
+
+  * Added IntrospectionProcessor to get info from where the logger was called
+  * Fixed WebProcessor in CLI
+
+### 1.0.0-RC1 (2011-07-01)
+
+  * Initial release

+ 19 - 0
api/vendor/monolog/monolog/LICENSE

@@ -0,0 +1,19 @@
+Copyright (c) 2011-2016 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.

+ 94 - 0
api/vendor/monolog/monolog/README.md

@@ -0,0 +1,94 @@
+# Monolog - Logging for PHP [![Build Status](https://img.shields.io/travis/Seldaek/monolog.svg)](https://travis-ci.org/Seldaek/monolog)
+
+[![Total Downloads](https://img.shields.io/packagist/dt/monolog/monolog.svg)](https://packagist.org/packages/monolog/monolog)
+[![Latest Stable Version](https://img.shields.io/packagist/v/monolog/monolog.svg)](https://packagist.org/packages/monolog/monolog)
+
+
+Monolog sends your logs to files, sockets, inboxes, databases and various
+web services. See the complete list of handlers below. Special handlers
+allow you to build advanced logging strategies.
+
+This library implements the [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md)
+interface that you can type-hint against in your own libraries to keep
+a maximum of interoperability. You can also use it in your applications to
+make sure you can always use another compatible logger at a later time.
+As of 1.11.0 Monolog public APIs will also accept PSR-3 log levels.
+Internally Monolog still uses its own level scheme since it predates PSR-3.
+
+## Installation
+
+Install the latest version with
+
+```bash
+$ composer require monolog/monolog
+```
+
+## Basic Usage
+
+```php
+<?php
+
+use Monolog\Logger;
+use Monolog\Handler\StreamHandler;
+
+// create a log channel
+$log = new Logger('name');
+$log->pushHandler(new StreamHandler('path/to/your.log', Logger::WARNING));
+
+// add records to the log
+$log->addWarning('Foo');
+$log->addError('Bar');
+```
+
+## Documentation
+
+- [Usage Instructions](doc/01-usage.md)
+- [Handlers, Formatters and Processors](doc/02-handlers-formatters-processors.md)
+- [Utility classes](doc/03-utilities.md)
+- [Extending Monolog](doc/04-extending.md)
+
+## Third Party Packages
+
+Third party handlers, formatters and processors are
+[listed in the wiki](https://github.com/Seldaek/monolog/wiki/Third-Party-Packages). You
+can also add your own there if you publish one.
+
+## About
+
+### Requirements
+
+- Monolog works with PHP 5.3 or above, and is also tested to work with HHVM.
+
+### Submitting bugs and feature requests
+
+Bugs and feature request are tracked on [GitHub](https://github.com/Seldaek/monolog/issues)
+
+### Framework Integrations
+
+- Frameworks and libraries using [PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md)
+  can be used very easily with Monolog since it implements the interface.
+- [Symfony2](http://symfony.com) comes out of the box with Monolog.
+- [Silex](http://silex.sensiolabs.org/) comes out of the box with Monolog.
+- [Laravel 4 & 5](http://laravel.com/) come out of the box with Monolog.
+- [Lumen](http://lumen.laravel.com/) comes out of the box with Monolog.
+- [PPI](http://www.ppi.io/) comes out of the box with Monolog.
+- [CakePHP](http://cakephp.org/) is usable with Monolog via the [cakephp-monolog](https://github.com/jadb/cakephp-monolog) plugin.
+- [Slim](http://www.slimframework.com/) is usable with Monolog via the [Slim-Monolog](https://github.com/Flynsarmy/Slim-Monolog) log writer.
+- [XOOPS 2.6](http://xoops.org/) comes out of the box with Monolog.
+- [Aura.Web_Project](https://github.com/auraphp/Aura.Web_Project) comes out of the box with Monolog.
+- [Nette Framework](http://nette.org/en/) can be used with Monolog via [Kdyby/Monolog](https://github.com/Kdyby/Monolog) extension.
+- [Proton Micro Framework](https://github.com/alexbilbie/Proton) comes out of the box with Monolog.
+
+### Author
+
+Jordi Boggiano - <j.boggiano@seld.be> - <http://twitter.com/seldaek><br />
+See also the list of [contributors](https://github.com/Seldaek/monolog/contributors) which participated in this project.
+
+### License
+
+Monolog is licensed under the MIT License - see the `LICENSE` file for details
+
+### Acknowledgements
+
+This library is heavily inspired by Python's [Logbook](https://logbook.readthedocs.io/en/stable/)
+library, although most concepts have been adjusted to fit to the PHP world.

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

@@ -0,0 +1,58 @@
+{
+    "name": "monolog/monolog",
+    "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
+    "keywords": ["log", "logging", "psr-3"],
+    "homepage": "http://github.com/Seldaek/monolog",
+    "type": "library",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Jordi Boggiano",
+            "email": "j.boggiano@seld.be",
+            "homepage": "http://seld.be"
+        }
+    ],
+    "require": {
+        "php": ">=5.3.0",
+        "psr/log": "~1.0"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "~4.5",
+        "graylog2/gelf-php": "~1.0",
+        "sentry/sentry": "^0.13",
+        "ruflin/elastica": ">=0.90 <3.0",
+        "doctrine/couchdb": "~1.0@dev",
+        "aws/aws-sdk-php": "^2.4.9 || ^3.0",
+        "php-amqplib/php-amqplib": "~2.4",
+        "swiftmailer/swiftmailer": "^5.3|^6.0",
+        "php-console/php-console": "^3.1.3",
+        "phpstan/phpstan": "^0.12.59"
+    },
+    "suggest": {
+        "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
+        "sentry/sentry": "Allow sending log messages to a Sentry server",
+        "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+        "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
+        "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
+        "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
+        "ext-mongo": "Allow sending log messages to a MongoDB server",
+        "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
+        "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
+        "rollbar/rollbar": "Allow sending log messages to Rollbar",
+        "php-console/php-console": "Allow sending log messages to Google Chrome"
+    },
+    "autoload": {
+        "psr-4": {"Monolog\\": "src/Monolog"}
+    },
+    "autoload-dev": {
+        "psr-4": {"Monolog\\": "tests/Monolog"}
+    },
+    "provide": {
+        "psr/log-implementation": "1.0.0"
+    },
+    "scripts": {
+        "test": "vendor/bin/phpunit",
+        "phpstan": "vendor/bin/phpstan analyse"
+    },
+    "lock": false
+}

+ 16 - 0
api/vendor/monolog/monolog/phpstan.neon.dist

@@ -0,0 +1,16 @@
+parameters:
+    level: 3
+
+    paths:
+        - src/
+#        - tests/
+
+
+    ignoreErrors:
+        - '#zend_monitor_|ZEND_MONITOR_#'
+        - '#RollbarNotifier#'
+        - '#Predis\\Client#'
+        - '#^Cannot call method ltrim\(\) on int\|false.$#'
+        - '#^Access to an undefined property Raven_Client::\$context.$#'
+        - '#MongoDB\\(Client|Collection)#'
+        - '#Gelf\\IMessagePublisher#'

+ 239 - 0
api/vendor/monolog/monolog/src/Monolog/ErrorHandler.php

@@ -0,0 +1,239 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog;
+
+use Psr\Log\LoggerInterface;
+use Psr\Log\LogLevel;
+use Monolog\Handler\AbstractHandler;
+
+/**
+ * Monolog error handler
+ *
+ * A facility to enable logging of runtime errors, exceptions and fatal errors.
+ *
+ * Quick setup: <code>ErrorHandler::register($logger);</code>
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class ErrorHandler
+{
+    private $logger;
+
+    private $previousExceptionHandler;
+    private $uncaughtExceptionLevel;
+
+    private $previousErrorHandler;
+    private $errorLevelMap;
+    private $handleOnlyReportedErrors;
+
+    private $hasFatalErrorHandler;
+    private $fatalLevel;
+    private $reservedMemory;
+    private $lastFatalTrace;
+    private static $fatalErrors = array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR);
+
+    public function __construct(LoggerInterface $logger)
+    {
+        $this->logger = $logger;
+    }
+
+    /**
+     * Registers a new ErrorHandler for a given Logger
+     *
+     * By default it will handle errors, exceptions and fatal errors
+     *
+     * @param  LoggerInterface $logger
+     * @param  array|false     $errorLevelMap  an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling
+     * @param  int|false       $exceptionLevel a LogLevel::* constant, or false to disable exception handling
+     * @param  int|false       $fatalLevel     a LogLevel::* constant, or false to disable fatal error handling
+     * @return ErrorHandler
+     */
+    public static function register(LoggerInterface $logger, $errorLevelMap = array(), $exceptionLevel = null, $fatalLevel = null)
+    {
+        //Forces the autoloader to run for LogLevel. Fixes an autoload issue at compile-time on PHP5.3. See https://github.com/Seldaek/monolog/pull/929
+        class_exists('\\Psr\\Log\\LogLevel', true);
+
+        /** @phpstan-ignore-next-line */
+        $handler = new static($logger);
+        if ($errorLevelMap !== false) {
+            $handler->registerErrorHandler($errorLevelMap);
+        }
+        if ($exceptionLevel !== false) {
+            $handler->registerExceptionHandler($exceptionLevel);
+        }
+        if ($fatalLevel !== false) {
+            $handler->registerFatalHandler($fatalLevel);
+        }
+
+        return $handler;
+    }
+
+    public function registerExceptionHandler($level = null, $callPrevious = true)
+    {
+        $prev = set_exception_handler(array($this, 'handleException'));
+        $this->uncaughtExceptionLevel = $level;
+        if ($callPrevious && $prev) {
+            $this->previousExceptionHandler = $prev;
+        }
+    }
+
+    public function registerErrorHandler(array $levelMap = array(), $callPrevious = true, $errorTypes = -1, $handleOnlyReportedErrors = true)
+    {
+        $prev = set_error_handler(array($this, 'handleError'), $errorTypes);
+        $this->errorLevelMap = array_replace($this->defaultErrorLevelMap(), $levelMap);
+        if ($callPrevious) {
+            $this->previousErrorHandler = $prev ?: true;
+        }
+
+        $this->handleOnlyReportedErrors = $handleOnlyReportedErrors;
+    }
+
+    public function registerFatalHandler($level = null, $reservedMemorySize = 20)
+    {
+        register_shutdown_function(array($this, 'handleFatalError'));
+
+        $this->reservedMemory = str_repeat(' ', 1024 * $reservedMemorySize);
+        $this->fatalLevel = $level;
+        $this->hasFatalErrorHandler = true;
+    }
+
+    protected function defaultErrorLevelMap()
+    {
+        return array(
+            E_ERROR             => LogLevel::CRITICAL,
+            E_WARNING           => LogLevel::WARNING,
+            E_PARSE             => LogLevel::ALERT,
+            E_NOTICE            => LogLevel::NOTICE,
+            E_CORE_ERROR        => LogLevel::CRITICAL,
+            E_CORE_WARNING      => LogLevel::WARNING,
+            E_COMPILE_ERROR     => LogLevel::ALERT,
+            E_COMPILE_WARNING   => LogLevel::WARNING,
+            E_USER_ERROR        => LogLevel::ERROR,
+            E_USER_WARNING      => LogLevel::WARNING,
+            E_USER_NOTICE       => LogLevel::NOTICE,
+            E_STRICT            => LogLevel::NOTICE,
+            E_RECOVERABLE_ERROR => LogLevel::ERROR,
+            E_DEPRECATED        => LogLevel::NOTICE,
+            E_USER_DEPRECATED   => LogLevel::NOTICE,
+        );
+    }
+
+    /**
+     * @private
+     */
+    public function handleException($e)
+    {
+        $this->logger->log(
+            $this->uncaughtExceptionLevel === null ? LogLevel::ERROR : $this->uncaughtExceptionLevel,
+            sprintf('Uncaught Exception %s: "%s" at %s line %s', Utils::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()),
+            array('exception' => $e)
+        );
+
+        if ($this->previousExceptionHandler) {
+            call_user_func($this->previousExceptionHandler, $e);
+        }
+
+        exit(255);
+    }
+
+    /**
+     * @private
+     */
+    public function handleError($code, $message, $file = '', $line = 0, $context = array())
+    {
+        if ($this->handleOnlyReportedErrors && !(error_reporting() & $code)) {
+            return;
+        }
+
+        // fatal error codes are ignored if a fatal error handler is present as well to avoid duplicate log entries
+        if (!$this->hasFatalErrorHandler || !in_array($code, self::$fatalErrors, true)) {
+            $level = isset($this->errorLevelMap[$code]) ? $this->errorLevelMap[$code] : LogLevel::CRITICAL;
+            $this->logger->log($level, self::codeToString($code).': '.$message, array('code' => $code, 'message' => $message, 'file' => $file, 'line' => $line));
+        } else {
+            // http://php.net/manual/en/function.debug-backtrace.php
+            // As of 5.3.6, DEBUG_BACKTRACE_IGNORE_ARGS option was added.
+            // Any version less than 5.3.6 must use the DEBUG_BACKTRACE_IGNORE_ARGS constant value '2'.
+            $trace = debug_backtrace((PHP_VERSION_ID < 50306) ? 2 : DEBUG_BACKTRACE_IGNORE_ARGS);
+            array_shift($trace); // Exclude handleError from trace
+            $this->lastFatalTrace = $trace;
+        }
+
+        if ($this->previousErrorHandler === true) {
+            return false;
+        } elseif ($this->previousErrorHandler) {
+            return call_user_func($this->previousErrorHandler, $code, $message, $file, $line, $context);
+        }
+    }
+
+    /**
+     * @private
+     */
+    public function handleFatalError()
+    {
+        $this->reservedMemory = null;
+
+        $lastError = error_get_last();
+        if ($lastError && in_array($lastError['type'], self::$fatalErrors, true)) {
+            $this->logger->log(
+                $this->fatalLevel === null ? LogLevel::ALERT : $this->fatalLevel,
+                'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'],
+                array('code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $this->lastFatalTrace)
+            );
+
+            if ($this->logger instanceof Logger) {
+                foreach ($this->logger->getHandlers() as $handler) {
+                    if ($handler instanceof AbstractHandler) {
+                        $handler->close();
+                    }
+                }
+            }
+        }
+    }
+
+    private static function codeToString($code)
+    {
+        switch ($code) {
+            case E_ERROR:
+                return 'E_ERROR';
+            case E_WARNING:
+                return 'E_WARNING';
+            case E_PARSE:
+                return 'E_PARSE';
+            case E_NOTICE:
+                return 'E_NOTICE';
+            case E_CORE_ERROR:
+                return 'E_CORE_ERROR';
+            case E_CORE_WARNING:
+                return 'E_CORE_WARNING';
+            case E_COMPILE_ERROR:
+                return 'E_COMPILE_ERROR';
+            case E_COMPILE_WARNING:
+                return 'E_COMPILE_WARNING';
+            case E_USER_ERROR:
+                return 'E_USER_ERROR';
+            case E_USER_WARNING:
+                return 'E_USER_WARNING';
+            case E_USER_NOTICE:
+                return 'E_USER_NOTICE';
+            case E_STRICT:
+                return 'E_STRICT';
+            case E_RECOVERABLE_ERROR:
+                return 'E_RECOVERABLE_ERROR';
+            case E_DEPRECATED:
+                return 'E_DEPRECATED';
+            case E_USER_DEPRECATED:
+                return 'E_USER_DEPRECATED';
+        }
+
+        return 'Unknown PHP error';
+    }
+}

+ 78 - 0
api/vendor/monolog/monolog/src/Monolog/Formatter/ChromePHPFormatter.php

@@ -0,0 +1,78 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Formatter;
+
+use Monolog\Logger;
+
+/**
+ * Formats a log message according to the ChromePHP array format
+ *
+ * @author Christophe Coevoet <stof@notk.org>
+ */
+class ChromePHPFormatter implements FormatterInterface
+{
+    /**
+     * Translates Monolog log levels to Wildfire levels.
+     */
+    private $logLevels = array(
+        Logger::DEBUG     => 'log',
+        Logger::INFO      => 'info',
+        Logger::NOTICE    => 'info',
+        Logger::WARNING   => 'warn',
+        Logger::ERROR     => 'error',
+        Logger::CRITICAL  => 'error',
+        Logger::ALERT     => 'error',
+        Logger::EMERGENCY => 'error',
+    );
+
+    /**
+     * {@inheritdoc}
+     */
+    public function format(array $record)
+    {
+        // Retrieve the line and file if set and remove them from the formatted extra
+        $backtrace = 'unknown';
+        if (isset($record['extra']['file'], $record['extra']['line'])) {
+            $backtrace = $record['extra']['file'].' : '.$record['extra']['line'];
+            unset($record['extra']['file'], $record['extra']['line']);
+        }
+
+        $message = array('message' => $record['message']);
+        if ($record['context']) {
+            $message['context'] = $record['context'];
+        }
+        if ($record['extra']) {
+            $message['extra'] = $record['extra'];
+        }
+        if (count($message) === 1) {
+            $message = reset($message);
+        }
+
+        return array(
+            $record['channel'],
+            $message,
+            $backtrace,
+            $this->logLevels[$record['level']],
+        );
+    }
+
+    public function formatBatch(array $records)
+    {
+        $formatted = array();
+
+        foreach ($records as $record) {
+            $formatted[] = $this->format($record);
+        }
+
+        return $formatted;
+    }
+}

+ 89 - 0
api/vendor/monolog/monolog/src/Monolog/Formatter/ElasticaFormatter.php

@@ -0,0 +1,89 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Formatter;
+
+use Elastica\Document;
+
+/**
+ * Format a log message into an Elastica Document
+ *
+ * @author Jelle Vink <jelle.vink@gmail.com>
+ */
+class ElasticaFormatter extends NormalizerFormatter
+{
+    /**
+     * @var string Elastic search index name
+     */
+    protected $index;
+
+    /**
+     * @var string Elastic search document type
+     */
+    protected $type;
+
+    /**
+     * @param string $index Elastic Search index name
+     * @param string $type  Elastic Search document type
+     */
+    public function __construct($index, $type)
+    {
+        // elasticsearch requires a ISO 8601 format date with optional millisecond precision.
+        parent::__construct('Y-m-d\TH:i:s.uP');
+
+        $this->index = $index;
+        $this->type = $type;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function format(array $record)
+    {
+        $record = parent::format($record);
+
+        return $this->getDocument($record);
+    }
+
+    /**
+     * Getter index
+     * @return string
+     */
+    public function getIndex()
+    {
+        return $this->index;
+    }
+
+    /**
+     * Getter type
+     * @return string
+     */
+    public function getType()
+    {
+        return $this->type;
+    }
+
+    /**
+     * Convert a log message into an Elastica Document
+     *
+     * @param  array    $record Log message
+     * @return Document
+     */
+    protected function getDocument($record)
+    {
+        $document = new Document();
+        $document->setData($record);
+        $document->setType($this->type);
+        $document->setIndex($this->index);
+
+        return $document;
+    }
+}

+ 116 - 0
api/vendor/monolog/monolog/src/Monolog/Formatter/FlowdockFormatter.php

@@ -0,0 +1,116 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Formatter;
+
+/**
+ * formats the record to be used in the FlowdockHandler
+ *
+ * @author Dominik Liebler <liebler.dominik@gmail.com>
+ */
+class FlowdockFormatter implements FormatterInterface
+{
+    /**
+     * @var string
+     */
+    private $source;
+
+    /**
+     * @var string
+     */
+    private $sourceEmail;
+
+    /**
+     * @param string $source
+     * @param string $sourceEmail
+     */
+    public function __construct($source, $sourceEmail)
+    {
+        $this->source = $source;
+        $this->sourceEmail = $sourceEmail;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function format(array $record)
+    {
+        $tags = array(
+            '#logs',
+            '#' . strtolower($record['level_name']),
+            '#' . $record['channel'],
+        );
+
+        foreach ($record['extra'] as $value) {
+            $tags[] = '#' . $value;
+        }
+
+        $subject = sprintf(
+            'in %s: %s - %s',
+            $this->source,
+            $record['level_name'],
+            $this->getShortMessage($record['message'])
+        );
+
+        $record['flowdock'] = array(
+            'source' => $this->source,
+            'from_address' => $this->sourceEmail,
+            'subject' => $subject,
+            'content' => $record['message'],
+            'tags' => $tags,
+            'project' => $this->source,
+        );
+
+        return $record;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function formatBatch(array $records)
+    {
+        $formatted = array();
+
+        foreach ($records as $record) {
+            $formatted[] = $this->format($record);
+        }
+
+        return $formatted;
+    }
+
+    /**
+     * @param string $message
+     *
+     * @return string
+     */
+    public function getShortMessage($message)
+    {
+        static $hasMbString;
+
+        if (null === $hasMbString) {
+            $hasMbString = function_exists('mb_strlen');
+        }
+
+        $maxLength = 45;
+
+        if ($hasMbString) {
+            if (mb_strlen($message, 'UTF-8') > $maxLength) {
+                $message = mb_substr($message, 0, $maxLength - 4, 'UTF-8') . ' ...';
+            }
+        } else {
+            if (strlen($message) > $maxLength) {
+                $message = substr($message, 0, $maxLength - 4) . ' ...';
+            }
+        }
+
+        return $message;
+    }
+}

+ 88 - 0
api/vendor/monolog/monolog/src/Monolog/Formatter/FluentdFormatter.php

@@ -0,0 +1,88 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Formatter;
+
+use Monolog\Utils;
+
+/**
+ * Class FluentdFormatter
+ *
+ * Serializes a log message to Fluentd unix socket protocol
+ *
+ * Fluentd config:
+ *
+ * <source>
+ *  type unix
+ *  path /var/run/td-agent/td-agent.sock
+ * </source>
+ *
+ * Monolog setup:
+ *
+ * $logger = new Monolog\Logger('fluent.tag');
+ * $fluentHandler = new Monolog\Handler\SocketHandler('unix:///var/run/td-agent/td-agent.sock');
+ * $fluentHandler->setFormatter(new Monolog\Formatter\FluentdFormatter());
+ * $logger->pushHandler($fluentHandler);
+ *
+ * @author Andrius Putna <fordnox@gmail.com>
+ */
+class FluentdFormatter implements FormatterInterface
+{
+    /**
+     * @var bool $levelTag should message level be a part of the fluentd tag
+     */
+    protected $levelTag = false;
+
+    public function __construct($levelTag = false)
+    {
+        if (!function_exists('json_encode')) {
+            throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s FluentdUnixFormatter');
+        }
+
+        $this->levelTag = (bool) $levelTag;
+    }
+
+    public function isUsingLevelsInTag()
+    {
+        return $this->levelTag;
+    }
+
+    public function format(array $record)
+    {
+        $tag = $record['channel'];
+        if ($this->levelTag) {
+            $tag .= '.' . strtolower($record['level_name']);
+        }
+
+        $message = array(
+            'message' => $record['message'],
+            'context' => $record['context'],
+            'extra' => $record['extra'],
+        );
+
+        if (!$this->levelTag) {
+            $message['level'] = $record['level'];
+            $message['level_name'] = $record['level_name'];
+        }
+
+        return Utils::jsonEncode(array($tag, $record['datetime']->getTimestamp(), $message));
+    }
+
+    public function formatBatch(array $records)
+    {
+        $message = '';
+        foreach ($records as $record) {
+            $message .= $this->format($record);
+        }
+
+        return $message;
+    }
+}

+ 36 - 0
api/vendor/monolog/monolog/src/Monolog/Formatter/FormatterInterface.php

@@ -0,0 +1,36 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Formatter;
+
+/**
+ * Interface for formatters
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+interface FormatterInterface
+{
+    /**
+     * Formats a log record.
+     *
+     * @param  array $record A record to format
+     * @return mixed The formatted record
+     */
+    public function format(array $record);
+
+    /**
+     * Formats a set of log records.
+     *
+     * @param  array $records A set of records to format
+     * @return mixed The formatted set of records
+     */
+    public function formatBatch(array $records);
+}

+ 138 - 0
api/vendor/monolog/monolog/src/Monolog/Formatter/GelfMessageFormatter.php

@@ -0,0 +1,138 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Formatter;
+
+use Monolog\Logger;
+use Gelf\Message;
+
+/**
+ * Serializes a log message to GELF
+ * @see http://www.graylog2.org/about/gelf
+ *
+ * @author Matt Lehner <mlehner@gmail.com>
+ */
+class GelfMessageFormatter extends NormalizerFormatter
+{
+    const DEFAULT_MAX_LENGTH = 32766;
+
+    /**
+     * @var string the name of the system for the Gelf log message
+     */
+    protected $systemName;
+
+    /**
+     * @var string a prefix for 'extra' fields from the Monolog record (optional)
+     */
+    protected $extraPrefix;
+
+    /**
+     * @var string a prefix for 'context' fields from the Monolog record (optional)
+     */
+    protected $contextPrefix;
+
+    /**
+     * @var int max length per field
+     */
+    protected $maxLength;
+
+    /**
+     * Translates Monolog log levels to Graylog2 log priorities.
+     */
+    private $logLevels = array(
+        Logger::DEBUG     => 7,
+        Logger::INFO      => 6,
+        Logger::NOTICE    => 5,
+        Logger::WARNING   => 4,
+        Logger::ERROR     => 3,
+        Logger::CRITICAL  => 2,
+        Logger::ALERT     => 1,
+        Logger::EMERGENCY => 0,
+    );
+
+    public function __construct($systemName = null, $extraPrefix = null, $contextPrefix = 'ctxt_', $maxLength = null)
+    {
+        parent::__construct('U.u');
+
+        $this->systemName = $systemName ?: gethostname();
+
+        $this->extraPrefix = $extraPrefix;
+        $this->contextPrefix = $contextPrefix;
+        $this->maxLength = is_null($maxLength) ? self::DEFAULT_MAX_LENGTH : $maxLength;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function format(array $record)
+    {
+        $record = parent::format($record);
+
+        if (!isset($record['datetime'], $record['message'], $record['level'])) {
+            throw new \InvalidArgumentException('The record should at least contain datetime, message and level keys, '.var_export($record, true).' given');
+        }
+
+        $message = new Message();
+        $message
+            ->setTimestamp($record['datetime'])
+            ->setShortMessage((string) $record['message'])
+            ->setHost($this->systemName)
+            ->setLevel($this->logLevels[$record['level']]);
+
+        // message length + system name length + 200 for padding / metadata 
+        $len = 200 + strlen((string) $record['message']) + strlen($this->systemName);
+
+        if ($len > $this->maxLength) {
+            $message->setShortMessage(substr($record['message'], 0, $this->maxLength));
+        }
+
+        if (isset($record['channel'])) {
+            $message->setFacility($record['channel']);
+        }
+        if (isset($record['extra']['line'])) {
+            $message->setLine($record['extra']['line']);
+            unset($record['extra']['line']);
+        }
+        if (isset($record['extra']['file'])) {
+            $message->setFile($record['extra']['file']);
+            unset($record['extra']['file']);
+        }
+
+        foreach ($record['extra'] as $key => $val) {
+            $val = is_scalar($val) || null === $val ? $val : $this->toJson($val);
+            $len = strlen($this->extraPrefix . $key . $val);
+            if ($len > $this->maxLength) {
+                $message->setAdditional($this->extraPrefix . $key, substr($val, 0, $this->maxLength));
+                break;
+            }
+            $message->setAdditional($this->extraPrefix . $key, $val);
+        }
+
+        foreach ($record['context'] as $key => $val) {
+            $val = is_scalar($val) || null === $val ? $val : $this->toJson($val);
+            $len = strlen($this->contextPrefix . $key . $val);
+            if ($len > $this->maxLength) {
+                $message->setAdditional($this->contextPrefix . $key, substr($val, 0, $this->maxLength));
+                break;
+            }
+            $message->setAdditional($this->contextPrefix . $key, $val);
+        }
+
+        if (null === $message->getFile() && isset($record['context']['exception']['file'])) {
+            if (preg_match("/^(.+):([0-9]+)$/", $record['context']['exception']['file'], $matches)) {
+                $message->setFile($matches[1]);
+                $message->setLine($matches[2]);
+            }
+        }
+
+        return $message;
+    }
+}

+ 142 - 0
api/vendor/monolog/monolog/src/Monolog/Formatter/HtmlFormatter.php

@@ -0,0 +1,142 @@
+<?php
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Formatter;
+
+use Monolog\Logger;
+use Monolog\Utils;
+
+/**
+ * Formats incoming records into an HTML table
+ *
+ * This is especially useful for html email logging
+ *
+ * @author Tiago Brito <tlfbrito@gmail.com>
+ */
+class HtmlFormatter extends NormalizerFormatter
+{
+    /**
+     * Translates Monolog log levels to html color priorities.
+     */
+    protected $logLevels = array(
+        Logger::DEBUG     => '#cccccc',
+        Logger::INFO      => '#468847',
+        Logger::NOTICE    => '#3a87ad',
+        Logger::WARNING   => '#c09853',
+        Logger::ERROR     => '#f0ad4e',
+        Logger::CRITICAL  => '#FF7708',
+        Logger::ALERT     => '#C12A19',
+        Logger::EMERGENCY => '#000000',
+    );
+
+    /**
+     * @param string $dateFormat The format of the timestamp: one supported by DateTime::format
+     */
+    public function __construct($dateFormat = null)
+    {
+        parent::__construct($dateFormat);
+    }
+
+    /**
+     * Creates an HTML table row
+     *
+     * @param  string $th       Row header content
+     * @param  string $td       Row standard cell content
+     * @param  bool   $escapeTd false if td content must not be html escaped
+     * @return string
+     */
+    protected function addRow($th, $td = ' ', $escapeTd = true)
+    {
+        $th = htmlspecialchars($th, ENT_NOQUOTES, 'UTF-8');
+        if ($escapeTd) {
+            $td = '<pre>'.htmlspecialchars($td, ENT_NOQUOTES, 'UTF-8').'</pre>';
+        }
+
+        return "<tr style=\"padding: 4px;text-align: left;\">\n<th style=\"vertical-align: top;background: #ccc;color: #000\" width=\"100\">$th:</th>\n<td style=\"padding: 4px;text-align: left;vertical-align: top;background: #eee;color: #000\">".$td."</td>\n</tr>";
+    }
+
+    /**
+     * Create a HTML h1 tag
+     *
+     * @param  string $title Text to be in the h1
+     * @param  int    $level Error level
+     * @return string
+     */
+    protected function addTitle($title, $level)
+    {
+        $title = htmlspecialchars($title, ENT_NOQUOTES, 'UTF-8');
+
+        return '<h1 style="background: '.$this->logLevels[$level].';color: #ffffff;padding: 5px;" class="monolog-output">'.$title.'</h1>';
+    }
+
+    /**
+     * Formats a log record.
+     *
+     * @param  array $record A record to format
+     * @return mixed The formatted record
+     */
+    public function format(array $record)
+    {
+        $output = $this->addTitle($record['level_name'], $record['level']);
+        $output .= '<table cellspacing="1" width="100%" class="monolog-output">';
+
+        $output .= $this->addRow('Message', (string) $record['message']);
+        $output .= $this->addRow('Time', $record['datetime']->format($this->dateFormat));
+        $output .= $this->addRow('Channel', $record['channel']);
+        if ($record['context']) {
+            $embeddedTable = '<table cellspacing="1" width="100%">';
+            foreach ($record['context'] as $key => $value) {
+                $embeddedTable .= $this->addRow($key, $this->convertToString($value));
+            }
+            $embeddedTable .= '</table>';
+            $output .= $this->addRow('Context', $embeddedTable, false);
+        }
+        if ($record['extra']) {
+            $embeddedTable = '<table cellspacing="1" width="100%">';
+            foreach ($record['extra'] as $key => $value) {
+                $embeddedTable .= $this->addRow($key, $this->convertToString($value));
+            }
+            $embeddedTable .= '</table>';
+            $output .= $this->addRow('Extra', $embeddedTable, false);
+        }
+
+        return $output.'</table>';
+    }
+
+    /**
+     * Formats a set of log records.
+     *
+     * @param  array $records A set of records to format
+     * @return mixed The formatted set of records
+     */
+    public function formatBatch(array $records)
+    {
+        $message = '';
+        foreach ($records as $record) {
+            $message .= $this->format($record);
+        }
+
+        return $message;
+    }
+
+    protected function convertToString($data)
+    {
+        if (null === $data || is_scalar($data)) {
+            return (string) $data;
+        }
+
+        $data = $this->normalize($data);
+        if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
+            return Utils::jsonEncode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, true);
+        }
+
+        return str_replace('\\/', '/', Utils::jsonEncode($data, null, true));
+    }
+}

+ 212 - 0
api/vendor/monolog/monolog/src/Monolog/Formatter/JsonFormatter.php

@@ -0,0 +1,212 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Formatter;
+
+use Exception;
+use Monolog\Utils;
+use Throwable;
+
+/**
+ * Encodes whatever record data is passed to it as json
+ *
+ * This can be useful to log to databases or remote APIs
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class JsonFormatter extends NormalizerFormatter
+{
+    const BATCH_MODE_JSON = 1;
+    const BATCH_MODE_NEWLINES = 2;
+
+    protected $batchMode;
+    protected $appendNewline;
+
+    /**
+     * @var bool
+     */
+    protected $includeStacktraces = false;
+
+    /**
+     * @param int $batchMode
+     * @param bool $appendNewline
+     */
+    public function __construct($batchMode = self::BATCH_MODE_JSON, $appendNewline = true)
+    {
+        $this->batchMode = $batchMode;
+        $this->appendNewline = $appendNewline;
+    }
+
+    /**
+     * The batch mode option configures the formatting style for
+     * multiple records. By default, multiple records will be
+     * formatted as a JSON-encoded array. However, for
+     * compatibility with some API endpoints, alternative styles
+     * are available.
+     *
+     * @return int
+     */
+    public function getBatchMode()
+    {
+        return $this->batchMode;
+    }
+
+    /**
+     * True if newlines are appended to every formatted record
+     *
+     * @return bool
+     */
+    public function isAppendingNewlines()
+    {
+        return $this->appendNewline;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function format(array $record)
+    {
+        return $this->toJson($this->normalize($record), true) . ($this->appendNewline ? "\n" : '');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function formatBatch(array $records)
+    {
+        switch ($this->batchMode) {
+            case static::BATCH_MODE_NEWLINES:
+                return $this->formatBatchNewlines($records);
+
+            case static::BATCH_MODE_JSON:
+            default:
+                return $this->formatBatchJson($records);
+        }
+    }
+
+    /**
+     * @param bool $include
+     */
+    public function includeStacktraces($include = true)
+    {
+        $this->includeStacktraces = $include;
+    }
+
+    /**
+     * Return a JSON-encoded array of records.
+     *
+     * @param  array  $records
+     * @return string
+     */
+    protected function formatBatchJson(array $records)
+    {
+        return $this->toJson($this->normalize($records), true);
+    }
+
+    /**
+     * Use new lines to separate records instead of a
+     * JSON-encoded array.
+     *
+     * @param  array  $records
+     * @return string
+     */
+    protected function formatBatchNewlines(array $records)
+    {
+        $instance = $this;
+
+        $oldNewline = $this->appendNewline;
+        $this->appendNewline = false;
+        array_walk($records, function (&$value, $key) use ($instance) {
+            $value = $instance->format($value);
+        });
+        $this->appendNewline = $oldNewline;
+
+        return implode("\n", $records);
+    }
+
+    /**
+     * Normalizes given $data.
+     *
+     * @param mixed $data
+     *
+     * @return mixed
+     */
+    protected function normalize($data, $depth = 0)
+    {
+        if ($depth > 9) {
+            return 'Over 9 levels deep, aborting normalization';
+        }
+
+        if (is_array($data)) {
+            $normalized = array();
+
+            $count = 1;
+            foreach ($data as $key => $value) {
+                if ($count++ > 1000) {
+                    $normalized['...'] = 'Over 1000 items ('.count($data).' total), aborting normalization';
+                    break;
+                }
+
+                $normalized[$key] = $this->normalize($value, $depth+1);
+            }
+
+            return $normalized;
+        }
+
+        if ($data instanceof Exception || $data instanceof Throwable) {
+            return $this->normalizeException($data);
+        }
+
+        if (is_resource($data)) {
+            return parent::normalize($data);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Normalizes given exception with or without its own stack trace based on
+     * `includeStacktraces` property.
+     *
+     * @param Exception|Throwable $e
+     *
+     * @return array
+     */
+    protected function normalizeException($e)
+    {
+        // TODO 2.0 only check for Throwable
+        if (!$e instanceof Exception && !$e instanceof Throwable) {
+            throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Utils::getClass($e));
+        }
+
+        $data = array(
+            'class' => Utils::getClass($e),
+            'message' => $e->getMessage(),
+            'code' => (int) $e->getCode(),
+            'file' => $e->getFile().':'.$e->getLine(),
+        );
+
+        if ($this->includeStacktraces) {
+            $trace = $e->getTrace();
+            foreach ($trace as $frame) {
+                if (isset($frame['file'])) {
+                    $data['trace'][] = $frame['file'].':'.$frame['line'];
+                }
+            }
+        }
+
+        if ($previous = $e->getPrevious()) {
+            $data['previous'] = $this->normalizeException($previous);
+        }
+
+        return $data;
+    }
+}

+ 181 - 0
api/vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php

@@ -0,0 +1,181 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Formatter;
+
+use Monolog\Utils;
+
+/**
+ * Formats incoming records into a one-line string
+ *
+ * This is especially useful for logging to files
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author Christophe Coevoet <stof@notk.org>
+ */
+class LineFormatter extends NormalizerFormatter
+{
+    const SIMPLE_FORMAT = "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n";
+
+    protected $format;
+    protected $allowInlineLineBreaks;
+    protected $ignoreEmptyContextAndExtra;
+    protected $includeStacktraces;
+
+    /**
+     * @param string $format                     The format of the message
+     * @param string $dateFormat                 The format of the timestamp: one supported by DateTime::format
+     * @param bool   $allowInlineLineBreaks      Whether to allow inline line breaks in log entries
+     * @param bool   $ignoreEmptyContextAndExtra
+     */
+    public function __construct($format = null, $dateFormat = null, $allowInlineLineBreaks = false, $ignoreEmptyContextAndExtra = false)
+    {
+        $this->format = $format ?: static::SIMPLE_FORMAT;
+        $this->allowInlineLineBreaks = $allowInlineLineBreaks;
+        $this->ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra;
+        parent::__construct($dateFormat);
+    }
+
+    public function includeStacktraces($include = true)
+    {
+        $this->includeStacktraces = $include;
+        if ($this->includeStacktraces) {
+            $this->allowInlineLineBreaks = true;
+        }
+    }
+
+    public function allowInlineLineBreaks($allow = true)
+    {
+        $this->allowInlineLineBreaks = $allow;
+    }
+
+    public function ignoreEmptyContextAndExtra($ignore = true)
+    {
+        $this->ignoreEmptyContextAndExtra = $ignore;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function format(array $record)
+    {
+        $vars = parent::format($record);
+
+        $output = $this->format;
+
+        foreach ($vars['extra'] as $var => $val) {
+            if (false !== strpos($output, '%extra.'.$var.'%')) {
+                $output = str_replace('%extra.'.$var.'%', $this->stringify($val), $output);
+                unset($vars['extra'][$var]);
+            }
+        }
+
+
+        foreach ($vars['context'] as $var => $val) {
+            if (false !== strpos($output, '%context.'.$var.'%')) {
+                $output = str_replace('%context.'.$var.'%', $this->stringify($val), $output);
+                unset($vars['context'][$var]);
+            }
+        }
+
+        if ($this->ignoreEmptyContextAndExtra) {
+            if (empty($vars['context'])) {
+                unset($vars['context']);
+                $output = str_replace('%context%', '', $output);
+            }
+
+            if (empty($vars['extra'])) {
+                unset($vars['extra']);
+                $output = str_replace('%extra%', '', $output);
+            }
+        }
+
+        foreach ($vars as $var => $val) {
+            if (false !== strpos($output, '%'.$var.'%')) {
+                $output = str_replace('%'.$var.'%', $this->stringify($val), $output);
+            }
+        }
+
+        // remove leftover %extra.xxx% and %context.xxx% if any
+        if (false !== strpos($output, '%')) {
+            $output = preg_replace('/%(?:extra|context)\..+?%/', '', $output);
+        }
+
+        return $output;
+    }
+
+    public function formatBatch(array $records)
+    {
+        $message = '';
+        foreach ($records as $record) {
+            $message .= $this->format($record);
+        }
+
+        return $message;
+    }
+
+    public function stringify($value)
+    {
+        return $this->replaceNewlines($this->convertToString($value));
+    }
+
+    protected function normalizeException($e)
+    {
+        // TODO 2.0 only check for Throwable
+        if (!$e instanceof \Exception && !$e instanceof \Throwable) {
+            throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Utils::getClass($e));
+        }
+
+        $previousText = '';
+        if ($previous = $e->getPrevious()) {
+            do {
+                $previousText .= ', '.Utils::getClass($previous).'(code: '.$previous->getCode().'): '.$previous->getMessage().' at '.$previous->getFile().':'.$previous->getLine();
+            } while ($previous = $previous->getPrevious());
+        }
+
+        $str = '[object] ('.Utils::getClass($e).'(code: '.$e->getCode().'): '.$e->getMessage().' at '.$e->getFile().':'.$e->getLine().$previousText.')';
+        if ($this->includeStacktraces) {
+            $str .= "\n[stacktrace]\n".$e->getTraceAsString()."\n";
+        }
+
+        return $str;
+    }
+
+    protected function convertToString($data)
+    {
+        if (null === $data || is_bool($data)) {
+            return var_export($data, true);
+        }
+
+        if (is_scalar($data)) {
+            return (string) $data;
+        }
+
+        if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
+            return $this->toJson($data, true);
+        }
+
+        return str_replace('\\/', '/', $this->toJson($data, true));
+    }
+
+    protected function replaceNewlines($str)
+    {
+        if ($this->allowInlineLineBreaks) {
+            if (0 === strpos($str, '{')) {
+                return str_replace(array('\r', '\n'), array("\r", "\n"), $str);
+            }
+
+            return $str;
+        }
+
+        return str_replace(array("\r\n", "\r", "\n"), ' ', $str);
+    }
+}

+ 47 - 0
api/vendor/monolog/monolog/src/Monolog/Formatter/LogglyFormatter.php

@@ -0,0 +1,47 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Formatter;
+
+/**
+ * Encodes message information into JSON in a format compatible with Loggly.
+ *
+ * @author Adam Pancutt <adam@pancutt.com>
+ */
+class LogglyFormatter extends JsonFormatter
+{
+    /**
+     * Overrides the default batch mode to new lines for compatibility with the
+     * Loggly bulk API.
+     *
+     * @param int $batchMode
+     */
+    public function __construct($batchMode = self::BATCH_MODE_NEWLINES, $appendNewline = false)
+    {
+        parent::__construct($batchMode, $appendNewline);
+    }
+
+    /**
+     * Appends the 'timestamp' parameter for indexing by Loggly.
+     *
+     * @see https://www.loggly.com/docs/automated-parsing/#json
+     * @see \Monolog\Formatter\JsonFormatter::format()
+     */
+    public function format(array $record)
+    {
+        if (isset($record["datetime"]) && ($record["datetime"] instanceof \DateTime)) {
+            $record["timestamp"] = $record["datetime"]->format("Y-m-d\TH:i:s.uO");
+            // TODO 2.0 unset the 'datetime' parameter, retained for BC
+        }
+
+        return parent::format($record);
+    }
+}

+ 166 - 0
api/vendor/monolog/monolog/src/Monolog/Formatter/LogstashFormatter.php

@@ -0,0 +1,166 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Formatter;
+
+/**
+ * Serializes a log message to Logstash Event Format
+ *
+ * @see http://logstash.net/
+ * @see https://github.com/logstash/logstash/blob/master/lib/logstash/event.rb
+ *
+ * @author Tim Mower <timothy.mower@gmail.com>
+ */
+class LogstashFormatter extends NormalizerFormatter
+{
+    const V0 = 0;
+    const V1 = 1;
+
+    /**
+     * @var string the name of the system for the Logstash log message, used to fill the @source field
+     */
+    protected $systemName;
+
+    /**
+     * @var string an application name for the Logstash log message, used to fill the @type field
+     */
+    protected $applicationName;
+
+    /**
+     * @var string a prefix for 'extra' fields from the Monolog record (optional)
+     */
+    protected $extraPrefix;
+
+    /**
+     * @var string a prefix for 'context' fields from the Monolog record (optional)
+     */
+    protected $contextPrefix;
+
+    /**
+     * @var int logstash format version to use
+     */
+    protected $version;
+
+    /**
+     * @param string $applicationName the application that sends the data, used as the "type" field of logstash
+     * @param string $systemName      the system/machine name, used as the "source" field of logstash, defaults to the hostname of the machine
+     * @param string $extraPrefix     prefix for extra keys inside logstash "fields"
+     * @param string $contextPrefix   prefix for context keys inside logstash "fields", defaults to ctxt_
+     * @param int    $version         the logstash format version to use, defaults to 0
+     */
+    public function __construct($applicationName, $systemName = null, $extraPrefix = null, $contextPrefix = 'ctxt_', $version = self::V0)
+    {
+        // logstash requires a ISO 8601 format date with optional millisecond precision.
+        parent::__construct('Y-m-d\TH:i:s.uP');
+
+        $this->systemName = $systemName ?: gethostname();
+        $this->applicationName = $applicationName;
+        $this->extraPrefix = $extraPrefix;
+        $this->contextPrefix = $contextPrefix;
+        $this->version = $version;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function format(array $record)
+    {
+        $record = parent::format($record);
+
+        if ($this->version === self::V1) {
+            $message = $this->formatV1($record);
+        } else {
+            $message = $this->formatV0($record);
+        }
+
+        return $this->toJson($message) . "\n";
+    }
+
+    protected function formatV0(array $record)
+    {
+        if (empty($record['datetime'])) {
+            $record['datetime'] = gmdate('c');
+        }
+        $message = array(
+            '@timestamp' => $record['datetime'],
+            '@source' => $this->systemName,
+            '@fields' => array(),
+        );
+        if (isset($record['message'])) {
+            $message['@message'] = $record['message'];
+        }
+        if (isset($record['channel'])) {
+            $message['@tags'] = array($record['channel']);
+            $message['@fields']['channel'] = $record['channel'];
+        }
+        if (isset($record['level'])) {
+            $message['@fields']['level'] = $record['level'];
+        }
+        if ($this->applicationName) {
+            $message['@type'] = $this->applicationName;
+        }
+        if (isset($record['extra']['server'])) {
+            $message['@source_host'] = $record['extra']['server'];
+        }
+        if (isset($record['extra']['url'])) {
+            $message['@source_path'] = $record['extra']['url'];
+        }
+        if (!empty($record['extra'])) {
+            foreach ($record['extra'] as $key => $val) {
+                $message['@fields'][$this->extraPrefix . $key] = $val;
+            }
+        }
+        if (!empty($record['context'])) {
+            foreach ($record['context'] as $key => $val) {
+                $message['@fields'][$this->contextPrefix . $key] = $val;
+            }
+        }
+
+        return $message;
+    }
+
+    protected function formatV1(array $record)
+    {
+        if (empty($record['datetime'])) {
+            $record['datetime'] = gmdate('c');
+        }
+        $message = array(
+            '@timestamp' => $record['datetime'],
+            '@version' => 1,
+            'host' => $this->systemName,
+        );
+        if (isset($record['message'])) {
+            $message['message'] = $record['message'];
+        }
+        if (isset($record['channel'])) {
+            $message['type'] = $record['channel'];
+            $message['channel'] = $record['channel'];
+        }
+        if (isset($record['level_name'])) {
+            $message['level'] = $record['level_name'];
+        }
+        if ($this->applicationName) {
+            $message['type'] = $this->applicationName;
+        }
+        if (!empty($record['extra'])) {
+            foreach ($record['extra'] as $key => $val) {
+                $message[$this->extraPrefix . $key] = $val;
+            }
+        }
+        if (!empty($record['context'])) {
+            foreach ($record['context'] as $key => $val) {
+                $message[$this->contextPrefix . $key] = $val;
+            }
+        }
+
+        return $message;
+    }
+}

+ 107 - 0
api/vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php

@@ -0,0 +1,107 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Formatter;
+
+use Monolog\Utils;
+
+/**
+ * Formats a record for use with the MongoDBHandler.
+ *
+ * @author Florian Plattner <me@florianplattner.de>
+ */
+class MongoDBFormatter implements FormatterInterface
+{
+    private $exceptionTraceAsString;
+    private $maxNestingLevel;
+
+    /**
+     * @param int  $maxNestingLevel        0 means infinite nesting, the $record itself is level 1, $record['context'] is 2
+     * @param bool $exceptionTraceAsString set to false to log exception traces as a sub documents instead of strings
+     */
+    public function __construct($maxNestingLevel = 3, $exceptionTraceAsString = true)
+    {
+        $this->maxNestingLevel = max($maxNestingLevel, 0);
+        $this->exceptionTraceAsString = (bool) $exceptionTraceAsString;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function format(array $record)
+    {
+        return $this->formatArray($record);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function formatBatch(array $records)
+    {
+        foreach ($records as $key => $record) {
+            $records[$key] = $this->format($record);
+        }
+
+        return $records;
+    }
+
+    protected function formatArray(array $record, $nestingLevel = 0)
+    {
+        if ($this->maxNestingLevel == 0 || $nestingLevel <= $this->maxNestingLevel) {
+            foreach ($record as $name => $value) {
+                if ($value instanceof \DateTime) {
+                    $record[$name] = $this->formatDate($value, $nestingLevel + 1);
+                } elseif ($value instanceof \Exception) {
+                    $record[$name] = $this->formatException($value, $nestingLevel + 1);
+                } elseif (is_array($value)) {
+                    $record[$name] = $this->formatArray($value, $nestingLevel + 1);
+                } elseif (is_object($value)) {
+                    $record[$name] = $this->formatObject($value, $nestingLevel + 1);
+                }
+            }
+        } else {
+            $record = '[...]';
+        }
+
+        return $record;
+    }
+
+    protected function formatObject($value, $nestingLevel)
+    {
+        $objectVars = get_object_vars($value);
+        $objectVars['class'] = Utils::getClass($value);
+
+        return $this->formatArray($objectVars, $nestingLevel);
+    }
+
+    protected function formatException(\Exception $exception, $nestingLevel)
+    {
+        $formattedException = array(
+            'class' => Utils::getClass($exception),
+            'message' => $exception->getMessage(),
+            'code' => (int) $exception->getCode(),
+            'file' => $exception->getFile() . ':' . $exception->getLine(),
+        );
+
+        if ($this->exceptionTraceAsString === true) {
+            $formattedException['trace'] = $exception->getTraceAsString();
+        } else {
+            $formattedException['trace'] = $exception->getTrace();
+        }
+
+        return $this->formatArray($formattedException, $nestingLevel);
+    }
+
+    protected function formatDate(\DateTime $value, $nestingLevel)
+    {
+        return new \MongoDate($value->getTimestamp());
+    }
+}

+ 180 - 0
api/vendor/monolog/monolog/src/Monolog/Formatter/NormalizerFormatter.php

@@ -0,0 +1,180 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Formatter;
+
+use Exception;
+use Monolog\Utils;
+
+/**
+ * Normalizes incoming records to remove objects/resources so it's easier to dump to various targets
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class NormalizerFormatter implements FormatterInterface
+{
+    const SIMPLE_DATE = "Y-m-d H:i:s";
+
+    protected $dateFormat;
+
+    /**
+     * @param string $dateFormat The format of the timestamp: one supported by DateTime::format
+     */
+    public function __construct($dateFormat = null)
+    {
+        $this->dateFormat = $dateFormat ?: static::SIMPLE_DATE;
+        if (!function_exists('json_encode')) {
+            throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s NormalizerFormatter');
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function format(array $record)
+    {
+        return $this->normalize($record);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function formatBatch(array $records)
+    {
+        foreach ($records as $key => $record) {
+            $records[$key] = $this->format($record);
+        }
+
+        return $records;
+    }
+
+    protected function normalize($data, $depth = 0)
+    {
+        if ($depth > 9) {
+            return 'Over 9 levels deep, aborting normalization';
+        }
+
+        if (null === $data || is_scalar($data)) {
+            if (is_float($data)) {
+                if (is_infinite($data)) {
+                    return ($data > 0 ? '' : '-') . 'INF';
+                }
+                if (is_nan($data)) {
+                    return 'NaN';
+                }
+            }
+
+            return $data;
+        }
+
+        if (is_array($data)) {
+            $normalized = array();
+
+            $count = 1;
+            foreach ($data as $key => $value) {
+                if ($count++ > 1000) {
+                    $normalized['...'] = 'Over 1000 items ('.count($data).' total), aborting normalization';
+                    break;
+                }
+
+                $normalized[$key] = $this->normalize($value, $depth+1);
+            }
+
+            return $normalized;
+        }
+
+        if ($data instanceof \DateTime) {
+            return $data->format($this->dateFormat);
+        }
+
+        if (is_object($data)) {
+            // TODO 2.0 only check for Throwable
+            if ($data instanceof Exception || (PHP_VERSION_ID > 70000 && $data instanceof \Throwable)) {
+                return $this->normalizeException($data);
+            }
+
+            // non-serializable objects that implement __toString stringified
+            if (method_exists($data, '__toString') && !$data instanceof \JsonSerializable) {
+                $value = $data->__toString();
+            } else {
+                // the rest is json-serialized in some way
+                $value = $this->toJson($data, true);
+            }
+
+            return sprintf("[object] (%s: %s)", Utils::getClass($data), $value);
+        }
+
+        if (is_resource($data)) {
+            return sprintf('[resource] (%s)', get_resource_type($data));
+        }
+
+        return '[unknown('.gettype($data).')]';
+    }
+
+    protected function normalizeException($e)
+    {
+        // TODO 2.0 only check for Throwable
+        if (!$e instanceof Exception && !$e instanceof \Throwable) {
+            throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Utils::getClass($e));
+        }
+
+        $data = array(
+            'class' => Utils::getClass($e),
+            'message' => $e->getMessage(),
+            'code' => (int) $e->getCode(),
+            'file' => $e->getFile().':'.$e->getLine(),
+        );
+
+        if ($e instanceof \SoapFault) {
+            if (isset($e->faultcode)) {
+                $data['faultcode'] = $e->faultcode;
+            }
+
+            if (isset($e->faultactor)) {
+                $data['faultactor'] = $e->faultactor;
+            }
+
+            if (isset($e->detail)) {
+                if  (is_string($e->detail)) {
+                    $data['detail'] = $e->detail;
+                } elseif (is_object($e->detail) || is_array($e->detail)) {
+                    $data['detail'] = $this->toJson($e->detail, true);
+                }
+            }
+        }
+
+        $trace = $e->getTrace();
+        foreach ($trace as $frame) {
+            if (isset($frame['file'])) {
+                $data['trace'][] = $frame['file'].':'.$frame['line'];
+            }
+        }
+
+        if ($previous = $e->getPrevious()) {
+            $data['previous'] = $this->normalizeException($previous);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Return the JSON representation of a value
+     *
+     * @param  mixed             $data
+     * @param  bool              $ignoreErrors
+     * @throws \RuntimeException if encoding fails and errors are not ignored
+     * @return string
+     */
+    protected function toJson($data, $ignoreErrors = false)
+    {
+        return Utils::jsonEncode($data, null, $ignoreErrors);
+    }
+}

+ 48 - 0
api/vendor/monolog/monolog/src/Monolog/Formatter/ScalarFormatter.php

@@ -0,0 +1,48 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Formatter;
+
+/**
+ * Formats data into an associative array of scalar values.
+ * Objects and arrays will be JSON encoded.
+ *
+ * @author Andrew Lawson <adlawson@gmail.com>
+ */
+class ScalarFormatter extends NormalizerFormatter
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function format(array $record)
+    {
+        foreach ($record as $key => $value) {
+            $record[$key] = $this->normalizeValue($value);
+        }
+
+        return $record;
+    }
+
+    /**
+     * @param  mixed $value
+     * @return mixed
+     */
+    protected function normalizeValue($value)
+    {
+        $normalized = $this->normalize($value);
+
+        if (is_array($normalized) || is_object($normalized)) {
+            return $this->toJson($normalized, true);
+        }
+
+        return $normalized;
+    }
+}

+ 113 - 0
api/vendor/monolog/monolog/src/Monolog/Formatter/WildfireFormatter.php

@@ -0,0 +1,113 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Formatter;
+
+use Monolog\Logger;
+
+/**
+ * Serializes a log message according to Wildfire's header requirements
+ *
+ * @author Eric Clemmons (@ericclemmons) <eric@uxdriven.com>
+ * @author Christophe Coevoet <stof@notk.org>
+ * @author Kirill chEbba Chebunin <iam@chebba.org>
+ */
+class WildfireFormatter extends NormalizerFormatter
+{
+    const TABLE = 'table';
+
+    /**
+     * Translates Monolog log levels to Wildfire levels.
+     */
+    private $logLevels = array(
+        Logger::DEBUG     => 'LOG',
+        Logger::INFO      => 'INFO',
+        Logger::NOTICE    => 'INFO',
+        Logger::WARNING   => 'WARN',
+        Logger::ERROR     => 'ERROR',
+        Logger::CRITICAL  => 'ERROR',
+        Logger::ALERT     => 'ERROR',
+        Logger::EMERGENCY => 'ERROR',
+    );
+
+    /**
+     * {@inheritdoc}
+     */
+    public function format(array $record)
+    {
+        // Retrieve the line and file if set and remove them from the formatted extra
+        $file = $line = '';
+        if (isset($record['extra']['file'])) {
+            $file = $record['extra']['file'];
+            unset($record['extra']['file']);
+        }
+        if (isset($record['extra']['line'])) {
+            $line = $record['extra']['line'];
+            unset($record['extra']['line']);
+        }
+
+        $record = $this->normalize($record);
+        $message = array('message' => $record['message']);
+        $handleError = false;
+        if ($record['context']) {
+            $message['context'] = $record['context'];
+            $handleError = true;
+        }
+        if ($record['extra']) {
+            $message['extra'] = $record['extra'];
+            $handleError = true;
+        }
+        if (count($message) === 1) {
+            $message = reset($message);
+        }
+
+        if (isset($record['context'][self::TABLE])) {
+            $type  = 'TABLE';
+            $label = $record['channel'] .': '. $record['message'];
+            $message = $record['context'][self::TABLE];
+        } else {
+            $type  = $this->logLevels[$record['level']];
+            $label = $record['channel'];
+        }
+
+        // Create JSON object describing the appearance of the message in the console
+        $json = $this->toJson(array(
+            array(
+                'Type'  => $type,
+                'File'  => $file,
+                'Line'  => $line,
+                'Label' => $label,
+            ),
+            $message,
+        ), $handleError);
+
+        // The message itself is a serialization of the above JSON object + it's length
+        return sprintf(
+            '%s|%s|',
+            strlen($json),
+            $json
+        );
+    }
+
+    public function formatBatch(array $records)
+    {
+        throw new \BadMethodCallException('Batch formatting does not make sense for the WildfireFormatter');
+    }
+
+    protected function normalize($data, $depth = 0)
+    {
+        if (is_object($data) && !$data instanceof \DateTime) {
+            return $data;
+        }
+
+        return parent::normalize($data, $depth);
+    }
+}

+ 196 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/AbstractHandler.php

@@ -0,0 +1,196 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Formatter\FormatterInterface;
+use Monolog\Formatter\LineFormatter;
+use Monolog\Logger;
+use Monolog\ResettableInterface;
+
+/**
+ * Base Handler class providing the Handler structure
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+abstract class AbstractHandler implements HandlerInterface, ResettableInterface
+{
+    protected $level = Logger::DEBUG;
+    protected $bubble = true;
+
+    /**
+     * @var FormatterInterface
+     */
+    protected $formatter;
+    protected $processors = array();
+
+    /**
+     * @param int|string $level  The minimum logging level at which this handler will be triggered
+     * @param bool       $bubble Whether the messages that are handled can bubble up the stack or not
+     */
+    public function __construct($level = Logger::DEBUG, $bubble = true)
+    {
+        $this->setLevel($level);
+        $this->bubble = $bubble;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isHandling(array $record)
+    {
+        return $record['level'] >= $this->level;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function handleBatch(array $records)
+    {
+        foreach ($records as $record) {
+            $this->handle($record);
+        }
+    }
+
+    /**
+     * Closes the handler.
+     *
+     * This will be called automatically when the object is destroyed
+     */
+    public function close()
+    {
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function pushProcessor($callback)
+    {
+        if (!is_callable($callback)) {
+            throw new \InvalidArgumentException('Processors must be valid callables (callback or object with an __invoke method), '.var_export($callback, true).' given');
+        }
+        array_unshift($this->processors, $callback);
+
+        return $this;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function popProcessor()
+    {
+        if (!$this->processors) {
+            throw new \LogicException('You tried to pop from an empty processor stack.');
+        }
+
+        return array_shift($this->processors);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setFormatter(FormatterInterface $formatter)
+    {
+        $this->formatter = $formatter;
+
+        return $this;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getFormatter()
+    {
+        if (!$this->formatter) {
+            $this->formatter = $this->getDefaultFormatter();
+        }
+
+        return $this->formatter;
+    }
+
+    /**
+     * Sets minimum logging level at which this handler will be triggered.
+     *
+     * @param  int|string $level Level or level name
+     * @return self
+     */
+    public function setLevel($level)
+    {
+        $this->level = Logger::toMonologLevel($level);
+
+        return $this;
+    }
+
+    /**
+     * Gets minimum logging level at which this handler will be triggered.
+     *
+     * @return int
+     */
+    public function getLevel()
+    {
+        return $this->level;
+    }
+
+    /**
+     * Sets the bubbling behavior.
+     *
+     * @param  bool $bubble true means that this handler allows bubbling.
+     *                      false means that bubbling is not permitted.
+     * @return self
+     */
+    public function setBubble($bubble)
+    {
+        $this->bubble = $bubble;
+
+        return $this;
+    }
+
+    /**
+     * Gets the bubbling behavior.
+     *
+     * @return bool true means that this handler allows bubbling.
+     *              false means that bubbling is not permitted.
+     */
+    public function getBubble()
+    {
+        return $this->bubble;
+    }
+
+    public function __destruct()
+    {
+        try {
+            $this->close();
+        } catch (\Exception $e) {
+            // do nothing
+        } catch (\Throwable $e) {
+            // do nothing
+        }
+    }
+
+    public function reset()
+    {
+        foreach ($this->processors as $processor) {
+            if ($processor instanceof ResettableInterface) {
+                $processor->reset();
+            }
+        }
+    }
+
+    /**
+     * Gets the default formatter.
+     *
+     * @return FormatterInterface
+     */
+    protected function getDefaultFormatter()
+    {
+        return new LineFormatter();
+    }
+}

+ 68 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/AbstractProcessingHandler.php

@@ -0,0 +1,68 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\ResettableInterface;
+
+/**
+ * Base Handler class providing the Handler structure
+ *
+ * Classes extending it should (in most cases) only implement write($record)
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @author Christophe Coevoet <stof@notk.org>
+ */
+abstract class AbstractProcessingHandler extends AbstractHandler
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function handle(array $record)
+    {
+        if (!$this->isHandling($record)) {
+            return false;
+        }
+
+        $record = $this->processRecord($record);
+
+        $record['formatted'] = $this->getFormatter()->format($record);
+
+        $this->write($record);
+
+        return false === $this->bubble;
+    }
+
+    /**
+     * Writes the record down to the log of the implementing handler
+     *
+     * @param  array $record
+     * @return void
+     */
+    abstract protected function write(array $record);
+
+    /**
+     * Processes a record.
+     *
+     * @param  array $record
+     * @return array
+     */
+    protected function processRecord(array $record)
+    {
+        if ($this->processors) {
+            foreach ($this->processors as $processor) {
+                $record = call_user_func($processor, $record);
+            }
+        }
+
+        return $record;
+    }
+}

+ 101 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/AbstractSyslogHandler.php

@@ -0,0 +1,101 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Logger;
+use Monolog\Formatter\LineFormatter;
+
+/**
+ * Common syslog functionality
+ */
+abstract class AbstractSyslogHandler extends AbstractProcessingHandler
+{
+    protected $facility;
+
+    /**
+     * Translates Monolog log levels to syslog log priorities.
+     */
+    protected $logLevels = array(
+        Logger::DEBUG     => LOG_DEBUG,
+        Logger::INFO      => LOG_INFO,
+        Logger::NOTICE    => LOG_NOTICE,
+        Logger::WARNING   => LOG_WARNING,
+        Logger::ERROR     => LOG_ERR,
+        Logger::CRITICAL  => LOG_CRIT,
+        Logger::ALERT     => LOG_ALERT,
+        Logger::EMERGENCY => LOG_EMERG,
+    );
+
+    /**
+     * List of valid log facility names.
+     */
+    protected $facilities = array(
+        'auth'     => LOG_AUTH,
+        'authpriv' => LOG_AUTHPRIV,
+        'cron'     => LOG_CRON,
+        'daemon'   => LOG_DAEMON,
+        'kern'     => LOG_KERN,
+        'lpr'      => LOG_LPR,
+        'mail'     => LOG_MAIL,
+        'news'     => LOG_NEWS,
+        'syslog'   => LOG_SYSLOG,
+        'user'     => LOG_USER,
+        'uucp'     => LOG_UUCP,
+    );
+
+    /**
+     * @param mixed $facility
+     * @param int   $level The minimum logging level at which this handler will be triggered
+     * @param bool  $bubble Whether the messages that are handled can bubble up the stack or not
+     */
+    public function __construct($facility = LOG_USER, $level = Logger::DEBUG, $bubble = true)
+    {
+        parent::__construct($level, $bubble);
+
+        if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
+            $this->facilities['local0'] = LOG_LOCAL0;
+            $this->facilities['local1'] = LOG_LOCAL1;
+            $this->facilities['local2'] = LOG_LOCAL2;
+            $this->facilities['local3'] = LOG_LOCAL3;
+            $this->facilities['local4'] = LOG_LOCAL4;
+            $this->facilities['local5'] = LOG_LOCAL5;
+            $this->facilities['local6'] = LOG_LOCAL6;
+            $this->facilities['local7'] = LOG_LOCAL7;
+        } else {
+            $this->facilities['local0'] = 128; // LOG_LOCAL0
+            $this->facilities['local1'] = 136; // LOG_LOCAL1
+            $this->facilities['local2'] = 144; // LOG_LOCAL2
+            $this->facilities['local3'] = 152; // LOG_LOCAL3
+            $this->facilities['local4'] = 160; // LOG_LOCAL4
+            $this->facilities['local5'] = 168; // LOG_LOCAL5
+            $this->facilities['local6'] = 176; // LOG_LOCAL6
+            $this->facilities['local7'] = 184; // LOG_LOCAL7
+        }
+
+        // convert textual description of facility to syslog constant
+        if (array_key_exists(strtolower($facility), $this->facilities)) {
+            $facility = $this->facilities[strtolower($facility)];
+        } elseif (!in_array($facility, array_values($this->facilities), true)) {
+            throw new \UnexpectedValueException('Unknown facility value "'.$facility.'" given');
+        }
+
+        $this->facility = $facility;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function getDefaultFormatter()
+    {
+        return new LineFormatter('%channel%.%level_name%: %message% %context% %extra%');
+    }
+}

+ 148 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/AmqpHandler.php

@@ -0,0 +1,148 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Logger;
+use Monolog\Formatter\JsonFormatter;
+use PhpAmqpLib\Message\AMQPMessage;
+use PhpAmqpLib\Channel\AMQPChannel;
+use AMQPExchange;
+
+class AmqpHandler extends AbstractProcessingHandler
+{
+    /**
+     * @var AMQPExchange|AMQPChannel $exchange
+     */
+    protected $exchange;
+
+    /**
+     * @var string
+     */
+    protected $exchangeName;
+
+    /**
+     * @param AMQPExchange|AMQPChannel $exchange     AMQPExchange (php AMQP ext) or PHP AMQP lib channel, ready for use
+     * @param string                   $exchangeName
+     * @param int                      $level
+     * @param bool                     $bubble       Whether the messages that are handled can bubble up the stack or not
+     */
+    public function __construct($exchange, $exchangeName = 'log', $level = Logger::DEBUG, $bubble = true)
+    {
+        if ($exchange instanceof AMQPExchange) {
+            $exchange->setName($exchangeName);
+        } elseif ($exchange instanceof AMQPChannel) {
+            $this->exchangeName = $exchangeName;
+        } else {
+            throw new \InvalidArgumentException('PhpAmqpLib\Channel\AMQPChannel or AMQPExchange instance required');
+        }
+        $this->exchange = $exchange;
+
+        parent::__construct($level, $bubble);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function write(array $record)
+    {
+        $data = $record["formatted"];
+        $routingKey = $this->getRoutingKey($record);
+
+        if ($this->exchange instanceof AMQPExchange) {
+            $this->exchange->publish(
+                $data,
+                $routingKey,
+                0,
+                array(
+                    'delivery_mode' => 2,
+                    'content_type' => 'application/json',
+                )
+            );
+        } else {
+            $this->exchange->basic_publish(
+                $this->createAmqpMessage($data),
+                $this->exchangeName,
+                $routingKey
+            );
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function handleBatch(array $records)
+    {
+        if ($this->exchange instanceof AMQPExchange) {
+            parent::handleBatch($records);
+
+            return;
+        }
+
+        foreach ($records as $record) {
+            if (!$this->isHandling($record)) {
+                continue;
+            }
+
+            $record = $this->processRecord($record);
+            $data = $this->getFormatter()->format($record);
+
+            $this->exchange->batch_basic_publish(
+                $this->createAmqpMessage($data),
+                $this->exchangeName,
+                $this->getRoutingKey($record)
+            );
+        }
+
+        $this->exchange->publish_batch();
+    }
+
+    /**
+     * Gets the routing key for the AMQP exchange
+     *
+     * @param  array  $record
+     * @return string
+     */
+    protected function getRoutingKey(array $record)
+    {
+        $routingKey = sprintf(
+            '%s.%s',
+            // TODO 2.0 remove substr call
+            substr($record['level_name'], 0, 4),
+            $record['channel']
+        );
+
+        return strtolower($routingKey);
+    }
+
+    /**
+     * @param  string      $data
+     * @return AMQPMessage
+     */
+    private function createAmqpMessage($data)
+    {
+        return new AMQPMessage(
+            (string) $data,
+            array(
+                'delivery_mode' => 2,
+                'content_type' => 'application/json',
+            )
+        );
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function getDefaultFormatter()
+    {
+        return new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false);
+    }
+}

+ 241 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/BrowserConsoleHandler.php

@@ -0,0 +1,241 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Formatter\LineFormatter;
+
+/**
+ * Handler sending logs to browser's javascript console with no browser extension required
+ *
+ * @author Olivier Poitrey <rs@dailymotion.com>
+ */
+class BrowserConsoleHandler extends AbstractProcessingHandler
+{
+    protected static $initialized = false;
+    protected static $records = array();
+
+    /**
+     * {@inheritDoc}
+     *
+     * Formatted output may contain some formatting markers to be transferred to `console.log` using the %c format.
+     *
+     * Example of formatted string:
+     *
+     *     You can do [[blue text]]{color: blue} or [[green background]]{background-color: green; color: white}
+     */
+    protected function getDefaultFormatter()
+    {
+        return new LineFormatter('[[%channel%]]{macro: autolabel} [[%level_name%]]{font-weight: bold} %message%');
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function write(array $record)
+    {
+        // Accumulate records
+        static::$records[] = $record;
+
+        // Register shutdown handler if not already done
+        if (!static::$initialized) {
+            static::$initialized = true;
+            $this->registerShutdownFunction();
+        }
+    }
+
+    /**
+     * Convert records to javascript console commands and send it to the browser.
+     * This method is automatically called on PHP shutdown if output is HTML or Javascript.
+     */
+    public static function send()
+    {
+        $format = static::getResponseFormat();
+        if ($format === 'unknown') {
+            return;
+        }
+
+        if (count(static::$records)) {
+            if ($format === 'html') {
+                static::writeOutput('<script>' . static::generateScript() . '</script>');
+            } elseif ($format === 'js') {
+                static::writeOutput(static::generateScript());
+            }
+            static::resetStatic();
+        }
+    }
+
+    public function close()
+    {
+        self::resetStatic();
+    }
+
+    public function reset()
+    {
+        self::resetStatic();
+    }
+
+    /**
+     * Forget all logged records
+     */
+    public static function resetStatic()
+    {
+        static::$records = array();
+    }
+
+    /**
+     * Wrapper for register_shutdown_function to allow overriding
+     */
+    protected function registerShutdownFunction()
+    {
+        if (PHP_SAPI !== 'cli') {
+            register_shutdown_function(array('Monolog\Handler\BrowserConsoleHandler', 'send'));
+        }
+    }
+
+    /**
+     * Wrapper for echo to allow overriding
+     *
+     * @param string $str
+     */
+    protected static function writeOutput($str)
+    {
+        echo $str;
+    }
+
+    /**
+     * Checks the format of the response
+     *
+     * If Content-Type is set to application/javascript or text/javascript -> js
+     * If Content-Type is set to text/html, or is unset -> html
+     * If Content-Type is anything else -> unknown
+     *
+     * @return string One of 'js', 'html' or 'unknown'
+     */
+    protected static function getResponseFormat()
+    {
+        // Check content type
+        foreach (headers_list() as $header) {
+            if (stripos($header, 'content-type:') === 0) {
+                // This handler only works with HTML and javascript outputs
+                // text/javascript is obsolete in favour of application/javascript, but still used
+                if (stripos($header, 'application/javascript') !== false || stripos($header, 'text/javascript') !== false) {
+                    return 'js';
+                }
+                if (stripos($header, 'text/html') === false) {
+                    return 'unknown';
+                }
+                break;
+            }
+        }
+
+        return 'html';
+    }
+
+    private static function generateScript()
+    {
+        $script = array();
+        foreach (static::$records as $record) {
+            $context = static::dump('Context', $record['context']);
+            $extra = static::dump('Extra', $record['extra']);
+
+            if (empty($context) && empty($extra)) {
+                $script[] = static::call_array('log', static::handleStyles($record['formatted']));
+            } else {
+                $script = array_merge($script,
+                    array(static::call_array('groupCollapsed', static::handleStyles($record['formatted']))),
+                    $context,
+                    $extra,
+                    array(static::call('groupEnd'))
+                );
+            }
+        }
+
+        return "(function (c) {if (c && c.groupCollapsed) {\n" . implode("\n", $script) . "\n}})(console);";
+    }
+
+    private static function handleStyles($formatted)
+    {
+        $args = array();
+        $format = '%c' . $formatted;
+        preg_match_all('/\[\[(.*?)\]\]\{([^}]*)\}/s', $format, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
+
+        foreach (array_reverse($matches) as $match) {
+            $args[] = '"font-weight: normal"';
+            $args[] = static::quote(static::handleCustomStyles($match[2][0], $match[1][0]));
+
+            $pos = $match[0][1];
+            $format = substr($format, 0, $pos) . '%c' . $match[1][0] . '%c' . substr($format, $pos + strlen($match[0][0]));
+        }
+
+        $args[] = static::quote('font-weight: normal');
+        $args[] = static::quote($format);
+
+        return array_reverse($args);
+    }
+
+    private static function handleCustomStyles($style, $string)
+    {
+        static $colors = array('blue', 'green', 'red', 'magenta', 'orange', 'black', 'grey');
+        static $labels = array();
+
+        return preg_replace_callback('/macro\s*:(.*?)(?:;|$)/', function ($m) use ($string, &$colors, &$labels) {
+            if (trim($m[1]) === 'autolabel') {
+                // Format the string as a label with consistent auto assigned background color
+                if (!isset($labels[$string])) {
+                    $labels[$string] = $colors[count($labels) % count($colors)];
+                }
+                $color = $labels[$string];
+
+                return "background-color: $color; color: white; border-radius: 3px; padding: 0 2px 0 2px";
+            }
+
+            return $m[1];
+        }, $style);
+    }
+
+    private static function dump($title, array $dict)
+    {
+        $script = array();
+        $dict = array_filter($dict);
+        if (empty($dict)) {
+            return $script;
+        }
+        $script[] = static::call('log', static::quote('%c%s'), static::quote('font-weight: bold'), static::quote($title));
+        foreach ($dict as $key => $value) {
+            $value = json_encode($value);
+            if (empty($value)) {
+                $value = static::quote('');
+            }
+            $script[] = static::call('log', static::quote('%s: %o'), static::quote($key), $value);
+        }
+
+        return $script;
+    }
+
+    private static function quote($arg)
+    {
+        return '"' . addcslashes($arg, "\"\n\\") . '"';
+    }
+
+    private static function call()
+    {
+        $args = func_get_args();
+        $method = array_shift($args);
+
+        return static::call_array($method, $args);
+    }
+
+    private static function call_array($method, array $args)
+    {
+        return 'c.' . $method . '(' . implode(', ', $args) . ');';
+    }
+}

+ 148 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/BufferHandler.php

@@ -0,0 +1,148 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Logger;
+use Monolog\ResettableInterface;
+use Monolog\Formatter\FormatterInterface;
+
+/**
+ * Buffers all records until closing the handler and then pass them as batch.
+ *
+ * This is useful for a MailHandler to send only one mail per request instead of
+ * sending one per log message.
+ *
+ * @author Christophe Coevoet <stof@notk.org>
+ */
+class BufferHandler extends AbstractHandler
+{
+    protected $handler;
+    protected $bufferSize = 0;
+    protected $bufferLimit;
+    protected $flushOnOverflow;
+    protected $buffer = array();
+    protected $initialized = false;
+
+    /**
+     * @param HandlerInterface $handler         Handler.
+     * @param int              $bufferLimit     How many entries should be buffered at most, beyond that the oldest items are removed from the buffer.
+     * @param int              $level           The minimum logging level at which this handler will be triggered
+     * @param bool             $bubble          Whether the messages that are handled can bubble up the stack or not
+     * @param bool             $flushOnOverflow If true, the buffer is flushed when the max size has been reached, by default oldest entries are discarded
+     */
+    public function __construct(HandlerInterface $handler, $bufferLimit = 0, $level = Logger::DEBUG, $bubble = true, $flushOnOverflow = false)
+    {
+        parent::__construct($level, $bubble);
+        $this->handler = $handler;
+        $this->bufferLimit = (int) $bufferLimit;
+        $this->flushOnOverflow = $flushOnOverflow;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function handle(array $record)
+    {
+        if ($record['level'] < $this->level) {
+            return false;
+        }
+
+        if (!$this->initialized) {
+            // __destructor() doesn't get called on Fatal errors
+            register_shutdown_function(array($this, 'close'));
+            $this->initialized = true;
+        }
+
+        if ($this->bufferLimit > 0 && $this->bufferSize === $this->bufferLimit) {
+            if ($this->flushOnOverflow) {
+                $this->flush();
+            } else {
+                array_shift($this->buffer);
+                $this->bufferSize--;
+            }
+        }
+
+        if ($this->processors) {
+            foreach ($this->processors as $processor) {
+                $record = call_user_func($processor, $record);
+            }
+        }
+
+        $this->buffer[] = $record;
+        $this->bufferSize++;
+
+        return false === $this->bubble;
+    }
+
+    public function flush()
+    {
+        if ($this->bufferSize === 0) {
+            return;
+        }
+
+        $this->handler->handleBatch($this->buffer);
+        $this->clear();
+    }
+
+    public function __destruct()
+    {
+        // suppress the parent behavior since we already have register_shutdown_function()
+        // to call close(), and the reference contained there will prevent this from being
+        // GC'd until the end of the request
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function close()
+    {
+        $this->flush();
+    }
+
+    /**
+     * Clears the buffer without flushing any messages down to the wrapped handler.
+     */
+    public function clear()
+    {
+        $this->bufferSize = 0;
+        $this->buffer = array();
+    }
+
+    public function reset()
+    {
+        $this->flush();
+
+        parent::reset();
+
+        if ($this->handler instanceof ResettableInterface) {
+            $this->handler->reset();
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setFormatter(FormatterInterface $formatter)
+    {
+        $this->handler->setFormatter($formatter);
+
+        return $this;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getFormatter()
+    {
+        return $this->handler->getFormatter();
+    }
+}

+ 212 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/ChromePHPHandler.php

@@ -0,0 +1,212 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Formatter\ChromePHPFormatter;
+use Monolog\Logger;
+use Monolog\Utils;
+
+/**
+ * Handler sending logs to the ChromePHP extension (http://www.chromephp.com/)
+ *
+ * This also works out of the box with Firefox 43+
+ *
+ * @author Christophe Coevoet <stof@notk.org>
+ */
+class ChromePHPHandler extends AbstractProcessingHandler
+{
+    /**
+     * Version of the extension
+     */
+    const VERSION = '4.0';
+
+    /**
+     * Header name
+     */
+    const HEADER_NAME = 'X-ChromeLogger-Data';
+
+    /**
+     * Regular expression to detect supported browsers (matches any Chrome, or Firefox 43+)
+     */
+    const USER_AGENT_REGEX = '{\b(?:Chrome/\d+(?:\.\d+)*|HeadlessChrome|Firefox/(?:4[3-9]|[5-9]\d|\d{3,})(?:\.\d)*)\b}';
+
+    protected static $initialized = false;
+
+    /**
+     * Tracks whether we sent too much data
+     *
+     * Chrome limits the headers to 4KB, so when we sent 3KB we stop sending
+     *
+     * @var bool
+     */
+    protected static $overflowed = false;
+
+    protected static $json = array(
+        'version' => self::VERSION,
+        'columns' => array('label', 'log', 'backtrace', 'type'),
+        'rows' => array(),
+    );
+
+    protected static $sendHeaders = true;
+
+    /**
+     * @param int  $level  The minimum logging level at which this handler will be triggered
+     * @param bool $bubble Whether the messages that are handled can bubble up the stack or not
+     */
+    public function __construct($level = Logger::DEBUG, $bubble = true)
+    {
+        parent::__construct($level, $bubble);
+        if (!function_exists('json_encode')) {
+            throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s ChromePHPHandler');
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function handleBatch(array $records)
+    {
+        $messages = array();
+
+        foreach ($records as $record) {
+            if ($record['level'] < $this->level) {
+                continue;
+            }
+            $messages[] = $this->processRecord($record);
+        }
+
+        if (!empty($messages)) {
+            $messages = $this->getFormatter()->formatBatch($messages);
+            self::$json['rows'] = array_merge(self::$json['rows'], $messages);
+            $this->send();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function getDefaultFormatter()
+    {
+        return new ChromePHPFormatter();
+    }
+
+    /**
+     * Creates & sends header for a record
+     *
+     * @see sendHeader()
+     * @see send()
+     * @param array $record
+     */
+    protected function write(array $record)
+    {
+        self::$json['rows'][] = $record['formatted'];
+
+        $this->send();
+    }
+
+    /**
+     * Sends the log header
+     *
+     * @see sendHeader()
+     */
+    protected function send()
+    {
+        if (self::$overflowed || !self::$sendHeaders) {
+            return;
+        }
+
+        if (!self::$initialized) {
+            self::$initialized = true;
+
+            self::$sendHeaders = $this->headersAccepted();
+            if (!self::$sendHeaders) {
+                return;
+            }
+
+            self::$json['request_uri'] = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
+        }
+
+        $json = Utils::jsonEncode(self::$json, null, true);
+        $data = base64_encode(utf8_encode($json));
+        if (strlen($data) > 3 * 1024) {
+            self::$overflowed = true;
+
+            $record = array(
+                'message' => 'Incomplete logs, chrome header size limit reached',
+                'context' => array(),
+                'level' => Logger::WARNING,
+                'level_name' => Logger::getLevelName(Logger::WARNING),
+                'channel' => 'monolog',
+                'datetime' => new \DateTime(),
+                'extra' => array(),
+            );
+            self::$json['rows'][count(self::$json['rows']) - 1] = $this->getFormatter()->format($record);
+            $json = Utils::jsonEncode(self::$json, null, true);
+            $data = base64_encode(utf8_encode($json));
+        }
+
+        if (trim($data) !== '') {
+            $this->sendHeader(self::HEADER_NAME, $data);
+        }
+    }
+
+    /**
+     * Send header string to the client
+     *
+     * @param string $header
+     * @param string $content
+     */
+    protected function sendHeader($header, $content)
+    {
+        if (!headers_sent() && self::$sendHeaders) {
+            header(sprintf('%s: %s', $header, $content));
+        }
+    }
+
+    /**
+     * Verifies if the headers are accepted by the current user agent
+     *
+     * @return bool
+     */
+    protected function headersAccepted()
+    {
+        if (empty($_SERVER['HTTP_USER_AGENT'])) {
+            return false;
+        }
+
+        return preg_match(self::USER_AGENT_REGEX, $_SERVER['HTTP_USER_AGENT']);
+    }
+
+    /**
+     * BC getter for the sendHeaders property that has been made static
+     */
+    public function __get($property)
+    {
+        if ('sendHeaders' !== $property) {
+            throw new \InvalidArgumentException('Undefined property '.$property);
+        }
+
+        return static::$sendHeaders;
+    }
+
+    /**
+     * BC setter for the sendHeaders property that has been made static
+     */
+    public function __set($property, $value)
+    {
+        if ('sendHeaders' !== $property) {
+            throw new \InvalidArgumentException('Undefined property '.$property);
+        }
+
+        static::$sendHeaders = $value;
+    }
+}

+ 72 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/CouchDBHandler.php

@@ -0,0 +1,72 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Formatter\JsonFormatter;
+use Monolog\Logger;
+
+/**
+ * CouchDB handler
+ *
+ * @author Markus Bachmann <markus.bachmann@bachi.biz>
+ */
+class CouchDBHandler extends AbstractProcessingHandler
+{
+    private $options;
+
+    public function __construct(array $options = array(), $level = Logger::DEBUG, $bubble = true)
+    {
+        $this->options = array_merge(array(
+            'host'     => 'localhost',
+            'port'     => 5984,
+            'dbname'   => 'logger',
+            'username' => null,
+            'password' => null,
+        ), $options);
+
+        parent::__construct($level, $bubble);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function write(array $record)
+    {
+        $basicAuth = null;
+        if ($this->options['username']) {
+            $basicAuth = sprintf('%s:%s@', $this->options['username'], $this->options['password']);
+        }
+
+        $url = 'http://'.$basicAuth.$this->options['host'].':'.$this->options['port'].'/'.$this->options['dbname'];
+        $context = stream_context_create(array(
+            'http' => array(
+                'method'        => 'POST',
+                'content'       => $record['formatted'],
+                'ignore_errors' => true,
+                'max_redirects' => 0,
+                'header'        => 'Content-type: application/json',
+            ),
+        ));
+
+        if (false === @file_get_contents($url, null, $context)) {
+            throw new \RuntimeException(sprintf('Could not connect to %s', $url));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function getDefaultFormatter()
+    {
+        return new JsonFormatter(JsonFormatter::BATCH_MODE_JSON, false);
+    }
+}

+ 152 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/CubeHandler.php

@@ -0,0 +1,152 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Logger;
+use Monolog\Utils;
+
+/**
+ * Logs to Cube.
+ *
+ * @link http://square.github.com/cube/
+ * @author Wan Chen <kami@kamisama.me>
+ */
+class CubeHandler extends AbstractProcessingHandler
+{
+    private $udpConnection;
+    private $httpConnection;
+    private $scheme;
+    private $host;
+    private $port;
+    private $acceptedSchemes = array('http', 'udp');
+
+    /**
+     * Create a Cube handler
+     *
+     * @throws \UnexpectedValueException when given url is not a valid url.
+     *                                   A valid url must consist of three parts : protocol://host:port
+     *                                   Only valid protocols used by Cube are http and udp
+     */
+    public function __construct($url, $level = Logger::DEBUG, $bubble = true)
+    {
+        $urlInfo = parse_url($url);
+
+        if (!isset($urlInfo['scheme'], $urlInfo['host'], $urlInfo['port'])) {
+            throw new \UnexpectedValueException('URL "'.$url.'" is not valid');
+        }
+
+        if (!in_array($urlInfo['scheme'], $this->acceptedSchemes)) {
+            throw new \UnexpectedValueException(
+                'Invalid protocol (' . $urlInfo['scheme']  . ').'
+                . ' Valid options are ' . implode(', ', $this->acceptedSchemes));
+        }
+
+        $this->scheme = $urlInfo['scheme'];
+        $this->host = $urlInfo['host'];
+        $this->port = $urlInfo['port'];
+
+        parent::__construct($level, $bubble);
+    }
+
+    /**
+     * Establish a connection to an UDP socket
+     *
+     * @throws \LogicException           when unable to connect to the socket
+     * @throws MissingExtensionException when there is no socket extension
+     */
+    protected function connectUdp()
+    {
+        if (!extension_loaded('sockets')) {
+            throw new MissingExtensionException('The sockets extension is required to use udp URLs with the CubeHandler');
+        }
+
+        $this->udpConnection = socket_create(AF_INET, SOCK_DGRAM, 0);
+        if (!$this->udpConnection) {
+            throw new \LogicException('Unable to create a socket');
+        }
+
+        if (!socket_connect($this->udpConnection, $this->host, $this->port)) {
+            throw new \LogicException('Unable to connect to the socket at ' . $this->host . ':' . $this->port);
+        }
+    }
+
+    /**
+     * Establish a connection to a http server
+     * @throws \LogicException when no curl extension
+     */
+    protected function connectHttp()
+    {
+        if (!extension_loaded('curl')) {
+            throw new \LogicException('The curl extension is needed to use http URLs with the CubeHandler');
+        }
+
+        $this->httpConnection = curl_init('http://'.$this->host.':'.$this->port.'/1.0/event/put');
+
+        if (!$this->httpConnection) {
+            throw new \LogicException('Unable to connect to ' . $this->host . ':' . $this->port);
+        }
+
+        curl_setopt($this->httpConnection, CURLOPT_CUSTOMREQUEST, "POST");
+        curl_setopt($this->httpConnection, CURLOPT_RETURNTRANSFER, true);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function write(array $record)
+    {
+        $date = $record['datetime'];
+
+        $data = array('time' => $date->format('Y-m-d\TH:i:s.uO'));
+        unset($record['datetime']);
+
+        if (isset($record['context']['type'])) {
+            $data['type'] = $record['context']['type'];
+            unset($record['context']['type']);
+        } else {
+            $data['type'] = $record['channel'];
+        }
+
+        $data['data'] = $record['context'];
+        $data['data']['level'] = $record['level'];
+
+        if ($this->scheme === 'http') {
+            $this->writeHttp(Utils::jsonEncode($data));
+        } else {
+            $this->writeUdp(Utils::jsonEncode($data));
+        }
+    }
+
+    private function writeUdp($data)
+    {
+        if (!$this->udpConnection) {
+            $this->connectUdp();
+        }
+
+        socket_send($this->udpConnection, $data, strlen($data), 0);
+    }
+
+    private function writeHttp($data)
+    {
+        if (!$this->httpConnection) {
+            $this->connectHttp();
+        }
+
+        curl_setopt($this->httpConnection, CURLOPT_POSTFIELDS, '['.$data.']');
+        curl_setopt($this->httpConnection, CURLOPT_HTTPHEADER, array(
+            'Content-Type: application/json',
+            'Content-Length: ' . strlen('['.$data.']'),
+        ));
+
+        Curl\Util::execute($this->httpConnection, 5, false);
+    }
+}

+ 57 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/Curl/Util.php

@@ -0,0 +1,57 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler\Curl;
+
+class Util
+{
+    private static $retriableErrorCodes = array(
+        CURLE_COULDNT_RESOLVE_HOST,
+        CURLE_COULDNT_CONNECT,
+        CURLE_HTTP_NOT_FOUND,
+        CURLE_READ_ERROR,
+        CURLE_OPERATION_TIMEOUTED,
+        CURLE_HTTP_POST_ERROR,
+        CURLE_SSL_CONNECT_ERROR,
+    );
+
+    /**
+     * Executes a CURL request with optional retries and exception on failure
+     *
+     * @param  resource          $ch curl handler
+     * @throws \RuntimeException
+     */
+    public static function execute($ch, $retries = 5, $closeAfterDone = true)
+    {
+        while ($retries--) {
+            if (curl_exec($ch) === false) {
+                $curlErrno = curl_errno($ch);
+
+                if (false === in_array($curlErrno, self::$retriableErrorCodes, true) || !$retries) {
+                    $curlError = curl_error($ch);
+
+                    if ($closeAfterDone) {
+                        curl_close($ch);
+                    }
+
+                    throw new \RuntimeException(sprintf('Curl error (code %s): %s', $curlErrno, $curlError));
+                }
+
+                continue;
+            }
+
+            if ($closeAfterDone) {
+                curl_close($ch);
+            }
+            break;
+        }
+    }
+}

+ 169 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php

@@ -0,0 +1,169 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Logger;
+
+/**
+ * Simple handler wrapper that deduplicates log records across multiple requests
+ *
+ * It also includes the BufferHandler functionality and will buffer
+ * all messages until the end of the request or flush() is called.
+ *
+ * This works by storing all log records' messages above $deduplicationLevel
+ * to the file specified by $deduplicationStore. When further logs come in at the end of the
+ * request (or when flush() is called), all those above $deduplicationLevel are checked
+ * against the existing stored logs. If they match and the timestamps in the stored log is
+ * not older than $time seconds, the new log record is discarded. If no log record is new, the
+ * whole data set is discarded.
+ *
+ * This is mainly useful in combination with Mail handlers or things like Slack or HipChat handlers
+ * that send messages to people, to avoid spamming with the same message over and over in case of
+ * a major component failure like a database server being down which makes all requests fail in the
+ * same way.
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class DeduplicationHandler extends BufferHandler
+{
+    /**
+     * @var string
+     */
+    protected $deduplicationStore;
+
+    /**
+     * @var int
+     */
+    protected $deduplicationLevel;
+
+    /**
+     * @var int
+     */
+    protected $time;
+
+    /**
+     * @var bool
+     */
+    private $gc = false;
+
+    /**
+     * @param HandlerInterface $handler            Handler.
+     * @param string           $deduplicationStore The file/path where the deduplication log should be kept
+     * @param int              $deduplicationLevel The minimum logging level for log records to be looked at for deduplication purposes
+     * @param int              $time               The period (in seconds) during which duplicate entries should be suppressed after a given log is sent through
+     * @param bool             $bubble             Whether the messages that are handled can bubble up the stack or not
+     */
+    public function __construct(HandlerInterface $handler, $deduplicationStore = null, $deduplicationLevel = Logger::ERROR, $time = 60, $bubble = true)
+    {
+        parent::__construct($handler, 0, Logger::DEBUG, $bubble, false);
+
+        $this->deduplicationStore = $deduplicationStore === null ? sys_get_temp_dir() . '/monolog-dedup-' . substr(md5(__FILE__), 0, 20) .'.log' : $deduplicationStore;
+        $this->deduplicationLevel = Logger::toMonologLevel($deduplicationLevel);
+        $this->time = $time;
+    }
+
+    public function flush()
+    {
+        if ($this->bufferSize === 0) {
+            return;
+        }
+
+        $passthru = null;
+
+        foreach ($this->buffer as $record) {
+            if ($record['level'] >= $this->deduplicationLevel) {
+
+                $passthru = $passthru || !$this->isDuplicate($record);
+                if ($passthru) {
+                    $this->appendRecord($record);
+                }
+            }
+        }
+
+        // default of null is valid as well as if no record matches duplicationLevel we just pass through
+        if ($passthru === true || $passthru === null) {
+            $this->handler->handleBatch($this->buffer);
+        }
+
+        $this->clear();
+
+        if ($this->gc) {
+            $this->collectLogs();
+        }
+    }
+
+    private function isDuplicate(array $record)
+    {
+        if (!file_exists($this->deduplicationStore)) {
+            return false;
+        }
+
+        $store = file($this->deduplicationStore, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
+        if (!is_array($store)) {
+            return false;
+        }
+
+        $yesterday = time() - 86400;
+        $timestampValidity = $record['datetime']->getTimestamp() - $this->time;
+        $expectedMessage = preg_replace('{[\r\n].*}', '', $record['message']);
+
+        for ($i = count($store) - 1; $i >= 0; $i--) {
+            list($timestamp, $level, $message) = explode(':', $store[$i], 3);
+
+            if ($level === $record['level_name'] && $message === $expectedMessage && $timestamp > $timestampValidity) {
+                return true;
+            }
+
+            if ($timestamp < $yesterday) {
+                $this->gc = true;
+            }
+        }
+
+        return false;
+    }
+
+    private function collectLogs()
+    {
+        if (!file_exists($this->deduplicationStore)) {
+            return false;
+        }
+
+        $handle = fopen($this->deduplicationStore, 'rw+');
+        flock($handle, LOCK_EX);
+        $validLogs = array();
+
+        $timestampValidity = time() - $this->time;
+
+        while (!feof($handle)) {
+            $log = fgets($handle);
+            if (substr($log, 0, 10) >= $timestampValidity) {
+                $validLogs[] = $log;
+            }
+        }
+
+        ftruncate($handle, 0);
+        rewind($handle);
+        foreach ($validLogs as $log) {
+            fwrite($handle, $log);
+        }
+
+        flock($handle, LOCK_UN);
+        fclose($handle);
+
+        $this->gc = false;
+    }
+
+    private function appendRecord(array $record)
+    {
+        file_put_contents($this->deduplicationStore, $record['datetime']->getTimestamp() . ':' . $record['level_name'] . ':' . preg_replace('{[\r\n].*}', '', $record['message']) . "\n", FILE_APPEND);
+    }
+}

+ 45 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/DoctrineCouchDBHandler.php

@@ -0,0 +1,45 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Logger;
+use Monolog\Formatter\NormalizerFormatter;
+use Doctrine\CouchDB\CouchDBClient;
+
+/**
+ * CouchDB handler for Doctrine CouchDB ODM
+ *
+ * @author Markus Bachmann <markus.bachmann@bachi.biz>
+ */
+class DoctrineCouchDBHandler extends AbstractProcessingHandler
+{
+    private $client;
+
+    public function __construct(CouchDBClient $client, $level = Logger::DEBUG, $bubble = true)
+    {
+        $this->client = $client;
+        parent::__construct($level, $bubble);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function write(array $record)
+    {
+        $this->client->postDocument($record['formatted']);
+    }
+
+    protected function getDefaultFormatter()
+    {
+        return new NormalizerFormatter;
+    }
+}

+ 108 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/DynamoDbHandler.php

@@ -0,0 +1,108 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Aws\Sdk;
+use Aws\DynamoDb\DynamoDbClient;
+use Aws\DynamoDb\Marshaler;
+use Monolog\Formatter\ScalarFormatter;
+use Monolog\Logger;
+
+/**
+ * Amazon DynamoDB handler (http://aws.amazon.com/dynamodb/)
+ *
+ * @link https://github.com/aws/aws-sdk-php/
+ * @author Andrew Lawson <adlawson@gmail.com>
+ */
+class DynamoDbHandler extends AbstractProcessingHandler
+{
+    const DATE_FORMAT = 'Y-m-d\TH:i:s.uO';
+
+    /**
+     * @var DynamoDbClient
+     */
+    protected $client;
+
+    /**
+     * @var string
+     */
+    protected $table;
+
+    /**
+     * @var int
+     */
+    protected $version;
+
+    /**
+     * @var Marshaler
+     */
+    protected $marshaler;
+
+    /**
+     * @param DynamoDbClient $client
+     * @param string         $table
+     * @param int            $level
+     * @param bool           $bubble
+     */
+    public function __construct(DynamoDbClient $client, $table, $level = Logger::DEBUG, $bubble = true)
+    {
+        if (defined('Aws\Sdk::VERSION') && version_compare(Sdk::VERSION, '3.0', '>=')) {
+            $this->version = 3;
+            $this->marshaler = new Marshaler;
+        } else {
+            $this->version = 2;
+        }
+
+        $this->client = $client;
+        $this->table = $table;
+
+        parent::__construct($level, $bubble);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function write(array $record)
+    {
+        $filtered = $this->filterEmptyFields($record['formatted']);
+        if ($this->version === 3) {
+            $formatted = $this->marshaler->marshalItem($filtered);
+        } else {
+            /** @phpstan-ignore-next-line */
+            $formatted = $this->client->formatAttributes($filtered);
+        }
+
+        $this->client->putItem(array(
+            'TableName' => $this->table,
+            'Item' => $formatted,
+        ));
+    }
+
+    /**
+     * @param  array $record
+     * @return array
+     */
+    protected function filterEmptyFields(array $record)
+    {
+        return array_filter($record, function ($value) {
+            return !empty($value) || false === $value || 0 === $value;
+        });
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function getDefaultFormatter()
+    {
+        return new ScalarFormatter(self::DATE_FORMAT);
+    }
+}

+ 128 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/ElasticSearchHandler.php

@@ -0,0 +1,128 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Formatter\FormatterInterface;
+use Monolog\Formatter\ElasticaFormatter;
+use Monolog\Logger;
+use Elastica\Client;
+use Elastica\Exception\ExceptionInterface;
+
+/**
+ * Elastic Search handler
+ *
+ * Usage example:
+ *
+ *    $client = new \Elastica\Client();
+ *    $options = array(
+ *        'index' => 'elastic_index_name',
+ *        'type' => 'elastic_doc_type',
+ *    );
+ *    $handler = new ElasticSearchHandler($client, $options);
+ *    $log = new Logger('application');
+ *    $log->pushHandler($handler);
+ *
+ * @author Jelle Vink <jelle.vink@gmail.com>
+ */
+class ElasticSearchHandler extends AbstractProcessingHandler
+{
+    /**
+     * @var Client
+     */
+    protected $client;
+
+    /**
+     * @var array Handler config options
+     */
+    protected $options = array();
+
+    /**
+     * @param Client $client  Elastica Client object
+     * @param array  $options Handler configuration
+     * @param int    $level   The minimum logging level at which this handler will be triggered
+     * @param bool   $bubble  Whether the messages that are handled can bubble up the stack or not
+     */
+    public function __construct(Client $client, array $options = array(), $level = Logger::DEBUG, $bubble = true)
+    {
+        parent::__construct($level, $bubble);
+        $this->client = $client;
+        $this->options = array_merge(
+            array(
+                'index'          => 'monolog',      // Elastic index name
+                'type'           => 'record',       // Elastic document type
+                'ignore_error'   => false,          // Suppress Elastica exceptions
+            ),
+            $options
+        );
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function write(array $record)
+    {
+        $this->bulkSend(array($record['formatted']));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setFormatter(FormatterInterface $formatter)
+    {
+        if ($formatter instanceof ElasticaFormatter) {
+            return parent::setFormatter($formatter);
+        }
+        throw new \InvalidArgumentException('ElasticSearchHandler is only compatible with ElasticaFormatter');
+    }
+
+    /**
+     * Getter options
+     * @return array
+     */
+    public function getOptions()
+    {
+        return $this->options;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function getDefaultFormatter()
+    {
+        return new ElasticaFormatter($this->options['index'], $this->options['type']);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function handleBatch(array $records)
+    {
+        $documents = $this->getFormatter()->formatBatch($records);
+        $this->bulkSend($documents);
+    }
+
+    /**
+     * Use Elasticsearch bulk API to send list of documents
+     * @param  array             $documents
+     * @throws \RuntimeException
+     */
+    protected function bulkSend(array $documents)
+    {
+        try {
+            $this->client->addDocuments($documents);
+        } catch (ExceptionInterface $e) {
+            if (!$this->options['ignore_error']) {
+                throw new \RuntimeException("Error sending messages to Elasticsearch", 0, $e);
+            }
+        }
+    }
+}

+ 82 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/ErrorLogHandler.php

@@ -0,0 +1,82 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Formatter\LineFormatter;
+use Monolog\Logger;
+
+/**
+ * Stores to PHP error_log() handler.
+ *
+ * @author Elan Ruusamäe <glen@delfi.ee>
+ */
+class ErrorLogHandler extends AbstractProcessingHandler
+{
+    const OPERATING_SYSTEM = 0;
+    const SAPI = 4;
+
+    protected $messageType;
+    protected $expandNewlines;
+
+    /**
+     * @param int  $messageType    Says where the error should go.
+     * @param int  $level          The minimum logging level at which this handler will be triggered
+     * @param bool $bubble         Whether the messages that are handled can bubble up the stack or not
+     * @param bool $expandNewlines If set to true, newlines in the message will be expanded to be take multiple log entries
+     */
+    public function __construct($messageType = self::OPERATING_SYSTEM, $level = Logger::DEBUG, $bubble = true, $expandNewlines = false)
+    {
+        parent::__construct($level, $bubble);
+
+        if (false === in_array($messageType, self::getAvailableTypes())) {
+            $message = sprintf('The given message type "%s" is not supported', print_r($messageType, true));
+            throw new \InvalidArgumentException($message);
+        }
+
+        $this->messageType = $messageType;
+        $this->expandNewlines = $expandNewlines;
+    }
+
+    /**
+     * @return array With all available types
+     */
+    public static function getAvailableTypes()
+    {
+        return array(
+            self::OPERATING_SYSTEM,
+            self::SAPI,
+        );
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function getDefaultFormatter()
+    {
+        return new LineFormatter('[%datetime%] %channel%.%level_name%: %message% %context% %extra%');
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function write(array $record)
+    {
+        if ($this->expandNewlines) {
+            $lines = preg_split('{[\r\n]+}', (string) $record['formatted']);
+            foreach ($lines as $line) {
+                error_log($line, $this->messageType);
+            }
+        } else {
+            error_log((string) $record['formatted'], $this->messageType);
+        }
+    }
+}

+ 172 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/FilterHandler.php

@@ -0,0 +1,172 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Logger;
+use Monolog\Formatter\FormatterInterface;
+
+/**
+ * Simple handler wrapper that filters records based on a list of levels
+ *
+ * It can be configured with an exact list of levels to allow, or a min/max level.
+ *
+ * @author Hennadiy Verkh
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class FilterHandler extends AbstractHandler
+{
+    /**
+     * Handler or factory callable($record, $this)
+     *
+     * @var callable|\Monolog\Handler\HandlerInterface
+     */
+    protected $handler;
+
+    /**
+     * Minimum level for logs that are passed to handler
+     *
+     * @var int[]
+     */
+    protected $acceptedLevels;
+
+    /**
+     * Whether the messages that are handled can bubble up the stack or not
+     *
+     * @var bool
+     */
+    protected $bubble;
+
+    /**
+     * @param callable|HandlerInterface $handler        Handler or factory callable($record|null, $filterHandler).
+     * @param int|array                 $minLevelOrList A list of levels to accept or a minimum level if maxLevel is provided
+     * @param int                       $maxLevel       Maximum level to accept, only used if $minLevelOrList is not an array
+     * @param bool                      $bubble         Whether the messages that are handled can bubble up the stack or not
+     */
+    public function __construct($handler, $minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY, $bubble = true)
+    {
+        $this->handler  = $handler;
+        $this->bubble   = $bubble;
+        $this->setAcceptedLevels($minLevelOrList, $maxLevel);
+
+        if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) {
+            throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object");
+        }
+    }
+
+    /**
+     * @return array
+     */
+    public function getAcceptedLevels()
+    {
+        return array_flip($this->acceptedLevels);
+    }
+
+    /**
+     * @param int|string|array $minLevelOrList A list of levels to accept or a minimum level or level name if maxLevel is provided
+     * @param int|string       $maxLevel       Maximum level or level name to accept, only used if $minLevelOrList is not an array
+     */
+    public function setAcceptedLevels($minLevelOrList = Logger::DEBUG, $maxLevel = Logger::EMERGENCY)
+    {
+        if (is_array($minLevelOrList)) {
+            $acceptedLevels = array_map('Monolog\Logger::toMonologLevel', $minLevelOrList);
+        } else {
+            $minLevelOrList = Logger::toMonologLevel($minLevelOrList);
+            $maxLevel = Logger::toMonologLevel($maxLevel);
+            $acceptedLevels = array_values(array_filter(Logger::getLevels(), function ($level) use ($minLevelOrList, $maxLevel) {
+                return $level >= $minLevelOrList && $level <= $maxLevel;
+            }));
+        }
+        $this->acceptedLevels = array_flip($acceptedLevels);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isHandling(array $record)
+    {
+        return isset($this->acceptedLevels[$record['level']]);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function handle(array $record)
+    {
+        if (!$this->isHandling($record)) {
+            return false;
+        }
+
+        if ($this->processors) {
+            foreach ($this->processors as $processor) {
+                $record = call_user_func($processor, $record);
+            }
+        }
+
+        $this->getHandler($record)->handle($record);
+
+        return false === $this->bubble;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function handleBatch(array $records)
+    {
+        $filtered = array();
+        foreach ($records as $record) {
+            if ($this->isHandling($record)) {
+                $filtered[] = $record;
+            }
+        }
+
+        if (count($filtered) > 0) {
+            $this->getHandler($filtered[count($filtered) - 1])->handleBatch($filtered);
+        }
+    }
+
+    /**
+     * Return the nested handler
+     *
+     * If the handler was provided as a factory callable, this will trigger the handler's instantiation.
+     *
+     * @return HandlerInterface
+     */
+    public function getHandler(array $record = null)
+    {
+        if (!$this->handler instanceof HandlerInterface) {
+            $this->handler = call_user_func($this->handler, $record, $this);
+            if (!$this->handler instanceof HandlerInterface) {
+                throw new \RuntimeException("The factory callable should return a HandlerInterface");
+            }
+        }
+
+        return $this->handler;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setFormatter(FormatterInterface $formatter)
+    {
+        $this->getHandler()->setFormatter($formatter);
+
+        return $this;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getFormatter()
+    {
+        return $this->getHandler()->getFormatter();
+    }
+}

+ 28 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ActivationStrategyInterface.php

@@ -0,0 +1,28 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler\FingersCrossed;
+
+/**
+ * Interface for activation strategies for the FingersCrossedHandler.
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+interface ActivationStrategyInterface
+{
+    /**
+     * Returns whether the given record activates the handler.
+     *
+     * @param  array   $record
+     * @return bool
+     */
+    public function isHandlerActivated(array $record);
+}

+ 59 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ChannelLevelActivationStrategy.php

@@ -0,0 +1,59 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler\FingersCrossed;
+
+use Monolog\Logger;
+
+/**
+ * Channel and Error level based monolog activation strategy. Allows to trigger activation
+ * based on level per channel. e.g. trigger activation on level 'ERROR' by default, except
+ * for records of the 'sql' channel; those should trigger activation on level 'WARN'.
+ *
+ * Example:
+ *
+ * <code>
+ *   $activationStrategy = new ChannelLevelActivationStrategy(
+ *       Logger::CRITICAL,
+ *       array(
+ *           'request' => Logger::ALERT,
+ *           'sensitive' => Logger::ERROR,
+ *       )
+ *   );
+ *   $handler = new FingersCrossedHandler(new StreamHandler('php://stderr'), $activationStrategy);
+ * </code>
+ *
+ * @author Mike Meessen <netmikey@gmail.com>
+ */
+class ChannelLevelActivationStrategy implements ActivationStrategyInterface
+{
+    private $defaultActionLevel;
+    private $channelToActionLevel;
+
+    /**
+     * @param int   $defaultActionLevel   The default action level to be used if the record's category doesn't match any
+     * @param array $channelToActionLevel An array that maps channel names to action levels.
+     */
+    public function __construct($defaultActionLevel, $channelToActionLevel = array())
+    {
+        $this->defaultActionLevel = Logger::toMonologLevel($defaultActionLevel);
+        $this->channelToActionLevel = array_map('Monolog\Logger::toMonologLevel', $channelToActionLevel);
+    }
+
+    public function isHandlerActivated(array $record)
+    {
+        if (isset($this->channelToActionLevel[$record['channel']])) {
+            return $record['level'] >= $this->channelToActionLevel[$record['channel']];
+        }
+
+        return $record['level'] >= $this->defaultActionLevel;
+    }
+}

+ 34 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossed/ErrorLevelActivationStrategy.php

@@ -0,0 +1,34 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler\FingersCrossed;
+
+use Monolog\Logger;
+
+/**
+ * Error level based activation strategy.
+ *
+ * @author Johannes M. Schmitt <schmittjoh@gmail.com>
+ */
+class ErrorLevelActivationStrategy implements ActivationStrategyInterface
+{
+    private $actionLevel;
+
+    public function __construct($actionLevel)
+    {
+        $this->actionLevel = Logger::toMonologLevel($actionLevel);
+    }
+
+    public function isHandlerActivated(array $record)
+    {
+        return $record['level'] >= $this->actionLevel;
+    }
+}

+ 207 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/FingersCrossedHandler.php

@@ -0,0 +1,207 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy;
+use Monolog\Handler\FingersCrossed\ActivationStrategyInterface;
+use Monolog\Logger;
+use Monolog\ResettableInterface;
+use Monolog\Formatter\FormatterInterface;
+
+/**
+ * Buffers all records until a certain level is reached
+ *
+ * The advantage of this approach is that you don't get any clutter in your log files.
+ * Only requests which actually trigger an error (or whatever your actionLevel is) will be
+ * in the logs, but they will contain all records, not only those above the level threshold.
+ *
+ * You can find the various activation strategies in the
+ * Monolog\Handler\FingersCrossed\ namespace.
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class FingersCrossedHandler extends AbstractHandler
+{
+    protected $handler;
+    protected $activationStrategy;
+    protected $buffering = true;
+    protected $bufferSize;
+    protected $buffer = array();
+    protected $stopBuffering;
+    protected $passthruLevel;
+
+    /**
+     * @param callable|HandlerInterface       $handler            Handler or factory callable($record|null, $fingersCrossedHandler).
+     * @param int|ActivationStrategyInterface $activationStrategy Strategy which determines when this handler takes action
+     * @param int                             $bufferSize         How many entries should be buffered at most, beyond that the oldest items are removed from the buffer.
+     * @param bool                            $bubble             Whether the messages that are handled can bubble up the stack or not
+     * @param bool                            $stopBuffering      Whether the handler should stop buffering after being triggered (default true)
+     * @param int                             $passthruLevel      Minimum level to always flush to handler on close, even if strategy not triggered
+     */
+    public function __construct($handler, $activationStrategy = null, $bufferSize = 0, $bubble = true, $stopBuffering = true, $passthruLevel = null)
+    {
+        if (null === $activationStrategy) {
+            $activationStrategy = new ErrorLevelActivationStrategy(Logger::WARNING);
+        }
+
+        // convert simple int activationStrategy to an object
+        if (!$activationStrategy instanceof ActivationStrategyInterface) {
+            $activationStrategy = new ErrorLevelActivationStrategy($activationStrategy);
+        }
+
+        $this->handler = $handler;
+        $this->activationStrategy = $activationStrategy;
+        $this->bufferSize = $bufferSize;
+        $this->bubble = $bubble;
+        $this->stopBuffering = $stopBuffering;
+
+        if ($passthruLevel !== null) {
+            $this->passthruLevel = Logger::toMonologLevel($passthruLevel);
+        }
+
+        if (!$this->handler instanceof HandlerInterface && !is_callable($this->handler)) {
+            throw new \RuntimeException("The given handler (".json_encode($this->handler).") is not a callable nor a Monolog\Handler\HandlerInterface object");
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isHandling(array $record)
+    {
+        return true;
+    }
+
+    /**
+     * Manually activate this logger regardless of the activation strategy
+     */
+    public function activate()
+    {
+        if ($this->stopBuffering) {
+            $this->buffering = false;
+        }
+        $this->getHandler(end($this->buffer) ?: null)->handleBatch($this->buffer);
+        $this->buffer = array();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function handle(array $record)
+    {
+        if ($this->processors) {
+            foreach ($this->processors as $processor) {
+                $record = call_user_func($processor, $record);
+            }
+        }
+
+        if ($this->buffering) {
+            $this->buffer[] = $record;
+            if ($this->bufferSize > 0 && count($this->buffer) > $this->bufferSize) {
+                array_shift($this->buffer);
+            }
+            if ($this->activationStrategy->isHandlerActivated($record)) {
+                $this->activate();
+            }
+        } else {
+            $this->getHandler($record)->handle($record);
+        }
+
+        return false === $this->bubble;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function close()
+    {
+        $this->flushBuffer();
+    }
+
+    public function reset()
+    {
+        $this->flushBuffer();
+
+        parent::reset();
+
+        if ($this->getHandler() instanceof ResettableInterface) {
+            $this->getHandler()->reset();
+        }
+    }
+
+    /**
+     * Clears the buffer without flushing any messages down to the wrapped handler.
+     *
+     * It also resets the handler to its initial buffering state.
+     */
+    public function clear()
+    {
+        $this->buffer = array();
+        $this->reset();
+    }
+
+    /**
+     * Resets the state of the handler. Stops forwarding records to the wrapped handler.
+     */
+    private function flushBuffer()
+    {
+        if (null !== $this->passthruLevel) {
+            $level = $this->passthruLevel;
+            $this->buffer = array_filter($this->buffer, function ($record) use ($level) {
+                return $record['level'] >= $level;
+            });
+            if (count($this->buffer) > 0) {
+                $this->getHandler(end($this->buffer) ?: null)->handleBatch($this->buffer);
+            }
+        }
+
+        $this->buffer = array();
+        $this->buffering = true;
+    }
+
+    /**
+     * Return the nested handler
+     *
+     * If the handler was provided as a factory callable, this will trigger the handler's instantiation.
+     *
+     * @return HandlerInterface
+     */
+    public function getHandler(array $record = null)
+    {
+        if (!$this->handler instanceof HandlerInterface) {
+            $this->handler = call_user_func($this->handler, $record, $this);
+            if (!$this->handler instanceof HandlerInterface) {
+                throw new \RuntimeException("The factory callable should return a HandlerInterface");
+            }
+        }
+
+        return $this->handler;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setFormatter(FormatterInterface $formatter)
+    {
+        $this->getHandler()->setFormatter($formatter);
+
+        return $this;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getFormatter()
+    {
+        return $this->getHandler()->getFormatter();
+    }
+}

+ 195 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/FirePHPHandler.php

@@ -0,0 +1,195 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Formatter\WildfireFormatter;
+
+/**
+ * Simple FirePHP Handler (http://www.firephp.org/), which uses the Wildfire protocol.
+ *
+ * @author Eric Clemmons (@ericclemmons) <eric@uxdriven.com>
+ */
+class FirePHPHandler extends AbstractProcessingHandler
+{
+    /**
+     * WildFire JSON header message format
+     */
+    const PROTOCOL_URI = 'http://meta.wildfirehq.org/Protocol/JsonStream/0.2';
+
+    /**
+     * FirePHP structure for parsing messages & their presentation
+     */
+    const STRUCTURE_URI = 'http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1';
+
+    /**
+     * Must reference a "known" plugin, otherwise headers won't display in FirePHP
+     */
+    const PLUGIN_URI = 'http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/0.3';
+
+    /**
+     * Header prefix for Wildfire to recognize & parse headers
+     */
+    const HEADER_PREFIX = 'X-Wf';
+
+    /**
+     * Whether or not Wildfire vendor-specific headers have been generated & sent yet
+     */
+    protected static $initialized = false;
+
+    /**
+     * Shared static message index between potentially multiple handlers
+     * @var int
+     */
+    protected static $messageIndex = 1;
+
+    protected static $sendHeaders = true;
+
+    /**
+     * Base header creation function used by init headers & record headers
+     *
+     * @param  array  $meta    Wildfire Plugin, Protocol & Structure Indexes
+     * @param  string $message Log message
+     * @return array  Complete header string ready for the client as key and message as value
+     */
+    protected function createHeader(array $meta, $message)
+    {
+        $header = sprintf('%s-%s', self::HEADER_PREFIX, join('-', $meta));
+
+        return array($header => $message);
+    }
+
+    /**
+     * Creates message header from record
+     *
+     * @see createHeader()
+     * @param  array  $record
+     * @return array
+     */
+    protected function createRecordHeader(array $record)
+    {
+        // Wildfire is extensible to support multiple protocols & plugins in a single request,
+        // but we're not taking advantage of that (yet), so we're using "1" for simplicity's sake.
+        return $this->createHeader(
+            array(1, 1, 1, self::$messageIndex++),
+            $record['formatted']
+        );
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function getDefaultFormatter()
+    {
+        return new WildfireFormatter();
+    }
+
+    /**
+     * Wildfire initialization headers to enable message parsing
+     *
+     * @see createHeader()
+     * @see sendHeader()
+     * @return array
+     */
+    protected function getInitHeaders()
+    {
+        // Initial payload consists of required headers for Wildfire
+        return array_merge(
+            $this->createHeader(array('Protocol', 1), self::PROTOCOL_URI),
+            $this->createHeader(array(1, 'Structure', 1), self::STRUCTURE_URI),
+            $this->createHeader(array(1, 'Plugin', 1), self::PLUGIN_URI)
+        );
+    }
+
+    /**
+     * Send header string to the client
+     *
+     * @param string $header
+     * @param string $content
+     */
+    protected function sendHeader($header, $content)
+    {
+        if (!headers_sent() && self::$sendHeaders) {
+            header(sprintf('%s: %s', $header, $content));
+        }
+    }
+
+    /**
+     * Creates & sends header for a record, ensuring init headers have been sent prior
+     *
+     * @see sendHeader()
+     * @see sendInitHeaders()
+     * @param array $record
+     */
+    protected function write(array $record)
+    {
+        if (!self::$sendHeaders) {
+            return;
+        }
+
+        // WildFire-specific headers must be sent prior to any messages
+        if (!self::$initialized) {
+            self::$initialized = true;
+
+            self::$sendHeaders = $this->headersAccepted();
+            if (!self::$sendHeaders) {
+                return;
+            }
+
+            foreach ($this->getInitHeaders() as $header => $content) {
+                $this->sendHeader($header, $content);
+            }
+        }
+
+        $header = $this->createRecordHeader($record);
+        if (trim(current($header)) !== '') {
+            $this->sendHeader(key($header), current($header));
+        }
+    }
+
+    /**
+     * Verifies if the headers are accepted by the current user agent
+     *
+     * @return bool
+     */
+    protected function headersAccepted()
+    {
+        if (!empty($_SERVER['HTTP_USER_AGENT']) && preg_match('{\bFirePHP/\d+\.\d+\b}', $_SERVER['HTTP_USER_AGENT'])) {
+            return true;
+        }
+
+        return isset($_SERVER['HTTP_X_FIREPHP_VERSION']);
+    }
+
+    /**
+     * BC getter for the sendHeaders property that has been made static
+     */
+    public function __get($property)
+    {
+        if ('sendHeaders' !== $property) {
+            throw new \InvalidArgumentException('Undefined property '.$property);
+        }
+
+        return static::$sendHeaders;
+    }
+
+    /**
+     * BC setter for the sendHeaders property that has been made static
+     */
+    public function __set($property, $value)
+    {
+        if ('sendHeaders' !== $property) {
+            throw new \InvalidArgumentException('Undefined property '.$property);
+        }
+
+        static::$sendHeaders = $value;
+    }
+}

+ 126 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/FleepHookHandler.php

@@ -0,0 +1,126 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Formatter\LineFormatter;
+use Monolog\Logger;
+
+/**
+ * Sends logs to Fleep.io using Webhook integrations
+ *
+ * You'll need a Fleep.io account to use this handler.
+ *
+ * @see https://fleep.io/integrations/webhooks/ Fleep Webhooks Documentation
+ * @author Ando Roots <ando@sqroot.eu>
+ */
+class FleepHookHandler extends SocketHandler
+{
+    const FLEEP_HOST = 'fleep.io';
+
+    const FLEEP_HOOK_URI = '/hook/';
+
+    /**
+     * @var string Webhook token (specifies the conversation where logs are sent)
+     */
+    protected $token;
+
+    /**
+     * Construct a new Fleep.io Handler.
+     *
+     * For instructions on how to create a new web hook in your conversations
+     * see https://fleep.io/integrations/webhooks/
+     *
+     * @param  string                    $token  Webhook token
+     * @param  bool|int                  $level  The minimum logging level at which this handler will be triggered
+     * @param  bool                      $bubble Whether the messages that are handled can bubble up the stack or not
+     * @throws MissingExtensionException
+     */
+    public function __construct($token, $level = Logger::DEBUG, $bubble = true)
+    {
+        if (!extension_loaded('openssl')) {
+            throw new MissingExtensionException('The OpenSSL PHP extension is required to use the FleepHookHandler');
+        }
+
+        $this->token = $token;
+
+        $connectionString = 'ssl://' . self::FLEEP_HOST . ':443';
+        parent::__construct($connectionString, $level, $bubble);
+    }
+
+    /**
+     * Returns the default formatter to use with this handler
+     *
+     * Overloaded to remove empty context and extra arrays from the end of the log message.
+     *
+     * @return LineFormatter
+     */
+    protected function getDefaultFormatter()
+    {
+        return new LineFormatter(null, null, true, true);
+    }
+
+    /**
+     * Handles a log record
+     *
+     * @param array $record
+     */
+    public function write(array $record)
+    {
+        parent::write($record);
+        $this->closeSocket();
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * @param  array  $record
+     * @return string
+     */
+    protected function generateDataStream($record)
+    {
+        $content = $this->buildContent($record);
+
+        return $this->buildHeader($content) . $content;
+    }
+
+    /**
+     * Builds the header of the API Call
+     *
+     * @param  string $content
+     * @return string
+     */
+    private function buildHeader($content)
+    {
+        $header = "POST " . self::FLEEP_HOOK_URI . $this->token . " HTTP/1.1\r\n";
+        $header .= "Host: " . self::FLEEP_HOST . "\r\n";
+        $header .= "Content-Type: application/x-www-form-urlencoded\r\n";
+        $header .= "Content-Length: " . strlen($content) . "\r\n";
+        $header .= "\r\n";
+
+        return $header;
+    }
+
+    /**
+     * Builds the body of API call
+     *
+     * @param  array  $record
+     * @return string
+     */
+    private function buildContent($record)
+    {
+        $dataArray = array(
+            'message' => $record['formatted'],
+        );
+
+        return http_build_query($dataArray);
+    }
+}

+ 128 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/FlowdockHandler.php

@@ -0,0 +1,128 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Logger;
+use Monolog\Utils;
+use Monolog\Formatter\FlowdockFormatter;
+use Monolog\Formatter\FormatterInterface;
+
+/**
+ * Sends notifications through the Flowdock push API
+ *
+ * This must be configured with a FlowdockFormatter instance via setFormatter()
+ *
+ * Notes:
+ * API token - Flowdock API token
+ *
+ * @author Dominik Liebler <liebler.dominik@gmail.com>
+ * @see https://www.flowdock.com/api/push
+ */
+class FlowdockHandler extends SocketHandler
+{
+    /**
+     * @var string
+     */
+    protected $apiToken;
+
+    /**
+     * @param string   $apiToken
+     * @param bool|int $level    The minimum logging level at which this handler will be triggered
+     * @param bool     $bubble   Whether the messages that are handled can bubble up the stack or not
+     *
+     * @throws MissingExtensionException if OpenSSL is missing
+     */
+    public function __construct($apiToken, $level = Logger::DEBUG, $bubble = true)
+    {
+        if (!extension_loaded('openssl')) {
+            throw new MissingExtensionException('The OpenSSL PHP extension is required to use the FlowdockHandler');
+        }
+
+        parent::__construct('ssl://api.flowdock.com:443', $level, $bubble);
+        $this->apiToken = $apiToken;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setFormatter(FormatterInterface $formatter)
+    {
+        if (!$formatter instanceof FlowdockFormatter) {
+            throw new \InvalidArgumentException('The FlowdockHandler requires an instance of Monolog\Formatter\FlowdockFormatter to function correctly');
+        }
+
+        return parent::setFormatter($formatter);
+    }
+
+    /**
+     * Gets the default formatter.
+     *
+     * @return FormatterInterface
+     */
+    protected function getDefaultFormatter()
+    {
+        throw new \InvalidArgumentException('The FlowdockHandler must be configured (via setFormatter) with an instance of Monolog\Formatter\FlowdockFormatter to function correctly');
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * @param array $record
+     */
+    protected function write(array $record)
+    {
+        parent::write($record);
+
+        $this->closeSocket();
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * @param  array  $record
+     * @return string
+     */
+    protected function generateDataStream($record)
+    {
+        $content = $this->buildContent($record);
+
+        return $this->buildHeader($content) . $content;
+    }
+
+    /**
+     * Builds the body of API call
+     *
+     * @param  array  $record
+     * @return string
+     */
+    private function buildContent($record)
+    {
+        return Utils::jsonEncode($record['formatted']['flowdock']);
+    }
+
+    /**
+     * Builds the header of the API Call
+     *
+     * @param  string $content
+     * @return string
+     */
+    private function buildHeader($content)
+    {
+        $header = "POST /v1/messages/team_inbox/" . $this->apiToken . " HTTP/1.1\r\n";
+        $header .= "Host: api.flowdock.com\r\n";
+        $header .= "Content-Type: application/json\r\n";
+        $header .= "Content-Length: " . strlen($content) . "\r\n";
+        $header .= "\r\n";
+
+        return $header;
+    }
+}

+ 39 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerInterface.php

@@ -0,0 +1,39 @@
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Formatter\FormatterInterface;
+
+/**
+ * Interface to describe loggers that have a formatter
+ *
+ * This interface is present in monolog 1.x to ease forward compatibility.
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+interface FormattableHandlerInterface
+{
+    /**
+     * Sets the formatter.
+     *
+     * @param  FormatterInterface $formatter
+     * @return HandlerInterface   self
+     */
+    public function setFormatter(FormatterInterface $formatter): HandlerInterface;
+
+    /**
+     * Gets the formatter.
+     *
+     * @return FormatterInterface
+     */
+    public function getFormatter(): FormatterInterface;
+}

+ 63 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/FormattableHandlerTrait.php

@@ -0,0 +1,63 @@
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Formatter\FormatterInterface;
+use Monolog\Formatter\LineFormatter;
+
+/**
+ * Helper trait for implementing FormattableInterface
+ *
+ * This trait is present in monolog 1.x to ease forward compatibility.
+ *
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+trait FormattableHandlerTrait
+{
+    /**
+     * @var FormatterInterface
+     */
+    protected $formatter;
+
+    /**
+     * {@inheritdoc}
+     * @suppress PhanTypeMismatchReturn
+     */
+    public function setFormatter(FormatterInterface $formatter): HandlerInterface
+    {
+        $this->formatter = $formatter;
+
+        return $this;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getFormatter(): FormatterInterface
+    {
+        if (!$this->formatter) {
+            $this->formatter = $this->getDefaultFormatter();
+        }
+
+        return $this->formatter;
+    }
+
+    /**
+     * Gets the default formatter.
+     *
+     * Overwrite this if the LineFormatter is not a good default for your handler.
+     */
+    protected function getDefaultFormatter(): FormatterInterface
+    {
+        return new LineFormatter();
+    }
+}

+ 65 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/GelfHandler.php

@@ -0,0 +1,65 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Gelf\IMessagePublisher;
+use Gelf\PublisherInterface;
+use Gelf\Publisher;
+use InvalidArgumentException;
+use Monolog\Logger;
+use Monolog\Formatter\GelfMessageFormatter;
+
+/**
+ * Handler to send messages to a Graylog2 (http://www.graylog2.org) server
+ *
+ * @author Matt Lehner <mlehner@gmail.com>
+ * @author Benjamin Zikarsky <benjamin@zikarsky.de>
+ */
+class GelfHandler extends AbstractProcessingHandler
+{
+    /**
+     * @var Publisher|PublisherInterface|IMessagePublisher the publisher object that sends the message to the server
+     */
+    protected $publisher;
+
+    /**
+     * @param PublisherInterface|IMessagePublisher|Publisher $publisher a publisher object
+     * @param int                                            $level     The minimum logging level at which this handler will be triggered
+     * @param bool                                           $bubble    Whether the messages that are handled can bubble up the stack or not
+     */
+    public function __construct($publisher, $level = Logger::DEBUG, $bubble = true)
+    {
+        parent::__construct($level, $bubble);
+
+        if (!$publisher instanceof Publisher && !$publisher instanceof IMessagePublisher && !$publisher instanceof PublisherInterface) {
+            throw new InvalidArgumentException('Invalid publisher, expected a Gelf\Publisher, Gelf\IMessagePublisher or Gelf\PublisherInterface instance');
+        }
+
+        $this->publisher = $publisher;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function write(array $record)
+    {
+        $this->publisher->publish($record['formatted']);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function getDefaultFormatter()
+    {
+        return new GelfMessageFormatter();
+    }
+}

+ 117 - 0
api/vendor/monolog/monolog/src/Monolog/Handler/GroupHandler.php

@@ -0,0 +1,117 @@
+<?php
+
+/*
+ * This file is part of the Monolog package.
+ *
+ * (c) 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 Monolog\Handler;
+
+use Monolog\Formatter\FormatterInterface;
+use Monolog\ResettableInterface;
+
+/**
+ * Forwards records to multiple handlers
+ *
+ * @author Lenar Lõhmus <lenar@city.ee>
+ */
+class GroupHandler extends AbstractHandler
+{
+    protected $handlers;
+
+    /**
+     * @param array $handlers Array of Handlers.
+     * @param bool  $bubble   Whether the messages that are handled can bubble up the stack or not
+     */
+    public function __construct(array $handlers, $bubble = true)
+    {
+        foreach ($handlers as $handler) {
+            if (!$handler instanceof HandlerInterface) {
+                throw new \InvalidArgumentException('The first argument of the GroupHandler must be an array of HandlerInterface instances.');
+            }
+        }
+
+        $this->handlers = $handlers;
+        $this->bubble = $bubble;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isHandling(array $record)
+    {
+        foreach ($this->handlers as $handler) {
+            if ($handler->isHandling($record)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function handle(array $record)
+    {
+        if ($this->processors) {
+            foreach ($this->processors as $processor) {
+                $record = call_user_func($processor, $record);
+            }
+        }
+
+        foreach ($this->handlers as $handler) {
+            $handler->handle($record);
+        }
+
+        return false === $this->bubble;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function handleBatch(array $records)
+    {
+        if ($this->processors) {
+            $processed = array();
+            foreach ($records as $record) {
+                foreach ($this->processors as $processor) {
+                    $record = call_user_func($processor, $record);
+                }
+                $processed[] = $record;
+            }
+            $records = $processed;
+        }
+
+        foreach ($this->handlers as $handler) {
+            $handler->handleBatch($records);
+        }
+    }
+
+    public function reset()
+    {
+        parent::reset();
+
+        foreach ($this->handlers as $handler) {
+            if ($handler instanceof ResettableInterface) {
+                $handler->reset();
+            }
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function setFormatter(FormatterInterface $formatter)
+    {
+        foreach ($this->handlers as $handler) {
+            $handler->setFormatter($formatter);
+        }
+
+        return $this;
+    }
+}

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