Browse Source

Release0.7

Release0.7
Dennis 3 years ago
parent
commit
babf8a5bf8
100 changed files with 12983 additions and 929 deletions
  1. 10 27
      .env.example
  2. 4 0
      .gitignore
  3. 6 0
      CONTRIBUTING.md
  4. 16 10
      README.md
  5. 67 16
      app/Classes/Pterodactyl.php
  6. 50 0
      app/Classes/Settings/Invoices.php
  7. 61 0
      app/Classes/Settings/Language.php
  8. 85 0
      app/Classes/Settings/Misc.php
  9. 61 0
      app/Classes/Settings/Payments.php
  10. 73 0
      app/Classes/Settings/System.php
  11. 5 5
      app/Http/Controllers/Admin/ApplicationApiController.php
  12. 0 123
      app/Http/Controllers/Admin/ConfigurationController.php
  13. 45 39
      app/Http/Controllers/Admin/CreditProductController.php
  14. 70 0
      app/Http/Controllers/Admin/InvoiceController.php
  15. 420 57
      app/Http/Controllers/Admin/PaymentController.php
  16. 13 15
      app/Http/Controllers/Admin/ProductController.php
  17. 10 8
      app/Http/Controllers/Admin/ServerController.php
  18. 55 11
      app/Http/Controllers/Admin/SettingsController.php
  19. 5 5
      app/Http/Controllers/Admin/UsefulLinkController.php
  20. 14 12
      app/Http/Controllers/Admin/UserController.php
  21. 12 12
      app/Http/Controllers/Admin/VoucherController.php
  22. 69 5
      app/Http/Controllers/Api/UserController.php
  23. 13 5
      app/Http/Controllers/Auth/LoginController.php
  24. 23 20
      app/Http/Controllers/Auth/RegisterController.php
  25. 16 12
      app/Http/Controllers/Auth/SocialiteController.php
  26. 5 11
      app/Http/Controllers/HomeController.php
  27. 46 14
      app/Http/Controllers/ProfileController.php
  28. 43 15
      app/Http/Controllers/ServerController.php
  29. 14 11
      app/Http/Controllers/StoreController.php
  30. 23 0
      app/Http/Controllers/TranslationController.php
  31. 4 3
      app/Http/Kernel.php
  32. 0 23
      app/Http/Middleware/CreditsDisplayName.php
  33. 29 0
      app/Http/Middleware/GlobalNames.php
  34. 41 0
      app/Http/Middleware/SetLocale.php
  35. 1 1
      app/Http/Middleware/VerifyCsrfToken.php
  36. 7 8
      app/Listeners/UnsuspendServers.php
  37. 3 5
      app/Listeners/Verified.php
  38. 21 21
      app/Models/CreditProduct.php
  39. 18 0
      app/Models/Invoice.php
  40. 4 4
      app/Models/Payment.php
  41. 8 6
      app/Models/Settings.php
  42. 2 2
      app/Models/Voucher.php
  43. 6 3
      app/Notifications/ConfirmPaymentNotification.php
  44. 69 0
      app/Notifications/InvoiceNotification.php
  45. 1 1
      app/Notifications/ServerCreationError.php
  46. 10 11
      app/Notifications/ServersSuspendedNotification.php
  47. 20 20
      app/Notifications/WelcomeMessage.php
  48. 57 4
      app/Providers/AppServiceProvider.php
  49. 6 2
      composer.json
  50. 377 190
      composer.lock
  51. 22 2
      config/app.php
  52. 97 0
      config/invoices.php
  53. 16 25
      config/mail.php
  54. 0 0
      database/migrations/.gitkeep
  55. 1 1
      database/migrations/2021_05_08_081218_create_paypal_products_table.php
  56. 0 1
      database/migrations/2021_05_08_164658_create_configurations_table.php
  57. 1 1
      database/migrations/2021_05_09_153742_add_display_to_paypal_products_table.php
  58. 34 0
      database/migrations/2021_11_27_014226_create_invoices.php
  59. 42 0
      database/migrations/2021_12_15_120346_update_to_payments_table.php
  60. 28 0
      database/migrations/2021_12_28_203515_rename_paypal_products_table.php
  61. 61 0
      database/migrations/2022_01_05_144858_rename_configurations_table.php
  62. 34 0
      database/migrations/2022_01_14_234418_update_settings_table_allow_nullable.php
  63. 2 2
      database/seeders/DatabaseSeeder.php
  64. 2 2
      database/seeders/ExampleItemsSeeder.php
  65. 0 149
      database/seeders/Seeds/ConfigurationSeeder.php
  66. 6 6
      database/seeders/Seeds/CreditProductSeeder.php
  67. 469 0
      database/seeders/Seeds/SettingsSeeder.php
  68. 1 1
      package-lock.json
  69. 17 1
      public/css/app.css
  70. BIN
      public/images/discord_logo.png
  71. BIN
      public/images/paypal_logo.png
  72. BIN
      public/images/stripe_logo.png
  73. 48 0
      public/install/dotenv.php
  74. 281 0
      public/install/forms.php
  75. 133 0
      public/install/functions.php
  76. 486 0
      public/install/index.php
  77. 40 0
      public/install/phpmailer/Exception.php
  78. 5041 0
      public/install/phpmailer/PHPMailer.php
  79. 1456 0
      public/install/phpmailer/SMTP.php
  80. 0 1
      public/js/app.js
  81. 354 0
      resources/lang/cs.json
  82. 18 0
      resources/lang/cs/auth.php
  83. 17 0
      resources/lang/cs/pagination.php
  84. 20 0
      resources/lang/cs/passwords.php
  85. 135 0
      resources/lang/cs/validation.php
  86. 445 0
      resources/lang/de.json
  87. 18 0
      resources/lang/de/auth.php
  88. 17 0
      resources/lang/de/pagination.php
  89. 20 0
      resources/lang/de/passwords.php
  90. 135 0
      resources/lang/de/validation.php
  91. 447 0
      resources/lang/en.json
  92. 445 0
      resources/lang/es.json
  93. 18 0
      resources/lang/es/auth.php
  94. 17 0
      resources/lang/es/pagination.php
  95. 20 0
      resources/lang/es/passwords.php
  96. 138 0
      resources/lang/es/validation.php
  97. 328 0
      resources/lang/fr.json
  98. 18 0
      resources/lang/fr/auth.php
  99. 17 0
      resources/lang/fr/pagination.php
  100. 20 0
      resources/lang/fr/passwords.php

+ 10 - 27
.env.example

@@ -1,46 +1,28 @@
-APP_NAME=Dashboard
+### --- App Settings --- ###
+APP_NAME=Controlpanel.gg
 APP_ENV=production
 APP_KEY=
 APP_DEBUG=false
 APP_URL=http://localhost
-#list with timezones https://www.php.net/manual/en/timezones.php
+# List with timezones https://www.php.net/manual/en/timezones.php
 APP_TIMEZONE=UTC
+### --- App Settings End --- ###
 
+### --- DB Settings (required) --- ###
 DB_CONNECTION=mysql
 DB_HOST=127.0.0.1
 DB_PORT=3306
 DB_DATABASE=dashboard
 DB_USERNAME=dashboarduser
 DB_PASSWORD=
+### --- DB Settings End --- ###
 
-#without a pterodactyl api token, this panel won't work!
-PTERODACTYL_TOKEN=
 
-#paypal details, you only need sandbox for testing! you can do this by setting the APP_ENV to local
-PAYPAL_SANDBOX_SECRET=
-PAYPAL_SANDBOX_CLIENT_ID=
-PAYPAL_SECRET=
-PAYPAL_CLIENT_ID=
-PAYPAL_EMAIL=
-
-#set-up for extra discord verification
-DISCORD_CLIENT_ID=
-DISCORD_CLIENT_SECRET=
-#set-up will join users automaticly to your discord
-DISCORD_BOT_TOKEN=
-DISCORD_GUILD_ID=
-#set-up will give the verified user the given role
-DISCORD_ROLE_ID=
-
-#nesseary URL's
-PTERODACTYL_URL=https://panel.controlpanel.gg
-PHPMYADMIN_URL=https://mysql.controlpanel.gg #optional. remove to remove database button
-DISCORD_INVITE_URL=https://discord.gg/vrUYdxG4wZ
-
-#GOOGLE RECAPTCHA
+# Google Recaptcha API Credentials - https://www.google.com/recaptcha/admin - reCaptcha V2 (not v3)
 RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
 RECAPTCHA_SECRET_KEY=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
 
+# Mail Server Settings - (HOST -> SMTP Server)
 MAIL_MAILER=smtp
 MAIL_HOST=mailhog
 MAIL_PORT=1025
@@ -51,10 +33,11 @@ MAIL_FROM_ADDRESS=null
 MAIL_FROM_NAME="${APP_NAME}"
 
 
-#you can leave everything below the way it is
+# Laravel Logging Settings - https://laravel.com/docs/5.7/logging - Not needed to be changed
 LOG_CHANNEL=stack
 LOG_LEVEL=debug
 
+# Do not change anything below this line
 BROADCAST_DRIVER=log
 CACHE_DRIVER=file
 QUEUE_CONNECTION=database

+ 4 - 0
.gitignore

@@ -18,3 +18,7 @@ yarn-error.log
 .gitignore
 .env.dev
 .env.testing
+storage/invoices.zip
+storage/app/public/logo.png
+*vscode
+ - Kopie.env

+ 6 - 0
CONTRIBUTING.md

@@ -4,6 +4,12 @@ When contributing to this repository, please go through the open issues to see i
 
 Please note we have a code of conduct, please follow it in all your interactions with the project.
 
+If you added any Strings which are displayed at the frontend please localize them (e.g. "New String" -> {{ __('New String') }}) and run the localization string generation:
+
+```cmd
+php artisan translatable:export en
+```
+
 ## Pull request process
 
 1. Give your PR a good descriptive title, so we can view immediately what the PR is about.

+ 16 - 10
README.md

@@ -1,26 +1,32 @@
 ### Features
 
-- PayPal Integration
-- Email Verification
-- Audit Log
-- Admin Dashboard
-- User/Server Management
-- Store (credit system)
-- Vouchers
-- and so much more!
+-   PayPal Integration
+-   Stripe Integration
+-   Email Verification
+-   Audit Log
+-   Admin Dashboard
+-   User/Server Management
+-   Store (credit system)
+-   Vouchers
+-   and so much more!
 
 # ControlPanel-gg
-![controlpanel](https://user-images.githubusercontent.com/45005889/123518824-06b05000-d6a8-11eb-91b9-d1ed36bd2317.png)
 
-![](https://img.shields.io/github/stars/ControlPanel-gg/dashboard) ![](https://img.shields.io/github/forks/ControlPanel-gg/dashboard) ![](https://img.shields.io/github/tag/ControlPanel-gg/dashboard) ![](https://img.shields.io/github/issues/ControlPanel-gg/dashboard) ![](https://img.shields.io/github/license/ControlPanel-gg/dashboard) ![](https://img.shields.io/discord/787829714483019826)
+![controlpanel](https://user-images.githubusercontent.com/45005889/123518824-06b05000-d6a8-11eb-91b9-d1ed36bd2317.png)
 
+![](https://img.shields.io/github/stars/ControlPanel-gg/dashboard) ![](https://img.shields.io/github/forks/ControlPanel-gg/dashboard) ![](https://img.shields.io/github/tag/ControlPanel-gg/dashboard) [![Crowdin](https://badges.crowdin.net/controlpanelgg/localized.svg)](https://crowdin.com/project/controlpanelgg) ![](https://img.shields.io/github/issues/ControlPanel-gg/dashboard) ![](https://img.shields.io/github/license/ControlPanel-gg/dashboard) ![](https://img.shields.io/discord/787829714483019826)
 ## About
+
 ControlPanel's Dashboard is a dashboard application designed to offer clients a management tool to manage their pterodactyl servers. This dashboard comes with a credit-based billing solution that credits users hourly for each server they have and suspends them if they run out of credits.
 
 This dashboard offers an easy to use and free billing solution for all starting and experienced hosting providers. This dashboard has many customization options and added discord 0auth verification to offer a solid link between your discord server and your dashboard.
 
 ### [Installation](https://controlpanel.gg/docs/intro "Installation")
+
 ### [Updating](https://controlpanel.gg/docs/Installation/updating "Updating")
+
 ### [Discord](https://discord.gg/4Y6HjD2uyU "discord")
+
 ### [Contributing](https://controlpanel.gg/docs/Contributing/contributing "Contributing")
+
 ### [Donating](https://controlpanel.gg/docs/Contributing/donating "Donating")

+ 67 - 16
app/Classes/Pterodactyl.php

@@ -2,11 +2,11 @@
 
 namespace App\Classes;
 
-use App\Models\Configuration;
 use App\Models\Egg;
 use App\Models\Nest;
 use App\Models\Node;
 use App\Models\Server;
+use App\Models\Settings;
 use Exception;
 use Illuminate\Http\Client\PendingRequest;
 use Illuminate\Http\Client\Response;
@@ -17,7 +17,7 @@ class Pterodactyl
     /**
      * @description per_page option to pull more than the default 50 from pterodactyl
      */
-    public CONST PER_PAGE = 200;
+    public const PER_PAGE = 200;
 
     //TODO: Extend error handling (maybe logger for more errors when debugging)
 
@@ -27,10 +27,10 @@ class Pterodactyl
     public static function client()
     {
         return Http::withHeaders([
-            'Authorization' => 'Bearer ' . env('PTERODACTYL_TOKEN', false),
+            'Authorization' => 'Bearer ' . config("SETTINGS::SYSTEM:PTERODACTYL:TOKEN"),
             'Content-type'  => 'application/json',
             'Accept'        => 'Application/vnd.pterodactyl.v1+json',
-        ])->baseUrl(env('PTERODACTYL_URL') . '/api');
+        ])->baseUrl(config("SETTINGS::SYSTEM:PTERODACTYL:URL") . '/api');
     }
 
     /**
@@ -48,7 +48,11 @@ class Pterodactyl
      */
     public static function getEggs(Nest $nest)
     {
-        $response = self::client()->get("/application/nests/{$nest->id}/eggs?include=nest,variables&per_page=" . self::PER_PAGE);
+        try {
+            $response = self::client()->get("/application/nests/{$nest->id}/eggs?include=nest,variables&per_page=" . self::PER_PAGE);
+        } catch (Exception $e) {
+            throw self::getException();
+        }
         if ($response->failed()) throw self::getException();
         return $response->json()['data'];
     }
@@ -59,7 +63,11 @@ class Pterodactyl
      */
     public static function getNodes()
     {
-        $response = self::client()->get('/application/nodes?per_page=' . self::PER_PAGE);
+        try {
+            $response = self::client()->get('/application/nodes?per_page=' . self::PER_PAGE);
+        } catch (Exception $e) {
+            throw self::getException();
+        }
         if ($response->failed()) throw self::getException();
         return $response->json()['data'];
     }
@@ -70,7 +78,11 @@ class Pterodactyl
      */
     public static function getNests()
     {
-        $response = self::client()->get('/application/nests?per_page=' . self::PER_PAGE);
+        try {
+            $response = self::client()->get('/application/nests?per_page=' . self::PER_PAGE);
+        } catch (Exception $e) {
+            throw self::getException();
+        }
         if ($response->failed()) throw self::getException();
         return $response->json()['data'];
     }
@@ -81,8 +93,13 @@ class Pterodactyl
      */
     public static function getLocations()
     {
-        $response = self::client()->get('/application/locations?per_page=' . self::PER_PAGE);
+        try {
+            $response = self::client()->get('/application/locations?per_page=' . self::PER_PAGE);
+        } catch (Exception $e) {
+            throw self::getException();
+        }
         if ($response->failed()) throw self::getException();
+
         return $response->json()['data'];
     }
 
@@ -124,9 +141,14 @@ class Pterodactyl
      */
     public static function getAllocations(Node $node)
     {
-        $per_page = Configuration::getValueByKey('ALLOCATION_LIMIT', 200);
-        $response = self::client()->get("/application/nodes/{$node->id}/allocations?per_page={$per_page}");
+        $per_page = config('SETTINGS::SERVER:ALLOCATION_LIMIT', 200);
+        try {
+            $response = self::client()->get("/application/nodes/{$node->id}/allocations?per_page={$per_page}");
+        } catch (Exception $e) {
+            throw self::getException();
+        }
         if ($response->failed()) throw self::getException();
+
         return $response->json();
     }
 
@@ -136,7 +158,7 @@ class Pterodactyl
      */
     public static function url(string $route): string
     {
-        return env('PTERODACTYL_URL') . $route;
+        return config("SETTINGS::SYSTEM:PTERODACTYL:URL") . $route;
     }
 
     /**
@@ -171,20 +193,29 @@ class Pterodactyl
                 "default" => $allocationId
             ]
         ]);
-
     }
 
     public static function suspendServer(Server $server)
     {
-        $response = self::client()->post("/application/servers/$server->pterodactyl_id/suspend");
+        try {
+            $response = self::client()->post("/application/servers/$server->pterodactyl_id/suspend");
+        } catch (Exception $e) {
+            throw self::getException();
+        }
         if ($response->failed()) throw self::getException();
+
         return $response;
     }
 
     public static function unSuspendServer(Server $server)
     {
-        $response = self::client()->post("/application/servers/$server->pterodactyl_id/unsuspend");
+        try {
+            $response = self::client()->post("/application/servers/$server->pterodactyl_id/unsuspend");
+        } catch (Exception $e) {
+            throw self::getException();
+        }
         if ($response->failed()) throw self::getException();
+
         return $response;
     }
 
@@ -195,9 +226,29 @@ class Pterodactyl
      */
     public function getUser(int $pterodactylId)
     {
-        $response = self::client()->get("/application/users/{$pterodactylId}");
+        try {
+            $response = self::client()->get("/application/users/{$pterodactylId}");
+        } catch (Exception $e) {
+            throw self::getException();
+        }
+        if ($response->failed()) throw self::getException();
+
+        return $response->json()['attributes'];
+    }
 
-        if ($response->failed()) return $response->json();
+    /**
+     * Get serverAttributes by pterodactyl id
+     * @param int $pterodactylId
+     * @return mixed
+     */
+    public static function getServerAttributes(string $pterodactylId)
+    {
+        try {
+            $response = self::client()->get("/application/servers/{$pterodactylId}?include=egg,node,nest,location");
+        } catch (Exception $e) {
+            throw self::getException();
+        }
+        if ($response->failed()) throw self::getException();
         return $response->json()['attributes'];
     }
 }

+ 50 - 0
app/Classes/Settings/Invoices.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace App\Classes\Settings;
+
+use App\Models\Settings;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Cache;
+
+class Invoices
+{
+    public function __construct()
+    {
+        return;
+    }
+
+
+    public function updateSettings(Request $request)
+    {
+        $request->validate([
+            'logo' => 'nullable|max:10000|mimes:jpg,png,jpeg',
+        ]);
+
+        $values = [
+            //SETTINGS::VALUE => REQUEST-VALUE (coming from the html-form)
+            "SETTINGS::INVOICE:COMPANY_NAME" => "company-name",
+            "SETTINGS::INVOICE:COMPANY_ADDRESS" => "company-address",
+            "SETTINGS::INVOICE:COMPANY_PHONE" => "company-phone",
+            "SETTINGS::INVOICE:COMPANY_MAIL" => "company-mail",
+            "SETTINGS::INVOICE:COMPANY_VAT" => "company-vat",
+            "SETTINGS::INVOICE:COMPANY_WEBSITE" => "company-web",
+            "SETTINGS::INVOICE:PREFIX" => "invoice-prefix",
+            "SETTINGS::INVOICE:ENABLED" => "enable-invoices",
+        ];
+
+        foreach ($values as $key => $value) {
+            $param = $request->get($value);
+
+            Settings::where('key', $key)->updateOrCreate(['key' => $key], ['value' => $param]);
+            Cache::forget("setting" . ':' . $key);
+        }
+
+
+        if ($request->hasFile('logo')) {
+            $request->file('logo')->storeAs('public', 'logo.png');
+        }
+
+
+        return redirect(route('admin.settings.index') . '#invoices')->with('success', __('Invoice settings updated!'));
+    }
+}

+ 61 - 0
app/Classes/Settings/Language.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace App\Classes\Settings;
+
+use App\Models\Settings;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Session;
+use Illuminate\Support\Facades\Validator;
+
+
+class Language
+{
+    public function __construct()
+    {
+        return;
+    }
+
+
+    public function updateSettings(Request $request)
+    {
+        $validator = Validator::make($request->all(), [
+            'autotranslate' => 'string',
+            'canClientChangeLanguage' => 'string',
+            'defaultLanguage' => 'required|string',
+            'languages' => 'required|array',
+            'languages.*' => 'required|string',
+            'datatable-language' => 'required|string',
+        ]);
+
+
+        if ($validator->fails()) {
+            return redirect(route('admin.settings.index') . '#language')->with('error', __('Language settings have not been updated!'))->withErrors($validator);
+        }
+
+        $values = [
+            //SETTINGS::VALUE => REQUEST-VALUE (coming from the html-form)
+            "SETTINGS::LOCALE:DEFAULT" => "defaultLanguage",
+            "SETTINGS::LOCALE:DYNAMIC" => "autotranslate",
+            "SETTINGS::LOCALE:CLIENTS_CAN_CHANGE" => "canClientChangeLanguage",
+            "SETTINGS::LOCALE:AVAILABLE" => "languages",
+            "SETTINGS::LOCALE:DATATABLES" => "datatable-language"
+        ];
+
+
+        foreach ($values as $key => $value) {
+            $param = $request->get($value);
+
+            if (is_array($param)) {
+                $param = implode(",", $param);
+            }
+
+            Settings::where('key', $key)->updateOrCreate(['key' => $key], ['value' => $param]);
+            Cache::forget("setting" . ':' . $key);
+            Session::remove("locale");
+        }
+
+
+        return redirect(route('admin.settings.index') . '#language')->with('success', __('Language settings updated!'));
+    }
+}

+ 85 - 0
app/Classes/Settings/Misc.php

@@ -0,0 +1,85 @@
+<?php
+
+namespace App\Classes\Settings;
+
+use App\Models\Settings;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Validator;
+
+
+class Misc
+{
+    public function __construct()
+    {
+        return;
+    }
+
+    public function updateSettings(Request $request)
+    {
+        $validator = Validator::make($request->all(), [
+            'icon' => 'nullable|max:10000|mimes:jpg,png,jpeg',
+            'favicon' => 'nullable|max:10000|mimes:ico',
+            'discord-bot-token' => 'nullable|string',
+            'discord-client-id' => 'nullable|string',
+            'discord-client-secret' => 'nullable|string',
+            'discord-guild-id' => 'nullable|string',
+            'discord-invite-url' => 'nullable|string',
+            'discord-role-id' => 'nullable|string',
+            'recaptcha-site-key' => 'nullable|string',
+            'recaptcha-secret-key' => 'nullable|string',
+            'enable-recaptcha' => 'nullable|string',
+            'mailservice' => 'nullable|string',
+            'mailhost' => 'nullable|string',
+            'mailport' => 'nullable|string',
+            'mailusername' => 'nullable|string',
+            'mailpassword' => 'nullable|string',
+            'mailencryption' => 'nullable|string',
+            'mailfromadress' => 'nullable|string',
+            'mailfromname' => 'nullable|string',
+        ]);
+
+        if ($validator->fails()) {
+            return redirect(route('admin.settings.index') . '#misc')->with('error', __('Misc settings have not been updated!'))->withErrors($validator)
+                ->withInput();
+        }
+
+        if ($request->hasFile('icon')) {
+            $request->file('icon')->storeAs('public', 'icon.png');
+        }
+        if ($request->hasFile('favicon')) {
+            $request->file('favicon')->storeAs('public', 'favicon.ico');
+        }
+
+        $values = [
+            "SETTINGS::DISCORD:BOT_TOKEN" => "discord-bot-token",
+            "SETTINGS::DISCORD:CLIENT_ID" => "discord-client-id",
+            "SETTINGS::DISCORD:CLIENT_SECRET" => "discord-client-secret",
+            "SETTINGS::DISCORD:GUILD_ID" => "discord-guild-id",
+            "SETTINGS::DISCORD:INVITE_URL" => "discord-invite-url",
+            "SETTINGS::DISCORD:ROLE_ID" => "discord-role-id",
+            "SETTINGS::RECAPTCHA:SITE_KEY" => "recaptcha-site-key",
+            "SETTINGS::RECAPTCHA:SECRET_KEY" => "recaptcha-secret-key",
+            "SETTINGS::RECAPTCHA:ENABLED" => "enable-recaptcha",
+            "SETTINGS::MAIL:MAILER" => "mailservice",
+            "SETTINGS::MAIL:HOST" => "mailhost",
+            "SETTINGS::MAIL:PORT" => "mailport",
+            "SETTINGS::MAIL:USERNAME" => "mailusername",
+            "SETTINGS::MAIL:PASSWORD" => "mailpassword",
+            "SETTINGS::MAIL:ENCRYPTION" => "mailencryption",
+            "SETTINGS::MAIL:FROM_ADDRESS" => "mailfromadress",
+            "SETTINGS::MAIL:FROM_NAME" => "mailfromname",
+
+        ];
+
+        foreach ($values as $key => $value) {
+            $param = $request->get($value);
+
+            Settings::where('key', $key)->updateOrCreate(['key' => $key], ['value' => $param]);
+            Cache::forget("setting" . ':' . $key);
+        }
+
+
+        return redirect(route('admin.settings.index') . '#misc')->with('success', __('Misc settings updated!'));
+    }
+}

+ 61 - 0
app/Classes/Settings/Payments.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace App\Classes\Settings;
+
+use App\Models\Settings;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Validator;
+
+
+class Payments
+{
+    public function __construct()
+    {
+        return;
+    }
+
+
+    public function updateSettings(Request $request)
+    {
+        $validator = Validator::make($request->all(), [
+            "paypal-client_id" => "nullable|string",
+            "paypal-client-secret" => "nullable|string",
+            "paypal-sandbox-secret" => "nullable|string",
+            "stripe-secret-key" => "nullable|string",
+            "stripe-endpoint-secret" => "nullable|string",
+            "stripe-test-secret-key" => "nullable|string",
+            "stripe-test-endpoint-secret" => "nullable|string",
+            "stripe-methods" => "nullable|string",
+            "sales-tax" => "nullable|numeric",
+        ]);
+        if ($validator->fails()) {
+            return redirect(route('admin.settings.index') . '#payment')->with('error', __('Payment settings have not been updated!'))->withErrors($validator)
+                ->withInput();
+        }
+
+        $values = [
+            //SETTINGS::VALUE => REQUEST-VALUE (coming from the html-form)
+            "SETTINGS::PAYMENTS:PAYPAL:SECRET" => "paypal-client-secret",
+            "SETTINGS::PAYMENTS:PAYPAL:CLIENT_ID" => "paypal-client-id",
+            "SETTINGS::PAYMENTS:PAYPAL:SANDBOX_SECRET" => "paypal-sandbox-secret",
+            "SETTINGS::PAYMENTS:PAYPAL:SANDBOX_CLIENT_ID" => "paypal-sandbox-id",
+            "SETTINGS::PAYMENTS:STRIPE:SECRET" => "stripe-secret",
+            "SETTINGS::PAYMENTS:STRIPE:ENDPOINT_SECRET" => "stripe-endpoint-secret",
+            "SETTINGS::PAYMENTS:STRIPE:TEST_SECRET" => "stripe-test-secret",
+            "SETTINGS::PAYMENTS:STRIPE:ENDPOINT_TEST_SECRET" => "stripe-endpoint-test-secret",
+            "SETTINGS::PAYMENTS:STRIPE:METHODS" => "stripe-methods",
+            "SETTINGS::PAYMENTS:SALES_TAX" => "sales-tax"
+        ];
+
+
+        foreach ($values as $key => $value) {
+            $param = $request->get($value);
+
+            Settings::where('key', $key)->updateOrCreate(['key' => $key], ['value' => $param]);
+            Cache::forget("setting" . ':' . $key);
+        }
+
+        return redirect(route('admin.settings.index') . '#payment')->with('success', __('Payment settings updated!'));
+    }
+}

+ 73 - 0
app/Classes/Settings/System.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace App\Classes\Settings;
+
+use App\Models\Settings;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Validator;
+
+class System
+{
+
+
+    public function __construct()
+    {
+        return;
+    }
+
+
+
+    public function updateSettings(Request $request)
+    {
+        $validator = Validator::make($request->all(), [
+            "register-ip-check" => "string",
+            "server-create-charge-first-hour" => "string",
+            "credits-display-name" => "required|string",
+            "allocation-limit" => "required|min:0|integer",
+            "force-email-verification" => "string",
+            "force-discord-verification" => "string",
+            "initial-credits" => "required|min:0|integer",
+            "initial-server-limit" => "required|min:0|integer",
+            "credits-reward-amount-discord" => "required|min:0|integer",
+            "credits-reward-amount-email" => "required|min:0|integer",
+            "server-limit-discord" => "required|min:0|integer",
+            "server-limit-email" => "required|min:0|integer",
+            "pterodactyl-api-key" => "required|string",
+            "pterodactyl-url" => "required|string",
+
+        ]);
+        if ($validator->fails()) {
+            return redirect(route('admin.settings.index') . '#system')->with('error', __('System settings have not been updated!'))->withErrors($validator)
+                ->withInput();
+        }
+
+
+        $values = [
+            "SETTINGS::SYSTEM:REGISTER_IP_CHECK" => "register-ip-check",
+            "SETTINGS::SYSTEM:SERVER_CREATE_CHARGE_FIRST_HOUR" => "server-create-charge-first-hour",
+            "SETTINGS::SYSTEM:CREDITS_DISPLAY_NAME" => "credits-display-name",
+            "SETTINGS::SERVER:ALLOCATION_LIMIT" => "allocation-limit",
+            "SETTINGS::USER:FORCE_DISCORD_VERIFICATION" => "force-discord-verification",
+            "SETTINGS::USER:FORCE_EMAIL_VERIFICATION" => "force-email-verification",
+            "SETTINGS::USER:INITIAL_CREDITS" => "initial-credits",
+            "SETTINGS::USER:INITIAL_SERVER_LIMIT" => "initial-server-limit",
+            "SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_DISCORD" => "credits-reward-amount-discord",
+            "SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_EMAIL" => "credits-reward-amount-email",
+            "SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD" => "server-limit-discord",
+            "SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL" => "server-limit-email",
+            "SETTINGS::MISC:PHPMYADMIN:URL" => "phpmyadmin-url",
+            "SETTINGS::SYSTEM:PTERODACTYL:URL" => "pterodactyl-url",
+            "SETTINGS::SYSTEM:PTERODACTYL:TOKEN" => "pterodactyl-api-key",
+        ];
+
+
+        foreach ($values as $key => $value) {
+            $param = $request->get($value);
+
+            Settings::where('key', $key)->updateOrCreate(['key' => $key], ['value' => $param]);
+            Cache::forget("setting" . ':' . $key);
+        }
+        return redirect(route('admin.settings.index') . '#system')->with('success', __('System settings updated!'));
+    }
+}

+ 5 - 5
app/Http/Controllers/Admin/ApplicationApiController.php

@@ -52,7 +52,7 @@ class ApplicationApiController extends Controller
             'memo' => $request->input('memo')
         ]);
 
-        return redirect()->route('admin.api.index')->with('success', 'api key created!');
+        return redirect()->route('admin.api.index')->with('success', __('api key created!'));
     }
 
     /**
@@ -94,7 +94,7 @@ class ApplicationApiController extends Controller
 
         $applicationApi->update($request->all());
 
-        return redirect()->route('admin.api.index')->with('success', 'api key updated!');
+        return redirect()->route('admin.api.index')->with('success', __('api key updated!'));
     }
 
     /**
@@ -106,7 +106,7 @@ class ApplicationApiController extends Controller
     public function destroy(ApplicationApi $applicationApi)
     {
         $applicationApi->delete();
-        return redirect()->back()->with('success', 'api key has been removed!');
+        return redirect()->back()->with('success', __('api key has been removed!'));
     }
 
     /**
@@ -121,11 +121,11 @@ class ApplicationApiController extends Controller
         return datatables($query)
             ->addColumn('actions', function (ApplicationApi $apiKey) {
                 return '
-                <a data-content="Edit" data-toggle="popover" data-trigger="hover" data-placement="top"  href="' . route('admin.api.edit', $apiKey->token) . '" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
+                <a data-content="'.__("Edit").'" data-toggle="popover" data-trigger="hover" data-placement="top"  href="' . route('admin.api.edit', $apiKey->token) . '" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
                 <form class="d-inline" onsubmit="return submitResult();" method="post" action="' . route('admin.api.destroy', $apiKey->token) . '">
                             ' . csrf_field() . '
                             ' . method_field("DELETE") . '
-                           <button data-content="Delete" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
+                           <button data-content="'.__("Delete").'" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
                        </form>
                 ';
             })

+ 0 - 123
app/Http/Controllers/Admin/ConfigurationController.php

@@ -1,123 +0,0 @@
-<?php
-
-namespace App\Http\Controllers\Admin;
-
-use App\Http\Controllers\Controller;
-use App\Models\Configuration;
-use Illuminate\Contracts\Foundation\Application;
-use Illuminate\Contracts\View\Factory;
-use Illuminate\Contracts\View\View;
-use Illuminate\Http\Request;
-use Illuminate\Http\Response;
-
-class ConfigurationController extends Controller
-{
-    /**
-     * Display a listing of the resource.
-     *
-     * @return Application|Factory|View|Response
-     */
-    public function index()
-    {
-        return view('admin.configurations.index');
-    }
-
-    /**
-     * Show the form for creating a new resource.
-     *
-     * @return Response
-     */
-    public function create()
-    {
-        //
-    }
-
-    /**
-     * Store a newly created resource in storage.
-     *
-     * @param Request $request
-     * @return Response
-     */
-    public function store(Request $request)
-    {
-        //
-    }
-
-    /**
-     * Display the specified resource.
-     *
-     * @param Configuration $configuration
-     * @return Response
-     */
-    public function show(Configuration $configuration)
-    {
-        //
-    }
-
-    /**
-     * Show the form for editing the specified resource.
-     *
-     * @param Configuration $configuration
-     * @return Response
-     */
-    public function edit(Configuration $configuration)
-    {
-        //
-    }
-
-    /**
-     * Update the specified resource in storage.
-     *
-     * @param Request $request
-     * @param Configuration $configuration
-     * @return Response
-     */
-    public function update(Request $request, Configuration $configuration)
-    {
-        //
-    }
-
-    /**
-     * @param Request $request
-     * @return \Illuminate\Http\RedirectResponse
-     */
-    public function updatevalue(Request $request)
-    {
-        $configuration = Configuration::findOrFail($request->input('key'));
-
-        $request->validate([
-            'key'   => 'required|string|max:191',
-            'value' => 'required|string|max:191',
-        ]);
-
-        $configuration->update($request->all());
-
-        return redirect()->route('admin.configurations.index')->with('success', 'configuration has been updated!');
-    }
-
-    /**
-     * Remove the specified resource from storage.
-     *
-     * @param Configuration $configuration
-     * @return Response
-     */
-    public function destroy(Configuration $configuration)
-    {
-        //
-    }
-
-    public function datatable()
-    {
-        $query = Configuration::query();
-
-        return datatables($query)
-            ->addColumn('actions', function (Configuration $configuration) {
-                return '<button data-content="Edit" data-toggle="popover" data-trigger="hover" data-placement="top" onclick="configuration.parse(\'' . $configuration->key . '\',\'' . $configuration->value . '\',\'' . $configuration->type . '\')" data-content="Edit" data-trigger="hover" data-toggle="tooltip" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></button> ';
-            })
-            ->editColumn('created_at', function (Configuration $configuration) {
-                return $configuration->created_at ? $configuration->created_at->diffForHumans() : '';
-            })
-            ->rawColumns(['actions'])
-            ->make();
-    }
-}

+ 45 - 39
app/Http/Controllers/Admin/PaypalProductController.php → app/Http/Controllers/Admin/CreditProductController.php

@@ -2,7 +2,8 @@
 
 namespace App\Http\Controllers\Admin;
 
-use App\Models\PaypalProduct;
+use App\Models\CreditProduct;
+use App\Models\Settings;
 use Illuminate\Contracts\Foundation\Application;
 use Illuminate\Contracts\View\Factory;
 use Illuminate\Contracts\View\View;
@@ -12,7 +13,7 @@ use Illuminate\Http\Response;
 use Illuminate\Routing\Controller;
 use Illuminate\Validation\Rule;
 
-class PaypalProductController extends Controller
+class CreditProductController extends Controller
 {
     /**
      * Display a listing of the resource.
@@ -21,11 +22,16 @@ class PaypalProductController extends Controller
      */
     public function index(Request $request)
     {
-        $isPaypalSetup = false;
-        if (env('PAYPAL_SECRET') && env('PAYPAL_CLIENT_ID')) $isPaypalSetup = true;
+        $isPaymentSetup = false;
 
-        return view('admin.store.index' , [
-            'isPaypalSetup' => $isPaypalSetup
+        if (
+            env('APP_ENV') == 'local' ||
+            config("SETTINGS::PAYMENTS:PAYPAL:SECRET") && config("SETTINGS::PAYMENTS:PAYPAL:CLIENT_ID") ||
+            config("SETTINGS::PAYMENTS:STRIPE:SECRET") && config("SETTINGS::PAYMENTS:STRIPE:ENDPOINT_SECRET") && config("SETTINGS::PAYMENTS:STRIPE:METHODS")
+        ) $isPaymentSetup = true;
+
+        return view('admin.store.index', [
+            'isPaymentSetup' => $isPaymentSetup
         ]);
     }
 
@@ -60,18 +66,18 @@ class PaypalProductController extends Controller
         ]);
 
         $disabled = !is_null($request->input('disabled'));
-        PaypalProduct::create(array_merge($request->all(), ['disabled' => $disabled]));
+        CreditProduct::create(array_merge($request->all(), ['disabled' => $disabled]));
 
-        return redirect()->route('admin.store.index')->with('success', 'Store item has been created!');
+        return redirect()->route('admin.store.index')->with('success', __('Store item has been created!'));
     }
 
     /**
      * Display the specified resource.
      *
-     * @param PaypalProduct $paypalProduct
+     * @param CreditProduct $creditProduct
      * @return Response
      */
-    public function show(PaypalProduct $paypalProduct)
+    public function show(CreditProduct $creditProduct)
     {
         //
     }
@@ -79,14 +85,14 @@ class PaypalProductController extends Controller
     /**
      * Show the form for editing the specified resource.
      *
-     * @param PaypalProduct $paypalProduct
+     * @param CreditProduct $creditProduct
      * @return Application|Factory|View|Response
      */
-    public function edit(PaypalProduct $paypalProduct)
+    public function edit(CreditProduct $creditProduct)
     {
         return view('admin.store.edit', [
             'currencyCodes' => config('currency_codes'),
-            'paypalProduct' => $paypalProduct
+            'creditProduct' => $creditProduct
         ]);
     }
 
@@ -94,10 +100,10 @@ class PaypalProductController extends Controller
      * Update the specified resource in storage.
      *
      * @param Request $request
-     * @param PaypalProduct $paypalProduct
+     * @param CreditProduct $creditProduct
      * @return RedirectResponse
      */
-    public function update(Request $request, PaypalProduct $paypalProduct)
+    public function update(Request $request, CreditProduct $creditProduct)
     {
         $request->validate([
             "disabled"      => "nullable",
@@ -110,70 +116,70 @@ class PaypalProductController extends Controller
         ]);
 
         $disabled = !is_null($request->input('disabled'));
-        $paypalProduct->update(array_merge($request->all(), ['disabled' => $disabled]));
+        $creditProduct->update(array_merge($request->all(), ['disabled' => $disabled]));
 
-        return redirect()->route('admin.store.index')->with('success', 'Store item has been updated!');
+        return redirect()->route('admin.store.index')->with('success', __('Store item has been updated!'));
     }
 
     /**
      * @param Request $request
-     * @param PaypalProduct $paypalProduct
+     * @param CreditProduct $creditProduct
      * @return RedirectResponse
      */
-    public function disable(Request $request, PaypalProduct $paypalProduct)
+    public function disable(Request $request, CreditProduct $creditProduct)
     {
-        $paypalProduct->update(['disabled' => !$paypalProduct->disabled]);
+        $creditProduct->update(['disabled' => !$creditProduct->disabled]);
 
-        return redirect()->route('admin.store.index')->with('success', 'Product has been updated!');
+        return redirect()->route('admin.store.index')->with('success', __('Product has been updated!'));
     }
 
     /**
      * Remove the specified resource from storage.
      *
-     * @param PaypalProduct $paypalProduct
+     * @param CreditProduct $creditProduct
      * @return RedirectResponse
      */
-    public function destroy(PaypalProduct $paypalProduct)
+    public function destroy(CreditProduct $creditProduct)
     {
-        $paypalProduct->delete();
-        return redirect()->back()->with('success', 'Store item has been removed!');
+        $creditProduct->delete();
+        return redirect()->back()->with('success', __('Store item has been removed!'));
     }
 
 
     public function dataTable()
     {
-        $query = PaypalProduct::query();
+        $query = CreditProduct::query();
 
         return datatables($query)
-            ->addColumn('actions', function (PaypalProduct $paypalProduct) {
+            ->addColumn('actions', function (CreditProduct $creditProduct) {
                 return '
-                            <a data-content="Edit" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.store.edit', $paypalProduct->id) . '" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
+                            <a data-content="' . __("Edit") . '" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.store.edit', $creditProduct->id) . '" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
 
-                           <form class="d-inline" onsubmit="return submitResult();" method="post" action="' . route('admin.store.destroy', $paypalProduct->id) . '">
+                           <form class="d-inline" onsubmit="return submitResult();" method="post" action="' . route('admin.store.destroy', $creditProduct->id) . '">
                             ' . csrf_field() . '
                             ' . method_field("DELETE") . '
-                           <button data-content="Delete" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
+                           <button data-content="' . __("Delete") . '" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
                        </form>
                 ';
             })
-            ->addColumn('disabled', function (PaypalProduct $paypalProduct) {
-                $checked = $paypalProduct->disabled == false ? "checked" : "";
+            ->addColumn('disabled', function (CreditProduct $creditProduct) {
+                $checked = $creditProduct->disabled == false ? "checked" : "";
                 return '
-                                <form class="d-inline" onsubmit="return submitResult();" method="post" action="' . route('admin.store.disable', $paypalProduct->id) . '">
+                                <form class="d-inline" onsubmit="return submitResult();" method="post" action="' . route('admin.store.disable', $creditProduct->id) . '">
                             ' . csrf_field() . '
                             ' . method_field("PATCH") . '
                             <div class="custom-control custom-switch">
-                            <input ' . $checked . ' name="disabled" onchange="this.form.submit()" type="checkbox" class="custom-control-input" id="switch' . $paypalProduct->id . '">
-                            <label class="custom-control-label" for="switch' . $paypalProduct->id . '"></label>
+                            <input ' . $checked . ' name="disabled" onchange="this.form.submit()" type="checkbox" class="custom-control-input" id="switch' . $creditProduct->id . '">
+                            <label class="custom-control-label" for="switch' . $creditProduct->id . '"></label>
                           </div>
                        </form>
                 ';
             })
-            ->editColumn('created_at', function (PaypalProduct $paypalProduct) {
-                return $paypalProduct->created_at ? $paypalProduct->created_at->diffForHumans() : '';
+            ->editColumn('created_at', function (CreditProduct $creditProduct) {
+                return $creditProduct->created_at ? $creditProduct->created_at->diffForHumans() : '';
             })
-            ->editColumn('price', function (PaypalProduct $paypalProduct) {
-                return $paypalProduct->formatToCurrency($paypalProduct->price);
+            ->editColumn('price', function (CreditProduct $creditProduct) {
+                return $creditProduct->formatToCurrency($creditProduct->price);
             })
             ->rawColumns(['actions', 'disabled'])
             ->make();

+ 70 - 0
app/Http/Controllers/Admin/InvoiceController.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace App\Http\Controllers\Admin;
+
+use App\Http\Controllers\Controller;
+use App\Models\Invoice;
+use Illuminate\Http\Request;
+use Throwable;
+use ZipArchive;
+
+class InvoiceController extends Controller
+{
+
+    public function downloadAllInvoices()
+    {
+        $zip = new ZipArchive;
+        $zip_safe_path = storage_path('invoices.zip');
+        $res = $zip->open($zip_safe_path, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+        $result = $dthis::rglob(storage_path('app/invoice/*'));
+        if ($res === TRUE) {
+            $zip->addFromString("1. Info.txt", __("Created at") . " " . now()->format("d.m.Y"));
+            foreach ($result as $file) {
+                if (file_exists($file) && is_file($file)) {
+                    $zip->addFile($file, basename($file));
+                }
+            }
+            $zip->close();
+        }
+        return response()->download($zip_safe_path);
+    }
+
+    /**
+     * @param $pattern
+     * @param $flags
+     * @return array|false
+     */
+    public function rglob($pattern, $flags = 0)
+    {
+        $files = glob($pattern, $flags);
+        foreach (glob(dirname($pattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) {
+            $files = array_merge($files, $this::rglob($dir . '/' . basename($pattern), $flags));
+        }
+        return $files;
+    }
+
+    /**
+     * @param $paymentID
+     * @param $date
+     */
+    public function downloadSingleInvoice(Request $request)
+    {
+        $id = $request->id;
+        try {
+            $query = Invoice::where('payment_id', '=', $id)->firstOrFail();
+        } catch (Throwable $e) {
+            return redirect()->back()->with("error", __("Error!"));
+        }
+
+        $invoice_path = storage_path('app/invoice/' . $query->invoice_user . '/' . $query->created_at->format("Y") . '/' . $query->invoice_name . '.pdf');
+
+        if (!file_exists($invoice_path)) {
+            return redirect()->back()->with("error", __("Error!"));
+        }
+
+
+        return response()->download($invoice_path);
+
+    }
+
+}

+ 420 - 57
app/Http/Controllers/Admin/PaymentController.php

@@ -4,11 +4,12 @@ namespace App\Http\Controllers\Admin;
 
 use App\Events\UserUpdateCreditsEvent;
 use App\Http\Controllers\Controller;
-use App\Models\Configuration;
+use App\Models\InvoiceSettings;
 use App\Models\Payment;
-use App\Models\PaypalProduct;
-use App\Models\Product;
+use App\Models\CreditProduct;
+use App\Models\Settings;
 use App\Models\User;
+use App\Notifications\InvoiceNotification;
 use App\Notifications\ConfirmPaymentNotification;
 use Exception;
 use Illuminate\Contracts\Foundation\Application;
@@ -18,12 +19,20 @@ use Illuminate\Http\JsonResponse;
 use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Storage;
+use LaravelDaily\Invoices\Classes\Buyer;
+use LaravelDaily\Invoices\Classes\InvoiceItem;
+use LaravelDaily\Invoices\Classes\Party;
+use LaravelDaily\Invoices\Invoice;
 use PayPalCheckoutSdk\Core\PayPalHttpClient;
 use PayPalCheckoutSdk\Core\ProductionEnvironment;
 use PayPalCheckoutSdk\Core\SandboxEnvironment;
 use PayPalCheckoutSdk\Orders\OrdersCaptureRequest;
 use PayPalCheckoutSdk\Orders\OrdersCreateRequest;
 use PayPalHttp\HttpException;
+use Stripe\Stripe;
+use Symfony\Component\Intl\Currencies;
+
 
 class PaymentController extends Controller
 {
@@ -40,25 +49,25 @@ class PaymentController extends Controller
 
     /**
      * @param Request $request
-     * @param PaypalProduct $paypalProduct
+     * @param CreditProduct $creditProduct
      * @return Application|Factory|View
      */
-    public function checkOut(Request $request, PaypalProduct $paypalProduct)
+    public function checkOut(Request $request, CreditProduct $creditProduct)
     {
         return view('store.checkout')->with([
-            'product'      => $paypalProduct,
-            'taxvalue'     => $paypalProduct->getTaxValue(),
-            'taxpercent'   => $paypalProduct->getTaxPercent(),
-            'total'        => $paypalProduct->getTotalPrice()
+            'product'      => $creditProduct,
+            'taxvalue'     => $creditProduct->getTaxValue(),
+            'taxpercent'   => $creditProduct->getTaxPercent(),
+            'total'        => $creditProduct->getTotalPrice()
         ]);
     }
 
     /**
      * @param Request $request
-     * @param PaypalProduct $paypalProduct
+     * @param CreditProduct $creditProduct
      * @return RedirectResponse
      */
-    public function pay(Request $request, PaypalProduct $paypalProduct)
+    public function PaypalPay(Request $request, CreditProduct $creditProduct)
     {
         $request = new OrdersCreateRequest();
         $request->prefer('return=representation');
@@ -67,33 +76,33 @@ class PaymentController extends Controller
             "purchase_units" => [
                 [
                     "reference_id" => uniqid(),
-                    "description" => $paypalProduct->description,
+                    "description" => $creditProduct->description,
                     "amount"       => [
-                        "value"         => $paypalProduct->getTotalPrice(),
-                        'currency_code' => strtoupper($paypalProduct->currency_code),
-                        'breakdown' =>[
+                        "value"         => $creditProduct->getTotalPrice(),
+                        'currency_code' => strtoupper($creditProduct->currency_code),
+                        'breakdown' => [
                             'item_total' =>
-                               [
-                                    'currency_code' => strtoupper($paypalProduct->currency_code),
-                                    'value' => $paypalProduct->price,
-                                ],
+                            [
+                                'currency_code' => strtoupper($creditProduct->currency_code),
+                                'value' => $creditProduct->price,
+                            ],
                             'tax_total' =>
-                                [
-                                    'currency_code' => strtoupper($paypalProduct->currency_code),
-                                    'value' => $paypalProduct->getTaxValue(),
-                                ]
+                            [
+                                'currency_code' => strtoupper($creditProduct->currency_code),
+                                'value' => $creditProduct->getTaxValue(),
+                            ]
                         ]
                     ]
                 ]
             ],
             "application_context" => [
-                "cancel_url" => route('payment.cancel'),
-                "return_url" => route('payment.success', ['product' => $paypalProduct->id]),
+                "cancel_url" => route('payment.Cancel'),
+                "return_url" => route('payment.PaypalSuccess', ['product' => $creditProduct->id]),
                 'brand_name' =>  config('app.name', 'Laravel'),
                 'shipping_preference'  => 'NO_SHIPPING'
             ]
 
-        
+
         ];
 
 
@@ -107,7 +116,6 @@ class PaymentController extends Controller
             echo $ex->statusCode;
             dd(json_decode($ex->getMessage()));
         }
-
     }
 
     /**
@@ -116,8 +124,8 @@ class PaymentController extends Controller
     protected function getPayPalClient()
     {
         $environment = env('APP_ENV') == 'local'
-            ? new SandboxEnvironment($this->getClientId(), $this->getClientSecret())
-            : new ProductionEnvironment($this->getClientId(), $this->getClientSecret());
+            ? new SandboxEnvironment($this->getPaypalClientId(), $this->getPaypalClientSecret())
+            : new ProductionEnvironment($this->getPaypalClientId(), $this->getPaypalClientSecret());
 
         return new PayPalHttpClient($environment);
     }
@@ -125,26 +133,27 @@ class PaymentController extends Controller
     /**
      * @return string
      */
-    protected function getClientId()
+    protected function getPaypalClientId()
     {
-        return env('APP_ENV') == 'local' ? env('PAYPAL_SANDBOX_CLIENT_ID') : env('PAYPAL_CLIENT_ID');
+        return env('APP_ENV') == 'local' ?  config("SETTINGS::PAYMENTS:PAYPAL:SANDBOX_CLIENT_ID") : config("SETTINGS::PAYMENTS:PAYPAL:CLIENT_ID");
     }
 
     /**
      * @return string
      */
-    protected function getClientSecret()
+    protected function getPaypalClientSecret()
     {
-        return env('APP_ENV') == 'local' ? env('PAYPAL_SANDBOX_SECRET') : env('PAYPAL_SECRET');
+        return env('APP_ENV') == 'local' ? config("SETTINGS::PAYMENTS:PAYPAL:SANDBOX_SECRET") : config("SETTINGS::PAYMENTS:PAYPAL:SECRET");
     }
 
     /**
      * @param Request $laravelRequest
      */
-    public function success(Request $laravelRequest)
+    public function PaypalSuccess(Request $laravelRequest)
     {
-        /** @var PaypalProduct $paypalProduct */
-        $paypalProduct = PaypalProduct::findOrFail($laravelRequest->input('product'));
+        /** @var CreditProduct $creditProduct */
+        $creditProduct = CreditProduct::findOrFail($laravelRequest->input('product'));
+
         /** @var User $user */
         $user = Auth::user();
 
@@ -156,15 +165,15 @@ class PaymentController extends Controller
             if ($response->statusCode == 201 || $response->statusCode == 200) {
 
                 //update credits
-                $user->increment('credits', $paypalProduct->quantity);
+                $user->increment('credits', $creditProduct->quantity);
 
                 //update server limit
-                if (Configuration::getValueByKey('SERVER_LIMIT_AFTER_IRL_PURCHASE') !== 0) {
-                    if ($user->server_limit < Configuration::getValueByKey('SERVER_LIMIT_AFTER_IRL_PURCHASE')) {
-                        $user->update(['server_limit' => Configuration::getValueByKey('SERVER_LIMIT_AFTER_IRL_PURCHASE')]);
+                if (config('SETTINGS::USER:SERVER_LIMIT_AFTER_IRL_PURCHASE') !== 0) {
+                    if ($user->server_limit < config('SETTINGS::USER:SERVER_LIMIT_AFTER_IRL_PURCHASE')) {
+                        $user->update(['server_limit' => config('SETTINGS::USER:SERVER_LIMIT_AFTER_IRL_PURCHASE')]);
                     }
                 }
-                
+
                 //update role
                 if ($user->role == 'member') {
                     $user->update(['role' => 'client']);
@@ -174,34 +183,207 @@ class PaymentController extends Controller
                 $payment = Payment::create([
                     'user_id' => $user->id,
                     'payment_id' => $response->result->id,
-                    'payer_id' => $laravelRequest->input('PayerID'),
+                    'payment_method' => 'paypal',
                     'type' => 'Credits',
-                    'status' => $response->result->status,
-                    'amount' => $paypalProduct->quantity,
-                    'price' => $paypalProduct->price,
-                    'tax_value' => $paypalProduct->getTaxValue(),
-                    'tax_percent' => $paypalProduct->getTaxPercent(),
-                    'total_price' => $paypalProduct->getTotalPrice(),
-                    'currency_code' => $paypalProduct->currency_code,
-                    'payer' => json_encode($response->result->payer),
+                    'status' => 'paid',
+                    'amount' => $creditProduct->quantity,
+                    'price' => $creditProduct->price,
+                    'tax_value' => $creditProduct->getTaxValue(),
+                    'tax_percent' => $creditProduct->getTaxPercent(),
+                    'total_price' => $creditProduct->getTotalPrice(),
+                    'currency_code' => $creditProduct->currency_code,
+                    'credit_product_id' => $creditProduct->id,
                 ]);
 
-                //payment notification
-                $user->notify(new ConfirmPaymentNotification($payment));
 
                 event(new UserUpdateCreditsEvent($user));
 
+                //only create invoice if SETTINGS::INVOICE:ENABLED is true
+                if (config('SETTINGS::INVOICE:ENABLED') == 'true') {
+                    $this->createInvoice($user, $payment, 'paid', $creditProduct->currency_code);
+                }
+
+
                 //redirect back to home
-                return redirect()->route('home')->with('success', 'Your credit balance has been increased!');
+                return redirect()->route('home')->with('success', __('Your credit balance has been increased!'));
             }
 
+
             // If call returns body in response, you can get the deserialized version from the result attribute of the response
             if (env('APP_ENV') == 'local') {
                 dd($response);
             } else {
                 abort(500);
             }
+        } catch (HttpException $ex) {
+            if (env('APP_ENV') == 'local') {
+                echo $ex->statusCode;
+                dd($ex->getMessage());
+            } else {
+                abort(422);
+            }
+        }
+    }
+
+
+    /**
+     * @param Request $request
+     */
+    public function Cancel(Request $request)
+    {
+        return redirect()->route('store.index')->with('success', 'Payment was Canceled');
+    }
+
+    /**
+     * @param Request $request
+     * @param CreditProduct $creditProduct
+     * @return RedirectResponse
+     */
+    public function StripePay(Request $request, CreditProduct $creditProduct)
+    {
+        $stripeClient = $this->getStripeClient();
+
+
+        $request = $stripeClient->checkout->sessions->create([
+            'line_items' => [
+                [
+                    'price_data' => [
+                        'currency' => $creditProduct->currency_code,
+                        'product_data' => [
+                            'name' => $creditProduct->display,
+                            'description' => $creditProduct->description,
+                        ],
+                        'unit_amount_decimal' => round($creditProduct->price * 100, 2),
+                    ],
+                    'quantity' => 1,
+                ],
+                [
+                    'price_data' => [
+                        'currency' => $creditProduct->currency_code,
+                        'product_data' => [
+                            'name' => 'Product Tax',
+                            'description' => $creditProduct->getTaxPercent() . "%",
+                        ],
+                        'unit_amount_decimal' => round($creditProduct->getTaxValue(), 2) * 100,
+                    ],
+                    'quantity' => 1,
+                ]
+            ],
+
+            'mode' => 'payment',
+            "payment_method_types" => str_getcsv(config("SETTINGS::PAYMENTS:STRIPE:METHODS")),
+            'success_url' => route('payment.StripeSuccess',  ['product' => $creditProduct->id]) . '&session_id={CHECKOUT_SESSION_ID}',
+            'cancel_url' => route('payment.Cancel'),
+        ]);
+
+
+
+        return redirect($request->url, 303);
+    }
+
+    /**
+     * @param Request $request
+     */
+    public function StripeSuccess(Request $request)
+    {
+        /** @var CreditProduct $creditProduct */
+        $creditProduct = CreditProduct::findOrFail($request->input('product'));
+
+        /** @var User $user */
+        $user = Auth::user();
+
+        $stripeClient = $this->getStripeClient();
+
+        try {
+            //get stripe data
+            $paymentSession = $stripeClient->checkout->sessions->retrieve($request->input('session_id'));
+            $paymentIntent = $stripeClient->paymentIntents->retrieve($paymentSession->payment_intent);
+
+            //get DB entry of this payment ID if existing
+            $paymentDbEntry = Payment::where('payment_id', $paymentSession->payment_intent)->count();
+
+            // check if payment is 100% completed and payment does not exist in db already
+            if ($paymentSession->status == "complete" && $paymentIntent->status == "succeeded" && $paymentDbEntry == 0) {
+
+                //update credits
+                $user->increment('credits', $creditProduct->quantity);
+
+                //update server limit
+                if (config('SETTINGS::USER:SERVER_LIMIT_AFTER_IRL_PURCHASE') !== 0) {
+                    if ($user->server_limit < config('SETTINGS::USER:SERVER_LIMIT_AFTER_IRL_PURCHASE')) {
+                        $user->update(['server_limit' => config('SETTINGS::USER:SERVER_LIMIT_AFTER_IRL_PURCHASE')]);
+                    }
+                }
+
+                //update role
+                if ($user->role == 'member') {
+                    $user->update(['role' => 'client']);
+                }
+
+                //store paid payment
+                $payment = Payment::create([
+                    'user_id' => $user->id,
+                    'payment_id' => $paymentSession->payment_intent,
+                    'payment_method' => 'stripe',
+                    'type' => 'Credits',
+                    'status' => 'paid',
+                    'amount' => $creditProduct->quantity,
+                    'price' => $creditProduct->price,
+                    'tax_value' => $creditProduct->getTaxValue(),
+                    'total_price' => $creditProduct->getTotalPrice(),
+                    'tax_percent' => $creditProduct->getTaxPercent(),
+                    'currency_code' => $creditProduct->currency_code,
+                    'credit_product_id' => $creditProduct->id,
+                ]);
+
+                //payment notification
+                $user->notify(new ConfirmPaymentNotification($payment));
+
+                event(new UserUpdateCreditsEvent($user));
 
+                //only create invoice if SETTINGS::INVOICE:ENABLED is true
+                if (config('SETTINGS::INVOICE:ENABLED') == 'true') {
+                    $this->createInvoice($user, $payment, 'paid', $creditProduct->currency_code);
+                }
+
+                //redirect back to home
+                return redirect()->route('home')->with('success', __('Your credit balance has been increased!'));
+            } else {
+                if ($paymentIntent->status == "processing") {
+
+                    //store processing payment
+                    $payment = Payment::create([
+                        'user_id' => $user->id,
+                        'payment_id' => $paymentSession->payment_intent,
+                        'payment_method' => 'stripe',
+                        'type' => 'Credits',
+                        'status' => 'processing',
+                        'amount' => $creditProduct->quantity,
+                        'price' => $creditProduct->price,
+                        'tax_value' => $creditProduct->getTaxValue(),
+                        'total_price' => $creditProduct->getTotalPrice(),
+                        'tax_percent' => $creditProduct->getTaxPercent(),
+                        'currency_code' => $creditProduct->currency_code,
+                        'credit_product_id' => $creditProduct->id,
+                    ]);
+
+                    //only create invoice if SETTINGS::INVOICE:ENABLED is true
+                    if (config('SETTINGS::INVOICE:ENABLED') == 'true') {
+                        $this->createInvoice($user, $payment, 'paid', $creditProduct->currency_code);
+                    }
+
+                    //redirect back to home
+                    return redirect()->route('home')->with('success', __('Your payment is being processed!'));
+                }
+                if ($paymentDbEntry == 0 && $paymentIntent->status != "processing") {
+                    $stripeClient->paymentIntents->cancel($paymentIntent->id);
+
+                    //redirect back to home
+                    return redirect()->route('home')->with('success', __('Your payment has been canceled!'));
+                } else {
+                    abort(402);
+                }
+            }
         } catch (HttpException $ex) {
             if (env('APP_ENV') == 'local') {
                 echo $ex->statusCode;
@@ -209,21 +391,193 @@ class PaymentController extends Controller
             } else {
                 abort(422);
             }
+        }
+    }
+
+    /**
+     * @param Request $request
+     */
+    protected function handleStripePaymentSuccessHook($paymentIntent)
+    {
+        try {
+            // Get payment db entry
+            $payment = Payment::where('payment_id', $paymentIntent->id)->first();
+            $user = User::where('id', $payment->user_id)->first();
+
+            if ($paymentIntent->status == 'succeeded' && $payment->status == 'processing') {
+                // Increment User Credits
+                $user->increment('credits', $payment->amount);
+
+                //update server limit
+                if (config('SETTINGS::USER:SERVER_LIMIT_AFTER_IRL_PURCHASE') !== 0) {
+                    if ($user->server_limit < config('SETTINGS::USER:SERVER_LIMIT_AFTER_IRL_PURCHASE')) {
+                        $user->update(['server_limit' => config('SETTINGS::USER:SERVER_LIMIT_AFTER_IRL_PURCHASE')]);
+                    }
+                }
+
+                //update role
+                if ($user->role == 'member') {
+                    $user->update(['role' => 'client']);
+                }
+
+                //update payment db entry status
+                $payment->update(['status' => 'paid']);
+
+                //payment notification
+                $user->notify(new ConfirmPaymentNotification($payment));
+                event(new UserUpdateCreditsEvent($user));
 
+                //only create invoice if SETTINGS::INVOICE:ENABLED is true
+                if (config('SETTINGS::INVOICE:ENABLED') == 'true') {
+                    $this->createInvoice($user, $payment, 'paid', strtoupper($paymentIntent->currency));
+                }
+            }
+        } catch (HttpException $ex) {
+            abort(422);
         }
+    }
 
+    /**
+     * @param Request $request
+     */
+    public function StripeWebhooks(Request $request)
+    {
+        \Stripe\Stripe::setApiKey($this->getStripeSecret());
+
+        try {
+            $payload = @file_get_contents('php://input');
+            $sig_header = $request->header('Stripe-Signature');
+            $event = null;
+            $event = \Stripe\Webhook::constructEvent(
+                $payload,
+                $sig_header,
+                $this->getStripeEndpointSecret()
+            );
+        } catch (\UnexpectedValueException $e) {
+            // Invalid payload
+
+            abort(400);
+        } catch (\Stripe\Exception\SignatureVerificationException $e) {
+            // Invalid signature
+
+            abort(400);
+        }
+
+        // Handle the event
+        switch ($event->type) {
+            case 'payment_intent.succeeded':
+                $paymentIntent = $event->data->object; // contains a \Stripe\PaymentIntent
+                $this->handleStripePaymentSuccessHook($paymentIntent);
+                break;
+            default:
+                echo 'Received unknown event type ' . $event->type;
+        }
+    }
+
+    /**
+     * @return \Stripe\StripeClient
+     */
+    protected function getStripeClient()
+    {
+        return new \Stripe\StripeClient($this->getStripeSecret());
     }
 
+    /**
+     * @return string
+     */
+    protected function getStripeSecret()
+    {
+        return env('APP_ENV') == 'local'
+            ?  config("SETTINGS::PAYMENTS:STRIPE:TEST_SECRET")
+            :  config("SETTINGS::PAYMENTS:STRIPE:SECRET");
+    }
 
     /**
-     * @param Request $request
+     * @return string
      */
-    public function cancel(Request $request)
+    protected function getStripeEndpointSecret()
     {
-        return redirect()->route('store.index')->with('success', 'Payment was Canceled');
+        return env('APP_ENV') == 'local'
+            ?  config("SETTINGS::PAYMENTS:STRIPE:ENDPOINT_TEST_SECRET")
+            :  config("SETTINGS::PAYMENTS:STRIPE:ENDPOINT_SECRET");
     }
 
 
+    protected function createInvoice($user, $payment, $paymentStatus, $currencyCode)
+    {
+        $creditProduct = CreditProduct::where('id', $payment->credit_product_id)->first();
+        //create invoice
+        $lastInvoiceID = \App\Models\Invoice::where("invoice_name", "like", "%" . now()->format('mY') . "%")->count("id");
+        $newInvoiceID = $lastInvoiceID + 1;
+        $logoPath = storage_path('app/public/logo.png');
+
+        $seller = new Party([
+            'name' => config("SETTINGS::INVOICE:COMPANY_NAME"),
+            'phone' => config("SETTINGS::INVOICE:COMPANY_PHONE"),
+            'address' => config("SETTINGS::INVOICE:COMPANY_ADDRESS"),
+            'vat' => config("SETTINGS::INVOICE:COMPANY_VAT"),
+            'custom_fields' => [
+                'E-Mail' => config("SETTINGS::INVOICE:COMPANY_MAIL"),
+                "Web" => config("SETTINGS::INVOICE:COMPANY_WEBSITE")
+            ],
+        ]);
+
+
+        $customer = new Buyer([
+            'name' => $user->name,
+            'custom_fields' => [
+                'E-Mail' => $user->email,
+                'Client ID' => $user->id,
+            ],
+        ]);
+        $item = (new InvoiceItem())
+            ->title($creditProduct->description)
+            ->pricePerUnit($creditProduct->price);
+
+        $notes = [
+            __("Payment method") . ": " . $payment->payment_method,
+        ];
+        $notes = implode("<br>", $notes);
+
+
+        $invoice = Invoice::make()
+            ->template('controlpanel')
+            ->name(__("Invoice"))
+            ->buyer($customer)
+            ->seller($seller)
+            ->discountByPercent(0)
+            ->taxRate(floatval($creditProduct->getTaxPercent()))
+            ->shipping(0)
+            ->addItem($item)
+            ->status(__($paymentStatus))
+            ->series(now()->format('mY'))
+            ->delimiter("-")
+            ->sequence($newInvoiceID)
+            ->serialNumberFormat(config("SETTINGS::INVOICE:PREFIX") . '{DELIMITER}{SERIES}{SEQUENCE}')
+            ->currencyCode($currencyCode)
+            ->currencySymbol(Currencies::getSymbol($currencyCode))
+            ->notes($notes);
+
+        if (file_exists($logoPath)) {
+            $invoice->logo($logoPath);
+        }
+
+        //Save the invoice in "storage\app\invoice\USER_ID\YEAR"
+        $invoice->filename = $invoice->getSerialNumber() . '.pdf';
+        $invoice->render();
+        Storage::disk("local")->put("invoice/" . $user->id . "/" . now()->format('Y') . "/" . $invoice->filename, $invoice->output);
+
+
+        \App\Models\Invoice::create([
+            'invoice_user' => $user->id,
+            'invoice_name' => $invoice->getSerialNumber(),
+            'payment_id' => $payment->payment_id,
+        ]);
+
+        //Send Invoice per Mail
+        $user->notify(new InvoiceNotification($invoice, $user, $payment));
+    }
+
     /**
      * @return JsonResponse|mixed
      * @throws Exception
@@ -242,12 +596,21 @@ class PaymentController extends Controller
             ->editColumn('tax_value', function (Payment $payment) {
                 return $payment->formatToCurrency($payment->tax_value);
             })
+            ->editColumn('tax_percent', function (Payment $payment) {
+                return $payment->tax_percent . ' %';
+            })
             ->editColumn('total_price', function (Payment $payment) {
                 return $payment->formatToCurrency($payment->total_price);
             })
+
             ->editColumn('created_at', function (Payment $payment) {
                 return $payment->created_at ? $payment->created_at->diffForHumans() : '';
             })
-            ->make();
+            ->addColumn('actions', function (Payment $payment) {
+                return ' <a data-content="' . __("Download") . '" data-toggle="popover" data-trigger="hover" data-placement="top"  href="' . route('admin.invoices.downloadSingleInvoice', "id=" . $payment->payment_id) . '" class="btn btn-sm text-white btn-info mr-1"><i class="fas fa-file-download"></i></a>
+';
+            })
+            ->rawColumns(['actions'])
+            ->make(true);
     }
 }

+ 13 - 15
app/Http/Controllers/Admin/ProductController.php

@@ -3,12 +3,10 @@
 namespace App\Http\Controllers\Admin;
 
 use App\Http\Controllers\Controller;
-use App\Models\Egg;
 use App\Models\Location;
 use App\Models\Nest;
-use App\Models\Node;
-use App\Models\Configuration;
 use App\Models\Product;
+use App\Models\Settings;
 use Exception;
 use Illuminate\Contracts\Foundation\Application;
 use Illuminate\Contracts\View\Factory;
@@ -16,7 +14,6 @@ use Illuminate\Contracts\View\View;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
-use Illuminate\Http\Response;
 
 class ProductController extends Controller
 {
@@ -37,14 +34,15 @@ class ProductController extends Controller
      */
     public function create()
     {
-        return view('admin.products.create' , [
+        return view('admin.products.create', [
             'locations' => Location::with('nodes')->get(),
             'nests' => Nest::with('eggs')->get(),
         ]);
     }
 
-    public function clone(Request $request , Product $product){
-        return view('admin.products.create' , [
+    public function clone(Request $request, Product $product)
+    {
+        return view('admin.products.create', [
             'product' => $product,
             'locations' => Location::with('nodes')->get(),
             'nests' => Nest::with('eggs')->get(),
@@ -84,7 +82,7 @@ class ProductController extends Controller
         $product->eggs()->attach($request->input('eggs'));
         $product->nodes()->attach($request->input('nodes'));
 
-        return redirect()->route('admin.products.index')->with('success', 'Product has been created!');
+        return redirect()->route('admin.products.index')->with('success', __('Product has been created!'));
     }
 
     /**
@@ -97,7 +95,7 @@ class ProductController extends Controller
     {
         return view('admin.products.show', [
             'product' => $product,
-            'minimum_credits' => Configuration::getValueByKey("MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER"),
+            'minimum_credits' => config("SETTINGS::USER:MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER"),
         ]);
     }
 
@@ -152,7 +150,7 @@ class ProductController extends Controller
         $product->eggs()->attach($request->input('eggs'));
         $product->nodes()->attach($request->input('nodes'));
 
-        return redirect()->route('admin.products.index')->with('success', 'Product has been updated!');
+        return redirect()->route('admin.products.index')->with('success', __('Product has been updated!'));
     }
 
     /**
@@ -181,7 +179,7 @@ class ProductController extends Controller
         }
 
         $product->delete();
-        return redirect()->back()->with('success', 'Product has been removed!');
+        return redirect()->back()->with('success', __('Product has been removed!'));
     }
 
 
@@ -196,14 +194,14 @@ class ProductController extends Controller
         return datatables($query)
             ->addColumn('actions', function (Product $product) {
                 return '
-                            <a data-content="Show" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.products.show', $product->id) . '" class="btn btn-sm text-white btn-warning mr-1"><i class="fas fa-eye"></i></a>
-                            <a data-content="Clone" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.products.clone', $product->id) . '" class="btn btn-sm text-white btn-primary mr-1"><i class="fas fa-clone"></i></a>
-                            <a data-content="Edit" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.products.edit', $product->id) . '" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
+                            <a data-content="' . __("Show") . '" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.products.show', $product->id) . '" class="btn btn-sm text-white btn-warning mr-1"><i class="fas fa-eye"></i></a>
+                            <a data-content="' . __("Clone") . '" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.products.clone', $product->id) . '" class="btn btn-sm text-white btn-primary mr-1"><i class="fas fa-clone"></i></a>
+                            <a data-content="' . __("Edit") . '" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.products.edit', $product->id) . '" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
 
                            <form class="d-inline" onsubmit="return submitResult();" method="post" action="' . route('admin.products.destroy', $product->id) . '">
                             ' . csrf_field() . '
                             ' . method_field("DELETE") . '
-                           <button data-content="Delete" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
+                           <button data-content="' . __("Delete") . '" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
                        </form>
                 ';
             })

+ 10 - 8
app/Http/Controllers/Admin/ServerController.php

@@ -6,6 +6,7 @@ use App\Classes\Pterodactyl;
 use App\Classes\PterodactylWrapper;
 use App\Http\Controllers\Controller;
 use App\Models\Server;
+use App\Models\Settings;
 use Exception;
 use Illuminate\Contracts\Foundation\Application;
 use Illuminate\Contracts\View\Factory;
@@ -92,9 +93,9 @@ class ServerController extends Controller
     {
         try {
             $server->delete();
-            return redirect()->route('admin.servers.index')->with('success', 'Server removed');
+            return redirect()->route('admin.servers.index')->with('success', __('Server removed'));
         } catch (Exception $e) {
-            return redirect()->route('admin.servers.index')->with('error', 'An exception has occurred while trying to remove a resource "' . $e->getMessage() . '"');
+            return redirect()->route('admin.servers.index')->with('error', __('An exception has occurred while trying to remove a resource "') . $e->getMessage() . '"');
         }
     }
 
@@ -102,14 +103,15 @@ class ServerController extends Controller
      * @param Server $server
      * @return RedirectResponse
      */
-    public function toggleSuspended(Server $server){
+    public function toggleSuspended(Server $server)
+    {
         try {
             $server->isSuspended() ?  $server->unSuspend() :  $server->suspend();
         } catch (Exception $exception) {
             return redirect()->back()->with('error', $exception->getMessage());
         }
 
-        return redirect()->back()->with('success', 'Server has been updated!');
+        return redirect()->back()->with('success', __('Server has been updated!'));
     }
 
     /**
@@ -134,18 +136,18 @@ class ServerController extends Controller
             ->addColumn('actions', function (Server $server) {
                 $suspendColor = $server->isSuspended() ? "btn-success" : "btn-warning";
                 $suspendIcon = $server->isSuspended() ? "fa-play-circle" : "fa-pause-circle";
-                $suspendText = $server->isSuspended() ? "Unsuspend" : "Suspend";
+                $suspendText = $server->isSuspended() ? __("Unsuspend") : __("Suspend");
 
                 return '
                         <form class="d-inline" method="post" action="' . route('admin.servers.togglesuspend', $server->id) . '">
                             ' . csrf_field() . '
-                           <button data-content="'.$suspendText.'" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm '.$suspendColor.' text-white mr-1"><i class="far '.$suspendIcon.'"></i></button>
+                           <button data-content="' . $suspendText . '" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm ' . $suspendColor . ' text-white mr-1"><i class="far ' . $suspendIcon . '"></i></button>
                        </form>
 
                        <form class="d-inline" onsubmit="return submitResult();" method="post" action="' . route('admin.servers.destroy', $server->id) . '">
                             ' . csrf_field() . '
                             ' . method_field("DELETE") . '
-                           <button data-content="Delete" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
+                           <button data-content="' . __("Delete") . '" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
                        </form>
 
                 ';
@@ -161,7 +163,7 @@ class ServerController extends Controller
                 return $server->suspended ? $server->suspended->diffForHumans() : '';
             })
             ->editColumn('name', function (Server $server) {
-                return '<a class="text-info" target="_blank" href="' . env('PTERODACTYL_URL', 'http://localhost') . '/admin/servers/view/' . $server->pterodactyl_id . '">' . $server->name . '</a>';
+                return '<a class="text-info" target="_blank" href="' . config("SETTINGS::SYSTEM:PTERODACTYL:URL") . '/admin/servers/view/' . $server->pterodactyl_id . '">' . $server->name . '</a>';
             })
             ->rawColumns(['user', 'actions', 'status', 'name'])
             ->make();

+ 55 - 11
app/Http/Controllers/Admin/SettingsController.php

@@ -3,6 +3,7 @@
 namespace App\Http\Controllers\Admin;
 
 use App\Http\Controllers\Controller;
+use App\Models\Settings;
 use Illuminate\Contracts\Foundation\Application;
 use Illuminate\Contracts\View\Factory;
 use Illuminate\Contracts\View\View;
@@ -18,25 +19,68 @@ class SettingsController extends Controller
      */
     public function index()
     {
-        return view('admin.settings.index');
+        //Get all tabs as laravel view paths
+        $tabs = [];
+        foreach (glob(resource_path('views/admin/settings/tabs/*.blade.php')) as $filename) {
+            $tabs[] = 'admin.settings.tabs.' . basename($filename, '.blade.php');
+        }
+
+        //Generate a html list item for each tab based on tabs file basename, set first tab as active
+        $tabListItems = [];
+        foreach ($tabs as $tab) {
+            $tabName = str_replace('admin.settings.tabs.', '', $tab);
+            $tabListItems[] = '<li class="nav-item">
+            <a class="nav-link ' . (empty($tabListItems) ? 'active' : '') . '" data-toggle="pill" href="#' . $tabName . '">
+            ' . __(ucfirst($tabName)) . '
+            </a></li>';
+        }
+
+        return view('admin.settings.index', [
+            'tabs' => $tabs,
+            'tabListItems' => $tabListItems,
+        ]);
     }
 
-    public function updateIcons(Request $request)
+
+    public function updatevalue(Request $request)
     {
+        $setting = Settings::findOrFail($request->input('key'));
+
         $request->validate([
-            'icon' => 'nullable|max:10000|mimes:jpg,png,jpeg',
-            'favicon' => 'nullable|max:10000|mimes:ico',
+            'key'   => 'required|string|max:191',
+            'value' => 'required|string|max:191',
         ]);
 
-        if ($request->hasFile('icon')) {
-            $request->file('icon')->storeAs('public', 'icon.png');
-        }
+        $setting->update($request->all());
 
-        if ($request->hasFile('favicon')) {
-            $request->file('favicon')->storeAs('public', 'favicon.ico');
-        }
+        return redirect()->route('admin.settings.index')->with('success', __('configuration has been updated!'));
+    }
 
-        return redirect()->route('admin.settings.index')->with('success', 'Icons updated!');
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param Settings $setting
+     * @return Response
+     */
+    public function destroy(Settings $setting)
+    {
+        //
     }
 
+    public function datatable()
+    {
+        $query = Settings::where('key', 'like', '%SYSTEM%')
+            ->orWhere('key', 'like', '%USER%')
+            ->orWhere('key', 'like', '%SERVER%');
+
+        return datatables($query)
+            ->addColumn('actions', function (Settings $setting) {
+                return '<button data-content="' . __("Edit") . '" data-toggle="popover" data-trigger="hover" data-placement="top" onclick="configuration.parse(\'' . $setting->key . '\',\'' . $setting->value . '\',\'' . $setting->type . '\')" data-content="Edit" data-trigger="hover" data-toggle="tooltip" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></button> ';
+            })
+            ->editColumn('created_at', function (Settings $setting) {
+                return $setting->created_at ? $setting->created_at->diffForHumans() : '';
+            })
+            ->rawColumns(['actions'])
+            ->make();
+    }
 }

+ 5 - 5
app/Http/Controllers/Admin/UsefulLinkController.php

@@ -50,7 +50,7 @@ class UsefulLinkController extends Controller
         ]);
 
         UsefulLink::create($request->all());
-        return redirect()->route('admin.usefullinks.index')->with('success', 'link has been created!');
+        return redirect()->route('admin.usefullinks.index')->with('success', __('link has been created!'));
     }
 
     /**
@@ -94,7 +94,7 @@ class UsefulLinkController extends Controller
         ]);
 
         $usefullink->update($request->all());
-        return redirect()->route('admin.usefullinks.index')->with('success', 'link has been updated!');
+        return redirect()->route('admin.usefullinks.index')->with('success', __('link has been updated!'));
     }
 
     /**
@@ -106,7 +106,7 @@ class UsefulLinkController extends Controller
     public function destroy(UsefulLink $usefullink)
     {
         $usefullink->delete();
-        return redirect()->back()->with('success', 'product has been removed!');
+        return redirect()->back()->with('success', __('product has been removed!'));
     }
 
     public function dataTable()
@@ -116,12 +116,12 @@ class UsefulLinkController extends Controller
         return datatables($query)
             ->addColumn('actions', function (UsefulLink $link) {
                 return '
-                            <a data-content="Edit" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.usefullinks.edit', $link->id) . '" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
+                            <a data-content="'.__("Edit").'" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.usefullinks.edit', $link->id) . '" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
 
                            <form class="d-inline" onsubmit="return submitResult();" method="post" action="' . route('admin.usefullinks.destroy', $link->id) . '">
                             ' . csrf_field() . '
                             ' . method_field("DELETE") . '
-                           <button data-content="Delete" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
+                           <button data-content="'.__("Delete").'" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
                        </form>
                 ';
             })

+ 14 - 12
app/Http/Controllers/Admin/UserController.php

@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin;
 use App\Classes\Pterodactyl;
 use App\Events\UserUpdateCreditsEvent;
 use App\Http\Controllers\Controller;
+use App\Models\Settings;
 use App\Models\User;
 use App\Notifications\DynamicNotification;
 use Spatie\QueryBuilder\QueryBuilder;
@@ -115,7 +116,7 @@ class UserController extends Controller
 
         if (isset($this->pterodactyl->getUser($request->input('pterodactyl_id'))['errors'])) {
             throw ValidationException::withMessages([
-                'pterodactyl_id' => ["User does not exists on pterodactyl's panel"]
+                'pterodactyl_id' => [__("User does not exists on pterodactyl's panel")]
             ]);
         }
 
@@ -145,7 +146,7 @@ class UserController extends Controller
     public function destroy(User $user)
     {
         $user->delete();
-        return redirect()->back()->with('success', 'user has been removed!');
+        return redirect()->back()->with('success', __('user has been removed!'));
     }
 
     /**
@@ -218,21 +219,22 @@ class UserController extends Controller
         $all = $data["all"] ?? false;
         $users = $all ? User::all() : User::whereIn("id", $data["users"])->get();
         Notification::send($users, new DynamicNotification($data["via"], $database, $mail));
-        return redirect()->route('admin.users.notifications')->with('success', 'Notification sent!');
+        return redirect()->route('admin.users.notifications')->with('success', __('Notification sent!'));
     }
 
     /**
      * @param User $user
      * @return RedirectResponse
      */
-    public function toggleSuspended(User $user){
+    public function toggleSuspended(User $user)
+    {
         try {
             !$user->isSuspended() ? $user->suspend() : $user->unSuspend();
         } catch (Exception $exception) {
             return redirect()->back()->with('error', $exception->getMessage());
         }
 
-        return redirect()->back()->with('success', 'User has been updated!');
+        return redirect()->back()->with('success', __('User has been updated!'));
     }
 
     /**
@@ -265,19 +267,19 @@ class UserController extends Controller
             ->addColumn('actions', function (User $user) {
                 $suspendColor = $user->isSuspended() ? "btn-success" : "btn-warning";
                 $suspendIcon = $user->isSuspended() ? "fa-play-circle" : "fa-pause-circle";
-                $suspendText = $user->isSuspended() ? "Unsuspend" : "Suspend";
+                $suspendText = $user->isSuspended() ? __("Unsuspend") : __("Suspend");
                 return '
-                <a data-content="Login as user" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.users.loginas', $user->id) . '" class="btn btn-sm btn-primary mr-1"><i class="fas fa-sign-in-alt"></i></a>
-                <a data-content="Show" data-toggle="popover" data-trigger="hover" data-placement="top"  href="' . route('admin.users.show', $user->id) . '" class="btn btn-sm text-white btn-warning mr-1"><i class="fas fa-eye"></i></a>
-                <a data-content="Edit" data-toggle="popover" data-trigger="hover" data-placement="top"  href="' . route('admin.users.edit', $user->id) . '" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
+                <a data-content="' . __("Login as User") . '" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.users.loginas', $user->id) . '" class="btn btn-sm btn-primary mr-1"><i class="fas fa-sign-in-alt"></i></a>
+                <a data-content="' . __("Show") . '" data-toggle="popover" data-trigger="hover" data-placement="top"  href="' . route('admin.users.show', $user->id) . '" class="btn btn-sm text-white btn-warning mr-1"><i class="fas fa-eye"></i></a>
+                <a data-content="' . __("Edit") . '" data-toggle="popover" data-trigger="hover" data-placement="top"  href="' . route('admin.users.edit', $user->id) . '" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
                <form class="d-inline" method="post" action="' . route('admin.users.togglesuspend', $user->id) . '">
                             ' . csrf_field() . '
-                           <button data-content="'.$suspendText.'" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm '.$suspendColor.' text-white mr-1"><i class="far '.$suspendIcon.'"></i></button>
+                           <button data-content="' . $suspendText . '" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm ' . $suspendColor . ' text-white mr-1"><i class="far ' . $suspendIcon . '"></i></button>
                        </form>
                 <form class="d-inline" onsubmit="return submitResult();" method="post" action="' . route('admin.users.destroy', $user->id) . '">
                             ' . csrf_field() . '
                             ' . method_field("DELETE") . '
-                           <button data-content="Delete" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
+                           <button data-content="' . __("Delete") . '" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
                        </form>
                 ';
             })
@@ -300,7 +302,7 @@ class UserController extends Controller
                 return '<span class="badge ' . $badgeColor . '">' . $user->role . '</span>';
             })
             ->editColumn('name', function (User $user) {
-                return '<a class="text-info" target="_blank" href="' . env('PTERODACTYL_URL', 'http://localhost') . '/admin/users/view/' . $user->pterodactyl_id . '">' . $user->name . '</a>';
+                return '<a class="text-info" target="_blank" href="' . config("SETTINGS::SYSTEM:PTERODACTYL:URL") . '/admin/users/view/' . $user->pterodactyl_id . '">' . $user->name . '</a>';
             })
             ->orderColumn('last_seen', function ($query, $order) {
                 $query->orderBy('last_seen', $order);

+ 12 - 12
app/Http/Controllers/Admin/VoucherController.php

@@ -55,7 +55,7 @@ class VoucherController extends Controller
 
         Voucher::create($request->except('_token'));
 
-        return redirect()->route('admin.vouchers.index')->with('success', 'voucher has been created!');
+        return redirect()->route('admin.vouchers.index')->with('success', __('voucher has been created!'));
     }
 
     /**
@@ -101,7 +101,7 @@ class VoucherController extends Controller
 
         $voucher->update($request->except('_token'));
 
-        return redirect()->route('admin.vouchers.index')->with('success', 'voucher has been updated!');
+        return redirect()->route('admin.vouchers.index')->with('success', __('voucher has been updated!'));
     }
 
     /**
@@ -113,7 +113,7 @@ class VoucherController extends Controller
     public function destroy(Voucher $voucher)
     {
         $voucher->delete();
-        return redirect()->back()->with('success', 'voucher has been removed!');
+        return redirect()->back()->with('success', __('voucher has been removed!'));
     }
 
     public function users(Voucher $voucher)
@@ -140,19 +140,19 @@ class VoucherController extends Controller
 
         #extra validations
         if ($voucher->getStatus() == 'USES_LIMIT_REACHED') throw ValidationException::withMessages([
-            'code' => 'This voucher has reached the maximum amount of uses'
+            'code' => __('This voucher has reached the maximum amount of uses')
         ]);
 
         if ($voucher->getStatus() == 'EXPIRED') throw ValidationException::withMessages([
-            'code' => 'This voucher has expired'
+            'code' => __('This voucher has expired')
         ]);
 
         if (!$request->user()->vouchers()->where('id', '=', $voucher->id)->get()->isEmpty()) throw ValidationException::withMessages([
-            'code' => 'You already redeemed this voucher code'
+            'code' => __('You already redeemed this voucher code')
         ]);
 
         if ($request->user()->credits + $voucher->credits >= 99999999) throw ValidationException::withMessages([
-            'code' => "You can't redeem this voucher because you would exceed the " . CREDITS_DISPLAY_NAME . " limit"
+            'code' => "You can't redeem this voucher because you would exceed the  limit of " . CREDITS_DISPLAY_NAME
         ]);
 
         #redeem voucher
@@ -161,7 +161,7 @@ class VoucherController extends Controller
         event(new UserUpdateCreditsEvent($request->user()));
 
         return response()->json([
-            'success' => "{$voucher->credits} " . CREDITS_DISPLAY_NAME . " have been added to your balance!"
+            'success' => "{$voucher->credits} " . CREDITS_DISPLAY_NAME ." ". __("have been added to your balance!")
         ]);
     }
 
@@ -189,19 +189,19 @@ class VoucherController extends Controller
         return datatables($query)
             ->addColumn('actions', function (Voucher $voucher) {
                 return '
-                            <a data-content="Users" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.vouchers.users', $voucher->id) . '" class="btn btn-sm btn-primary mr-1"><i class="fas fa-users"></i></a>
-                            <a data-content="Edit" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.vouchers.edit', $voucher->id) . '" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
+                            <a data-content="'.__("Users").'" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.vouchers.users', $voucher->id) . '" class="btn btn-sm btn-primary mr-1"><i class="fas fa-users"></i></a>
+                            <a data-content="'.__("Edit").'" data-toggle="popover" data-trigger="hover" data-placement="top" href="' . route('admin.vouchers.edit', $voucher->id) . '" class="btn btn-sm btn-info mr-1"><i class="fas fa-pen"></i></a>
 
                            <form class="d-inline" onsubmit="return submitResult();" method="post" action="' . route('admin.vouchers.destroy', $voucher->id) . '">
                             ' . csrf_field() . '
                             ' . method_field("DELETE") . '
-                           <button data-content="Delete" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
+                           <button data-content="'.__("Delete").'" data-toggle="popover" data-trigger="hover" data-placement="top" class="btn btn-sm btn-danger mr-1"><i class="fas fa-trash"></i></button>
                        </form>
                 ';
             })
             ->addColumn('status', function (Voucher $voucher) {
                 $color = 'success';
-                if ($voucher->getStatus() != 'VALID') $color = 'danger';
+                if ($voucher->getStatus() != __('VALID')) $color = 'danger';
                 return '<span class="badge badge-' . $color . '">' . $voucher->getStatus() . '</span>';
             })
             ->editColumn('uses', function (Voucher $voucher) {

+ 69 - 5
app/Http/Controllers/Api/UserController.php

@@ -5,8 +5,8 @@ namespace App\Http\Controllers\Api;
 use App\Classes\Pterodactyl;
 use App\Events\UserUpdateCreditsEvent;
 use App\Http\Controllers\Controller;
-use App\Models\Configuration;
 use App\Models\DiscordUser;
+use App\Models\Settings;
 use App\Models\User;
 use Illuminate\Contracts\Foundation\Application;
 use Illuminate\Contracts\Pagination\LengthAwarePaginator;
@@ -88,10 +88,25 @@ class UserController extends Controller
             "role" => ['sometimes', Rule::in(['admin', 'mod', 'client', 'member'])],
         ]);
 
-        $user->update($request->all());
-
         event(new UserUpdateCreditsEvent($user));
 
+        //Update Users Password on Pterodactyl
+        //Username,Mail,First and Lastname are required aswell
+        $response = Pterodactyl::client()->patch('/application/users/' . $user->pterodactyl_id, [
+            "username" => $request->name,
+            "first_name" => $request->name,
+            "last_name" => $request->name,
+            "email" => $request->email,
+
+        ]);
+        if ($response->failed()) {
+            throw ValidationException::withMessages([
+                'pterodactyl_error_message' => $response->toException()->getMessage(),
+                'pterodactyl_error_status' => $response->toException()->getCode()
+            ]);
+        }
+        $user->update($request->all());
+
         return $user;
     }
 
@@ -166,6 +181,53 @@ class UserController extends Controller
         return $user;
     }
 
+    /**
+     * Suspends the user
+     *
+     * @param Request $request
+     * @param int $id
+     * @return bool
+     * @throws ValidationException
+     */
+    public function suspend(Request $request, int $id)
+    {
+        $discordUser = DiscordUser::find($id);
+        $user = $discordUser ? $discordUser->user : User::findOrFail($id);
+
+        if ($user->isSuspended()) {
+            throw ValidationException::withMessages([
+                'error' => 'The user is already suspended',
+            ]);
+        }
+        $user->suspend();
+
+        return $user;
+    }
+
+    /**
+     * Unsuspend the user
+     *
+     * @param Request $request
+     * @param int $id
+     * @return bool
+     * @throws ValidationException
+     */
+    public function unsuspend(Request $request, int $id)
+    {
+        $discordUser = DiscordUser::find($id);
+        $user = $discordUser ? $discordUser->user : User::findOrFail($id);
+
+        if (!$user->isSuspended()) {
+            throw ValidationException::withMessages([
+                'error' => "You cannot unsuspend an User who is not suspended."
+            ]);
+        }
+
+        $user->unSuspend();
+
+        return $user;
+    }
+
     /**
      * @throws ValidationException
      */
@@ -180,8 +242,8 @@ class UserController extends Controller
         $user = User::create([
             'name' => $request->input('name'),
             'email' => $request->input('email'),
-            'credits' => Configuration::getValueByKey('INITIAL_CREDITS', 150),
-            'server_limit' => Configuration::getValueByKey('INITIAL_SERVER_LIMIT', 1),
+            'credits' => config('SETTINGS::USER:INITIAL_CREDITS', 150),
+            'server_limit' => config('SETTINGS::USER:INITIAL_SERVER_LIMIT', 1),
             'password' => Hash::make($request->input('password')),
         ]);
 
@@ -208,6 +270,8 @@ class UserController extends Controller
             'pterodactyl_id' => $response->json()['attributes']['id']
         ]);
 
+        $user->sendEmailVerificationNotification();
+
         return $user;
     }
 

+ 13 - 5
app/Http/Controllers/Auth/LoginController.php

@@ -41,17 +41,25 @@ class LoginController extends Controller
 
     public function login(Request $request)
     {
-        $request->validate([
+
+        $validationRules = [
             $this->username()      => 'required|string',
             'password'             => 'required|string',
-            'g-recaptcha-response' => ['required','recaptcha'],
-        ]);
+        ];
+        if (config('SETTINGS::RECAPTCHA:ENABLED') == 'true') {
+            $validationRules['g-recaptcha-response'] = ['required', 'recaptcha'];
+        }
+        $request->validate($validationRules);
+
+
 
         // If the class is using the ThrottlesLogins trait, we can automatically throttle
         // the login attempts for this application. We'll key this by the username and
         // the IP address of the client making these requests into this application.
-        if (method_exists($this, 'hasTooManyLoginAttempts') &&
-            $this->hasTooManyLoginAttempts($request)) {
+        if (
+            method_exists($this, 'hasTooManyLoginAttempts') &&
+            $this->hasTooManyLoginAttempts($request)
+        ) {
             $this->fireLockoutEvent($request);
 
             return $this->sendLockoutResponse($request);

+ 23 - 20
app/Http/Controllers/Auth/RegisterController.php

@@ -4,7 +4,7 @@ namespace App\Http\Controllers\Auth;
 
 use App\Classes\Pterodactyl;
 use App\Http\Controllers\Controller;
-use App\Models\Configuration;
+use App\Models\Settings;
 use App\Models\User;
 use App\Providers\RouteServiceProvider;
 use Illuminate\Foundation\Auth\RegistersUsers;
@@ -12,6 +12,7 @@ use Illuminate\Support\Facades\App;
 use Illuminate\Support\Facades\Hash;
 use Illuminate\Support\Facades\Validator;
 use Illuminate\Support\Str;
+use Illuminate\Validation\ValidationException;
 
 class RegisterController extends Controller
 {
@@ -53,30 +54,28 @@ class RegisterController extends Controller
      */
     protected function validator(array $data)
     {
-        if (Configuration::getValueByKey('REGISTER_IP_CHECK', 'true') == 'true') {
+        $validationRules = [
+            'name'                 => ['required', 'string', 'max:30', 'min:4', 'alpha_num', 'unique:users'],
+            'email'                => ['required', 'string', 'email', 'max:64', 'unique:users'],
+            'password'             => ['required', 'string', 'min:8', 'confirmed'],
+        ];
+        if (config('SETTINGS::RECAPTCHA:ENABLED') == 'true') {
+            $validationRules['g-recaptcha-response'] = ['required', 'recaptcha'];
+        }
+
+        if (config('SETTINGS::SYSTEM:REGISTER_IP_CHECK', 'true') == 'true') {
 
             //check if ip has already made an account
             $data['ip'] = session()->get('ip') ?? request()->ip();
             if (User::where('ip', '=', request()->ip())->exists()) session()->put('ip', request()->ip());
+            $validationRules['ip']  = ['unique:users'];
+            return Validator::make($data, $validationRules, [
+                'ip.unique' => "You have already made an account! Please contact support if you think this is incorrect."
 
-            return Validator::make($data, [
-                'name'                 => ['required', 'string', 'max:30', 'min:4', 'alpha_num', 'unique:users'],
-                'email'                => ['required', 'string', 'email', 'max:64', 'unique:users'],
-                'password'             => ['required', 'string', 'min:8', 'confirmed'],
-                'g-recaptcha-response' => ['recaptcha'],
-                'ip'                   => ['unique:users'],
-            ], [
-                'ip.unique' => "You have already made an account with us! Please contact support if you think this is incorrect."
             ]);
         }
 
-        return Validator::make($data, [
-            'name'                 => ['required', 'string', 'max:30', 'min:4', 'alpha_num', 'unique:users'],
-            'email'                => ['required', 'string', 'email', 'max:64', 'unique:users'],
-            'password'             => ['required', 'string', 'min:8', 'confirmed'],
-            'g-recaptcha-response' => ['recaptcha'],
-        ]);
-
+        return Validator::make($data, $validationRules);
     }
 
     /**
@@ -90,8 +89,8 @@ class RegisterController extends Controller
         $user = User::create([
             'name'         => $data['name'],
             'email'        => $data['email'],
-            'credits'      => Configuration::getValueByKey('INITIAL_CREDITS', 150),
-            'server_limit' => Configuration::getValueByKey('INITIAL_SERVER_LIMIT', 1),
+            'credits'      => config('SETTINGS::USER:INITIAL_CREDITS', 150),
+            'server_limit' => config('SETTINGS::USER:INITIAL_SERVER_LIMIT', 1),
             'password'     => Hash::make($data['password']),
         ]);
 
@@ -108,13 +107,17 @@ class RegisterController extends Controller
 
         if ($response->failed()) {
             $user->delete();
-            return $user;
+            throw ValidationException::withMessages([
+                'ptero_registration_error' => [__('Account already exists on Pterodactyl. Please contact the Support!')],
+            ]);
         }
 
         $user->update([
             'pterodactyl_id' => $response->json()['attributes']['id']
         ]);
 
+
+
         return $user;
     }
 }

+ 16 - 12
app/Http/Controllers/Auth/SocialiteController.php

@@ -3,8 +3,8 @@
 namespace App\Http\Controllers\Auth;
 
 use App\Http\Controllers\Controller;
-use App\Models\Configuration;
 use App\Models\DiscordUser;
+use App\Models\Settings;
 use App\Models\User;
 use App\Models\Voucher;
 use Illuminate\Support\Facades\Auth;
@@ -15,7 +15,7 @@ class SocialiteController extends Controller
 {
     public function redirect()
     {
-        $scopes = !empty(env('DISCORD_BOT_TOKEN')) && !empty(env('DISCORD_GUILD_ID')) ? ['guilds.join'] : [];
+        $scopes = !empty(config("SETTINGS::DISCORD:BOT_TOKEN")) && !empty(config("SETTINGS::DISCORD:GUILD_ID")) ? ['guilds.join'] : [];
 
         return Socialite::driver('discord')
             ->scopes($scopes)
@@ -31,17 +31,17 @@ class SocialiteController extends Controller
         /** @var User $user */
         $user = Auth::user();
         $discord = Socialite::driver('discord')->user();
-        $botToken = env('DISCORD_BOT_TOKEN');
-        $guildId = env('DISCORD_GUILD_ID');
-        $roleId = env('DISCORD_ROLE_ID');
+        $botToken = config("SETTINGS::DISCORD:BOT_TOKEN");
+        $guildId = config("SETTINGS::DISCORD:GUILD_ID");
+        $roleId = config("SETTINGS::DISCORD:ROLE_ID");
 
         //save / update discord_users
         if (is_null($user->discordUser)) {
             //create discord user in db
             DiscordUser::create(array_merge($discord->user, ['user_id' => Auth::user()->id]));
             //update user
-            Auth::user()->increment('credits', Configuration::getValueByKey('CREDITS_REWARD_AFTER_VERIFY_DISCORD'));
-            Auth::user()->increment('server_limit', Configuration::getValueByKey('SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD'));
+            Auth::user()->increment('credits', config('SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_DISCORD'));
+            Auth::user()->increment('server_limit', config('SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD'));
             Auth::user()->update(['discord_verified_at' => now()]);
         } else {
             $user->discordUser->update($discord->user);
@@ -55,18 +55,22 @@ class SocialiteController extends Controller
                     'Authorization' => 'Bot ' . $botToken,
                     'Content-Type' => 'application/json',
                 ]
-            )->put("https://discord.com/api/guilds/{$guildId}/members/{$discord->id}",
-                ['access_token' => $discord->token]);
+            )->put(
+                "https://discord.com/api/guilds/{$guildId}/members/{$discord->id}",
+                ['access_token' => $discord->token]
+            );
 
             //give user a role in the discord server
-            if (!empty($roleId)){
+            if (!empty($roleId)) {
                 $response = Http::withHeaders(
                     [
                         'Authorization' => 'Bot ' . $botToken,
                         'Content-Type' => 'application/json',
                     ]
-                )->put("https://discord.com/api/guilds/{$guildId}/members/{$discord->id}/roles/{$roleId}",
-                    ['access_token' => $discord->token]);
+                )->put(
+                    "https://discord.com/api/guilds/{$guildId}/members/{$discord->id}/roles/{$roleId}",
+                    ['access_token' => $discord->token]
+                );
             }
         }
 

+ 5 - 11
app/Http/Controllers/HomeController.php

@@ -2,10 +2,7 @@
 
 namespace App\Http\Controllers;
 
-use App\Models\Egg;
-use App\Models\Product;
 use App\Models\UsefulLink;
-use App\Models\Configuration;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
 
@@ -15,7 +12,6 @@ class HomeController extends Controller
     const TIME_LEFT_BG_SUCCESS = "bg-success";
     const TIME_LEFT_BG_WARNING = "bg-warning";
     const TIME_LEFT_BG_DANGER = "bg-danger";
-    const TIME_LEFT_OUT_OF_CREDITS_TEXT = "You ran out of Credits";
 
     public function __construct()
     {
@@ -51,8 +47,8 @@ class HomeController extends Controller
      */
     public function getTimeLeftBoxUnit(float $daysLeft, float $hoursLeft)
     {
-        if ($daysLeft > 1) return 'days';
-        return $hoursLeft < 1 ? null : "hours";
+        if ($daysLeft > 1) return __('days');
+        return $hoursLeft < 1 ? null : __("hours");
     }
 
     /**
@@ -66,7 +62,7 @@ class HomeController extends Controller
     public function getTimeLeftBoxText(float $daysLeft, float $hoursLeft)
     {
         if ($daysLeft > 1) return strval(number_format($daysLeft, 0));
-        return ($hoursLeft < 1 ? $this::TIME_LEFT_OUT_OF_CREDITS_TEXT : strval($hoursLeft));
+        return ($hoursLeft < 1 ? __("You ran out of Credits") : strval($hoursLeft));
     }
 
     /** Show the application dashboard. */
@@ -85,13 +81,13 @@ class HomeController extends Controller
 
             $bg = $this->getTimeLeftBoxBackground($daysLeft);
             $boxText = $this->getTimeLeftBoxText($daysLeft, $hoursLeft);
-            $unit = $daysLeft < 1 ? ($hoursLeft < 1 ? null : "hours") : "days";
+            $unit = $daysLeft < 1 ? ($hoursLeft < 1 ? null : __("hours")) : __("days");
         }
 
 
         // RETURN ALL VALUES
         return view('home')->with([
-            'useage' => $usage,
+            'usage' => $usage,
             'credits' => $credits,
             'useful_links' => UsefulLink::all()->sortBy('id'),
             'bg' => $bg,
@@ -99,6 +95,4 @@ class HomeController extends Controller
             'unit' => $unit
         ]);
     }
-
 }
-

+ 46 - 14
app/Http/Controllers/ProfileController.php

@@ -2,15 +2,14 @@
 
 namespace App\Http\Controllers;
 
-use App\Models\Configuration;
+
+use App\Classes\Pterodactyl;
 use App\Models\User;
-use Illuminate\Contracts\View\Factory;
-use Illuminate\Contracts\View\View;
 use Illuminate\Http\RedirectResponse;
 use Illuminate\Http\Request;
-use Illuminate\Http\Response;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Hash;
+use Illuminate\Validation\ValidationException;
 
 class ProfileController extends Controller
 {
@@ -19,9 +18,9 @@ class ProfileController extends Controller
     {
         return view('profile.index')->with([
             'user' => Auth::user(),
-            'credits_reward_after_verify_discord' => Configuration::getValueByKey('CREDITS_REWARD_AFTER_VERIFY_DISCORD'),
-            'force_email_verification' => Configuration::getValueByKey('FORCE_EMAIL_VERIFICATION'),
-            'force_discord_verification' => Configuration::getValueByKey('FORCE_DISCORD_VERIFICATION'),
+            'credits_reward_after_verify_discord' => config('SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_DISCORD'),
+            'force_email_verification' => config('SETTINGS::USER:FORCE_EMAIL_VERIFICATION'),
+            'force_discord_verification' => config('SETTINGS::USER:FORCE_DISCORD_VERIFICATION'),
         ]);
     }
 
@@ -37,15 +36,15 @@ class ProfileController extends Controller
         $user = User::findOrFail($id);
 
         //update password if necessary
-        if (!is_null($request->input('new_password'))){
+        if (!is_null($request->input('new_password'))) {
 
             //validate password request
             $request->validate([
                 'current_password' => [
-                    'required' ,
+                    'required',
                     function ($attribute, $value, $fail) use ($user) {
                         if (!Hash::check($value, $user->password)) {
-                            $fail('The '.$attribute.' is invalid.');
+                            $fail('The ' . $attribute . ' is invalid.');
                         }
                     },
                 ],
@@ -53,21 +52,38 @@ class ProfileController extends Controller
                 'new_password_confirmation' => 'required|same:new_password'
             ]);
 
+            //Update Users Password on Pterodactyl
+            //Username,Mail,First and Lastname are required aswell
+            $response = Pterodactyl::client()->patch('/application/users/'.$user->pterodactyl_id, [
+                "password" => $request->input('new_password'),
+                "username" => $request->input('name'),
+                "first_name" => $request->input('name'),
+                "last_name" => $request->input('name'),
+                "email" => $request->input('email'),
+
+            ]);
+            if ($response->failed()) {
+                throw ValidationException::withMessages([
+                    'pterodactyl_error_message' => $response->toException()->getMessage(),
+                    'pterodactyl_error_status' => $response->toException()->getCode()
+                ]);
+            }
             //update password
             $user->update([
                 'password' => Hash::make($request->input('new_password')),
             ]);
+
         }
 
         //validate request
         $request->validate([
-            'name' => 'required|min:4|max:30|alpha_num|unique:users,name,'.$id.',id',
-            'email' => 'required|email|max:64|unique:users,email,'.$id.',id',
+            'name' => 'required|min:4|max:30|alpha_num|unique:users,name,' . $id . ',id',
+            'email' => 'required|email|max:64|unique:users,email,' . $id . ',id',
             'avatar' => 'nullable'
         ]);
 
         //update avatar
-        if(!is_null($request->input('avatar'))){
+        if (!is_null($request->input('avatar'))) {
             $avatar = json_decode($request->input('avatar'));
             if ($avatar->input->size > 3000000) abort(500);
 
@@ -80,12 +96,28 @@ class ProfileController extends Controller
             ]);
         }
 
+        //update name and email on Pterodactyl
+        $response = Pterodactyl::client()->patch('/application/users/'.$user->pterodactyl_id, [
+            "username" => $request->input('name'),
+            "first_name" => $request->input('name'),
+            "last_name" => $request->input('name'),
+            "email" => $request->input('email'),
+        ]);
+
+        if ($response->failed()) {
+            throw ValidationException::withMessages([
+                'pterodactyl_error_message' => $response->toException()->getMessage(),
+                'pterodactyl_error_status' => $response->toException()->getCode()
+            ]);
+        }
+
         //update name and email
         $user->update([
             'name' => $request->input('name'),
             'email' => $request->input('email'),
         ]);
+        $user->sendEmailVerificationNotification();
 
-        return redirect()->route('profile.index')->with('success' , 'Profile updated');
+        return redirect()->route('profile.index')->with('success', __('Profile updated'));
     }
 }

+ 43 - 15
app/Http/Controllers/ServerController.php

@@ -3,13 +3,13 @@
 namespace App\Http\Controllers;
 
 use App\Classes\Pterodactyl;
-use App\Models\Configuration;
 use App\Models\Egg;
 use App\Models\Location;
 use App\Models\Nest;
 use App\Models\Node;
 use App\Models\Product;
 use App\Models\Server;
+use App\Models\Settings;
 use App\Notifications\ServerCreationError;
 use Exception;
 use Illuminate\Database\Eloquent\Builder;
@@ -24,8 +24,35 @@ class ServerController extends Controller
     /** Display a listing of the resource. */
     public function index()
     {
+        $servers = Auth::user()->servers;
+
+        //Get and set server infos each server
+        foreach ($servers as $server) {
+
+            //Get server infos from ptero
+            $serverAttributes = Pterodactyl::getServerAttributes($server->pterodactyl_id);
+
+            $serverRelationships = $serverAttributes['relationships'];
+            $serverLocationAttributes = $serverRelationships['location']['attributes'];
+
+            //Set server infos
+            $server->location = $serverLocationAttributes['long'] ?
+                $serverLocationAttributes['long'] :
+                $serverLocationAttributes['short'];
+
+            $server->egg = $serverRelationships['egg']['attributes']['name'];
+            $server->nest = $serverRelationships['nest']['attributes']['name'];
+
+            $server->node = $serverRelationships['node']['attributes']['name'];
+
+            //get productname by product_id for server
+            $product = Product::find($server->product_id);
+
+            $server->product = $product;
+        }
+
         return view('servers.index')->with([
-            'servers' => Auth::user()->Servers
+            'servers' => $servers
         ]);
     }
 
@@ -71,7 +98,7 @@ class ServerController extends Controller
     {
         //limit validation
         if (Auth::user()->servers()->count() >= Auth::user()->server_limit) {
-            return redirect()->route('servers.index')->with('error', 'Server limit reached!');
+            return redirect()->route('servers.index')->with('error', __('Server limit reached!'));
         }
 
         // minimum credits
@@ -80,7 +107,7 @@ class ServerController extends Controller
             if (
                 Auth::user()->credits <
                 ($product->minimum_credits == -1
-                    ? Configuration::getValueByKey('MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER', 50)
+                    ? config('SETTINGS::USER:MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER', 50)
                     : $product->minimum_credits)
             ) {
                 return redirect()->route('servers.index')->with('error', "You do not have the required amount of " . CREDITS_DISPLAY_NAME . " to use this product!");
@@ -88,13 +115,13 @@ class ServerController extends Controller
         }
 
         //Required Verification for creating an server
-        if (Configuration::getValueByKey('FORCE_EMAIL_VERIFICATION', 'false') === 'true' && !Auth::user()->hasVerifiedEmail()) {
-            return redirect()->route('profile.index')->with('error', "You are required to verify your email address before you can create a server.");
+        if (config('SETTINGS::USER:FORCE_EMAIL_VERIFICATION', 'false') === 'true' && !Auth::user()->hasVerifiedEmail()) {
+            return redirect()->route('profile.index')->with('error', __("You are required to verify your email address before you can create a server."));
         }
 
         //Required Verification for creating an server
-        if (Configuration::getValueByKey('FORCE_DISCORD_VERIFICATION', 'false') === 'true' && !Auth::user()->discordUser) {
-            return redirect()->route('profile.index')->with('error', "You are required to link your discord account before you can create a server.");
+        if (config('SETTINGS::USER:FORCE_DISCORD_VERIFICATION', 'false') === 'true' && !Auth::user()->discordUser) {
+            return redirect()->route('profile.index')->with('error', __("You are required to link your discord account before you can create a server."));
         }
 
         return null;
@@ -134,19 +161,20 @@ class ServerController extends Controller
         $response = Pterodactyl::createServer($server, $egg, $allocationId);
         if ($response->failed()) return $this->serverCreationFailed($response, $server);
 
+        $serverAttributes = $response->json()['attributes'];
         //update server with pterodactyl_id
         $server->update([
-            'pterodactyl_id' => $response->json()['attributes']['id'],
-            'identifier'     => $response->json()['attributes']['identifier']
+            'pterodactyl_id' => $serverAttributes['id'],
+            'identifier'     => $serverAttributes['identifier']
         ]);
 
-        if (Configuration::getValueByKey('SERVER_CREATE_CHARGE_FIRST_HOUR', 'true') == 'true') {
+        if (config('SETTINGS::SYSTEM:SERVER_CREATE_CHARGE_FIRST_HOUR', 'true') == 'true') {
             if ($request->user()->credits >= $server->product->getHourlyPrice()) {
                 $request->user()->decrement('credits', $server->product->getHourlyPrice());
             }
         }
 
-        return redirect()->route('servers.index')->with('success', 'Server created');
+        return redirect()->route('servers.index')->with('success', __('Server created'));
     }
 
     /**
@@ -159,7 +187,7 @@ class ServerController extends Controller
         $server->delete();
 
         Auth::user()->notify(new ServerCreationError($server));
-        return redirect()->route('servers.index')->with('error', 'No allocations satisfying the requirements for automatic deployment on this node were found.');
+        return redirect()->route('servers.index')->with('error', __('No allocations satisfying the requirements for automatic deployment on this node were found.'));
     }
 
     /**
@@ -180,9 +208,9 @@ class ServerController extends Controller
     {
         try {
             $server->delete();
-            return redirect()->route('servers.index')->with('success', 'Server removed');
+            return redirect()->route('servers.index')->with('success', __('Server removed'));
         } catch (Exception $e) {
-            return redirect()->route('servers.index')->with('error', 'An exception has occurred while trying to remove a resource "' . $e->getMessage() . '"');
+            return redirect()->route('servers.index')->with('error', __('An exception has occurred while trying to remove a resource "') . $e->getMessage() . '"');
         }
     }
 }

+ 14 - 11
app/Http/Controllers/StoreController.php

@@ -2,8 +2,8 @@
 
 namespace App\Http\Controllers;
 
-use App\Models\Configuration;
-use App\Models\PaypalProduct;
+use App\Models\CreditProduct;
+use App\Models\Settings;
 use Illuminate\Support\Facades\Auth;
 
 class StoreController extends Controller
@@ -11,24 +11,27 @@ class StoreController extends Controller
     /** Display a listing of the resource. */
     public function index()
     {
-        $isPaypalSetup = false;
-        if (env('PAYPAL_SECRET') && env('PAYPAL_CLIENT_ID')) $isPaypalSetup = true;
-        if (env('APP_ENV', 'local') == 'local') $isPaypalSetup = true;
+        $isPaymentSetup = false;
 
+        if (
+            env('APP_ENV') == 'local' ||
+            config("SETTINGS::PAYMENTS:PAYPAL:SECRET") && config("SETTINGS::PAYMENTS:PAYPAL:CLIENT_ID") ||
+            config("SETTINGS::PAYMENTS:STRIPE:SECRET") && config("SETTINGS::PAYMENTS:STRIPE:ENDPOINT_SECRET") && config("SETTINGS::PAYMENTS:STRIPE:METHODS")
+        ) $isPaymentSetup = true;
 
         //Required Verification for creating an server
-        if (Configuration::getValueByKey('FORCE_EMAIL_VERIFICATION', false) === 'true' && !Auth::user()->hasVerifiedEmail()) {
-            return redirect()->route('profile.index')->with('error', "You are required to verify your email address before you can purchase credits.");
+        if (config('SETTINGS::USER:FORCE_EMAIL_VERIFICATION', false) === 'true' && !Auth::user()->hasVerifiedEmail()) {
+            return redirect()->route('profile.index')->with('error', __("You are required to verify your email address before you can purchase credits."));
         }
 
         //Required Verification for creating an server
-        if (Configuration::getValueByKey('FORCE_DISCORD_VERIFICATION', false) === 'true' && !Auth::user()->discordUser) {
-            return redirect()->route('profile.index')->with('error', "You are required to link your discord account before you can purchase ".CREDITS_DISPLAY_NAME.".");
+        if (config('SETTINGS::USER:FORCE_DISCORD_VERIFICATION', false) === 'true' && !Auth::user()->discordUser) {
+            return redirect()->route('profile.index')->with('error', __("You are required to link your discord account before you can purchase Credits"));
         }
 
         return view('store.index')->with([
-            'products' => PaypalProduct::where('disabled', '=', false)->orderBy('price', 'asc')->get(),
-            'isPaypalSetup' => $isPaypalSetup
+            'products' => CreditProduct::where('disabled', '=', false)->orderBy('price', 'asc')->get(),
+            'isPaymentSetup' => $isPaymentSetup,
         ]);
     }
 }

+ 23 - 0
app/Http/Controllers/TranslationController.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Session;
+
+class TranslationController extends Controller
+{
+    /**
+     *
+     * Change session locale
+     * @param Request $request
+     * @return Response
+     */
+    public function changeLocale(Request $request)
+    {
+        Session::put('locale', $request->inputLocale);
+        return redirect()->back();
+    }
+
+
+}

+ 4 - 3
app/Http/Kernel.php

@@ -4,7 +4,7 @@ namespace App\Http;
 
 use App\Http\Middleware\ApiAuthToken;
 use App\Http\Middleware\CheckSuspended;
-use App\Http\Middleware\CreditsDisplayName;
+use App\Http\Middleware\GlobalNames;
 use App\Http\Middleware\isAdmin;
 use App\Http\Middleware\LastSeen;
 use Illuminate\Foundation\Http\Kernel as HttpKernel;
@@ -43,13 +43,14 @@ class Kernel extends HttpKernel
             \App\Http\Middleware\VerifyCsrfToken::class,
             \Illuminate\Routing\Middleware\SubstituteBindings::class,
             LastSeen::class,
-            CreditsDisplayName::class,
+            GlobalNames::class,
+            \App\Http\Middleware\SetLocale::class,
         ],
 
         'api' => [
             'throttle:api',
             \Illuminate\Routing\Middleware\SubstituteBindings::class,
-            CreditsDisplayName::class
+            GlobalNames::class
         ],
     ];
 

+ 0 - 23
app/Http/Middleware/CreditsDisplayName.php

@@ -1,23 +0,0 @@
-<?php
-
-namespace App\Http\Middleware;
-
-use App\Models\Configuration;
-use Closure;
-use Illuminate\Http\Request;
-
-class CreditsDisplayName
-{
-    /**
-     * Handle an incoming request.
-     *
-     * @param Request $request
-     * @param Closure $next
-     * @return mixed
-     */
-    public function handle(Request $request, Closure $next)
-    {
-        define('CREDITS_DISPLAY_NAME' , Configuration::getValueByKey('CREDITS_DISPLAY_NAME' , 'Credits'));
-        return $next($request);
-    }
-}

+ 29 - 0
app/Http/Middleware/GlobalNames.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use App\Models\Configuration;
+use App\Models\Settings;
+use Closure;
+use Illuminate\Http\Request;
+
+class GlobalNames
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param Request $request
+     * @param Closure $next
+     * @return mixed
+     */
+    public function handle(Request $request, Closure $next)
+    {
+        define('CREDITS_DISPLAY_NAME', config('SETTINGS::SYSTEM:CREDITS_DISPLAY_NAME', 'Credits'));
+
+        $unsupported_lang_array = explode(',', config("app.unsupported_locales"));
+        $unsupported_lang_array = array_map('strtolower', $unsupported_lang_array);
+        define('UNSUPPORTED_LANGS', $unsupported_lang_array);
+
+        return $next($request);
+    }
+}

+ 41 - 0
app/Http/Middleware/SetLocale.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use App\Models\Settings;
+use Closure;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\App;
+use Illuminate\Support\Facades\Session;
+
+class SetLocale
+{
+
+    /**
+     *
+     * Handle an incoming request.
+     *
+     * @param Request $request
+     * @param Closure $next
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        if (Session::has('locale')) {
+            $locale = Session::get('locale', config("SETTINGS::LOCALE:DEFAULT"));
+        } else {
+            if (config("SETTINGS::LOCALE:DYNAMIC") !== "true") {
+                $locale = config("SETTINGS::LOCALE:DEFAULT");
+            } else {
+                $locale = substr($request->server('HTTP_ACCEPT_LANGUAGE'), 0, 2);
+
+                if (!in_array($locale, explode(',', config("SETTINGS::LOCALE:AVAILABLE")))) {
+                    $locale = config("SETTINGS::LOCALE:DEFAULT");
+                }
+            }
+        }
+        App::setLocale($locale);
+
+        return $next($request);
+    }
+}

+ 1 - 1
app/Http/Middleware/VerifyCsrfToken.php

@@ -12,6 +12,6 @@ class VerifyCsrfToken extends Middleware
      * @var array
      */
     protected $except = [
-        //
+        'payment/StripeWebhooks'
     ];
 }

+ 7 - 8
app/Listeners/UnsuspendServers.php

@@ -3,11 +3,10 @@
 namespace App\Listeners;
 
 use App\Events\UserUpdateCreditsEvent;
-use App\Models\Configuration;
 use App\Models\Server;
+use App\Models\Settings;
 use Exception;
 use Illuminate\Contracts\Queue\ShouldQueue;
-use Illuminate\Queue\InteractsWithQueue;
 
 class UnsuspendServers implements ShouldQueue
 {
@@ -20,11 +19,11 @@ class UnsuspendServers implements ShouldQueue
      */
     public function handle(UserUpdateCreditsEvent $event)
     {
-       if ($event->user->credits > Configuration::getValueByKey('MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER' , 50)){
-           /** @var Server $server */
-           foreach ($event->user->servers as $server){
-               if ($server->isSuspended()) $server->unSuspend();
-           }
-       }
+        if ($event->user->credits > config('SETTINGS::USER:MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER', 50)) {
+            /** @var Server $server */
+            foreach ($event->user->servers as $server) {
+                if ($server->isSuspended()) $server->unSuspend();
+            }
+        }
     }
 }

+ 3 - 5
app/Listeners/Verified.php

@@ -2,9 +2,7 @@
 
 namespace App\Listeners;
 
-use App\Models\Configuration;
-use Illuminate\Contracts\Queue\ShouldQueue;
-use Illuminate\Queue\InteractsWithQueue;
+use App\Models\Settings;
 
 class Verified
 {
@@ -26,7 +24,7 @@ class Verified
      */
     public function handle($event)
     {
-        $event->user->increment('server_limit' , Configuration::getValueByKey('SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL'));
-        $event->user->increment('credits' , Configuration::getValueByKey('CREDITS_REWARD_AFTER_VERIFY_EMAIL'));
+        $event->user->increment('server_limit', config('SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL'));
+        $event->user->increment('credits', config('SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_EMAIL'));
     }
 }

+ 21 - 21
app/Models/PaypalProduct.php → app/Models/CreditProduct.php

@@ -8,7 +8,7 @@ use NumberFormatter;
 use Spatie\Activitylog\Traits\LogsActivity;
 use App\Models\Configuration;
 
-class PaypalProduct extends Model
+class CreditProduct extends Model
 {
     use LogsActivity;
     /**
@@ -33,53 +33,53 @@ class PaypalProduct extends Model
     {
         parent::boot();
 
-        static::creating(function (PaypalProduct $paypalProduct) {
+        static::creating(function (CreditProduct $creditProduct) {
             $client = new Client();
 
-            $paypalProduct->{$paypalProduct->getKeyName()} = $client->generateId($size = 21);
+            $creditProduct->{$creditProduct->getKeyName()} = $client->generateId($size = 21);
         });
     }
 
     /**
      * @param mixed $value
      * @param string $locale
-     * 
+     *
      * @return float
      */
-    public function formatToCurrency($value,$locale = 'en_US')
+    public function formatToCurrency($value, $locale = 'en_US')
     {
         $formatter = new NumberFormatter($locale, NumberFormatter::CURRENCY);
         return $formatter->formatCurrency($value, $this->currency_code);
     }
 
     /**
-    * @description Returns the tax in % taken from the Configuration
-    *
-    * @return int
-    */
+     * @description Returns the tax in % taken from the Configuration
+     *
+     * @return int
+     */
     public function getTaxPercent()
     {
-        $tax = Configuration::getValueByKey("SALES_TAX");
+        $tax = config("SETTINGS::PAYMENTS:SALES_TAX");
         return $tax < 0 ? 0 : $tax;
     }
 
     /**
-    * @description Returns the tax as Number
-    *
-    * @return float
-    */
+     * @description Returns the tax as Number
+     *
+     * @return float
+     */
     public function getTaxValue()
     {
-        return $this->price*$this->getTaxPercent()/100;
+        return number_format($this->price * $this->getTaxPercent() / 100, 2);
     }
 
     /**
-    * @description Returns the full price of a Product including tax
-    *
-    * @return float
-    */
-    public function getTotalPrice() 
+     * @description Returns the full price of a Product including tax
+     *
+     * @return float
+     */
+    public function getTotalPrice()
     {
-        return $this->price+($this->getTaxValue());
+        return number_format($this->price + $this->getTaxValue(), 2);
     }
 }

+ 18 - 0
app/Models/Invoice.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+
+class Invoice extends Model
+{
+    use HasFactory;
+
+    protected $fillable = [
+        'invoice_name',
+        'invoice_user',
+        'payment_id'
+    ];
+
+}

+ 4 - 4
app/Models/Payment.php

@@ -23,8 +23,7 @@ class Payment extends Model
         'id',
         'user_id',
         'payment_id',
-        'payer_id',
-        'payer',
+        'payment_method',
         'status',
         'type',
         'amount',
@@ -33,6 +32,7 @@ class Payment extends Model
         'total_price',
         'tax_percent',
         'currency_code',
+        'credit_product_id',
     ];
 
     public static function boot()
@@ -57,10 +57,10 @@ class Payment extends Model
     /**
      * @param mixed $value
      * @param string $locale
-     * 
+     *
      * @return float
      */
-    public function formatToCurrency($value,$locale = 'en_US')
+    public function formatToCurrency($value, $locale = 'en_US')
     {
         $formatter = new NumberFormatter($locale, NumberFormatter::CURRENCY);
         return $formatter->formatCurrency($value, $this->currency_code);

+ 8 - 6
app/Models/Configuration.php → app/Models/Settings.php

@@ -6,11 +6,13 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Support\Facades\Cache;
 
-class Configuration extends Model
+class Settings extends Model
 {
     use HasFactory;
 
-    public const CACHE_TAG = 'configuration';
+    protected $table = 'settings';
+
+    public const CACHE_TAG = 'setting';
 
     public $primaryKey = 'key';
 
@@ -28,8 +30,8 @@ class Configuration extends Model
     {
         parent::boot();
 
-        static::updated(function (Configuration $configuration) {
-            Cache::forget(self::CACHE_TAG .':'. $configuration->key);
+        static::updated(function (Settings $settings) {
+            Cache::forget(self::CACHE_TAG .':'. $settings->key);
         });
     }
 
@@ -41,8 +43,8 @@ class Configuration extends Model
     public static function getValueByKey(string $key, $default = null)
     {
         return Cache::rememberForever(self::CACHE_TAG .':'. $key, function () use ($default, $key) {
-            $configuration = self::find($key);
-            return $configuration ? $configuration->value : $default;
+            $settings = self::find($key);
+            return $settings ? $settings->value : $default;
         });
     }
 }

+ 2 - 2
app/Models/Voucher.php

@@ -86,10 +86,10 @@ class Voucher extends Model
     {
         if ($this->users()->count() >= $this->uses) return 'USES_LIMIT_REACHED';
         if (!is_null($this->expires_at)) {
-            if ($this->expires_at->isPast()) return 'EXPIRED';
+            if ($this->expires_at->isPast()) return __('EXPIRED');
         }
 
-        return 'VALID';
+        return __('VALID');
     }
 
     /**

+ 6 - 3
app/Notifications/ConfirmPaymentNotification.php

@@ -10,6 +10,9 @@ use Illuminate\Notifications\Notification;
 
 class ConfirmPaymentNotification extends Notification implements ShouldQueue
 {
+
+    //THIS IS BASICALLY NOT USED ANYMORE WITH INVOICENOTIFICATION IN PLACE
+
     use Queueable;
 
     private Payment $payment;
@@ -44,7 +47,7 @@ class ConfirmPaymentNotification extends Notification implements ShouldQueue
     public function toMail($notifiable)
     {
         return (new MailMessage)
-            ->subject('Payment Confirmation')
+            ->subject(__('Payment Confirmation'))
             ->markdown('mail.payment.confirmed' , ['payment' => $this->payment]);
     }
 
@@ -57,8 +60,8 @@ class ConfirmPaymentNotification extends Notification implements ShouldQueue
     public function toArray($notifiable)
     {
         return [
-            'title'   => "Payment Confirmed!",
-            'content' => "Payment Confirmed!",
+            'title'   => __("Payment Confirmed!"),
+            'content' => __("Payment Confirmed!"),
         ];
     }
 }

+ 69 - 0
app/Notifications/InvoiceNotification.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace App\Notifications;
+
+use App\Models\Payment;
+use App\Models\User;
+use Illuminate\Bus\Queueable;
+use Illuminate\Notifications\Messages\MailMessage;
+use Illuminate\Notifications\Notification;
+use LaravelDaily\Invoices\Invoice;
+
+class InvoiceNotification extends Notification
+
+{
+    use Queueable;
+
+    /**
+     * @var invoice
+     *      * @var invoice
+     *      * @var invoice
+     */
+    private $invoice;
+    private $user;
+    private $payment;
+
+    /**
+     * Create a new notification instance.
+     *
+     * @param Invoice $invoice
+     */
+    public function __construct(Invoice $invoice, User $user, Payment $payment)
+    {
+        $this->invoice = $invoice;
+        $this->user = $user;
+        $this->payment = $payment;
+    }
+
+    /**
+     * Get the notification's delivery channels.
+     *
+     * @param mixed $notifiable
+     * @return array
+     */
+    public function via($notifiable)
+    {
+        return ['mail'];
+    }
+
+    /**
+     * Get the array representation of the notification.
+     *
+     * @param mixed $notifiable
+     * @return MailMessage
+     */
+    public function toMail($notifiable)
+    {
+        return (new MailMessage)
+            ->subject(__('Your Payment was successful!'))
+            ->greeting(__('Hello').',')
+            ->line(__("Your payment was processed successfully!"))
+            ->line(__('Status').': ' . $this->payment->status)
+            ->line(__('Price').': ' . $this->payment->formatToCurrency($this->payment->total_price))
+            ->line(__('Type').': ' . $this->payment->type)
+            ->line(__('Amount').': ' . $this->payment->amount)
+            ->line(__('Balance').': ' . number_format($this->user->credits,2))
+            ->line(__('User ID').': ' . $this->payment->user_id)
+            ->attach(storage_path('app/invoice/' . $this->user->id . '/' . now()->format('Y') . '/' . $this->invoice->filename));
+    }
+}

+ 1 - 1
app/Notifications/ServerCreationError.php

@@ -46,7 +46,7 @@ class ServerCreationError extends Notification
     public function toArray($notifiable)
     {
         return [
-            'title' => "Server Creation Error",
+            'title' => __("Server Creation Error"),
             'content' => "
                 <p>Hello <strong>{$this->server->User->name}</strong>, An unexpected error has occurred...</p>
                 <p>There was a problem creating your server on our pterodactyl panel. There are likely no allocations or rooms left on the selected node. Please contact one of our support members through our discord server to get this resolved asap!</p>

+ 10 - 11
app/Notifications/ServersSuspendedNotification.php

@@ -2,7 +2,6 @@
 
 namespace App\Notifications;
 
-use App\Models\Configuration;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Notifications\Messages\MailMessage;
@@ -42,11 +41,11 @@ class ServersSuspendedNotification extends Notification implements ShouldQueue
     public function toMail($notifiable)
     {
         return (new MailMessage)
-                    ->subject('Your servers have been suspended!')
-                    ->greeting('Your servers have been suspended!')
-                    ->line("To automatically re-enable your server/s, you need to purchase more credits.")
-                    ->action('Purchase credits', route('store.index'))
-                    ->line('If you have any questions please let us know.');
+                    ->subject(__('Your servers have been suspended!'))
+                    ->greeting(__('Your servers have been suspended!'))
+                    ->line(__("To automatically re-enable your server/s, you need to purchase more credits."))
+                    ->action(__('Purchase credits'), route('store.index'))
+                    ->line(__('If you have any questions please let us know.'));
     }
 
     /**
@@ -58,12 +57,12 @@ class ServersSuspendedNotification extends Notification implements ShouldQueue
     public function toArray($notifiable)
     {
         return [
-            'title'   => "Servers suspended!",
+            'title'   => __('Your servers have been suspended!'),
             'content' => "
-                <h5>Your servers have been suspended!</h5>
-                <p>To automatically re-enable your server/s, you need to purchase more credits.</p>
-                <p>If you have any questions please let us know.</p>
-                <p>Regards,<br />" . config('app.name', 'Laravel') . "</p>
+                <h5>". __('Your servers have been suspended!')."</h5>
+                <p>". __("To automatically re-enable your server/s, you need to purchase more credits.")."</p>
+                <p>". __('If you have any questions please let us know.')."</p>
+                <p>". __('Regards').",<br />" . config('app.name', 'Laravel') . "</p>
             ",
         ];
     }

+ 20 - 20
app/Notifications/WelcomeMessage.php

@@ -2,7 +2,7 @@
 
 namespace App\Notifications;
 
-use App\Models\Configuration;
+use App\Models\Settings;
 use App\Models\User;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
@@ -38,25 +38,25 @@ class WelcomeMessage extends Notification implements ShouldQueue
         return ['database'];
     }
     public function AdditionalLines()
-        {
-
-            $AdditionalLine = "";
-            if(Configuration::getValueByKey('CREDITS_REWARD_AFTER_VERIFY_EMAIL') != 0) {
-                $AdditionalLine .= "Verifying your e-mail address will grant you ".Configuration::getValueByKey('CREDITS_REWARD_AFTER_VERIFY_EMAIL')." additional " . Configuration::getValueByKey('CREDITS_DISPLAY_NAME') . ". <br />";
-            }
-            if(Configuration::getValueByKey('SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL') != 0) {
-                $AdditionalLine .= "Verifying your e-mail will also increase your Server Limit by " . Configuration::getValueByKey('SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL') . ". <br />";
-            }
-            $AdditionalLine .="<br />";
-            if(Configuration::getValueByKey('CREDITS_REWARD_AFTER_VERIFY_DISCORD') != 0) {
-                $AdditionalLine .=  "You can also verify your discord account to get another " . Configuration::getValueByKey('CREDITS_REWARD_AFTER_VERIFY_DISCORD') . " " . Configuration::getValueByKey('CREDITS_DISPLAY_NAME') . ". <br />";
-            }
-            if(Configuration::getValueByKey('SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD') != 0) {
-                $AdditionalLine .=  "Verifying your Discord account will also increase your Server Limit by " . Configuration::getValueByKey('SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD') . ". <br />";
-            }
+    {
 
-            return $AdditionalLine;
+        $AdditionalLine = "";
+        if (config('SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_EMAIL') != 0) {
+            $AdditionalLine .= "Verifying your e-mail address will grant you " . config('SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_EMAIL') . " additional " . config('SETTINGS::SYSTEM:CREDITS_DISPLAY_NAME') . ". <br />";
+        }
+        if (config('SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL') != 0) {
+            $AdditionalLine .= "Verifying your e-mail will also increase your Server Limit by " . config('SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL') . ". <br />";
+        }
+        $AdditionalLine .= "<br />";
+        if (config('SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_DISCORD') != 0) {
+            $AdditionalLine .=  "You can also verify your discord account to get another " . config('SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_DISCORD') . " " . config('SETTINGS::SYSTEM:CREDITS_DISPLAY_NAME') . ". <br />";
         }
+        if (config('SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD') != 0) {
+            $AdditionalLine .=  "Verifying your Discord account will also increase your Server Limit by " . config('SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD') . ". <br />";
+        }
+
+        return $AdditionalLine;
+    }
     /**
      * Get the array representation of the notification.
      *
@@ -66,13 +66,13 @@ class WelcomeMessage extends Notification implements ShouldQueue
     public function toArray($notifiable)
     {
         return [
-            'title'   => "Getting started!",
+            'title'   => __("Getting started!"),
             'content' => "
                <p>Hello <strong>{$this->user->name}</strong>, Welcome to our dashboard!</p>
                 <h5>Verification</h5>
                 <p>You can verify your e-mail address and link/verify your Discord account.</p>
                 <p>
-                  ".$this->AdditionalLines()."
+                  " . $this->AdditionalLines() . "
                 </p>
                 <h5>Information</h5>
                 <p>This dashboard can be used to create and delete servers.<br /> These servers can be used and managed on our pterodactyl panel.<br /> If you have any questions, please join our Discord server and #create-a-ticket.</p>

+ 57 - 4
app/Providers/AppServiceProvider.php

@@ -2,11 +2,12 @@
 
 namespace App\Providers;
 
+use App\Models\Settings;
 use Illuminate\Pagination\Paginator;
+use Illuminate\Support\Facades\Artisan;
 use Illuminate\Support\Facades\Schema;
 use Illuminate\Support\Facades\Validator;
 use Illuminate\Support\ServiceProvider;
-use Spatie\QueryBuilder\QueryBuilderRequest;
 
 class AppServiceProvider extends ServiceProvider
 {
@@ -31,14 +32,11 @@ class AppServiceProvider extends ServiceProvider
         Schema::defaultStringLength(191);
 
         Validator::extend('multiple_date_format', function ($attribute, $value, $parameters, $validator) {
-
             $ok = true;
-
             $result = [];
 
             // iterate through all formats
             foreach ($parameters as $parameter) {
-
                 //validate with laravels standard date format validation
                 $result[] = $validator->validateDateFormat($attribute, $value, [$parameter]);
             }
@@ -51,5 +49,60 @@ class AppServiceProvider extends ServiceProvider
 
             return $ok;
         });
+
+        //only run if the installer has been executed
+        if (file_exists(base_path()."/install.lock")) {
+            $settings = Settings::all();
+            // Set all configs from database
+            foreach ($settings as $setting) {
+                config([$setting->key => $setting->value]);
+            }
+
+            // Set Mail Config
+            //only update config if mail settings have changed in DB
+            if (
+                config('mail.default') != config('SETTINGS:MAIL:MAILER') ||
+                config('mail.mailers.smtp.host') != config('SETTINGS:MAIL:HOST') ||
+                config('mail.mailers.smtp.port') != config('SETTINGS:MAIL:PORT') ||
+                config('mail.mailers.smtp.username') != config('SETTINGS:MAIL:USERNAME') ||
+                config('mail.mailers.smtp.password') != config('SETTINGS:MAIL:PASSWORD') ||
+                config('mail.mailers.smtp.encryption') != config('SETTINGS:MAIL:ENCRYPTION') ||
+                config('mail.from.address') != config('SETTINGS:MAIL:FROM_ADDRESS') ||
+                config('mail.from.name') != config('SETTINGS:MAIL:FROM_NAME')
+            ) {
+                config(['mail.default' => config('SETTINGS::MAIL:MAILER')]);
+                config(['mail.mailers.smtp' => [
+                    'transport' => 'smtp',
+                    'host' => config('SETTINGS::MAIL:HOST'),
+                    'port' => config('SETTINGS::MAIL:PORT'),
+                    'encryption' => config('SETTINGS::MAIL:ENCRYPTION'),
+                    'username' => config('SETTINGS::MAIL:USERNAME'),
+                    'password' => config('SETTINGS::MAIL:PASSWORD'),
+                    'timeout' => null,
+                    'auth_mode' => null,
+                ]]);
+                config(['mail.from' => ['address' => config('SETTINGS::MAIL:FROM_ADDRESS'), 'name' => config('SETTINGS::MAIL:FROM_NAME')]]);
+
+                Artisan::call('queue:restart');
+            }
+
+
+            // Set Recaptcha API Config
+            //only update config if recaptcha settings have changed in DB
+            if (
+                config('recaptcha.api_site_key') != config('SETTINGS::RECAPTCHA:SITE_KEY') ||
+                config('recaptcha.api_secret_key') != config('SETTINGS::RECAPTCHA:SECRET_KEY')
+            ) {
+                config(['recaptcha.api_site_key' => config('SETTINGS::RECAPTCHA:SITE_KEY')]);
+                config(['recaptcha.api_secret_key' => config('SETTINGS::RECAPTCHA:SECRET_KEY')]);
+
+                Artisan::call('config:clear');
+                Artisan::call('cache:clear');
+            }
+
+            // Set Discord-API Config
+            config(['services.discord.client_id' => config('SETTINGS::DISCORD:CLIENT_ID')]);
+            config(['services.discord.client_secret' => config('SETTINGS::DISCORD:CLIENT_SECRET')]);
+        }
     }
 }

+ 6 - 2
composer.json

@@ -1,7 +1,7 @@
 {
-    "name": "laravel/laravel",
+    "name": "cpgg/dashboard",
     "type": "project",
-    "description": "The Laravel Framework.",
+    "description": "A billing and control panel made for Pterodactyl.",
     "keywords": [
         "framework",
         "laravel"
@@ -16,15 +16,19 @@
         "fruitcake/laravel-cors": "^2.0",
         "guzzlehttp/guzzle": "^7.0.1",
         "hidehalo/nanoid-php": "^1.1",
+        "kkomelin/laravel-translatable-string-exporter": "^1.14",
         "laravel/framework": "^8.12",
         "laravel/tinker": "^2.5",
         "laravel/ui": "^3.2",
+        "laraveldaily/laravel-invoices": "^2.0",
         "paypal/paypal-checkout-sdk": "^1.0",
         "paypal/rest-api-sdk-php": "^1.14",
         "socialiteproviders/discord": "^4.1",
         "spatie/laravel-activitylog": "^3.16",
         "spatie/laravel-query-builder": "^3.6",
         "spatie/laravel-validation-rules": "^3.0",
+        "stripe/stripe-php": "^7.107",
+        "symfony/intl": "^6.0",
         "yajra/laravel-datatables-oracle": "~9.0"
     },
     "require-dev": {

File diff suppressed because it is too large
+ 377 - 190
composer.lock


+ 22 - 2
config/app.php

@@ -1,8 +1,10 @@
 <?php
 
+use App\Models\Settings;
+
 return [
 
-    'version' => '0.6.2',
+    'version' => '0.7',
     /*
     |--------------------------------------------------------------------------
     | Application Name
@@ -70,6 +72,7 @@ return [
 
     'timezone' => env('APP_TIMEZONE', 'UTC'),
 
+
     /*
     |--------------------------------------------------------------------------
     | Application Locale Configuration
@@ -81,7 +84,21 @@ return [
     |
     */
 
-    'locale' => 'en',
+    'locale' =>"en",
+
+    /*
+    |--------------------------------------------------------------------------
+    | Available Languages
+    |--------------------------------------------------------------------------
+    |
+    | The application locale determines the default locale that will be used
+    | by the translation service provider. You are free to set this value
+    | to any of the locales which will be supported by the application.
+    |
+    */
+
+    'available_locales' => array_map('basename', preg_replace('/\\.[^.\\s]{3,4}$/', '', glob(resource_path()."/lang/*.json", GLOB_BRACE))),
+
 
     /*
     |--------------------------------------------------------------------------
@@ -178,6 +195,9 @@ return [
         App\Providers\RouteServiceProvider::class,
         Yajra\DataTables\DataTablesServiceProvider::class,
 
+        KKomelin\TranslatableStringExporter\Providers\ExporterServiceProvider::class,
+
+
     ],
 
     /*

+ 97 - 0
config/invoices.php

@@ -0,0 +1,97 @@
+<?php
+
+return [
+    'date' => [
+        /*
+         * Carbon date format
+         */
+        'format' => 'Y-m-d',
+        /*
+         * Due date for payment since invoice's date.
+         */
+        'pay_until_days' => 7,
+    ],
+
+    'serial_number' => [
+        'series'   => 'AA',
+        'sequence' => 1,
+        /*
+         * Sequence will be padded accordingly, for ex. 00001
+         */
+        'sequence_padding' => 5,
+        'delimiter'        => '.',
+        /*
+         * Supported tags {SERIES}, {DELIMITER}, {SEQUENCE}
+         * Example: AA.00001
+         */
+        'format' => '{SERIES}{DELIMITER}{SEQUENCE}',
+    ],
+
+    'currency' => [
+        'code' => 'eur',
+        /*
+         * Usually cents
+         * Used when spelling out the amount and if your currency has decimals.
+         *
+         * Example: Amount in words: Eight hundred fifty thousand sixty-eight EUR and fifteen ct.
+         */
+        'fraction' => 'ct.',
+        'symbol'   => '€',
+        /*
+         * Example: 19.00
+         */
+        'decimals' => 2,
+        /*
+         * Example: 1.99
+         */
+        'decimal_point' => '.',
+        /*
+         * By default empty.
+         * Example: 1,999.00
+         */
+        'thousands_separator' => '',
+        /*
+         * Supported tags {VALUE}, {SYMBOL}, {CODE}
+         * Example: 1.99 €
+         */
+        'format' => '{VALUE} {SYMBOL}',
+    ],
+
+    'paper' => [
+        // A4 = 210 mm x 297 mm = 595 pt x 842 pt
+        'size'        => 'a4',
+        'orientation' => 'portrait',
+    ],
+
+    'disk' => 'local',
+
+    'seller' => [
+        /*
+         * Class used in templates via $invoice->seller
+         *
+         * Must implement LaravelDaily\Invoices\Contracts\PartyContract
+         *      or extend LaravelDaily\Invoices\Classes\Party
+         */
+        'class' => \LaravelDaily\Invoices\Classes\Seller::class,
+
+        /*
+         * Default attributes for Seller::class
+         */
+        'attributes' => [
+            'name'          => 'Towne, Smith and Ebert',
+            'address'       => '89982 Pfeffer Falls Damianstad, CO 66972-8160',
+            'code'          => '41-1985581',
+            'vat'           => '123456789',
+            'phone'         => '760-355-3930',
+            'custom_fields' => [
+                /*
+                 * Custom attributes for Seller::class
+                 *
+                 * Used to display additional info on Seller section in invoice
+                 * attribute => value
+                 */
+                'SWIFT' => 'BANK101',
+            ],
+        ],
+    ],
+];

+ 16 - 25
config/mail.php

@@ -45,31 +45,22 @@ return [
             'auth_mode' => null,
         ],
 
-        'ses' => [
-            'transport' => 'ses',
-        ],
-
-        'mailgun' => [
-            'transport' => 'mailgun',
-        ],
-
-        'postmark' => [
-            'transport' => 'postmark',
-        ],
-
-        'sendmail' => [
-            'transport' => 'sendmail',
-            'path' => '/usr/sbin/sendmail -bs',
-        ],
-
-        'log' => [
-            'transport' => 'log',
-            'channel' => env('MAIL_LOG_CHANNEL'),
-        ],
-
-        'array' => [
-            'transport' => 'array',
-        ],
+        // 'ses' => [
+        // 'transport' => 'ses',
+        // ],
+        //
+        // 'mailgun' => [
+        // 'transport' => 'mailgun',
+        // ],
+        //
+        // 'postmark' => [
+        // 'transport' => 'postmark',
+        // ],
+        //
+        // 'sendmail' => [
+        // 'transport' => 'sendmail',
+        // 'path' => '/usr/sbin/sendmail -bs',
+        // ],
     ],
 
     /*

+ 0 - 0
database/migrations/.gitkeep


+ 1 - 1
database/migrations/2021_05_08_081218_create_paypal_products_table.php

@@ -19,7 +19,7 @@ class CreatePaypalProductsTable extends Migration
             $table->decimal('price')->default(0);
             $table->unsignedInteger('quantity');
             $table->string('description');
-            $table->string('currency_code' , 3);
+            $table->string('currency_code', 3);
             $table->boolean('disabled')->default(true);
             $table->timestamps();
         });

+ 0 - 1
database/migrations/2021_05_08_164658_create_configurations_table.php

@@ -21,7 +21,6 @@ class CreateConfigurationsTable extends Migration
             $table->timestamps();
         });
     }
-
     /**
      * Reverse the migrations.
      *

+ 1 - 1
database/migrations/2021_05_09_153742_add_display_to_paypal_products_table.php

@@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
 use Illuminate\Database\Schema\Blueprint;
 use Illuminate\Support\Facades\Schema;
 
-class AddDisplayToPaypalProductsTable extends Migration
+class AddDisplayToPayPalProductsTable extends Migration
 {
     /**
      * Run the migrations.

+ 34 - 0
database/migrations/2021_11_27_014226_create_invoices.php

@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateInvoices extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('invoices', function (Blueprint $table) {
+            $table->id();
+            $table->string('invoice_name');
+            $table->string('invoice_user');
+            $table->string('payment_id');
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('invoices');
+    }
+}

+ 42 - 0
database/migrations/2021_12_15_120346_update_to_payments_table.php

@@ -0,0 +1,42 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Support\Facades\DB;
+
+
+class UpdateToPaymentsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::table('payments', function (Blueprint $table) {
+            $table->string('payment_method');
+            $table->dropColumn('payer');
+            $table->dropColumn('payer_id');
+            $table->string('credit_product_id');
+        });
+
+        DB::statement('UPDATE payments SET payment_method="paypal"');
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::table('payments', function (Blueprint $table) {
+            $table->dropColumn('payment_method');
+            $table->string('payer_id')->nullable();
+            $table->text('payer')->nullable();
+            $table->dropColumn('credit_product_id');
+        });
+    }
+}

+ 28 - 0
database/migrations/2021_12_28_203515_rename_paypal_products_table.php

@@ -0,0 +1,28 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class RenamePaypalProductsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::rename('paypal_products', 'credit_products');
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::rename('credit_products', 'paypal_products');
+    }
+}

+ 61 - 0
database/migrations/2022_01_05_144858_rename_configurations_table.php

@@ -0,0 +1,61 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Support\Facades\DB;
+
+
+class RenameConfigurationsTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::rename('configurations', 'settings');
+
+        DB::table('settings')->where('key', 'INITIAL_CREDITS')->update(['key' => 'SETTINGS::USER:INITIAL_CREDITS']);
+        DB::table('settings')->where('key', 'INITIAL_SERVER_LIMIT')->update(['key' => 'SETTINGS::USER:INITIAL_SERVER_LIMIT']);
+        DB::table('settings')->where('key', 'CREDITS_REWARD_AFTER_VERIFY_EMAIL')->update(['key' => 'SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_EMAIL']);
+        DB::table('settings')->where('key', 'SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL')->update(['key' => 'SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL']);
+        DB::table('settings')->where('key', 'CREDITS_REWARD_AFTER_VERIFY_DISCORD')->update(['key' => 'SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_DISCORD']);
+        DB::table('settings')->where('key', 'SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD')->update(['key' => 'SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD']);
+        DB::table('settings')->where('key', 'MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER')->update(['key' => 'SETTINGS::USER:MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER']);
+        DB::table('settings')->where('key', 'SERVER_LIMIT_AFTER_IRL_PURCHASE')->update(['key' => 'SETTINGS::USER:SERVER_LIMIT_AFTER_IRL_PURCHASE']);
+        DB::table('settings')->where('key', 'FORCE_EMAIL_VERIFICATION')->update(['key' => 'SETTINGS::USER:FORCE_EMAIL_VERIFICATION']);
+        DB::table('settings')->where('key', 'FORCE_DISCORD_VERIFICATION')->update(['key' => 'SETTINGS::USER:FORCE_DISCORD_VERIFICATION']);
+        DB::table('settings')->where('key', 'REGISTER_IP_CHECK')->update(['key' => 'SETTINGS::SYSTEM:REGISTER_IP_CHECK']);
+        DB::table('settings')->where('key', 'CREDITS_DISPLAY_NAME')->update(['key' => 'SETTINGS::SYSTEM:CREDITS_DISPLAY_NAME']);
+        DB::table('settings')->where('key', 'ALLOCATION_LIMIT')->update(['key' => 'SETTINGS::SERVER:ALLOCATION_LIMIT']);
+        DB::table('settings')->where('key', 'SERVER_CREATE_CHARGE_FIRST_HOUR')->update(['key' => 'SETTINGS::SYSTEM:SERVER_CREATE_CHARGE_FIRST_HOUR']);
+        DB::table('settings')->where('key', 'SALES_TAX')->update(['key' => 'SETTINGS::PAYMENTS:SALES_TAX']);
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::rename('settings', 'configurations');
+
+        DB::table('configurations')->where('key', 'SETTINGS::USER:INITIAL_CREDITS')->update(['key' => 'INITIAL_CREDITS']);
+        DB::table('configurations')->where('key', 'SETTINGS::USER:INITIAL_SERVER_LIMIT')->update(['key' => 'INITIAL_SERVER_LIMIT']);
+        DB::table('configurations')->where('key', 'SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_EMAIL')->update(['key' => 'CREDITS_REWARD_AFTER_VERIFY_EMAIL']);
+        DB::table('configurations')->where('key', 'SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL')->update(['key' => 'SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL']);
+        DB::table('configurations')->where('key', 'SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_DISCORD')->update(['key' => 'CREDITS_REWARD_AFTER_VERIFY_DISCORD']);
+        DB::table('configurations')->where('key', 'SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD')->update(['key' => 'SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD']);
+        DB::table('configurations')->where('key', 'SETTINGS::USER:MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER')->update(['key' => 'MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER']);
+        DB::table('configurations')->where('key', 'SETTINGS::USER:SERVER_LIMIT_AFTER_IRL_PURCHASE')->update(['key' => 'SERVER_LIMIT_AFTER_IRL_PURCHASE']);
+        DB::table('configurations')->where('key', 'SETTINGS::USER:FORCE_EMAIL_VERIFICATION')->update(['key' => 'FORCE_EMAIL_VERIFICATION']);
+        DB::table('configurations')->where('key', 'SETTINGS::USER:FORCE_DISCORD_VERIFICATION')->update(['key' => 'FORCE_DISCORD_VERIFICATION']);
+        DB::table('configurations')->where('key', 'SETTINGS::SYSTEM:REGISTER_IP_CHECK')->update(['key' => 'REGISTER_IP_CHECK']);
+        DB::table('configurations')->where('key', 'SETTINGS::SYSTEM:SERVER_CREATE_CHARGE_FIRST_HOUR')->update(['key' => 'SERVER_CREATE_CHARGE_FIRST_HOUR']);
+        DB::table('configurations')->where('key', 'SETTINGS::SERVER:ALLOCATION_LIMIT')->update(['key' => 'ALLOCATION_LIMIT']);
+        DB::table('configurations')->where('key', 'SETTINGS::SERVER:CREDITS_DISPLAY_NAME')->update(['key' => 'SETTINGS::SYSTEM:CREDITS_DISPLAY_NAME']);
+        DB::table('configurations')->where('key', 'SETTINGS::PAYMENTS:SALES_TAX')->update(['key' => 'SALES_TAX']);
+    }
+}

+ 34 - 0
database/migrations/2022_01_14_234418_update_settings_table_allow_nullable.php

@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class UpdateSettingsTableAllowNullable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        //allow value column in settings table to be nullable
+        Schema::table('settings', function (Blueprint $table) {
+            $table->string('value')->nullable()->change();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        //disallow value column in settings table to be nullable
+        Schema::table('settings', function (Blueprint $table) {
+            $table->string('value')->nullable(false)->change();
+        });
+    }
+}

+ 2 - 2
database/seeders/DatabaseSeeder.php

@@ -2,7 +2,7 @@
 
 namespace Database\Seeders;
 
-use Database\Seeders\Seeds\ConfigurationSeeder;
+use Database\Seeders\Seeds\SettingsSeeder;
 use Illuminate\Database\Seeder;
 
 class DatabaseSeeder extends Seeder
@@ -15,7 +15,7 @@ class DatabaseSeeder extends Seeder
     public function run()
     {
         $this->call([
-            ConfigurationSeeder::class,
+            SettingsSeeder::class,
         ]);
 
     }

+ 2 - 2
database/seeders/ExampleItemsSeeder.php

@@ -3,7 +3,7 @@
 namespace Database\Seeders;
 
 use Database\Seeders\Seeds\ProductSeeder;
-use Database\Seeders\Seeds\PaypalProductSeeder;
+use Database\Seeders\Seeds\CreditProductSeeder;
 use Database\Seeders\Seeds\ApplicationApiSeeder;
 use Database\Seeders\Seeds\UsefulLinksSeeder;
 use Illuminate\Database\Seeder;
@@ -19,7 +19,7 @@ class ExampleItemsSeeder extends Seeder
     {
         $this->call([
             ProductSeeder::class,
-            PaypalProductSeeder::class,
+            CreditProductSeeder::class,
             ApplicationApiSeeder::class,
             UsefulLinksSeeder::class
         ]);

+ 0 - 149
database/seeders/Seeds/ConfigurationSeeder.php

@@ -1,149 +0,0 @@
-<?php
-
-namespace Database\Seeders\Seeds;
-
-use App\Models\Configuration;
-use Illuminate\Database\Seeder;
-
-class ConfigurationSeeder extends Seeder
-{
-    /**
-     * Run the database seeds.
-     *
-     * @return void
-     */
-    public function run()
-    {
-        //initials
-        Configuration::firstOrCreate([
-            'key' => 'INITIAL_CREDITS',
-        ], [
-            'value'       => '250',
-            'type'        => 'integer',
-            'description' => 'The initial amount of credits the user starts with.'
-        ]);
-
-        Configuration::firstOrCreate([
-            'key' => 'INITIAL_SERVER_LIMIT',
-        ], [
-            'value'       => '1',
-            'type'        => 'integer',
-            'description' => 'The initial server limit the user starts with.'
-        ]);
-
-        //verify email event
-        Configuration::firstOrCreate([
-            'key' => 'CREDITS_REWARD_AFTER_VERIFY_EMAIL',
-        ], [
-            'value'       => '250',
-            'type'        => 'integer',
-            'description' => 'Increase in credits after the user has verified their email account.'
-        ]);
-
-        Configuration::firstOrCreate([
-            'key' => 'SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL',
-        ], [
-            'value'       => '2',
-            'type'        => 'integer',
-            'description' => 'Increase in server limit after the user has verified their email account.'
-        ]);
-
-        //verify discord event
-        Configuration::firstOrCreate([
-            'key' => 'CREDITS_REWARD_AFTER_VERIFY_DISCORD',
-        ], [
-            'value'       => '375',
-            'type'        => 'integer',
-            'description' => 'Increase in credits after the user has verified their discord account.'
-        ]);
-
-        Configuration::firstOrCreate([
-            'key' => 'SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD',
-        ], [
-            'value'       => '2',
-            'type'        => 'integer',
-            'description' => 'Increase in server limit after the user has verified their discord account.'
-        ]);
-
-        //other
-        Configuration::firstOrCreate([
-            'key' => 'MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER',
-        ], [
-            'value'       => '50',
-            'type'        => 'integer',
-            'description' => 'The minimum amount of credits the user would need to make a server.'
-        ]);
-
-        //purchasing
-        Configuration::firstOrCreate([
-            'key' => 'SERVER_LIMIT_AFTER_IRL_PURCHASE',
-        ], [
-            'value'       => '10',
-            'type'        => 'integer',
-            'description' => 'updates the users server limit to this amount (unless the user already has a higher server limit) after making a purchase with real money, set to 0 to ignore this.',
-        ]);
-
-
-        //force email and discord verification
-        Configuration::firstOrCreate([
-            'key' => 'FORCE_EMAIL_VERIFICATION',
-        ], [
-            'value'       => 'false',
-            'type'        => 'boolean',
-            'description' => 'Force an user to verify the email adress before creating a server / buying credits.'
-        ]);
-
-        Configuration::firstOrCreate([
-            'key' => 'FORCE_DISCORD_VERIFICATION',
-        ], [
-            'value'       => 'false',
-            'type'        => 'boolean',
-            'description' => 'Force an user to link an Discord Account before creating a server / buying credits.'
-        ]);
-
-        //disable ip check on register
-        Configuration::firstOrCreate([
-            'key' => 'REGISTER_IP_CHECK',
-        ], [
-            'value'       => 'true',
-            'type'        => 'boolean',
-            'description' => 'Prevent users from making multiple accounts using the same IP address'
-        ]);
-
-        //per_page on allocations request
-        Configuration::firstOrCreate([
-            'key' => 'ALLOCATION_LIMIT',
-        ], [
-            'value'       => '200',
-            'type'        => 'integer',
-            'description' => 'The maximum amount of allocations to pull per node for automatic deployment, if more allocations are being used than this limit is set to, no new servers can be created!'
-        ]);
-
-        //credits display name
-        Configuration::firstOrCreate([
-            'key' => 'CREDITS_DISPLAY_NAME',
-        ], [
-            'value'       => 'Credits',
-            'type'        => 'string',
-            'description' => 'Set the display name of your currency :)'
-        ]);
-
-        //credits display name
-        Configuration::firstOrCreate([
-            'key' => 'SERVER_CREATE_CHARGE_FIRST_HOUR',
-        ], [
-            'value'       => 'true',
-            'type'        => 'boolean',
-            'description' => 'Charges the first hour worth of credits upon creating a server.'
-        ]);
-        //sales tax
-        Configuration::firstOrCreate([
-            'key'   => 'SALES_TAX',
-        ], [
-            'value' => '0',
-            'type'  => 'integer',
-            'description'  => 'The %-value of tax that will be added to the product price on checkout'
-        ]);
-
-    }
-}

+ 6 - 6
database/seeders/Seeds/PaypalProductSeeder.php → database/seeders/Seeds/CreditProductSeeder.php

@@ -2,10 +2,10 @@
 
 namespace Database\Seeders\Seeds;
 
-use App\Models\PaypalProduct;
+use App\Models\CreditProduct;
 use Illuminate\Database\Seeder;
 
-class PaypalProductSeeder extends Seeder
+class CreditProductSeeder extends Seeder
 {
     /**
      * Run the database seeds.
@@ -14,7 +14,7 @@ class PaypalProductSeeder extends Seeder
      */
     public function run()
     {
-        PaypalProduct::create([
+        CreditProduct::create([
             'type' => 'Credits',
             'display' => '350',
             'description' => 'Adds 350 credits to your account',
@@ -24,7 +24,7 @@ class PaypalProductSeeder extends Seeder
             'disabled' => false,
         ]);
 
-        PaypalProduct::create([
+        CreditProduct::create([
             'type' => 'Credits',
             'display' => '875 + 125',
             'description' => 'Adds 1000 credits to your account',
@@ -34,7 +34,7 @@ class PaypalProductSeeder extends Seeder
             'disabled' => false,
         ]);
 
-        PaypalProduct::create([
+        CreditProduct::create([
             'type' => 'Credits',
             'display' => '1750 + 250',
             'description' => 'Adds 2000 credits to your account',
@@ -44,7 +44,7 @@ class PaypalProductSeeder extends Seeder
             'disabled' => false,
         ]);
 
-        PaypalProduct::create([
+        CreditProduct::create([
             'type' => 'Credits',
             'display' => '3500 + 500',
             'description' => 'Adds 4000 credits to your account',

+ 469 - 0
database/seeders/Seeds/SettingsSeeder.php

@@ -0,0 +1,469 @@
+<?php
+
+namespace Database\Seeders\Seeds;
+
+use App\Models\Settings;
+use Illuminate\Database\Seeder;
+
+class SettingsSeeder extends Seeder
+{
+    /**
+     * Run the database seeds.
+     *
+     * @return void
+     */
+    public function run()
+    {
+        //initials
+        Settings::firstOrCreate([
+            'key' => 'SETTINGS::USER:INITIAL_CREDITS',
+        ], [
+            'value'       => '250',
+            'type'        => 'integer',
+            'description' => 'The initial amount of credits the user starts with.'
+        ]);
+
+        Settings::firstOrCreate([
+            'key' => 'SETTINGS::USER:NITIAL_SERVER_LIMIT',
+        ], [
+            'value'       => '1',
+            'type'        => 'integer',
+            'description' => 'The initial server limit the user starts with.'
+        ]);
+
+        //verify email event
+        Settings::firstOrCreate([
+            'key' => 'SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_EMAIL',
+        ], [
+            'value'       => '250',
+            'type'        => 'integer',
+            'description' => 'Increase in credits after the user has verified their email account.'
+        ]);
+
+        Settings::firstOrCreate([
+            'key' => 'SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_EMAIL',
+        ], [
+            'value'       => '2',
+            'type'        => 'integer',
+            'description' => 'Increase in server limit after the user has verified their email account.'
+        ]);
+
+        //verify discord event
+        Settings::firstOrCreate([
+            'key' => 'SETTINGS::USER:CREDITS_REWARD_AFTER_VERIFY_DISCORD',
+        ], [
+            'value'       => '375',
+            'type'        => 'integer',
+            'description' => 'Increase in credits after the user has verified their discord account.'
+        ]);
+
+        Settings::firstOrCreate([
+            'key' => 'SETTINGS::USER:SERVER_LIMIT_REWARD_AFTER_VERIFY_DISCORD',
+        ], [
+            'value'       => '2',
+            'type'        => 'integer',
+            'description' => 'Increase in server limit after the user has verified their discord account.'
+        ]);
+
+        //other
+        Settings::firstOrCreate([
+            'key' => 'SETTINGS::USER:MINIMUM_REQUIRED_CREDITS_TO_MAKE_SERVER',
+        ], [
+            'value'       => '50',
+            'type'        => 'integer',
+            'description' => 'The minimum amount of credits the user would need to make a server.'
+        ]);
+
+        //purchasing
+        Settings::firstOrCreate([
+            'key' => 'SETTINGS::USER:SERVER_LIMIT_AFTER_IRL_PURCHASE',
+        ], [
+            'value'       => '10',
+            'type'        => 'integer',
+            'description' => 'updates the users server limit to this amount (unless the user already has a higher server limit) after making a purchase with real money, set to 0 to ignore this.',
+        ]);
+
+
+        //force email and discord verification
+        Settings::firstOrCreate([
+            'key' => 'SETTINGS::USER:FORCE_EMAIL_VERIFICATION',
+        ], [
+            'value'       => 'false',
+            'type'        => 'boolean',
+            'description' => 'Force an user to verify the email adress before creating a server / buying credits.'
+        ]);
+
+        Settings::firstOrCreate([
+            'key' => 'SETTINGS::USER:FORCE_DISCORD_VERIFICATION',
+        ], [
+            'value'       => 'false',
+            'type'        => 'boolean',
+            'description' => 'Force an user to link an Discord Account before creating a server / buying credits.'
+        ]);
+
+        //disable ip check on register
+        Settings::firstOrCreate([
+            'key' => 'SETTINGS::SYSTEM:REGISTER_IP_CHECK',
+        ], [
+            'value'       => 'true',
+            'type'        => 'boolean',
+            'description' => 'Prevent users from making multiple accounts using the same IP address'
+        ]);
+
+        //per_page on allocations request
+        Settings::firstOrCreate([
+            'key' => 'SETTINGS::SERVER:ALLOCATION_LIMIT',
+        ], [
+            'value'       => '200',
+            'type'        => 'integer',
+            'description' => 'The maximum amount of allocations to pull per node for automatic deployment, if more allocations are being used than this limit is set to, no new servers can be created!'
+        ]);
+
+        //credits display name
+        Settings::firstOrCreate([
+            'key' => 'SETTINGS::SYSTEM:CREDITS_DISPLAY_NAME',
+        ], [
+            'value'       => 'Credits',
+            'type'        => 'string',
+            'description' => 'Set the display name of your currency :)'
+        ]);
+
+        //credits display name
+        Settings::firstOrCreate([
+            'key' => 'SETTINGS::SYSTEM:SERVER_CREATE_CHARGE_FIRST_HOUR',
+        ], [
+            'value'       => 'true',
+            'type'        => 'boolean',
+            'description' => 'Charges the first hour worth of credits upon creating a server.'
+        ]);
+        //sales tax
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::PAYMENTS:SALES_TAX',
+        ], [
+            'value' => '0',
+            'type'  => 'integer',
+            'description'  => 'The %-value of tax that will be added to the product price on checkout'
+        ]);
+        //Invoices enabled
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::INVOICE:ENABLED',
+        ], [
+            'value' => 'false',
+            'type'  => 'boolean',
+            'description'  => 'Enables or disables the invoice feature for payments'
+        ]);
+        //Invoice company name
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::INVOICE:COMPANY_NAME',
+        ], [
+            'value' => '',
+            'type'  => 'string',
+            'description'  => 'The name of the Company on the Invoices'
+        ]);
+        //Invoice company address
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::INVOICE:COMPANY_ADDRESS',
+        ], [
+            'value' => '',
+            'type'  => 'string',
+            'description'  => 'The address of the Company on the Invoices'
+        ]);
+        //Invoice company phone
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::INVOICE:COMPANY_PHONE',
+        ], [
+            'value' => '',
+            'type'  => 'string',
+            'description'  => 'The phone number of the Company on the Invoices'
+        ]);
+
+        //Invoice company mail
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::INVOICE:COMPANY_MAIL',
+        ], [
+            'value' => '',
+            'type'  => 'string',
+            'description'  => 'The email address of the Company on the Invoices'
+        ]);
+
+        //Invoice VAT
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::INVOICE:COMPANY_VAT',
+        ], [
+            'value' => '',
+            'type'  => 'string',
+            'description'  => 'The VAT-Number of the Company on the Invoices'
+        ]);
+
+        //Invoice Website
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::INVOICE:COMPANY_WEBSITE',
+        ], [
+            'value' => '',
+            'type'  => 'string',
+            'description'  => 'The Website of the Company on the Invoices'
+        ]);
+
+        //Invoice Website
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::INVOICE:PREFIX',
+        ], [
+            'value' => 'INV',
+            'type'  => 'string',
+            'description'  => 'The invoice prefix'
+        ]);
+
+        //Locale
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::LOCALE:DEFAULT',
+        ], [
+            'value' => 'en',
+            'type'  => 'string',
+            'description'  => 'The default Language the dashboard will be shown in'
+        ]);
+        //Dynamic locale
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::LOCALE:DYNAMIC',
+        ], [
+            'value' => 'false',
+            'type'  => 'boolean',
+            'description'  => 'If this is true, the Language will change to the Clients browserlanguage or default.'
+        ]);
+        //User can change Locale
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::LOCALE:CLIENTS_CAN_CHANGE',
+        ], [
+            'value' => 'false',
+            'type'  => 'boolean',
+            'description'  => 'If this is true, the clients will be able to change their Locale.'
+        ]);
+        //Locale
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::LOCALE:AVAILABLE',
+        ], [
+            'value' => '',
+            'type'  => 'string',
+            'description'  => 'The available languages'
+        ]);
+        //Locale
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::LOCALE:DATATABLES',
+        ], [
+            'value' => 'en-gb',
+            'type'  => 'string',
+            'description'  => 'The Language of the Datatables. Grab the Language-Codes from here https://datatables.net/plug-ins/i18n/'
+        ]);
+
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::PAYMENTS:PAYPAL:SECRET',
+        ], [
+            'value' => env('PAYPAL_SECRET', ''),
+            'type'  => 'string',
+            'description'  => 'Your PayPal Secret-Key ( https://developer.paypal.com/docs/integration/direct/rest/)'
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::PAYMENTS:PAYPAL:CLIENT_ID',
+        ], [
+            'value' => env('PAYPAL_CLIENT_ID', ''),
+            'type'  => 'string',
+            'description'  => 'Your PayPal Client_ID'
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::PAYMENTS:PAYPAL:SANDBOX_SECRET',
+        ], [
+            'value' => env('PAYPAL_SANDBOX_SECRET', ''),
+            'type'  => 'string',
+            'description'  => 'Your PayPal SANDBOX Secret-Key used for testing '
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::PAYMENTS:PAYPAL:SANDBOX_CLIENT_ID',
+        ], [
+            'value' => env('PAYPAL_SANDBOX_CLIENT_ID', ''),
+            'type'  => 'string',
+            'description'  => 'Your PayPal SANDBOX Client-ID used for testing '
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::PAYMENTS:STRIPE:SECRET',
+        ], [
+            'value' => env('STRIPE_SECRET', ''),
+            'type'  => 'string',
+            'description'  => 'Your Stripe  Secret-Key  ( https://dashboard.stripe.com/account/apikeys )'
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::PAYMENTS:STRIPE:ENDPOINT_SECRET',
+        ], [
+            'value' => env('STRIPE_ENDPOINT_SECRET', ''),
+            'type'  => 'string',
+            'description'  => 'Your Stripe endpoint secret-key'
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::PAYMENTS:STRIPE:TEST_SECRET',
+        ], [
+            'value' => env('STRIPE_TEST_SECRET', ''),
+            'type'  => 'string',
+            'description'  => 'Your Stripe test secret-key'
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::PAYMENTS:STRIPE:ENDPOINT_TEST_SECRET',
+        ], [
+            'value' => env('STRIPE_ENDPOINT_TEST_SECRET', ''),
+            'type'  => 'string',
+            'description'  => 'Your Stripe endpoint test secret-key'
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::PAYMENTS:STRIPE:METHODS',
+        ], [
+            'value' => env('STRIPE_METHODS', 'card,sepa_debit'),
+            'type'  => 'string',
+            'description'  => 'Comma seperated list of payment methods that are enabled (https://stripe.com/docs/payments/payment-methods/integration-options)'
+        ]);
+
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::DISCORD:CLIENT_ID',
+        ], [
+            'value' => env('DISCORD_CLIENT_ID', ''),
+            'type'  => 'string',
+            'description'  => 'Discord API Credentials - https://discordapp.com/developers/applications/'
+        ]);
+
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::DISCORD:CLIENT_SECRET',
+        ], [
+            'value' => env('DISCORD_CLIENT_SECRET', ''),
+            'type'  => 'string',
+            'description'  => 'Discord API Credentials - https://discordapp.com/developers/applications/'
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::DISCORD:BOT_TOKEN',
+        ], [
+            'value' =>  env('DISCORD_BOT_TOKEN', ''),
+            'type'  => 'string',
+            'description'  => 'Discord API Credentials - https://discordapp.com/developers/applications/'
+        ]);
+
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::DISCORD:GUILD_ID',
+        ], [
+            'value' =>  env('DISCORD_GUILD_ID', ''),
+            'type'  => 'string',
+            'description'  => 'Discord API Credentials - https://discordapp.com/developers/applications/'
+        ]);
+
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::DISCORD:ROLE_ID',
+        ], [
+            'value' => env('DISCORD_ROLE_ID', ''),
+            'type'  => 'string',
+            'description'  => 'Discord role that will be assigned to users when they register'
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::DISCORD:INVITE_URL',
+        ], [
+            'value' => env('DISCORD_INVITE_URL', ''),
+            'type'  => 'string',
+            'description'  => 'The invite URL to your Discord Server'
+        ]);
+
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::SYSTEM:PTERODACTYL:TOKEN',
+        ], [
+            'value' => env('PTERODACTYL_TOKEN', ''),
+            'type'  => 'string',
+            'description'  => 'Admin API Token from Pterodactyl Panel - necessary for the Panel to work. The Key needs all read&write permissions!'
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::SYSTEM:PTERODACTYL:URL',
+        ], [
+            'value' => env('PTERODACTYL_URL', ''),
+            'type'  => 'string',
+            'description'  => 'The URL to your Pterodactyl Panel. Must not end with a / '
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::MISC:PHPMYADMIN:URL',
+        ], [
+            'value' => env('PHPMYADMIN_URL', ''),
+            'type'  => 'string',
+            'description'  => 'The URL to your PHPMYADMIN Panel. Must not end with a /, remove to remove database button'
+        ]);
+
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::RECAPTCHA:SITE_KEY',
+        ], [
+            'value' => env('RECAPTCHA_SITE_KEY', '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'),
+            'type'  => 'string',
+            'description'  => 'Google Recaptcha API Credentials - https://www.google.com/recaptcha/admin - reCaptcha V2 (not v3)'
+        ]);
+
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::RECAPTCHA:SECRET_KEY',
+        ], [
+            'value' => env('RECAPTCHA_SECRET_KEY', '6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe'),
+            'type'  => 'string',
+            'description'  => 'Google Recaptcha API Credentials - https://www.google.com/recaptcha/admin - reCaptcha V2 (not v3)'
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::RECAPTCHA:ENABLED',
+        ], [
+            'value' => 'true',
+            'type'  => 'boolean',
+            'description'  => 'Enables or disables the ReCaptcha feature on the registration/login page'
+
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::MAIL:MAILER',
+        ], [
+            'value' => env('MAIL_MAILER', 'smtp'),
+            'type'  => 'string',
+            'description'  => 'Selected Mailer (smtp, mailgun, sendgrid, mailtrap)'
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::MAIL:HOST',
+        ], [
+            'value' => env('MAIL_HOST', 'localhost'),
+            'type'  => 'string',
+            'description'  => 'Mailer Host Adress'
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::MAIL:PORT',
+        ], [
+            'value' =>  env('MAIL_PORT', '25'),
+            'type'  => 'string',
+            'description'  => 'Mailer Server Port'
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::MAIL:USERNAME',
+        ], [
+            'value' =>  env('MAIL_USERNAME', ''),
+            'type'  => 'string',
+            'description'  => 'Mailer Username'
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::MAIL:PASSWORD',
+        ], [
+            'value' =>  env('MAIL_PASSWORD', ''),
+            'type'  => 'string',
+            'description'  => 'Mailer Password'
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::MAIL:ENCRYPTION',
+        ], [
+            'value' =>  env('MAIL_ENCRYPTION', 'tls'),
+            'type'  => 'string',
+            'description'  => 'Mailer Encryption (tls, ssl)'
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::MAIL:FROM_ADDRESS',
+        ], [
+            'value' =>  env('MAIL_FROM_ADDRESS', ''),
+            'type'  => 'string',
+            'description'  => 'Mailer From Address'
+        ]);
+        Settings::firstOrCreate([
+            'key'   => 'SETTINGS::MAIL:FROM_NAME',
+        ], [
+            'value' => env('APP_NAME', 'Controlpanel'),
+            'type'  => 'string',
+            'description'  => 'Mailer From Name'
+        ]);
+    }
+}

+ 1 - 1
package-lock.json

@@ -1,5 +1,5 @@
 {
-    "name": "bitsec-dashboard",
+    "name": "controllpanelgg",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {

File diff suppressed because it is too large
+ 17 - 1
public/css/app.css


BIN
public/images/discord_logo.png


BIN
public/images/paypal_logo.png


BIN
public/images/stripe_logo.png


+ 48 - 0
public/install/dotenv.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace DevCoder;
+
+class DotEnv
+{
+    /**
+     * The directory where the .env file can be located.
+     *
+     * @var string
+     */
+    protected $path;
+
+
+    public function __construct(string $path)
+    {
+        if (!file_exists($path)) {
+            throw new \InvalidArgumentException(sprintf('%s does not exist', $path));
+        }
+        $this->path = $path;
+    }
+
+    public function load(): void
+    {
+        if (!is_readable($this->path)) {
+            throw new \RuntimeException(sprintf('%s file is not readable', $this->path));
+        }
+
+        $lines = file($this->path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
+        foreach ($lines as $line) {
+
+            if (strpos(trim($line), '#') === 0) {
+                continue;
+            }
+
+            list($name, $value) = explode('=', $line, 2);
+            $name = trim($name);
+            $value = trim($value);
+
+            if (!array_key_exists($name, $_SERVER) && !array_key_exists($name, $_ENV)) {
+                putenv(sprintf('%s=%s', $name, $value));
+                $_ENV[$name] = $value;
+                $_SERVER[$name] = $value;
+            }
+        }
+    }
+}
+

+ 281 - 0
public/install/forms.php

@@ -0,0 +1,281 @@
+<?php
+
+use DevCoder\DotEnv;
+use PHPMailer\PHPMailer\Exception;
+use PHPMailer\PHPMailer\PHPMailer;
+
+require 'dotenv.php';
+require 'phpmailer/Exception.php';
+require 'phpmailer/PHPMailer.php';
+require 'phpmailer/SMTP.php';
+
+
+(new DotEnv(dirname(__FILE__, 3) . "/.env"))->load();
+
+include("functions.php");
+
+if (isset($_POST['checkDB'])) {
+
+    $values = [
+        //SETTINGS::VALUE => REQUEST-VALUE (coming from the html-form)
+        "DB_HOST" => "databasehost",
+        "DB_DATABASE" => "database",
+        "DB_USERNAME" => "databaseuser",
+        "DB_PASSWORD" => "databaseuserpass",
+        "DB_PORT" => "databaseport",
+        "DB_CONNECTION" => "databasedriver"
+    ];
+
+
+    $db = new mysqli($_POST["databasehost"], $_POST["databaseuser"], $_POST["databaseuserpass"], $_POST["database"], $_POST["databaseport"]);
+    if ($db->connect_error) {
+        header("LOCATION: index.php?step=2&message=Could not connect to the Database");
+        die();
+    }
+
+    foreach ($values as $key => $value) {
+        $param = $_POST[$value];
+       # if ($key == "DB_PASSWORD") {
+        #    $param = '"' . $_POST[$value] . '"';
+       # }
+        setEnvironmentValue($key, $param);
+    }
+
+    header("LOCATION: index.php?step=2.5");
+
+}
+
+
+if (isset($_POST['checkGeneral'])) {
+
+
+    $appname = '"' . $_POST['name'] . '"';
+    $appurl = $_POST['url'];
+
+    if (substr($appurl, -1) === "/") {
+        $appurl = substr_replace($appurl, "", -1);
+    }
+
+
+    setEnvironmentValue("APP_NAME", $appname);
+    setEnvironmentValue("APP_URL", $url);
+
+    header("LOCATION: index.php?step=4");
+
+}
+
+if (isset($_POST['feedDB'])) {
+    $logs = "";
+
+    #$logs .= run_console(putenv('COMPOSER_HOME=' . dirname(__FILE__, 3) . '/vendor/bin/composer'));
+    #$logs .= run_console('composer install --no-dev --optimize-autoloader');
+    $logs .= run_console('php artisan migrate --seed --force');
+    $logs .= run_console('php artisan db:seed --class=ExampleItemsSeeder --force');
+    $logs .= run_console('php artisan key:generate --force');
+    $logs .= run_console('php artisan storage:link');
+
+    $logsfile = fopen("logs.txt", "w") or die("Unable to open file!");
+    fwrite($logsfile, $logs);
+    fclose($logsfile);
+
+    if (strpos(getEnvironmentValue("APP_KEY"), 'base64') !== false) {
+        header("LOCATION: index.php?step=3");
+    } else {
+        header("LOCATION: index.php?step=2.5&message=There was an error. Please check install/logs.txt !");
+    }
+
+
+}
+
+if (isset($_POST['checkSMTP'])) {
+
+    try {
+        $mail = new PHPMailer(true);
+
+        //Server settings
+        $mail->isSMTP();                                            // Send using SMTP
+        $mail->Host = $_POST['host'];                    // Set the SMTP server to send through
+        $mail->SMTPAuth = true;                                   // Enable SMTP authentication
+        $mail->Username = $_POST['user'];                     // SMTP username
+        $mail->Password = $_POST['pass'];                               // SMTP password
+        $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;         // Enable TLS encryption; `PHPMailer::ENCRYPTION_SMTPS` encouraged
+        $mail->Port = $_POST['port'];                                    // TCP port to connect to, use 465 for `PHPMailer::ENCRYPTION_SMTPS`
+
+        //Recipients
+        $mail->setFrom($_POST['user'], $_POST['user']);
+        $mail->addAddress($_POST['user'], $_POST['user']);     // Add a recipient
+
+        // Content
+        $mail->isHTML(true);                                  // Set email format to HTML
+        $mail->Subject = 'It Worked!';
+        $mail->Body = "Your E-Mail Settings are correct!";
+
+
+        $mail->send();
+    } catch (Exception $e) {
+        header("LOCATION: index.php?step=4&message=Something wasnt right when sending the E-Mail!");
+        die();
+    }
+
+    $db = new mysqli(getEnvironmentValue("DB_HOST"), getEnvironmentValue("DB_USERNAME"), getEnvironmentValue("DB_PASSWORD"), getEnvironmentValue("DB_DATABASE"), getEnvironmentValue("DB_PORT"));
+    if ($db->connect_error) {
+        header("LOCATION: index.php?step=4&message=Could not connect to the Database");
+    die();
+    }
+    $values = [
+        "SETTINGS::MAIL:MAILER" => $_POST["method"],
+        "SETTINGS::MAIL:HOST" => $_POST["host"],
+        "SETTINGS::MAIL:PORT" => $_POST["port"],
+        "SETTINGS::MAIL:USERNAME" => $_POST["user"],
+        "SETTINGS::MAIL:PASSWORD" => $_POST["pass"],
+        "SETTINGS::MAIL:ENCRYPTION" => $_POST["encryption"],
+        "SETTINGS::MAIL:FROM_ADDRESS" => $_POST["user"]
+    ];
+
+    foreach ($values as $key => $value) {
+        $query = "UPDATE `" . getEnvironmentValue("DB_DATABASE") . "`.`settings` SET `value` = '$value' WHERE (`key` = '$key')";
+        $db->query($query);
+    }
+
+    header("LOCATION: index.php?step=5");
+
+
+}
+
+if (isset($_POST['checkPtero'])) {
+    $url = $_POST['url'];
+    $key = $_POST['key'];
+
+    if (substr($url, -1) === "/") {
+        $url = substr_replace($url, "", -1);
+    }
+
+
+    $pteroURL = $url . "/api/application/users";
+    $ch = curl_init();
+
+    curl_setopt($ch, CURLOPT_URL, $pteroURL);
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+    curl_setopt($ch, CURLOPT_HTTPHEADER, array(
+        "Accept: application/json",
+        "Content-Type: application/json",
+        "Authorization: Bearer " . $key
+    ));
+    $response = curl_exec($ch);
+    $result = json_decode($response, true);
+    curl_close($ch); // Close the connection
+
+
+    if (!is_array($result) or in_array($result["errors"][0]["code"], $result)) {
+        header("LOCATION: index.php?step=5&message=Couldnt connect to Pterodactyl. Make sure your API key has all read and write permissions!");
+        die();
+    } else {
+
+        $query1 = "UPDATE `" . getEnvironmentValue("DB_DATABASE") . "`.`settings` SET `value` = '$url' WHERE (`key` = 'SETTINGS::SYSTEM:PTERODACTYL:URL')";
+        $query2 = "UPDATE `" . getEnvironmentValue("DB_DATABASE") . "`.`settings` SET `value` = '$key' WHERE (`key` = 'SETTINGS::SYSTEM:PTERODACTYL:TOKEN')";
+
+
+        $db = new mysqli(getEnvironmentValue("DB_HOST"), getEnvironmentValue("DB_USERNAME"), getEnvironmentValue("DB_PASSWORD"), getEnvironmentValue("DB_DATABASE"), getEnvironmentValue("DB_PORT"));
+        if ($db->connect_error) {
+            header("LOCATION: index.php?step=5&message=Could not connect to the Database");
+            die();
+        }
+
+        if ($db->query($query1) && $db->query($query2)) {
+            header("LOCATION: index.php?step=6");
+        } else {
+            header("LOCATION: index.php?step=5&message=Something went wrong when communicating with the Database!");
+        }
+    }
+
+
+}
+
+if (isset($_POST['createUser'])) {
+    $db = new mysqli(getEnvironmentValue("DB_HOST"), getEnvironmentValue("DB_USERNAME"), getEnvironmentValue("DB_PASSWORD"), getEnvironmentValue("DB_DATABASE"), getEnvironmentValue("DB_PORT"));
+    if ($db->connect_error) {
+        header("LOCATION: index.php?step=6&message=Could not connect to the Database");
+        die();
+    }
+
+
+    $name = $_POST['user'];
+    $mail = $_POST['email'];
+    $pteroID = $_POST['pteroID'];
+    $pass = $_POST['pass'];
+    $repass = $_POST['repass'];
+
+    $key = $db->query("SELECT `value` FROM `" . getEnvironmentValue("DB_DATABASE") . "`.`settings` WHERE `key` = 'SETTINGS::SYSTEM:PTERODACTYL:TOKEN'")->fetch_assoc();
+    $pterobaseurl = $db->query("SELECT `value` FROM `" . getEnvironmentValue("DB_DATABASE") . "`.`settings` WHERE `key` = 'SETTINGS::SYSTEM:PTERODACTYL:URL'")->fetch_assoc();
+
+
+
+    $pteroURL = $pterobaseurl["value"] . "/api/application/users/" . $pteroID;
+    $ch = curl_init();
+
+    curl_setopt($ch, CURLOPT_URL, $pteroURL);
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+    curl_setopt($ch, CURLOPT_HTTPHEADER, array(
+        "Accept: application/json",
+        "Content-Type: application/json",
+        "Authorization: Bearer " . $key["value"]
+    ));
+    $response = curl_exec($ch);
+    $result = json_decode($response, true);
+    curl_close($ch); // Close the connection
+
+
+    if ($result["attributes"]["email"] !== $mail) {
+        header("LOCATION: index.php?step=6&message=The Email is not the same as the one used on Pterodactyl");
+        die();
+    }
+    if ($pass !== $repass) {
+        header("LOCATION: index.php?step=6&message=The Passwords did not match!");
+        die();
+    }
+
+    $pass = password_hash($pass, PASSWORD_DEFAULT);
+
+    $pteroURL = $pterobaseurl["value"] . "/api/application/users/" . $pteroID;
+    $ch = curl_init();
+
+    curl_setopt($ch, CURLOPT_URL, $pteroURL);
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+    curl_setopt($ch, CURLOPT_HTTPHEADER, array(
+        "Accept: application/json",
+        "Content-Type: application/json",
+        "Authorization: Bearer " . $key["value"]
+    ));
+    curl_setopt($ch, CURLOPT_POSTFIELDS, array(
+        "email" => $mail,
+        "username" => $name,
+        "first_name" => $name,
+        "last_name" => $name,
+        "password" => $pass
+    ));
+    $response = curl_exec($ch);
+    $result = json_decode($response, true);
+    curl_close($ch); // Close the connection
+
+    if (!is_array($result) or in_array($result["errors"][0]["code"], $result)) {
+        header("LOCATION: index.php?step=5&message=Couldnt connect to Pterodactyl. Make sure your API key has all read and write permissions!");
+        die();
+    }
+
+    $query1 = "INSERT INTO `" . getEnvironmentValue("DB_DATABASE") . "`.`users` (`name`, `role`, `credits`, `server_limit`, `pterodactyl_id`, `email`, `password`, `created_at`) VALUES ('$name', 'admin', '250', '1', '$pteroID', '$mail', '$pass', CURRENT_TIMESTAMP)";
+
+
+
+    if ($db->query($query1)) {
+        header("LOCATION: index.php?step=7");
+    } else {
+
+        header("LOCATION: index.php?step=6&message=Something went wrong when communicating with the Database!");
+
+    }
+
+
+}
+
+
+?>

+ 133 - 0
public/install/functions.php

@@ -0,0 +1,133 @@
+<?php
+
+
+$required_extentions = array("openssl", "gd", "mysql", "PDO", "mbstring", "tokenizer", "bcmath", "xml", "curl", "zip", "fpm");
+
+$requirements = [
+    "php" => "7.4",
+    "mysql" => "5.7.22",
+];
+
+function checkPhpVersion()
+{
+    global $requirements;
+    if (version_compare(phpversion(), $requirements["php"], '>=')) {
+        return "OK";
+    }
+    return "not OK";
+}
+function checkWriteable()
+{
+    return is_writable("../../.env");
+}
+function checkHTTPS()
+{
+    return
+        (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
+        || $_SERVER['SERVER_PORT'] == 443;
+}
+
+function getMySQLVersion()
+{
+    global $requirements;
+
+    $output = shell_exec('mysql -V');
+    preg_match('@[0-9]+\.[0-9]+\.[0-9]+@', $output, $version);
+
+    $versionoutput = $version[0] ?? "0";
+
+    return (intval($versionoutput) > intval($requirements["mysql"]) ? "OK" : $versionoutput);
+}
+
+function getZipVersion()
+{
+
+    $output = shell_exec('zip  -v');
+    preg_match('@[0-9]+\.[0-9]+\.[0-9]+@', $output, $version);
+
+    $versionoutput = $version[0] ?? 0;
+
+    return ($versionoutput != 0 ? "OK" : "not OK");
+}
+
+function getGitVersion()
+{
+
+    $output = shell_exec('git  --version');
+    preg_match('@[0-9]+\.[0-9]+\.[0-9]+@', $output, $version);
+
+    $versionoutput = $version[0] ?? 0;
+
+    return ($versionoutput != 0 ? "OK" : "not OK");
+}
+
+function getTarVersion()
+{
+
+    $output = shell_exec('tar  --version');
+    preg_match('@[0-9]+\.[0-9]+@', $output, $version);
+
+    $versionoutput = $version[0] ?? 0;
+
+    return ($versionoutput != 0 ? "OK" : "not OK");
+}
+
+function checkExtensions()
+{
+    global $required_extentions;
+
+    $not_ok = [];
+    $extentions = get_loaded_extensions();
+
+    foreach ($required_extentions as $ext) {
+        if (!preg_grep("/^(?=.*" . $ext . ").*$/", $extentions))
+            array_push($not_ok, $ext);
+    }
+    return $not_ok;
+
+}
+
+function setEnvironmentValue($envKey, $envValue)
+{
+
+    $envFile = dirname(__FILE__, 3) . "/.env";
+    $str = file_get_contents($envFile);
+
+    $str .= "\n"; // In case the searched variable is in the last line without \n
+    $keyPosition = strpos($str, "{$envKey}=");
+    $endOfLinePosition = strpos($str, PHP_EOL, $keyPosition);
+    $oldLine = substr($str, $keyPosition, $endOfLinePosition - $keyPosition);
+    $str = str_replace($oldLine, "{$envKey}={$envValue}", $str);
+    $str = substr($str, 0, -1);
+
+    $fp = fopen($envFile, 'w');
+    fwrite($fp, $str);
+    fclose($fp);
+}
+
+function getEnvironmentValue($envKey)
+{
+    $envFile = dirname(__FILE__, 3) . "/.env";
+    $str = file_get_contents($envFile);
+
+    $str .= "\n"; // In case the searched variable is in the last line without \n
+    $keyPosition = strpos($str, "{$envKey}=");
+    $endOfLinePosition = strpos($str, PHP_EOL, $keyPosition);
+    $oldLine = substr($str, $keyPosition, $endOfLinePosition - $keyPosition);
+    $value = substr($oldLine, strpos($oldLine, "=") + 1);
+
+
+
+    return $value;
+
+}
+
+
+function run_console($command)
+{
+    $path = dirname(__FILE__, 3);
+    $cmd = "cd '$path' && bash -c 'exec -a ServerCPP $command' 2>&1";
+    return shell_exec($cmd);
+}
+
+?>

+ 486 - 0
public/install/index.php

@@ -0,0 +1,486 @@
+<?php
+include("functions.php");
+
+if (file_exists("../../install.lock")) {
+    die("The installation has been completed already. Please delete the File 'install.lock' to re-run");
+}
+?>
+
+<html>
+<head>
+    <title>Controlpanel.gg installer Script</title>
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
+          integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
+    <style>
+        body {
+            background-color: powderblue;
+        }
+
+        .card {
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            margin-right: -50%;
+            transform: translate(-50%, -50%);
+        }
+
+        .ok {
+            color: green;
+        }
+
+        .ok::before {
+            content: "✔️";
+        }
+
+        .notok {
+            color: red;
+        }
+
+        .notok::before {
+            content: "❌";
+        }
+    </style>
+</head>
+<body>
+
+<?php
+$cardheader = '
+        <div class="card card-outline-success bg-dark">
+        <div class="card-header text-center">
+            <b class="mr-1 text-light">Controlpanel.GG</b>
+        </div>
+        <div class="card-body bg-light">';
+
+
+if (!isset($_GET['step'])) {
+
+
+    if (!file_exists("../../.env")) {
+        echo run_console('cp .env.example .env');
+
+    }
+    echo $cardheader;
+    ?>
+    <p class="login-box-msg">This installer will lead you through the most crucial Steps of Controlpanel.gg`s
+        setup</p>
+    <p class="<?php print(checkHTTPS() == true ? "ok" : "notok"); ?>">HTTPS is required</p>
+
+    <p class="<?php print(checkWriteable() == true ? "ok" : "notok"); ?>">Write-permissions on .env-file</p>
+
+    <p class="<?php print(checkPhpVersion() === "OK" ? "ok" : "notok"); ?>"> php
+        version: <?php echo phpversion(); ?> (required <?php echo $requirements["php"]; ?>)</p>
+    <p class="<?php print(getMySQLVersion() === "OK" ? "ok" : "notok"); ?>"> mysql
+        version: <?php echo getMySQLVersion(); ?> (minimum required <?php echo $requirements["mysql"]; ?>)</p>
+
+    <p class="<?php print(sizeof(checkExtensions()) == 0 ? "ok" : "notok"); ?>"> Missing
+        php-extentions: <?php print(sizeof(checkExtensions()) == 0 ? "none" : "");
+        foreach (checkExtensions() as $ext) {
+            echo $ext . ", ";
+        }
+
+        print(sizeof(checkExtensions()) == 0 ? "" : "(Proceed anyway)"); ?></p>
+
+
+    <!-- <p class="<?php print(getZipVersion() === "OK" ? "ok" : "notok"); ?>"> Zip
+                version: <?php echo getZipVersion(); ?> </p> -->
+
+    <p class="<?php print(getGitVersion() === "OK" ? "ok" : "notok"); ?>"> Git
+        version: <?php echo getGitVersion(); ?> </p>
+
+    <p class="<?php print(getTarVersion() === "OK" ? "ok" : "notok"); ?>"> Tar
+        version: <?php echo getTarVersion(); ?> </p>
+
+
+    <a href="?step=2">
+        <button class="btn btn-primary">Lets go</button>
+    </a>
+    </div>
+    </div>
+
+    <?php
+}
+if (isset($_GET['step']) && $_GET['step'] == 2) {
+echo $cardheader;
+?>
+<p class="login-box-msg">Lets start with your Database</p>
+<?php if (isset($_GET['message'])) {
+    echo "<p class='notok'>" . $_GET['message'] . "</p>";
+}
+?>
+
+<form method="POST" enctype="multipart/form-data" class="mb-3"
+      action="/install/forms.php" name="checkDB">
+
+    <div class="row">
+        <div class="col-md-12">
+            <div class="form-group">
+                <div class="custom-control mb-3">
+                    <label for="database">Database Driver</label>
+                    <input x-model="databasedriver" id="databasedriver" name="databasedriver"
+                           type="text" required
+                           value="mysql" class="form-control">
+                </div>
+            </div>
+            <div class="form-group">
+                <div class="custom-control mb-3">
+                    <label for="databasehost">Database Host</label>
+                    <input x-model="databasehost" id="databasehost" name="databasehost" type="text"
+                           required
+                           value="127.0.0.1" class="form-control">
+                </div>
+            </div>
+            <div class="form-group">
+                <div class="custom-control mb-3">
+                    <label for="databaseport">Database Port</label>
+                    <input x-model="databaseport" id="databaseport" name="databaseport"
+                           type="number" required
+                           value="3306" class="form-control">
+                </div>
+            </div>
+            <div class="form-group">
+                <div class="custom-control mb-3">
+                    <label for="databaseuser">Database User</label>
+                    <input x-model="databaseuser" id="databaseuser" name="databaseuser" type="text"
+                           required
+                           value="dashboarduser" class="form-control">
+                </div>
+            </div>
+            <div class="form-group">
+                <div class="custom-control mb-3">
+                    <label for="databaseuserpass">Database User Password</label>
+                    <input x-model="databaseuserpass" id="databaseuserpass" name="databaseuserpass"
+                           type="text" required
+                           class="form-control ">
+                </div>
+            </div>
+
+            <div class="form-group">
+                <div class="custom-control mb-3">
+                    <label for="database">Database</label>
+                    <input x-model="database" id="database" name="database" type="text" required
+                           value="dashboard" class="form-control">
+                </div>
+            </div>
+
+        </div>
+
+        <button class="btn btn-primary" name="checkDB">Submit</button>
+    </div>
+</form>
+    </div>
+
+
+    </div>
+
+    <?php
+    }
+    if (isset($_GET['step']) && $_GET['step'] == 2.5) {
+    echo $cardheader;
+    ?>
+    <p class="login-box-msg">Lets feed your Database and generate some security keys!</p>
+    <p> This process might take a while. Please do not refresh or close this page!</p>
+    <?php if (isset($_GET['message'])) {
+        echo "<p class='notok'>" . $_GET['message'] . "</p>";
+    }
+
+    ?>
+
+    <form method="POST" enctype="multipart/form-data" class="mb-3"
+          action="/install/forms.php" name="feedDB">
+
+
+        <button class="btn btn-primary" name="feedDB">Submit</button>
+        </div>
+        </div>
+
+
+        </div>
+
+        <?php
+        }
+
+
+        if (isset($_GET['step']) && $_GET['step'] == 3) {
+        echo $cardheader;
+        ?>
+        <p class="login-box-msg">Tell us something about your Host</p>
+
+        <?php if (isset($_GET['message'])) {
+            echo "<p class='notok'>" . $_GET['message'] . "</p>";
+        }
+        ?>
+
+        <form method="POST" enctype="multipart/form-data" class="mb-3"
+              action="/install/forms.php" name="checkGeneral">
+
+
+            <div class="row">
+                <div class="col-md-12">
+                    <div class="form-group">
+                        <div class="custom-control mb-3">
+                            <label for="database">Your Dashboard URL</label>
+                            <input id="url" name="url"
+                                   type="text" required
+                                   value="<?php echo "https://" . $_SERVER['SERVER_NAME']; ?>" class="form-control">
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <div class="custom-control mb-3">
+                            <label for="name">Your Host-Name</label>
+                            <input id="name" name="name" type="text"
+                                   required
+                                   value="Controlpanel.gg" class="form-control">
+                        </div>
+                    </div>
+
+                </div>
+
+                <button class="btn btn-primary" name="checkGeneral">Submit</button>
+            </div>
+        </form>
+            </div>
+
+
+            </div>
+
+            <?php
+            }
+            if (isset($_GET['step']) && $_GET['step'] == 4) {
+            echo $cardheader;
+            ?>
+            <p class="login-box-msg">Lets get your E-Mails going! </p>
+            <p class="login-box-msg">This might take a few seconds when submitted! </p>
+
+            <?php if (isset($_GET['message'])) {
+                echo "<p class='notok'>" . $_GET['message'] . "</p>";
+            }
+            ?>
+
+            <form method="POST" enctype="multipart/form-data" class="mb-3"
+
+                  action="/install/forms.php" name="checkSMTP">
+
+
+                <div class="row">
+                    <div class="col-md-12">
+                        <div class="form-group">
+                            <div class="custom-control mb-3">
+                                <label for="method">Your E-Mail method</label>
+                                <input id="method" name="method"
+                                       type="text" required
+                                       value="smtp" class="form-control">
+
+                            </div>
+                        </div>
+                        <div class="form-group">
+                            <div class="custom-control mb-3">
+                                <label for="host">Your Mailer-Host</label>
+                                <input id="host" name="host" type="text"
+                                       required
+                                       value="smtp.google.com" class="form-control">
+                            </div>
+                        </div>
+
+                        <div class="form-group">
+                            <div class="custom-control mb-3">
+                                <label for="port">Your Mail Port</label>
+                                <input id="port" name="port" type="port"
+                                       required
+                                       value="567" class="form-control">
+                            </div>
+                        </div>
+
+                        <div class="form-group">
+                            <div class="custom-control mb-3">
+                                <label for="user">Your Mail User</label>
+                                <input id="user" name="user" type="text"
+                                       required
+                                       value="info@mydomain.com" class="form-control">
+                            </div>
+                        </div>
+
+
+                        <div class="form-group">
+                            <div class="custom-control mb-3">
+                                <label for="pass">Your Mail-User Password</label>
+                                <input id="pass" name="pass" type="password"
+                                       required
+                                       value="" class="form-control">
+                            </div>
+                        </div>
+
+                        <div class="form-group">
+                            <div class="custom-control mb-3">
+                                <label for="encryption">Your Mail encryption method</label>
+                                <input id="encryption" name="encryption" type="text"
+                                       required
+                                       value="tls" class="form-control">
+                            </div>
+                        </div>
+
+                    </div>
+
+                    <button class="btn btn-primary" name="checkSMTP">Submit</button>
+            </form>
+
+                </div>
+
+                <a href="?step=5"><button class="btn btn-warning">Skip this step for now</button></a>
+                </div>
+
+                </div>
+                <?php
+                }
+
+                if (isset($_GET['step']) && $_GET['step'] == 5) {
+
+                echo $cardheader;
+                ?>
+
+                <p class="login-box-msg">Almost done! </p>
+                <p class="login-box-msg">Lets get some info about your Pterodactyl Installation!</p>
+
+
+                <?php if (isset($_GET['message'])) {
+                    echo "<p class='notok'>" . $_GET['message'] . "</p>";
+                }
+                ?>
+
+                <form method="POST" enctype="multipart/form-data" class="mb-3"
+
+                      action="/install/forms.php" name="checkPtero">
+
+
+                    <div class="row">
+                        <div class="col-md-12">
+                            <div class="form-group">
+                                <div class="custom-control mb-3">
+
+                                    <label for="url">Pterodactyl URL</label>
+                                    <input id="url" name="url"
+                                           type="text" required
+                                           value="https://ptero.example.com" class="form-control">
+                                </div>
+                            </div>
+                            <div class="form-group">
+                                <div class="custom-control mb-3">
+                                    <label for="key">Pterodactyl API-Key</label>
+                                    <input id="key" name="key" type="text"
+                                           required
+                                           value="" class="form-control"
+                                           placeholder="The Key needs ALL read&write Permissions!">
+                                </div>
+                            </div>
+
+
+                        </div>
+
+                        <button  class="btn btn-primary" name="checkPtero">Submit</button>
+                    </div>
+                </form>
+                    </div>
+
+
+                    </div>
+
+                    <?php
+                    }
+
+                    if (isset($_GET['step']) && $_GET['step'] == 6) {
+                    echo $cardheader;
+                    ?>
+                    <p class="login-box-msg">Lets create yourself!</p>
+                    <p class="login-box-msg">We're making the first Admin user</p>
+                    <?php if (isset($_GET['message'])) {
+                        echo "<p class='notok'>" . $_GET['message'] . "</p>";
+                    }
+                    ?>
+
+                    <form method="POST" enctype="multipart/form-data" class="mb-3"
+                          action="/install/forms.php" name="createUser">
+
+                        <div class="row">
+                            <div class="col-md-12">
+                                <div class="form-group">
+                                    <div class="custom-control mb-3">
+                                        <label for="user">Your Username</label>
+                                        <input id="user" name="user"
+                                               type="text" required
+                                               value="" class="form-control">
+                                    </div>
+                                </div>
+                                <div class="form-group">
+                                    <div class="custom-control mb-3">
+                                        <label for="email">Your Email Adress (used to Login)</label>
+                                        <input id="email" name="email"
+                                               type="text" required
+                                               value="" class="form-control">
+                                    </div>
+                                </div>
+
+
+                                <div class="form-group">
+                                    <div class="custom-control mb-3">
+                                        <label for="pass">Password</label>
+                                        <input id="pass" name="pass" type="password"
+                                               required
+                                               value="" minlength="8" class="form-control">
+                                    </div>
+                                </div>
+                                <div class="form-group">
+                                    <div class="custom-control mb-3">
+                                        <label for="repass">Retype Password</label>
+                                        <input id="repass" name="repass" type="password"
+                                               required
+                                               value="" minlength="8" class="form-control">
+                                    </div>
+                                </div>
+
+                                <div class="form-group">
+                                    <div class="custom-control mb-3">
+                                        <label for="pteroID">Your Pterodactyl User-ID</label>
+                                        <input id="pteroID" name="pteroID" type="text"
+                                               required
+                                               value="" class="form-control">
+                                    </div>
+                                </div>
+
+                            </div>
+
+                            <button class="btn btn-primary" name="createUser">Submit</button>
+                        </div>
+                    </form>
+                        </div>
+
+
+                        </div>
+
+                        <?php
+                        }
+                        if (isset($_GET['step']) && $_GET['step'] == 7) {
+                            $lockfile = fopen("../../install.lock", "w") or die("Unable to open file!");
+                            fwrite($lockfile, "locked");
+                            fclose($lockfile);
+
+                            echo $cardheader;
+
+                            ?>
+                            <p class="login-box-msg">All done!</p>
+                            <p class="login-box-msg">You may navigate to your Dashboard now and log in!</p>
+                            <a href="<?php echo "https://" . $_SERVER['SERVER_NAME']; ?>">
+                                <button class="btn btn-success">Lets go!</button>
+                            </a>
+                            </div>
+
+
+                            </div>
+                            <?php
+                        }
+                        ?>
+
+
+                        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
+                                integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
+                                crossorigin="anonymous"></script>
+</body>
+</html>

+ 40 - 0
public/install/phpmailer/Exception.php

@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * PHPMailer Exception class.
+ * PHP Version 5.5.
+ *
+ * @see       https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
+ *
+ * @author    Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
+ * @author    Jim Jagielski (jimjag) <jimjag@gmail.com>
+ * @author    Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
+ * @author    Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2020 Marcus Bointon
+ * @copyright 2010 - 2012 Jim Jagielski
+ * @copyright 2004 - 2009 Andy Prevost
+ * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
+ * @note      This program is distributed in the hope that it will be useful - WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+namespace PHPMailer\PHPMailer;
+
+/**
+ * PHPMailer exception handler.
+ *
+ * @author Marcus Bointon <phpmailer@synchromedia.co.uk>
+ */
+class Exception extends \Exception
+{
+    /**
+     * Prettify error message output.
+     *
+     * @return string
+     */
+    public function errorMessage()
+    {
+        return '<strong>' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . "</strong><br />\n";
+    }
+}

+ 5041 - 0
public/install/phpmailer/PHPMailer.php

@@ -0,0 +1,5041 @@
+<?php
+
+/**
+ * PHPMailer - PHP email creation and transport class.
+ * PHP Version 5.5.
+ *
+ * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
+ *
+ * @author    Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
+ * @author    Jim Jagielski (jimjag) <jimjag@gmail.com>
+ * @author    Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
+ * @author    Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2020 Marcus Bointon
+ * @copyright 2010 - 2012 Jim Jagielski
+ * @copyright 2004 - 2009 Andy Prevost
+ * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
+ * @note      This program is distributed in the hope that it will be useful - WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+namespace PHPMailer\PHPMailer;
+
+/**
+ * PHPMailer - PHP email creation and transport class.
+ *
+ * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
+ * @author Jim Jagielski (jimjag) <jimjag@gmail.com>
+ * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
+ * @author Brent R. Matzelle (original founder)
+ */
+class PHPMailer
+{
+    const CHARSET_ASCII = 'us-ascii';
+    const CHARSET_ISO88591 = 'iso-8859-1';
+    const CHARSET_UTF8 = 'utf-8';
+
+    const CONTENT_TYPE_PLAINTEXT = 'text/plain';
+    const CONTENT_TYPE_TEXT_CALENDAR = 'text/calendar';
+    const CONTENT_TYPE_TEXT_HTML = 'text/html';
+    const CONTENT_TYPE_MULTIPART_ALTERNATIVE = 'multipart/alternative';
+    const CONTENT_TYPE_MULTIPART_MIXED = 'multipart/mixed';
+    const CONTENT_TYPE_MULTIPART_RELATED = 'multipart/related';
+
+    const ENCODING_7BIT = '7bit';
+    const ENCODING_8BIT = '8bit';
+    const ENCODING_BASE64 = 'base64';
+    const ENCODING_BINARY = 'binary';
+    const ENCODING_QUOTED_PRINTABLE = 'quoted-printable';
+
+    const ENCRYPTION_STARTTLS = 'tls';
+    const ENCRYPTION_SMTPS = 'ssl';
+
+    const ICAL_METHOD_REQUEST = 'REQUEST';
+    const ICAL_METHOD_PUBLISH = 'PUBLISH';
+    const ICAL_METHOD_REPLY = 'REPLY';
+    const ICAL_METHOD_ADD = 'ADD';
+    const ICAL_METHOD_CANCEL = 'CANCEL';
+    const ICAL_METHOD_REFRESH = 'REFRESH';
+    const ICAL_METHOD_COUNTER = 'COUNTER';
+    const ICAL_METHOD_DECLINECOUNTER = 'DECLINECOUNTER';
+
+    /**
+     * Email priority.
+     * Options: null (default), 1 = High, 3 = Normal, 5 = low.
+     * When null, the header is not set at all.
+     *
+     * @var int|null
+     */
+    public $Priority;
+
+    /**
+     * The character set of the message.
+     *
+     * @var string
+     */
+    public $CharSet = self::CHARSET_ISO88591;
+
+    /**
+     * The MIME Content-type of the message.
+     *
+     * @var string
+     */
+    public $ContentType = self::CONTENT_TYPE_PLAINTEXT;
+
+    /**
+     * The message encoding.
+     * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable".
+     *
+     * @var string
+     */
+    public $Encoding = self::ENCODING_8BIT;
+
+    /**
+     * Holds the most recent mailer error message.
+     *
+     * @var string
+     */
+    public $ErrorInfo = '';
+
+    /**
+     * The From email address for the message.
+     *
+     * @var string
+     */
+    public $From = '';
+
+    /**
+     * The From name of the message.
+     *
+     * @var string
+     */
+    public $FromName = '';
+
+    /**
+     * The envelope sender of the message.
+     * This will usually be turned into a Return-Path header by the receiver,
+     * and is the address that bounces will be sent to.
+     * If not empty, will be passed via `-f` to sendmail or as the 'MAIL FROM' value over SMTP.
+     *
+     * @var string
+     */
+    public $Sender = '';
+
+    /**
+     * The Subject of the message.
+     *
+     * @var string
+     */
+    public $Subject = '';
+
+    /**
+     * An HTML or plain text message body.
+     * If HTML then call isHTML(true).
+     *
+     * @var string
+     */
+    public $Body = '';
+
+    /**
+     * The plain-text message body.
+     * This body can be read by mail clients that do not have HTML email
+     * capability such as mutt & Eudora.
+     * Clients that can read HTML will view the normal Body.
+     *
+     * @var string
+     */
+    public $AltBody = '';
+
+    /**
+     * An iCal message part body.
+     * Only supported in simple alt or alt_inline message types
+     * To generate iCal event structures, use classes like EasyPeasyICS or iCalcreator.
+     *
+     * @see http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/
+     * @see http://kigkonsult.se/iCalcreator/
+     *
+     * @var string
+     */
+    public $Ical = '';
+
+    /**
+     * Value-array of "method" in Contenttype header "text/calendar"
+     *
+     * @var string[]
+     */
+    protected static $IcalMethods = [
+        self::ICAL_METHOD_REQUEST,
+        self::ICAL_METHOD_PUBLISH,
+        self::ICAL_METHOD_REPLY,
+        self::ICAL_METHOD_ADD,
+        self::ICAL_METHOD_CANCEL,
+        self::ICAL_METHOD_REFRESH,
+        self::ICAL_METHOD_COUNTER,
+        self::ICAL_METHOD_DECLINECOUNTER,
+    ];
+
+    /**
+     * The complete compiled MIME message body.
+     *
+     * @var string
+     */
+    protected $MIMEBody = '';
+
+    /**
+     * The complete compiled MIME message headers.
+     *
+     * @var string
+     */
+    protected $MIMEHeader = '';
+
+    /**
+     * Extra headers that createHeader() doesn't fold in.
+     *
+     * @var string
+     */
+    protected $mailHeader = '';
+
+    /**
+     * Word-wrap the message body to this number of chars.
+     * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance.
+     *
+     * @see static::STD_LINE_LENGTH
+     *
+     * @var int
+     */
+    public $WordWrap = 0;
+
+    /**
+     * Which method to use to send mail.
+     * Options: "mail", "sendmail", or "smtp".
+     *
+     * @var string
+     */
+    public $Mailer = 'mail';
+
+    /**
+     * The path to the sendmail program.
+     *
+     * @var string
+     */
+    public $Sendmail = '/usr/sbin/sendmail';
+
+    /**
+     * Whether mail() uses a fully sendmail-compatible MTA.
+     * One which supports sendmail's "-oi -f" options.
+     *
+     * @var bool
+     */
+    public $UseSendmailOptions = true;
+
+    /**
+     * The email address that a reading confirmation should be sent to, also known as read receipt.
+     *
+     * @var string
+     */
+    public $ConfirmReadingTo = '';
+
+    /**
+     * The hostname to use in the Message-ID header and as default HELO string.
+     * If empty, PHPMailer attempts to find one with, in order,
+     * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value
+     * 'localhost.localdomain'.
+     *
+     * @see PHPMailer::$Helo
+     *
+     * @var string
+     */
+    public $Hostname = '';
+
+    /**
+     * An ID to be used in the Message-ID header.
+     * If empty, a unique id will be generated.
+     * You can set your own, but it must be in the format "<id@domain>",
+     * as defined in RFC5322 section 3.6.4 or it will be ignored.
+     *
+     * @see https://tools.ietf.org/html/rfc5322#section-3.6.4
+     *
+     * @var string
+     */
+    public $MessageID = '';
+
+    /**
+     * The message Date to be used in the Date header.
+     * If empty, the current date will be added.
+     *
+     * @var string
+     */
+    public $MessageDate = '';
+
+    /**
+     * SMTP hosts.
+     * Either a single hostname or multiple semicolon-delimited hostnames.
+     * You can also specify a different port
+     * for each host by using this format: [hostname:port]
+     * (e.g. "smtp1.example.com:25;smtp2.example.com").
+     * You can also specify encryption type, for example:
+     * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465").
+     * Hosts will be tried in order.
+     *
+     * @var string
+     */
+    public $Host = 'localhost';
+
+    /**
+     * The default SMTP server port.
+     *
+     * @var int
+     */
+    public $Port = 25;
+
+    /**
+     * The SMTP HELO/EHLO name used for the SMTP connection.
+     * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find
+     * one with the same method described above for $Hostname.
+     *
+     * @see PHPMailer::$Hostname
+     *
+     * @var string
+     */
+    public $Helo = '';
+
+    /**
+     * What kind of encryption to use on the SMTP connection.
+     * Options: '', static::ENCRYPTION_STARTTLS, or static::ENCRYPTION_SMTPS.
+     *
+     * @var string
+     */
+    public $SMTPSecure = '';
+
+    /**
+     * Whether to enable TLS encryption automatically if a server supports it,
+     * even if `SMTPSecure` is not set to 'tls'.
+     * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid.
+     *
+     * @var bool
+     */
+    public $SMTPAutoTLS = true;
+
+    /**
+     * Whether to use SMTP authentication.
+     * Uses the Username and Password properties.
+     *
+     * @see PHPMailer::$Username
+     * @see PHPMailer::$Password
+     *
+     * @var bool
+     */
+    public $SMTPAuth = false;
+
+    /**
+     * Options array passed to stream_context_create when connecting via SMTP.
+     *
+     * @var array
+     */
+    public $SMTPOptions = [];
+
+    /**
+     * SMTP username.
+     *
+     * @var string
+     */
+    public $Username = '';
+
+    /**
+     * SMTP password.
+     *
+     * @var string
+     */
+    public $Password = '';
+
+    /**
+     * SMTP auth type.
+     * Options are CRAM-MD5, LOGIN, PLAIN, XOAUTH2, attempted in that order if not specified.
+     *
+     * @var string
+     */
+    public $AuthType = '';
+
+    /**
+     * An instance of the PHPMailer OAuth class.
+     *
+     * @var OAuth
+     */
+    protected $oauth;
+
+    /**
+     * The SMTP server timeout in seconds.
+     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
+     *
+     * @var int
+     */
+    public $Timeout = 300;
+
+    /**
+     * Comma separated list of DSN notifications
+     * 'NEVER' under no circumstances a DSN must be returned to the sender.
+     *         If you use NEVER all other notifications will be ignored.
+     * 'SUCCESS' will notify you when your mail has arrived at its destination.
+     * 'FAILURE' will arrive if an error occurred during delivery.
+     * 'DELAY'   will notify you if there is an unusual delay in delivery, but the actual
+     *           delivery's outcome (success or failure) is not yet decided.
+     *
+     * @see https://tools.ietf.org/html/rfc3461 See section 4.1 for more information about NOTIFY
+     */
+    public $dsn = '';
+
+    /**
+     * SMTP class debug output mode.
+     * Debug output level.
+     * Options:
+     * @see SMTP::DEBUG_OFF: No output
+     * @see SMTP::DEBUG_CLIENT: Client messages
+     * @see SMTP::DEBUG_SERVER: Client and server messages
+     * @see SMTP::DEBUG_CONNECTION: As SERVER plus connection status
+     * @see SMTP::DEBUG_LOWLEVEL: Noisy, low-level data output, rarely needed
+     *
+     * @see SMTP::$do_debug
+     *
+     * @var int
+     */
+    public $SMTPDebug = 0;
+
+    /**
+     * How to handle debug output.
+     * Options:
+     * * `echo` Output plain-text as-is, appropriate for CLI
+     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
+     * * `error_log` Output to error log as configured in php.ini
+     * By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise.
+     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
+     *
+     * ```php
+     * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
+     * ```
+     *
+     * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
+     * level output is used:
+     *
+     * ```php
+     * $mail->Debugoutput = new myPsr3Logger;
+     * ```
+     *
+     * @see SMTP::$Debugoutput
+     *
+     * @var string|callable|\Psr\Log\LoggerInterface
+     */
+    public $Debugoutput = 'echo';
+
+    /**
+     * Whether to keep the SMTP connection open after each message.
+     * If this is set to true then the connection will remain open after a send,
+     * and closing the connection will require an explicit call to smtpClose().
+     * It's a good idea to use this if you are sending multiple messages as it reduces overhead.
+     * See the mailing list example for how to use it.
+     *
+     * @var bool
+     */
+    public $SMTPKeepAlive = false;
+
+    /**
+     * Whether to split multiple to addresses into multiple messages
+     * or send them all in one message.
+     * Only supported in `mail` and `sendmail` transports, not in SMTP.
+     *
+     * @var bool
+     *
+     * @deprecated 6.0.0 PHPMailer isn't a mailing list manager!
+     */
+    public $SingleTo = false;
+
+    /**
+     * Storage for addresses when SingleTo is enabled.
+     *
+     * @var array
+     */
+    protected $SingleToArray = [];
+
+    /**
+     * Whether to generate VERP addresses on send.
+     * Only applicable when sending via SMTP.
+     *
+     * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path
+     * @see http://www.postfix.org/VERP_README.html Postfix VERP info
+     *
+     * @var bool
+     */
+    public $do_verp = false;
+
+    /**
+     * Whether to allow sending messages with an empty body.
+     *
+     * @var bool
+     */
+    public $AllowEmpty = false;
+
+    /**
+     * DKIM selector.
+     *
+     * @var string
+     */
+    public $DKIM_selector = '';
+
+    /**
+     * DKIM Identity.
+     * Usually the email address used as the source of the email.
+     *
+     * @var string
+     */
+    public $DKIM_identity = '';
+
+    /**
+     * DKIM passphrase.
+     * Used if your key is encrypted.
+     *
+     * @var string
+     */
+    public $DKIM_passphrase = '';
+
+    /**
+     * DKIM signing domain name.
+     *
+     * @example 'example.com'
+     *
+     * @var string
+     */
+    public $DKIM_domain = '';
+
+    /**
+     * DKIM Copy header field values for diagnostic use.
+     *
+     * @var bool
+     */
+    public $DKIM_copyHeaderFields = true;
+
+    /**
+     * DKIM Extra signing headers.
+     *
+     * @example ['List-Unsubscribe', 'List-Help']
+     *
+     * @var array
+     */
+    public $DKIM_extraHeaders = [];
+
+    /**
+     * DKIM private key file path.
+     *
+     * @var string
+     */
+    public $DKIM_private = '';
+
+    /**
+     * DKIM private key string.
+     *
+     * If set, takes precedence over `$DKIM_private`.
+     *
+     * @var string
+     */
+    public $DKIM_private_string = '';
+
+    /**
+     * Callback Action function name.
+     *
+     * The function that handles the result of the send email action.
+     * It is called out by send() for each email sent.
+     *
+     * Value can be any php callable: http://www.php.net/is_callable
+     *
+     * Parameters:
+     *   bool $result        result of the send action
+     *   array   $to            email addresses of the recipients
+     *   array   $cc            cc email addresses
+     *   array   $bcc           bcc email addresses
+     *   string  $subject       the subject
+     *   string  $body          the email body
+     *   string  $from          email address of sender
+     *   string  $extra         extra information of possible use
+     *                          "smtp_transaction_id' => last smtp transaction id
+     *
+     * @var string
+     */
+    public $action_function = '';
+
+    /**
+     * What to put in the X-Mailer header.
+     * Options: An empty string for PHPMailer default, whitespace/null for none, or a string to use.
+     *
+     * @var string|null
+     */
+    public $XMailer = '';
+
+    /**
+     * Which validator to use by default when validating email addresses.
+     * May be a callable to inject your own validator, but there are several built-in validators.
+     * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option.
+     *
+     * @see PHPMailer::validateAddress()
+     *
+     * @var string|callable
+     */
+    public static $validator = 'php';
+
+    /**
+     * An instance of the SMTP sender class.
+     *
+     * @var SMTP
+     */
+    protected $smtp;
+
+    /**
+     * The array of 'to' names and addresses.
+     *
+     * @var array
+     */
+    protected $to = [];
+
+    /**
+     * The array of 'cc' names and addresses.
+     *
+     * @var array
+     */
+    protected $cc = [];
+
+    /**
+     * The array of 'bcc' names and addresses.
+     *
+     * @var array
+     */
+    protected $bcc = [];
+
+    /**
+     * The array of reply-to names and addresses.
+     *
+     * @var array
+     */
+    protected $ReplyTo = [];
+
+    /**
+     * An array of all kinds of addresses.
+     * Includes all of $to, $cc, $bcc.
+     *
+     * @see PHPMailer::$to
+     * @see PHPMailer::$cc
+     * @see PHPMailer::$bcc
+     *
+     * @var array
+     */
+    protected $all_recipients = [];
+
+    /**
+     * An array of names and addresses queued for validation.
+     * In send(), valid and non duplicate entries are moved to $all_recipients
+     * and one of $to, $cc, or $bcc.
+     * This array is used only for addresses with IDN.
+     *
+     * @see PHPMailer::$to
+     * @see PHPMailer::$cc
+     * @see PHPMailer::$bcc
+     * @see PHPMailer::$all_recipients
+     *
+     * @var array
+     */
+    protected $RecipientsQueue = [];
+
+    /**
+     * An array of reply-to names and addresses queued for validation.
+     * In send(), valid and non duplicate entries are moved to $ReplyTo.
+     * This array is used only for addresses with IDN.
+     *
+     * @see PHPMailer::$ReplyTo
+     *
+     * @var array
+     */
+    protected $ReplyToQueue = [];
+
+    /**
+     * The array of attachments.
+     *
+     * @var array
+     */
+    protected $attachment = [];
+
+    /**
+     * The array of custom headers.
+     *
+     * @var array
+     */
+    protected $CustomHeader = [];
+
+    /**
+     * The most recent Message-ID (including angular brackets).
+     *
+     * @var string
+     */
+    protected $lastMessageID = '';
+
+    /**
+     * The message's MIME type.
+     *
+     * @var string
+     */
+    protected $message_type = '';
+
+    /**
+     * The array of MIME boundary strings.
+     *
+     * @var array
+     */
+    protected $boundary = [];
+
+    /**
+     * The array of available text strings for the current language.
+     *
+     * @var array
+     */
+    protected $language = [];
+
+    /**
+     * The number of errors encountered.
+     *
+     * @var int
+     */
+    protected $error_count = 0;
+
+    /**
+     * The S/MIME certificate file path.
+     *
+     * @var string
+     */
+    protected $sign_cert_file = '';
+
+    /**
+     * The S/MIME key file path.
+     *
+     * @var string
+     */
+    protected $sign_key_file = '';
+
+    /**
+     * The optional S/MIME extra certificates ("CA Chain") file path.
+     *
+     * @var string
+     */
+    protected $sign_extracerts_file = '';
+
+    /**
+     * The S/MIME password for the key.
+     * Used only if the key is encrypted.
+     *
+     * @var string
+     */
+    protected $sign_key_pass = '';
+
+    /**
+     * Whether to throw exceptions for errors.
+     *
+     * @var bool
+     */
+    protected $exceptions = false;
+
+    /**
+     * Unique ID used for message ID and boundaries.
+     *
+     * @var string
+     */
+    protected $uniqueid = '';
+
+    /**
+     * The PHPMailer Version number.
+     *
+     * @var string
+     */
+    const VERSION = '6.5.3';
+
+    /**
+     * Error severity: message only, continue processing.
+     *
+     * @var int
+     */
+    const STOP_MESSAGE = 0;
+
+    /**
+     * Error severity: message, likely ok to continue processing.
+     *
+     * @var int
+     */
+    const STOP_CONTINUE = 1;
+
+    /**
+     * Error severity: message, plus full stop, critical error reached.
+     *
+     * @var int
+     */
+    const STOP_CRITICAL = 2;
+
+    /**
+     * The SMTP standard CRLF line break.
+     * If you want to change line break format, change static::$LE, not this.
+     */
+    const CRLF = "\r\n";
+
+    /**
+     * "Folding White Space" a white space string used for line folding.
+     */
+    const FWS = ' ';
+
+    /**
+     * SMTP RFC standard line ending; Carriage Return, Line Feed.
+     *
+     * @var string
+     */
+    protected static $LE = self::CRLF;
+
+    /**
+     * The maximum line length supported by mail().
+     *
+     * Background: mail() will sometimes corrupt messages
+     * with headers headers longer than 65 chars, see #818.
+     *
+     * @var int
+     */
+    const MAIL_MAX_LINE_LENGTH = 63;
+
+    /**
+     * The maximum line length allowed by RFC 2822 section 2.1.1.
+     *
+     * @var int
+     */
+    const MAX_LINE_LENGTH = 998;
+
+    /**
+     * The lower maximum line length allowed by RFC 2822 section 2.1.1.
+     * This length does NOT include the line break
+     * 76 means that lines will be 77 or 78 chars depending on whether
+     * the line break format is LF or CRLF; both are valid.
+     *
+     * @var int
+     */
+    const STD_LINE_LENGTH = 76;
+
+    /**
+     * Constructor.
+     *
+     * @param bool $exceptions Should we throw external exceptions?
+     */
+    public function __construct($exceptions = null)
+    {
+        if (null !== $exceptions) {
+            $this->exceptions = (bool) $exceptions;
+        }
+        //Pick an appropriate debug output format automatically
+        $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html');
+    }
+
+    /**
+     * Destructor.
+     */
+    public function __destruct()
+    {
+        //Close any open SMTP connection nicely
+        $this->smtpClose();
+    }
+
+    /**
+     * Call mail() in a safe_mode-aware fashion.
+     * Also, unless sendmail_path points to sendmail (or something that
+     * claims to be sendmail), don't pass params (not a perfect fix,
+     * but it will do).
+     *
+     * @param string      $to      To
+     * @param string      $subject Subject
+     * @param string      $body    Message Body
+     * @param string      $header  Additional Header(s)
+     * @param string|null $params  Params
+     *
+     * @return bool
+     */
+    private function mailPassthru($to, $subject, $body, $header, $params)
+    {
+        //Check overloading of mail function to avoid double-encoding
+        if (ini_get('mbstring.func_overload') & 1) {
+            $subject = $this->secureHeader($subject);
+        } else {
+            $subject = $this->encodeHeader($this->secureHeader($subject));
+        }
+        //Calling mail() with null params breaks
+        $this->edebug('Sending with mail()');
+        $this->edebug('Sendmail path: ' . ini_get('sendmail_path'));
+        $this->edebug("Envelope sender: {$this->Sender}");
+        $this->edebug("To: {$to}");
+        $this->edebug("Subject: {$subject}");
+        $this->edebug("Headers: {$header}");
+        if (!$this->UseSendmailOptions || null === $params) {
+            $result = @mail($to, $subject, $body, $header);
+        } else {
+            $this->edebug("Additional params: {$params}");
+            $result = @mail($to, $subject, $body, $header, $params);
+        }
+        $this->edebug('Result: ' . ($result ? 'true' : 'false'));
+        return $result;
+    }
+
+    /**
+     * Output debugging info via a user-defined method.
+     * Only generates output if debug output is enabled.
+     *
+     * @see PHPMailer::$Debugoutput
+     * @see PHPMailer::$SMTPDebug
+     *
+     * @param string $str
+     */
+    protected function edebug($str)
+    {
+        if ($this->SMTPDebug <= 0) {
+            return;
+        }
+        //Is this a PSR-3 logger?
+        if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
+            $this->Debugoutput->debug($str);
+
+            return;
+        }
+        //Avoid clash with built-in function names
+        if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
+            call_user_func($this->Debugoutput, $str, $this->SMTPDebug);
+
+            return;
+        }
+        switch ($this->Debugoutput) {
+            case 'error_log':
+                //Don't output, just log
+                /** @noinspection ForgottenDebugOutputInspection */
+                error_log($str);
+                break;
+            case 'html':
+                //Cleans up output a bit for a better looking, HTML-safe output
+                echo htmlentities(
+                    preg_replace('/[\r\n]+/', '', $str),
+                    ENT_QUOTES,
+                    'UTF-8'
+                ), "<br>\n";
+                break;
+            case 'echo':
+            default:
+                //Normalize line breaks
+                $str = preg_replace('/\r\n|\r/m', "\n", $str);
+                echo gmdate('Y-m-d H:i:s'),
+                "\t",
+                    //Trim trailing space
+                trim(
+                    //Indent for readability, except for trailing break
+                    str_replace(
+                        "\n",
+                        "\n                   \t                  ",
+                        trim($str)
+                    )
+                ),
+                "\n";
+        }
+    }
+
+    /**
+     * Sets message type to HTML or plain.
+     *
+     * @param bool $isHtml True for HTML mode
+     */
+    public function isHTML($isHtml = true)
+    {
+        if ($isHtml) {
+            $this->ContentType = static::CONTENT_TYPE_TEXT_HTML;
+        } else {
+            $this->ContentType = static::CONTENT_TYPE_PLAINTEXT;
+        }
+    }
+
+    /**
+     * Send messages using SMTP.
+     */
+    public function isSMTP()
+    {
+        $this->Mailer = 'smtp';
+    }
+
+    /**
+     * Send messages using PHP's mail() function.
+     */
+    public function isMail()
+    {
+        $this->Mailer = 'mail';
+    }
+
+    /**
+     * Send messages using $Sendmail.
+     */
+    public function isSendmail()
+    {
+        $ini_sendmail_path = ini_get('sendmail_path');
+
+        if (false === stripos($ini_sendmail_path, 'sendmail')) {
+            $this->Sendmail = '/usr/sbin/sendmail';
+        } else {
+            $this->Sendmail = $ini_sendmail_path;
+        }
+        $this->Mailer = 'sendmail';
+    }
+
+    /**
+     * Send messages using qmail.
+     */
+    public function isQmail()
+    {
+        $ini_sendmail_path = ini_get('sendmail_path');
+
+        if (false === stripos($ini_sendmail_path, 'qmail')) {
+            $this->Sendmail = '/var/qmail/bin/qmail-inject';
+        } else {
+            $this->Sendmail = $ini_sendmail_path;
+        }
+        $this->Mailer = 'qmail';
+    }
+
+    /**
+     * Add a "To" address.
+     *
+     * @param string $address The email address to send to
+     * @param string $name
+     *
+     * @throws Exception
+     *
+     * @return bool true on success, false if address already used or invalid in some way
+     */
+    public function addAddress($address, $name = '')
+    {
+        return $this->addOrEnqueueAnAddress('to', $address, $name);
+    }
+
+    /**
+     * Add a "CC" address.
+     *
+     * @param string $address The email address to send to
+     * @param string $name
+     *
+     * @throws Exception
+     *
+     * @return bool true on success, false if address already used or invalid in some way
+     */
+    public function addCC($address, $name = '')
+    {
+        return $this->addOrEnqueueAnAddress('cc', $address, $name);
+    }
+
+    /**
+     * Add a "BCC" address.
+     *
+     * @param string $address The email address to send to
+     * @param string $name
+     *
+     * @throws Exception
+     *
+     * @return bool true on success, false if address already used or invalid in some way
+     */
+    public function addBCC($address, $name = '')
+    {
+        return $this->addOrEnqueueAnAddress('bcc', $address, $name);
+    }
+
+    /**
+     * Add a "Reply-To" address.
+     *
+     * @param string $address The email address to reply to
+     * @param string $name
+     *
+     * @throws Exception
+     *
+     * @return bool true on success, false if address already used or invalid in some way
+     */
+    public function addReplyTo($address, $name = '')
+    {
+        return $this->addOrEnqueueAnAddress('Reply-To', $address, $name);
+    }
+
+    /**
+     * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer
+     * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still
+     * be modified after calling this function), addition of such addresses is delayed until send().
+     * Addresses that have been added already return false, but do not throw exceptions.
+     *
+     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
+     * @param string $address The email address to send, resp. to reply to
+     * @param string $name
+     *
+     * @throws Exception
+     *
+     * @return bool true on success, false if address already used or invalid in some way
+     */
+    protected function addOrEnqueueAnAddress($kind, $address, $name)
+    {
+        $address = trim($address);
+        $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
+        $pos = strrpos($address, '@');
+        if (false === $pos) {
+            //At-sign is missing.
+            $error_message = sprintf(
+                '%s (%s): %s',
+                $this->lang('invalid_address'),
+                $kind,
+                $address
+            );
+            $this->setError($error_message);
+            $this->edebug($error_message);
+            if ($this->exceptions) {
+                throw new Exception($error_message);
+            }
+
+            return false;
+        }
+        $params = [$kind, $address, $name];
+        //Enqueue addresses with IDN until we know the PHPMailer::$CharSet.
+        if (static::idnSupported() && $this->has8bitChars(substr($address, ++$pos))) {
+            if ('Reply-To' !== $kind) {
+                if (!array_key_exists($address, $this->RecipientsQueue)) {
+                    $this->RecipientsQueue[$address] = $params;
+
+                    return true;
+                }
+            } elseif (!array_key_exists($address, $this->ReplyToQueue)) {
+                $this->ReplyToQueue[$address] = $params;
+
+                return true;
+            }
+
+            return false;
+        }
+
+        //Immediately add standard addresses without IDN.
+        return call_user_func_array([$this, 'addAnAddress'], $params);
+    }
+
+    /**
+     * Add an address to one of the recipient arrays or to the ReplyTo array.
+     * Addresses that have been added already return false, but do not throw exceptions.
+     *
+     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
+     * @param string $address The email address to send, resp. to reply to
+     * @param string $name
+     *
+     * @throws Exception
+     *
+     * @return bool true on success, false if address already used or invalid in some way
+     */
+    protected function addAnAddress($kind, $address, $name = '')
+    {
+        if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) {
+            $error_message = sprintf(
+                '%s: %s',
+                $this->lang('Invalid recipient kind'),
+                $kind
+            );
+            $this->setError($error_message);
+            $this->edebug($error_message);
+            if ($this->exceptions) {
+                throw new Exception($error_message);
+            }
+
+            return false;
+        }
+        if (!static::validateAddress($address)) {
+            $error_message = sprintf(
+                '%s (%s): %s',
+                $this->lang('invalid_address'),
+                $kind,
+                $address
+            );
+            $this->setError($error_message);
+            $this->edebug($error_message);
+            if ($this->exceptions) {
+                throw new Exception($error_message);
+            }
+
+            return false;
+        }
+        if ('Reply-To' !== $kind) {
+            if (!array_key_exists(strtolower($address), $this->all_recipients)) {
+                $this->{$kind}[] = [$address, $name];
+                $this->all_recipients[strtolower($address)] = true;
+
+                return true;
+            }
+        } elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) {
+            $this->ReplyTo[strtolower($address)] = [$address, $name];
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Parse and validate a string containing one or more RFC822-style comma-separated email addresses
+     * of the form "display name <address>" into an array of name/address pairs.
+     * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available.
+     * Note that quotes in the name part are removed.
+     *
+     * @see http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation
+     *
+     * @param string $addrstr The address list string
+     * @param bool   $useimap Whether to use the IMAP extension to parse the list
+     * @param string $charset The charset to use when decoding the address list string.
+     *
+     * @return array
+     */
+    public static function parseAddresses($addrstr, $useimap = true, $charset = self::CHARSET_ISO88591)
+    {
+        $addresses = [];
+        if ($useimap && function_exists('imap_rfc822_parse_adrlist')) {
+            //Use this built-in parser if it's available
+            $list = imap_rfc822_parse_adrlist($addrstr, '');
+            // Clear any potential IMAP errors to get rid of notices being thrown at end of script.
+            imap_errors();
+            foreach ($list as $address) {
+                if (
+                    '.SYNTAX-ERROR.' !== $address->host &&
+                    static::validateAddress($address->mailbox . '@' . $address->host)
+                ) {
+                    //Decode the name part if it's present and encoded
+                    if (
+                        property_exists($address, 'personal') &&
+                        //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled
+                        defined('MB_CASE_UPPER') &&
+                        preg_match('/^=\?.*\?=$/s', $address->personal)
+                    ) {
+                        $origCharset = mb_internal_encoding();
+                        mb_internal_encoding($charset);
+                        //Undo any RFC2047-encoded spaces-as-underscores
+                        $address->personal = str_replace('_', '=20', $address->personal);
+                        //Decode the name
+                        $address->personal = mb_decode_mimeheader($address->personal);
+                        mb_internal_encoding($origCharset);
+                    }
+
+                    $addresses[] = [
+                        'name' => (property_exists($address, 'personal') ? $address->personal : ''),
+                        'address' => $address->mailbox . '@' . $address->host,
+                    ];
+                }
+            }
+        } else {
+            //Use this simpler parser
+            $list = explode(',', $addrstr);
+            foreach ($list as $address) {
+                $address = trim($address);
+                //Is there a separate name part?
+                if (strpos($address, '<') === false) {
+                    //No separate name, just use the whole thing
+                    if (static::validateAddress($address)) {
+                        $addresses[] = [
+                            'name' => '',
+                            'address' => $address,
+                        ];
+                    }
+                } else {
+                    list($name, $email) = explode('<', $address);
+                    $email = trim(str_replace('>', '', $email));
+                    $name = trim($name);
+                    if (static::validateAddress($email)) {
+                        //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled
+                        //If this name is encoded, decode it
+                        if (defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $name)) {
+                            $origCharset = mb_internal_encoding();
+                            mb_internal_encoding($charset);
+                            //Undo any RFC2047-encoded spaces-as-underscores
+                            $name = str_replace('_', '=20', $name);
+                            //Decode the name
+                            $name = mb_decode_mimeheader($name);
+                            mb_internal_encoding($origCharset);
+                        }
+                        $addresses[] = [
+                            //Remove any surrounding quotes and spaces from the name
+                            'name' => trim($name, '\'" '),
+                            'address' => $email,
+                        ];
+                    }
+                }
+            }
+        }
+
+        return $addresses;
+    }
+
+    /**
+     * Set the From and FromName properties.
+     *
+     * @param string $address
+     * @param string $name
+     * @param bool   $auto    Whether to also set the Sender address, defaults to true
+     *
+     * @throws Exception
+     *
+     * @return bool
+     */
+    public function setFrom($address, $name = '', $auto = true)
+    {
+        $address = trim($address);
+        $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
+        //Don't validate now addresses with IDN. Will be done in send().
+        $pos = strrpos($address, '@');
+        if (
+            (false === $pos)
+            || ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnSupported())
+            && !static::validateAddress($address))
+        ) {
+            $error_message = sprintf(
+                '%s (From): %s',
+                $this->lang('invalid_address'),
+                $address
+            );
+            $this->setError($error_message);
+            $this->edebug($error_message);
+            if ($this->exceptions) {
+                throw new Exception($error_message);
+            }
+
+            return false;
+        }
+        $this->From = $address;
+        $this->FromName = $name;
+        if ($auto && empty($this->Sender)) {
+            $this->Sender = $address;
+        }
+
+        return true;
+    }
+
+    /**
+     * Return the Message-ID header of the last email.
+     * Technically this is the value from the last time the headers were created,
+     * but it's also the message ID of the last sent message except in
+     * pathological cases.
+     *
+     * @return string
+     */
+    public function getLastMessageID()
+    {
+        return $this->lastMessageID;
+    }
+
+    /**
+     * Check that a string looks like an email address.
+     * Validation patterns supported:
+     * * `auto` Pick best pattern automatically;
+     * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0;
+     * * `pcre` Use old PCRE implementation;
+     * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL;
+     * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements.
+     * * `noregex` Don't use a regex: super fast, really dumb.
+     * Alternatively you may pass in a callable to inject your own validator, for example:
+     *
+     * ```php
+     * PHPMailer::validateAddress('user@example.com', function($address) {
+     *     return (strpos($address, '@') !== false);
+     * });
+     * ```
+     *
+     * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator.
+     *
+     * @param string          $address       The email address to check
+     * @param string|callable $patternselect Which pattern to use
+     *
+     * @return bool
+     */
+    public static function validateAddress($address, $patternselect = null)
+    {
+        if (null === $patternselect) {
+            $patternselect = static::$validator;
+        }
+        //Don't allow strings as callables, see SECURITY.md and CVE-2021-3603
+        if (is_callable($patternselect) && !is_string($patternselect)) {
+            return call_user_func($patternselect, $address);
+        }
+        //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321
+        if (strpos($address, "\n") !== false || strpos($address, "\r") !== false) {
+            return false;
+        }
+        switch ($patternselect) {
+            case 'pcre': //Kept for BC
+            case 'pcre8':
+                /*
+                 * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL
+                 * is based.
+                 * In addition to the addresses allowed by filter_var, also permits:
+                 *  * dotless domains: `a@b`
+                 *  * comments: `1234 @ local(blah) .machine .example`
+                 *  * quoted elements: `'"test blah"@example.org'`
+                 *  * numeric TLDs: `a@b.123`
+                 *  * unbracketed IPv4 literals: `a@192.168.0.1`
+                 *  * IPv6 literals: 'first.last@[IPv6:a1::]'
+                 * Not all of these will necessarily work for sending!
+                 *
+                 * @see       http://squiloople.com/2009/12/20/email-address-validation/
+                 * @copyright 2009-2010 Michael Rushton
+                 * Feel free to use and redistribute this code. But please keep this copyright notice.
+                 */
+                return (bool) preg_match(
+                    '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' .
+                    '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' .
+                    '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' .
+                    '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' .
+                    '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' .
+                    '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' .
+                    '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' .
+                    '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
+                    '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD',
+                    $address
+                );
+            case 'html5':
+                /*
+                 * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements.
+                 *
+                 * @see https://html.spec.whatwg.org/#e-mail-state-(type=email)
+                 */
+                return (bool) preg_match(
+                    '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' .
+                    '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD',
+                    $address
+                );
+            case 'php':
+            default:
+                return filter_var($address, FILTER_VALIDATE_EMAIL) !== false;
+        }
+    }
+
+    /**
+     * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the
+     * `intl` and `mbstring` PHP extensions.
+     *
+     * @return bool `true` if required functions for IDN support are present
+     */
+    public static function idnSupported()
+    {
+        return function_exists('idn_to_ascii') && function_exists('mb_convert_encoding');
+    }
+
+    /**
+     * Converts IDN in given email address to its ASCII form, also known as punycode, if possible.
+     * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet.
+     * This function silently returns unmodified address if:
+     * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form)
+     * - Conversion to punycode is impossible (e.g. required PHP functions are not available)
+     *   or fails for any reason (e.g. domain contains characters not allowed in an IDN).
+     *
+     * @see PHPMailer::$CharSet
+     *
+     * @param string $address The email address to convert
+     *
+     * @return string The encoded address in ASCII form
+     */
+    public function punyencodeAddress($address)
+    {
+        //Verify we have required functions, CharSet, and at-sign.
+        $pos = strrpos($address, '@');
+        if (
+            !empty($this->CharSet) &&
+            false !== $pos &&
+            static::idnSupported()
+        ) {
+            $domain = substr($address, ++$pos);
+            //Verify CharSet string is a valid one, and domain properly encoded in this CharSet.
+            if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $this->CharSet)) {
+                //Convert the domain from whatever charset it's in to UTF-8
+                $domain = mb_convert_encoding($domain, self::CHARSET_UTF8, $this->CharSet);
+                //Ignore IDE complaints about this line - method signature changed in PHP 5.4
+                $errorcode = 0;
+                if (defined('INTL_IDNA_VARIANT_UTS46')) {
+                    //Use the current punycode standard (appeared in PHP 7.2)
+                    $punycode = idn_to_ascii(
+                        $domain,
+                        \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI |
+                            \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII,
+                        \INTL_IDNA_VARIANT_UTS46
+                    );
+                } elseif (defined('INTL_IDNA_VARIANT_2003')) {
+                    //Fall back to this old, deprecated/removed encoding
+                    $punycode = idn_to_ascii($domain, $errorcode, \INTL_IDNA_VARIANT_2003);
+                } else {
+                    //Fall back to a default we don't know about
+                    $punycode = idn_to_ascii($domain, $errorcode);
+                }
+                if (false !== $punycode) {
+                    return substr($address, 0, $pos) . $punycode;
+                }
+            }
+        }
+
+        return $address;
+    }
+
+    /**
+     * Create a message and send it.
+     * Uses the sending method specified by $Mailer.
+     *
+     * @throws Exception
+     *
+     * @return bool false on error - See the ErrorInfo property for details of the error
+     */
+    public function send()
+    {
+        try {
+            if (!$this->preSend()) {
+                return false;
+            }
+
+            return $this->postSend();
+        } catch (Exception $exc) {
+            $this->mailHeader = '';
+            $this->setError($exc->getMessage());
+            if ($this->exceptions) {
+                throw $exc;
+            }
+
+            return false;
+        }
+    }
+
+    /**
+     * Prepare a message for sending.
+     *
+     * @throws Exception
+     *
+     * @return bool
+     */
+    public function preSend()
+    {
+        if (
+            'smtp' === $this->Mailer
+            || ('mail' === $this->Mailer && (\PHP_VERSION_ID >= 80000 || stripos(PHP_OS, 'WIN') === 0))
+        ) {
+            //SMTP mandates RFC-compliant line endings
+            //and it's also used with mail() on Windows
+            static::setLE(self::CRLF);
+        } else {
+            //Maintain backward compatibility with legacy Linux command line mailers
+            static::setLE(PHP_EOL);
+        }
+        //Check for buggy PHP versions that add a header with an incorrect line break
+        if (
+            'mail' === $this->Mailer
+            && ((\PHP_VERSION_ID >= 70000 && \PHP_VERSION_ID < 70017)
+                || (\PHP_VERSION_ID >= 70100 && \PHP_VERSION_ID < 70103))
+            && ini_get('mail.add_x_header') === '1'
+            && stripos(PHP_OS, 'WIN') === 0
+        ) {
+            trigger_error($this->lang('buggy_php'), E_USER_WARNING);
+        }
+
+        try {
+            $this->error_count = 0; //Reset errors
+            $this->mailHeader = '';
+
+            //Dequeue recipient and Reply-To addresses with IDN
+            foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) {
+                $params[1] = $this->punyencodeAddress($params[1]);
+                call_user_func_array([$this, 'addAnAddress'], $params);
+            }
+            if (count($this->to) + count($this->cc) + count($this->bcc) < 1) {
+                throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL);
+            }
+
+            //Validate From, Sender, and ConfirmReadingTo addresses
+            foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) {
+                $this->$address_kind = trim($this->$address_kind);
+                if (empty($this->$address_kind)) {
+                    continue;
+                }
+                $this->$address_kind = $this->punyencodeAddress($this->$address_kind);
+                if (!static::validateAddress($this->$address_kind)) {
+                    $error_message = sprintf(
+                        '%s (%s): %s',
+                        $this->lang('invalid_address'),
+                        $address_kind,
+                        $this->$address_kind
+                    );
+                    $this->setError($error_message);
+                    $this->edebug($error_message);
+                    if ($this->exceptions) {
+                        throw new Exception($error_message);
+                    }
+
+                    return false;
+                }
+            }
+
+            //Set whether the message is multipart/alternative
+            if ($this->alternativeExists()) {
+                $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE;
+            }
+
+            $this->setMessageType();
+            //Refuse to send an empty message unless we are specifically allowing it
+            if (!$this->AllowEmpty && empty($this->Body)) {
+                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
+            }
+
+            //Trim subject consistently
+            $this->Subject = trim($this->Subject);
+            //Create body before headers in case body makes changes to headers (e.g. altering transfer encoding)
+            $this->MIMEHeader = '';
+            $this->MIMEBody = $this->createBody();
+            //createBody may have added some headers, so retain them
+            $tempheaders = $this->MIMEHeader;
+            $this->MIMEHeader = $this->createHeader();
+            $this->MIMEHeader .= $tempheaders;
+
+            //To capture the complete message when using mail(), create
+            //an extra header list which createHeader() doesn't fold in
+            if ('mail' === $this->Mailer) {
+                if (count($this->to) > 0) {
+                    $this->mailHeader .= $this->addrAppend('To', $this->to);
+                } else {
+                    $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;');
+                }
+                $this->mailHeader .= $this->headerLine(
+                    'Subject',
+                    $this->encodeHeader($this->secureHeader($this->Subject))
+                );
+            }
+
+            //Sign with DKIM if enabled
+            if (
+                !empty($this->DKIM_domain)
+                && !empty($this->DKIM_selector)
+                && (!empty($this->DKIM_private_string)
+                    || (!empty($this->DKIM_private)
+                        && static::isPermittedPath($this->DKIM_private)
+                        && file_exists($this->DKIM_private)
+                    )
+                )
+            ) {
+                $header_dkim = $this->DKIM_Add(
+                    $this->MIMEHeader . $this->mailHeader,
+                    $this->encodeHeader($this->secureHeader($this->Subject)),
+                    $this->MIMEBody
+                );
+                $this->MIMEHeader = static::stripTrailingWSP($this->MIMEHeader) . static::$LE .
+                    static::normalizeBreaks($header_dkim) . static::$LE;
+            }
+
+            return true;
+        } catch (Exception $exc) {
+            $this->setError($exc->getMessage());
+            if ($this->exceptions) {
+                throw $exc;
+            }
+
+            return false;
+        }
+    }
+
+    /**
+     * Actually send a message via the selected mechanism.
+     *
+     * @throws Exception
+     *
+     * @return bool
+     */
+    public function postSend()
+    {
+        try {
+            //Choose the mailer and send through it
+            switch ($this->Mailer) {
+                case 'sendmail':
+                case 'qmail':
+                    return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody);
+                case 'smtp':
+                    return $this->smtpSend($this->MIMEHeader, $this->MIMEBody);
+                case 'mail':
+                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
+                default:
+                    $sendMethod = $this->Mailer . 'Send';
+                    if (method_exists($this, $sendMethod)) {
+                        return $this->$sendMethod($this->MIMEHeader, $this->MIMEBody);
+                    }
+
+                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
+            }
+        } catch (Exception $exc) {
+            if ($this->Mailer === 'smtp' && $this->SMTPKeepAlive == true) {
+                $this->smtp->reset();
+            }
+            $this->setError($exc->getMessage());
+            $this->edebug($exc->getMessage());
+            if ($this->exceptions) {
+                throw $exc;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Send mail using the $Sendmail program.
+     *
+     * @see PHPMailer::$Sendmail
+     *
+     * @param string $header The message headers
+     * @param string $body   The message body
+     *
+     * @throws Exception
+     *
+     * @return bool
+     */
+    protected function sendmailSend($header, $body)
+    {
+        if ($this->Mailer === 'qmail') {
+            $this->edebug('Sending with qmail');
+        } else {
+            $this->edebug('Sending with sendmail');
+        }
+        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
+        //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
+        //A space after `-f` is optional, but there is a long history of its presence
+        //causing problems, so we don't use one
+        //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
+        //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html
+        //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html
+        //Example problem: https://www.drupal.org/node/1057954
+
+        //PHP 5.6 workaround
+        $sendmail_from_value = ini_get('sendmail_from');
+        if (empty($this->Sender) && !empty($sendmail_from_value)) {
+            //PHP config has a sender address we can use
+            $this->Sender = ini_get('sendmail_from');
+        }
+        //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
+        if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) {
+            if ($this->Mailer === 'qmail') {
+                $sendmailFmt = '%s -f%s';
+            } else {
+                $sendmailFmt = '%s -oi -f%s -t';
+            }
+        } else {
+            //allow sendmail to choose a default envelope sender. It may
+            //seem preferable to force it to use the From header as with
+            //SMTP, but that introduces new problems (see
+            //<https://github.com/PHPMailer/PHPMailer/issues/2298>), and
+            //it has historically worked this way.
+            $sendmailFmt = '%s -oi -t';
+        }
+
+        $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender);
+        $this->edebug('Sendmail path: ' . $this->Sendmail);
+        $this->edebug('Sendmail command: ' . $sendmail);
+        $this->edebug('Envelope sender: ' . $this->Sender);
+        $this->edebug("Headers: {$header}");
+
+        if ($this->SingleTo) {
+            foreach ($this->SingleToArray as $toAddr) {
+                $mail = @popen($sendmail, 'w');
+                if (!$mail) {
+                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
+                }
+                $this->edebug("To: {$toAddr}");
+                fwrite($mail, 'To: ' . $toAddr . "\n");
+                fwrite($mail, $header);
+                fwrite($mail, $body);
+                $result = pclose($mail);
+                $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet);
+                $this->doCallback(
+                    ($result === 0),
+                    [[$addrinfo['address'], $addrinfo['name']]],
+                    $this->cc,
+                    $this->bcc,
+                    $this->Subject,
+                    $body,
+                    $this->From,
+                    []
+                );
+                $this->edebug("Result: " . ($result === 0 ? 'true' : 'false'));
+                if (0 !== $result) {
+                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
+                }
+            }
+        } else {
+            $mail = @popen($sendmail, 'w');
+            if (!$mail) {
+                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
+            }
+            fwrite($mail, $header);
+            fwrite($mail, $body);
+            $result = pclose($mail);
+            $this->doCallback(
+                ($result === 0),
+                $this->to,
+                $this->cc,
+                $this->bcc,
+                $this->Subject,
+                $body,
+                $this->From,
+                []
+            );
+            $this->edebug("Result: " . ($result === 0 ? 'true' : 'false'));
+            if (0 !== $result) {
+                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters.
+     * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows.
+     *
+     * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report
+     *
+     * @param string $string The string to be validated
+     *
+     * @return bool
+     */
+    protected static function isShellSafe($string)
+    {
+        //Future-proof
+        if (
+            escapeshellcmd($string) !== $string
+            || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""])
+        ) {
+            return false;
+        }
+
+        $length = strlen($string);
+
+        for ($i = 0; $i < $length; ++$i) {
+            $c = $string[$i];
+
+            //All other characters have a special meaning in at least one common shell, including = and +.
+            //Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.
+            //Note that this does permit non-Latin alphanumeric characters based on the current locale.
+            if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Check whether a file path is of a permitted type.
+     * Used to reject URLs and phar files from functions that access local file paths,
+     * such as addAttachment.
+     *
+     * @param string $path A relative or absolute path to a file
+     *
+     * @return bool
+     */
+    protected static function isPermittedPath($path)
+    {
+        //Matches scheme definition from https://tools.ietf.org/html/rfc3986#section-3.1
+        return !preg_match('#^[a-z][a-z\d+.-]*://#i', $path);
+    }
+
+    /**
+     * Check whether a file path is safe, accessible, and readable.
+     *
+     * @param string $path A relative or absolute path to a file
+     *
+     * @return bool
+     */
+    protected static function fileIsAccessible($path)
+    {
+        if (!static::isPermittedPath($path)) {
+            return false;
+        }
+        $readable = file_exists($path);
+        //If not a UNC path (expected to start with \\), check read permission, see #2069
+        if (strpos($path, '\\\\') !== 0) {
+            $readable = $readable && is_readable($path);
+        }
+        return  $readable;
+    }
+
+    /**
+     * Send mail using the PHP mail() function.
+     *
+     * @see http://www.php.net/manual/en/book.mail.php
+     *
+     * @param string $header The message headers
+     * @param string $body   The message body
+     *
+     * @throws Exception
+     *
+     * @return bool
+     */
+    protected function mailSend($header, $body)
+    {
+        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
+
+        $toArr = [];
+        foreach ($this->to as $toaddr) {
+            $toArr[] = $this->addrFormat($toaddr);
+        }
+        $to = implode(', ', $toArr);
+
+        $params = null;
+        //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
+        //A space after `-f` is optional, but there is a long history of its presence
+        //causing problems, so we don't use one
+        //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
+        //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html
+        //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html
+        //Example problem: https://www.drupal.org/node/1057954
+        //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
+
+        //PHP 5.6 workaround
+        $sendmail_from_value = ini_get('sendmail_from');
+        if (empty($this->Sender) && !empty($sendmail_from_value)) {
+            //PHP config has a sender address we can use
+            $this->Sender = ini_get('sendmail_from');
+        }
+        if (!empty($this->Sender) && static::validateAddress($this->Sender)) {
+            if (self::isShellSafe($this->Sender)) {
+                $params = sprintf('-f%s', $this->Sender);
+            }
+            $old_from = ini_get('sendmail_from');
+            ini_set('sendmail_from', $this->Sender);
+        }
+        $result = false;
+        if ($this->SingleTo && count($toArr) > 1) {
+            foreach ($toArr as $toAddr) {
+                $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);
+                $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet);
+                $this->doCallback(
+                    $result,
+                    [[$addrinfo['address'], $addrinfo['name']]],
+                    $this->cc,
+                    $this->bcc,
+                    $this->Subject,
+                    $body,
+                    $this->From,
+                    []
+                );
+            }
+        } else {
+            $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params);
+            $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []);
+        }
+        if (isset($old_from)) {
+            ini_set('sendmail_from', $old_from);
+        }
+        if (!$result) {
+            throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL);
+        }
+
+        return true;
+    }
+
+    /**
+     * Get an instance to use for SMTP operations.
+     * Override this function to load your own SMTP implementation,
+     * or set one with setSMTPInstance.
+     *
+     * @return SMTP
+     */
+    public function getSMTPInstance()
+    {
+        if (!is_object($this->smtp)) {
+            $this->smtp = new SMTP();
+        }
+
+        return $this->smtp;
+    }
+
+    /**
+     * Provide an instance to use for SMTP operations.
+     *
+     * @return SMTP
+     */
+    public function setSMTPInstance(SMTP $smtp)
+    {
+        $this->smtp = $smtp;
+
+        return $this->smtp;
+    }
+
+    /**
+     * Send mail via SMTP.
+     * Returns false if there is a bad MAIL FROM, RCPT, or DATA input.
+     *
+     * @see PHPMailer::setSMTPInstance() to use a different class.
+     *
+     * @uses \PHPMailer\PHPMailer\SMTP
+     *
+     * @param string $header The message headers
+     * @param string $body   The message body
+     *
+     * @throws Exception
+     *
+     * @return bool
+     */
+    protected function smtpSend($header, $body)
+    {
+        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
+        $bad_rcpt = [];
+        if (!$this->smtpConnect($this->SMTPOptions)) {
+            throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL);
+        }
+        //Sender already validated in preSend()
+        if ('' === $this->Sender) {
+            $smtp_from = $this->From;
+        } else {
+            $smtp_from = $this->Sender;
+        }
+        if (!$this->smtp->mail($smtp_from)) {
+            $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError()));
+            throw new Exception($this->ErrorInfo, self::STOP_CRITICAL);
+        }
+
+        $callbacks = [];
+        //Attempt to send to all recipients
+        foreach ([$this->to, $this->cc, $this->bcc] as $togroup) {
+            foreach ($togroup as $to) {
+                if (!$this->smtp->recipient($to[0], $this->dsn)) {
+                    $error = $this->smtp->getError();
+                    $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']];
+                    $isSent = false;
+                } else {
+                    $isSent = true;
+                }
+
+                $callbacks[] = ['issent' => $isSent, 'to' => $to[0], 'name' => $to[1]];
+            }
+        }
+
+        //Only send the DATA command if we have viable recipients
+        if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) {
+            throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL);
+        }
+
+        $smtp_transaction_id = $this->smtp->getLastTransactionID();
+
+        if ($this->SMTPKeepAlive) {
+            $this->smtp->reset();
+        } else {
+            $this->smtp->quit();
+            $this->smtp->close();
+        }
+
+        foreach ($callbacks as $cb) {
+            $this->doCallback(
+                $cb['issent'],
+                [[$cb['to'], $cb['name']]],
+                [],
+                [],
+                $this->Subject,
+                $body,
+                $this->From,
+                ['smtp_transaction_id' => $smtp_transaction_id]
+            );
+        }
+
+        //Create error message for any bad addresses
+        if (count($bad_rcpt) > 0) {
+            $errstr = '';
+            foreach ($bad_rcpt as $bad) {
+                $errstr .= $bad['to'] . ': ' . $bad['error'];
+            }
+            throw new Exception($this->lang('recipients_failed') . $errstr, self::STOP_CONTINUE);
+        }
+
+        return true;
+    }
+
+    /**
+     * Initiate a connection to an SMTP server.
+     * Returns false if the operation failed.
+     *
+     * @param array $options An array of options compatible with stream_context_create()
+     *
+     * @throws Exception
+     *
+     * @uses \PHPMailer\PHPMailer\SMTP
+     *
+     * @return bool
+     */
+    public function smtpConnect($options = null)
+    {
+        if (null === $this->smtp) {
+            $this->smtp = $this->getSMTPInstance();
+        }
+
+        //If no options are provided, use whatever is set in the instance
+        if (null === $options) {
+            $options = $this->SMTPOptions;
+        }
+
+        //Already connected?
+        if ($this->smtp->connected()) {
+            return true;
+        }
+
+        $this->smtp->setTimeout($this->Timeout);
+        $this->smtp->setDebugLevel($this->SMTPDebug);
+        $this->smtp->setDebugOutput($this->Debugoutput);
+        $this->smtp->setVerp($this->do_verp);
+        $hosts = explode(';', $this->Host);
+        $lastexception = null;
+
+        foreach ($hosts as $hostentry) {
+            $hostinfo = [];
+            if (
+                !preg_match(
+                    '/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/',
+                    trim($hostentry),
+                    $hostinfo
+                )
+            ) {
+                $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry));
+                //Not a valid host entry
+                continue;
+            }
+            //$hostinfo[1]: optional ssl or tls prefix
+            //$hostinfo[2]: the hostname
+            //$hostinfo[3]: optional port number
+            //The host string prefix can temporarily override the current setting for SMTPSecure
+            //If it's not specified, the default value is used
+
+            //Check the host name is a valid name or IP address before trying to use it
+            if (!static::isValidHost($hostinfo[2])) {
+                $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]);
+                continue;
+            }
+            $prefix = '';
+            $secure = $this->SMTPSecure;
+            $tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure);
+            if ('ssl' === $hostinfo[1] || ('' === $hostinfo[1] && static::ENCRYPTION_SMTPS === $this->SMTPSecure)) {
+                $prefix = 'ssl://';
+                $tls = false; //Can't have SSL and TLS at the same time
+                $secure = static::ENCRYPTION_SMTPS;
+            } elseif ('tls' === $hostinfo[1]) {
+                $tls = true;
+                //TLS doesn't use a prefix
+                $secure = static::ENCRYPTION_STARTTLS;
+            }
+            //Do we need the OpenSSL extension?
+            $sslext = defined('OPENSSL_ALGO_SHA256');
+            if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) {
+                //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled
+                if (!$sslext) {
+                    throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL);
+                }
+            }
+            $host = $hostinfo[2];
+            $port = $this->Port;
+            if (
+                array_key_exists(3, $hostinfo) &&
+                is_numeric($hostinfo[3]) &&
+                $hostinfo[3] > 0 &&
+                $hostinfo[3] < 65536
+            ) {
+                $port = (int) $hostinfo[3];
+            }
+            if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) {
+                try {
+                    if ($this->Helo) {
+                        $hello = $this->Helo;
+                    } else {
+                        $hello = $this->serverHostname();
+                    }
+                    $this->smtp->hello($hello);
+                    //Automatically enable TLS encryption if:
+                    //* it's not disabled
+                    //* we have openssl extension
+                    //* we are not already using SSL
+                    //* the server offers STARTTLS
+                    if ($this->SMTPAutoTLS && $sslext && 'ssl' !== $secure && $this->smtp->getServerExt('STARTTLS')) {
+                        $tls = true;
+                    }
+                    if ($tls) {
+                        if (!$this->smtp->startTLS()) {
+                            throw new Exception($this->lang('connect_host'));
+                        }
+                        //We must resend EHLO after TLS negotiation
+                        $this->smtp->hello($hello);
+                    }
+                    if (
+                        $this->SMTPAuth && !$this->smtp->authenticate(
+                            $this->Username,
+                            $this->Password,
+                            $this->AuthType,
+                            $this->oauth
+                        )
+                    ) {
+                        throw new Exception($this->lang('authenticate'));
+                    }
+
+                    return true;
+                } catch (Exception $exc) {
+                    $lastexception = $exc;
+                    $this->edebug($exc->getMessage());
+                    //We must have connected, but then failed TLS or Auth, so close connection nicely
+                    $this->smtp->quit();
+                }
+            }
+        }
+        //If we get here, all connection attempts have failed, so close connection hard
+        $this->smtp->close();
+        //As we've caught all exceptions, just report whatever the last one was
+        if ($this->exceptions && null !== $lastexception) {
+            throw $lastexception;
+        }
+
+        return false;
+    }
+
+    /**
+     * Close the active SMTP session if one exists.
+     */
+    public function smtpClose()
+    {
+        if ((null !== $this->smtp) && $this->smtp->connected()) {
+            $this->smtp->quit();
+            $this->smtp->close();
+        }
+    }
+
+    /**
+     * Set the language for error messages.
+     * The default language is English.
+     *
+     * @param string $langcode  ISO 639-1 2-character language code (e.g. French is "fr")
+     *                          Optionally, the language code can be enhanced with a 4-character
+     *                          script annotation and/or a 2-character country annotation.
+     * @param string $lang_path Path to the language file directory, with trailing separator (slash)
+     *                          Do not set this from user input!
+     *
+     * @return bool Returns true if the requested language was loaded, false otherwise.
+     */
+    public function setLanguage($langcode = 'en', $lang_path = '')
+    {
+        //Backwards compatibility for renamed language codes
+        $renamed_langcodes = [
+            'br' => 'pt_br',
+            'cz' => 'cs',
+            'dk' => 'da',
+            'no' => 'nb',
+            'se' => 'sv',
+            'rs' => 'sr',
+            'tg' => 'tl',
+            'am' => 'hy',
+        ];
+
+        if (array_key_exists($langcode, $renamed_langcodes)) {
+            $langcode = $renamed_langcodes[$langcode];
+        }
+
+        //Define full set of translatable strings in English
+        $PHPMAILER_LANG = [
+            'authenticate' => 'SMTP Error: Could not authenticate.',
+            'buggy_php' => 'Your version of PHP is affected by a bug that may result in corrupted messages.' .
+                ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' .
+                ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.',
+            'connect_host' => 'SMTP Error: Could not connect to SMTP host.',
+            'data_not_accepted' => 'SMTP Error: data not accepted.',
+            'empty_message' => 'Message body empty',
+            'encoding' => 'Unknown encoding: ',
+            'execute' => 'Could not execute: ',
+            'extension_missing' => 'Extension missing: ',
+            'file_access' => 'Could not access file: ',
+            'file_open' => 'File Error: Could not open file: ',
+            'from_failed' => 'The following From address failed: ',
+            'instantiate' => 'Could not instantiate mail function.',
+            'invalid_address' => 'Invalid address: ',
+            'invalid_header' => 'Invalid header name or value',
+            'invalid_hostentry' => 'Invalid hostentry: ',
+            'invalid_host' => 'Invalid host: ',
+            'mailer_not_supported' => ' mailer is not supported.',
+            'provide_address' => 'You must provide at least one recipient email address.',
+            'recipients_failed' => 'SMTP Error: The following recipients failed: ',
+            'signing' => 'Signing Error: ',
+            'smtp_code' => 'SMTP code: ',
+            'smtp_code_ex' => 'Additional SMTP info: ',
+            'smtp_connect_failed' => 'SMTP connect() failed.',
+            'smtp_detail' => 'Detail: ',
+            'smtp_error' => 'SMTP server error: ',
+            'variable_set' => 'Cannot set or reset variable: ',
+        ];
+        if (empty($lang_path)) {
+            //Calculate an absolute path so it can work if CWD is not here
+            $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR;
+        }
+
+        //Validate $langcode
+        $foundlang = true;
+        $langcode  = strtolower($langcode);
+        if (
+            !preg_match('/^(?P<lang>[a-z]{2})(?P<script>_[a-z]{4})?(?P<country>_[a-z]{2})?$/', $langcode, $matches)
+            && $langcode !== 'en'
+        ) {
+            $foundlang = false;
+            $langcode = 'en';
+        }
+
+        //There is no English translation file
+        if ('en' !== $langcode) {
+            $langcodes = [];
+            if (!empty($matches['script']) && !empty($matches['country'])) {
+                $langcodes[] = $matches['lang'] . $matches['script'] . $matches['country'];
+            }
+            if (!empty($matches['country'])) {
+                $langcodes[] = $matches['lang'] . $matches['country'];
+            }
+            if (!empty($matches['script'])) {
+                $langcodes[] = $matches['lang'] . $matches['script'];
+            }
+            $langcodes[] = $matches['lang'];
+
+            //Try and find a readable language file for the requested language.
+            $foundFile = false;
+            foreach ($langcodes as $code) {
+                $lang_file = $lang_path . 'phpmailer.lang-' . $code . '.php';
+                if (static::fileIsAccessible($lang_file)) {
+                    $foundFile = true;
+                    break;
+                }
+            }
+
+            if ($foundFile === false) {
+                $foundlang = false;
+            } else {
+                $lines = file($lang_file);
+                foreach ($lines as $line) {
+                    //Translation file lines look like this:
+                    //$PHPMAILER_LANG['authenticate'] = 'SMTP-Fehler: Authentifizierung fehlgeschlagen.';
+                    //These files are parsed as text and not PHP so as to avoid the possibility of code injection
+                    //See https://blog.stevenlevithan.com/archives/match-quoted-string
+                    $matches = [];
+                    if (
+                        preg_match(
+                            '/^\$PHPMAILER_LANG\[\'([a-z\d_]+)\'\]\s*=\s*(["\'])(.+)*?\2;/',
+                            $line,
+                            $matches
+                        ) &&
+                        //Ignore unknown translation keys
+                        array_key_exists($matches[1], $PHPMAILER_LANG)
+                    ) {
+                        //Overwrite language-specific strings so we'll never have missing translation keys.
+                        $PHPMAILER_LANG[$matches[1]] = (string)$matches[3];
+                    }
+                }
+            }
+        }
+        $this->language = $PHPMAILER_LANG;
+
+        return $foundlang; //Returns false if language not found
+    }
+
+    /**
+     * Get the array of strings for the current language.
+     *
+     * @return array
+     */
+    public function getTranslations()
+    {
+        if (empty($this->language)) {
+            $this->setLanguage(); // Set the default language.
+        }
+
+        return $this->language;
+    }
+
+    /**
+     * Create recipient headers.
+     *
+     * @param string $type
+     * @param array  $addr An array of recipients,
+     *                     where each recipient is a 2-element indexed array with element 0 containing an address
+     *                     and element 1 containing a name, like:
+     *                     [['joe@example.com', 'Joe User'], ['zoe@example.com', 'Zoe User']]
+     *
+     * @return string
+     */
+    public function addrAppend($type, $addr)
+    {
+        $addresses = [];
+        foreach ($addr as $address) {
+            $addresses[] = $this->addrFormat($address);
+        }
+
+        return $type . ': ' . implode(', ', $addresses) . static::$LE;
+    }
+
+    /**
+     * Format an address for use in a message header.
+     *
+     * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name like
+     *                    ['joe@example.com', 'Joe User']
+     *
+     * @return string
+     */
+    public function addrFormat($addr)
+    {
+        if (empty($addr[1])) { //No name provided
+            return $this->secureHeader($addr[0]);
+        }
+
+        return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') .
+            ' <' . $this->secureHeader($addr[0]) . '>';
+    }
+
+    /**
+     * Word-wrap message.
+     * For use with mailers that do not automatically perform wrapping
+     * and for quoted-printable encoded messages.
+     * Original written by philippe.
+     *
+     * @param string $message The message to wrap
+     * @param int    $length  The line length to wrap to
+     * @param bool   $qp_mode Whether to run in Quoted-Printable mode
+     *
+     * @return string
+     */
+    public function wrapText($message, $length, $qp_mode = false)
+    {
+        if ($qp_mode) {
+            $soft_break = sprintf(' =%s', static::$LE);
+        } else {
+            $soft_break = static::$LE;
+        }
+        //If utf-8 encoding is used, we will need to make sure we don't
+        //split multibyte characters when we wrap
+        $is_utf8 = static::CHARSET_UTF8 === strtolower($this->CharSet);
+        $lelen = strlen(static::$LE);
+        $crlflen = strlen(static::$LE);
+
+        $message = static::normalizeBreaks($message);
+        //Remove a trailing line break
+        if (substr($message, -$lelen) === static::$LE) {
+            $message = substr($message, 0, -$lelen);
+        }
+
+        //Split message into lines
+        $lines = explode(static::$LE, $message);
+        //Message will be rebuilt in here
+        $message = '';
+        foreach ($lines as $line) {
+            $words = explode(' ', $line);
+            $buf = '';
+            $firstword = true;
+            foreach ($words as $word) {
+                if ($qp_mode && (strlen($word) > $length)) {
+                    $space_left = $length - strlen($buf) - $crlflen;
+                    if (!$firstword) {
+                        if ($space_left > 20) {
+                            $len = $space_left;
+                            if ($is_utf8) {
+                                $len = $this->utf8CharBoundary($word, $len);
+                            } elseif ('=' === substr($word, $len - 1, 1)) {
+                                --$len;
+                            } elseif ('=' === substr($word, $len - 2, 1)) {
+                                $len -= 2;
+                            }
+                            $part = substr($word, 0, $len);
+                            $word = substr($word, $len);
+                            $buf .= ' ' . $part;
+                            $message .= $buf . sprintf('=%s', static::$LE);
+                        } else {
+                            $message .= $buf . $soft_break;
+                        }
+                        $buf = '';
+                    }
+                    while ($word !== '') {
+                        if ($length <= 0) {
+                            break;
+                        }
+                        $len = $length;
+                        if ($is_utf8) {
+                            $len = $this->utf8CharBoundary($word, $len);
+                        } elseif ('=' === substr($word, $len - 1, 1)) {
+                            --$len;
+                        } elseif ('=' === substr($word, $len - 2, 1)) {
+                            $len -= 2;
+                        }
+                        $part = substr($word, 0, $len);
+                        $word = (string) substr($word, $len);
+
+                        if ($word !== '') {
+                            $message .= $part . sprintf('=%s', static::$LE);
+                        } else {
+                            $buf = $part;
+                        }
+                    }
+                } else {
+                    $buf_o = $buf;
+                    if (!$firstword) {
+                        $buf .= ' ';
+                    }
+                    $buf .= $word;
+
+                    if ('' !== $buf_o && strlen($buf) > $length) {
+                        $message .= $buf_o . $soft_break;
+                        $buf = $word;
+                    }
+                }
+                $firstword = false;
+            }
+            $message .= $buf . static::$LE;
+        }
+
+        return $message;
+    }
+
+    /**
+     * Find the last character boundary prior to $maxLength in a utf-8
+     * quoted-printable encoded string.
+     * Original written by Colin Brown.
+     *
+     * @param string $encodedText utf-8 QP text
+     * @param int    $maxLength   Find the last character boundary prior to this length
+     *
+     * @return int
+     */
+    public function utf8CharBoundary($encodedText, $maxLength)
+    {
+        $foundSplitPos = false;
+        $lookBack = 3;
+        while (!$foundSplitPos) {
+            $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack);
+            $encodedCharPos = strpos($lastChunk, '=');
+            if (false !== $encodedCharPos) {
+                //Found start of encoded character byte within $lookBack block.
+                //Check the encoded byte value (the 2 chars after the '=')
+                $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2);
+                $dec = hexdec($hex);
+                if ($dec < 128) {
+                    //Single byte character.
+                    //If the encoded char was found at pos 0, it will fit
+                    //otherwise reduce maxLength to start of the encoded char
+                    if ($encodedCharPos > 0) {
+                        $maxLength -= $lookBack - $encodedCharPos;
+                    }
+                    $foundSplitPos = true;
+                } elseif ($dec >= 192) {
+                    //First byte of a multi byte character
+                    //Reduce maxLength to split at start of character
+                    $maxLength -= $lookBack - $encodedCharPos;
+                    $foundSplitPos = true;
+                } elseif ($dec < 192) {
+                    //Middle byte of a multi byte character, look further back
+                    $lookBack += 3;
+                }
+            } else {
+                //No encoded character found
+                $foundSplitPos = true;
+            }
+        }
+
+        return $maxLength;
+    }
+
+    /**
+     * Apply word wrapping to the message body.
+     * Wraps the message body to the number of chars set in the WordWrap property.
+     * You should only do this to plain-text bodies as wrapping HTML tags may break them.
+     * This is called automatically by createBody(), so you don't need to call it yourself.
+     */
+    public function setWordWrap()
+    {
+        if ($this->WordWrap < 1) {
+            return;
+        }
+
+        switch ($this->message_type) {
+            case 'alt':
+            case 'alt_inline':
+            case 'alt_attach':
+            case 'alt_inline_attach':
+                $this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap);
+                break;
+            default:
+                $this->Body = $this->wrapText($this->Body, $this->WordWrap);
+                break;
+        }
+    }
+
+    /**
+     * Assemble message headers.
+     *
+     * @return string The assembled headers
+     */
+    public function createHeader()
+    {
+        $result = '';
+
+        $result .= $this->headerLine('Date', '' === $this->MessageDate ? self::rfcDate() : $this->MessageDate);
+
+        //The To header is created automatically by mail(), so needs to be omitted here
+        if ('mail' !== $this->Mailer) {
+            if ($this->SingleTo) {
+                foreach ($this->to as $toaddr) {
+                    $this->SingleToArray[] = $this->addrFormat($toaddr);
+                }
+            } elseif (count($this->to) > 0) {
+                $result .= $this->addrAppend('To', $this->to);
+            } elseif (count($this->cc) === 0) {
+                $result .= $this->headerLine('To', 'undisclosed-recipients:;');
+            }
+        }
+        $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]);
+
+        //sendmail and mail() extract Cc from the header before sending
+        if (count($this->cc) > 0) {
+            $result .= $this->addrAppend('Cc', $this->cc);
+        }
+
+        //sendmail and mail() extract Bcc from the header before sending
+        if (
+            (
+                'sendmail' === $this->Mailer || 'qmail' === $this->Mailer || 'mail' === $this->Mailer
+            )
+            && count($this->bcc) > 0
+        ) {
+            $result .= $this->addrAppend('Bcc', $this->bcc);
+        }
+
+        if (count($this->ReplyTo) > 0) {
+            $result .= $this->addrAppend('Reply-To', $this->ReplyTo);
+        }
+
+        //mail() sets the subject itself
+        if ('mail' !== $this->Mailer) {
+            $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject)));
+        }
+
+        //Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4
+        //https://tools.ietf.org/html/rfc5322#section-3.6.4
+        if (
+            '' !== $this->MessageID &&
+            preg_match(
+                '/^<((([a-z\d!#$%&\'*+\/=?^_`{|}~-]+(\.[a-z\d!#$%&\'*+\/=?^_`{|}~-]+)*)' .
+                '|("(([\x01-\x08\x0B\x0C\x0E-\x1F\x7F]|[\x21\x23-\x5B\x5D-\x7E])' .
+                '|(\\[\x01-\x09\x0B\x0C\x0E-\x7F]))*"))@(([a-z\d!#$%&\'*+\/=?^_`{|}~-]+' .
+                '(\.[a-z\d!#$%&\'*+\/=?^_`{|}~-]+)*)|(\[(([\x01-\x08\x0B\x0C\x0E-\x1F\x7F]' .
+                '|[\x21-\x5A\x5E-\x7E])|(\\[\x01-\x09\x0B\x0C\x0E-\x7F]))*\])))>$/Di',
+                $this->MessageID
+            )
+        ) {
+            $this->lastMessageID = $this->MessageID;
+        } else {
+            $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname());
+        }
+        $result .= $this->headerLine('Message-ID', $this->lastMessageID);
+        if (null !== $this->Priority) {
+            $result .= $this->headerLine('X-Priority', $this->Priority);
+        }
+        if ('' === $this->XMailer) {
+            $result .= $this->headerLine(
+                'X-Mailer',
+                'PHPMailer ' . self::VERSION . ' (https://github.com/PHPMailer/PHPMailer)'
+            );
+        } else {
+            $myXmailer = trim($this->XMailer);
+            if ($myXmailer) {
+                $result .= $this->headerLine('X-Mailer', $myXmailer);
+            }
+        }
+
+        if ('' !== $this->ConfirmReadingTo) {
+            $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>');
+        }
+
+        //Add custom headers
+        foreach ($this->CustomHeader as $header) {
+            $result .= $this->headerLine(
+                trim($header[0]),
+                $this->encodeHeader(trim($header[1]))
+            );
+        }
+        if (!$this->sign_key_file) {
+            $result .= $this->headerLine('MIME-Version', '1.0');
+            $result .= $this->getMailMIME();
+        }
+
+        return $result;
+    }
+
+    /**
+     * Get the message MIME type headers.
+     *
+     * @return string
+     */
+    public function getMailMIME()
+    {
+        $result = '';
+        $ismultipart = true;
+        switch ($this->message_type) {
+            case 'inline':
+                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
+                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
+                break;
+            case 'attach':
+            case 'inline_attach':
+            case 'alt_attach':
+            case 'alt_inline_attach':
+                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_MIXED . ';');
+                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
+                break;
+            case 'alt':
+            case 'alt_inline':
+                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
+                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
+                break;
+            default:
+                //Catches case 'plain': and case '':
+                $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet);
+                $ismultipart = false;
+                break;
+        }
+        //RFC1341 part 5 says 7bit is assumed if not specified
+        if (static::ENCODING_7BIT !== $this->Encoding) {
+            //RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE
+            if ($ismultipart) {
+                if (static::ENCODING_8BIT === $this->Encoding) {
+                    $result .= $this->headerLine('Content-Transfer-Encoding', static::ENCODING_8BIT);
+                }
+                //The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible
+            } else {
+                $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding);
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Returns the whole MIME message.
+     * Includes complete headers and body.
+     * Only valid post preSend().
+     *
+     * @see PHPMailer::preSend()
+     *
+     * @return string
+     */
+    public function getSentMIMEMessage()
+    {
+        return static::stripTrailingWSP($this->MIMEHeader . $this->mailHeader) .
+            static::$LE . static::$LE . $this->MIMEBody;
+    }
+
+    /**
+     * Create a unique ID to use for boundaries.
+     *
+     * @return string
+     */
+    protected function generateId()
+    {
+        $len = 32; //32 bytes = 256 bits
+        $bytes = '';
+        if (function_exists('random_bytes')) {
+            try {
+                $bytes = random_bytes($len);
+            } catch (\Exception $e) {
+                //Do nothing
+            }
+        } elseif (function_exists('openssl_random_pseudo_bytes')) {
+            /** @noinspection CryptographicallySecureRandomnessInspection */
+            $bytes = openssl_random_pseudo_bytes($len);
+        }
+        if ($bytes === '') {
+            //We failed to produce a proper random string, so make do.
+            //Use a hash to force the length to the same as the other methods
+            $bytes = hash('sha256', uniqid((string) mt_rand(), true), true);
+        }
+
+        //We don't care about messing up base64 format here, just want a random string
+        return str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true)));
+    }
+
+    /**
+     * Assemble the message body.
+     * Returns an empty string on failure.
+     *
+     * @throws Exception
+     *
+     * @return string The assembled message body
+     */
+    public function createBody()
+    {
+        $body = '';
+        //Create unique IDs and preset boundaries
+        $this->uniqueid = $this->generateId();
+        $this->boundary[1] = 'b1_' . $this->uniqueid;
+        $this->boundary[2] = 'b2_' . $this->uniqueid;
+        $this->boundary[3] = 'b3_' . $this->uniqueid;
+
+        if ($this->sign_key_file) {
+            $body .= $this->getMailMIME() . static::$LE;
+        }
+
+        $this->setWordWrap();
+
+        $bodyEncoding = $this->Encoding;
+        $bodyCharSet = $this->CharSet;
+        //Can we do a 7-bit downgrade?
+        if (static::ENCODING_8BIT === $bodyEncoding && !$this->has8bitChars($this->Body)) {
+            $bodyEncoding = static::ENCODING_7BIT;
+            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
+            $bodyCharSet = static::CHARSET_ASCII;
+        }
+        //If lines are too long, and we're not already using an encoding that will shorten them,
+        //change to quoted-printable transfer encoding for the body part only
+        if (static::ENCODING_BASE64 !== $this->Encoding && static::hasLineLongerThanMax($this->Body)) {
+            $bodyEncoding = static::ENCODING_QUOTED_PRINTABLE;
+        }
+
+        $altBodyEncoding = $this->Encoding;
+        $altBodyCharSet = $this->CharSet;
+        //Can we do a 7-bit downgrade?
+        if (static::ENCODING_8BIT === $altBodyEncoding && !$this->has8bitChars($this->AltBody)) {
+            $altBodyEncoding = static::ENCODING_7BIT;
+            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
+            $altBodyCharSet = static::CHARSET_ASCII;
+        }
+        //If lines are too long, and we're not already using an encoding that will shorten them,
+        //change to quoted-printable transfer encoding for the alt body part only
+        if (static::ENCODING_BASE64 !== $altBodyEncoding && static::hasLineLongerThanMax($this->AltBody)) {
+            $altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE;
+        }
+        //Use this as a preamble in all multipart message types
+        $mimepre = 'This is a multi-part message in MIME format.' . static::$LE . static::$LE;
+        switch ($this->message_type) {
+            case 'inline':
+                $body .= $mimepre;
+                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
+                $body .= $this->encodeString($this->Body, $bodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->attachAll('inline', $this->boundary[1]);
+                break;
+            case 'attach':
+                $body .= $mimepre;
+                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
+                $body .= $this->encodeString($this->Body, $bodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->attachAll('attachment', $this->boundary[1]);
+                break;
+            case 'inline_attach':
+                $body .= $mimepre;
+                $body .= $this->textLine('--' . $this->boundary[1]);
+                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
+                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '";');
+                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
+                $body .= static::$LE;
+                $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding);
+                $body .= $this->encodeString($this->Body, $bodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->attachAll('inline', $this->boundary[2]);
+                $body .= static::$LE;
+                $body .= $this->attachAll('attachment', $this->boundary[1]);
+                break;
+            case 'alt':
+                $body .= $mimepre;
+                $body .= $this->getBoundary(
+                    $this->boundary[1],
+                    $altBodyCharSet,
+                    static::CONTENT_TYPE_PLAINTEXT,
+                    $altBodyEncoding
+                );
+                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->getBoundary(
+                    $this->boundary[1],
+                    $bodyCharSet,
+                    static::CONTENT_TYPE_TEXT_HTML,
+                    $bodyEncoding
+                );
+                $body .= $this->encodeString($this->Body, $bodyEncoding);
+                $body .= static::$LE;
+                if (!empty($this->Ical)) {
+                    $method = static::ICAL_METHOD_REQUEST;
+                    foreach (static::$IcalMethods as $imethod) {
+                        if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) {
+                            $method = $imethod;
+                            break;
+                        }
+                    }
+                    $body .= $this->getBoundary(
+                        $this->boundary[1],
+                        '',
+                        static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method,
+                        ''
+                    );
+                    $body .= $this->encodeString($this->Ical, $this->Encoding);
+                    $body .= static::$LE;
+                }
+                $body .= $this->endBoundary($this->boundary[1]);
+                break;
+            case 'alt_inline':
+                $body .= $mimepre;
+                $body .= $this->getBoundary(
+                    $this->boundary[1],
+                    $altBodyCharSet,
+                    static::CONTENT_TYPE_PLAINTEXT,
+                    $altBodyEncoding
+                );
+                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->textLine('--' . $this->boundary[1]);
+                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
+                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '";');
+                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
+                $body .= static::$LE;
+                $body .= $this->getBoundary(
+                    $this->boundary[2],
+                    $bodyCharSet,
+                    static::CONTENT_TYPE_TEXT_HTML,
+                    $bodyEncoding
+                );
+                $body .= $this->encodeString($this->Body, $bodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->attachAll('inline', $this->boundary[2]);
+                $body .= static::$LE;
+                $body .= $this->endBoundary($this->boundary[1]);
+                break;
+            case 'alt_attach':
+                $body .= $mimepre;
+                $body .= $this->textLine('--' . $this->boundary[1]);
+                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
+                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '"');
+                $body .= static::$LE;
+                $body .= $this->getBoundary(
+                    $this->boundary[2],
+                    $altBodyCharSet,
+                    static::CONTENT_TYPE_PLAINTEXT,
+                    $altBodyEncoding
+                );
+                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->getBoundary(
+                    $this->boundary[2],
+                    $bodyCharSet,
+                    static::CONTENT_TYPE_TEXT_HTML,
+                    $bodyEncoding
+                );
+                $body .= $this->encodeString($this->Body, $bodyEncoding);
+                $body .= static::$LE;
+                if (!empty($this->Ical)) {
+                    $method = static::ICAL_METHOD_REQUEST;
+                    foreach (static::$IcalMethods as $imethod) {
+                        if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) {
+                            $method = $imethod;
+                            break;
+                        }
+                    }
+                    $body .= $this->getBoundary(
+                        $this->boundary[2],
+                        '',
+                        static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method,
+                        ''
+                    );
+                    $body .= $this->encodeString($this->Ical, $this->Encoding);
+                }
+                $body .= $this->endBoundary($this->boundary[2]);
+                $body .= static::$LE;
+                $body .= $this->attachAll('attachment', $this->boundary[1]);
+                break;
+            case 'alt_inline_attach':
+                $body .= $mimepre;
+                $body .= $this->textLine('--' . $this->boundary[1]);
+                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
+                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '"');
+                $body .= static::$LE;
+                $body .= $this->getBoundary(
+                    $this->boundary[2],
+                    $altBodyCharSet,
+                    static::CONTENT_TYPE_PLAINTEXT,
+                    $altBodyEncoding
+                );
+                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->textLine('--' . $this->boundary[2]);
+                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
+                $body .= $this->textLine(' boundary="' . $this->boundary[3] . '";');
+                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
+                $body .= static::$LE;
+                $body .= $this->getBoundary(
+                    $this->boundary[3],
+                    $bodyCharSet,
+                    static::CONTENT_TYPE_TEXT_HTML,
+                    $bodyEncoding
+                );
+                $body .= $this->encodeString($this->Body, $bodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->attachAll('inline', $this->boundary[3]);
+                $body .= static::$LE;
+                $body .= $this->endBoundary($this->boundary[2]);
+                $body .= static::$LE;
+                $body .= $this->attachAll('attachment', $this->boundary[1]);
+                break;
+            default:
+                //Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types
+                //Reset the `Encoding` property in case we changed it for line length reasons
+                $this->Encoding = $bodyEncoding;
+                $body .= $this->encodeString($this->Body, $this->Encoding);
+                break;
+        }
+
+        if ($this->isError()) {
+            $body = '';
+            if ($this->exceptions) {
+                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
+            }
+        } elseif ($this->sign_key_file) {
+            try {
+                if (!defined('PKCS7_TEXT')) {
+                    throw new Exception($this->lang('extension_missing') . 'openssl');
+                }
+
+                $file = tempnam(sys_get_temp_dir(), 'srcsign');
+                $signed = tempnam(sys_get_temp_dir(), 'mailsign');
+                file_put_contents($file, $body);
+
+                //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197
+                if (empty($this->sign_extracerts_file)) {
+                    $sign = @openssl_pkcs7_sign(
+                        $file,
+                        $signed,
+                        'file://' . realpath($this->sign_cert_file),
+                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
+                        []
+                    );
+                } else {
+                    $sign = @openssl_pkcs7_sign(
+                        $file,
+                        $signed,
+                        'file://' . realpath($this->sign_cert_file),
+                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
+                        [],
+                        PKCS7_DETACHED,
+                        $this->sign_extracerts_file
+                    );
+                }
+
+                @unlink($file);
+                if ($sign) {
+                    $body = file_get_contents($signed);
+                    @unlink($signed);
+                    //The message returned by openssl contains both headers and body, so need to split them up
+                    $parts = explode("\n\n", $body, 2);
+                    $this->MIMEHeader .= $parts[0] . static::$LE . static::$LE;
+                    $body = $parts[1];
+                } else {
+                    @unlink($signed);
+                    throw new Exception($this->lang('signing') . openssl_error_string());
+                }
+            } catch (Exception $exc) {
+                $body = '';
+                if ($this->exceptions) {
+                    throw $exc;
+                }
+            }
+        }
+
+        return $body;
+    }
+
+    /**
+     * Return the start of a message boundary.
+     *
+     * @param string $boundary
+     * @param string $charSet
+     * @param string $contentType
+     * @param string $encoding
+     *
+     * @return string
+     */
+    protected function getBoundary($boundary, $charSet, $contentType, $encoding)
+    {
+        $result = '';
+        if ('' === $charSet) {
+            $charSet = $this->CharSet;
+        }
+        if ('' === $contentType) {
+            $contentType = $this->ContentType;
+        }
+        if ('' === $encoding) {
+            $encoding = $this->Encoding;
+        }
+        $result .= $this->textLine('--' . $boundary);
+        $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet);
+        $result .= static::$LE;
+        //RFC1341 part 5 says 7bit is assumed if not specified
+        if (static::ENCODING_7BIT !== $encoding) {
+            $result .= $this->headerLine('Content-Transfer-Encoding', $encoding);
+        }
+        $result .= static::$LE;
+
+        return $result;
+    }
+
+    /**
+     * Return the end of a message boundary.
+     *
+     * @param string $boundary
+     *
+     * @return string
+     */
+    protected function endBoundary($boundary)
+    {
+        return static::$LE . '--' . $boundary . '--' . static::$LE;
+    }
+
+    /**
+     * Set the message type.
+     * PHPMailer only supports some preset message types, not arbitrary MIME structures.
+     */
+    protected function setMessageType()
+    {
+        $type = [];
+        if ($this->alternativeExists()) {
+            $type[] = 'alt';
+        }
+        if ($this->inlineImageExists()) {
+            $type[] = 'inline';
+        }
+        if ($this->attachmentExists()) {
+            $type[] = 'attach';
+        }
+        $this->message_type = implode('_', $type);
+        if ('' === $this->message_type) {
+            //The 'plain' message_type refers to the message having a single body element, not that it is plain-text
+            $this->message_type = 'plain';
+        }
+    }
+
+    /**
+     * Format a header line.
+     *
+     * @param string     $name
+     * @param string|int $value
+     *
+     * @return string
+     */
+    public function headerLine($name, $value)
+    {
+        return $name . ': ' . $value . static::$LE;
+    }
+
+    /**
+     * Return a formatted mail line.
+     *
+     * @param string $value
+     *
+     * @return string
+     */
+    public function textLine($value)
+    {
+        return $value . static::$LE;
+    }
+
+    /**
+     * Add an attachment from a path on the filesystem.
+     * Never use a user-supplied path to a file!
+     * Returns false if the file could not be found or read.
+     * Explicitly *does not* support passing URLs; PHPMailer is not an HTTP client.
+     * If you need to do that, fetch the resource yourself and pass it in via a local file or string.
+     *
+     * @param string $path        Path to the attachment
+     * @param string $name        Overrides the attachment name
+     * @param string $encoding    File encoding (see $Encoding)
+     * @param string $type        MIME type, e.g. `image/jpeg`; determined automatically from $path if not specified
+     * @param string $disposition Disposition to use
+     *
+     * @throws Exception
+     *
+     * @return bool
+     */
+    public function addAttachment(
+        $path,
+        $name = '',
+        $encoding = self::ENCODING_BASE64,
+        $type = '',
+        $disposition = 'attachment'
+    ) {
+        try {
+            if (!static::fileIsAccessible($path)) {
+                throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
+            }
+
+            //If a MIME type is not specified, try to work it out from the file name
+            if ('' === $type) {
+                $type = static::filenameToType($path);
+            }
+
+            $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);
+            if ('' === $name) {
+                $name = $filename;
+            }
+            if (!$this->validateEncoding($encoding)) {
+                throw new Exception($this->lang('encoding') . $encoding);
+            }
+
+            $this->attachment[] = [
+                0 => $path,
+                1 => $filename,
+                2 => $name,
+                3 => $encoding,
+                4 => $type,
+                5 => false, //isStringAttachment
+                6 => $disposition,
+                7 => $name,
+            ];
+        } catch (Exception $exc) {
+            $this->setError($exc->getMessage());
+            $this->edebug($exc->getMessage());
+            if ($this->exceptions) {
+                throw $exc;
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Return the array of attachments.
+     *
+     * @return array
+     */
+    public function getAttachments()
+    {
+        return $this->attachment;
+    }
+
+    /**
+     * Attach all file, string, and binary attachments to the message.
+     * Returns an empty string on failure.
+     *
+     * @param string $disposition_type
+     * @param string $boundary
+     *
+     * @throws Exception
+     *
+     * @return string
+     */
+    protected function attachAll($disposition_type, $boundary)
+    {
+        //Return text of body
+        $mime = [];
+        $cidUniq = [];
+        $incl = [];
+
+        //Add all attachments
+        foreach ($this->attachment as $attachment) {
+            //Check if it is a valid disposition_filter
+            if ($attachment[6] === $disposition_type) {
+                //Check for string attachment
+                $string = '';
+                $path = '';
+                $bString = $attachment[5];
+                if ($bString) {
+                    $string = $attachment[0];
+                } else {
+                    $path = $attachment[0];
+                }
+
+                $inclhash = hash('sha256', serialize($attachment));
+                if (in_array($inclhash, $incl, true)) {
+                    continue;
+                }
+                $incl[] = $inclhash;
+                $name = $attachment[2];
+                $encoding = $attachment[3];
+                $type = $attachment[4];
+                $disposition = $attachment[6];
+                $cid = $attachment[7];
+                if ('inline' === $disposition && array_key_exists($cid, $cidUniq)) {
+                    continue;
+                }
+                $cidUniq[$cid] = true;
+
+                $mime[] = sprintf('--%s%s', $boundary, static::$LE);
+                //Only include a filename property if we have one
+                if (!empty($name)) {
+                    $mime[] = sprintf(
+                        'Content-Type: %s; name=%s%s',
+                        $type,
+                        static::quotedString($this->encodeHeader($this->secureHeader($name))),
+                        static::$LE
+                    );
+                } else {
+                    $mime[] = sprintf(
+                        'Content-Type: %s%s',
+                        $type,
+                        static::$LE
+                    );
+                }
+                //RFC1341 part 5 says 7bit is assumed if not specified
+                if (static::ENCODING_7BIT !== $encoding) {
+                    $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE);
+                }
+
+                //Only set Content-IDs on inline attachments
+                if ((string) $cid !== '' && $disposition === 'inline') {
+                    $mime[] = 'Content-ID: <' . $this->encodeHeader($this->secureHeader($cid)) . '>' . static::$LE;
+                }
+
+                //Allow for bypassing the Content-Disposition header
+                if (!empty($disposition)) {
+                    $encoded_name = $this->encodeHeader($this->secureHeader($name));
+                    if (!empty($encoded_name)) {
+                        $mime[] = sprintf(
+                            'Content-Disposition: %s; filename=%s%s',
+                            $disposition,
+                            static::quotedString($encoded_name),
+                            static::$LE . static::$LE
+                        );
+                    } else {
+                        $mime[] = sprintf(
+                            'Content-Disposition: %s%s',
+                            $disposition,
+                            static::$LE . static::$LE
+                        );
+                    }
+                } else {
+                    $mime[] = static::$LE;
+                }
+
+                //Encode as string attachment
+                if ($bString) {
+                    $mime[] = $this->encodeString($string, $encoding);
+                } else {
+                    $mime[] = $this->encodeFile($path, $encoding);
+                }
+                if ($this->isError()) {
+                    return '';
+                }
+                $mime[] = static::$LE;
+            }
+        }
+
+        $mime[] = sprintf('--%s--%s', $boundary, static::$LE);
+
+        return implode('', $mime);
+    }
+
+    /**
+     * Encode a file attachment in requested format.
+     * Returns an empty string on failure.
+     *
+     * @param string $path     The full path to the file
+     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
+     *
+     * @return string
+     */
+    protected function encodeFile($path, $encoding = self::ENCODING_BASE64)
+    {
+        try {
+            if (!static::fileIsAccessible($path)) {
+                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
+            }
+            $file_buffer = file_get_contents($path);
+            if (false === $file_buffer) {
+                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
+            }
+            $file_buffer = $this->encodeString($file_buffer, $encoding);
+
+            return $file_buffer;
+        } catch (Exception $exc) {
+            $this->setError($exc->getMessage());
+            $this->edebug($exc->getMessage());
+            if ($this->exceptions) {
+                throw $exc;
+            }
+
+            return '';
+        }
+    }
+
+    /**
+     * Encode a string in requested format.
+     * Returns an empty string on failure.
+     *
+     * @param string $str      The text to encode
+     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
+     *
+     * @throws Exception
+     *
+     * @return string
+     */
+    public function encodeString($str, $encoding = self::ENCODING_BASE64)
+    {
+        $encoded = '';
+        switch (strtolower($encoding)) {
+            case static::ENCODING_BASE64:
+                $encoded = chunk_split(
+                    base64_encode($str),
+                    static::STD_LINE_LENGTH,
+                    static::$LE
+                );
+                break;
+            case static::ENCODING_7BIT:
+            case static::ENCODING_8BIT:
+                $encoded = static::normalizeBreaks($str);
+                //Make sure it ends with a line break
+                if (substr($encoded, -(strlen(static::$LE))) !== static::$LE) {
+                    $encoded .= static::$LE;
+                }
+                break;
+            case static::ENCODING_BINARY:
+                $encoded = $str;
+                break;
+            case static::ENCODING_QUOTED_PRINTABLE:
+                $encoded = $this->encodeQP($str);
+                break;
+            default:
+                $this->setError($this->lang('encoding') . $encoding);
+                if ($this->exceptions) {
+                    throw new Exception($this->lang('encoding') . $encoding);
+                }
+                break;
+        }
+
+        return $encoded;
+    }
+
+    /**
+     * Encode a header value (not including its label) optimally.
+     * Picks shortest of Q, B, or none. Result includes folding if needed.
+     * See RFC822 definitions for phrase, comment and text positions.
+     *
+     * @param string $str      The header value to encode
+     * @param string $position What context the string will be used in
+     *
+     * @return string
+     */
+    public function encodeHeader($str, $position = 'text')
+    {
+        $matchcount = 0;
+        switch (strtolower($position)) {
+            case 'phrase':
+                if (!preg_match('/[\200-\377]/', $str)) {
+                    //Can't use addslashes as we don't know the value of magic_quotes_sybase
+                    $encoded = addcslashes($str, "\0..\37\177\\\"");
+                    if (($str === $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) {
+                        return $encoded;
+                    }
+
+                    return "\"$encoded\"";
+                }
+                $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches);
+                break;
+            /* @noinspection PhpMissingBreakStatementInspection */
+            case 'comment':
+                $matchcount = preg_match_all('/[()"]/', $str, $matches);
+            //fallthrough
+            case 'text':
+            default:
+                $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches);
+                break;
+        }
+
+        if ($this->has8bitChars($str)) {
+            $charset = $this->CharSet;
+        } else {
+            $charset = static::CHARSET_ASCII;
+        }
+
+        //Q/B encoding adds 8 chars and the charset ("` =?<charset>?[QB]?<content>?=`").
+        $overhead = 8 + strlen($charset);
+
+        if ('mail' === $this->Mailer) {
+            $maxlen = static::MAIL_MAX_LINE_LENGTH - $overhead;
+        } else {
+            $maxlen = static::MAX_LINE_LENGTH - $overhead;
+        }
+
+        //Select the encoding that produces the shortest output and/or prevents corruption.
+        if ($matchcount > strlen($str) / 3) {
+            //More than 1/3 of the content needs encoding, use B-encode.
+            $encoding = 'B';
+        } elseif ($matchcount > 0) {
+            //Less than 1/3 of the content needs encoding, use Q-encode.
+            $encoding = 'Q';
+        } elseif (strlen($str) > $maxlen) {
+            //No encoding needed, but value exceeds max line length, use Q-encode to prevent corruption.
+            $encoding = 'Q';
+        } else {
+            //No reformatting needed
+            $encoding = false;
+        }
+
+        switch ($encoding) {
+            case 'B':
+                if ($this->hasMultiBytes($str)) {
+                    //Use a custom function which correctly encodes and wraps long
+                    //multibyte strings without breaking lines within a character
+                    $encoded = $this->base64EncodeWrapMB($str, "\n");
+                } else {
+                    $encoded = base64_encode($str);
+                    $maxlen -= $maxlen % 4;
+                    $encoded = trim(chunk_split($encoded, $maxlen, "\n"));
+                }
+                $encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded);
+                break;
+            case 'Q':
+                $encoded = $this->encodeQ($str, $position);
+                $encoded = $this->wrapText($encoded, $maxlen, true);
+                $encoded = str_replace('=' . static::$LE, "\n", trim($encoded));
+                $encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded);
+                break;
+            default:
+                return $str;
+        }
+
+        return trim(static::normalizeBreaks($encoded));
+    }
+
+    /**
+     * Check if a string contains multi-byte characters.
+     *
+     * @param string $str multi-byte text to wrap encode
+     *
+     * @return bool
+     */
+    public function hasMultiBytes($str)
+    {
+        if (function_exists('mb_strlen')) {
+            return strlen($str) > mb_strlen($str, $this->CharSet);
+        }
+
+        //Assume no multibytes (we can't handle without mbstring functions anyway)
+        return false;
+    }
+
+    /**
+     * Does a string contain any 8-bit chars (in any charset)?
+     *
+     * @param string $text
+     *
+     * @return bool
+     */
+    public function has8bitChars($text)
+    {
+        return (bool) preg_match('/[\x80-\xFF]/', $text);
+    }
+
+    /**
+     * Encode and wrap long multibyte strings for mail headers
+     * without breaking lines within a character.
+     * Adapted from a function by paravoid.
+     *
+     * @see http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283
+     *
+     * @param string $str       multi-byte text to wrap encode
+     * @param string $linebreak string to use as linefeed/end-of-line
+     *
+     * @return string
+     */
+    public function base64EncodeWrapMB($str, $linebreak = null)
+    {
+        $start = '=?' . $this->CharSet . '?B?';
+        $end = '?=';
+        $encoded = '';
+        if (null === $linebreak) {
+            $linebreak = static::$LE;
+        }
+
+        $mb_length = mb_strlen($str, $this->CharSet);
+        //Each line must have length <= 75, including $start and $end
+        $length = 75 - strlen($start) - strlen($end);
+        //Average multi-byte ratio
+        $ratio = $mb_length / strlen($str);
+        //Base64 has a 4:3 ratio
+        $avgLength = floor($length * $ratio * .75);
+
+        $offset = 0;
+        for ($i = 0; $i < $mb_length; $i += $offset) {
+            $lookBack = 0;
+            do {
+                $offset = $avgLength - $lookBack;
+                $chunk = mb_substr($str, $i, $offset, $this->CharSet);
+                $chunk = base64_encode($chunk);
+                ++$lookBack;
+            } while (strlen($chunk) > $length);
+            $encoded .= $chunk . $linebreak;
+        }
+
+        //Chomp the last linefeed
+        return substr($encoded, 0, -strlen($linebreak));
+    }
+
+    /**
+     * Encode a string in quoted-printable format.
+     * According to RFC2045 section 6.7.
+     *
+     * @param string $string The text to encode
+     *
+     * @return string
+     */
+    public function encodeQP($string)
+    {
+        return static::normalizeBreaks(quoted_printable_encode($string));
+    }
+
+    /**
+     * Encode a string using Q encoding.
+     *
+     * @see http://tools.ietf.org/html/rfc2047#section-4.2
+     *
+     * @param string $str      the text to encode
+     * @param string $position Where the text is going to be used, see the RFC for what that means
+     *
+     * @return string
+     */
+    public function encodeQ($str, $position = 'text')
+    {
+        //There should not be any EOL in the string
+        $pattern = '';
+        $encoded = str_replace(["\r", "\n"], '', $str);
+        switch (strtolower($position)) {
+            case 'phrase':
+                //RFC 2047 section 5.3
+                $pattern = '^A-Za-z0-9!*+\/ -';
+                break;
+            /*
+             * RFC 2047 section 5.2.
+             * Build $pattern without including delimiters and []
+             */
+            /* @noinspection PhpMissingBreakStatementInspection */
+            case 'comment':
+                $pattern = '\(\)"';
+            /* Intentional fall through */
+            case 'text':
+            default:
+                //RFC 2047 section 5.1
+                //Replace every high ascii, control, =, ? and _ characters
+                $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern;
+                break;
+        }
+        $matches = [];
+        if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) {
+            //If the string contains an '=', make sure it's the first thing we replace
+            //so as to avoid double-encoding
+            $eqkey = array_search('=', $matches[0], true);
+            if (false !== $eqkey) {
+                unset($matches[0][$eqkey]);
+                array_unshift($matches[0], '=');
+            }
+            foreach (array_unique($matches[0]) as $char) {
+                $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded);
+            }
+        }
+        //Replace spaces with _ (more readable than =20)
+        //RFC 2047 section 4.2(2)
+        return str_replace(' ', '_', $encoded);
+    }
+
+    /**
+     * Add a string or binary attachment (non-filesystem).
+     * This method can be used to attach ascii or binary data,
+     * such as a BLOB record from a database.
+     *
+     * @param string $string      String attachment data
+     * @param string $filename    Name of the attachment
+     * @param string $encoding    File encoding (see $Encoding)
+     * @param string $type        File extension (MIME) type
+     * @param string $disposition Disposition to use
+     *
+     * @throws Exception
+     *
+     * @return bool True on successfully adding an attachment
+     */
+    public function addStringAttachment(
+        $string,
+        $filename,
+        $encoding = self::ENCODING_BASE64,
+        $type = '',
+        $disposition = 'attachment'
+    ) {
+        try {
+            //If a MIME type is not specified, try to work it out from the file name
+            if ('' === $type) {
+                $type = static::filenameToType($filename);
+            }
+
+            if (!$this->validateEncoding($encoding)) {
+                throw new Exception($this->lang('encoding') . $encoding);
+            }
+
+            //Append to $attachment array
+            $this->attachment[] = [
+                0 => $string,
+                1 => $filename,
+                2 => static::mb_pathinfo($filename, PATHINFO_BASENAME),
+                3 => $encoding,
+                4 => $type,
+                5 => true, //isStringAttachment
+                6 => $disposition,
+                7 => 0,
+            ];
+        } catch (Exception $exc) {
+            $this->setError($exc->getMessage());
+            $this->edebug($exc->getMessage());
+            if ($this->exceptions) {
+                throw $exc;
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Add an embedded (inline) attachment from a file.
+     * This can include images, sounds, and just about any other document type.
+     * These differ from 'regular' attachments in that they are intended to be
+     * displayed inline with the message, not just attached for download.
+     * This is used in HTML messages that embed the images
+     * the HTML refers to using the $cid value.
+     * Never use a user-supplied path to a file!
+     *
+     * @param string $path        Path to the attachment
+     * @param string $cid         Content ID of the attachment; Use this to reference
+     *                            the content when using an embedded image in HTML
+     * @param string $name        Overrides the attachment name
+     * @param string $encoding    File encoding (see $Encoding)
+     * @param string $type        File MIME type
+     * @param string $disposition Disposition to use
+     *
+     * @throws Exception
+     *
+     * @return bool True on successfully adding an attachment
+     */
+    public function addEmbeddedImage(
+        $path,
+        $cid,
+        $name = '',
+        $encoding = self::ENCODING_BASE64,
+        $type = '',
+        $disposition = 'inline'
+    ) {
+        try {
+            if (!static::fileIsAccessible($path)) {
+                throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
+            }
+
+            //If a MIME type is not specified, try to work it out from the file name
+            if ('' === $type) {
+                $type = static::filenameToType($path);
+            }
+
+            if (!$this->validateEncoding($encoding)) {
+                throw new Exception($this->lang('encoding') . $encoding);
+            }
+
+            $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);
+            if ('' === $name) {
+                $name = $filename;
+            }
+
+            //Append to $attachment array
+            $this->attachment[] = [
+                0 => $path,
+                1 => $filename,
+                2 => $name,
+                3 => $encoding,
+                4 => $type,
+                5 => false, //isStringAttachment
+                6 => $disposition,
+                7 => $cid,
+            ];
+        } catch (Exception $exc) {
+            $this->setError($exc->getMessage());
+            $this->edebug($exc->getMessage());
+            if ($this->exceptions) {
+                throw $exc;
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Add an embedded stringified attachment.
+     * This can include images, sounds, and just about any other document type.
+     * If your filename doesn't contain an extension, be sure to set the $type to an appropriate MIME type.
+     *
+     * @param string $string      The attachment binary data
+     * @param string $cid         Content ID of the attachment; Use this to reference
+     *                            the content when using an embedded image in HTML
+     * @param string $name        A filename for the attachment. If this contains an extension,
+     *                            PHPMailer will attempt to set a MIME type for the attachment.
+     *                            For example 'file.jpg' would get an 'image/jpeg' MIME type.
+     * @param string $encoding    File encoding (see $Encoding), defaults to 'base64'
+     * @param string $type        MIME type - will be used in preference to any automatically derived type
+     * @param string $disposition Disposition to use
+     *
+     * @throws Exception
+     *
+     * @return bool True on successfully adding an attachment
+     */
+    public function addStringEmbeddedImage(
+        $string,
+        $cid,
+        $name = '',
+        $encoding = self::ENCODING_BASE64,
+        $type = '',
+        $disposition = 'inline'
+    ) {
+        try {
+            //If a MIME type is not specified, try to work it out from the name
+            if ('' === $type && !empty($name)) {
+                $type = static::filenameToType($name);
+            }
+
+            if (!$this->validateEncoding($encoding)) {
+                throw new Exception($this->lang('encoding') . $encoding);
+            }
+
+            //Append to $attachment array
+            $this->attachment[] = [
+                0 => $string,
+                1 => $name,
+                2 => $name,
+                3 => $encoding,
+                4 => $type,
+                5 => true, //isStringAttachment
+                6 => $disposition,
+                7 => $cid,
+            ];
+        } catch (Exception $exc) {
+            $this->setError($exc->getMessage());
+            $this->edebug($exc->getMessage());
+            if ($this->exceptions) {
+                throw $exc;
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Validate encodings.
+     *
+     * @param string $encoding
+     *
+     * @return bool
+     */
+    protected function validateEncoding($encoding)
+    {
+        return in_array(
+            $encoding,
+            [
+                self::ENCODING_7BIT,
+                self::ENCODING_QUOTED_PRINTABLE,
+                self::ENCODING_BASE64,
+                self::ENCODING_8BIT,
+                self::ENCODING_BINARY,
+            ],
+            true
+        );
+    }
+
+    /**
+     * Check if an embedded attachment is present with this cid.
+     *
+     * @param string $cid
+     *
+     * @return bool
+     */
+    protected function cidExists($cid)
+    {
+        foreach ($this->attachment as $attachment) {
+            if ('inline' === $attachment[6] && $cid === $attachment[7]) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Check if an inline attachment is present.
+     *
+     * @return bool
+     */
+    public function inlineImageExists()
+    {
+        foreach ($this->attachment as $attachment) {
+            if ('inline' === $attachment[6]) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Check if an attachment (non-inline) is present.
+     *
+     * @return bool
+     */
+    public function attachmentExists()
+    {
+        foreach ($this->attachment as $attachment) {
+            if ('attachment' === $attachment[6]) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Check if this message has an alternative body set.
+     *
+     * @return bool
+     */
+    public function alternativeExists()
+    {
+        return !empty($this->AltBody);
+    }
+
+    /**
+     * Clear queued addresses of given kind.
+     *
+     * @param string $kind 'to', 'cc', or 'bcc'
+     */
+    public function clearQueuedAddresses($kind)
+    {
+        $this->RecipientsQueue = array_filter(
+            $this->RecipientsQueue,
+            static function ($params) use ($kind) {
+                return $params[0] !== $kind;
+            }
+        );
+    }
+
+    /**
+     * Clear all To recipients.
+     */
+    public function clearAddresses()
+    {
+        foreach ($this->to as $to) {
+            unset($this->all_recipients[strtolower($to[0])]);
+        }
+        $this->to = [];
+        $this->clearQueuedAddresses('to');
+    }
+
+    /**
+     * Clear all CC recipients.
+     */
+    public function clearCCs()
+    {
+        foreach ($this->cc as $cc) {
+            unset($this->all_recipients[strtolower($cc[0])]);
+        }
+        $this->cc = [];
+        $this->clearQueuedAddresses('cc');
+    }
+
+    /**
+     * Clear all BCC recipients.
+     */
+    public function clearBCCs()
+    {
+        foreach ($this->bcc as $bcc) {
+            unset($this->all_recipients[strtolower($bcc[0])]);
+        }
+        $this->bcc = [];
+        $this->clearQueuedAddresses('bcc');
+    }
+
+    /**
+     * Clear all ReplyTo recipients.
+     */
+    public function clearReplyTos()
+    {
+        $this->ReplyTo = [];
+        $this->ReplyToQueue = [];
+    }
+
+    /**
+     * Clear all recipient types.
+     */
+    public function clearAllRecipients()
+    {
+        $this->to = [];
+        $this->cc = [];
+        $this->bcc = [];
+        $this->all_recipients = [];
+        $this->RecipientsQueue = [];
+    }
+
+    /**
+     * Clear all filesystem, string, and binary attachments.
+     */
+    public function clearAttachments()
+    {
+        $this->attachment = [];
+    }
+
+    /**
+     * Clear all custom headers.
+     */
+    public function clearCustomHeaders()
+    {
+        $this->CustomHeader = [];
+    }
+
+    /**
+     * Add an error message to the error container.
+     *
+     * @param string $msg
+     */
+    protected function setError($msg)
+    {
+        ++$this->error_count;
+        if ('smtp' === $this->Mailer && null !== $this->smtp) {
+            $lasterror = $this->smtp->getError();
+            if (!empty($lasterror['error'])) {
+                $msg .= $this->lang('smtp_error') . $lasterror['error'];
+                if (!empty($lasterror['detail'])) {
+                    $msg .= ' ' . $this->lang('smtp_detail') . $lasterror['detail'];
+                }
+                if (!empty($lasterror['smtp_code'])) {
+                    $msg .= ' ' . $this->lang('smtp_code') . $lasterror['smtp_code'];
+                }
+                if (!empty($lasterror['smtp_code_ex'])) {
+                    $msg .= ' ' . $this->lang('smtp_code_ex') . $lasterror['smtp_code_ex'];
+                }
+            }
+        }
+        $this->ErrorInfo = $msg;
+    }
+
+    /**
+     * Return an RFC 822 formatted date.
+     *
+     * @return string
+     */
+    public static function rfcDate()
+    {
+        //Set the time zone to whatever the default is to avoid 500 errors
+        //Will default to UTC if it's not set properly in php.ini
+        date_default_timezone_set(@date_default_timezone_get());
+
+        return date('D, j M Y H:i:s O');
+    }
+
+    /**
+     * Get the server hostname.
+     * Returns 'localhost.localdomain' if unknown.
+     *
+     * @return string
+     */
+    protected function serverHostname()
+    {
+        $result = '';
+        if (!empty($this->Hostname)) {
+            $result = $this->Hostname;
+        } elseif (isset($_SERVER) && array_key_exists('SERVER_NAME', $_SERVER)) {
+            $result = $_SERVER['SERVER_NAME'];
+        } elseif (function_exists('gethostname') && gethostname() !== false) {
+            $result = gethostname();
+        } elseif (php_uname('n') !== false) {
+            $result = php_uname('n');
+        }
+        if (!static::isValidHost($result)) {
+            return 'localhost.localdomain';
+        }
+
+        return $result;
+    }
+
+    /**
+     * Validate whether a string contains a valid value to use as a hostname or IP address.
+     * IPv6 addresses must include [], e.g. `[::1]`, not just `::1`.
+     *
+     * @param string $host The host name or IP address to check
+     *
+     * @return bool
+     */
+    public static function isValidHost($host)
+    {
+        //Simple syntax limits
+        if (
+            empty($host)
+            || !is_string($host)
+            || strlen($host) > 256
+            || !preg_match('/^([a-zA-Z\d.-]*|\[[a-fA-F\d:]+\])$/', $host)
+        ) {
+            return false;
+        }
+        //Looks like a bracketed IPv6 address
+        if (strlen($host) > 2 && substr($host, 0, 1) === '[' && substr($host, -1, 1) === ']') {
+            return filter_var(substr($host, 1, -1), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
+        }
+        //If removing all the dots results in a numeric string, it must be an IPv4 address.
+        //Need to check this first because otherwise things like `999.0.0.0` are considered valid host names
+        if (is_numeric(str_replace('.', '', $host))) {
+            //Is it a valid IPv4 address?
+            return filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
+        }
+        if (filter_var('http://' . $host, FILTER_VALIDATE_URL) !== false) {
+            //Is it a syntactically valid hostname?
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Get an error message in the current language.
+     *
+     * @param string $key
+     *
+     * @return string
+     */
+    protected function lang($key)
+    {
+        if (count($this->language) < 1) {
+            $this->setLanguage(); //Set the default language
+        }
+
+        if (array_key_exists($key, $this->language)) {
+            if ('smtp_connect_failed' === $key) {
+                //Include a link to troubleshooting docs on SMTP connection failure.
+                //This is by far the biggest cause of support questions
+                //but it's usually not PHPMailer's fault.
+                return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting';
+            }
+
+            return $this->language[$key];
+        }
+
+        //Return the key as a fallback
+        return $key;
+    }
+
+    /**
+     * Check if an error occurred.
+     *
+     * @return bool True if an error did occur
+     */
+    public function isError()
+    {
+        return $this->error_count > 0;
+    }
+
+    /**
+     * Add a custom header.
+     * $name value can be overloaded to contain
+     * both header name and value (name:value).
+     *
+     * @param string      $name  Custom header name
+     * @param string|null $value Header value
+     *
+     * @throws Exception
+     */
+    public function addCustomHeader($name, $value = null)
+    {
+        if (null === $value && strpos($name, ':') !== false) {
+            //Value passed in as name:value
+            list($name, $value) = explode(':', $name, 2);
+        }
+        $name = trim($name);
+        $value = (null === $value) ? '' : trim($value);
+        //Ensure name is not empty, and that neither name nor value contain line breaks
+        if (empty($name) || strpbrk($name . $value, "\r\n") !== false) {
+            if ($this->exceptions) {
+                throw new Exception($this->lang('invalid_header'));
+            }
+
+            return false;
+        }
+        $this->CustomHeader[] = [$name, $value];
+
+        return true;
+    }
+
+    /**
+     * Returns all custom headers.
+     *
+     * @return array
+     */
+    public function getCustomHeaders()
+    {
+        return $this->CustomHeader;
+    }
+
+    /**
+     * Create a message body from an HTML string.
+     * Automatically inlines images and creates a plain-text version by converting the HTML,
+     * overwriting any existing values in Body and AltBody.
+     * Do not source $message content from user input!
+     * $basedir is prepended when handling relative URLs, e.g. <img src="/images/a.png"> and must not be empty
+     * will look for an image file in $basedir/images/a.png and convert it to inline.
+     * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email)
+     * Converts data-uri images into embedded attachments.
+     * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly.
+     *
+     * @param string        $message  HTML message string
+     * @param string        $basedir  Absolute path to a base directory to prepend to relative paths to images
+     * @param bool|callable $advanced Whether to use the internal HTML to text converter
+     *                                or your own custom converter
+     * @return string The transformed message body
+     *
+     * @throws Exception
+     *
+     * @see PHPMailer::html2text()
+     */
+    public function msgHTML($message, $basedir = '', $advanced = false)
+    {
+        preg_match_all('/(?<!-)(src|background)=["\'](.*)["\']/Ui', $message, $images);
+        if (array_key_exists(2, $images)) {
+            if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
+                //Ensure $basedir has a trailing /
+                $basedir .= '/';
+            }
+            foreach ($images[2] as $imgindex => $url) {
+                //Convert data URIs into embedded images
+                //e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+                $match = [];
+                if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) {
+                    if (count($match) === 4 && static::ENCODING_BASE64 === $match[2]) {
+                        $data = base64_decode($match[3]);
+                    } elseif ('' === $match[2]) {
+                        $data = rawurldecode($match[3]);
+                    } else {
+                        //Not recognised so leave it alone
+                        continue;
+                    }
+                    //Hash the decoded data, not the URL, so that the same data-URI image used in multiple places
+                    //will only be embedded once, even if it used a different encoding
+                    $cid = substr(hash('sha256', $data), 0, 32) . '@phpmailer.0'; //RFC2392 S 2
+
+                    if (!$this->cidExists($cid)) {
+                        $this->addStringEmbeddedImage(
+                            $data,
+                            $cid,
+                            'embed' . $imgindex,
+                            static::ENCODING_BASE64,
+                            $match[1]
+                        );
+                    }
+                    $message = str_replace(
+                        $images[0][$imgindex],
+                        $images[1][$imgindex] . '="cid:' . $cid . '"',
+                        $message
+                    );
+                    continue;
+                }
+                if (
+                    //Only process relative URLs if a basedir is provided (i.e. no absolute local paths)
+                    !empty($basedir)
+                    //Ignore URLs containing parent dir traversal (..)
+                    && (strpos($url, '..') === false)
+                    //Do not change urls that are already inline images
+                    && 0 !== strpos($url, 'cid:')
+                    //Do not change absolute URLs, including anonymous protocol
+                    && !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url)
+                ) {
+                    $filename = static::mb_pathinfo($url, PATHINFO_BASENAME);
+                    $directory = dirname($url);
+                    if ('.' === $directory) {
+                        $directory = '';
+                    }
+                    //RFC2392 S 2
+                    $cid = substr(hash('sha256', $url), 0, 32) . '@phpmailer.0';
+                    if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
+                        $basedir .= '/';
+                    }
+                    if (strlen($directory) > 1 && '/' !== substr($directory, -1)) {
+                        $directory .= '/';
+                    }
+                    if (
+                        $this->addEmbeddedImage(
+                            $basedir . $directory . $filename,
+                            $cid,
+                            $filename,
+                            static::ENCODING_BASE64,
+                            static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION))
+                        )
+                    ) {
+                        $message = preg_replace(
+                            '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui',
+                            $images[1][$imgindex] . '="cid:' . $cid . '"',
+                            $message
+                        );
+                    }
+                }
+            }
+        }
+        $this->isHTML();
+        //Convert all message body line breaks to LE, makes quoted-printable encoding work much better
+        $this->Body = static::normalizeBreaks($message);
+        $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced));
+        if (!$this->alternativeExists()) {
+            $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.'
+                . static::$LE;
+        }
+
+        return $this->Body;
+    }
+
+    /**
+     * Convert an HTML string into plain text.
+     * This is used by msgHTML().
+     * Note - older versions of this function used a bundled advanced converter
+     * which was removed for license reasons in #232.
+     * Example usage:
+     *
+     * ```php
+     * //Use default conversion
+     * $plain = $mail->html2text($html);
+     * //Use your own custom converter
+     * $plain = $mail->html2text($html, function($html) {
+     *     $converter = new MyHtml2text($html);
+     *     return $converter->get_text();
+     * });
+     * ```
+     *
+     * @param string        $html     The HTML text to convert
+     * @param bool|callable $advanced Any boolean value to use the internal converter,
+     *                                or provide your own callable for custom conversion.
+     *                                *Never* pass user-supplied data into this parameter
+     *
+     * @return string
+     */
+    public function html2text($html, $advanced = false)
+    {
+        if (is_callable($advanced)) {
+            return call_user_func($advanced, $html);
+        }
+
+        return html_entity_decode(
+            trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
+            ENT_QUOTES,
+            $this->CharSet
+        );
+    }
+
+    /**
+     * Get the MIME type for a file extension.
+     *
+     * @param string $ext File extension
+     *
+     * @return string MIME type of file
+     */
+    public static function _mime_types($ext = '')
+    {
+        $mimes = [
+            'xl' => 'application/excel',
+            'js' => 'application/javascript',
+            'hqx' => 'application/mac-binhex40',
+            'cpt' => 'application/mac-compactpro',
+            'bin' => 'application/macbinary',
+            'doc' => 'application/msword',
+            'word' => 'application/msword',
+            'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+            'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
+            'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
+            'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
+            'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+            'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
+            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+            'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
+            'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
+            'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
+            'class' => 'application/octet-stream',
+            'dll' => 'application/octet-stream',
+            'dms' => 'application/octet-stream',
+            'exe' => 'application/octet-stream',
+            'lha' => 'application/octet-stream',
+            'lzh' => 'application/octet-stream',
+            'psd' => 'application/octet-stream',
+            'sea' => 'application/octet-stream',
+            'so' => 'application/octet-stream',
+            'oda' => 'application/oda',
+            'pdf' => 'application/pdf',
+            'ai' => 'application/postscript',
+            'eps' => 'application/postscript',
+            'ps' => 'application/postscript',
+            'smi' => 'application/smil',
+            'smil' => 'application/smil',
+            'mif' => 'application/vnd.mif',
+            'xls' => 'application/vnd.ms-excel',
+            'ppt' => 'application/vnd.ms-powerpoint',
+            'wbxml' => 'application/vnd.wap.wbxml',
+            'wmlc' => 'application/vnd.wap.wmlc',
+            'dcr' => 'application/x-director',
+            'dir' => 'application/x-director',
+            'dxr' => 'application/x-director',
+            'dvi' => 'application/x-dvi',
+            'gtar' => 'application/x-gtar',
+            'php3' => 'application/x-httpd-php',
+            'php4' => 'application/x-httpd-php',
+            'php' => 'application/x-httpd-php',
+            'phtml' => 'application/x-httpd-php',
+            'phps' => 'application/x-httpd-php-source',
+            'swf' => 'application/x-shockwave-flash',
+            'sit' => 'application/x-stuffit',
+            'tar' => 'application/x-tar',
+            'tgz' => 'application/x-tar',
+            'xht' => 'application/xhtml+xml',
+            'xhtml' => 'application/xhtml+xml',
+            'zip' => 'application/zip',
+            'mid' => 'audio/midi',
+            'midi' => 'audio/midi',
+            'mp2' => 'audio/mpeg',
+            'mp3' => 'audio/mpeg',
+            'm4a' => 'audio/mp4',
+            'mpga' => 'audio/mpeg',
+            'aif' => 'audio/x-aiff',
+            'aifc' => 'audio/x-aiff',
+            'aiff' => 'audio/x-aiff',
+            'ram' => 'audio/x-pn-realaudio',
+            'rm' => 'audio/x-pn-realaudio',
+            'rpm' => 'audio/x-pn-realaudio-plugin',
+            'ra' => 'audio/x-realaudio',
+            'wav' => 'audio/x-wav',
+            'mka' => 'audio/x-matroska',
+            'bmp' => 'image/bmp',
+            'gif' => 'image/gif',
+            'jpeg' => 'image/jpeg',
+            'jpe' => 'image/jpeg',
+            'jpg' => 'image/jpeg',
+            'png' => 'image/png',
+            'tiff' => 'image/tiff',
+            'tif' => 'image/tiff',
+            'webp' => 'image/webp',
+            'avif' => 'image/avif',
+            'heif' => 'image/heif',
+            'heifs' => 'image/heif-sequence',
+            'heic' => 'image/heic',
+            'heics' => 'image/heic-sequence',
+            'eml' => 'message/rfc822',
+            'css' => 'text/css',
+            'html' => 'text/html',
+            'htm' => 'text/html',
+            'shtml' => 'text/html',
+            'log' => 'text/plain',
+            'text' => 'text/plain',
+            'txt' => 'text/plain',
+            'rtx' => 'text/richtext',
+            'rtf' => 'text/rtf',
+            'vcf' => 'text/vcard',
+            'vcard' => 'text/vcard',
+            'ics' => 'text/calendar',
+            'xml' => 'text/xml',
+            'xsl' => 'text/xml',
+            'wmv' => 'video/x-ms-wmv',
+            'mpeg' => 'video/mpeg',
+            'mpe' => 'video/mpeg',
+            'mpg' => 'video/mpeg',
+            'mp4' => 'video/mp4',
+            'm4v' => 'video/mp4',
+            'mov' => 'video/quicktime',
+            'qt' => 'video/quicktime',
+            'rv' => 'video/vnd.rn-realvideo',
+            'avi' => 'video/x-msvideo',
+            'movie' => 'video/x-sgi-movie',
+            'webm' => 'video/webm',
+            'mkv' => 'video/x-matroska',
+        ];
+        $ext = strtolower($ext);
+        if (array_key_exists($ext, $mimes)) {
+            return $mimes[$ext];
+        }
+
+        return 'application/octet-stream';
+    }
+
+    /**
+     * Map a file name to a MIME type.
+     * Defaults to 'application/octet-stream', i.e.. arbitrary binary data.
+     *
+     * @param string $filename A file name or full path, does not need to exist as a file
+     *
+     * @return string
+     */
+    public static function filenameToType($filename)
+    {
+        //In case the path is a URL, strip any query string before getting extension
+        $qpos = strpos($filename, '?');
+        if (false !== $qpos) {
+            $filename = substr($filename, 0, $qpos);
+        }
+        $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION);
+
+        return static::_mime_types($ext);
+    }
+
+    /**
+     * Multi-byte-safe pathinfo replacement.
+     * Drop-in replacement for pathinfo(), but multibyte- and cross-platform-safe.
+     *
+     * @see http://www.php.net/manual/en/function.pathinfo.php#107461
+     *
+     * @param string     $path    A filename or path, does not need to exist as a file
+     * @param int|string $options Either a PATHINFO_* constant,
+     *                            or a string name to return only the specified piece
+     *
+     * @return string|array
+     */
+    public static function mb_pathinfo($path, $options = null)
+    {
+        $ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''];
+        $pathinfo = [];
+        if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^.\\\\/]+?)|))[\\\\/.]*$#m', $path, $pathinfo)) {
+            if (array_key_exists(1, $pathinfo)) {
+                $ret['dirname'] = $pathinfo[1];
+            }
+            if (array_key_exists(2, $pathinfo)) {
+                $ret['basename'] = $pathinfo[2];
+            }
+            if (array_key_exists(5, $pathinfo)) {
+                $ret['extension'] = $pathinfo[5];
+            }
+            if (array_key_exists(3, $pathinfo)) {
+                $ret['filename'] = $pathinfo[3];
+            }
+        }
+        switch ($options) {
+            case PATHINFO_DIRNAME:
+            case 'dirname':
+                return $ret['dirname'];
+            case PATHINFO_BASENAME:
+            case 'basename':
+                return $ret['basename'];
+            case PATHINFO_EXTENSION:
+            case 'extension':
+                return $ret['extension'];
+            case PATHINFO_FILENAME:
+            case 'filename':
+                return $ret['filename'];
+            default:
+                return $ret;
+        }
+    }
+
+    /**
+     * Set or reset instance properties.
+     * You should avoid this function - it's more verbose, less efficient, more error-prone and
+     * harder to debug than setting properties directly.
+     * Usage Example:
+     * `$mail->set('SMTPSecure', static::ENCRYPTION_STARTTLS);`
+     *   is the same as:
+     * `$mail->SMTPSecure = static::ENCRYPTION_STARTTLS;`.
+     *
+     * @param string $name  The property name to set
+     * @param mixed  $value The value to set the property to
+     *
+     * @return bool
+     */
+    public function set($name, $value = '')
+    {
+        if (property_exists($this, $name)) {
+            $this->$name = $value;
+
+            return true;
+        }
+        $this->setError($this->lang('variable_set') . $name);
+
+        return false;
+    }
+
+    /**
+     * Strip newlines to prevent header injection.
+     *
+     * @param string $str
+     *
+     * @return string
+     */
+    public function secureHeader($str)
+    {
+        return trim(str_replace(["\r", "\n"], '', $str));
+    }
+
+    /**
+     * Normalize line breaks in a string.
+     * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format.
+     * Defaults to CRLF (for message bodies) and preserves consecutive breaks.
+     *
+     * @param string $text
+     * @param string $breaktype What kind of line break to use; defaults to static::$LE
+     *
+     * @return string
+     */
+    public static function normalizeBreaks($text, $breaktype = null)
+    {
+        if (null === $breaktype) {
+            $breaktype = static::$LE;
+        }
+        //Normalise to \n
+        $text = str_replace([self::CRLF, "\r"], "\n", $text);
+        //Now convert LE as needed
+        if ("\n" !== $breaktype) {
+            $text = str_replace("\n", $breaktype, $text);
+        }
+
+        return $text;
+    }
+
+    /**
+     * Remove trailing breaks from a string.
+     *
+     * @param string $text
+     *
+     * @return string The text to remove breaks from
+     */
+    public static function stripTrailingWSP($text)
+    {
+        return rtrim($text, " \r\n\t");
+    }
+
+    /**
+     * Return the current line break format string.
+     *
+     * @return string
+     */
+    public static function getLE()
+    {
+        return static::$LE;
+    }
+
+    /**
+     * Set the line break format string, e.g. "\r\n".
+     *
+     * @param string $le
+     */
+    protected static function setLE($le)
+    {
+        static::$LE = $le;
+    }
+
+    /**
+     * Set the public and private key files and password for S/MIME signing.
+     *
+     * @param string $cert_filename
+     * @param string $key_filename
+     * @param string $key_pass            Password for private key
+     * @param string $extracerts_filename Optional path to chain certificate
+     */
+    public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '')
+    {
+        $this->sign_cert_file = $cert_filename;
+        $this->sign_key_file = $key_filename;
+        $this->sign_key_pass = $key_pass;
+        $this->sign_extracerts_file = $extracerts_filename;
+    }
+
+    /**
+     * Quoted-Printable-encode a DKIM header.
+     *
+     * @param string $txt
+     *
+     * @return string
+     */
+    public function DKIM_QP($txt)
+    {
+        $line = '';
+        $len = strlen($txt);
+        for ($i = 0; $i < $len; ++$i) {
+            $ord = ord($txt[$i]);
+            if (((0x21 <= $ord) && ($ord <= 0x3A)) || $ord === 0x3C || ((0x3E <= $ord) && ($ord <= 0x7E))) {
+                $line .= $txt[$i];
+            } else {
+                $line .= '=' . sprintf('%02X', $ord);
+            }
+        }
+
+        return $line;
+    }
+
+    /**
+     * Generate a DKIM signature.
+     *
+     * @param string $signHeader
+     *
+     * @throws Exception
+     *
+     * @return string The DKIM signature value
+     */
+    public function DKIM_Sign($signHeader)
+    {
+        if (!defined('PKCS7_TEXT')) {
+            if ($this->exceptions) {
+                throw new Exception($this->lang('extension_missing') . 'openssl');
+            }
+
+            return '';
+        }
+        $privKeyStr = !empty($this->DKIM_private_string) ?
+            $this->DKIM_private_string :
+            file_get_contents($this->DKIM_private);
+        if ('' !== $this->DKIM_passphrase) {
+            $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase);
+        } else {
+            $privKey = openssl_pkey_get_private($privKeyStr);
+        }
+        if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) {
+            if (\PHP_MAJOR_VERSION < 8) {
+                openssl_pkey_free($privKey);
+            }
+
+            return base64_encode($signature);
+        }
+        if (\PHP_MAJOR_VERSION < 8) {
+            openssl_pkey_free($privKey);
+        }
+
+        return '';
+    }
+
+    /**
+     * Generate a DKIM canonicalization header.
+     * Uses the 'relaxed' algorithm from RFC6376 section 3.4.2.
+     * Canonicalized headers should *always* use CRLF, regardless of mailer setting.
+     *
+     * @see https://tools.ietf.org/html/rfc6376#section-3.4.2
+     *
+     * @param string $signHeader Header
+     *
+     * @return string
+     */
+    public function DKIM_HeaderC($signHeader)
+    {
+        //Normalize breaks to CRLF (regardless of the mailer)
+        $signHeader = static::normalizeBreaks($signHeader, self::CRLF);
+        //Unfold header lines
+        //Note PCRE \s is too broad a definition of whitespace; RFC5322 defines it as `[ \t]`
+        //@see https://tools.ietf.org/html/rfc5322#section-2.2
+        //That means this may break if you do something daft like put vertical tabs in your headers.
+        $signHeader = preg_replace('/\r\n[ \t]+/', ' ', $signHeader);
+        //Break headers out into an array
+        $lines = explode(self::CRLF, $signHeader);
+        foreach ($lines as $key => $line) {
+            //If the header is missing a :, skip it as it's invalid
+            //This is likely to happen because the explode() above will also split
+            //on the trailing LE, leaving an empty line
+            if (strpos($line, ':') === false) {
+                continue;
+            }
+            list($heading, $value) = explode(':', $line, 2);
+            //Lower-case header name
+            $heading = strtolower($heading);
+            //Collapse white space within the value, also convert WSP to space
+            $value = preg_replace('/[ \t]+/', ' ', $value);
+            //RFC6376 is slightly unclear here - it says to delete space at the *end* of each value
+            //But then says to delete space before and after the colon.
+            //Net result is the same as trimming both ends of the value.
+            //By elimination, the same applies to the field name
+            $lines[$key] = trim($heading, " \t") . ':' . trim($value, " \t");
+        }
+
+        return implode(self::CRLF, $lines);
+    }
+
+    /**
+     * Generate a DKIM canonicalization body.
+     * Uses the 'simple' algorithm from RFC6376 section 3.4.3.
+     * Canonicalized bodies should *always* use CRLF, regardless of mailer setting.
+     *
+     * @see https://tools.ietf.org/html/rfc6376#section-3.4.3
+     *
+     * @param string $body Message Body
+     *
+     * @return string
+     */
+    public function DKIM_BodyC($body)
+    {
+        if (empty($body)) {
+            return self::CRLF;
+        }
+        //Normalize line endings to CRLF
+        $body = static::normalizeBreaks($body, self::CRLF);
+
+        //Reduce multiple trailing line breaks to a single one
+        return static::stripTrailingWSP($body) . self::CRLF;
+    }
+
+    /**
+     * Create the DKIM header and body in a new message header.
+     *
+     * @param string $headers_line Header lines
+     * @param string $subject      Subject
+     * @param string $body         Body
+     *
+     * @throws Exception
+     *
+     * @return string
+     */
+    public function DKIM_Add($headers_line, $subject, $body)
+    {
+        $DKIMsignatureType = 'rsa-sha256'; //Signature & hash algorithms
+        $DKIMcanonicalization = 'relaxed/simple'; //Canonicalization methods of header & body
+        $DKIMquery = 'dns/txt'; //Query method
+        $DKIMtime = time();
+        //Always sign these headers without being asked
+        //Recommended list from https://tools.ietf.org/html/rfc6376#section-5.4.1
+        $autoSignHeaders = [
+            'from',
+            'to',
+            'cc',
+            'date',
+            'subject',
+            'reply-to',
+            'message-id',
+            'content-type',
+            'mime-version',
+            'x-mailer',
+        ];
+        if (stripos($headers_line, 'Subject') === false) {
+            $headers_line .= 'Subject: ' . $subject . static::$LE;
+        }
+        $headerLines = explode(static::$LE, $headers_line);
+        $currentHeaderLabel = '';
+        $currentHeaderValue = '';
+        $parsedHeaders = [];
+        $headerLineIndex = 0;
+        $headerLineCount = count($headerLines);
+        foreach ($headerLines as $headerLine) {
+            $matches = [];
+            if (preg_match('/^([^ \t]*?)(?::[ \t]*)(.*)$/', $headerLine, $matches)) {
+                if ($currentHeaderLabel !== '') {
+                    //We were previously in another header; This is the start of a new header, so save the previous one
+                    $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
+                }
+                $currentHeaderLabel = $matches[1];
+                $currentHeaderValue = $matches[2];
+            } elseif (preg_match('/^[ \t]+(.*)$/', $headerLine, $matches)) {
+                //This is a folded continuation of the current header, so unfold it
+                $currentHeaderValue .= ' ' . $matches[1];
+            }
+            ++$headerLineIndex;
+            if ($headerLineIndex >= $headerLineCount) {
+                //This was the last line, so finish off this header
+                $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
+            }
+        }
+        $copiedHeaders = [];
+        $headersToSignKeys = [];
+        $headersToSign = [];
+        foreach ($parsedHeaders as $header) {
+            //Is this header one that must be included in the DKIM signature?
+            if (in_array(strtolower($header['label']), $autoSignHeaders, true)) {
+                $headersToSignKeys[] = $header['label'];
+                $headersToSign[] = $header['label'] . ': ' . $header['value'];
+                if ($this->DKIM_copyHeaderFields) {
+                    $copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC
+                        str_replace('|', '=7C', $this->DKIM_QP($header['value']));
+                }
+                continue;
+            }
+            //Is this an extra custom header we've been asked to sign?
+            if (in_array($header['label'], $this->DKIM_extraHeaders, true)) {
+                //Find its value in custom headers
+                foreach ($this->CustomHeader as $customHeader) {
+                    if ($customHeader[0] === $header['label']) {
+                        $headersToSignKeys[] = $header['label'];
+                        $headersToSign[] = $header['label'] . ': ' . $header['value'];
+                        if ($this->DKIM_copyHeaderFields) {
+                            $copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC
+                                str_replace('|', '=7C', $this->DKIM_QP($header['value']));
+                        }
+                        //Skip straight to the next header
+                        continue 2;
+                    }
+                }
+            }
+        }
+        $copiedHeaderFields = '';
+        if ($this->DKIM_copyHeaderFields && count($copiedHeaders) > 0) {
+            //Assemble a DKIM 'z' tag
+            $copiedHeaderFields = ' z=';
+            $first = true;
+            foreach ($copiedHeaders as $copiedHeader) {
+                if (!$first) {
+                    $copiedHeaderFields .= static::$LE . ' |';
+                }
+                //Fold long values
+                if (strlen($copiedHeader) > self::STD_LINE_LENGTH - 3) {
+                    $copiedHeaderFields .= substr(
+                        chunk_split($copiedHeader, self::STD_LINE_LENGTH - 3, static::$LE . self::FWS),
+                        0,
+                        -strlen(static::$LE . self::FWS)
+                    );
+                } else {
+                    $copiedHeaderFields .= $copiedHeader;
+                }
+                $first = false;
+            }
+            $copiedHeaderFields .= ';' . static::$LE;
+        }
+        $headerKeys = ' h=' . implode(':', $headersToSignKeys) . ';' . static::$LE;
+        $headerValues = implode(static::$LE, $headersToSign);
+        $body = $this->DKIM_BodyC($body);
+        //Base64 of packed binary SHA-256 hash of body
+        $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body)));
+        $ident = '';
+        if ('' !== $this->DKIM_identity) {
+            $ident = ' i=' . $this->DKIM_identity . ';' . static::$LE;
+        }
+        //The DKIM-Signature header is included in the signature *except for* the value of the `b` tag
+        //which is appended after calculating the signature
+        //https://tools.ietf.org/html/rfc6376#section-3.5
+        $dkimSignatureHeader = 'DKIM-Signature: v=1;' .
+            ' d=' . $this->DKIM_domain . ';' .
+            ' s=' . $this->DKIM_selector . ';' . static::$LE .
+            ' a=' . $DKIMsignatureType . ';' .
+            ' q=' . $DKIMquery . ';' .
+            ' t=' . $DKIMtime . ';' .
+            ' c=' . $DKIMcanonicalization . ';' . static::$LE .
+            $headerKeys .
+            $ident .
+            $copiedHeaderFields .
+            ' bh=' . $DKIMb64 . ';' . static::$LE .
+            ' b=';
+        //Canonicalize the set of headers
+        $canonicalizedHeaders = $this->DKIM_HeaderC(
+            $headerValues . static::$LE . $dkimSignatureHeader
+        );
+        $signature = $this->DKIM_Sign($canonicalizedHeaders);
+        $signature = trim(chunk_split($signature, self::STD_LINE_LENGTH - 3, static::$LE . self::FWS));
+
+        return static::normalizeBreaks($dkimSignatureHeader . $signature);
+    }
+
+    /**
+     * Detect if a string contains a line longer than the maximum line length
+     * allowed by RFC 2822 section 2.1.1.
+     *
+     * @param string $str
+     *
+     * @return bool
+     */
+    public static function hasLineLongerThanMax($str)
+    {
+        return (bool) preg_match('/^(.{' . (self::MAX_LINE_LENGTH + strlen(static::$LE)) . ',})/m', $str);
+    }
+
+    /**
+     * If a string contains any "special" characters, double-quote the name,
+     * and escape any double quotes with a backslash.
+     *
+     * @param string $str
+     *
+     * @return string
+     *
+     * @see RFC822 3.4.1
+     */
+    public static function quotedString($str)
+    {
+        if (preg_match('/[ ()<>@,;:"\/\[\]?=]/', $str)) {
+            //If the string contains any of these chars, it must be double-quoted
+            //and any double quotes must be escaped with a backslash
+            return '"' . str_replace('"', '\\"', $str) . '"';
+        }
+
+        //Return the string untouched, it doesn't need quoting
+        return $str;
+    }
+
+    /**
+     * Allows for public read access to 'to' property.
+     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
+     *
+     * @return array
+     */
+    public function getToAddresses()
+    {
+        return $this->to;
+    }
+
+    /**
+     * Allows for public read access to 'cc' property.
+     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
+     *
+     * @return array
+     */
+    public function getCcAddresses()
+    {
+        return $this->cc;
+    }
+
+    /**
+     * Allows for public read access to 'bcc' property.
+     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
+     *
+     * @return array
+     */
+    public function getBccAddresses()
+    {
+        return $this->bcc;
+    }
+
+    /**
+     * Allows for public read access to 'ReplyTo' property.
+     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
+     *
+     * @return array
+     */
+    public function getReplyToAddresses()
+    {
+        return $this->ReplyTo;
+    }
+
+    /**
+     * Allows for public read access to 'all_recipients' property.
+     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
+     *
+     * @return array
+     */
+    public function getAllRecipientAddresses()
+    {
+        return $this->all_recipients;
+    }
+
+    /**
+     * Perform a callback.
+     *
+     * @param bool   $isSent
+     * @param array  $to
+     * @param array  $cc
+     * @param array  $bcc
+     * @param string $subject
+     * @param string $body
+     * @param string $from
+     * @param array  $extra
+     */
+    protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from, $extra)
+    {
+        if (!empty($this->action_function) && is_callable($this->action_function)) {
+            call_user_func($this->action_function, $isSent, $to, $cc, $bcc, $subject, $body, $from, $extra);
+        }
+    }
+
+    /**
+     * Get the OAuth instance.
+     *
+     * @return OAuth
+     */
+    public function getOAuth()
+    {
+        return $this->oauth;
+    }
+
+    /**
+     * Set an OAuth instance.
+     */
+    public function setOAuth(OAuth $oauth)
+    {
+        $this->oauth = $oauth;
+    }
+}

+ 1456 - 0
public/install/phpmailer/SMTP.php

@@ -0,0 +1,1456 @@
+<?php
+
+/**
+ * PHPMailer RFC821 SMTP email transport class.
+ * PHP Version 5.5.
+ *
+ * @see       https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
+ *
+ * @author    Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
+ * @author    Jim Jagielski (jimjag) <jimjag@gmail.com>
+ * @author    Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
+ * @author    Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2020 Marcus Bointon
+ * @copyright 2010 - 2012 Jim Jagielski
+ * @copyright 2004 - 2009 Andy Prevost
+ * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
+ * @note      This program is distributed in the hope that it will be useful - WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+namespace PHPMailer\PHPMailer;
+
+/**
+ * PHPMailer RFC821 SMTP email transport class.
+ * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.
+ *
+ * @author Chris Ryan
+ * @author Marcus Bointon <phpmailer@synchromedia.co.uk>
+ */
+class SMTP
+{
+    /**
+     * The PHPMailer SMTP version number.
+     *
+     * @var string
+     */
+    const VERSION = '6.5.3';
+
+    /**
+     * SMTP line break constant.
+     *
+     * @var string
+     */
+    const LE = "\r\n";
+
+    /**
+     * The SMTP port to use if one is not specified.
+     *
+     * @var int
+     */
+    const DEFAULT_PORT = 25;
+
+    /**
+     * The maximum line length allowed by RFC 5321 section 4.5.3.1.6,
+     * *excluding* a trailing CRLF break.
+     *
+     * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.6
+     *
+     * @var int
+     */
+    const MAX_LINE_LENGTH = 998;
+
+    /**
+     * The maximum line length allowed for replies in RFC 5321 section 4.5.3.1.5,
+     * *including* a trailing CRLF line break.
+     *
+     * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.5
+     *
+     * @var int
+     */
+    const MAX_REPLY_LENGTH = 512;
+
+    /**
+     * Debug level for no output.
+     *
+     * @var int
+     */
+    const DEBUG_OFF = 0;
+
+    /**
+     * Debug level to show client -> server messages.
+     *
+     * @var int
+     */
+    const DEBUG_CLIENT = 1;
+
+    /**
+     * Debug level to show client -> server and server -> client messages.
+     *
+     * @var int
+     */
+    const DEBUG_SERVER = 2;
+
+    /**
+     * Debug level to show connection status, client -> server and server -> client messages.
+     *
+     * @var int
+     */
+    const DEBUG_CONNECTION = 3;
+
+    /**
+     * Debug level to show all messages.
+     *
+     * @var int
+     */
+    const DEBUG_LOWLEVEL = 4;
+
+    /**
+     * Debug output level.
+     * Options:
+     * * self::DEBUG_OFF (`0`) No debug output, default
+     * * self::DEBUG_CLIENT (`1`) Client commands
+     * * self::DEBUG_SERVER (`2`) Client commands and server responses
+     * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status
+     * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages.
+     *
+     * @var int
+     */
+    public $do_debug = self::DEBUG_OFF;
+
+    /**
+     * How to handle debug output.
+     * Options:
+     * * `echo` Output plain-text as-is, appropriate for CLI
+     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
+     * * `error_log` Output to error log as configured in php.ini
+     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
+     *
+     * ```php
+     * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
+     * ```
+     *
+     * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
+     * level output is used:
+     *
+     * ```php
+     * $mail->Debugoutput = new myPsr3Logger;
+     * ```
+     *
+     * @var string|callable|\Psr\Log\LoggerInterface
+     */
+    public $Debugoutput = 'echo';
+
+    /**
+     * Whether to use VERP.
+     *
+     * @see http://en.wikipedia.org/wiki/Variable_envelope_return_path
+     * @see http://www.postfix.org/VERP_README.html Info on VERP
+     *
+     * @var bool
+     */
+    public $do_verp = false;
+
+    /**
+     * The timeout value for connection, in seconds.
+     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
+     * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
+     *
+     * @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2
+     *
+     * @var int
+     */
+    public $Timeout = 300;
+
+    /**
+     * How long to wait for commands to complete, in seconds.
+     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
+     *
+     * @var int
+     */
+    public $Timelimit = 300;
+
+    /**
+     * Patterns to extract an SMTP transaction id from reply to a DATA command.
+     * The first capture group in each regex will be used as the ID.
+     * MS ESMTP returns the message ID, which may not be correct for internal tracking.
+     *
+     * @var string[]
+     */
+    protected $smtp_transaction_id_patterns = [
+        'exim' => '/[\d]{3} OK id=(.*)/',
+        'sendmail' => '/[\d]{3} 2.0.0 (.*) Message/',
+        'postfix' => '/[\d]{3} 2.0.0 Ok: queued as (.*)/',
+        'Microsoft_ESMTP' => '/[0-9]{3} 2.[\d].0 (.*)@(?:.*) Queued mail for delivery/',
+        'Amazon_SES' => '/[\d]{3} Ok (.*)/',
+        'SendGrid' => '/[\d]{3} Ok: queued as (.*)/',
+        'CampaignMonitor' => '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/',
+        'Haraka' => '/[\d]{3} Message Queued \((.*)\)/',
+        'Mailjet' => '/[\d]{3} OK queued as (.*)/',
+    ];
+
+    /**
+     * The last transaction ID issued in response to a DATA command,
+     * if one was detected.
+     *
+     * @var string|bool|null
+     */
+    protected $last_smtp_transaction_id;
+
+    /**
+     * The socket for the server connection.
+     *
+     * @var ?resource
+     */
+    protected $smtp_conn;
+
+    /**
+     * Error information, if any, for the last SMTP command.
+     *
+     * @var array
+     */
+    protected $error = [
+        'error' => '',
+        'detail' => '',
+        'smtp_code' => '',
+        'smtp_code_ex' => '',
+    ];
+
+    /**
+     * The reply the server sent to us for HELO.
+     * If null, no HELO string has yet been received.
+     *
+     * @var string|null
+     */
+    protected $helo_rply;
+
+    /**
+     * The set of SMTP extensions sent in reply to EHLO command.
+     * Indexes of the array are extension names.
+     * Value at index 'HELO' or 'EHLO' (according to command that was sent)
+     * represents the server name. In case of HELO it is the only element of the array.
+     * Other values can be boolean TRUE or an array containing extension options.
+     * If null, no HELO/EHLO string has yet been received.
+     *
+     * @var array|null
+     */
+    protected $server_caps;
+
+    /**
+     * The most recent reply received from the server.
+     *
+     * @var string
+     */
+    protected $last_reply = '';
+
+    /**
+     * Output debugging info via a user-selected method.
+     *
+     * @param string $str   Debug string to output
+     * @param int    $level The debug level of this message; see DEBUG_* constants
+     *
+     * @see SMTP::$Debugoutput
+     * @see SMTP::$do_debug
+     */
+    protected function edebug($str, $level = 0)
+    {
+        if ($level > $this->do_debug) {
+            return;
+        }
+        //Is this a PSR-3 logger?
+        if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
+            $this->Debugoutput->debug($str);
+
+            return;
+        }
+        //Avoid clash with built-in function names
+        if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
+            call_user_func($this->Debugoutput, $str, $level);
+
+            return;
+        }
+        switch ($this->Debugoutput) {
+            case 'error_log':
+                //Don't output, just log
+                error_log($str);
+                break;
+            case 'html':
+                //Cleans up output a bit for a better looking, HTML-safe output
+                echo gmdate('Y-m-d H:i:s'), ' ', htmlentities(
+                    preg_replace('/[\r\n]+/', '', $str),
+                    ENT_QUOTES,
+                    'UTF-8'
+                ), "<br>\n";
+                break;
+            case 'echo':
+            default:
+                //Normalize line breaks
+                $str = preg_replace('/\r\n|\r/m', "\n", $str);
+                echo gmdate('Y-m-d H:i:s'),
+                "\t",
+                    //Trim trailing space
+                trim(
+                    //Indent for readability, except for trailing break
+                    str_replace(
+                        "\n",
+                        "\n                   \t                  ",
+                        trim($str)
+                    )
+                ),
+                "\n";
+        }
+    }
+
+    /**
+     * Connect to an SMTP server.
+     *
+     * @param string $host    SMTP server IP or host name
+     * @param int    $port    The port number to connect to
+     * @param int    $timeout How long to wait for the connection to open
+     * @param array  $options An array of options for stream_context_create()
+     *
+     * @return bool
+     */
+    public function connect($host, $port = null, $timeout = 30, $options = [])
+    {
+        //Clear errors to avoid confusion
+        $this->setError('');
+        //Make sure we are __not__ connected
+        if ($this->connected()) {
+            //Already connected, generate error
+            $this->setError('Already connected to a server');
+
+            return false;
+        }
+        if (empty($port)) {
+            $port = self::DEFAULT_PORT;
+        }
+        //Connect to the SMTP server
+        $this->edebug(
+            "Connection: opening to $host:$port, timeout=$timeout, options=" .
+            (count($options) > 0 ? var_export($options, true) : 'array()'),
+            self::DEBUG_CONNECTION
+        );
+
+        $this->smtp_conn = $this->getSMTPConnection($host, $port, $timeout, $options);
+
+        if ($this->smtp_conn === false) {
+            //Error info already set inside `getSMTPConnection()`
+            return false;
+        }
+
+        $this->edebug('Connection: opened', self::DEBUG_CONNECTION);
+
+        //Get any announcement
+        $this->last_reply = $this->get_lines();
+        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
+        $responseCode = (int)substr($this->last_reply, 0, 3);
+        if ($responseCode === 220) {
+            return true;
+        }
+        //Anything other than a 220 response means something went wrong
+        //RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error
+        //https://tools.ietf.org/html/rfc5321#section-3.1
+        if ($responseCode === 554) {
+            $this->quit();
+        }
+        //This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down)
+        $this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION);
+        $this->close();
+        return false;
+    }
+
+    /**
+     * Create connection to the SMTP server.
+     *
+     * @param string $host    SMTP server IP or host name
+     * @param int    $port    The port number to connect to
+     * @param int    $timeout How long to wait for the connection to open
+     * @param array  $options An array of options for stream_context_create()
+     *
+     * @return false|resource
+     */
+    protected function getSMTPConnection($host, $port = null, $timeout = 30, $options = [])
+    {
+        static $streamok;
+        //This is enabled by default since 5.0.0 but some providers disable it
+        //Check this once and cache the result
+        if (null === $streamok) {
+            $streamok = function_exists('stream_socket_client');
+        }
+
+        $errno = 0;
+        $errstr = '';
+        if ($streamok) {
+            $socket_context = stream_context_create($options);
+            set_error_handler([$this, 'errorHandler']);
+            $connection = stream_socket_client(
+                $host . ':' . $port,
+                $errno,
+                $errstr,
+                $timeout,
+                STREAM_CLIENT_CONNECT,
+                $socket_context
+            );
+        } else {
+            //Fall back to fsockopen which should work in more places, but is missing some features
+            $this->edebug(
+                'Connection: stream_socket_client not available, falling back to fsockopen',
+                self::DEBUG_CONNECTION
+            );
+            set_error_handler([$this, 'errorHandler']);
+            $connection = fsockopen(
+                $host,
+                $port,
+                $errno,
+                $errstr,
+                $timeout
+            );
+        }
+        restore_error_handler();
+
+        //Verify we connected properly
+        if (!is_resource($connection)) {
+            $this->setError(
+                'Failed to connect to server',
+                '',
+                (string) $errno,
+                $errstr
+            );
+            $this->edebug(
+                'SMTP ERROR: ' . $this->error['error']
+                . ": $errstr ($errno)",
+                self::DEBUG_CLIENT
+            );
+
+            return false;
+        }
+
+        //SMTP server can take longer to respond, give longer timeout for first read
+        //Windows does not have support for this timeout function
+        if (strpos(PHP_OS, 'WIN') !== 0) {
+            $max = (int)ini_get('max_execution_time');
+            //Don't bother if unlimited, or if set_time_limit is disabled
+            if (0 !== $max && $timeout > $max && strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
+                @set_time_limit($timeout);
+            }
+            stream_set_timeout($connection, $timeout, 0);
+        }
+
+        return $connection;
+    }
+
+    /**
+     * Initiate a TLS (encrypted) session.
+     *
+     * @return bool
+     */
+    public function startTLS()
+    {
+        if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
+            return false;
+        }
+
+        //Allow the best TLS version(s) we can
+        $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
+
+        //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
+        //so add them back in manually if we can
+        if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
+            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
+            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
+        }
+
+        //Begin encrypted connection
+        set_error_handler([$this, 'errorHandler']);
+        $crypto_ok = stream_socket_enable_crypto(
+            $this->smtp_conn,
+            true,
+            $crypto_method
+        );
+        restore_error_handler();
+
+        return (bool) $crypto_ok;
+    }
+
+    /**
+     * Perform SMTP authentication.
+     * Must be run after hello().
+     *
+     * @see    hello()
+     *
+     * @param string $username The user name
+     * @param string $password The password
+     * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2)
+     * @param OAuth  $OAuth    An optional OAuth instance for XOAUTH2 authentication
+     *
+     * @return bool True if successfully authenticated
+     */
+    public function authenticate(
+        $username,
+        $password,
+        $authtype = null,
+        $OAuth = null
+    ) {
+        if (!$this->server_caps) {
+            $this->setError('Authentication is not allowed before HELO/EHLO');
+
+            return false;
+        }
+
+        if (array_key_exists('EHLO', $this->server_caps)) {
+            //SMTP extensions are available; try to find a proper authentication method
+            if (!array_key_exists('AUTH', $this->server_caps)) {
+                $this->setError('Authentication is not allowed at this stage');
+                //'at this stage' means that auth may be allowed after the stage changes
+                //e.g. after STARTTLS
+
+                return false;
+            }
+
+            $this->edebug('Auth method requested: ' . ($authtype ?: 'UNSPECIFIED'), self::DEBUG_LOWLEVEL);
+            $this->edebug(
+                'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),
+                self::DEBUG_LOWLEVEL
+            );
+
+            //If we have requested a specific auth type, check the server supports it before trying others
+            if (null !== $authtype && !in_array($authtype, $this->server_caps['AUTH'], true)) {
+                $this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL);
+                $authtype = null;
+            }
+
+            if (empty($authtype)) {
+                //If no auth mechanism is specified, attempt to use these, in this order
+                //Try CRAM-MD5 first as it's more secure than the others
+                foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) {
+                    if (in_array($method, $this->server_caps['AUTH'], true)) {
+                        $authtype = $method;
+                        break;
+                    }
+                }
+                if (empty($authtype)) {
+                    $this->setError('No supported authentication methods found');
+
+                    return false;
+                }
+                $this->edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
+            }
+
+            if (!in_array($authtype, $this->server_caps['AUTH'], true)) {
+                $this->setError("The requested authentication method \"$authtype\" is not supported by the server");
+
+                return false;
+            }
+        } elseif (empty($authtype)) {
+            $authtype = 'LOGIN';
+        }
+        switch ($authtype) {
+            case 'PLAIN':
+                //Start authentication
+                if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
+                    return false;
+                }
+                //Send encoded username and password
+                if (
+                    //Format from https://tools.ietf.org/html/rfc4616#section-2
+                    //We skip the first field (it's forgery), so the string starts with a null byte
+                    !$this->sendCommand(
+                        'User & Password',
+                        base64_encode("\0" . $username . "\0" . $password),
+                        235
+                    )
+                ) {
+                    return false;
+                }
+                break;
+            case 'LOGIN':
+                //Start authentication
+                if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
+                    return false;
+                }
+                if (!$this->sendCommand('Username', base64_encode($username), 334)) {
+                    return false;
+                }
+                if (!$this->sendCommand('Password', base64_encode($password), 235)) {
+                    return false;
+                }
+                break;
+            case 'CRAM-MD5':
+                //Start authentication
+                if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
+                    return false;
+                }
+                //Get the challenge
+                $challenge = base64_decode(substr($this->last_reply, 4));
+
+                //Build the response
+                $response = $username . ' ' . $this->hmac($challenge, $password);
+
+                //send encoded credentials
+                return $this->sendCommand('Username', base64_encode($response), 235);
+            case 'XOAUTH2':
+                //The OAuth instance must be set up prior to requesting auth.
+                if (null === $OAuth) {
+                    return false;
+                }
+                $oauth = $OAuth->getOauth64();
+
+                //Start authentication
+                if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
+                    return false;
+                }
+                break;
+            default:
+                $this->setError("Authentication method \"$authtype\" is not supported");
+
+                return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Calculate an MD5 HMAC hash.
+     * Works like hash_hmac('md5', $data, $key)
+     * in case that function is not available.
+     *
+     * @param string $data The data to hash
+     * @param string $key  The key to hash with
+     *
+     * @return string
+     */
+    protected function hmac($data, $key)
+    {
+        if (function_exists('hash_hmac')) {
+            return hash_hmac('md5', $data, $key);
+        }
+
+        //The following borrowed from
+        //http://php.net/manual/en/function.mhash.php#27225
+
+        //RFC 2104 HMAC implementation for php.
+        //Creates an md5 HMAC.
+        //Eliminates the need to install mhash to compute a HMAC
+        //by Lance Rushing
+
+        $bytelen = 64; //byte length for md5
+        if (strlen($key) > $bytelen) {
+            $key = pack('H*', md5($key));
+        }
+        $key = str_pad($key, $bytelen, chr(0x00));
+        $ipad = str_pad('', $bytelen, chr(0x36));
+        $opad = str_pad('', $bytelen, chr(0x5c));
+        $k_ipad = $key ^ $ipad;
+        $k_opad = $key ^ $opad;
+
+        return md5($k_opad . pack('H*', md5($k_ipad . $data)));
+    }
+
+    /**
+     * Check connection state.
+     *
+     * @return bool True if connected
+     */
+    public function connected()
+    {
+        if (is_resource($this->smtp_conn)) {
+            $sock_status = stream_get_meta_data($this->smtp_conn);
+            if ($sock_status['eof']) {
+                //The socket is valid but we are not connected
+                $this->edebug(
+                    'SMTP NOTICE: EOF caught while checking if connected',
+                    self::DEBUG_CLIENT
+                );
+                $this->close();
+
+                return false;
+            }
+
+            return true; //everything looks good
+        }
+
+        return false;
+    }
+
+    /**
+     * Close the socket and clean up the state of the class.
+     * Don't use this function without first trying to use QUIT.
+     *
+     * @see quit()
+     */
+    public function close()
+    {
+        $this->setError('');
+        $this->server_caps = null;
+        $this->helo_rply = null;
+        if (is_resource($this->smtp_conn)) {
+            //Close the connection and cleanup
+            fclose($this->smtp_conn);
+            $this->smtp_conn = null; //Makes for cleaner serialization
+            $this->edebug('Connection: closed', self::DEBUG_CONNECTION);
+        }
+    }
+
+    /**
+     * Send an SMTP DATA command.
+     * Issues a data command and sends the msg_data to the server,
+     * finalizing the mail transaction. $msg_data is the message
+     * that is to be send with the headers. Each header needs to be
+     * on a single line followed by a <CRLF> with the message headers
+     * and the message body being separated by an additional <CRLF>.
+     * Implements RFC 821: DATA <CRLF>.
+     *
+     * @param string $msg_data Message data to send
+     *
+     * @return bool
+     */
+    public function data($msg_data)
+    {
+        //This will use the standard timelimit
+        if (!$this->sendCommand('DATA', 'DATA', 354)) {
+            return false;
+        }
+
+        /* The server is ready to accept data!
+         * According to rfc821 we should not send more than 1000 characters on a single line (including the LE)
+         * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
+         * smaller lines to fit within the limit.
+         * We will also look for lines that start with a '.' and prepend an additional '.'.
+         * NOTE: this does not count towards line-length limit.
+         */
+
+        //Normalize line breaks before exploding
+        $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data));
+
+        /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
+         * of the first line (':' separated) does not contain a space then it _should_ be a header and we will
+         * process all lines before a blank line as headers.
+         */
+
+        $field = substr($lines[0], 0, strpos($lines[0], ':'));
+        $in_headers = false;
+        if (!empty($field) && strpos($field, ' ') === false) {
+            $in_headers = true;
+        }
+
+        foreach ($lines as $line) {
+            $lines_out = [];
+            if ($in_headers && $line === '') {
+                $in_headers = false;
+            }
+            //Break this line up into several smaller lines if it's too long
+            //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),
+            while (isset($line[self::MAX_LINE_LENGTH])) {
+                //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
+                //so as to avoid breaking in the middle of a word
+                $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');
+                //Deliberately matches both false and 0
+                if (!$pos) {
+                    //No nice break found, add a hard break
+                    $pos = self::MAX_LINE_LENGTH - 1;
+                    $lines_out[] = substr($line, 0, $pos);
+                    $line = substr($line, $pos);
+                } else {
+                    //Break at the found point
+                    $lines_out[] = substr($line, 0, $pos);
+                    //Move along by the amount we dealt with
+                    $line = substr($line, $pos + 1);
+                }
+                //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1
+                if ($in_headers) {
+                    $line = "\t" . $line;
+                }
+            }
+            $lines_out[] = $line;
+
+            //Send the lines to the server
+            foreach ($lines_out as $line_out) {
+                //Dot-stuffing as per RFC5321 section 4.5.2
+                //https://tools.ietf.org/html/rfc5321#section-4.5.2
+                if (!empty($line_out) && $line_out[0] === '.') {
+                    $line_out = '.' . $line_out;
+                }
+                $this->client_send($line_out . static::LE, 'DATA');
+            }
+        }
+
+        //Message data has been sent, complete the command
+        //Increase timelimit for end of DATA command
+        $savetimelimit = $this->Timelimit;
+        $this->Timelimit *= 2;
+        $result = $this->sendCommand('DATA END', '.', 250);
+        $this->recordLastTransactionID();
+        //Restore timelimit
+        $this->Timelimit = $savetimelimit;
+
+        return $result;
+    }
+
+    /**
+     * Send an SMTP HELO or EHLO command.
+     * Used to identify the sending server to the receiving server.
+     * This makes sure that client and server are in a known state.
+     * Implements RFC 821: HELO <SP> <domain> <CRLF>
+     * and RFC 2821 EHLO.
+     *
+     * @param string $host The host name or IP to connect to
+     *
+     * @return bool
+     */
+    public function hello($host = '')
+    {
+        //Try extended hello first (RFC 2821)
+        if ($this->sendHello('EHLO', $host)) {
+            return true;
+        }
+
+        //Some servers shut down the SMTP service here (RFC 5321)
+        if (substr($this->helo_rply, 0, 3) == '421') {
+            return false;
+        }
+
+        return $this->sendHello('HELO', $host);
+    }
+
+    /**
+     * Send an SMTP HELO or EHLO command.
+     * Low-level implementation used by hello().
+     *
+     * @param string $hello The HELO string
+     * @param string $host  The hostname to say we are
+     *
+     * @return bool
+     *
+     * @see hello()
+     */
+    protected function sendHello($hello, $host)
+    {
+        $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
+        $this->helo_rply = $this->last_reply;
+        if ($noerror) {
+            $this->parseHelloFields($hello);
+        } else {
+            $this->server_caps = null;
+        }
+
+        return $noerror;
+    }
+
+    /**
+     * Parse a reply to HELO/EHLO command to discover server extensions.
+     * In case of HELO, the only parameter that can be discovered is a server name.
+     *
+     * @param string $type `HELO` or `EHLO`
+     */
+    protected function parseHelloFields($type)
+    {
+        $this->server_caps = [];
+        $lines = explode("\n", $this->helo_rply);
+
+        foreach ($lines as $n => $s) {
+            //First 4 chars contain response code followed by - or space
+            $s = trim(substr($s, 4));
+            if (empty($s)) {
+                continue;
+            }
+            $fields = explode(' ', $s);
+            if (!empty($fields)) {
+                if (!$n) {
+                    $name = $type;
+                    $fields = $fields[0];
+                } else {
+                    $name = array_shift($fields);
+                    switch ($name) {
+                        case 'SIZE':
+                            $fields = ($fields ? $fields[0] : 0);
+                            break;
+                        case 'AUTH':
+                            if (!is_array($fields)) {
+                                $fields = [];
+                            }
+                            break;
+                        default:
+                            $fields = true;
+                    }
+                }
+                $this->server_caps[$name] = $fields;
+            }
+        }
+    }
+
+    /**
+     * Send an SMTP MAIL command.
+     * Starts a mail transaction from the email address specified in
+     * $from. Returns true if successful or false otherwise. If True
+     * the mail transaction is started and then one or more recipient
+     * commands may be called followed by a data command.
+     * Implements RFC 821: MAIL <SP> FROM:<reverse-path> <CRLF>.
+     *
+     * @param string $from Source address of this message
+     *
+     * @return bool
+     */
+    public function mail($from)
+    {
+        $useVerp = ($this->do_verp ? ' XVERP' : '');
+
+        return $this->sendCommand(
+            'MAIL FROM',
+            'MAIL FROM:<' . $from . '>' . $useVerp,
+            250
+        );
+    }
+
+    /**
+     * Send an SMTP QUIT command.
+     * Closes the socket if there is no error or the $close_on_error argument is true.
+     * Implements from RFC 821: QUIT <CRLF>.
+     *
+     * @param bool $close_on_error Should the connection close if an error occurs?
+     *
+     * @return bool
+     */
+    public function quit($close_on_error = true)
+    {
+        $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
+        $err = $this->error; //Save any error
+        if ($noerror || $close_on_error) {
+            $this->close();
+            $this->error = $err; //Restore any error from the quit command
+        }
+
+        return $noerror;
+    }
+
+    /**
+     * Send an SMTP RCPT command.
+     * Sets the TO argument to $toaddr.
+     * Returns true if the recipient was accepted false if it was rejected.
+     * Implements from RFC 821: RCPT <SP> TO:<forward-path> <CRLF>.
+     *
+     * @param string $address The address the message is being sent to
+     * @param string $dsn     Comma separated list of DSN notifications. NEVER, SUCCESS, FAILURE
+     *                        or DELAY. If you specify NEVER all other notifications are ignored.
+     *
+     * @return bool
+     */
+    public function recipient($address, $dsn = '')
+    {
+        if (empty($dsn)) {
+            $rcpt = 'RCPT TO:<' . $address . '>';
+        } else {
+            $dsn = strtoupper($dsn);
+            $notify = [];
+
+            if (strpos($dsn, 'NEVER') !== false) {
+                $notify[] = 'NEVER';
+            } else {
+                foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) {
+                    if (strpos($dsn, $value) !== false) {
+                        $notify[] = $value;
+                    }
+                }
+            }
+
+            $rcpt = 'RCPT TO:<' . $address . '> NOTIFY=' . implode(',', $notify);
+        }
+
+        return $this->sendCommand(
+            'RCPT TO',
+            $rcpt,
+            [250, 251]
+        );
+    }
+
+    /**
+     * Send an SMTP RSET command.
+     * Abort any transaction that is currently in progress.
+     * Implements RFC 821: RSET <CRLF>.
+     *
+     * @return bool True on success
+     */
+    public function reset()
+    {
+        return $this->sendCommand('RSET', 'RSET', 250);
+    }
+
+    /**
+     * Send a command to an SMTP server and check its return code.
+     *
+     * @param string    $command       The command name - not sent to the server
+     * @param string    $commandstring The actual command to send
+     * @param int|array $expect        One or more expected integer success codes
+     *
+     * @return bool True on success
+     */
+    protected function sendCommand($command, $commandstring, $expect)
+    {
+        if (!$this->connected()) {
+            $this->setError("Called $command without being connected");
+
+            return false;
+        }
+        //Reject line breaks in all commands
+        if ((strpos($commandstring, "\n") !== false) || (strpos($commandstring, "\r") !== false)) {
+            $this->setError("Command '$command' contained line breaks");
+
+            return false;
+        }
+        $this->client_send($commandstring . static::LE, $command);
+
+        $this->last_reply = $this->get_lines();
+        //Fetch SMTP code and possible error code explanation
+        $matches = [];
+        if (preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->last_reply, $matches)) {
+            $code = (int) $matches[1];
+            $code_ex = (count($matches) > 2 ? $matches[2] : null);
+            //Cut off error code from each response line
+            $detail = preg_replace(
+                "/{$code}[ -]" .
+                ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m',
+                '',
+                $this->last_reply
+            );
+        } else {
+            //Fall back to simple parsing if regex fails
+            $code = (int) substr($this->last_reply, 0, 3);
+            $code_ex = null;
+            $detail = substr($this->last_reply, 4);
+        }
+
+        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
+
+        if (!in_array($code, (array) $expect, true)) {
+            $this->setError(
+                "$command command failed",
+                $detail,
+                $code,
+                $code_ex
+            );
+            $this->edebug(
+                'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,
+                self::DEBUG_CLIENT
+            );
+
+            return false;
+        }
+
+        $this->setError('');
+
+        return true;
+    }
+
+    /**
+     * Send an SMTP SAML command.
+     * Starts a mail transaction from the email address specified in $from.
+     * Returns true if successful or false otherwise. If True
+     * the mail transaction is started and then one or more recipient
+     * commands may be called followed by a data command. This command
+     * will send the message to the users terminal if they are logged
+     * in and send them an email.
+     * Implements RFC 821: SAML <SP> FROM:<reverse-path> <CRLF>.
+     *
+     * @param string $from The address the message is from
+     *
+     * @return bool
+     */
+    public function sendAndMail($from)
+    {
+        return $this->sendCommand('SAML', "SAML FROM:$from", 250);
+    }
+
+    /**
+     * Send an SMTP VRFY command.
+     *
+     * @param string $name The name to verify
+     *
+     * @return bool
+     */
+    public function verify($name)
+    {
+        return $this->sendCommand('VRFY', "VRFY $name", [250, 251]);
+    }
+
+    /**
+     * Send an SMTP NOOP command.
+     * Used to keep keep-alives alive, doesn't actually do anything.
+     *
+     * @return bool
+     */
+    public function noop()
+    {
+        return $this->sendCommand('NOOP', 'NOOP', 250);
+    }
+
+    /**
+     * Send an SMTP TURN command.
+     * This is an optional command for SMTP that this class does not support.
+     * This method is here to make the RFC821 Definition complete for this class
+     * and _may_ be implemented in future.
+     * Implements from RFC 821: TURN <CRLF>.
+     *
+     * @return bool
+     */
+    public function turn()
+    {
+        $this->setError('The SMTP TURN command is not implemented');
+        $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
+
+        return false;
+    }
+
+    /**
+     * Send raw data to the server.
+     *
+     * @param string $data    The data to send
+     * @param string $command Optionally, the command this is part of, used only for controlling debug output
+     *
+     * @return int|bool The number of bytes sent to the server or false on error
+     */
+    public function client_send($data, $command = '')
+    {
+        //If SMTP transcripts are left enabled, or debug output is posted online
+        //it can leak credentials, so hide credentials in all but lowest level
+        if (
+            self::DEBUG_LOWLEVEL > $this->do_debug &&
+            in_array($command, ['User & Password', 'Username', 'Password'], true)
+        ) {
+            $this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT);
+        } else {
+            $this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT);
+        }
+        set_error_handler([$this, 'errorHandler']);
+        $result = fwrite($this->smtp_conn, $data);
+        restore_error_handler();
+
+        return $result;
+    }
+
+    /**
+     * Get the latest error.
+     *
+     * @return array
+     */
+    public function getError()
+    {
+        return $this->error;
+    }
+
+    /**
+     * Get SMTP extensions available on the server.
+     *
+     * @return array|null
+     */
+    public function getServerExtList()
+    {
+        return $this->server_caps;
+    }
+
+    /**
+     * Get metadata about the SMTP server from its HELO/EHLO response.
+     * The method works in three ways, dependent on argument value and current state:
+     *   1. HELO/EHLO has not been sent - returns null and populates $this->error.
+     *   2. HELO has been sent -
+     *     $name == 'HELO': returns server name
+     *     $name == 'EHLO': returns boolean false
+     *     $name == any other string: returns null and populates $this->error
+     *   3. EHLO has been sent -
+     *     $name == 'HELO'|'EHLO': returns the server name
+     *     $name == any other string: if extension $name exists, returns True
+     *       or its options (e.g. AUTH mechanisms supported). Otherwise returns False.
+     *
+     * @param string $name Name of SMTP extension or 'HELO'|'EHLO'
+     *
+     * @return string|bool|null
+     */
+    public function getServerExt($name)
+    {
+        if (!$this->server_caps) {
+            $this->setError('No HELO/EHLO was sent');
+
+            return null;
+        }
+
+        if (!array_key_exists($name, $this->server_caps)) {
+            if ('HELO' === $name) {
+                return $this->server_caps['EHLO'];
+            }
+            if ('EHLO' === $name || array_key_exists('EHLO', $this->server_caps)) {
+                return false;
+            }
+            $this->setError('HELO handshake was used; No information about server extensions available');
+
+            return null;
+        }
+
+        return $this->server_caps[$name];
+    }
+
+    /**
+     * Get the last reply from the server.
+     *
+     * @return string
+     */
+    public function getLastReply()
+    {
+        return $this->last_reply;
+    }
+
+    /**
+     * Read the SMTP server's response.
+     * Either before eof or socket timeout occurs on the operation.
+     * With SMTP we can tell if we have more lines to read if the
+     * 4th character is '-' symbol. If it is a space then we don't
+     * need to read anything else.
+     *
+     * @return string
+     */
+    protected function get_lines()
+    {
+        //If the connection is bad, give up straight away
+        if (!is_resource($this->smtp_conn)) {
+            return '';
+        }
+        $data = '';
+        $endtime = 0;
+        stream_set_timeout($this->smtp_conn, $this->Timeout);
+        if ($this->Timelimit > 0) {
+            $endtime = time() + $this->Timelimit;
+        }
+        $selR = [$this->smtp_conn];
+        $selW = null;
+        while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
+            //Must pass vars in here as params are by reference
+            //solution for signals inspired by https://github.com/symfony/symfony/pull/6540
+            set_error_handler([$this, 'errorHandler']);
+            $n = stream_select($selR, $selW, $selW, $this->Timelimit);
+            restore_error_handler();
+
+            if ($n === false) {
+                $message = $this->getError()['detail'];
+
+                $this->edebug(
+                    'SMTP -> get_lines(): select failed (' . $message . ')',
+                    self::DEBUG_LOWLEVEL
+                );
+
+                //stream_select returns false when the `select` system call is interrupted
+                //by an incoming signal, try the select again
+                if (stripos($message, 'interrupted system call') !== false) {
+                    $this->edebug(
+                        'SMTP -> get_lines(): retrying stream_select',
+                        self::DEBUG_LOWLEVEL
+                    );
+                    $this->setError('');
+                    continue;
+                }
+
+                break;
+            }
+
+            if (!$n) {
+                $this->edebug(
+                    'SMTP -> get_lines(): select timed-out in (' . $this->Timelimit . ' sec)',
+                    self::DEBUG_LOWLEVEL
+                );
+                break;
+            }
+
+            //Deliberate noise suppression - errors are handled afterwards
+            $str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH);
+            $this->edebug('SMTP INBOUND: "' . trim($str) . '"', self::DEBUG_LOWLEVEL);
+            $data .= $str;
+            //If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
+            //or 4th character is a space or a line break char, we are done reading, break the loop.
+            //String array access is a significant micro-optimisation over strlen
+            if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") {
+                break;
+            }
+            //Timed-out? Log and break
+            $info = stream_get_meta_data($this->smtp_conn);
+            if ($info['timed_out']) {
+                $this->edebug(
+                    'SMTP -> get_lines(): stream timed-out (' . $this->Timeout . ' sec)',
+                    self::DEBUG_LOWLEVEL
+                );
+                break;
+            }
+            //Now check if reads took too long
+            if ($endtime && time() > $endtime) {
+                $this->edebug(
+                    'SMTP -> get_lines(): timelimit reached (' .
+                    $this->Timelimit . ' sec)',
+                    self::DEBUG_LOWLEVEL
+                );
+                break;
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * Enable or disable VERP address generation.
+     *
+     * @param bool $enabled
+     */
+    public function setVerp($enabled = false)
+    {
+        $this->do_verp = $enabled;
+    }
+
+    /**
+     * Get VERP address generation mode.
+     *
+     * @return bool
+     */
+    public function getVerp()
+    {
+        return $this->do_verp;
+    }
+
+    /**
+     * Set error messages and codes.
+     *
+     * @param string $message      The error message
+     * @param string $detail       Further detail on the error
+     * @param string $smtp_code    An associated SMTP error code
+     * @param string $smtp_code_ex Extended SMTP code
+     */
+    protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
+    {
+        $this->error = [
+            'error' => $message,
+            'detail' => $detail,
+            'smtp_code' => $smtp_code,
+            'smtp_code_ex' => $smtp_code_ex,
+        ];
+    }
+
+    /**
+     * Set debug output method.
+     *
+     * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it
+     */
+    public function setDebugOutput($method = 'echo')
+    {
+        $this->Debugoutput = $method;
+    }
+
+    /**
+     * Get debug output method.
+     *
+     * @return string
+     */
+    public function getDebugOutput()
+    {
+        return $this->Debugoutput;
+    }
+
+    /**
+     * Set debug output level.
+     *
+     * @param int $level
+     */
+    public function setDebugLevel($level = 0)
+    {
+        $this->do_debug = $level;
+    }
+
+    /**
+     * Get debug output level.
+     *
+     * @return int
+     */
+    public function getDebugLevel()
+    {
+        return $this->do_debug;
+    }
+
+    /**
+     * Set SMTP timeout.
+     *
+     * @param int $timeout The timeout duration in seconds
+     */
+    public function setTimeout($timeout = 0)
+    {
+        $this->Timeout = $timeout;
+    }
+
+    /**
+     * Get SMTP timeout.
+     *
+     * @return int
+     */
+    public function getTimeout()
+    {
+        return $this->Timeout;
+    }
+
+    /**
+     * Reports an error number and string.
+     *
+     * @param int    $errno   The error number returned by PHP
+     * @param string $errmsg  The error message returned by PHP
+     * @param string $errfile The file the error occurred in
+     * @param int    $errline The line number the error occurred on
+     */
+    protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
+    {
+        $notice = 'Connection failed.';
+        $this->setError(
+            $notice,
+            $errmsg,
+            (string) $errno
+        );
+        $this->edebug(
+            "$notice Error #$errno: $errmsg [$errfile line $errline]",
+            self::DEBUG_CONNECTION
+        );
+    }
+
+    /**
+     * Extract and return the ID of the last SMTP transaction based on
+     * a list of patterns provided in SMTP::$smtp_transaction_id_patterns.
+     * Relies on the host providing the ID in response to a DATA command.
+     * If no reply has been received yet, it will return null.
+     * If no pattern was matched, it will return false.
+     *
+     * @return bool|string|null
+     */
+    protected function recordLastTransactionID()
+    {
+        $reply = $this->getLastReply();
+
+        if (empty($reply)) {
+            $this->last_smtp_transaction_id = null;
+        } else {
+            $this->last_smtp_transaction_id = false;
+            foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
+                $matches = [];
+                if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
+                    $this->last_smtp_transaction_id = trim($matches[1]);
+                    break;
+                }
+            }
+        }
+
+        return $this->last_smtp_transaction_id;
+    }
+
+    /**
+     * Get the queue/transaction ID of the last SMTP transaction
+     * If no reply has been received yet, it will return null.
+     * If no pattern was matched, it will return false.
+     *
+     * @return bool|string|null
+     *
+     * @see recordLastTransactionID()
+     */
+    public function getLastTransactionID()
+    {
+        return $this->last_smtp_transaction_id;
+    }
+}

File diff suppressed because it is too large
+ 0 - 1
public/js/app.js


+ 354 - 0
resources/lang/cs.json

@@ -0,0 +1,354 @@
+{
+    "api key created!": "api klíč vygenerován!",
+    "api key updated!": "api klíč aktualizován!",
+    "api key has been removed!": "api klíč byl odstraněn!",
+    "Edit": "Upravit",
+    "Delete": "Smazat",
+    "configuration has been updated!": "konfigurace byla aktualizována!",
+    "Store item has been created!": "Balíček dokoupení kreditů byl vytvořen!",
+    "Store item has been updated!": "Balíček dokoupení kreditů byl aktualizován!",
+    "Product has been updated!": "Balíček byl aktualizován!",
+    "Store item has been removed!": "Balíček dokoupení kreditů byl odstraněn!",
+    "unknown": "neznámý",
+    "Pterodactyl synced": "Pterodactyl synchronizován",
+    "Your credit balance has been increased!": "Kredity byly připsány!",
+    "Your payment is being processed!": "Vaše platba se nyní zpracovává!",
+    "Your payment has been canceled!": "Vaše platba byla zrušena!",
+    "Payment method": "Platební metoda",
+    "Invoice": "Faktura",
+    "Product has been created!": "Balíček byl vytvořen!",
+    "Product has been removed!": "Balíček byl odstraněn!",
+    "Show": "Zobrazit",
+    "Clone": "Duplikovat",
+    "Server removed": "Server byl odstraněn",
+    "An exception has occurred while trying to remove a resource \"": "Nastala chyba při odstraňování balíčku",
+    "Server has been updated!": "Server byl aktualizován!",
+    "Unsuspend": "Zrušit pozastavení",
+    "Suspend": "Pozastavit",
+    "Icons updated!": "Ikony byly aktualizovány!",
+    "link has been created!": "užitečný odkaz byl úspěšně vytvořen!",
+    "link has been updated!": "užitečný odkaz byl aktualizován!",
+    "product has been removed!": "balíček byl odstraněn!",
+    "User does not exists on pterodactyl's panel": "Uživatel neexistuje na pterodactyl panelu",
+    "user has been removed!": "uživatel byl odstraněn!",
+    "Notification sent!": "Oznámení odesláno!",
+    "User has been updated!": "Uživatel byl aktualizován!",
+    "Login as User": "Přihlásit se jako uživatel",
+    "voucher has been created!": "poukaz byl vytvořen!",
+    "voucher has been updated!": "poukaz byl aktualizován!",
+    "voucher has been removed!": "poukaz byl odstraněn!",
+    "This voucher has reached the maximum amount of uses": "Tento poukaz dosáhl maximálního počtu použití",
+    "This voucher has expired": "Tento poukaz už není platný",
+    "You already redeemed this voucher code": "Tento poukaz už jste využil",
+    "have been added to your balance!": "bylo připsáno na váš účet!",
+    "Users": "Uživatelé",
+    "VALID": "platný",
+    "days": "dní",
+    "hours": "hodin",
+    "You ran out of Credits": "Došly Vám kredity",
+    "Profile updated": "Profil byl aktualizován",
+    "Server limit reached!": "Dosažen limit počtu serverů!",
+    "You are required to verify your email address before you can create a server.": "Pro vytvoření serveru je třeba si ověřit emailovou adresu.",
+    "You are required to link your discord account before you can create a server.": "Pro vytvoření serveru je třeba ověřit si discord účet.",
+    "Server created": "Server vytvořen",
+    "No allocations satisfying the requirements for automatic deployment on this node were found.": "Na vámi vybraném uzlu už nejsou žádné volné porty.",
+    "You are required to verify your email address before you can purchase credits.": "Pro dobití kreditu je třeba ověřit si emailovou adresu.",
+    "You are required to link your discord account before you can purchase Credits": "Pro dobití kreditu je třeba ověřit si discord účet",
+    "EXPIRED": "Po expiraci",
+    "Payment Confirmation": "Potvrzení Platby",
+    "Payment Confirmed!": "Platba proběhla v pořádku!",
+    "Your Payment was successful!": "Vaše platba proběhla úspěšně!",
+    "Hello": "Dobrý den",
+    "Your payment was processed successfully!": "Platba byla úspěšně zpracována!",
+    "Status": "Stav",
+    "Price": "Cena",
+    "Type": "Typ",
+    "Amount": "Množství",
+    "Balance": "Zůstatek",
+    "User ID": "ID uživatele",
+    "Server Creation Error": "Chyba při vytváření serveru",
+    "Your servers have been suspended!": "Vaše servery byly pozastaveny!",
+    "To automatically re-enable your server\/s, you need to purchase more credits.": "Pro opětovné spuštění vašich serverů dobijte prosím kredity.",
+    "Purchase credits": "Zakoupit kredity",
+    "If you have any questions please let us know.": "Máte-li jakékoli dotazy, dejte nám vědět.",
+    "Regards": "S pozdravem",
+    "Getting started!": "Začínáme!",
+    "Activity Logs": "Historie akcí",
+    "Dashboard": "Přehled",
+    "No recent activity from cronjobs": "Žádná nedávná aktivita cronjobů",
+    "Are cronjobs running?": "Funguje crontab?",
+    "Check the docs for it here": "Nápovědu naleznete zde",
+    "Causer": "Průvodce",
+    "Description": "Popis",
+    "Created at": "Vytvořeno",
+    "Application API": "API",
+    "Create": "Vytvořit",
+    "Memo": "Název",
+    "Submit": "Potvrdit",
+    "Create new": "Vytvořit nový",
+    "Token": "Token",
+    "Last used": "Naposledy použito",
+    "Are you sure you wish to delete?": "Opravdu si přejete odstranit?",
+    "Edit Configuration": "Upravit konfiguraci",
+    "Text Field": "Textové pole",
+    "Cancel": "Zrušit",
+    "Save": "Uložit",
+    "Configurations": "Konfigurace",
+    "Key": "Klíč",
+    "Value": "Hodnota",
+    "Nests": "Software\/hra",
+    "Sync": "Synchronizovat",
+    "Active": "Aktivní",
+    "ID": "ID",
+    "eggs": "distribuce",
+    "Name": "Název",
+    "Nodes": "Uzly",
+    "Location": "Umístění",
+    "Admin Overview": "Přehled pro správce",
+    "Support server": "Server podpory",
+    "Documentation": "Dokumentace",
+    "Github": "GitHub",
+    "Support ControlPanel": "Podpořit ControlPanel",
+    "Servers": "Servery",
+    "Total": "Celkem",
+    "Payments": "Platby",
+    "Pterodactyl": "Pterodactyl",
+    "Resources": "Prostředky",
+    "Count": "Počet",
+    "Locations": "Umístění",
+    "Eggs": "Distribuce",
+    "Last updated :date": "Naposledy aktualizováno :datum",
+    "Product Price": "Cena balíčku",
+    "Tax Value": "Hodnota daně",
+    "Tax Percentage": "Procento daně",
+    "Total Price": "Celková cena",
+    "Payment ID": "ID Platby",
+    "Payment Method": "Platební metoda",
+    "Products": "Balíčky",
+    "Product Details": "Detaily balíčku",
+    "Disabled": "Zakázáno",
+    "Will hide this option from being selected": "Zabrání této možnosti aby nemohla být vybrána",
+    "Price in": "Cena v",
+    "Memory": "Paměť RAM",
+    "Cpu": "CPU",
+    "Swap": "Swap",
+    "This is what the users sees": "Toto je náhled, co uvidí zákazník",
+    "Disk": "Úložiště",
+    "Minimum": "Minimum",
+    "Setting to -1 will use the value from configuration.": "Nastavením na -1 použijete hodnotu z konfigurace.",
+    "IO": "IO",
+    "Databases": "Databáze",
+    "Backups": "Zálohy",
+    "Allocations": "Porty",
+    "Product Linking": "Propojení balíčku",
+    "Link your products to nodes and eggs to create dynamic pricing for each option": "Propojte své balíčky s uzly a distribucemi pro vytvoření cen jednotlivých kombinací",
+    "This product will only be available for these nodes": "Tento balíček bude dostupný pouze pro tyto uzly",
+    "This product will only be available for these eggs": "Tento balíček bude dostupný pouze pro tyto distribuce",
+    "Product": "Balíček",
+    "CPU": "CPU",
+    "Updated at": "Aktualizováno",
+    "User": "Uživatel",
+    "Config": "Konfigurace",
+    "Suspended at": "Pozastaveno",
+    "Settings": "Nastavení",
+    "Dashboard icons": "Ikony hlavního panelu",
+    "Invoice Settings": "Nastavení fakturace",
+    "Select panel icon": "Vybrat ikonu panelu",
+    "Select panel favicon": "Vybrat favikonu panelu",
+    "Download all Invoices": "Stáhnout všechny faktury",
+    "Enter your companys name": "Zadejte název vaší společnosti",
+    "Enter your companys address": "Zadejte adresu vaší společnosti",
+    "Enter your companys phone number": "Zadejte telefon na vaši společnost",
+    "Enter your companys VAT id": "Zadejte DIČ vaší společnosti",
+    "Enter your companys email address": "Zadejte emailovou adresu na vaši společnost",
+    "Enter your companys website": "Zadejte webovou adresu vaší společnosti",
+    "Enter your custom invoice prefix": "Zadejte vlastní předčíslí faktury",
+    "Logo": "Logo",
+    "Select Invoice Logo": "Zvolte logo na faktuře",
+    "Store": "Obchod",
+    "Currency code": "Kód měny",
+    "Checkout the paypal docs to select the appropriate code": "Nahlédněte do dokumentace PayPalu pro vybrání správného kódu",
+    "Quantity": "Množství",
+    "Amount given to the user after purchasing": "Množství kreditů připsaných uživateli po zakoupení",
+    "Display": "Zobrazení",
+    "This is what the user sees at store and checkout": "Toto je náhled co uvidí zákazník v obchodu při objednávání",
+    "Adds 1000 credits to your account": "Připíše 1000 kreditů na váš účet",
+    "This is what the user sees at checkout": "Toto je náhled co uvidí zákazník při objednávání",
+    "No payment method is configured.": "Není nastavena žádná platební metoda.",
+    "To configure the payment methods, head to the .env and add the required options for your prefered payment method.": "Pro nastavení platebních metod běžte prosím do .env souboru a přidejte nutné nastavení požadované platební metody.",
+    "Useful Links": "Užitečné odkazy",
+    "Icon class name": "Jméno třídy ikony",
+    "You can find available free icons": "Dostupné ikony zdarma můžete najít",
+    "Title": "Nadpis",
+    "Link": "Odkaz",
+    "description": "popis",
+    "Icon": "Ikona",
+    "Username": "Uživatelské jméno",
+    "Email": "Email",
+    "Pterodactyl ID": "ID Pterodactylu",
+    "This ID refers to the user account created on pterodactyls panel.": "Toto ID koresponduje s účtem vytvořeným na pterodactyl panelu.",
+    "Only edit this if you know what youre doing :)": "Upravujte pouze v případě, že víte, co děláte :)",
+    "Server Limit": "Limit počtu serverů",
+    "Role": "Role",
+    " Administrator": " Správce",
+    "Client": "Zákazník",
+    "Member": "Člen",
+    "New Password": "Nové heslo",
+    "Confirm Password": "Potvrdit heslo",
+    "Notify": "Oznámit",
+    "Avatar": "Avatar",
+    "Verified": "Ověřený",
+    "Last seen": "Naposledy online",
+    "Notifications": "Oznámení",
+    "All": "Vše",
+    "Send via": "Poslat přes",
+    "Database": "Databáze",
+    "Content": "Obsah",
+    "Server limit": "Limit počtu serverů",
+    "Discord": "Discord",
+    "Usage": "Využití",
+    "IP": "IP",
+    "Vouchers": "Poukazy",
+    "Voucher details": "Podrobnosti poukazu",
+    "Summer break voucher": "Poukaz na letní prázdniny",
+    "Code": "Kód",
+    "Random": "Random",
+    "Uses": "Použití",
+    "A voucher can only be used one time per user. Uses specifies the number of different users that can use this voucher.": "Poukaz může být použit pouze jednou na uživatele. Počet použití upravuje počet různých uživatelů, kteří můžou poukaz použít.",
+    "Max": "Maximum",
+    "Expires at": "Vyprší",
+    "Used \/ Uses": "Použito",
+    "Expires": "Vyprší",
+    "Sign in to start your session": "Pro pokračování se prosím přihlašte",
+    "Password": "Heslo",
+    "Remember Me": "Zapamatovat si mě",
+    "Sign In": "Přihlásit se",
+    "Forgot Your Password?": "Zapomenuté heslo?",
+    "Register a new membership": "Zaregistrovat se",
+    "Please confirm your password before continuing.": "Před pokračováním prosím potvrďte své heslo.",
+    "You forgot your password? Here you can easily retrieve a new password.": "Zapomenuté heslo? Zde si můžete snadno vytvořit nové.",
+    "Request new password": "Zažádat o nové heslo",
+    "Login": "Přihlášení",
+    "You are only one step a way from your new password, recover your password now.": "Jste poslední krok od nového hesla. Obnovte své heslo teď.",
+    "Retype password": "Zopakovat heslo",
+    "Change password": "Změnit heslo",
+    "Register": "Registrovat",
+    "I already have a membership": "Už mám účet",
+    "Verify Your Email Address": "Ověř svou e-mailovou adresu",
+    "A fresh verification link has been sent to your email address.": "Nový ověřovací email byl odeslán na váš e-mail.",
+    "Before proceeding, please check your email for a verification link.": "Před pokračováním zkontrolujte vaši emailovou schránku a klikněte na ověřovací link.",
+    "If you did not receive the email": "Pokud vám nebyl email doručen",
+    "click here to request another": "klikněte zde pro získání nového",
+    "per month": "za měsíc",
+    "Out of Credits in": "Dostatek kreditů na",
+    "Home": "Domů",
+    "Languages": "Jazyky",
+    "See all Notifications": "Zobrazit všechna oznámení",
+    "Redeem code": "Uplatnit kód",
+    "Profile": "Profil",
+    "Log back in": "Přihlásit se zpět",
+    "Logout": "Odhlásit se",
+    "Administration": "Správa",
+    "Overview": "Přehled pro správce",
+    "Management": "Administrace",
+    "Other": "Další",
+    "Logs": "Logy",
+    "Warning!": "Varování!",
+    "You have not yet verified your email address": "Ještě nemáte ověřenou emailovou adresu",
+    "Click here to resend verification email": "Kliknutím zde odešlete nový ověřovací link",
+    "Please contact support If you didnt receive your verification email.": "Prosím kontaktujte podporu, pokud jste nedostal ověřovací email.",
+    "Thank you for your purchase!": "Děkujeme za váš nákup!",
+    "Your payment has been confirmed; Your credit balance has been updated.": "Platbaa byla úspěšná, vaše kredity byly připsány.",
+    "Thanks": "Děkujeme",
+    "Redeem voucher code": "Uplatnit kód poukazu",
+    "Close": "Zavřít",
+    "Redeem": "Uplatnit",
+    "All notifications": "Všechna oznámení",
+    "Required Email verification!": "Je potřeba mít ověřený email!",
+    "Required Discord verification!": "Je potřeba mít ověřený discord!",
+    "You have not yet verified your discord account": "Ještě nemáte ověřený discord účet",
+    "Login with discord": "Přihlásit se s Discordem",
+    "Please contact support If you face any issues.": "Prosím kontaktujte podporu pokud máte jakýkoliv problém.",
+    "Due to system settings you are required to verify your discord account!": "Kvůli systémovému nastavení je třeba ověřit si discord účet!",
+    "It looks like this hasnt been set-up correctly! Please contact support.": "Vypadá to, že je něco špatně nastaveno! Prosím kontaktujte správce.",
+    "Change Password": "Změnit heslo",
+    "Current Password": "Aktuální heslo",
+    "Link your discord account!": "Ověřte váš Discord účet!",
+    "By verifying your discord account, you receive extra Credits and increased Server amounts": "Ověřením vašeho discord účtu získáte kredity navíc a zvýšený limit počtu serverů",
+    "Login with Discord": "Přihlašte se s Discordem",
+    "You are verified!": "Jste ověřený!",
+    "Re-Sync Discord": "Aktualizovat propojení s discordem",
+    "Save Changes": "Uložit změny",
+    "Server configuration": "Konfigurace serveru",
+    "Error!": "Chyba!",
+    "Make sure to link your products to nodes and eggs.": "Nezapomeňte propojit své balíčky k uzlům a distribucím.",
+    "There has to be at least 1 valid product for server creation": "Pro vytvoření serveru musí být nastavena alespoň 1 balíček",
+    "No products available!": "Žádné dostupné balíčky!",
+    "No nodes have been linked!": "Nebyly propojeny žádné uzly!",
+    "No nests available!": "Žádný dostupný software\/hry!",
+    "No eggs have been linked!": "Nebyly nastaveny žádné distribuce!",
+    "Software \/ Games": "Software\/hry",
+    "Please select software ...": "Prosím zvolte software ...",
+    "---": "---",
+    "Specification ": "Specifikace ",
+    "Node": "Uzel",
+    "Resource Data:": "Data prostředků:",
+    "vCores": "vlákna",
+    "MB": "MB",
+    "MySQL": "MySQL",
+    "ports": "portů",
+    "Not enough": "Nedostatek",
+    "Create server": "Vytvořit server",
+    "Please select a node ...": "Prosím vyberte uzel ...",
+    "No nodes found matching current configuration": "Pro zvolenou konfiguraci nebyly nalezeny žádné uzly",
+    "Please select a resource ...": "Prosím vyberte balíček ...",
+    "No resources found matching current configuration": "Pro zvolenou konfiguraci nebyly nalezeny žádné balíčky",
+    "Please select a configuration ...": "Prosím vyberte konfiguraci ...",
+    "Not enough credits!": "Nedostatek kreditů!",
+    "Create Server": "Vytvořit server",
+    "Software": "Software",
+    "Specification": "Specifikace",
+    "Resource plan": "Balíček prosředků",
+    "RAM": "RAM",
+    "MySQL Databases": "Databáze MySQL",
+    "per Hour": "za hodinu",
+    "per Month": "za měsíc",
+    "Manage": "Spravovat",
+    "Are you sure?": "Jste si jistý?",
+    "This is an irreversible action, all files of this server will be removed.": "Tato akce je nevratná. Všechny soubory tohoto serveru budou odstraněny.",
+    "Yes, delete it!": "Ano, odstranit!",
+    "No, cancel!": "Ne, zrušit!",
+    "Canceled ...": "Zrušeno ...",
+    "Deletion has been canceled.": "Mazání bylo zrušeno.",
+    "Date": "Datum",
+    "To": "Komu",
+    "From": "Od",
+    "Pending": "Čekající",
+    "Subtotal": "Mezisoučet",
+    "Payment Methods": "Platební metody",
+    "Amount Due": "Platba ke dni",
+    "Tax": "Poplatek",
+    "Submit Payment": "Potvrdit platbu",
+    "Purchase": "Zakoupit",
+    "There are no store products!": "Nejsou dostupné žádné balíčky pro nákup kreditů!",
+    "The store is not correctly configured!": "Obchod není správně nastaven!",
+    "Serial No.": "Sériové číslo",
+    "Invoice date": "Datum fakturace",
+    "Seller": "Prodejce",
+    "Buyer": "Kupující",
+    "Address": "Adresa",
+    "VAT Code": "DIČ",
+    "Phone": "Telefon",
+    "Units": "Jednotky",
+    "Discount": "Sleva",
+    "Total discount": "Celková sleva",
+    "Taxable amount": "Zpoplatněná částka",
+    "Tax rate": "Poplatková sazba",
+    "Total taxes": "Celkem poplatek",
+    "Shipping": "Dodání",
+    "Total amount": "Celková částka",
+    "Notes": "Poznámky",
+    "Amount in words": "Celkem slovy",
+    "Please pay until": "Splatné do",
+    "Account already exists on Pterodactyl. Please contact the Support!": "Účet na Pterodaktylu již existuje. Kontaktujte prosím podporu!"
+}

+ 18 - 0
resources/lang/cs/auth.php

@@ -0,0 +1,18 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Authentication Language Lines
+|--------------------------------------------------------------------------
+|
+| The following language lines are used during authentication for various
+| messages that we need to display to the user. You are free to modify
+| these language lines according to your application's requirements.
+|
+*/
+
+return [
+    'failed'   => 'Tyto přihlašovací údaje neodpovídají žadnému záznamu.',
+    'password' => 'Zadané heslo je neplatné.',
+    'throttle' => 'Příliš mnoho pokusů o přihlášení. Zkuste to prosím znovu za :seconds sekund.',
+];

+ 17 - 0
resources/lang/cs/pagination.php

@@ -0,0 +1,17 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Pagination Language Lines
+|--------------------------------------------------------------------------
+|
+| The following language lines are used by the paginator library to build
+| the simple pagination links. You are free to change them to anything
+| you want to customize your views to better match your application.
+|
+*/
+
+return [
+    'next'     => 'další &raquo;',
+    'previous' => '&laquo; předchozí',
+];

+ 20 - 0
resources/lang/cs/passwords.php

@@ -0,0 +1,20 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Password Reset Language Lines
+|--------------------------------------------------------------------------
+|
+| The following language lines are the default lines which match reasons
+| that are given by the password broker for a password update attempt
+| has failed, such as for an invalid token or invalid new password.
+|
+*/
+
+return [
+    'reset'     => 'Heslo bylo obnoveno!',
+    'sent'      => 'E-mail s instrukcemi k obnovení hesla byl odeslán!',
+    'throttled' => 'Počkejte prosím a zkuste to znovu.',
+    'token'     => 'Klíč pro obnovu hesla je nesprávný.',
+    'user'      => 'Nepodařilo se najít uživatele s touto e-mailovou adresou.',
+];

+ 135 - 0
resources/lang/cs/validation.php

@@ -0,0 +1,135 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Validation Language Lines
+|--------------------------------------------------------------------------
+|
+| The following language lines contain the default error messages used by
+| the validator class. Some of these rules have multiple versions such
+| as the size rules. Feel free to tweak each of these messages here.
+|
+*/
+
+return [
+    'accepted'             => ':attribute musí být přijat.',
+    'accepted_if'          => 'The :attribute must be accepted when :other is :value.',
+    'active_url'           => ':attribute není platnou URL adresou.',
+    'after'                => ':attribute musí být datum po :date.',
+    'after_or_equal'       => ':attribute musí být datum :date nebo pozdější.',
+    'alpha'                => ':attribute může obsahovat pouze písmena.',
+    'alpha_dash'           => ':attribute může obsahovat pouze písmena, číslice, pomlčky a podtržítka. České znaky (á, é, í, ó, ú, ů, ž, š, č, ř, ď, ť, ň) nejsou podporovány.',
+    'alpha_num'            => ':attribute může obsahovat pouze písmena a číslice.',
+    'array'                => ':attribute musí být pole.',
+    'attached'             => 'Tento :attribute je již připojen.',
+    'before'               => ':attribute musí být datum před :date.',
+    'before_or_equal'      => 'Datum :attribute musí být před nebo rovno :date.',
+    'between'              => [
+        'array'   => ':attribute musí obsahovat nejméně :min a nesmí obsahovat více než :max prvků.',
+        'file'    => ':attribute musí být větší než :min a menší než :max Kilobytů.',
+        'numeric' => ':attribute musí být hodnota mezi :min a :max.',
+        'string'  => ':attribute musí být delší než :min a kratší než :max znaků.',
+    ],
+    'boolean'              => ':attribute musí být true nebo false',
+    'confirmed'            => ':attribute nesouhlasí.',
+    'current_password'     => 'Současné heslo není spravné.',
+    'date'                 => ':attribute musí být platné datum.',
+    'date_equals'          => ':attribute musí být datum shodné s :date.',
+    'date_format'          => ':attribute není platný formát data podle :format.',
+    'declined'             => 'The :attribute must be declined.',
+    'declined_if'          => 'The :attribute must be declined when :other is :value.',
+    'different'            => ':attribute a :other se musí lišit.',
+    'digits'               => ':attribute musí být :digits pozic dlouhé.',
+    'digits_between'       => ':attribute musí být dlouhé nejméně :min a nejvíce :max pozic.',
+    'dimensions'           => ':attribute má neplatné rozměry.',
+    'distinct'             => ':attribute má duplicitní hodnotu.',
+    'email'                => ':attribute není platný formát.',
+    'ends_with'            => ':attribute musí končit jednou z následujících hodnot: :values',
+    'exists'               => 'Zvolená hodnota pro :attribute není platná.',
+    'file'                 => ':attribute musí být soubor.',
+    'filled'               => ':attribute musí být vyplněno.',
+    'gt'                   => [
+        'array'   => 'Pole :attribute musí mít více prvků než :value.',
+        'file'    => 'Velikost souboru :attribute musí být větší než :value kB.',
+        'numeric' => ':attribute musí být větší než :value.',
+        'string'  => 'Počet znaků :attribute musí být větší :value.',
+    ],
+    'gte'                  => [
+        'array'   => 'Pole :attribute musí mít :value prvků nebo více.',
+        'file'    => 'Velikost souboru :attribute musí být větší nebo rovno :value kB.',
+        'numeric' => ':attribute musí být větší nebo rovno :value.',
+        'string'  => 'Počet znaků :attribute musí být větší nebo rovno :value.',
+    ],
+    'image'                => ':attribute musí být obrázek.',
+    'in'                   => 'Zvolená hodnota pro :attribute je neplatná.',
+    'in_array'             => ':attribute není obsažen v :other.',
+    'integer'              => ':attribute musí být celé číslo.',
+    'ip'                   => ':attribute musí být platnou IP adresou.',
+    'ipv4'                 => ':attribute musí být platná IPv4 adresa.',
+    'ipv6'                 => ':attribute musí být platná IPv6 adresa.',
+    'json'                 => ':attribute musí být platný JSON řetězec.',
+    'lt'                   => [
+        'array'   => ':attribute by měl obsahovat méně než :value položek.',
+        'file'    => 'Velikost souboru :attribute musí být menší než :value kB.',
+        'numeric' => ':attribute musí být menší než :value.',
+        'string'  => ':attribute musí obsahovat méně než :value znaků.',
+    ],
+    'lte'                  => [
+        'array'   => ':attribute by měl obsahovat maximálně :value položek.',
+        'file'    => 'Velikost souboru :attribute musí být menší než :value kB.',
+        'numeric' => ':attribute musí být menší nebo rovno než :value.',
+        'string'  => ':attribute nesmí být delší než :value znaků.',
+    ],
+    'max'                  => [
+        'array'   => ':attribute nemůže obsahovat více než :max prvků.',
+        'file'    => 'Velikost souboru :attribute musí být menší než :value kB.',
+        'numeric' => ':attribute nemůže být větší než :max.',
+        'string'  => ':attribute nemůže být delší než :max znaků.',
+    ],
+    'mimes'                => ':attribute musí být jeden z následujících datových typů :values.',
+    'mimetypes'            => ':attribute musí být jeden z následujících datových typů :values.',
+    'min'                  => [
+        'array'   => ':attribute musí obsahovat více než :min prvků.',
+        'file'    => ':attribute musí být větší než :min kB.',
+        'numeric' => ':attribute musí být větší než :min.',
+        'string'  => ':attribute musí být delší než :min znaků.',
+    ],
+    'multiple_of'          => ':attribute musí být násobkem :value',
+    'not_in'               => 'Zvolená hodnota pro :attribute je neplatná.',
+    'not_regex'            => ':attribute musí být regulární výraz.',
+    'numeric'              => ':attribute musí být číslo.',
+    'password'             => 'Heslo je nesprávné.',
+    'present'              => ':attribute musí být vyplněno.',
+    'prohibited'           => 'Pole :attribute je zakázáno.',
+    'prohibited_if'        => 'Pole :attribute je zakázáno, když je :other :value.',
+    'prohibited_unless'    => 'Pole :attribute je zakázáno, pokud není rok :other v roce :values.',
+    'prohibits'            => 'The :attribute field prohibits :other from being present.',
+    'regex'                => ':attribute nemá správný formát.',
+    'relatable'            => 'Tento :attribute nemusí být spojen s tímto zdrojem.',
+    'required'             => ':attribute musí být vyplněno.',
+    'required_if'          => ':attribute musí být vyplněno pokud :other je :value.',
+    'required_unless'      => ':attribute musí být vyplněno dokud :other je v :values.',
+    'required_with'        => ':attribute musí být vyplněno pokud :values je vyplněno.',
+    'required_with_all'    => ':attribute musí být vyplněno pokud :values je zvoleno.',
+    'required_without'     => ':attribute musí být vyplněno pokud :values není vyplněno.',
+    'required_without_all' => ':attribute musí být vyplněno pokud není žádné z :values zvoleno.',
+    'same'                 => ':attribute a :other se musí shodovat.',
+    'size'                 => [
+        'array'   => ':attribute musí obsahovat právě :size prvků.',
+        'file'    => ':attribute musí mít přesně :size Kilobytů.',
+        'numeric' => ':attribute musí být přesně :size.',
+        'string'  => ':attribute musí být přesně :size znaků dlouhý.',
+    ],
+    'starts_with'          => ':attribute musí začínat jednou z následujících hodnot: :values',
+    'string'               => ':attribute musí být řetězec znaků.',
+    'timezone'             => ':attribute musí být platná časová zóna.',
+    'unique'               => ':attribute musí být unikátní.',
+    'uploaded'             => 'Nahrávání :attribute se nezdařilo.',
+    'url'                  => 'Formát :attribute je neplatný.',
+    'uuid'                 => ':attribute musí být validní UUID.',
+    'custom'               => [
+        'attribute-name' => [
+            'rule-name' => 'custom-message',
+        ],
+    ],
+];

+ 445 - 0
resources/lang/de.json

@@ -0,0 +1,445 @@
+{
+    "Invoice settings updated!": "Rechnungseinstellungen aktualisiert!",
+    "Language settings have not been updated!": "Spracheinstellungen wurden nicht aktualisiert!",
+    "Language settings updated!": "Spracheinstellungen wurden aktualisiert!",
+    "Misc settings have not been updated!": "Allgemeine Einstellungen wurden nicht aktualisiert!",
+    "Misc settings updated!": "Allgemeine Einstellungen wurden aktualisiert!",
+    "Payment settings have not been updated!": "Zahlungseinstellungen wurden nicht aktualisiert!",
+    "Payment settings updated!": "Zahlungseinstellungen wurden aktualisiert!",
+    "System settings have not been updated!": "Systemeinstellungen wurden nicht aktualisiert!",
+    "System settings updated!": "Systemeinstellungen wurden aktualisiert!",
+    "api key created!": "API Key erstellt",
+    "api key updated!": "API Key updated",
+    "api key has been removed!": "API Key gelöscht",
+    "Edit": "Bearbeiten",
+    "Delete": "Löschen",
+    "Store item has been created!": "Item wurde erstellt!",
+    "Store item has been updated!": "Item updated",
+    "Product has been updated!": "Product updated",
+    "Store item has been removed!": "Item gelöscht",
+    "Created at": "Erstellt am",
+    "Error!": "Fehler!",
+    "unknown": "unbekannt",
+    "Pterodactyl synced": "Pterodactyl synced",
+    "Your credit balance has been increased!": "Dein Kontostand wurde updated",
+    "Your payment is being processed!": "Deine Bezahlung wurde verarbeitet!",
+    "Your payment has been canceled!": "Deine Bezahlung wurde abgebrochen!",
+    "Payment method": "Bezahlmethode",
+    "Invoice": "Rechnung",
+    "Download": "Herunterladen",
+    "Product has been created!": "Produkt erstellt",
+    "Product has been removed!": "Produkt gelöscht",
+    "Show": "Zeige",
+    "Clone": "Klonen",
+    "Server removed": "Server gelöscht",
+    "An exception has occurred while trying to remove a resource \"": "Folgender Fehler ist aufgetreten: ",
+    "Server has been updated!": "Server updated",
+    "Unsuspend": "Reaktivieren",
+    "Suspend": "Deaktivieren",
+    "configuration has been updated!": "Konfig updated",
+    "link has been created!": "Link erstellt!",
+    "link has been updated!": "Link updated!",
+    "product has been removed!": "Das Produkt wurde entfernt!",
+    "User does not exists on pterodactyl's panel": "User existiert nicht in Pterodactyl",
+    "user has been removed!": "User gelöscht",
+    "Notification sent!": "Nachricht gesendet",
+    "User has been updated!": "User updated!",
+    "Login as User": "Als User anmelden",
+    "voucher has been created!": "Gutschein erstellt",
+    "voucher has been updated!": "Gutschein Updated",
+    "voucher has been removed!": "Gutschein gelöscht",
+    "This voucher has reached the maximum amount of uses": "Maximale Anzahl an Einlösungen erreicht",
+    "This voucher has expired": "Gutschein abgelaufen",
+    "You already redeemed this voucher code": "Du hast diesen Gutschein bereits eingelöst",
+    "have been added to your balance!": "wurden deinem Konto hinzugefügt",
+    "Users": "Benutzer",
+    "VALID": "GÜLTIG",
+    "Account already exists on Pterodactyl. Please contact the Support!": "Der Account existiert bereits bei Pterodactyl. Kontaktiere den Support!",
+    "days": "Tage",
+    "hours": "Stunden",
+    "You ran out of Credits": "Keine Credits übrig!",
+    "Profile updated": "Profile updated",
+    "Server limit reached!": "Server Limit erreicht!",
+    "You are required to verify your email address before you can create a server.": "Du musst deine E-Mail verifizieren bevor du einen Server erstellen kannst",
+    "You are required to link your discord account before you can create a server.": "Du musst dein Discord verifizieren bevor du einen Server erstellen kannst",
+    "Server created": "Server erstellt!",
+    "No allocations satisfying the requirements for automatic deployment on this node were found.": "Keine automatischen Portzuweisungen für dieses Node vorhanden",
+    "You are required to verify your email address before you can purchase credits.": "Vor dem Kauf musst du deine E-Mail verifizieren",
+    "You are required to link your discord account before you can purchase Credits": "Du musst deinen Discord Account verbinden, bevor du Guthaben kaufen kannst",
+    "EXPIRED": "ABGELAUFEN",
+    "Payment Confirmation": "Zahlungsbestätigung",
+    "Payment Confirmed!": "Zahlung bestätigt!",
+    "Your Payment was successful!": "Deine Zahlung ist erfolgreich bei uns eingegangen!",
+    "Hello": "Hallo",
+    "Your payment was processed successfully!": "Deine Zahlung wurde erfolgreich verarbeitet!",
+    "Status": "Status",
+    "Price": "Preis",
+    "Type": "Typ",
+    "Amount": "Anzahl",
+    "Balance": "Stand",
+    "User ID": "User-ID",
+    "Server Creation Error": "Fehler beim erstellen des Servers",
+    "Your servers have been suspended!": "Deine Server wurden pausiert",
+    "To automatically re-enable your server\/s, you need to purchase more credits.": "Um deine Server zu reaktivieren, musst du mehr Credits kaufen!",
+    "Purchase credits": "Credits kaufen",
+    "If you have any questions please let us know.": "Solltest du weiter fragen haben, melde dich gerne beim Support!",
+    "Regards": "mit freundlichen Grüßen",
+    "Getting started!": "Den Anfang machen!",
+    "Activity Logs": "Aktivitäts logs",
+    "Dashboard": "Dashboard",
+    "No recent activity from cronjobs": "Keine neuen aktivitäten von Cronjobs",
+    "Are cronjobs running?": "Sind die Cronjobs gestartet?",
+    "Check the docs for it here": "Zur Dokumentation",
+    "Causer": "Verursacher",
+    "Description": "Beschreibung",
+    "Application API": "API",
+    "Create": "Erstellen",
+    "Memo": "Name",
+    "Submit": "Abschicken",
+    "Create new": "Neu erstellen",
+    "Token": "Token",
+    "Last used": "Zuletzt benutzt",
+    "Are you sure you wish to delete?": "Sicher, dass du dies löschen möchtest?",
+    "Nests": "Nests",
+    "Sync": "Sync",
+    "Active": "Aktiv",
+    "ID": "ID",
+    "eggs": "eggs",
+    "Name": "Name",
+    "Nodes": "Nodes",
+    "Location": "Standort",
+    "Admin Overview": "Admin Übersicht",
+    "Support server": "Discord Server",
+    "Documentation": "Dokumentation",
+    "Github": "Github",
+    "Support ControlPanel": "Unterstütze Controlpanel.gg",
+    "Servers": "Server",
+    "Total": "Gesamt",
+    "Payments": "Zahlungen",
+    "Pterodactyl": "Pterodactyl",
+    "Resources": "Ressourcen",
+    "Count": "Anzahl",
+    "Locations": "Standorte",
+    "Eggs": "Eggs",
+    "Last updated :date": "Zuletzt aktualisiert :date",
+    "Download all Invoices": "Alle Rechnungen runterladen",
+    "Product Price": "Produktpreis",
+    "Tax Value": "Steuern",
+    "Tax Percentage": "Steuersatz",
+    "Total Price": "Gesamtpreis",
+    "Payment ID": "Zahlungs-ID",
+    "Payment Method": "Bezahlmethode",
+    "Products": "Produkte",
+    "Product Details": "Produktdetails",
+    "Disabled": "Deaktiviert",
+    "Will hide this option from being selected": "Wird dieses Produkt nicht zum Kauf zur Verfügung stellen",
+    "Price in": "Preis in ",
+    "Memory": "Arbeitsspeicher",
+    "Cpu": "Prozessorleistung",
+    "Swap": "Swap",
+    "This is what the users sees": "Das wird der Benutzer sehen",
+    "Disk": "Festplatte",
+    "Minimum": "Mindest",
+    "Setting to -1 will use the value from configuration.": "Benutzt den Standard, wenn der Wert auf -1 gesetzt wird",
+    "IO": "IO",
+    "Databases": "Datenbanken",
+    "Backups": "Backups",
+    "Allocations": "Port Zuweisungen",
+    "Product Linking": "Produktbeziehungen",
+    "Link your products to nodes and eggs to create dynamic pricing for each option": "Verbinde deine Produkte mit Nodes und Eggs um ein dynamisches Preismodell zu erstellen",
+    "This product will only be available for these nodes": "Dieses Produkt wurd nur für die ausgewählten Nodes verfügbar sein",
+    "This product will only be available for these eggs": "Dieses Produkt wurd nur für die ausgewählten Eggs verfügbar sein",
+    "Product": "Produkt",
+    "CPU": "CPU",
+    "Updated at": "Aktualisiert",
+    "User": "Benutzer",
+    "Config": "Konfiguration",
+    "Suspended at": "Suspendiert",
+    "Settings": "Einstellungen",
+    "The installer is not locked!": "Der Installer ist nicht gesperrt!",
+    "please create a file called \"install.lock\" in your dashboard Root directory. Otherwise no settings will be loaded!": "Bitte erstellen Sie eine Datei mit dem Namen \"install.lock\" in Ihrem Dashboard-Root-Verzeichnis. Sonst werden keine Einstellungen geladen!",
+    "or click here": "oder klicke hier",
+    "Company Name": "Firmenname",
+    "Company Adress": "Firmenadresse",
+    "Company Phonenumber": "Firmen Telefonnummer",
+    "VAT ID": "Umsatzsteuer-ID",
+    "Company E-Mail Adress": "Firmen E-Mail Adresse",
+    "Company Website": "Firmenwebseite",
+    "Invoice Prefix": "Rechnungspräfix",
+    "Enable Invoices": "Rechnungen aktivieren",
+    "Logo": "Logo",
+    "Select Invoice Logo": "Firmenlogo auswählen",
+    "Available languages": "Verfügbare Sprachen",
+    "Default language": "Standardsprache",
+    "The fallback Language, if something goes wrong": "Die Rückfall-Sprache, falls etwas schief geht",
+    "Datable language": "Tabellensprache",
+    "The datatables lang-code. <br><strong>Example:<\/strong> en-gb, fr_fr, de_de<br>More Information: ": "Der Sprachcode der Tabellensprache. <br><strong>Beispiel:<\/strong> en-gb, fr_fr, de_de<br>Weitere Informationen: ",
+    "Auto-translate": "Automatisches übersetzen",
+    "If this is checked, the Dashboard will translate itself to the Clients language, if available": "Wenn dies aktiviert ist, übersetzt sich das Dashboard selbst in die Sprache des Clients, sofern diese verfügbar ist",
+    "Client Language-Switch": "Nutzer Sprachumschaltung",
+    "If this is checked, Clients will have the ability to manually change their Dashboard language": "Wenn dies aktiviert ist, haben Nutzer die Möglichkeit, ihre Dashboard-Sprache manuell zu ändern",
+    "Mail Service": "E-Mail Service",
+    "The Mailer to send e-mails with": "Der Mailer zum Versenden von E-Mails",
+    "Mail Host": "E-Mail Hostadresse",
+    "Mail Port": "E-Mail Port",
+    "Mail Username": "E-Mail Nutzername",
+    "Mail Password": "E-Mail Passwort",
+    "Mail Encryption": "E-Mail Verschlüsselungsart",
+    "Mail From Adress": "Absender E-Mailadresse",
+    "Mail From Name": "Absender E-Mailname",
+    "Discord Client-ID": "Discord Client-ID",
+    "Discord Client-Secret": "Discord Client-Secret",
+    "Discord Bot-Token": "Discord Bot-Token",
+    "Discord Guild-ID": "Discord Guild-ID",
+    "Discord Invite-URL": "Discord Invite-URL",
+    "Discord Role-ID": "Discord Rollen-ID",
+    "Enable ReCaptcha": "Aktiviere ReCaptcha",
+    "ReCaptcha Site-Key": "ReCaptcha Site-Key",
+    "ReCaptcha Secret-Key": "ReCaptcha Secret-Key",
+    "PayPal Client-ID": "PayPal Client-ID",
+    "PayPal Secret-Key": "PayPal Secret-Key",
+    "PayPal Sandbox Client-ID": "PayPal Sandbox Client-ID",
+    "optional": "Optional",
+    "PayPal Sandbox Secret-Key": "PayPal Sandbox Secret-Key",
+    "Stripe Secret-Key": "Stripe Secret-Key",
+    "Stripe Endpoint-Secret-Key": "Stripe Endpoint-Secret-Key",
+    "Stripe Test Secret-Key": "Stripe Test Secret-Key",
+    "Stripe Test Endpoint-Secret-Key": "Stripe Test Endpoint-Secret-Key",
+    "Payment Methods": "Zahlungsmethoden",
+    "Tax Value in %": "Steuer in %",
+    "System": "System",
+    "Register IP Check": "IP-Adressen registrierungs Prüfung",
+    "Prevent users from making multiple accounts using the same IP address.": "Verhindern Sie, dass Benutzer mehrere Konten mit derselben IP-Adresse erstellen.",
+    "Charge first hour at creation": "Berechne die erste Stunde bei Erstellung",
+    "Charges the first hour worth of credits upon creating a server.": "Rechne den ersten stündlichen Anteil direkt bei Erstellung des Servers ab.",
+    "Credits Display Name": "Credits Anzeigename",
+    "PHPMyAdmin URL": "PHPMyAdmin URL",
+    "Enter the URL to your PHPMyAdmin installation. <strong>Without a trailing slash!<\/strong>": "Geben Sie die URL zu Ihrer PHPMyAdmin-Installation ein. <strong>Ohne abschließendendes Slash!<\/strong>",
+    "Pterodactyl URL": "Pterodactyl URL",
+    "Enter the URL to your Pterodactyl installation. <strong>Without a trailing slash!<\/strong>": "Geben Sie die URL zu Ihrer Pterodactyl-Installation ein. <strong>Ohne abschließendendes Slash!<\/strong>",
+    "Pterodactyl API Key": "Pterodactyl API Key",
+    "Enter the API Key to your Pterodactyl installation.": "Geben Sie den API-Schlüssel zu Ihrer Pterodactyl-Installation ein.",
+    "Force Discord verification": "Discord Verifikation erzwingen",
+    "Force E-Mail verification": "E-Mail Verifikation erzwingen",
+    "Initial Credits": "Anfängliche Credits",
+    "Initial Server Limit": "Anfängliches Serverlimit",
+    "Credits Reward Amount - Discord": "Credits-Belohnungsbetrag - Discord",
+    "Credits Reward Amount - E-Mail": "Credits Belohnungsbetrag - E-Mail",
+    "Server Limit Increase - Discord": "Erhöhung des Serverlimits - Discord",
+    "Server Limit Increase - E-Mail": "Erhöhung des Serverlimits - E-Mail",
+    "Server": "Server",
+    "Server Allocation Limit": "Serverzuweisungslimit",
+    "The maximum amount of allocations to pull per node for automatic deployment, if more allocations are being used than this limit is set to, no new servers can be created!": "Die maximale Menge an Zuweisungen, die pro Knoten für die automatische Bereitstellung abgerufen werden können. Wenn mehr Zuweisungen verwendet werden, als dieses Limit festgelegt ist, können keine neuen Server erstellt werden!",
+    "Select panel icon": "Icon auswählen",
+    "Select panel favicon": "Favicon auswählen",
+    "Store": "Laden",
+    "Currency code": "Währungscode",
+    "Checkout the paypal docs to select the appropriate code": "Siehe Paypal für die entsprechenden Codes",
+    "Quantity": "Menge",
+    "Amount given to the user after purchasing": "Anzahl, die der User nach dem Kauf bekommt",
+    "Display": "Anzeigename",
+    "This is what the user sees at store and checkout": "Dies ist die 'Anzahl' welche der User beim Kaufen sieht",
+    "Adds 1000 credits to your account": "Fügt deinem Account 1000 Credits hinzu",
+    "This is what the user sees at checkout": "Dies ist die Beschreibung auf der Rechnung und was der Kunde beim kauf sieht",
+    "No payment method is configured.": "Zurzeit wurde keine Bezahlmethode festgelegt.",
+    "To configure the payment methods, head to the .env and add the required options for your prefered payment method.": "Öffne die .env Datei und fülle die benötigten Felder aus, um eine Bezahlmethode zu konfigurieren.",
+    "Useful Links": "Nützliche Links",
+    "Icon class name": "Icon Klassen-Name",
+    "You can find available free icons": "Hier gibt es kostenlose Icons",
+    "Title": "Titel",
+    "Link": "Link",
+    "description": "Beschreibung",
+    "Icon": "Symbol",
+    "Username": "Username",
+    "Email": "E-Mail",
+    "Pterodactyl ID": "Pterodactyl ID",
+    "This ID refers to the user account created on pterodactyls panel.": "Die ist die Pterodactyl-ID des Users",
+    "Only edit this if you know what youre doing :)": "Bearbeite dies nur, wenn du weißt, was du tust :)",
+    "Server Limit": "Serverlimit",
+    "Role": "Rolle",
+    " Administrator": " Administrator",
+    "Client": "Client",
+    "Member": "Member",
+    "New Password": "Neues Passwort",
+    "Confirm Password": "Passwort bestätigen",
+    "Notify": "Benachrichtigen",
+    "Avatar": "Avatar",
+    "Verified": "Verifiziert",
+    "Last seen": "Zuletzt online",
+    "Notifications": "Benachrichtigungen",
+    "All": "Alle",
+    "Send via": "Senden via",
+    "Database": "Datenbank",
+    "Content": "Inhalt",
+    "Server limit": "Serverlimit",
+    "Discord": "Discord",
+    "Usage": "Nutzung",
+    "IP": "IP",
+    "Vouchers": "Gutscheine",
+    "Voucher details": "Gutschein details",
+    "Summer break voucher": "Summer break Gutschein",
+    "Code": "Code",
+    "Random": "Zufällig",
+    "Uses": "Benutzungen",
+    "A voucher can only be used one time per user. Uses specifies the number of different users that can use this voucher.": "Ein Gutschein kann von einem User nur einmal eingelöst werden. \"Benutzungen\" setzt die Anzahl an Usern die diesen Gutschein einlösen können.",
+    "Max": "Max",
+    "Expires at": "Läuft ab am",
+    "Used \/ Uses": "Benutzungen",
+    "Expires": "Ablauf",
+    "Sign in to start your session": "Melde dich an um das Dashboard zu benutzen",
+    "Password": "Passwort",
+    "Remember Me": "Login Speichern",
+    "Sign In": "Anmelden",
+    "Forgot Your Password?": "Passwort vergessen?",
+    "Register a new membership": "Neuen Account registrieren",
+    "Please confirm your password before continuing.": "Bitte bestätige dein Passwort bevor du fortfährst",
+    "You forgot your password? Here you can easily retrieve a new password.": "Passwort vergessen? Hier kannst du ganz leicht ein neues anfordern",
+    "Request new password": "Neues Passwort anfordern",
+    "Login": "Anmelden",
+    "You are only one step a way from your new password, recover your password now.": "Du bist nurnoch einen Schritt von deinem Passwort entfernt.",
+    "Retype password": "Passwort bestätigen",
+    "Change password": "Passwort ändern",
+    "Register": "Registrieren",
+    "I already have a membership": "Ich habe bereits einen Account",
+    "Verify Your Email Address": "Bestätige deine E-Mail Adresse",
+    "A fresh verification link has been sent to your email address.": "Dir wurde ein neuer Verifizierungslink zugeschickt",
+    "Before proceeding, please check your email for a verification link.": "Bitte überprüfe dein E-Mail Postfach nach einem Verifizierungslink",
+    "If you did not receive the email": "Solltest du keine E-Mail erhalten haben",
+    "click here to request another": "Klicke hier um eine neue zu erhalten",
+    "per month": "pro Monat",
+    "Out of Credits in": "Keine :credits mehr in",
+    "Home": "Startseite",
+    "Language": "Sprache",
+    "See all Notifications": "Alle Nachrichten anzeigen",
+    "Redeem code": "Code einlösen",
+    "Profile": "Profil",
+    "Log back in": "Zurück anmelden",
+    "Logout": "Abmelden",
+    "Administration": "Administration",
+    "Overview": "Übersicht",
+    "Management": "Management",
+    "Other": "Anderes",
+    "Logs": "Logs",
+    "Warning!": "Warnung!",
+    "You have not yet verified your email address": "Deine E-Mail Adresse ist nicht bestätigt",
+    "Click here to resend verification email": "Klicke hier, um eine neue Bestätigungsmail zu senden",
+    "Please contact support If you didnt receive your verification email.": "Wende dich an den Kundensupport wenn du keine E-Mail erhalten hast",
+    "Thank you for your purchase!": "Vielen Dank für deinen Einkauf!",
+    "Your payment has been confirmed; Your credit balance has been updated.": "Deine Zahlung wurde bestätigt und deine Credits angepasst",
+    "Thanks": "Vielen Dank",
+    "Redeem voucher code": "Gutscheincode einlösen",
+    "Close": "Schließen",
+    "Redeem": "Einlösen",
+    "All notifications": "Alle Nachrichten",
+    "Required Email verification!": "E-Mail verifizierung nötig!",
+    "Required Discord verification!": "Discord verifizierung nötig!",
+    "You have not yet verified your discord account": "Du hast deinen Discord Account noch nicht bestätigt",
+    "Login with discord": "Mit discord anmelden",
+    "Please contact support If you face any issues.": "Melde dich beim Support, solltest du Probleme haben",
+    "Due to system settings you are required to verify your discord account!": "Um das System zu benutzten, musst du deinen Discord Account bestätigen",
+    "It looks like this hasnt been set-up correctly! Please contact support.": "Es scheint so, als wäre dies nicht richtig Konfiguriert. Bitte melde dich beim Support",
+    "Change Password": "Passwort ändern",
+    "Current Password": "Momentanes Passwort",
+    "Link your discord account!": "Discord Account verbinden!",
+    "By verifying your discord account, you receive extra Credits and increased Server amounts": "Wenn du deinen Discordaccount verifizierst, bekommst du extra Credits und ein erhöhtes Server Limit",
+    "Login with Discord": "Mit Discord anmelden",
+    "You are verified!": "Du bist verifiziert!",
+    "Re-Sync Discord": "Resync Discord",
+    "Save Changes": "Änderungen speichern",
+    "Server configuration": "Server Konfiguration",
+    "Make sure to link your products to nodes and eggs.": "Stelle sicher, dass deine Produkte mit Nodes und Eggs verknüpft sind",
+    "There has to be at least 1 valid product for server creation": "Es muss mindestens 1 aktives Produkt erstellt sein, bevor ein Server erstellt wird",
+    "Sync now": "Jetzt synchronisieren",
+    "No products available!": "Keine Produkte verfügbar!",
+    "No nodes have been linked!": "Es wurde keine Nodes verknüpft",
+    "No nests available!": "Keine Nests verfügbar",
+    "No eggs have been linked!": "Es wurde keine Eggs verknüpft",
+    "Software \/ Games": "Software \/ Spiele",
+    "Please select software ...": "Bitte Software auswählen",
+    "---": "---",
+    "Specification ": "Spezifikation",
+    "Node": "Node",
+    "Resource Data:": "Ressourcendaten:",
+    "vCores": "vCores",
+    "MB": "MB",
+    "MySQL": "MySQL",
+    "ports": "Ports",
+    "Not enough": "Nicht genug",
+    "Create server": "Server erstellen",
+    "Please select a node ...": "Bitte Node auswählen",
+    "No nodes found matching current configuration": "Kein Node passt zur momentanen Konfiguration",
+    "Please select a resource ...": "Wähle eine Ressource aus...",
+    "No resources found matching current configuration": "Keine Ressource passt zur momentanen Konfiguration",
+    "Please select a configuration ...": "Konfiguration Auswählen!",
+    "Not enough credits!": "Nicht genug Credits!",
+    "Create Server": "Server erstellen",
+    "Software": "Software",
+    "Specification": "Spezifikationen",
+    "Resource plan": "Ressourcenplan",
+    "RAM": "RAM",
+    "MySQL Databases": "MySQL Datenbank",
+    "per Hour": "pro Stunde",
+    "per Month": "pro Monat",
+    "Manage": "Verwalten",
+    "Are you sure?": "Sind Sie sicher?",
+    "This is an irreversible action, all files of this server will be removed.": "Dies kann nicht rückgängig gemacht werden. Alle Serverdaten werden gelöscht.",
+    "Yes, delete it!": "Ja, löschen!",
+    "No, cancel!": "Abbrechen",
+    "Canceled ...": "Abgebrochen...",
+    "Deletion has been canceled.": "Löschen abgebrochen.",
+    "Date": "Datum",
+    "Subtotal": "Zwischensumme",
+    "Amount Due": "Fälliger Betrag",
+    "Tax": "Steuer",
+    "Submit Payment": "Zahlung bestätigen",
+    "Purchase": "Kaufen",
+    "There are no store products!": "Es gibt keine Produkte",
+    "The store is not correctly configured!": "Der Laden wurde nicht richtig konfiguriert",
+    "Serial No.": "Rechnungsnr.",
+    "Invoice date": "Rechnungsdatum",
+    "Seller": "Verkäufer",
+    "Buyer": "Käufer",
+    "Address": "Adresse",
+    "VAT Code": "Steuernummer",
+    "Phone": "Telefon",
+    "Units": "Einheiten",
+    "Discount": "Rabatt",
+    "Total discount": "Gesamtrabatt",
+    "Taxable amount": "Steuerbetrag",
+    "Tax rate": "Steuerrate",
+    "Total taxes": "Steuerngesamt",
+    "Shipping": "Lieferbedingung",
+    "Total amount": "Gesamtbetrag",
+    "Notes": "Notizen",
+    "Amount in words": "Betrag in Worten",
+    "Please pay until": "Zahlbar bis",
+    "Key": "Schlüssel",
+    "Value": "Wert",
+    "Edit Configuration": "Konfiguration bearbeiten",
+    "Text Field": "Textfeld",
+    "Cancel": "Abbrechen",
+    "Save": "Speichern",
+    "Images and Icons may be cached, reload without cache to see your changes appear": "Bilder und Icons können zwischengespeichert werden, laden Sie sie ohne Cache neu, um zu sehen, wie Ihre Änderungen angezeigt werden (STRG + F5)",
+    "Enter your companys name": "Geben Sie Ihren Firmennamen ein",
+    "Enter your companys address": "Geben Sie die Adresse Ihrer Firma ein",
+    "Enter your companys phone number": "Geben Sie die Telefonnummer Ihrer Firma ein",
+    "Enter your companys VAT id": "Geben Sie die Umsatzsteuer-Identifikationsnummer Ihrer Firma ein",
+    "Enter your companys email address": "Geben Sie die E-Mail-Adresse Ihrer Firma ein",
+    "Enter your companys website": "Geben Sie die Website Ihrer Firma ein",
+    "Enter your custom invoice prefix": "Geben Sie Ihr benutzerdefiniertes Rechnungspräfix ein",
+    "The Language of the Datatables. Grab the Language-Codes from here": "Die Sprache der Datentabellen. Holen Sie sich die Sprach-codes von hier",
+    "Let the Client change the Language": "Lassen Sie den Nutzer die Sprache ändern",
+    "Icons updated!": "Icons aktualisiert!",
+    "cs": "Tschechisch",
+    "de": "Deutsch",
+    "en": "Englisch",
+    "es": "Spanisch",
+    "fr": "Französisch",
+    "hi": "Hindi",
+    "it": "Italienisch",
+    "nl": "Niederländisch",
+    "pl": "Polnisch",
+    "zh": "Chinesisch",
+    "tr": "Türkisch",
+    "ru": "Russisch"
+}

+ 18 - 0
resources/lang/de/auth.php

@@ -0,0 +1,18 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Authentication Language Lines
+|--------------------------------------------------------------------------
+|
+| The following language lines are used during authentication for various
+| messages that we need to display to the user. You are free to modify
+| these language lines according to your application's requirements.
+|
+*/
+
+return [
+    'failed'   => 'Diese Kombination aus Zugangsdaten wurde nicht in unserer Datenbank gefunden.',
+    'password' => 'Das eingegebene Passwort ist nicht korrekt.',
+    'throttle' => 'Zu viele Loginversuche. Versuchen Sie es bitte in :seconds Sekunden nochmal.',
+];

+ 17 - 0
resources/lang/de/pagination.php

@@ -0,0 +1,17 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Pagination Language Lines
+|--------------------------------------------------------------------------
+|
+| The following language lines are used by the paginator library to build
+| the simple pagination links. You are free to change them to anything
+| you want to customize your views to better match your application.
+|
+*/
+
+return [
+    'next'     => 'Weiter &raquo;',
+    'previous' => '&laquo; Zurück',
+];

+ 20 - 0
resources/lang/de/passwords.php

@@ -0,0 +1,20 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Password Reset Language Lines
+|--------------------------------------------------------------------------
+|
+| The following language lines are the default lines which match reasons
+| that are given by the password broker for a password update attempt
+| has failed, such as for an invalid token or invalid new password.
+|
+*/
+
+return [
+    'reset'     => 'Das Passwort wurde zurückgesetzt!',
+    'sent'      => 'Passworterinnerung wurde gesendet!',
+    'throttled' => 'Bitte warten Sie, bevor Sie es erneut versuchen.',
+    'token'     => 'Der Passwort-Wiederherstellungs-Schlüssel ist ungültig oder abgelaufen.',
+    'user'      => 'Es konnte leider kein Nutzer mit dieser E-Mail-Adresse gefunden werden.',
+];

+ 135 - 0
resources/lang/de/validation.php

@@ -0,0 +1,135 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Validation Language Lines
+|--------------------------------------------------------------------------
+|
+| The following language lines contain the default error messages used by
+| the validator class. Some of these rules have multiple versions such
+| as the size rules. Feel free to tweak each of these messages here.
+|
+*/
+
+return [
+    'accepted'             => ':attribute muss akzeptiert werden.',
+    'accepted_if'          => ':attribute muss akzeptiert werden, wenn :other :value ist.',
+    'active_url'           => ':attribute ist keine gültige Internet-Adresse.',
+    'after'                => ':attribute muss ein Datum nach :date sein.',
+    'after_or_equal'       => ':attribute muss ein Datum nach :date oder gleich :date sein.',
+    'alpha'                => ':attribute darf nur aus Buchstaben bestehen.',
+    'alpha_dash'           => ':attribute darf nur aus Buchstaben, Zahlen, Binde- und Unterstrichen bestehen.',
+    'alpha_num'            => ':attribute darf nur aus Buchstaben und Zahlen bestehen.',
+    'array'                => ':attribute muss ein Array sein.',
+    'attached'             => ':attribute ist bereits angehängt.',
+    'before'               => ':attribute muss ein Datum vor :date sein.',
+    'before_or_equal'      => ':attribute muss ein Datum vor :date oder gleich :date sein.',
+    'between'              => [
+        'array'   => ':attribute muss zwischen :min & :max Elemente haben.',
+        'file'    => ':attribute muss zwischen :min & :max Kilobytes groß sein.',
+        'numeric' => ':attribute muss zwischen :min & :max liegen.',
+        'string'  => ':attribute muss zwischen :min & :max Zeichen lang sein.',
+    ],
+    'boolean'              => ':attribute muss entweder \'true\' oder \'false\' sein.',
+    'confirmed'            => ':attribute stimmt nicht mit der Bestätigung überein.',
+    'current_password'     => 'Das Passwort ist falsch.',
+    'date'                 => ':attribute muss ein gültiges Datum sein.',
+    'date_equals'          => ':attribute muss ein Datum gleich :date sein.',
+    'date_format'          => ':attribute entspricht nicht dem gültigen Format für :format.',
+    'declined'             => 'The :attribute must be declined.',
+    'declined_if'          => 'The :attribute must be declined when :other is :value.',
+    'different'            => ':attribute und :other müssen sich unterscheiden.',
+    'digits'               => ':attribute muss :digits Stellen haben.',
+    'digits_between'       => ':attribute muss zwischen :min und :max Stellen haben.',
+    'dimensions'           => ':attribute hat ungültige Bildabmessungen.',
+    'distinct'             => ':attribute beinhaltet einen bereits vorhandenen Wert.',
+    'email'                => ':attribute muss eine gültige E-Mail-Adresse sein.',
+    'ends_with'            => ':attribute muss eine der folgenden Endungen aufweisen: :values',
+    'exists'               => 'Der gewählte Wert für :attribute ist ungültig.',
+    'file'                 => ':attribute muss eine Datei sein.',
+    'filled'               => ':attribute muss ausgefüllt sein.',
+    'gt'                   => [
+        'array'   => ':attribute muss mehr als :value Elemente haben.',
+        'file'    => ':attribute muss größer als :value Kilobytes sein.',
+        'numeric' => ':attribute muss größer als :value sein.',
+        'string'  => ':attribute muss länger als :value Zeichen sein.',
+    ],
+    'gte'                  => [
+        'array'   => ':attribute muss mindestens :value Elemente haben.',
+        'file'    => ':attribute muss größer oder gleich :value Kilobytes sein.',
+        'numeric' => ':attribute muss größer oder gleich :value sein.',
+        'string'  => ':attribute muss mindestens :value Zeichen lang sein.',
+    ],
+    'image'                => ':attribute muss ein Bild sein.',
+    'in'                   => 'Der gewählte Wert für :attribute ist ungültig.',
+    'in_array'             => 'Der gewählte Wert für :attribute kommt nicht in :other vor.',
+    'integer'              => ':attribute muss eine ganze Zahl sein.',
+    'ip'                   => ':attribute muss eine gültige IP-Adresse sein.',
+    'ipv4'                 => ':attribute muss eine gültige IPv4-Adresse sein.',
+    'ipv6'                 => ':attribute muss eine gültige IPv6-Adresse sein.',
+    'json'                 => ':attribute muss ein gültiger JSON-String sein.',
+    'lt'                   => [
+        'array'   => ':attribute muss weniger als :value Elemente haben.',
+        'file'    => ':attribute muss kleiner als :value Kilobytes sein.',
+        'numeric' => ':attribute muss kleiner als :value sein.',
+        'string'  => ':attribute muss kürzer als :value Zeichen sein.',
+    ],
+    'lte'                  => [
+        'array'   => ':attribute darf maximal :value Elemente haben.',
+        'file'    => ':attribute muss kleiner oder gleich :value Kilobytes sein.',
+        'numeric' => ':attribute muss kleiner oder gleich :value sein.',
+        'string'  => ':attribute darf maximal :value Zeichen lang sein.',
+    ],
+    'max'                  => [
+        'array'   => ':attribute darf maximal :max Elemente haben.',
+        'file'    => ':attribute darf maximal :max Kilobytes groß sein.',
+        'numeric' => ':attribute darf maximal :max sein.',
+        'string'  => ':attribute darf maximal :max Zeichen haben.',
+    ],
+    'mimes'                => ':attribute muss den Dateityp :values haben.',
+    'mimetypes'            => ':attribute muss den Dateityp :values haben.',
+    'min'                  => [
+        'array'   => ':attribute muss mindestens :min Elemente haben.',
+        'file'    => ':attribute muss mindestens :min Kilobytes groß sein.',
+        'numeric' => ':attribute muss mindestens :min sein.',
+        'string'  => ':attribute muss mindestens :min Zeichen lang sein.',
+    ],
+    'multiple_of'          => ':attribute muss ein Vielfaches von :value sein.',
+    'not_in'               => 'Der gewählte Wert für :attribute ist ungültig.',
+    'not_regex'            => ':attribute hat ein ungültiges Format.',
+    'numeric'              => ':attribute muss eine Zahl sein.',
+    'password'             => 'Das Passwort ist falsch.',
+    'present'              => ':attribute muss vorhanden sein.',
+    'prohibited'           => ':attribute ist unzulässig.',
+    'prohibited_if'        => ':attribute ist unzulässig, wenn :other :value ist.',
+    'prohibited_unless'    => ':attribute ist unzulässig, wenn :other nicht :values ist.',
+    'prohibits'            => ':attribute verbietet die Angabe von :other.',
+    'regex'                => ':attribute Format ist ungültig.',
+    'relatable'            => ':attribute kann nicht mit dieser Ressource verbunden werden.',
+    'required'             => ':attribute muss ausgefüllt werden.',
+    'required_if'          => ':attribute muss ausgefüllt werden, wenn :other den Wert :value hat.',
+    'required_unless'      => ':attribute muss ausgefüllt werden, wenn :other nicht den Wert :values hat.',
+    'required_with'        => ':attribute muss ausgefüllt werden, wenn :values ausgefüllt wurde.',
+    'required_with_all'    => ':attribute muss ausgefüllt werden, wenn :values ausgefüllt wurde.',
+    'required_without'     => ':attribute muss ausgefüllt werden, wenn :values nicht ausgefüllt wurde.',
+    'required_without_all' => ':attribute muss ausgefüllt werden, wenn keines der Felder :values ausgefüllt wurde.',
+    'same'                 => ':attribute und :other müssen übereinstimmen.',
+    'size'                 => [
+        'array'   => ':attribute muss genau :size Elemente haben.',
+        'file'    => ':attribute muss :size Kilobyte groß sein.',
+        'numeric' => ':attribute muss gleich :size sein.',
+        'string'  => ':attribute muss :size Zeichen lang sein.',
+    ],
+    'starts_with'          => ':attribute muss mit einem der folgenden Anfänge aufweisen: :values',
+    'string'               => ':attribute muss ein String sein.',
+    'timezone'             => ':attribute muss eine gültige Zeitzone sein.',
+    'unique'               => ':attribute ist bereits vergeben.',
+    'uploaded'             => ':attribute konnte nicht hochgeladen werden.',
+    'url'                  => ':attribute muss eine URL sein.',
+    'uuid'                 => ':attribute muss ein UUID sein.',
+    'custom'               => [
+        'attribute-name' => [
+            'rule-name' => 'custom-message',
+        ],
+    ],
+];

+ 447 - 0
resources/lang/en.json

@@ -0,0 +1,447 @@
+{
+    "Invoice settings updated!": "Invoice settings updated!",
+    "Language settings have not been updated!": "Language settings have not been updated!",
+    "Language settings updated!": "Language settings updated!",
+    "Misc settings have not been updated!": "Misc settings have not been updated!",
+    "Misc settings updated!": "Misc settings updated!",
+    "Payment settings have not been updated!": "Payment settings have not been updated!",
+    "Payment settings updated!": "Payment settings updated!",
+    "System settings have not been updated!": "System settings have not been updated!",
+    "System settings updated!": "System settings updated!",
+    "api key created!": "api key created!",
+    "api key updated!": "api key updated!",
+    "api key has been removed!": "api key has been removed!",
+    "Edit": "Edit",
+    "Delete": "Delete",
+    "Store item has been created!": "Store item has been created!",
+    "Store item has been updated!": "Store item has been updated!",
+    "Product has been updated!": "Product has been updated!",
+    "Store item has been removed!": "Store item has been removed!",
+    "Created at": "Created at",
+    "Error!": "Error!",
+    "unknown": "unknown",
+    "Pterodactyl synced": "Pterodactyl synced",
+    "Your credit balance has been increased!": "Your credit balance has been increased!",
+    "Your payment is being processed!": "Your payment is being processed!",
+    "Your payment has been canceled!": "Your payment has been canceled!",
+    "Payment method": "Payment method",
+    "Invoice": "Invoice",
+    "Download": "Download",
+    "Product has been created!": "Product has been created!",
+    "Product has been removed!": "Product has been removed!",
+    "Show": "Show",
+    "Clone": "Clone",
+    "Server removed": "Server removed",
+    "An exception has occurred while trying to remove a resource \"": "An exception has occurred while trying to remove a resource \"",
+    "Server has been updated!": "Server has been updated!",
+    "Unsuspend": "Unsuspend",
+    "Suspend": "Suspend",
+    "configuration has been updated!": "configuration has been updated!",
+    "link has been created!": "link has been created!",
+    "link has been updated!": "link has been updated!",
+    "product has been removed!": "product has been removed!",
+    "User does not exists on pterodactyl's panel": "User does not exists on pterodactyl's panel",
+    "user has been removed!": "user has been removed!",
+    "Notification sent!": "Notification sent!",
+    "User has been updated!": "User has been updated!",
+    "Login as User": "Login as User",
+    "voucher has been created!": "voucher has been created!",
+    "voucher has been updated!": "voucher has been updated!",
+    "voucher has been removed!": "voucher has been removed!",
+    "This voucher has reached the maximum amount of uses": "This voucher has reached the maximum amount of uses",
+    "This voucher has expired": "This voucher has expired",
+    "You already redeemed this voucher code": "You already redeemed this voucher code",
+    "have been added to your balance!": "have been added to your balance!",
+    "Users": "Users",
+    "VALID": "VALID",
+    "Account already exists on Pterodactyl. Please contact the Support!": "Account already exists on Pterodactyl. Please contact the Support!",
+    "days": "days",
+    "hours": "hours",
+    "You ran out of Credits": "You ran out of Credits",
+    "Profile updated": "Profile updated",
+    "Server limit reached!": "Server limit reached!",
+    "You are required to verify your email address before you can create a server.": "You are required to verify your email address before you can create a server.",
+    "You are required to link your discord account before you can create a server.": "You are required to link your discord account before you can create a server.",
+    "Server created": "Server created",
+    "No allocations satisfying the requirements for automatic deployment on this node were found.": "No allocations satisfying the requirements for automatic deployment on this node were found.",
+    "You are required to verify your email address before you can purchase credits.": "You are required to verify your email address before you can purchase credits.",
+    "You are required to link your discord account before you can purchase Credits": "You are required to link your discord account before you can purchase Credits",
+    "EXPIRED": "EXPIRED",
+    "Payment Confirmation": "Payment Confirmation",
+    "Payment Confirmed!": "Payment Confirmed!",
+    "Your Payment was successful!": "Your Payment was successful!",
+    "Hello": "Hello",
+    "Your payment was processed successfully!": "Your payment was processed successfully!",
+    "Status": "Status",
+    "Price": "Price",
+    "Type": "Type",
+    "Amount": "Amount",
+    "Balance": "Balance",
+    "User ID": "User ID",
+    "Server Creation Error": "Server Creation Error",
+    "Your servers have been suspended!": "Your servers have been suspended!",
+    "To automatically re-enable your server\/s, you need to purchase more credits.": "To automatically re-enable your server\/s, you need to purchase more credits.",
+    "Purchase credits": "Purchase credits",
+    "If you have any questions please let us know.": "If you have any questions please let us know.",
+    "Regards": "Regards",
+    "Getting started!": "Getting started!",
+    "Activity Logs": "Activity Logs",
+    "Dashboard": "Dashboard",
+    "No recent activity from cronjobs": "No recent activity from cronjobs",
+    "Are cronjobs running?": "Are cronjobs running?",
+    "Check the docs for it here": "Check the docs for it here",
+    "Causer": "Causer",
+    "Description": "Description",
+    "Application API": "Application API",
+    "Create": "Create",
+    "Memo": "Memo",
+    "Submit": "Submit",
+    "Create new": "Create new",
+    "Token": "Token",
+    "Last used": "Last used",
+    "Are you sure you wish to delete?": "Are you sure you wish to delete?",
+    "Nests": "Nests",
+    "Sync": "Sync",
+    "Active": "Active",
+    "ID": "ID",
+    "eggs": "eggs",
+    "Name": "Name",
+    "Nodes": "Nodes",
+    "Location": "Location",
+    "Admin Overview": "Admin Overview",
+    "Support server": "Support server",
+    "Documentation": "Documentation",
+    "Github": "Github",
+    "Support ControlPanel": "Support ControlPanel",
+    "Servers": "Servers",
+    "Total": "Total",
+    "Payments": "Payments",
+    "Pterodactyl": "Pterodactyl",
+    "Resources": "Resources",
+    "Count": "Count",
+    "Locations": "Locations",
+    "Eggs": "Eggs",
+    "Last updated :date": "Last updated :date",
+    "Download all Invoices": "Download all Invoices",
+    "Product Price": "Product Price",
+    "Tax Value": "Tax Value",
+    "Tax Percentage": "Tax Percentage",
+    "Total Price": "Total Price",
+    "Payment ID": "Payment ID",
+    "Payment Method": "Payment Method",
+    "Products": "Products",
+    "Product Details": "Product Details",
+    "Disabled": "Disabled",
+    "Will hide this option from being selected": "Will hide this option from being selected",
+    "Price in": "Price in",
+    "Memory": "Memory",
+    "Cpu": "Cpu",
+    "Swap": "Swap",
+    "This is what the users sees": "This is what the users sees",
+    "Disk": "Disk",
+    "Minimum": "Minimum",
+    "Setting to -1 will use the value from configuration.": "Setting to -1 will use the value from configuration.",
+    "IO": "IO",
+    "Databases": "Databases",
+    "Backups": "Backups",
+    "Allocations": "Allocations",
+    "Product Linking": "Product Linking",
+    "Link your products to nodes and eggs to create dynamic pricing for each option": "Link your products to nodes and eggs to create dynamic pricing for each option",
+    "This product will only be available for these nodes": "This product will only be available for these nodes",
+    "This product will only be available for these eggs": "This product will only be available for these eggs",
+    "Product": "Product",
+    "CPU": "CPU",
+    "Updated at": "Updated at",
+    "User": "User",
+    "Config": "Config",
+    "Suspended at": "Suspended at",
+    "Settings": "Settings",
+    "The installer is not locked!": "The installer is not locked!",
+    "please create a file called \"install.lock\" in your dashboard Root directory. Otherwise no settings will be loaded!": "please create a file called \"install.lock\" in your dashboard Root directory. Otherwise no settings will be loaded!",
+    "or click here": "or click here",
+    "Company Name": "Company Name",
+    "Company Adress": "Company Adress",
+    "Company Phonenumber": "Company Phonenumber",
+    "VAT ID": "VAT ID",
+    "Company E-Mail Adress": "Company E-Mail Adress",
+    "Company Website": "Company Website",
+    "Invoice Prefix": "Invoice Prefix",
+    "Enable Invoices": "Enable Invoices",
+    "Logo": "Logo",
+    "Select Invoice Logo": "Select Invoice Logo",
+    "Available languages": "Available languages",
+    "Default language": "Default language",
+    "The fallback Language, if something goes wrong": "The fallback Language, if something goes wrong",
+    "Datable language": "Datable language",
+    "The datatables lang-code. <br><strong>Example:<\/strong> en-gb, fr_fr, de_de<br>More Information: ": "The datatables lang-code. <br><strong>Example:<\/strong> en-gb, fr_fr, de_de<br>More Information: ",
+    "Auto-translate": "Auto-translate",
+    "If this is checked, the Dashboard will translate itself to the Clients language, if available": "If this is checked, the Dashboard will translate itself to the Clients language, if available",
+    "Client Language-Switch": "Client Language-Switch",
+    "If this is checked, Clients will have the ability to manually change their Dashboard language": "If this is checked, Clients will have the ability to manually change their Dashboard language",
+    "Mail Service": "Mail Service",
+    "The Mailer to send e-mails with": "The Mailer to send e-mails with",
+    "Mail Host": "Mail Host",
+    "Mail Port": "Mail Port",
+    "Mail Username": "Mail Username",
+    "Mail Password": "Mail Password",
+    "Mail Encryption": "Mail Encryption",
+    "Mail From Adress": "Mail From Adress",
+    "Mail From Name": "Mail From Name",
+    "Discord Client-ID": "Discord Client-ID",
+    "Discord Client-Secret": "Discord Client-Secret",
+    "Discord Bot-Token": "Discord Bot-Token",
+    "Discord Guild-ID": "Discord Guild-ID",
+    "Discord Invite-URL": "Discord Invite-URL",
+    "Discord Role-ID": "Discord Role-ID",
+    "Enable ReCaptcha": "Enable ReCaptcha",
+    "ReCaptcha Site-Key": "ReCaptcha Site-Key",
+    "ReCaptcha Secret-Key": "ReCaptcha Secret-Key",
+    "PayPal Client-ID": "PayPal Client-ID",
+    "PayPal Secret-Key": "PayPal Secret-Key",
+    "PayPal Sandbox Client-ID": "PayPal Sandbox Client-ID",
+    "optional": "optional",
+    "PayPal Sandbox Secret-Key": "PayPal Sandbox Secret-Key",
+    "Stripe Secret-Key": "Stripe Secret-Key",
+    "Stripe Endpoint-Secret-Key": "Stripe Endpoint-Secret-Key",
+    "Stripe Test Secret-Key": "Stripe Test Secret-Key",
+    "Stripe Test Endpoint-Secret-Key": "Stripe Test Endpoint-Secret-Key",
+    "Payment Methods": "Payment Methods",
+    "Tax Value in %": "Tax Value in %",
+    "System": "System",
+    "Register IP Check": "Register IP Check",
+    "Prevent users from making multiple accounts using the same IP address.": "Prevent users from making multiple accounts using the same IP address.",
+    "Charge first hour at creation": "Charge first hour at creation",
+    "Charges the first hour worth of credits upon creating a server.": "Charges the first hour worth of credits upon creating a server.",
+    "Credits Display Name": "Credits Display Name",
+    "PHPMyAdmin URL": "PHPMyAdmin URL",
+    "Enter the URL to your PHPMyAdmin installation. <strong>Without a trailing slash!<\/strong>": "Enter the URL to your PHPMyAdmin installation. <strong>Without a trailing slash!<\/strong>",
+    "Pterodactyl URL": "Pterodactyl URL",
+    "Enter the URL to your Pterodactyl installation. <strong>Without a trailing slash!<\/strong>": "Enter the URL to your Pterodactyl installation. <strong>Without a trailing slash!<\/strong>",
+    "Pterodactyl API Key": "Pterodactyl API Key",
+    "Enter the API Key to your Pterodactyl installation.": "Enter the API Key to your Pterodactyl installation.",
+    "Force Discord verification": "Force Discord verification",
+    "Force E-Mail verification": "Force E-Mail verification",
+    "Initial Credits": "Initial Credits",
+    "Initial Server Limit": "Initial Server Limit",
+    "Credits Reward Amount - Discord": "Credits Reward Amount - Discord",
+    "Credits Reward Amount - E-Mail": "Credits Reward Amount - E-Mail",
+    "Server Limit Increase - Discord": "Server Limit Increase - Discord",
+    "Server Limit Increase - E-Mail": "Server Limit Increase - E-Mail",
+    "Server": "Server",
+    "Server Allocation Limit": "Server Allocation Limit",
+    "The maximum amount of allocations to pull per node for automatic deployment, if more allocations are being used than this limit is set to, no new servers can be created!": "The maximum amount of allocations to pull per node for automatic deployment, if more allocations are being used than this limit is set to, no new servers can be created!",
+    "Select panel icon": "Select panel icon",
+    "Select panel favicon": "Select panel favicon",
+    "Store": "Store",
+    "Currency code": "Currency code",
+    "Checkout the paypal docs to select the appropriate code": "Checkout the paypal docs to select the appropriate code",
+    "Quantity": "Quantity",
+    "Amount given to the user after purchasing": "Amount given to the user after purchasing",
+    "Display": "Display",
+    "This is what the user sees at store and checkout": "This is what the user sees at store and checkout",
+    "Adds 1000 credits to your account": "Adds 1000 credits to your account",
+    "This is what the user sees at checkout": "This is what the user sees at checkout",
+    "No payment method is configured.": "No payment method is configured.",
+    "To configure the payment methods, head to the settings-page and add the required options for your prefered payment method.": "To configure the payment methods, head to the settings-page and add the required options for your prefered payment method.",
+    "Useful Links": "Useful Links",
+    "Icon class name": "Icon class name",
+    "You can find available free icons": "You can find available free icons",
+    "Title": "Title",
+    "Link": "Link",
+    "description": "description",
+    "Icon": "Icon",
+    "Username": "Username",
+    "Email": "Email",
+    "Pterodactyl ID": "Pterodactyl ID",
+    "This ID refers to the user account created on pterodactyls panel.": "This ID refers to the user account created on pterodactyls panel.",
+    "Only edit this if you know what youre doing :)": "Only edit this if you know what youre doing :)",
+    "Server Limit": "Server Limit",
+    "Role": "Role",
+    " Administrator": " Administrator",
+    "Client": "Client",
+    "Member": "Member",
+    "New Password": "New Password",
+    "Confirm Password": "Confirm Password",
+    "Notify": "Notify",
+    "Avatar": "Avatar",
+    "Verified": "Verified",
+    "Last seen": "Last seen",
+    "Notifications": "Notifications",
+    "All": "All",
+    "Send via": "Send via",
+    "Database": "Database",
+    "Content": "Content",
+    "Server limit": "Server limit",
+    "Discord": "Discord",
+    "Usage": "Usage",
+    "IP": "IP",
+    "Vouchers": "Vouchers",
+    "Voucher details": "Voucher details",
+    "Summer break voucher": "Summer break voucher",
+    "Code": "Code",
+    "Random": "Random",
+    "Uses": "Uses",
+    "A voucher can only be used one time per user. Uses specifies the number of different users that can use this voucher.": "A voucher can only be used one time per user. Uses specifies the number of different users that can use this voucher.",
+    "Max": "Max",
+    "Expires at": "Expires at",
+    "Used \/ Uses": "Used \/ Uses",
+    "Expires": "Expires",
+    "Sign in to start your session": "Sign in to start your session",
+    "Password": "Password",
+    "Remember Me": "Remember Me",
+    "Sign In": "Sign In",
+    "Forgot Your Password?": "Forgot Your Password?",
+    "Register a new membership": "Register a new membership",
+    "Please confirm your password before continuing.": "Please confirm your password before continuing.",
+    "You forgot your password? Here you can easily retrieve a new password.": "You forgot your password? Here you can easily retrieve a new password.",
+    "Request new password": "Request new password",
+    "Login": "Login",
+    "You are only one step a way from your new password, recover your password now.": "You are only one step a way from your new password, recover your password now.",
+    "Retype password": "Retype password",
+    "Change password": "Change password",
+    "Register": "Register",
+    "I already have a membership": "I already have a membership",
+    "Verify Your Email Address": "Verify Your Email Address",
+    "A fresh verification link has been sent to your email address.": "A fresh verification link has been sent to your email address.",
+    "Before proceeding, please check your email for a verification link.": "Before proceeding, please check your email for a verification link.",
+    "If you did not receive the email": "If you did not receive the email",
+    "click here to request another": "click here to request another",
+    "per month": "per month",
+    "Out of Credits in": "Out of Credits in",
+    "Home": "Home",
+    "Language": "Language",
+    "See all Notifications": "See all Notifications",
+    "Redeem code": "Redeem code",
+    "Profile": "Profile",
+    "Log back in": "Log back in",
+    "Logout": "Logout",
+    "Administration": "Administration",
+    "Overview": "Overview",
+    "Management": "Management",
+    "Other": "Other",
+    "Logs": "Logs",
+    "Warning!": "Warning!",
+    "You have not yet verified your email address": "You have not yet verified your email address",
+    "Click here to resend verification email": "Click here to resend verification email",
+    "Please contact support If you didnt receive your verification email.": "Please contact support If you didnt receive your verification email.",
+    "Thank you for your purchase!": "Thank you for your purchase!",
+    "Your payment has been confirmed; Your credit balance has been updated.": "Your payment has been confirmed; Your credit balance has been updated.",
+    "Thanks": "Thanks",
+    "Redeem voucher code": "Redeem voucher code",
+    "Close": "Close",
+    "Redeem": "Redeem",
+    "All notifications": "All notifications",
+    "Required Email verification!": "Required Email verification!",
+    "Required Discord verification!": "Required Discord verification!",
+    "You have not yet verified your discord account": "You have not yet verified your discord account",
+    "Login with discord": "Login with discord",
+    "Please contact support If you face any issues.": "Please contact support If you face any issues.",
+    "Due to system settings you are required to verify your discord account!": "Due to system settings you are required to verify your discord account!",
+    "It looks like this hasnt been set-up correctly! Please contact support.": "It looks like this hasnt been set-up correctly! Please contact support.",
+    "Change Password": "Change Password",
+    "Current Password": "Current Password",
+    "Link your discord account!": "Link your discord account!",
+    "By verifying your discord account, you receive extra Credits and increased Server amounts": "By verifying your discord account, you receive extra Credits and increased Server amounts",
+    "Login with Discord": "Login with Discord",
+    "You are verified!": "You are verified!",
+    "Re-Sync Discord": "Re-Sync Discord",
+    "Save Changes": "Save Changes",
+    "Server configuration": "Server configuration",
+    "Make sure to link your products to nodes and eggs.": "Make sure to link your products to nodes and eggs.",
+    "There has to be at least 1 valid product for server creation": "There has to be at least 1 valid product for server creation",
+    "Sync now": "Sync now",
+    "No products available!": "No products available!",
+    "No nodes have been linked!": "No nodes have been linked!",
+    "No nests available!": "No nests available!",
+    "No eggs have been linked!": "No eggs have been linked!",
+    "Software \/ Games": "Software \/ Games",
+    "Please select software ...": "Please select software ...",
+    "---": "---",
+    "Specification ": "Specification ",
+    "Node": "Node",
+    "Resource Data:": "Resource Data:",
+    "vCores": "vCores",
+    "MB": "MB",
+    "MySQL": "MySQL",
+    "ports": "ports",
+    "Not enough": "Not enough",
+    "Create server": "Create server",
+    "Please select a node ...": "Please select a node ...",
+    "No nodes found matching current configuration": "No nodes found matching current configuration",
+    "Please select a resource ...": "Please select a resource ...",
+    "No resources found matching current configuration": "No resources found matching current configuration",
+    "Please select a configuration ...": "Please select a configuration ...",
+    "Not enough credits!": "Not enough credits!",
+    "Create Server": "Create Server",
+    "Software": "Software",
+    "Specification": "Specification",
+    "Resource plan": "Resource plan",
+    "RAM": "RAM",
+    "MySQL Databases": "MySQL Databases",
+    "per Hour": "per Hour",
+    "per Month": "per Month",
+    "Manage": "Manage",
+    "Are you sure?": "Are you sure?",
+    "This is an irreversible action, all files of this server will be removed.": "This is an irreversible action, all files of this server will be removed.",
+    "Yes, delete it!": "Yes, delete it!",
+    "No, cancel!": "No, cancel!",
+    "Canceled ...": "Canceled ...",
+    "Deletion has been canceled.": "Deletion has been canceled.",
+    "Date": "Date",
+    "Subtotal": "Subtotal",
+    "Amount Due": "Amount Due",
+    "Tax": "Tax",
+    "Submit Payment": "Submit Payment",
+    "Purchase": "Purchase",
+    "There are no store products!": "There are no store products!",
+    "The store is not correctly configured!": "The store is not correctly configured!",
+    "Serial No.": "Serial No.",
+    "Invoice date": "Invoice date",
+    "Seller": "Seller",
+    "Buyer": "Buyer",
+    "Address": "Address",
+    "VAT Code": "VAT Code",
+    "Phone": "Phone",
+    "Units": "Units",
+    "Discount": "Discount",
+    "Total discount": "Total discount",
+    "Taxable amount": "Taxable amount",
+    "Tax rate": "Tax rate",
+    "Total taxes": "Total taxes",
+    "Shipping": "Shipping",
+    "Total amount": "Total amount",
+    "Notes": "Notes",
+    "Amount in words": "Amount in words",
+    "Please pay until": "Please pay until",
+    "Key": "Key",
+    "Value": "Value",
+    "Edit Configuration": "Edit Configuration",
+    "Text Field": "Text Field",
+    "Cancel": "Cancel",
+    "Save": "Save",
+    "Select panel icon": "Select panel icon",
+    "Select panel favicon": "Select panel favicon",
+    "Images and Icons may be cached, reload without cache to see your changes appear": "Images and Icons may be cached, reload without cache to see your changes appear",
+    "Enter your companys name": "Enter your companys name",
+    "Enter your companys address": "Enter your companys address",
+    "Enter your companys phone number": "Enter your companys phone number",
+    "Enter your companys VAT id": "Enter your companys VAT id",
+    "Enter your companys email address": "Enter your companys email address",
+    "Enter your companys website": "Enter your companys website",
+    "Enter your custom invoice prefix": "Enter your custom invoice prefix",
+    "The Language of the Datatables. Grab the Language-Codes from here": "The Language of the Datatables. Grab the Language-Codes from here",
+    "Let the Client change the Language": "Let the Client change the Language",
+    "Icons updated!": "Icons updated!",
+    "cs": "Czech",
+    "de": "German",
+    "en": "English",
+    "es": "Spanish",
+    "fr": "French",
+    "hi": "Hindi",
+    "it": "Italian",
+    "nl": "Dutch",
+    "pl": "Polish",
+    "zh": "Chinese",
+    "tr": "Turkish",
+    "ru": "Russian"
+}

+ 445 - 0
resources/lang/es.json

@@ -0,0 +1,445 @@
+{
+    "Invoice settings updated!": "¡Configuración de factura actualizada!",
+    "Language settings have not been updated!": "¡La configuración de idioma no se ha actualizado!",
+    "Language settings updated!": "¡Configuración de idioma actualizada!",
+    "Misc settings have not been updated!": "¡La configuración miscelánea no se ha actualizado!",
+    "Misc settings updated!": "¡Configuraciones misceláneas actualizadas!",
+    "Payment settings have not been updated!": "¡La configuración de pago no se ha actualizado!",
+    "Payment settings updated!": "¡Configuración de pago actualizada!",
+    "System settings have not been updated!": "¡La configuración del sistema no se ha actualizado!",
+    "System settings updated!": "¡Configuración del sistema actualizada!",
+    "api key created!": "¡API Key creada!",
+    "api key updated!": "¡API Key actualizada!",
+    "api key has been removed!": "¡La API Key a sido eliminada!",
+    "Edit": "Editar",
+    "Delete": "Eliminar",
+    "Store item has been created!": "¡Se ha creado el artículo en la tienda!",
+    "Store item has been updated!": "¡El artículo de la tienda ha sido actualizado!",
+    "Product has been updated!": "¡El producto ha sido actualizado!",
+    "Store item has been removed!": "¡El artículo de la tienda ha sido eliminado!",
+    "Created at": "Creado a",
+    "Error!": "Error!",
+    "unknown": "desconocido",
+    "Pterodactyl synced": "Pterodactyl sincronizado",
+    "Your credit balance has been increased!": "¡Su saldo de crédito ha aumentado!",
+    "Your payment is being processed!": "¡Tu pago está siendo procesado!",
+    "Your payment has been canceled!": "¡Tu pago ha sido cancelado!",
+    "Payment method": "Método de pago",
+    "Invoice": "Factura",
+    "Download": "Descargar",
+    "Product has been created!": "¡El producto ha sido creado!",
+    "Product has been removed!": "¡El producto ha sido eliminado!",
+    "Show": "Mostrar",
+    "Clone": "Clonar",
+    "Server removed": "Servidor eliminado",
+    "An exception has occurred while trying to remove a resource \"": "Se produjo una excepción al intentar eliminar un recurso \"",
+    "Server has been updated!": "¡El servidor ha sido actualizado!",
+    "Unsuspend": "Quitar suspensión",
+    "Suspend": "Suspender",
+    "configuration has been updated!": "¡La configuración ha sido actualizada!",
+    "link has been created!": "¡Se ha creado el enlace!",
+    "link has been updated!": "¡El enlace ha sido actualizado!",
+    "product has been removed!": "¡El producto ha sido eliminado!",
+    "User does not exists on pterodactyl's panel": "El usuario no existe en el panel pterodactyl",
+    "user has been removed!": "¡El usuario ha sido eliminado!",
+    "Notification sent!": "¡Notificación enviada!",
+    "User has been updated!": "¡El usuario ha sido actualizado!",
+    "Login as User": "Iniciar sesión como usuario",
+    "voucher has been created!": "¡Se a creado un cupón!",
+    "voucher has been updated!": "¡El cupón ha sido actualizado!",
+    "voucher has been removed!": "¡El cupón a sido eliminado!",
+    "This voucher has reached the maximum amount of uses": "Este cupón ha alcanzado la cantidad máxima de usos",
+    "This voucher has expired": "Este cupón a expirado",
+    "You already redeemed this voucher code": "Ya has usado este cupón",
+    "have been added to your balance!": "se han añadido a tu saldo!",
+    "Users": "Usuarios",
+    "VALID": "VÁLIDO",
+    "Account already exists on Pterodactyl. Please contact the Support!": "La cuenta ya existe en Pterodactyl. ¡Póngase en contacto con el soporte!",
+    "days": "días",
+    "hours": "horas",
+    "You ran out of Credits": "Te has quedado sin créditos",
+    "Profile updated": "Perfil actualizado",
+    "Server limit reached!": "¡Se alcanzó el límite de servidores!",
+    "You are required to verify your email address before you can create a server.": "Debe verificar su dirección de correo electrónico antes de poder crear un servidor.",
+    "You are required to link your discord account before you can create a server.": "Debe vincular su cuenta de discord antes de poder crear un servidor.",
+    "Server created": "Servidor creado",
+    "No allocations satisfying the requirements for automatic deployment on this node were found.": "No se encontraron asignaciones que satisfagan los requisitos para la implementación automática en este nodo.",
+    "You are required to verify your email address before you can purchase credits.": "Debes de verificar tu dirección de correo electrónico antes de poder comprar créditos.",
+    "You are required to link your discord account before you can purchase Credits": "Debe vincular su cuenta de discord antes de poder comprar Créditos",
+    "EXPIRED": "CADUCADO",
+    "Payment Confirmation": "Confirmación de Pago",
+    "Payment Confirmed!": "¡Pago Confirmado!",
+    "Your Payment was successful!": "¡El pago se ha realizado correctamente!",
+    "Hello": "Hola",
+    "Your payment was processed successfully!": "¡Su pago se procesó correctamente!",
+    "Status": "Estado",
+    "Price": "Precio",
+    "Type": "Tipo",
+    "Amount": "Cantidad",
+    "Balance": "Saldo",
+    "User ID": "ID Usuario",
+    "Server Creation Error": "Error de creación del servidor",
+    "Your servers have been suspended!": "¡Sus servidores han sido suspendidos!",
+    "To automatically re-enable your server\/s, you need to purchase more credits.": "Para volver a habilitar automáticamente sus servidores, debe comprar más créditos.",
+    "Purchase credits": "Comprar Créditos",
+    "If you have any questions please let us know.": "Si tienes más preguntas, por favor háznoslas saber.",
+    "Regards": "Atentamente",
+    "Getting started!": "¡Empezando!",
+    "Activity Logs": "Registros de Actividad",
+    "Dashboard": "Panel de control",
+    "No recent activity from cronjobs": "No hay actividad reciente de cronjobs",
+    "Are cronjobs running?": "¿Se están ejecutando los cronjobs?",
+    "Check the docs for it here": "Consulte la documentación aquí",
+    "Causer": "Causantes",
+    "Description": "Descripción",
+    "Application API": "Aplicación API",
+    "Create": "Crear",
+    "Memo": "Memo",
+    "Submit": "Enviar",
+    "Create new": "Crear nuevo",
+    "Token": "Token",
+    "Last used": "Último Uso",
+    "Are you sure you wish to delete?": "¿Está seguro que desea borrarlo?",
+    "Nests": "Nidos",
+    "Sync": "Sincronizar Ahora",
+    "Active": "Activo",
+    "ID": "ID",
+    "eggs": "huevos",
+    "Name": "Nombre",
+    "Nodes": "Nodos",
+    "Location": "Ubicación",
+    "Admin Overview": "Vista de Administrador",
+    "Support server": "Servidor de Ayuda",
+    "Documentation": "Documentación",
+    "Github": "Github",
+    "Support ControlPanel": "Apoya ControlPanel",
+    "Servers": "Servidores",
+    "Total": "Total",
+    "Payments": "Pagos",
+    "Pterodactyl": "Pterodactyl",
+    "Resources": "Recursos",
+    "Count": "Contador",
+    "Locations": "Localizaciones",
+    "Eggs": "Huevos",
+    "Last updated :date": "Ultima actualización :date",
+    "Download all Invoices": "Descargar todas las facturas",
+    "Product Price": "Precio del producto",
+    "Tax Value": "Valor Impuestos",
+    "Tax Percentage": "Porcentaje Impuestos",
+    "Total Price": "Precio Total",
+    "Payment ID": "ID del pago",
+    "Payment Method": "Método de Pago",
+    "Products": "Productos",
+    "Product Details": "Detalles del Producto",
+    "Disabled": "Deshabilitado",
+    "Will hide this option from being selected": "Ocultará esta opción para que no se seleccione",
+    "Price in": "Precio en",
+    "Memory": "Ram",
+    "Cpu": "Cpu",
+    "Swap": "Swap",
+    "This is what the users sees": "Esto es lo que ven los usuarios",
+    "Disk": "Disco",
+    "Minimum": "Mínimo",
+    "Setting to -1 will use the value from configuration.": "Si se establece en -1, se utilizará el valor de la configuración.",
+    "IO": "IO",
+    "Databases": "Bases de Datos",
+    "Backups": "Copias de Seguridad",
+    "Allocations": "Asignaciones",
+    "Product Linking": "Vinculación de Productos",
+    "Link your products to nodes and eggs to create dynamic pricing for each option": "Vincula tus productos a nodos y huevos para crear precios dinámicos para cada opción",
+    "This product will only be available for these nodes": "Este producto solo está disponible para estos nodos",
+    "This product will only be available for these eggs": "Este producto solo esta disponible para estos huevos",
+    "Product": "Producto",
+    "CPU": "CPU",
+    "Updated at": "Última Actualización",
+    "User": "Usuario",
+    "Config": "Configuración",
+    "Suspended at": "Suspendido en",
+    "Settings": "Configuraciones",
+    "The installer is not locked!": "¡El instalador no está bloqueado!",
+    "please create a file called \"install.lock\" in your dashboard Root directory. Otherwise no settings will be loaded!": "cree un archivo llamado \"install.lock\" en el directorio Raíz de su tablero. ¡De lo contrario, no se cargará ninguna configuración!",
+    "or click here": "o haga clic aquí",
+    "Company Name": "Nombre Empresa",
+    "Company Adress": "Dirección de la Empresa",
+    "Company Phonenumber": "Número de teléfono de la empresa",
+    "VAT ID": "ID de IVA",
+    "Company E-Mail Adress": "Dirección de correo electrónico de la empresa",
+    "Company Website": "Página Web de la empresa",
+    "Invoice Prefix": "Prefijo de factura",
+    "Enable Invoices": "Habilitar facturas",
+    "Logo": "Logo",
+    "Select Invoice Logo": "Seleccione el logotipo de la factura",
+    "Available languages": "Idiomas disponibles",
+    "Default language": "Idioma predeterminado",
+    "The fallback Language, if something goes wrong": "El lenguaje alternativo, si algo sale mal",
+    "Datable language": "Lenguaje de tabla de datos",
+    "The datatables lang-code. <br><strong>Example:<\/strong> en-gb, fr_fr, de_de<br>More Information: ": "El código de idioma de las tablas de datos. <br><strong>Ejemplo:<\/strong> en-gb, fr_fr, de_de<br>Más información: ",
+    "Auto-translate": "Traducir automáticamente",
+    "If this is checked, the Dashboard will translate itself to the Clients language, if available": "Si está marcado, el Tablero se traducirá solo al idioma del Cliente, si está disponible",
+    "Client Language-Switch": "Cambio de idioma del cliente",
+    "If this is checked, Clients will have the ability to manually change their Dashboard language": "Si esto está marcado, los Clientes tendrán la capacidad de cambiar manualmente el idioma de su Panel de Control",
+    "Mail Service": "Servicio de correo",
+    "The Mailer to send e-mails with": "El Mailer para enviar correos electrónicos con",
+    "Mail Host": "Host del correo",
+    "Mail Port": "Puerto del correo",
+    "Mail Username": "Nombre de usuario del correo",
+    "Mail Password": "Contraseña de correo",
+    "Mail Encryption": "Cifrado de correo",
+    "Mail From Adress": "Dirección del correo",
+    "Mail From Name": "Nombre del correo",
+    "Discord Client-ID": "Discord ID-Cliente",
+    "Discord Client-Secret": "Discord Secreto-Cliente",
+    "Discord Bot-Token": "Discord Bot-Token",
+    "Discord Guild-ID": "Identificación de Guild de Discord",
+    "Discord Invite-URL": "URL de invitación del servidor de Discord",
+    "Discord Role-ID": "ID del Rol de Discord",
+    "Enable ReCaptcha": "Habilitar ReCaptcha",
+    "ReCaptcha Site-Key": "Clave del sitio de ReCaptcha",
+    "ReCaptcha Secret-Key": "Clave secreta de ReCaptcha",
+    "PayPal Client-ID": "PayPal Cliente-ID",
+    "PayPal Secret-Key": "PayPal Clave-Secreta",
+    "PayPal Sandbox Client-ID": "PayPal Sandbox Cliente-ID",
+    "optional": "opcional",
+    "PayPal Sandbox Secret-Key": "PayPal Sandbox Clave-Secreta",
+    "Stripe Secret-Key": "Stripe Clave-Secreta",
+    "Stripe Endpoint-Secret-Key": "Stripe Extremo-Clave-Secreta",
+    "Stripe Test Secret-Key": "Stripe Test Clave-Secreta",
+    "Stripe Test Endpoint-Secret-Key": "Stripe Test Extremo-Clave-Secreta",
+    "Payment Methods": "Métodos de Pago",
+    "Tax Value in %": "Valor Impuestos en %",
+    "System": "Sistema",
+    "Register IP Check": "Registrar comprobación de IP",
+    "Prevent users from making multiple accounts using the same IP address.": "Evite que los usuarios creen varias cuentas con la misma dirección IP.",
+    "Charge first hour at creation": "Carga la primera hora en la creación",
+    "Charges the first hour worth of credits upon creating a server.": "Carga la primera hora de créditos al crear un servidor.",
+    "Credits Display Name": "Nombre de los Créditos para mostrar",
+    "PHPMyAdmin URL": "PHPMyAdmin URL",
+    "Enter the URL to your PHPMyAdmin installation. <strong>Without a trailing slash!<\/strong>": "Ingrese la URL de su instalación de PHPMyAdmin. <strong>¡Sin una barra diagonal final!<\/strong>",
+    "Pterodactyl URL": "Pterodactyl URL",
+    "Enter the URL to your Pterodactyl installation. <strong>Without a trailing slash!<\/strong>": "Introduzca la URL de su instalación de Pterodactyl. <strong>¡Sin una barra diagonal final!<\/strong>",
+    "Pterodactyl API Key": "Pterodactyl API Key",
+    "Enter the API Key to your Pterodactyl installation.": "Ingrese la API Key para su instalación de Pterodactyl.",
+    "Force Discord verification": "Forzar verificación de Discord",
+    "Force E-Mail verification": "Forzar verificación de E-Mail",
+    "Initial Credits": "Créditos Iniciales",
+    "Initial Server Limit": "Límite inicial de servidor",
+    "Credits Reward Amount - Discord": "Cantidad de recompensa de créditos - Discord",
+    "Credits Reward Amount - E-Mail": "Cantidad de recompensa de créditos: E-Mail",
+    "Server Limit Increase - Discord": "Aumento del límite de servidor - Discord",
+    "Server Limit Increase - E-Mail": "Aumento del límite de servidor: E-Mail",
+    "Server": "Servidor",
+    "Server Allocation Limit": "Límite de asignación del servidor",
+    "The maximum amount of allocations to pull per node for automatic deployment, if more allocations are being used than this limit is set to, no new servers can be created!": "La cantidad máxima de asignaciones para extraer por nodo para la implementación automática, si se utilizan más asignaciones que las establecidas en este límite, ¡no se pueden crear nuevos servidores!",
+    "Select panel icon": "Seleccionar icono de panel",
+    "Select panel favicon": "Seleccionar favicon del panel",
+    "Store": "Tienda",
+    "Currency code": "Código de divisa\/moneda",
+    "Checkout the paypal docs to select the appropriate code": "Consulte los documentos de PayPal para seleccionar el código apropiado",
+    "Quantity": "Cantidad",
+    "Amount given to the user after purchasing": "Importe dado al usuario después de la compra",
+    "Display": "Mostrar",
+    "This is what the user sees at store and checkout": "Esto es lo que ve el usuario en la tienda y al finalizar la compra",
+    "Adds 1000 credits to your account": "Agrega 1000 créditos a su cuenta",
+    "This is what the user sees at checkout": "Esto es lo que ve el usuario al finalizar la compra",
+    "No payment method is configured.": "No hay ningún método de pago configurado.",
+    "To configure the payment methods, head to the settings-page and add the required options for your prefered payment method.": "To configure the payment methods, head to the settings-page and add the required options for your prefered payment method.",
+    "Useful Links": "Enlaces útiles",
+    "Icon class name": "Nombre de la clase de icono",
+    "You can find available free icons": "Puedes encontrar iconos gratuitos disponibles",
+    "Title": "Titulo",
+    "Link": "Enlace",
+    "description": "descripción",
+    "Icon": "Icono",
+    "Username": "Nombre de usuario",
+    "Email": "Email",
+    "Pterodactyl ID": "Pterodactyl ID",
+    "This ID refers to the user account created on pterodactyls panel.": "Esta ID se refiere a la cuenta de usuario creada en el panel de pterodactyl.",
+    "Only edit this if you know what youre doing :)": "Edite esto solo si sabe lo que está haciendo :)",
+    "Server Limit": "Limite Servidor",
+    "Role": "Rol",
+    " Administrator": " Administrador",
+    "Client": "Cliente",
+    "Member": "Miembro",
+    "New Password": "Nueva Contraseña",
+    "Confirm Password": "Confirmar Contraseña",
+    "Notify": "Notificar",
+    "Avatar": "Avatar",
+    "Verified": "Verificado",
+    "Last seen": "Visto por ùltima vez",
+    "Notifications": "Notificaciones",
+    "All": "Todos",
+    "Send via": "Enviar vía",
+    "Database": "Base de Datos",
+    "Content": "Contenido",
+    "Server limit": "Limite Servidores",
+    "Discord": "Discord",
+    "Usage": "Uso",
+    "IP": "IP",
+    "Vouchers": "Descuentos",
+    "Voucher details": "Detalles del vale",
+    "Summer break voucher": "Descuento de vacaciones de verano",
+    "Code": "Código",
+    "Random": "Aleatorio",
+    "Uses": "Usos",
+    "A voucher can only be used one time per user. Uses specifies the number of different users that can use this voucher.": "El descuento solo se puede utilizar una vez por usuario. Los usos especifica el número de usuarios diferentes que pueden utilizar este cupón.",
+    "Max": "Máx",
+    "Expires at": "Expira el",
+    "Used \/ Uses": "Uso \/ Usos",
+    "Expires": "Expira",
+    "Sign in to start your session": "Iniciar sesión para comenzar",
+    "Password": "Contraseña",
+    "Remember Me": "Recuérdame",
+    "Sign In": "Iniciar sesión",
+    "Forgot Your Password?": "¿Olvidó su contraseña?",
+    "Register a new membership": "Registrar un nuevo miembro",
+    "Please confirm your password before continuing.": "Por favor confirme su contraseña antes de continuar.",
+    "You forgot your password? Here you can easily retrieve a new password.": "¿Olvidaste tu contraseña? Aquí puede recuperar fácilmente una nueva contraseña.",
+    "Request new password": "Solicitar nueva contraseña",
+    "Login": "Iniciar sesión",
+    "You are only one step a way from your new password, recover your password now.": "Está a solo un paso de su nueva contraseña, recupere su contraseña ahora.",
+    "Retype password": "Vuelva a escribir la contraseña",
+    "Change password": "Cambiar contraseña",
+    "Register": "Registrar",
+    "I already have a membership": "Ya soy miembro",
+    "Verify Your Email Address": "Verifica Tu Email",
+    "A fresh verification link has been sent to your email address.": "Se ha enviado un nuevo enlace de verificación a su correo electrónico.",
+    "Before proceeding, please check your email for a verification link.": "Antes de continuar, por favor, confirme su correo electrónico con el enlace de verificación que le fue enviado.",
+    "If you did not receive the email": "Si no ha recibido el correo electrónico",
+    "click here to request another": "haga clic aquí para solicitar otro",
+    "per month": "al mes",
+    "Out of Credits in": "Sin créditos en",
+    "Home": "Inicio",
+    "Language": "Idioma",
+    "See all Notifications": "Ver todas las notificaciones",
+    "Redeem code": "Canjear código",
+    "Profile": "Perfil",
+    "Log back in": "Volver a iniciar sesión",
+    "Logout": "Cerrar sesión",
+    "Administration": "‫Administración",
+    "Overview": "Resumen",
+    "Management": "Gestión",
+    "Other": "Otro",
+    "Logs": "Logs",
+    "Warning!": "¡Advertencia!",
+    "You have not yet verified your email address": "No has verificado tu correo electrónico",
+    "Click here to resend verification email": "Haz click aquí para reenviar tu correo electrónico de activación",
+    "Please contact support If you didnt receive your verification email.": "Contacte con el soporte si no recibió su correo electrónico de verificación.",
+    "Thank you for your purchase!": "¡Gracias por su compra!",
+    "Your payment has been confirmed; Your credit balance has been updated.": "Su pago ha sido confirmado; Se actualizó su saldo de crédito.",
+    "Thanks": "Gracias",
+    "Redeem voucher code": "Canjear código de descuento",
+    "Close": "Cerrar",
+    "Redeem": "Canjear",
+    "All notifications": "Todas las notificaciones",
+    "Required Email verification!": "¡Se requiere verificación de correo electrónico!",
+    "Required Discord verification!": "¡Se requiere verificación de Discord!",
+    "You have not yet verified your discord account": "Aún no has verificado tu cuenta de discord",
+    "Login with discord": "Acceder con Discord",
+    "Please contact support If you face any issues.": "Póngase en contacto con soporte si tiene algún problema.",
+    "Due to system settings you are required to verify your discord account!": "¡Debido a la configuración del sistema, debe verificar su cuenta de discord!",
+    "It looks like this hasnt been set-up correctly! Please contact support.": "¡Parece que esto no se ha configurado correctamente! Comuníquese con el soporte.",
+    "Change Password": "Cambiar Contraseña",
+    "Current Password": "Contraseña Actual",
+    "Link your discord account!": "¡Vincular tu cuenta de Discord!",
+    "By verifying your discord account, you receive extra Credits and increased Server amounts": "Al verificar su cuenta de discord, recibe créditos adicionales y aumenta su limite de cantidad de servidores",
+    "Login with Discord": "Iniciar sesión con Discord",
+    "You are verified!": "¡Estás verificado!",
+    "Re-Sync Discord": "Re-Sincronizar Discord",
+    "Save Changes": "Guardar Cambios",
+    "Server configuration": "Configuración del servidor",
+    "Make sure to link your products to nodes and eggs.": "Asegúrese de vincular sus productos a nodos y huevos.",
+    "There has to be at least 1 valid product for server creation": "Tiene que haber al menos 1 producto válido para la creación del servidor",
+    "Sync now": "Sincronizar ahora",
+    "No products available!": "¡No hay productos disponibles!",
+    "No nodes have been linked!": "¡No se han vinculado nodos!",
+    "No nests available!": "¡No hay nidos disponibles!",
+    "No eggs have been linked!": "¡No se han vinculado huevos!",
+    "Software \/ Games": "Software \/ Juegos",
+    "Please select software ...": "Seleccione el software...",
+    "---": "---",
+    "Specification ": "Especificación ",
+    "Node": "Nodo",
+    "Resource Data:": "Datos de recursos:",
+    "vCores": "vCores",
+    "MB": "MB",
+    "MySQL": "MySQL",
+    "ports": "puertos",
+    "Not enough": "No es suficiente",
+    "Create server": "Crear Servidor",
+    "Please select a node ...": "Por favor, seleccione un nodo...",
+    "No nodes found matching current configuration": "No se encontraron nodos que coincidan con la configuración actual",
+    "Please select a resource ...": "Por favor, seleccione un recurso ...",
+    "No resources found matching current configuration": "No se encontraron recursos que coincidan con la configuración actual",
+    "Please select a configuration ...": "Por favor elija su configuración...",
+    "Not enough credits!": "¡No tiene suficientes créditos!",
+    "Create Server": "Crear Servidor",
+    "Software": "Software",
+    "Specification": "Especificaciones",
+    "Resource plan": "Plan de recursos",
+    "RAM": "RAM",
+    "MySQL Databases": "Bases de datos MySQL",
+    "per Hour": "por Hora",
+    "per Month": "por Mes",
+    "Manage": "Gestionar",
+    "Are you sure?": "¿Estas seguro?",
+    "This is an irreversible action, all files of this server will be removed.": "Esta es una acción irreversible, se eliminarán todos los archivos de este servidor.",
+    "Yes, delete it!": "Si, borralo",
+    "No, cancel!": "No, cancelar",
+    "Canceled ...": "Cancelado ...",
+    "Deletion has been canceled.": "Se canceló la eliminación.",
+    "Date": "Fecha",
+    "Subtotal": "Subtotal",
+    "Amount Due": "Cantidad Adeudada",
+    "Tax": "Impuesto",
+    "Submit Payment": "Proceder al Pago",
+    "Purchase": "Comprar",
+    "There are no store products!": "¡No hay productos de la tienda!",
+    "The store is not correctly configured!": "¡La tienda no está configurada correctamente!",
+    "Serial No.": "Nº Serie.",
+    "Invoice date": "Fecha de Factura",
+    "Seller": "Vendedor",
+    "Buyer": "Comprador",
+    "Address": "Dirección",
+    "VAT Code": "Código de IVA",
+    "Phone": "Teléfono",
+    "Units": "Unidades",
+    "Discount": "Descuento",
+    "Total discount": "Descuento total",
+    "Taxable amount": "Base imponible",
+    "Tax rate": "Tasa de impuestos",
+    "Total taxes": "Total de impuestos",
+    "Shipping": "Envío",
+    "Total amount": "Cantidad total",
+    "Notes": "Notas",
+    "Amount in words": "Cantidad en palabras",
+    "Please pay until": "Por favor pague hasta",
+    "Key": "Clave",
+    "Value": "Valor",
+    "Edit Configuration": "Editar Configuración",
+    "Text Field": "Campo de texto",
+    "Cancel": "Cancelar",
+    "Save": "Guardar",
+    "Images and Icons may be cached, reload without cache to see your changes appear": "Las imágenes y los íconos pueden almacenarse en caché, vuelva a cargar sin caché para ver sus cambios",
+    "Enter your companys name": "Introduce el nombre de tu empresa",
+    "Enter your companys address": "Ingrese la dirección de su empresa",
+    "Enter your companys phone number": "Ingrese el número de teléfono de su empresa",
+    "Enter your companys VAT id": "Ingrese el ID de IVA de su empresa",
+    "Enter your companys email address": "Ingrese la dirección de correo electrónico de su empresa",
+    "Enter your companys website": "Ingresa la web de tu empresa",
+    "Enter your custom invoice prefix": "Ingrese su prefijo de factura personalizado",
+    "The Language of the Datatables. Grab the Language-Codes from here": "El lenguaje de las tablas de datos. Coge los códigos de idioma de aquí",
+    "Let the Client change the Language": "Dejar que el cliente cambie el idioma",
+    "Icons updated!": "¡Iconos actualizados!",
+    "cs": "Checo",
+    "de": "Alemán",
+    "en": "Inglés",
+    "es": "Español",
+    "fr": "Francés",
+    "hi": "Hindi",
+    "it": "Italiano",
+    "nl": "Holandés",
+    "pl": "Polaco",
+    "zh": "Chino",
+    "tr": "Turco",
+    "ru": "Ruso"
+}

+ 18 - 0
resources/lang/es/auth.php

@@ -0,0 +1,18 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Authentication Language Lines
+|--------------------------------------------------------------------------
+|
+| The following language lines are used during authentication for various
+| messages that we need to display to the user. You are free to modify
+| these language lines according to your application's requirements.
+|
+*/
+
+return [
+    'failed'   => 'Estas credenciales no coinciden con nuestros registros.',
+    'password' => 'La contraseña ingresada no es correcta.',
+    'throttle' => 'Demasiados intentos de acceso. Por favor intente nuevamente en :seconds segundos.',
+];

+ 17 - 0
resources/lang/es/pagination.php

@@ -0,0 +1,17 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Pagination Language Lines
+|--------------------------------------------------------------------------
+|
+| The following language lines are used by the paginator library to build
+| the simple pagination links. You are free to change them to anything
+| you want to customize your views to better match your application.
+|
+*/
+
+return [
+    'next'     => 'Siguiente &raquo;',
+    'previous' => '&laquo; Anterior',
+];

+ 20 - 0
resources/lang/es/passwords.php

@@ -0,0 +1,20 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Password Reset Language Lines
+|--------------------------------------------------------------------------
+|
+| The following language lines are the default lines which match reasons
+| that are given by the password broker for a password update attempt
+| has failed, such as for an invalid token or invalid new password.
+|
+*/
+
+return [
+    'reset'     => '¡Su contraseña ha sido restablecida!',
+    'sent'      => '¡Le hemos enviado por correo electrónico el enlace para restablecer su contraseña!',
+    'throttled' => 'Por favor espere antes de intentar de nuevo.',
+    'token'     => 'El token de restablecimiento de contraseña es inválido.',
+    'user'      => 'No encontramos ningún usuario con ese correo electrónico.',
+];

+ 138 - 0
resources/lang/es/validation.php

@@ -0,0 +1,138 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Validation Language Lines
+|--------------------------------------------------------------------------
+|
+| The following language lines contain the default error messages used by
+| the validator class. Some of these rules have multiple versions such
+| as the size rules. Feel free to tweak each of these messages here.
+|
+*/
+
+return [
+    'accepted'             => ':attribute debe ser aceptado.',
+    'accepted_if'          => ':attribute debe ser aceptado cuando :other sea :value.',
+    'active_url'           => ':attribute no es una URL válida.',
+    'after'                => ':attribute debe ser una fecha posterior a :date.',
+    'after_or_equal'       => ':attribute debe ser una fecha posterior o igual a :date.',
+    'alpha'                => ':attribute sólo debe contener letras.',
+    'alpha_dash'           => ':attribute sólo debe contener letras, números, guiones y guiones bajos.',
+    'alpha_num'            => ':attribute sólo debe contener letras y números.',
+    'array'                => ':attribute debe ser un conjunto.',
+    'attached'             => 'Este :attribute ya se adjuntó.',
+    'before'               => ':attribute debe ser una fecha anterior a :date.',
+    'before_or_equal'      => ':attribute debe ser una fecha anterior o igual a :date.',
+    'between'              => [
+        'array'   => ':attribute tiene que tener entre :min - :max elementos.',
+        'file'    => ':attribute debe pesar entre :min - :max kilobytes.',
+        'numeric' => ':attribute tiene que estar entre :min - :max.',
+        'string'  => ':attribute tiene que tener entre :min - :max caracteres.',
+    ],
+    'boolean'              => 'El campo :attribute debe tener un valor verdadero o falso.',
+    'confirmed'            => 'La confirmación de :attribute no coincide.',
+    'current_password'     => 'La contraseña es incorrecta.',
+    'date'                 => ':attribute no es una fecha válida.',
+    'date_equals'          => ':attribute debe ser una fecha igual a :date.',
+    'date_format'          => ':attribute no corresponde al formato :format.',
+    'declined'             => ':attribute debe ser rechazado.',
+    'declined_if'          => ':attribute debe ser rechazado cuando :other sea :value.',
+    'different'            => ':attribute y :other deben ser diferentes.',
+    'digits'               => ':attribute debe tener :digits dígitos.',
+    'digits_between'       => ':attribute debe tener entre :min y :max dígitos.',
+    'dimensions'           => 'Las dimensiones de la imagen :attribute no son válidas.',
+    'distinct'             => 'El campo :attribute contiene un valor duplicado.',
+    'email'                => ':attribute no es un correo válido.',
+    'ends_with'            => 'El campo :attribute debe finalizar con uno de los siguientes valores: :values',
+    'exists'               => ':attribute es inválido.',
+    'file'                 => 'El campo :attribute debe ser un archivo.',
+    'filled'               => 'El campo :attribute es obligatorio.',
+    'gt'                   => [
+        'array'   => 'El campo :attribute debe tener más de :value elementos.',
+        'file'    => 'El campo :attribute debe tener más de :value kilobytes.',
+        'numeric' => 'El campo :attribute debe ser mayor que :value.',
+        'string'  => 'El campo :attribute debe tener más de :value caracteres.',
+    ],
+    'gte'                  => [
+        'array'   => 'El campo :attribute debe tener como mínimo :value elementos.',
+        'file'    => 'El campo :attribute debe tener como mínimo :value kilobytes.',
+        'numeric' => 'El campo :attribute debe ser como mínimo :value.',
+        'string'  => 'El campo :attribute debe tener como mínimo :value caracteres.',
+    ],
+    'image'                => ':attribute debe ser una imagen.',
+    'in'                   => ':attribute es inválido.',
+    'in_array'             => 'El campo :attribute no existe en :other.',
+    'integer'              => ':attribute debe ser un número entero.',
+    'ip'                   => ':attribute debe ser una dirección IP válida.',
+    'ipv4'                 => ':attribute debe ser una dirección IPv4 válida.',
+    'ipv6'                 => ':attribute debe ser una dirección IPv6 válida.',
+    'json'                 => 'El campo :attribute debe ser una cadena JSON válida.',
+    'lt'                   => [
+        'array'   => 'El campo :attribute debe tener menos de :value elementos.',
+        'file'    => 'El campo :attribute debe tener menos de :value kilobytes.',
+        'numeric' => 'El campo :attribute debe ser menor que :value.',
+        'string'  => 'El campo :attribute debe tener menos de :value caracteres.',
+    ],
+    'lte'                  => [
+        'array'   => 'El campo :attribute debe tener como máximo :value elementos.',
+        'file'    => 'El campo :attribute debe tener como máximo :value kilobytes.',
+        'numeric' => 'El campo :attribute debe ser como máximo :value.',
+        'string'  => 'El campo :attribute debe tener como máximo :value caracteres.',
+    ],
+    'max'                  => [
+        'array'   => ':attribute no debe tener más de :max elementos.',
+        'file'    => ':attribute no debe ser mayor que :max kilobytes.',
+        'numeric' => ':attribute no debe ser mayor que :max.',
+        'string'  => ':attribute no debe ser mayor que :max caracteres.',
+    ],
+    'mimes'                => ':attribute debe ser un archivo con formato: :values.',
+    'mimetypes'            => ':attribute debe ser un archivo con formato: :values.',
+    'min'                  => [
+        'array'   => ':attribute debe tener al menos :min elementos.',
+        'file'    => 'El tamaño de :attribute debe ser de al menos :min kilobytes.',
+        'numeric' => 'El tamaño de :attribute debe ser de al menos :min.',
+        'string'  => ':attribute debe contener al menos :min caracteres.',
+    ],
+    'multiple_of'          => 'El campo :attribute debe ser múltiplo de :value',
+    'not_in'               => ':attribute es inválido.',
+    'not_regex'            => 'El formato del campo :attribute no es válido.',
+    'numeric'              => ':attribute debe ser numérico.',
+    'password'             => 'La contraseña es incorrecta.',
+    'present'              => 'El campo :attribute debe estar presente.',
+    'prohibited'           => 'El campo :attribute está prohibido.',
+    'prohibited_if'        => 'El campo :attribute está prohibido cuando :other es :value.',
+    'prohibited_unless'    => 'El campo :attribute está prohibido a menos que :other sea :values.',
+    'prohibits'            => 'El campo :attribute prohibe que :other esté presente.',
+    'regex'                => 'El formato de :attribute es inválido.',
+    'relatable'            => 'Este :attribute no se puede asociar con este recurso',
+    'required'             => 'El campo :attribute es obligatorio.',
+    'required_if'          => 'El campo :attribute es obligatorio cuando :other es :value.',
+    'required_unless'      => 'El campo :attribute es obligatorio a menos que :other esté en :values.',
+    'required_with'        => 'El campo :attribute es obligatorio cuando :values está presente.',
+    'required_with_all'    => 'El campo :attribute es obligatorio cuando :values están presentes.',
+    'required_without'     => 'El campo :attribute es obligatorio cuando :values no está presente.',
+    'required_without_all' => 'El campo :attribute es obligatorio cuando ninguno de :values está presente.',
+    'same'                 => ':attribute y :other deben coincidir.',
+    'size'                 => [
+        'array'   => ':attribute debe contener :size elementos.',
+        'file'    => 'El tamaño de :attribute debe ser :size kilobytes.',
+        'numeric' => 'El tamaño de :attribute debe ser :size.',
+        'string'  => ':attribute debe contener :size caracteres.',
+    ],
+    'starts_with'          => 'El campo :attribute debe comenzar con uno de los siguientes valores: :values',
+    'string'               => 'El campo :attribute debe ser una cadena de caracteres.',
+    'timezone'             => ':Attribute debe ser una zona horaria válida.',
+    'unique'               => 'El campo :attribute ya ha sido registrado.',
+    'uploaded'             => 'Subir :attribute ha fallado.',
+    'url'                  => ':Attribute debe ser una URL válida.',
+    'uuid'                 => 'El campo :attribute debe ser un UUID válido.',
+    'custom'               => [
+        'email'    => [
+            'unique' => 'El :attribute ya ha sido registrado.',
+        ],
+        'password' => [
+            'min' => 'La :attribute debe contener más de :min caracteres',
+        ],
+    ],
+];

+ 328 - 0
resources/lang/fr.json

@@ -0,0 +1,328 @@
+{
+    "Activity Logs": "Journal des activités",
+    "No recent activity from cronjobs": "Aucune activité récente de cronjobs",
+    "Check the docs for it here": "Consultez la documentation ici",
+    "Are cronjobs running?": "Les tâches cron sont-elles en cours d'exécution ?",
+    "Causer": "Cause",
+    "Description": "Description",
+    "Created at": "Créé à",
+    "Edit Configuration": "Modifier la configuration",
+    "Text Field": "Champ de texte",
+    "Cancel": "Annuler",
+    "Close": "Fermer",
+    "Save": "Sauvegarder",
+    "true": "vrai",
+    "false": "faux",
+    "Configurations": "Configuration",
+    "Dashboard": "Tableau de bord",
+    "Key": "Clé",
+    "Value": "Valeur",
+    "Type": "Type",
+    "Admin Overview": "Vue administrateur",
+    "Support server": "Serveur de support",
+    "Documentation": "Documentation",
+    "Github": "Github",
+    "Support ControlPanel": "ControlPanel Support",
+    "Servers": "Serveurs",
+    "Users": "Utilisateurs",
+    "Total": "Total",
+    "Payments": "Paiments",
+    "Pterodactyl": "Pterodactyl",
+    "Sync": "Synchroniser",
+    "Resources": "Ressources",
+    "Count": "Nombre",
+    "Locations": "Emplacements",
+    "Node": "Node",
+    "Nodes": "Nœuds",
+    "Nests": "Nids",
+    "Eggs": "Œufs",
+    "Last updated :date": "Dernière mise à jour",
+    "Purchase": "Acheter",
+    "ID": "IDENTIFIANT",
+    "User": "Utilisateur",
+    "Amount": "Montant ",
+    "Product Price": "Prix du produit",
+    "Tax": "Tva & autres taxes",
+    "Total Price": "Prix total",
+    "Payment_ID": "ID_PAIEMENT",
+    "Payer_ID": "Payer_ID",
+    "Product": "Article",
+    "Products": "Produits",
+    "Create": "Créer",
+    "Product Details": "Détails du produit",
+    "Server Details": "Détails du serveur",
+    "Product Linking": "Lien du produit",
+    "Name": "Nom",
+    "Price in": "Prix en",
+    "Memory": "Mémoire",
+    "Cpu": "Cpu",
+    "Swap": "Swap",
+    "Disk": "Disque",
+    "Minimum": "Minimum",
+    "IO": "IO",
+    "Databases": "Bases de données",
+    "Database": "Base de donnée",
+    "Backups": "Sauvegardes",
+    "Allocations": "Allocations",
+    "Disabled": "Désactivé",
+    "Submit": "Valider",
+    "This product will only be available for these nodes": "Ce produit est uniquement disponible pour cette node",
+    "This product will only be available for these eggs": "Ce produit n'est pas disponible pour cet eggs",
+    "Will hide this option from being selected": "Cachera cette option",
+    "Link your products to nodes and eggs to create dynamic pricing for each option": "Liez vos produits à des nodes et des eggs pour créer une tarification dynamique pour chaque option",
+    "Setting to -1 will use the value from configuration.": "Le réglage à -1 utilisera la valeur de la configuration.",
+    "This is what the users sees": "C'est ce que voient les utilisateurs",
+    "Edit": "Modifier",
+    "Price": "Prix",
+    "Are you sure you wish to delete?": "Êtes-vous sûr de vouloir supprimer ?",
+    "Create new": "Créer nouveau",
+    "Show": "Voir",
+    "Updated at": "Mis à jour le",
+    "Suspended at": "Suspendus le",
+    "Settings": "Paramètres",
+    "Dashboard icons": "Icônes du tableau de bord",
+    "Select panel icon": "Sélectionner l'icône du panel",
+    "Select panel favicon": "Sélectionner le favicon du panel",
+    "Token": "Token",
+    "Last used": "Dernière utilisation",
+    "Store": "Boutique",
+    "Currency code": "Code de devise",
+    "Checkout the paypal docs to select the appropriate code": "Vérifiez la doc de paypal pour sélectionner le code approprié",
+    "Quantity": "Quantité",
+    "Amount given to the user after purchasing": "Montant donné à l'utilisateur après l'achat",
+    "Display": "Affichage",
+    "This is what the user sees at store and checkout": "C'est ce que l'utilisateur voit dans la boutique et au moment de payé",
+    "This is what the user sees at checkout": "C'est ce que l'utilisateur voit dans au moment de payé",
+    "Adds 1000 credits to your account": "Ajoute 1000 crédits à votre compte",
+    "Active": "Actif",
+    "Paypal is not configured.": "Paypal n'est pas configuré.",
+    "To configure PayPal, head to the .env and add your PayPal’s client id and secret.": "Pour configurer PayPal, rendez-vous sur le fichier .env et ajoutez votre identifiant \"client id\" PayPal et votre \"client id secret\".",
+    "Useful Links": "Liens Utiles",
+    "Icon class name": "Nom de la classe de l'icône",
+    "You can find available free icons": "Vous pouvez trouver des icônes gratuites",
+    "Title": "Titre",
+    "Link": "Lien",
+    "Username": "Nom d'utilisateur",
+    "Email": "Adresse email",
+    "Pterodactly ID": "ID Pterodactyl",
+    "Server Limit": "Limite Serveur",
+    "Role": "Rôle",
+    "Administrator": "Administrateur",
+    "Client": "Client",
+    "Member": "Membre",
+    "New Password": "Nouveau mot de passe",
+    "Confirm Password": "Confirmez le mot de passe",
+    "This ID refers to the user account created on pterodactyls panel.": "Cet identifiant fait référence au compte utilisateur créé sur le panel Pterodactyl.",
+    "Only edit this if you know what youre doing :)": "Ne l'activez que si vous savez ce que vous faites.",
+    "Verified": "Verifié",
+    "Last seen": "Etait ici",
+    "Notify": "Notifier",
+    "All": "Tout",
+    "Send via": "Envoyer via",
+    "Content": "Contenu",
+    "Notifications": "Notifications",
+    "Usage": "Utilisation",
+    "Config": "Configuration",
+    "Vouchers": "Coupons",
+    "Voucher details": "Détails du bon de réduction",
+    "Memo": "Mémo",
+    "Code": "Code",
+    "Uses": "Utilisations",
+    "Expires at": "Expire à",
+    "Max": "Max",
+    "Random": "Aléatoire",
+    "Status": "Statut",
+    "Used / Uses": "Utilisé / Utilisations",
+    "Expires": "Expire",
+    "Please confirm your password before continuing.": "Veuillez confirmer votre mot de passe avant de continuer.",
+    "Password": "Mot de passe",
+    "Forgot Your Password?": "Mot de passe oublié ?",
+    "Sign in to start your session": "Identifiez-vous pour commencer votre session",
+    "Remember Me": "Se souvenir de moi",
+    "Sign In": "S'enregistrer",
+    "Register a new membership": "Enregistrer un nouveau membre",
+    "You forgot your password? Here you can easily retrieve a new password.": "Tu as oublie ton mot de passe ? Ici tu peux facilement le changé.",
+    "Request new password": "Changer son mot de passe",
+    "Login": "Connexion",
+    "You are only one step a way from your new password, recover your password now.": "Vous n'êtes qu'à une étape de votre nouveau mot de passe, récupérez votre mot de passe maintenant.",
+    "Retype password": "Retapez le mot de passe",
+    "Change password": "Changer le mot de passe",
+    "I already have a membership": "Je possède déjà un compte",
+    "Register": "Inscription",
+    "Verify Your Email Address": "Vérifiez votre adresse email",
+    "A fresh verification link has been sent to your email address.": "Un nouveau lien de vérification a été envoyé à votre adresse email.",
+    "Before proceeding, please check your email for a verification link.": "Avant de continuer, veuillez vérifier vos emails, vous devriez avoir reçu un lien de vérification.",
+    "If you did not receive the email": "Si vous ne recevez pas l'e-mail",
+    "click here to request another": "cliquez ici pour faire une nouvelle demande",
+    "Home": "Accueil",
+    "Languages": "Langues",
+    "See all Notifications": "Voir toutes les notifications",
+    "Profile": "Profil",
+    "Log back in": "Reconnectez-vous",
+    "Logout": "Déconnexion",
+    "Administration": "Administration",
+    "Overview": "Récapitulatif",
+    "Application API": "Application API",
+    "Management": "Gestion",
+    "Other": "Autre",
+    "Logs": "Logs",
+    "Redeem code": "Utiliser un code",
+    "You have not yet verified your email address": "Vous n'avez pas vérifiez votre adresse mail",
+    "Click here to resend verification email": "Cliquez ici pour renvoyer un mail de confirmation",
+    "Please contact support If you didnt receive your verification email.": "Veuillez contacter le support si vous n'avez pas reçu votre e-mail de vérification.",
+    "Thank you for your purchase!": "Merci pour votre achat !",
+    "Your payment has been confirmed; Your credit balance has been updated.": "Votre paiement à été accepter, Vos crédits son maintenant disponible sur votre compte.",
+    "Payment ID": "ID du paiement",
+    "Balance": "Solde",
+    "User ID": "ID d'utilisateur",
+    "Thanks": "Merci",
+    "Redeem voucher code": "Utiliser le code",
+    "Redeem": "Appliquer",
+    "All notifications": "Toutes les notifications",
+    "Required Email verification!": "La vérification du mail est requise !",
+    "Required Discord verification!": "Vérification de votre discord est requise !",
+    "You have not yet verified your discord account": "Vous n'avez pas vérifiez votre compte discord",
+    "Login with discord": "Se connecter avec Discord",
+    "Please contact support If you face any issues.": "Veuillez contacter le support si vous rencontrez des problèmes.",
+    "Due to system settings you are required to verify your discord account!": "En raison des paramètres système, vous devez vérifier votre compte Discord !",
+    "It looks like this hasnt been set-up correctly! Please contact support.": "Il semble que cela n'a pas été configuré correctement ! Veuillez contacter le support.",
+    "Change Password": "Modifier le mot de passe",
+    "Current Password": "Mot de passe actuel",
+    "Save Changes": "Sauvegarder les modifications",
+    "Re-Sync Discord": "Resynchroniser Discord",
+    "You are verified!": "Vous êtes vérifié !",
+    "By verifying your discord account, you receive extra Credits and increased Server amounts": "En vérifiant votre compte discord, vous recevez des crédits supplémentaires et la possibilité d'avoir plus de serveur",
+    "Server configuration": "Configuration du serveur",
+    "Error!": "Erreur !",
+    "Make sure to link your products to nodes and eggs.": "Assurez-vous de lier vos produits aux nodes aux eggs.",
+    "There has to be at least 1 valid product for server creation": "Il doit y avoir au moins 1 produit valide pour la création de serveur",
+    "No products available!": "Aucun produit disponible !",
+    "No nodes have been linked!": "Aucune node n'a été lié !",
+    "No nests available!": "Aucun nests disponible !",
+    "No eggs have been linked!": "Aucun eggs n'a été lié !",
+    "Software / Games": "Logiciels / Jeux",
+    "Please select software ...": "Veuillez sélectionner...",
+    "Specification": "Spécification",
+    "No selection": "Pas de sélection",
+    "per month": "par mois",
+    "Not enough credits!": "Pas assez de crédits !",
+    "Please select a configuration ...": "Veuillez sélectionner une configuration...",
+    "No resources found matching current configuration": "Aucune ressources trouvée pour la configuration actuelle",
+    "No nodes found matching current configuration": "Aucune node trouvée pour la configuration actuelle",
+    "Please select a node ...": "Veuillez sélectionner une node...",
+    "Create server": "Créer le serveur",
+    "Use your servers on our": "Utilisez vos serveurs sur notre",
+    "pterodactyl panel": "panel pterodactyl",
+    "Server limit reached!": "Limite de serveurs atteinte !",
+    "Create Server": "Créer le serveur",
+    "Manage": "Gérer",
+    "Delete server": "Supprimer le serveur",
+    "Price per Hour": "Prix par heure",
+    "Price per Month": "Prix par Mois",
+    "Date": "Date",
+    "To": "À",
+    "From": "De",
+    "Pending": "En attente",
+    "Subtotal": "Sous-total",
+    "Submit Payment": "Soumettre le Paiement",
+    "Payment Methods": "Moyens de paiement",
+    "By purchasing this product you agree and accept our terms of service": "En achetant ce produit, vous acceptez et acceptez nos conditions d'utilisation",
+    "There are no store products!": "Il n'y a plus de produits dans la boutique !",
+    "The store is not correctly configured!": "La boutique n'est pas configurée correctement !",
+    "Out of Credits in": "Hors crédits dans",
+    "days": "jours",
+    "hours": "heures",
+    "You ran out of Credits": "Vous n’avez plus de crédits",
+    "Profile updated": "Profil mis à jour",
+    "You are required to verify your email address before you can create a server.": "Vous devez vérifier votre email avant de pouvoir créer un serveur.",
+    "You are required to link your discord account before you can create a server.": "Vous devez vérifier votre discord avant de pouvoir créer un serveur.",
+    "No allocations satisfying the requirements for automatic deployment on this node were found.": "Aucune allocation répondant aux exigences de déploiement automatique sur cette node n'a été trouvée.",
+    "Server removed": "Serveur supprimé",
+    "Server created": "Serveur créé",
+    "An exception has occurred while trying to remove a resource \"": "Une erreur s'est produite en essayant de supprimer la ressource",
+    "You are required to verify your email address before you can purchase credits.": "Vous devez vérifier votre email avant de pouvoir acheter des crédits.",
+    "You are required to link your discord account before you can purchase ": "Vous devez vérifier votre compte discord avant de pouvoir acheter des crédits ",
+    "Warning!": "Attention !",
+    "api key created!": "La clé Api a été créée !",
+    "api key updated!": "La clé Api a été modifiée !",
+    "api key has been removed!": "La clé Api a été supprimée !",
+    "configuration has been updated!": "la configuration a été mise à jour!",
+    "Pterodactyl synced": "Synchroniser Pterodactyl",
+    "Your credit balance has been increased!": "Votre solde a été augmenté !",
+    "Payment was Canceled": "Le paiement a été annulé",
+    "Store item has been created!": "L'article de la boutique a été créé !",
+    "Store item has been updated!": "L'article de la boutique a été mis à jour !",
+    "Product has been updated!": "Produit mis à jour !",
+    "Store item has been removed!": "L'article de la boutique a été supprimé !",
+    "Product has been created!": "Produit a été créé !",
+    "Product has been removed!": "Produit a été supprimé !",
+    "Server has been updated!": "Le serveur à été mis à jour !",
+    "Icons updated!": "Icône mise à jour !",
+    "link has been created!": "Le lien à été créé !",
+    "link has been updated!": "Le lien à été mis à jour !",
+    "user has been removed!": "L'utilisateur a été supprimé !",
+    "Notification sent!": "Notification envoyée !",
+    "User has been updated!": "L'utilisateur a été mis à jour !",
+    "User does not exists on pterodactyl's panel": "L'utilisateur n'existe pas sur le panel pterodactyl",
+    "voucher has been created!": "Le code à été créé !",
+    "voucher has been updated!": "Le code à été mis à jour !",
+    "voucher has been removed!": "Le code à été supprimé !",
+    "This voucher has reached the maximum amount of uses": "Ce code a atteint le nombre maximum d'utilisations",
+    "This voucher has expired": "Ce code de réduction a expiré",
+    "You already redeemed this voucher code": "Vous avez déjà utilisé ce code promotionnel",
+    "You can't redeem this voucher because you would exceed the  limit of ": "Vous ne pouvez pas utiliser ce bon car vous dépasseriez la limite de ",
+    "have been added to your balance!": "ont été ajoutés à votre solde !",
+    "Invoice": "Facture",
+    "Serial No.": "N° de série",
+    "Invoice date": "Date de la facture",
+    "Seller": "Vendeur",
+    "Buyer": "Acheteur",
+    "Address": "Adresse",
+    "VAT code": "Taux TVA",
+    "Phone": "Téléphone",
+    "Units": "Unités",
+    "Qty": "Qté",
+    "Discount": "Remise",
+    "Sub total": "Sous-total",
+    "Total discount": "Total des réductions",
+    "Taxable amount": "Montant taxable",
+    "Total taxes": "Total des taxes",
+    "Tax rate": "Taux de taxes",
+    "Total amount": "Montant total",
+    "Please pay until": "Veuillez payer avant",
+    "Amount in words": "Montant en toutes lettres",
+    "Notes": "Notes",
+    "Shipping": "Expédition",
+    "Paid": "Payé",
+    "Due:": "Du:",
+    "Invoice Settings": "Paramètres de facturation",
+    "Download all Invoices": "Télécharger toutes les factures",
+    "Enter your companys name": "Entrez le nom de votre entreprise",
+    "Enter your companys address": "Entrez l'adresse de votre entreprise",
+    "Enter your companys phone number": "Entrez le numéro de téléphone de votre entreprise",
+    "Enter your companys VAT id": "Entrez le site internet de votre entreprise",
+    "Enter your companys email address": "Entrez l'adresse mail de votre entreprise",
+    "Enter your companys website": "Entrez le site internet de votre entreprise",
+    "Enter your custom invoice prefix": "Entrez votre préfixe de facture personnalisé",
+    "Select Invoice Logo": "Sélectionnez le logo des factures",
+    "Payment Confirmation": "Confirmation de paiement",
+    "Payment Confirmed!": "Paiement confirmé !",
+    "Server Creation Error": "Erreur lors de la création de votre serveur",
+    "Your servers have been suspended!": "Votre serveur à été suspendu !",
+    "To automatically re-enable your server/s, you need to purchase more credits.": "Pour réactiver automatiquement votre ou vos serveurs, vous devez racheter des crédits.",
+    "Purchase credits": "Acheter des crédits",
+    "If you have any questions please let us know.": "N'hésitez pas à nous contacter si vous avez des questions.",
+    "Regards": "Cordialement",
+    "Getting started!": "Commencer !",
+    "EXPIRED": "EXPIRÉ",
+    "VALID": "VALIDE",
+    "Unsuspend": "Annuler la suspension",
+    "Suspend": "Suspendre",
+    "Delete": "Supprimer",
+    "Login as User": "Connectez-vous en tant qu'utilisateur",
+    "Clone": "Dupliquer",
+    "Amount due": "Montant à payer",
+    "Your Payment was successful!": "Votre paiement a été reçu avec succès !",
+    "Hello": "Bonjour",
+    "Your payment was processed successfully!": "Votre requête a été traitée avec succès."
+}

+ 18 - 0
resources/lang/fr/auth.php

@@ -0,0 +1,18 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Authentication Language Lines
+|--------------------------------------------------------------------------
+|
+| The following language lines are used during authentication for various
+| messages that we need to display to the user. You are free to modify
+| these language lines according to your application's requirements.
+|
+*/
+
+return [
+    'failed'   => 'Ces identifiants ne correspondent pas à nos enregistrements.',
+    'password' => 'Le mot de passe fourni est incorrect.',
+    'throttle' => 'Tentatives de connexion trop nombreuses. Veuillez essayer de nouveau dans :seconds secondes.',
+];

+ 17 - 0
resources/lang/fr/pagination.php

@@ -0,0 +1,17 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Pagination Language Lines
+|--------------------------------------------------------------------------
+|
+| The following language lines are used by the paginator library to build
+| the simple pagination links. You are free to change them to anything
+| you want to customize your views to better match your application.
+|
+*/
+
+return [
+    'next'     => 'Suivant &raquo;',
+    'previous' => '&laquo; Précédent',
+];

+ 20 - 0
resources/lang/fr/passwords.php

@@ -0,0 +1,20 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Password Reset Language Lines
+|--------------------------------------------------------------------------
+|
+| The following language lines are the default lines which match reasons
+| that are given by the password broker for a password update attempt
+| has failed, such as for an invalid token or invalid new password.
+|
+*/
+
+return [
+    'reset'     => 'Votre mot de passe a été réinitialisé !',
+    'sent'      => 'Nous vous avons envoyé par email le lien de réinitialisation du mot de passe !',
+    'throttled' => 'Veuillez patienter avant de réessayer.',
+    'token'     => 'Ce jeton de réinitialisation du mot de passe n\'est pas valide.',
+    'user'      => 'Aucun utilisateur n\'a été trouvé avec cette adresse email.',
+];

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