diff --git a/bin/quickstart.sh b/bin/quickstart.sh --- a/bin/quickstart.sh +++ b/bin/quickstart.sh @@ -97,9 +97,8 @@ ./artisan db:ping --wait php -dmemory_limit=512M ./artisan migrate:refresh --seed ./artisan data:import || : -./artisan swoole:http stop >/dev/null 2>&1 || : -SWOOLE_HTTP_DAEMONIZE=true ./artisan swoole:http start +./artisan octane:stop >/dev/null 2>&1 || : +./artisan octane:start >/dev/null 2>&1 & ./artisan horizon:terminate >/dev/null 2>&1 || : nohup ./artisan horizon >/dev/null 2>&1 & popd - diff --git a/docker/swoole/rootfs/usr/local/bin/run-container b/docker/swoole/rootfs/usr/local/bin/run-container --- a/docker/swoole/rootfs/usr/local/bin/run-container +++ b/docker/swoole/rootfs/usr/local/bin/run-container @@ -36,7 +36,7 @@ env - exec ./artisan swoole:http start + exec ./artisan octane:start else exec $@ fi diff --git a/src/.env.example b/src/.env.example --- a/src/.env.example +++ b/src/.env.example @@ -30,6 +30,8 @@ LOG_CHANNEL=stack LOG_SLOW_REQUESTS=5 +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug DB_CONNECTION=mysql DB_DATABASE=kolabdev @@ -127,7 +129,7 @@ STRIPE_PUBLIC_KEY= STRIPE_WEBHOOK_SECRET= -MAIL_DRIVER=smtp +MAIL_MAILER=smtp MAIL_HOST=smtp.mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=null @@ -147,6 +149,7 @@ AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false PUSHER_APP_ID= PUSHER_APP_KEY= diff --git a/src/.gitattributes b/src/.gitattributes --- a/src/.gitattributes +++ b/src/.gitattributes @@ -1,5 +1,9 @@ * text=auto -*.css linguist-vendored -*.scss linguist-vendored -*.js linguist-vendored + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + CHANGELOG.md export-ignore diff --git a/src/.styleci.yml b/src/.styleci.yml --- a/src/.styleci.yml +++ b/src/.styleci.yml @@ -1,11 +1,10 @@ php: preset: laravel disabled: - - unused_use + - no_unused_imports finder: not-name: - index.php - - server.php js: finder: not-name: diff --git a/src/app/Auth/LDAPUserProvider.php b/src/app/Auth/LDAPUserProvider.php deleted file mode 100644 --- a/src/app/Auth/LDAPUserProvider.php +++ /dev/null @@ -1,54 +0,0 @@ -get(); - - $count = $entries->count(); - - if ($count == 1) { - return $entries->first(); - } - - if ($count > 1) { - \Log::warning("Multiple entries for {$credentials['email']}"); - } else { - \Log::warning("No entries for {$credentials['email']}"); - } - - return null; - } - - /** - * Validate the credentials for a user. - * - * @param Authenticatable $user The user. - * @param array $credentials The credentials. - * - * @return bool - */ - public function validateCredentials(Authenticatable $user, array $credentials): bool - { - return $user->validateCredentials($credentials['email'], $credentials['password']); - } -} diff --git a/src/app/AuthAttempt.php b/src/app/AuthAttempt.php --- a/src/app/AuthAttempt.php +++ b/src/app/AuthAttempt.php @@ -2,10 +2,10 @@ namespace App; -use Illuminate\Database\Eloquent\Model; -use Iatstuti\Database\Support\NullableFields; use App\Traits\UuidStrKeyTrait; use Carbon\Carbon; +use Dyrynda\Database\Support\NullableFields; +use Illuminate\Database\Eloquent\Model; /** * The eloquent definition of an AuthAttempt. @@ -27,10 +27,10 @@ private const STATUS_ACCEPTED = 'ACCEPTED'; private const STATUS_DENIED = 'DENIED'; - protected $nullable = [ - 'reason', - ]; + /** @var array The attributes that can be not set */ + protected $nullable = ['reason']; + /** @var array The attributes that are mass assignable */ protected $fillable = [ 'ip', 'user_id', @@ -40,6 +40,7 @@ 'last_seen', ]; + /** @var array The attributes that should be cast */ protected $casts = [ 'expires_at' => 'datetime', 'last_seen' => 'datetime' @@ -106,7 +107,7 @@ */ public function notify(): bool { - return \App\CompanionApp::notifyUser($this->user_id, ['token' => $this->id]); + return CompanionApp::notifyUser($this->user_id, ['token' => $this->id]); } /** @@ -154,12 +155,12 @@ * * @return \App\AuthAttempt */ - public static function recordAuthAttempt(\App\User $user, $clientIP) + public static function recordAuthAttempt(User $user, $clientIP) { - $authAttempt = \App\AuthAttempt::where('ip', $clientIP)->where('user_id', $user->id)->first(); + $authAttempt = AuthAttempt::where('ip', $clientIP)->where('user_id', $user->id)->first(); if (!$authAttempt) { - $authAttempt = new \App\AuthAttempt(); + $authAttempt = new AuthAttempt(); $authAttempt->ip = $clientIP; $authAttempt->user_id = $user->id; } diff --git a/src/app/Backends/LDAP.php b/src/app/Backends/LDAP.php --- a/src/app/Backends/LDAP.php +++ b/src/app/Backends/LDAP.php @@ -1031,6 +1031,7 @@ $entry['kolabfoldertype'] = $folder->type; $entry['kolabtargetfolder'] = $settings['folder'] ?? ''; $entry['acl'] = !empty($settings['acl']) ? json_decode($settings['acl'], true) : ''; + $entry['alias'] = $folder->aliases()->pluck('alias')->all(); } /** @@ -1073,7 +1074,7 @@ $entry['inetuserstatus'] = $user->status; $entry['o'] = $settings['organization']; $entry['mailquota'] = 0; - $entry['alias'] = $user->aliases->pluck('alias')->toArray(); + $entry['alias'] = $user->aliases()->pluck('alias')->all(); $roles = []; @@ -1193,7 +1194,7 @@ $domainName = explode('@', $email, 2)[1]; $base_dn = self::baseDN($ldap, $domainName, 'Shared Folders'); - $attrs = ['dn', 'cn', 'mail', 'objectclass', 'kolabtargetfolder', 'kolabfoldertype', 'acl']; + $attrs = ['dn', 'cn', 'mail', 'objectclass', 'kolabtargetfolder', 'kolabfoldertype', 'acl', 'alias']; // For shared folders we're using search() instead of get_entry() because // a folder name is not constant, so e.g. on update we might have @@ -1355,7 +1356,7 @@ */ private static function throwException($ldap, string $message): void { - if (empty(self::$ldap) && !empty($ldap)) { + if (empty(self::$ldap)) { $ldap->close(); } diff --git a/src/app/CompanionApp.php b/src/app/CompanionApp.php --- a/src/app/CompanionApp.php +++ b/src/app/CompanionApp.php @@ -11,6 +11,7 @@ */ class CompanionApp extends Model { + /** @var array The attributes that are mass assignable */ protected $fillable = [ 'name', 'user_id', @@ -67,7 +68,7 @@ */ public static function notifyUser($userId, $data): bool { - $notificationTokens = \App\CompanionApp::where('user_id', $userId) + $notificationTokens = CompanionApp::where('user_id', $userId) ->where('mfa_enabled', true) ->pluck('notification_token') ->all(); diff --git a/src/app/Console/Command.php b/src/app/Console/Command.php --- a/src/app/Console/Command.php +++ b/src/app/Console/Command.php @@ -9,9 +9,9 @@ /** * This needs to be here to be used. * - * @var null + * @var string */ - protected $commandPrefix = null; + protected $commandPrefix = ''; /** * Annotate this command as being dangerous for any potential unintended consequences. diff --git a/src/app/Console/Commands/Data/Import/LdifCommand.php b/src/app/Console/Commands/Data/Import/LdifCommand.php --- a/src/app/Console/Commands/Data/Import/LdifCommand.php +++ b/src/app/Console/Commands/Data/Import/LdifCommand.php @@ -121,6 +121,7 @@ $lastAttr = null; $insertFunc = function ($limit = 0) use (&$entry, &$inserts) { + // @phpstan-ignore-next-line if (!empty($entry)) { if ($entry = $this->parseLDAPEntry($entry)) { $inserts[] = $entry; @@ -342,7 +343,7 @@ $resource = new \App\Resource(); $resource->name = $data->name; - $resource->domain = $data->domain; + $resource->domainName = $data->domain; $resource->save(); $resource->assignToWallet($this->wallet); @@ -396,7 +397,7 @@ $folder = new \App\SharedFolder(); $folder->name = $data->name; $folder->type = $data->type ?? 'mail'; - $folder->domain = $data->domain; + $folder->domainName = $data->domain; $folder->save(); $folder->assignToWallet($this->wallet); @@ -410,6 +411,11 @@ if (!empty($data->folder)) { $folder->setSetting('folder', $data->folder); } + + // Import aliases + if (!empty($data->aliases)) { + $this->setObjectAliases($folder, $data->aliases); + } } $bar->finish(); @@ -431,7 +437,7 @@ // Import aliases of the owner, we got from importOwner() call if (!empty($this->aliases) && $this->wallet) { - $this->setUserAliases($this->wallet->owner, $this->aliases); + $this->setObjectAliases($this->wallet->owner, $this->aliases); } $bar = $this->createProgressBar($users->count(), "Importing users"); @@ -537,7 +543,7 @@ // domain records yet, save the aliases to be inserted later (in importUsers()) $this->aliases = $data->aliases; } else { - $this->setUserAliases($user, $data->aliases); + $this->setObjectAliases($user, $data->aliases); } } @@ -706,6 +712,10 @@ if (!empty($entry['acl'])) { $result['acl'] = $this->parseACL($this->attrArrayValue($entry, 'acl')); } + + if (!empty($entry['alias'])) { + $result['aliases'] = $this->attrArrayValue($entry, 'alias'); + } } return [$result, $error]; @@ -957,14 +967,14 @@ } /** - * Set aliases for the user + * Set aliases for for an object */ - protected function setUserAliases(\App\User $user, array $aliases = []) + protected function setObjectAliases($object, array $aliases = []) { if (!empty($aliases)) { // Some users might have alias entry with their main address, remove it $aliases = array_map('strtolower', $aliases); - $aliases = array_diff(array_unique($aliases), [$user->email]); + $aliases = array_diff(array_unique($aliases), [$object->email]); // Remove aliases for domains that do not exist if (!empty($aliases)) { @@ -977,7 +987,7 @@ } if (!empty($aliases)) { - $user->setAliases($aliases); + $object->setAliases($aliases); } } } diff --git a/src/app/Console/Commands/SharedFolder/AddAliasCommand.php b/src/app/Console/Commands/SharedFolder/AddAliasCommand.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/SharedFolder/AddAliasCommand.php @@ -0,0 +1,58 @@ +getSharedFolder($this->argument('folder')); + + if (!$folder) { + $this->error("Folder not found."); + return 1; + } + + $alias = \strtolower($this->argument('alias')); + + // Check if the alias already exists + if ($folder->aliases()->where('alias', $alias)->first()) { + $this->error("Address is already assigned to the folder."); + return 1; + } + + // Validate the alias + $error = UsersController::validateAlias($alias, $folder->walletOwner()); + + if ($error) { + if (!$this->option('force')) { + $this->error($error); + return 1; + } + } + + $folder->aliases()->create(['alias' => $alias]); + } +} diff --git a/src/app/Console/Commands/User/AliasesCommand.php b/src/app/Console/Commands/SharedFolder/AliasesCommand.php copy from src/app/Console/Commands/User/AliasesCommand.php copy to src/app/Console/Commands/SharedFolder/AliasesCommand.php --- a/src/app/Console/Commands/User/AliasesCommand.php +++ b/src/app/Console/Commands/SharedFolder/AliasesCommand.php @@ -1,6 +1,6 @@ getUser($this->argument('user')); + $folder = $this->getSharedFolder($this->argument('folder')); - if (!$user) { - $this->error("User not found."); + if (!$folder) { + $this->error("Folder not found."); return 1; } - foreach ($user->aliases as $alias) { - $this->info("{$alias->alias}"); + foreach ($folder->aliases()->pluck('alias')->all() as $alias) { + $this->info($alias); } } } diff --git a/src/app/Console/Commands/SharedFolder/CreateCommand.php b/src/app/Console/Commands/SharedFolder/CreateCommand.php --- a/src/app/Console/Commands/SharedFolder/CreateCommand.php +++ b/src/app/Console/Commands/SharedFolder/CreateCommand.php @@ -79,7 +79,7 @@ $folder = new SharedFolder(); $folder->name = $name; $folder->type = $type; - $folder->domain = $domainName; + $folder->domainName = $domainName; $folder->save(); $folder->assignToWallet($owner->wallets->first()); diff --git a/src/app/Console/Commands/SharedFolderAliasesCommand.php b/src/app/Console/Commands/SharedFolderAliasesCommand.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/SharedFolderAliasesCommand.php @@ -0,0 +1,13 @@ +aliases as $alias) { - $this->info("{$alias->alias}"); + foreach ($user->aliases()->pluck('alias')->all() as $alias) { + $this->info($alias); } } } diff --git a/src/app/Console/Kernel.php b/src/app/Console/Kernel.php --- a/src/app/Console/Kernel.php +++ b/src/app/Console/Kernel.php @@ -7,15 +7,6 @@ class Kernel extends ConsoleKernel { - /** - * The Artisan commands provided by your application. - * - * @var array - */ - protected $commands = [ - // - ]; - /** * Define the application's command schedule. * diff --git a/src/app/Console/ObjectCommand.php b/src/app/Console/ObjectCommand.php --- a/src/app/Console/ObjectCommand.php +++ b/src/app/Console/ObjectCommand.php @@ -13,7 +13,7 @@ * * @var string */ - protected $commandPrefix = null; + protected $commandPrefix = ''; /** * The object class that we are operating on, for example \App\User::class diff --git a/src/app/Console/ObjectCreateCommand.php b/src/app/Console/ObjectCreateCommand.php --- a/src/app/Console/ObjectCreateCommand.php +++ b/src/app/Console/ObjectCreateCommand.php @@ -61,7 +61,5 @@ } else { $this->error("Object could not be created."); } - - return $object; } } diff --git a/src/app/Discount.php b/src/app/Discount.php --- a/src/app/Discount.php +++ b/src/app/Discount.php @@ -26,17 +26,12 @@ 'discount' => 'integer', ]; - protected $fillable = [ - 'active', - 'code', - 'description', - 'discount', - ]; + /** @var array The attributes that are mass assignable */ + protected $fillable = ['active', 'code', 'description', 'discount']; + + /** @var array Translatable properties */ + public $translatable = ['description']; - /** @var array Translatable properties */ - public $translatable = [ - 'description', - ]; /** * Discount value mutator @@ -67,6 +62,6 @@ */ public function wallets() { - return $this->hasMany('App\Wallet'); + return $this->hasMany(Wallet::class); } } diff --git a/src/app/Domain.php b/src/app/Domain.php --- a/src/app/Domain.php +++ b/src/app/Domain.php @@ -2,7 +2,6 @@ namespace App; -use App\Wallet; use App\Traits\BelongsToTenantTrait; use App\Traits\DomainConfigTrait; use App\Traits\EntitleableTrait; @@ -56,12 +55,16 @@ public const HASH_TEXT = 2; public const HASH_CNAME = 3; - protected $fillable = [ - 'namespace', - 'status', - 'type' + /** @var array The attributes that should be cast */ + protected $casts = [ + 'created_at' => 'datetime:Y-m-d H:i:s', + 'deleted_at' => 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', ]; + /** @var array The attributes that are mass assignable */ + protected $fillable = ['namespace', 'status', 'type']; + /** * Assign a package to a domain. The domain should not belong to any existing entitlements. * @@ -97,8 +100,8 @@ public static function getPublicDomains(): array { return self::withEnvTenantContext() - ->whereRaw(sprintf('(type & %s)', Domain::TYPE_PUBLIC)) - ->get(['namespace'])->pluck('namespace')->toArray(); + ->where('type', '&', Domain::TYPE_PUBLIC) + ->pluck('namespace')->all(); } /** @@ -315,11 +318,11 @@ $suffixLen = strlen($suffix); return !( - \App\User::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists() - || \App\UserAlias::whereRaw('substr(alias, ?) = ?', [-$suffixLen, $suffix])->exists() - || \App\Group::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists() - || \App\Resource::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists() - || \App\SharedFolder::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists() + User::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists() + || UserAlias::whereRaw('substr(alias, ?) = ?', [-$suffixLen, $suffix])->exists() + || Group::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists() + || Resource::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists() + || SharedFolder::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists() ); } @@ -370,7 +373,7 @@ return []; } - $mailboxSKU = \App\Sku::withObjectTenantContext($this)->where('title', 'mailbox')->first(); + $mailboxSKU = Sku::withObjectTenantContext($this)->where('title', 'mailbox')->first(); if (!$mailboxSKU) { \Log::error("No mailbox SKU available."); @@ -378,7 +381,7 @@ } return $wallet->entitlements() - ->where('entitleable_type', \App\User::class) + ->where('entitleable_type', User::class) ->where('sku_id', $mailboxSKU->id) ->get() ->pluck('entitleable') diff --git a/src/app/DomainSetting.php b/src/app/DomainSetting.php --- a/src/app/DomainSetting.php +++ b/src/app/DomainSetting.php @@ -14,9 +14,8 @@ */ class DomainSetting extends Model { - protected $fillable = [ - 'domain_id', 'key', 'value' - ]; + /** @var array The attributes that are mass assignable */ + protected $fillable = ['domain_id', 'key', 'value']; /** * The domain to which this setting belongs. @@ -26,7 +25,7 @@ public function domain() { return $this->belongsTo( - '\App\Domain', + Domain::class, 'domain_id', /* local */ 'id' /* remote */ ); diff --git a/src/app/Entitlement.php b/src/app/Entitlement.php --- a/src/app/Entitlement.php +++ b/src/app/Entitlement.php @@ -2,9 +2,9 @@ namespace App; +use App\Traits\UuidStrKeyTrait; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; -use App\Traits\UuidStrKeyTrait; /** * The eloquent definition of an Entitlement. @@ -29,11 +29,7 @@ use SoftDeletes; use UuidStrKeyTrait; - /** - * The fillable columns for this Entitlement - * - * @var array - */ + /** @var array The attributes that are mass assignable */ protected $fillable = [ 'sku_id', 'wallet_id', @@ -44,6 +40,7 @@ 'fee', ]; + /** @var array The attributes that should be cast */ protected $casts = [ 'cost' => 'integer', 'fee' => 'integer' @@ -80,10 +77,10 @@ */ public function createTransaction($type, $amount = null) { - $transaction = \App\Transaction::create( + $transaction = Transaction::create( [ 'object_id' => $this->id, - 'object_type' => \App\Entitlement::class, + 'object_type' => Entitlement::class, 'type' => $type, 'amount' => $amount ] @@ -110,7 +107,7 @@ */ public function entitleableTitle(): ?string { - if ($this->entitleable instanceof \App\Domain) { + if ($this->entitleable instanceof Domain) { return $this->entitleable->namespace; } @@ -151,7 +148,7 @@ */ public function sku() { - return $this->belongsTo('App\Sku'); + return $this->belongsTo(Sku::class); } /** @@ -161,7 +158,7 @@ */ public function wallet() { - return $this->belongsTo('App\Wallet'); + return $this->belongsTo(Wallet::class); } /** diff --git a/src/app/Exceptions/Handler.php b/src/app/Exceptions/Handler.php --- a/src/app/Exceptions/Handler.php +++ b/src/app/Exceptions/Handler.php @@ -2,61 +2,37 @@ namespace App\Exceptions; -use Exception; use Illuminate\Auth\AuthenticationException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Support\Facades\DB; class Handler extends ExceptionHandler { - /** - * A list of the exception types that are not reported. - * - * @var array - */ + /** @var string[] A list of the exception types that are not reported */ protected $dontReport = [ \Laravel\Passport\Exceptions\OAuthServerException::class, \League\OAuth2\Server\Exception\OAuthServerException::class ]; - /** - * A list of the inputs that are never flashed for validation exceptions. - * - * @var array - */ + /** @var string[] A list of the inputs that are never flashed for validation exceptions */ protected $dontFlash = [ + 'current_password', 'password', 'password_confirmation', ]; - /** - * Report or log an exception. - * - * @param \Exception $exception - * - * @return void - */ - public function report(Exception $exception) - { - parent::report($exception); - } /** - * Render an exception into an HTTP response. - * - * @param \Illuminate\Http\Request $request - * @param \Exception $exception - * - * @return \Symfony\Component\HttpFoundation\Response + * Register the exception handling callbacks for the application. */ - public function render($request, Exception $exception) + public function register() { - // Rollback uncommitted transactions - while (DB::transactionLevel() > 0) { - DB::rollBack(); - } - - return parent::render($request, $exception); + $this->reportable(function (\Throwable $e) { + // Rollback uncommitted transactions + while (DB::transactionLevel() > 0) { + DB::rollBack(); + } + }); } /** diff --git a/src/app/Group.php b/src/app/Group.php --- a/src/app/Group.php +++ b/src/app/Group.php @@ -4,11 +4,11 @@ use App\Traits\BelongsToTenantTrait; use App\Traits\EntitleableTrait; +use App\Traits\EmailPropertyTrait; use App\Traits\GroupConfigTrait; use App\Traits\SettingsTrait; use App\Traits\StatusPropertyTrait; use App\Traits\UuidIntKeyTrait; -use App\Wallet; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -26,6 +26,7 @@ { use BelongsToTenantTrait; use EntitleableTrait; + use EmailPropertyTrait; use GroupConfigTrait; use SettingsTrait; use SoftDeletes; @@ -43,6 +44,14 @@ // group has been created in LDAP public const STATUS_LDAP_READY = 1 << 4; + /** @var array The attributes that should be cast */ + protected $casts = [ + 'created_at' => 'datetime:Y-m-d H:i:s', + 'deleted_at' => 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', + ]; + + /** @var array The attributes that are mass assignable */ protected $fillable = [ 'email', 'members', @@ -51,43 +60,6 @@ ]; - /** - * Returns group domain. - * - * @return ?\App\Domain The domain group belongs to, NULL if it does not exist - */ - public function domain(): ?Domain - { - list($local, $domainName) = explode('@', $this->email); - - return Domain::where('namespace', $domainName)->first(); - } - - /** - * Find whether an email address exists as a group (including deleted groups). - * - * @param string $email Email address - * @param bool $return_group Return Group instance instead of boolean - * - * @return \App\Group|bool True or Group model object if found, False otherwise - */ - public static function emailExists(string $email, bool $return_group = false) - { - if (strpos($email, '@') === false) { - return false; - } - - $email = \strtolower($email); - - $group = self::withTrashed()->where('email', $email)->first(); - - if ($group) { - return $return_group ? $group : true; - } - - return false; - } - /** * Group members propert accessor. Converts internal comma-separated list into an array * @@ -100,16 +72,6 @@ return $members ? explode(',', $members) : []; } - /** - * Ensure the email is appropriately cased. - * - * @param string $email Group email address - */ - public function setEmailAttribute(string $email) - { - $this->attributes['email'] = strtolower($email); - } - /** * Ensure the members are appropriately formatted. * diff --git a/src/app/GroupSetting.php b/src/app/GroupSetting.php --- a/src/app/GroupSetting.php +++ b/src/app/GroupSetting.php @@ -14,9 +14,8 @@ */ class GroupSetting extends Model { - protected $fillable = [ - 'group_id', 'key', 'value' - ]; + /** @var array The attributes that are mass assignable */ + protected $fillable = ['group_id', 'key', 'value']; /** * The group to which this setting belongs. @@ -25,6 +24,6 @@ */ public function group() { - return $this->belongsTo(\App\Group::class, 'group_id', 'id'); + return $this->belongsTo(Group::class, 'group_id', 'id'); } } diff --git a/src/app/Http/Controllers/API/AuthController.php b/src/app/Http/Controllers/API/AuthController.php --- a/src/app/Http/Controllers/API/AuthController.php +++ b/src/app/Http/Controllers/API/AuthController.php @@ -20,12 +20,13 @@ public function info() { $user = Auth::guard()->user(); - $response = V4\UsersController::userResponse($user); if (!empty(request()->input('refresh'))) { - return $this->refreshAndRespond(request(), $response); + return $this->refreshAndRespond(request(), $user); } + $response = V4\UsersController::userResponse($user); + return response()->json($response); } @@ -51,10 +52,7 @@ $tokenResponse = app()->handle($proxyRequest); - $response = V4\UsersController::userResponse($user); - $response['status'] = 'success'; - - return self::respondWithToken($tokenResponse, $response); + return self::respondWithToken($tokenResponse, $user); } /** @@ -104,6 +102,7 @@ // Revoke all of the token's refresh tokens... $refreshTokenRepository->revokeRefreshTokensByAccessTokenId($tokenId); + return response()->json([ 'status' => 'success', 'message' => \trans('auth.logoutsuccess') @@ -124,11 +123,11 @@ * Refresh the token and respond with it. * * @param \Illuminate\Http\Request $request The API request. - * @param array $response Additional response data + * @param ?\App\User $user The user being authenticated * * @return \Illuminate\Http\JsonResponse */ - protected static function refreshAndRespond(Request $request, array $response = []) + protected static function refreshAndRespond(Request $request, $user = null) { $proxyRequest = Request::create('/oauth/token', 'POST', [ 'grant_type' => 'refresh_token', @@ -139,18 +138,18 @@ $tokenResponse = app()->handle($proxyRequest); - return self::respondWithToken($tokenResponse, $response); + return self::respondWithToken($tokenResponse, $user); } /** * Get the token array structure. * * @param \Illuminate\Http\JsonResponse $tokenResponse The response containing the token. - * @param array $response Additional response data + * @param ?\App\User $user The user being authenticated * * @return \Illuminate\Http\JsonResponse */ - protected static function respondWithToken($tokenResponse, array $response = []) + protected static function respondWithToken($tokenResponse, $user = null) { $data = json_decode($tokenResponse->getContent()); @@ -163,6 +162,13 @@ return response()->json(['status' => 'error', 'message' => \trans('auth.failed')], 401); } + if ($user) { + $response = V4\UsersController::userResponse($user); + } else { + $response = []; + } + + $response['status'] = 'success'; $response['access_token'] = $data->access_token; $response['refresh_token'] = $data->refresh_token; $response['token_type'] = 'bearer'; diff --git a/src/app/Http/Controllers/API/PasswordResetController.php b/src/app/Http/Controllers/API/PasswordResetController.php --- a/src/app/Http/Controllers/API/PasswordResetController.php +++ b/src/app/Http/Controllers/API/PasswordResetController.php @@ -16,10 +16,6 @@ */ class PasswordResetController extends Controller { - /** @var \App\VerificationCode A verification code object */ - protected $code; - - /** * Sends password reset code to the user's external email * @@ -98,7 +94,7 @@ // For last-step remember the code object, so we can delete it // with single SQL query (->delete()) instead of two (::destroy()) - $this->code = $code; + $request->code = $code; return response()->json([ 'status' => 'success', @@ -121,7 +117,7 @@ return $v; } - $user = $this->code->user; + $user = $request->code->user; // Validate the password $v = Validator::make( @@ -138,7 +134,7 @@ $user->save(); // Remove the verification code - $this->code->delete(); + $request->code->delete(); return AuthController::logonResponse($user, $request->password); } diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php --- a/src/app/Http/Controllers/API/SignupController.php +++ b/src/app/Http/Controllers/API/SignupController.php @@ -25,13 +25,6 @@ */ class SignupController extends Controller { - /** @var ?\App\SignupCode A verification code object */ - protected $code; - - /** @var ?\App\Plan Signup plan object */ - protected $plan; - - /** * Returns plans definitions for signup. * @@ -177,7 +170,7 @@ // For signup last-step mode remember the code object, so we can delete it // with single SQL query (->delete()) instead of two (::destroy()) - $this->code = $code; + $request->code = $code; $has_domain = $this->getPlan()->hasDomain(); @@ -328,8 +321,8 @@ } // Remove the verification code - if ($this->code) { - $this->code->delete(); + if ($request->code) { + $request->code->delete(); } DB::commit(); @@ -344,10 +337,12 @@ */ protected function getPlan() { - if (!$this->plan) { + $request = request(); + + if (!$request->plan || !$request->plan instanceof Plan) { // Get the plan if specified and exists... - if ($this->code && $this->code->plan) { - $plan = Plan::withEnvTenantContext()->where('title', $this->code->plan)->first(); + if ($request->code && $request->code->plan) { + $plan = Plan::withEnvTenantContext()->where('title', $request->code->plan)->first(); } // ...otherwise use the default plan @@ -356,10 +351,10 @@ $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first(); } - $this->plan = $plan; + $request->plan = $plan; } - return $this->plan; + return $request->plan; } /** diff --git a/src/app/Http/Controllers/API/V4/Admin/UsersController.php b/src/app/Http/Controllers/API/V4/Admin/UsersController.php --- a/src/app/Http/Controllers/API/V4/Admin/UsersController.php +++ b/src/app/Http/Controllers/API/V4/Admin/UsersController.php @@ -58,13 +58,15 @@ $user_ids = $user_ids->merge($ext_user_ids)->unique(); - // Search by a distribution list or resource email + // Search by an email of a group, resource, shared folder, etc. if ($group = \App\Group::withTrashed()->where('email', $search)->first()) { $user_ids = $user_ids->merge([$group->wallet()->user_id])->unique(); } elseif ($resource = \App\Resource::withTrashed()->where('email', $search)->first()) { $user_ids = $user_ids->merge([$resource->wallet()->user_id])->unique(); } elseif ($folder = \App\SharedFolder::withTrashed()->where('email', $search)->first()) { $user_ids = $user_ids->merge([$folder->wallet()->user_id])->unique(); + } elseif ($alias = \App\SharedFolderAlias::where('alias', $search)->first()) { + $user_ids = $user_ids->merge([$alias->sharedFolder->wallet()->user_id])->unique(); } if (!$user_ids->isEmpty()) { @@ -213,6 +215,8 @@ } $user->assignSku($sku); + + /** @var \App\Entitlement $entitlement */ $entitlement = $user->entitlements()->where('sku_id', $sku->id)->first(); return response()->json([ diff --git a/src/app/Http/Controllers/API/V4/AuthAttemptsController.php b/src/app/Http/Controllers/API/V4/AuthAttemptsController.php --- a/src/app/Http/Controllers/API/V4/AuthAttemptsController.php +++ b/src/app/Http/Controllers/API/V4/AuthAttemptsController.php @@ -9,7 +9,6 @@ class AuthAttemptsController extends Controller { - /** * Confirm the authentication attempt. * diff --git a/src/app/Http/Controllers/API/V4/Reseller/UsersController.php b/src/app/Http/Controllers/API/V4/Reseller/UsersController.php --- a/src/app/Http/Controllers/API/V4/Reseller/UsersController.php +++ b/src/app/Http/Controllers/API/V4/Reseller/UsersController.php @@ -50,13 +50,15 @@ $user_ids = $user_ids->merge($ext_user_ids)->unique(); - // Search by a distribution list email + // Search by an email of a group, resource, shared folder, etc. if ($group = Group::withTrashed()->where('email', $search)->first()) { $user_ids = $user_ids->merge([$group->wallet()->user_id])->unique(); } elseif ($resource = \App\Resource::withTrashed()->where('email', $search)->first()) { $user_ids = $user_ids->merge([$resource->wallet()->user_id])->unique(); } elseif ($folder = \App\SharedFolder::withTrashed()->where('email', $search)->first()) { $user_ids = $user_ids->merge([$folder->wallet()->user_id])->unique(); + } elseif ($alias = \App\SharedFolderAlias::where('alias', $search)->first()) { + $user_ids = $user_ids->merge([$alias->sharedFolder->wallet()->user_id])->unique(); } if (!$user_ids->isEmpty()) { diff --git a/src/app/Http/Controllers/API/V4/ResourcesController.php b/src/app/Http/Controllers/API/V4/ResourcesController.php --- a/src/app/Http/Controllers/API/V4/ResourcesController.php +++ b/src/app/Http/Controllers/API/V4/ResourcesController.php @@ -74,7 +74,7 @@ // Create the resource $resource = new Resource(); $resource->name = request()->input('name'); - $resource->domain = $domain; + $resource->domainName = $domain; $resource->save(); $resource->assignToWallet($owner->wallets->first()); diff --git a/src/app/Http/Controllers/API/V4/SharedFoldersController.php b/src/app/Http/Controllers/API/V4/SharedFoldersController.php --- a/src/app/Http/Controllers/API/V4/SharedFoldersController.php +++ b/src/app/Http/Controllers/API/V4/SharedFoldersController.php @@ -9,6 +9,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; +use Illuminate\Support\Str; class SharedFoldersController extends RelationController { @@ -54,34 +55,29 @@ public function store(Request $request) { $current_user = $this->guard()->user(); - $owner = $current_user->wallet()->owner; + $owner = $current_user->walletOwner(); - if ($owner->id != $current_user->id) { + if (empty($owner) || $owner->id != $current_user->id) { return $this->errorResponse(403); } - $domain = request()->input('domain'); - - $rules = [ - 'name' => ['required', 'string', new SharedFolderName($owner, $domain)], - 'type' => ['required', 'string', new SharedFolderType()] - ]; - - $v = Validator::make($request->all(), $rules); - - if ($v->fails()) { - return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); + if ($error_response = $this->validateFolderRequest($request, null, $owner)) { + return $error_response; } DB::beginTransaction(); // Create the shared folder $folder = new SharedFolder(); - $folder->name = request()->input('name'); - $folder->type = request()->input('type'); - $folder->domain = $domain; + $folder->name = $request->input('name'); + $folder->type = $request->input('type'); + $folder->domainName = $request->input('domain'); $folder->save(); + if (!empty($request->aliases) && $folder->type === 'mail') { + $folder->setAliases($request->aliases); + } + $folder->assignToWallet($owner->wallets->first()); DB::commit(); @@ -114,30 +110,25 @@ return $this->errorResponse(403); } - $owner = $folder->wallet()->owner; + if ($error_response = $this->validateFolderRequest($request, $folder, $folder->walletOwner())) { + return $error_response; + } $name = $request->input('name'); - $errors = []; - // Validate the folder name - if ($name !== null && $name != $folder->name) { - $domainName = explode('@', $folder->email, 2)[1]; - $rules = ['name' => ['required', 'string', new SharedFolderName($owner, $domainName)]]; - - $v = Validator::make($request->all(), $rules); + DB::beginTransaction(); - if ($v->fails()) { - $errors = $v->errors()->toArray(); - } else { - $folder->name = $name; - } + if ($name && $name != $folder->name) { + $folder->name = $name; } - if (!empty($errors)) { - return response()->json(['status' => 'error', 'errors' => $errors], 422); + $folder->save(); + + if (isset($request->aliases) && $folder->type === 'mail') { + $folder->setAliases($request->aliases); } - $folder->save(); + DB::commit(); return response()->json([ 'status' => 'success', @@ -194,4 +185,78 @@ return false; } + + /** + * Validate shared folder input + * + * @param \Illuminate\Http\Request $request The API request. + * @param \App\SharedFolder|null $folder Shared folder + * @param \App\User|null $owner Account owner + * + * @return \Illuminate\Http\JsonResponse|null The error response on error + */ + protected function validateFolderRequest(Request $request, $folder, $owner) + { + $errors = []; + + if (empty($folder)) { + $domain = $request->input('domain'); + $rules = [ + 'name' => ['required', 'string', new SharedFolderName($owner, $domain)], + 'type' => ['required', 'string', new SharedFolderType()], + ]; + } else { + // On update validate the folder name (if changed) + $name = $request->input('name'); + if ($name !== null && $name != $folder->name) { + $domain = explode('@', $folder->email, 2)[1]; + $rules = ['name' => ['required', 'string', new SharedFolderName($owner, $domain)]]; + } + } + + if (!empty($rules)) { + $v = Validator::make($request->all(), $rules); + + if ($v->fails()) { + $errors = $v->errors()->toArray(); + } + } + + // Validate aliases input + if (isset($request->aliases)) { + $aliases = []; + $existing_aliases = $owner->aliases()->get()->pluck('alias')->toArray(); + + foreach ($request->aliases as $idx => $alias) { + if (is_string($alias) && !empty($alias)) { + // Alias cannot be the same as the email address + if (!empty($folder) && Str::lower($alias) == Str::lower($folder->email)) { + continue; + } + + // validate new aliases + if ( + !in_array($alias, $existing_aliases) + && ($error = UsersController::validateAlias($alias, $owner)) + ) { + if (!isset($errors['aliases'])) { + $errors['aliases'] = []; + } + $errors['aliases'][$idx] = $error; + continue; + } + + $aliases[] = $alias; + } + } + + $request->aliases = $aliases; + } + + if (!empty($errors)) { + return response()->json(['status' => 'error', 'errors' => $errors], 422); + } + + return null; + } } diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php --- a/src/app/Http/Controllers/API/V4/UsersController.php +++ b/src/app/Http/Controllers/API/V4/UsersController.php @@ -4,7 +4,6 @@ use App\Http\Controllers\RelationController; use App\Domain; -use App\Group; use App\Rules\Password; use App\Rules\UserEmailDomain; use App\Rules\UserEmailLocal; @@ -133,6 +132,7 @@ $response['skus'] = \App\Entitlement::objectEntitlementsSummary($user); $response['config'] = $user->getConfig(); + $response['aliases'] = $user->aliases()->pluck('alias')->all(); $code = $user->verificationcodes()->where('active', true) ->where('expires_at', '>', \Carbon\Carbon::now()) @@ -207,7 +207,7 @@ public function store(Request $request) { $current_user = $this->guard()->user(); - $owner = $current_user->wallet()->owner; + $owner = $current_user->walletOwner(); if ($owner->id != $current_user->id) { return $this->errorResponse(403); @@ -394,12 +394,6 @@ $response['settings'][$item->key] = $item->value; } - // Aliases - $response['aliases'] = []; - foreach ($user->aliases as $item) { - $response['aliases'][] = $item->alias; - } - // Status info $response['statusInfo'] = self::statusInfo($user); @@ -652,33 +646,19 @@ } // Check if it is one of domains available to the user - if (!$user->domains()->where('namespace', $domain->namespace)->exists()) { + if (!$domain->isPublic() && $user->id != $domain->walletOwner()->id) { return \trans('validation.entryexists', ['attribute' => 'domain']); } - // Check if a user with specified address already exists - if ($existing_user = User::emailExists($email, true)) { - // If this is a deleted user in the same custom domain - // we'll force delete him before - if (!$domain->isPublic() && $existing_user->trashed()) { - $deleted = $existing_user; - } else { - return \trans('validation.entryexists', ['attribute' => 'email']); - } - } - - // Check if an alias with specified address already exists. - if (User::aliasExists($email)) { - return \trans('validation.entryexists', ['attribute' => 'email']); - } - - // Check if a group or resource with specified address already exists + // Check if a user/group/resource/shared folder with specified address already exists if ( - ($existing = Group::emailExists($email, true)) + ($existing = User::emailExists($email, true)) + || ($existing = \App\Group::emailExists($email, true)) || ($existing = \App\Resource::emailExists($email, true)) + || ($existing = \App\SharedFolder::emailExists($email, true)) ) { - // If this is a deleted group/resource in the same custom domain - // we'll force delete it before + // If this is a deleted user/group/resource/folder in the same custom domain + // we'll force delete it before creating the target user if (!$domain->isPublic() && $existing->trashed()) { $deleted = $existing; } else { @@ -686,6 +666,11 @@ } } + // Check if an alias with specified address already exists. + if (User::aliasExists($email) || \App\SharedFolder::aliasExists($email)) { + return \trans('validation.entryexists', ['attribute' => 'email']); + } + return null; } @@ -727,7 +712,7 @@ } // Check if it is one of domains available to the user - if (!$user->domains()->where('namespace', $domain->namespace)->exists()) { + if (!$domain->isPublic() && $user->id != $domain->walletOwner()->id) { return \trans('validation.entryexists', ['attribute' => 'domain']); } @@ -739,8 +724,17 @@ } } + // Check if a group/resource/shared folder with specified address already exists + if ( + \App\Group::emailExists($email) + || \App\Resource::emailExists($email) + || \App\SharedFolder::emailExists($email) + ) { + return \trans('validation.entryexists', ['attribute' => 'alias']); + } + // Check if an alias with specified address already exists - if (User::aliasExists($email)) { + if (User::aliasExists($email) || \App\SharedFolder::aliasExists($email)) { // Allow assigning the same alias to a user in the same group account, // but only for non-public domains if ($domain->isPublic()) { @@ -748,11 +742,6 @@ } } - // Check if a group with specified address already exists - if (Group::emailExists($email)) { - return \trans('validation.entryexists', ['attribute' => 'alias']); - } - return null; } diff --git a/src/app/Http/Controllers/API/V4/WalletsController.php b/src/app/Http/Controllers/API/V4/WalletsController.php --- a/src/app/Http/Controllers/API/V4/WalletsController.php +++ b/src/app/Http/Controllers/API/V4/WalletsController.php @@ -161,6 +161,7 @@ // Get sub-transactions for the specified transaction ID, first // check access rights to the transaction's wallet + /** @var ?\App\Transaction $transaction */ $transaction = $wallet->transactions()->where('id', $transaction)->first(); if (!$transaction) { diff --git a/src/app/Http/Controllers/RelationController.php b/src/app/Http/Controllers/RelationController.php --- a/src/app/Http/Controllers/RelationController.php +++ b/src/app/Http/Controllers/RelationController.php @@ -308,6 +308,10 @@ $response['config'] = $resource->getConfig(); } + if (method_exists($resource, 'aliases')) { + $response['aliases'] = $resource->aliases()->pluck('alias')->all(); + } + return response()->json($response); } diff --git a/src/app/Http/Kernel.php b/src/app/Http/Kernel.php --- a/src/app/Http/Kernel.php +++ b/src/app/Http/Kernel.php @@ -11,12 +11,13 @@ * * These middleware are run during every request to your application. * - * @var array + * @var array */ protected $middleware = [ + // \App\Http\Middleware\TrustHosts::class, \App\Http\Middleware\RequestLogger::class, \App\Http\Middleware\TrustProxies::class, - \App\Http\Middleware\CheckForMaintenanceMode::class, + \App\Http\Middleware\PreventRequestsDuringMaintenance::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, @@ -31,7 +32,7 @@ /** * The application's route middleware groups. * - * @var array + * @var array> */ protected $middlewareGroups = [ 'web' => [ @@ -45,8 +46,8 @@ ], 'api' => [ - //'throttle:120,1', - 'bindings', + // 'throttle:api', + \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ]; @@ -55,13 +56,12 @@ * * These middleware may be assigned to groups or used individually. * - * @var array + * @var array */ protected $routeMiddleware = [ 'admin' => \App\Http\Middleware\AuthenticateAdmin::class, 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, - 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, @@ -71,25 +71,6 @@ 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, ]; - /** - * The priority-sorted list of middleware. - * - * This forces non-global middleware to always be in the given order. - * - * @var array - */ - protected $middlewarePriority = [ - \Illuminate\Session\Middleware\StartSession::class, - \Illuminate\View\Middleware\ShareErrorsFromSession::class, - \App\Http\Middleware\AuthenticateAdmin::class, - \App\Http\Middleware\AuthenticateReseller::class, - \App\Http\Middleware\Authenticate::class, - \Illuminate\Session\Middleware\AuthenticateSession::class, - \Illuminate\Routing\Middleware\SubstituteBindings::class, - \Illuminate\Auth\Middleware\Authorize::class, - \App\Http\Middleware\ContentSecurityPolicy::class, - ]; - /** * Handle an incoming HTTP request. * diff --git a/src/app/Http/Middleware/EncryptCookies.php b/src/app/Http/Middleware/EncryptCookies.php --- a/src/app/Http/Middleware/EncryptCookies.php +++ b/src/app/Http/Middleware/EncryptCookies.php @@ -9,7 +9,7 @@ /** * The names of the cookies that should not be encrypted. * - * @var array + * @var array */ protected $except = [ // diff --git a/src/app/Http/Middleware/CheckForMaintenanceMode.php b/src/app/Http/Middleware/PreventRequestsDuringMaintenance.php rename from src/app/Http/Middleware/CheckForMaintenanceMode.php rename to src/app/Http/Middleware/PreventRequestsDuringMaintenance.php --- a/src/app/Http/Middleware/CheckForMaintenanceMode.php +++ b/src/app/Http/Middleware/PreventRequestsDuringMaintenance.php @@ -2,14 +2,14 @@ namespace App\Http\Middleware; -use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Middleware; +use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware; -class CheckForMaintenanceMode extends Middleware +class PreventRequestsDuringMaintenance extends Middleware { /** * The URIs that should be reachable while maintenance mode is enabled. * - * @var array + * @var array */ protected $except = [ // diff --git a/src/app/Http/Middleware/RedirectIfAuthenticated.php b/src/app/Http/Middleware/RedirectIfAuthenticated.php --- a/src/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/src/app/Http/Middleware/RedirectIfAuthenticated.php @@ -3,6 +3,7 @@ namespace App\Http\Middleware; use Closure; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class RedirectIfAuthenticated @@ -12,14 +13,18 @@ * * @param \Illuminate\Http\Request $request * @param \Closure $next - * @param string|null $guard + * @param string|null ...$guards * * @return mixed */ - public function handle($request, Closure $next, $guard = null) + public function handle(Request $request, Closure $next, ...$guards) { - if (Auth::guard($guard)->check()) { - return redirect('/dashboard'); + $guards = empty($guards) ? [null] : $guards; + + foreach ($guards as $guard) { + if (Auth::guard($guard)->check()) { + return redirect('/dashboard'); + } } return $next($request); diff --git a/src/app/Http/Middleware/TrustHosts.php b/src/app/Http/Middleware/TrustHosts.php new file mode 100644 --- /dev/null +++ b/src/app/Http/Middleware/TrustHosts.php @@ -0,0 +1,20 @@ + + */ + public function hosts() + { + return [ + $this->allSubdomainsOfApplicationUrl(), + ]; + } +} diff --git a/src/app/Http/Middleware/TrustProxies.php b/src/app/Http/Middleware/TrustProxies.php --- a/src/app/Http/Middleware/TrustProxies.php +++ b/src/app/Http/Middleware/TrustProxies.php @@ -2,15 +2,15 @@ namespace App\Http\Middleware; +use Illuminate\Http\Middleware\TrustProxies as Middleware; use Illuminate\Http\Request; -use Fideloper\Proxy\TrustProxies as Middleware; class TrustProxies extends Middleware { /** * The trusted proxies for this application. * - * @var array|string + * @var array|string|null */ protected $proxies = [ '10.0.0.0/8', @@ -24,5 +24,9 @@ * * @var int */ - protected $headers = Request::HEADER_X_FORWARDED_ALL; + protected $headers = Request::HEADER_X_FORWARDED_FOR | + Request::HEADER_X_FORWARDED_HOST | + Request::HEADER_X_FORWARDED_PORT | + Request::HEADER_X_FORWARDED_PROTO | + Request::HEADER_X_FORWARDED_AWS_ELB; } diff --git a/src/app/Http/Middleware/VerifyCsrfToken.php b/src/app/Http/Middleware/VerifyCsrfToken.php --- a/src/app/Http/Middleware/VerifyCsrfToken.php +++ b/src/app/Http/Middleware/VerifyCsrfToken.php @@ -6,17 +6,10 @@ class VerifyCsrfToken extends Middleware { - /** - * Indicates whether the XSRF-TOKEN cookie should be set on the response. - * - * @var bool - */ - protected $addHttpCookie = true; - /** * The URIs that should be excluded from CSRF verification. * - * @var array + * @var array */ protected $except = [ // diff --git a/src/app/IP4Net.php b/src/app/IP4Net.php --- a/src/app/IP4Net.php +++ b/src/app/IP4Net.php @@ -9,6 +9,7 @@ { protected $table = "ip4nets"; + /** @var array The attributes that are mass assignable */ protected $fillable = [ 'rir_name', 'net_number', diff --git a/src/app/IP6Net.php b/src/app/IP6Net.php --- a/src/app/IP6Net.php +++ b/src/app/IP6Net.php @@ -9,6 +9,7 @@ { protected $table = "ip6nets"; + /** @var array The attributes that are mass assignable */ protected $fillable = [ 'rir_name', 'net_number', diff --git a/src/app/Jobs/PaymentEmail.php b/src/app/Jobs/PaymentEmail.php --- a/src/app/Jobs/PaymentEmail.php +++ b/src/app/Jobs/PaymentEmail.php @@ -22,7 +22,7 @@ public $tries = 2; /** @var int The number of seconds to wait before retrying the job. */ - public $retryAfter = 10; + public $backoff = 10; /** @var bool Delete the job if the wallet no longer exist. */ public $deleteWhenMissingModels = true; @@ -30,7 +30,7 @@ /** @var \App\Payment A payment object */ protected $payment; - /** @var \App\User A wallet controller */ + /** @var ?\App\User A wallet controller */ protected $controller; diff --git a/src/app/Jobs/PaymentMandateDisabledEmail.php b/src/app/Jobs/PaymentMandateDisabledEmail.php --- a/src/app/Jobs/PaymentMandateDisabledEmail.php +++ b/src/app/Jobs/PaymentMandateDisabledEmail.php @@ -22,7 +22,7 @@ public $tries = 2; /** @var int The number of seconds to wait before retrying the job. */ - public $retryAfter = 10; + public $backoff = 10; /** @var bool Delete the job if the wallet no longer exist. */ public $deleteWhenMissingModels = true; @@ -30,7 +30,7 @@ /** @var \App\Wallet A wallet object */ protected $wallet; - /** @var \App\User A wallet controller */ + /** @var ?\App\User A wallet controller */ protected $controller; diff --git a/src/app/Jobs/SignupInvitationEmail.php b/src/app/Jobs/SignupInvitationEmail.php --- a/src/app/Jobs/SignupInvitationEmail.php +++ b/src/app/Jobs/SignupInvitationEmail.php @@ -24,7 +24,7 @@ public $deleteWhenMissingModels = true; /** @var int The number of seconds to wait before retrying the job. */ - public $retryAfter = 10; + public $backoff = 10; /** @var SignupInvitation Signup invitation object */ protected $invitation; diff --git a/src/app/Jobs/WalletCharge.php b/src/app/Jobs/WalletCharge.php --- a/src/app/Jobs/WalletCharge.php +++ b/src/app/Jobs/WalletCharge.php @@ -21,7 +21,7 @@ protected $wallet; /** @var int The number of seconds to wait before retrying the job. */ - public $retryAfter = 10; + public $backoff = 10; /** @var int How many times retry the job if it fails. */ public $tries = 5; diff --git a/src/app/Jobs/WalletCheck.php b/src/app/Jobs/WalletCheck.php --- a/src/app/Jobs/WalletCheck.php +++ b/src/app/Jobs/WalletCheck.php @@ -30,7 +30,7 @@ public const THRESHOLD_INITIAL = 'initial'; /** @var int The number of seconds to wait before retrying the job. */ - public $retryAfter = 10; + public $backoff = 10; /** @var int How many times retry the job if it fails. */ public $tries = 5; diff --git a/src/app/Observers/OpenVidu/ConnectionObserver.php b/src/app/Observers/OpenVidu/ConnectionObserver.php --- a/src/app/Observers/OpenVidu/ConnectionObserver.php +++ b/src/app/Observers/OpenVidu/ConnectionObserver.php @@ -34,7 +34,7 @@ foreach ($keys as $key => $type) { $newState = $connection->metadata[$key] ?? null; - $oldState = $this->getOriginal($connection, 'metadata')[$key] ?? null; + $oldState = $connection->getOriginal('metadata')[$key] ?? null; if ($newState !== $oldState) { $params[$key] = $type == 'bool' ? !empty($newState) : $newState; @@ -47,25 +47,4 @@ $connection->room->signal('connectionUpdate', $params); } } - - /** - * A wrapper to getOriginal() on an object - * - * @param \App\OpenVidu\Connection $connection The connection. - * @param string $property The property name - * - * @return mixed - */ - private function getOriginal($connection, $property) - { - $original = $connection->getOriginal($property); - - // The original value for a property is in a format stored in database - // I.e. for 'metadata' it is a JSON string instead of an array - if ($property == 'metadata') { - $original = json_decode($original, true); - } - - return $original; - } } diff --git a/src/app/Observers/ResourceObserver.php b/src/app/Observers/ResourceObserver.php --- a/src/app/Observers/ResourceObserver.php +++ b/src/app/Observers/ResourceObserver.php @@ -15,18 +15,6 @@ */ public function creating(Resource $resource): void { - if (empty($resource->email)) { - if (!isset($resource->domain)) { - throw new \Exception("Missing 'domain' property for a new resource"); - } - - $domainName = \strtolower($resource->domain); - - $resource->email = "resource-{$resource->id}@{$domainName}"; - } else { - $resource->email = \strtolower($resource->email); - } - $resource->status |= Resource::STATUS_NEW | Resource::STATUS_ACTIVE; } diff --git a/src/app/Observers/SharedFolderAliasObserver.php b/src/app/Observers/SharedFolderAliasObserver.php new file mode 100644 --- /dev/null +++ b/src/app/Observers/SharedFolderAliasObserver.php @@ -0,0 +1,82 @@ +alias = \strtolower($alias->alias); + + $domainName = explode('@', $alias->alias)[1]; + + $domain = Domain::where('namespace', $domainName)->first(); + + if (!$domain) { + \Log::error("Failed creating alias {$alias->alias}. Domain does not exist."); + return false; + } + + if ($alias->sharedFolder) { + if ($alias->sharedFolder->tenant_id != $domain->tenant_id) { + \Log::error("Reseller for folder '{$alias->sharedFolder->email}' and domain '{$domainName}' differ."); + return false; + } + } + + return true; + } + + /** + * Handle the shared folder alias "created" event. + * + * @param \App\SharedFolderAlias $alias Shared folder email alias + * + * @return void + */ + public function created(SharedFolderAlias $alias) + { + if ($alias->sharedFolder) { + \App\Jobs\SharedFolder\UpdateJob::dispatch($alias->shared_folder_id); + } + } + + /** + * Handle the shared folder alias "updated" event. + * + * @param \App\SharedFolderAlias $alias Shared folder email alias + * + * @return void + */ + public function updated(SharedFolderAlias $alias) + { + if ($alias->sharedFolder) { + \App\Jobs\SharedFolder\UpdateJob::dispatch($alias->shared_folder_id); + } + } + + /** + * Handle the shared folder alias "deleted" event. + * + * @param \App\SharedFolderAlias $alias Shared folder email alias + * + * @return void + */ + public function deleted(SharedFolderAlias $alias) + { + if ($alias->sharedFolder) { + \App\Jobs\SharedFolder\UpdateJob::dispatch($alias->shared_folder_id); + } + } +} diff --git a/src/app/Observers/SharedFolderObserver.php b/src/app/Observers/SharedFolderObserver.php --- a/src/app/Observers/SharedFolderObserver.php +++ b/src/app/Observers/SharedFolderObserver.php @@ -19,18 +19,6 @@ $folder->type = 'mail'; } - if (empty($folder->email)) { - if (!isset($folder->domain)) { - throw new \Exception("Missing 'domain' property for a new shared folder"); - } - - $domainName = \strtolower($folder->domain); - - $folder->email = "{$folder->type}-{$folder->id}@{$domainName}"; - } else { - $folder->email = \strtolower($folder->email); - } - $folder->status |= SharedFolder::STATUS_NEW | SharedFolder::STATUS_ACTIVE; } diff --git a/src/app/Observers/UserAliasObserver.php b/src/app/Observers/UserAliasObserver.php --- a/src/app/Observers/UserAliasObserver.php +++ b/src/app/Observers/UserAliasObserver.php @@ -12,8 +12,6 @@ /** * Handle the "creating" event on an alias * - * Ensures that there's no user with specified email. - * * @param \App\UserAlias $alias The user email alias * * @return bool @@ -60,7 +58,7 @@ } /** - * Handle the user setting "updated" event. + * Handle the user alias "updated" event. * * @param \App\UserAlias $alias User email alias * @@ -74,7 +72,7 @@ } /** - * Handle the user setting "deleted" event. + * Handle the user alias "deleted" event. * * @param \App\UserAlias $alias User email alias * diff --git a/src/app/OpenVidu/Room.php b/src/app/OpenVidu/Room.php --- a/src/app/OpenVidu/Room.php +++ b/src/app/OpenVidu/Room.php @@ -27,9 +27,9 @@ public const REQUEST_ACCEPTED = 'accepted'; public const REQUEST_DENIED = 'denied'; - private const OV_ROLE_MODERATOR = 'MODERATOR'; + private const OV_ROLE_MODERATOR = 'MODERATOR'; // @phpstan-ignore-line private const OV_ROLE_PUBLISHER = 'PUBLISHER'; - private const OV_ROLE_SUBSCRIBER = 'SUBSCRIBER'; + private const OV_ROLE_SUBSCRIBER = 'SUBSCRIBER'; // @phpstan-ignore-line protected $fillable = [ 'user_id', diff --git a/src/app/Package.php b/src/app/Package.php --- a/src/app/Package.php +++ b/src/app/Package.php @@ -39,6 +39,7 @@ public $timestamps = false; + /** @var array The attributes that are mass assignable */ protected $fillable = [ 'description', 'discount_rate', @@ -46,7 +47,7 @@ 'title', ]; - /** @var array Translatable properties */ + /** @var array Translatable properties */ public $translatable = [ 'name', 'description', @@ -83,7 +84,7 @@ public function isDomain(): bool { foreach ($this->skus as $sku) { - if ($sku->handler_class::entitleableClass() == \App\Domain::class) { + if ($sku->handler_class::entitleableClass() == Domain::class) { return true; } } @@ -98,11 +99,8 @@ */ public function skus() { - return $this->belongsToMany( - 'App\Sku', - 'package_skus' - )->using('App\PackageSku')->withPivot( - ['qty'] - ); + return $this->belongsToMany(Sku::class, 'package_skus') + ->using(PackageSku::class) + ->withPivot(['qty']); } } diff --git a/src/app/PackageSku.php b/src/app/PackageSku.php --- a/src/app/PackageSku.php +++ b/src/app/PackageSku.php @@ -16,6 +16,7 @@ */ class PackageSku extends Pivot { + /** @var array The attributes that are mass assignable */ protected $fillable = [ 'package_id', 'sku_id', @@ -23,6 +24,7 @@ 'qty' ]; + /** @var array The attributes that should be cast */ protected $casts = [ 'cost' => 'integer', 'qty' => 'integer' @@ -71,7 +73,7 @@ */ public function package() { - return $this->belongsTo('App\Package'); + return $this->belongsTo(Package::class); } /** @@ -81,6 +83,6 @@ */ public function sku() { - return $this->belongsTo('App\Sku'); + return $this->belongsTo(Sku::class); } } diff --git a/src/app/Payment.php b/src/app/Payment.php --- a/src/app/Payment.php +++ b/src/app/Payment.php @@ -20,10 +20,12 @@ public $incrementing = false; protected $keyType = 'string'; + /** @var array The attributes that should be cast */ protected $casts = [ 'amount' => 'integer' ]; + /** @var array The attributes that are mass assignable */ protected $fillable = [ 'id', 'wallet_id', @@ -52,6 +54,6 @@ */ public function wallet() { - return $this->belongsTo('\App\Wallet', 'wallet_id', 'id'); + return $this->belongsTo(Wallet::class, 'wallet_id', 'id'); } } diff --git a/src/app/Plan.php b/src/app/Plan.php --- a/src/app/Plan.php +++ b/src/app/Plan.php @@ -34,6 +34,7 @@ public $timestamps = false; + /** @var array The attributes that are mass assignable */ protected $fillable = [ 'title', 'name', @@ -47,14 +48,15 @@ 'discount_rate', ]; + /** @var array The attributes that should be cast */ protected $casts = [ - 'promo_from' => 'datetime', - 'promo_to' => 'datetime', + 'promo_from' => 'datetime:Y-m-d H:i:s', + 'promo_to' => 'datetime:Y-m-d H:i:s', 'discount_qty' => 'integer', 'discount_rate' => 'integer' ]; - /** @var array Translatable properties */ + /** @var array Translatable properties */ public $translatable = [ 'name', 'description', @@ -87,18 +89,15 @@ */ public function packages() { - return $this->belongsToMany( - 'App\Package', - 'plan_packages' - )->using('App\PlanPackage')->withPivot( - [ - 'qty', - 'qty_min', - 'qty_max', - 'discount_qty', - 'discount_rate' - ] - ); + return $this->belongsToMany(Package::class, 'plan_packages') + ->using(PlanPackage::class) + ->withPivot([ + 'qty', + 'qty_min', + 'qty_max', + 'discount_qty', + 'discount_rate' + ]); } /** diff --git a/src/app/PlanPackage.php b/src/app/PlanPackage.php --- a/src/app/PlanPackage.php +++ b/src/app/PlanPackage.php @@ -19,6 +19,7 @@ */ class PlanPackage extends Pivot { + /** @var array The attributes that are mass assignable */ protected $fillable = [ 'plan_id', 'package_id', @@ -29,6 +30,7 @@ 'discount_rate' ]; + /** @var array The attributes that should be cast */ protected $casts = [ 'qty' => 'integer', 'qty_max' => 'integer', @@ -62,7 +64,7 @@ */ public function package() { - return $this->belongsTo('App\Package'); + return $this->belongsTo(Package::class); } /** @@ -72,6 +74,6 @@ */ public function plan() { - return $this->belongsTo('App\Plan'); + return $this->belongsTo(Plan::class); } } diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php --- a/src/app/Providers/AppServiceProvider.php +++ b/src/app/Providers/AppServiceProvider.php @@ -54,6 +54,7 @@ \App\Resource::observe(\App\Observers\ResourceObserver::class); \App\ResourceSetting::observe(\App\Observers\ResourceSettingObserver::class); \App\SharedFolder::observe(\App\Observers\SharedFolderObserver::class); + \App\SharedFolderAlias::observe(\App\Observers\SharedFolderAliasObserver::class); \App\SharedFolderSetting::observe(\App\Observers\SharedFolderSettingObserver::class); \App\SignupCode::observe(\App\Observers\SignupCodeObserver::class); \App\SignupInvitation::observe(\App\Observers\SignupInvitationObserver::class); diff --git a/src/app/Providers/AuthServiceProvider.php b/src/app/Providers/AuthServiceProvider.php --- a/src/app/Providers/AuthServiceProvider.php +++ b/src/app/Providers/AuthServiceProvider.php @@ -2,9 +2,8 @@ namespace App\Providers; -use App\Auth\LDAPUserProvider; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Facades\Route; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Laravel\Passport\Passport; @@ -13,7 +12,7 @@ /** * The policy mappings for the application. * - * @var array + * @var array */ protected $policies = [ // 'App\Model' => 'App\Policies\ModelPolicy', @@ -28,13 +27,6 @@ { $this->registerPolicies(); - Auth::provider( - 'ldap', - function ($app, array $config) { - return new LDAPUserProvider($app['hash'], $config['model']); - } - ); - // Hashes all secrets and thus makes them non-recoverable /* Passport::hashClientSecrets(); */ // Only enable routes for access tokens @@ -43,7 +35,7 @@ $router->forAccessTokens(); // Override the default route to avoid rate-limiting. - \Route::post('/token', [ + Route::post('/token', [ 'uses' => 'AccessTokenController@issueToken', 'as' => 'passport.token', ]); diff --git a/src/app/Providers/EventServiceProvider.php b/src/app/Providers/EventServiceProvider.php --- a/src/app/Providers/EventServiceProvider.php +++ b/src/app/Providers/EventServiceProvider.php @@ -12,7 +12,7 @@ /** * The event listener mappings for the application. * - * @var array + * @var array> */ protected $listen = [ Registered::class => [ @@ -27,8 +27,16 @@ */ public function boot() { - parent::boot(); - // } + + /** + * Determine if events and listeners should be automatically discovered. + * + * @return bool + */ + public function shouldDiscoverEvents() + { + return false; + } } diff --git a/src/app/Providers/PassportServiceProvider.php b/src/app/Providers/PassportServiceProvider.php --- a/src/app/Providers/PassportServiceProvider.php +++ b/src/app/Providers/PassportServiceProvider.php @@ -10,7 +10,6 @@ class PassportServiceProvider extends \Laravel\Passport\PassportServiceProvider { - /** * Make the authorization service instance. * diff --git a/src/app/Providers/RouteServiceProvider.php b/src/app/Providers/RouteServiceProvider.php --- a/src/app/Providers/RouteServiceProvider.php +++ b/src/app/Providers/RouteServiceProvider.php @@ -2,20 +2,14 @@ namespace App\Providers; -use Illuminate\Support\Facades\Route; +use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\RateLimiter; +use Illuminate\Support\Facades\Route; class RouteServiceProvider extends ServiceProvider { - /** - * This namespace is applied to your controller routes. - * - * In addition, it is set as the URL generator's root namespace. - * - * @var string - */ - protected $namespace = 'App\Http\Controllers'; - /** * Define your route model bindings, pattern filters, etc. * @@ -23,52 +17,28 @@ */ public function boot() { - // + $this->configureRateLimiting(); - parent::boot(); - } + $this->routes(function () { + $prefix = \trim(\parse_url(\config('app.url'), PHP_URL_PATH), '/') . '/'; - /** - * Define the routes for the application. - * - * @return void - */ - public function map() - { - $this->mapApiRoutes(); - - $this->mapWebRoutes(); + Route::prefix($prefix . 'api') + ->group(base_path('routes/api.php')); - // + Route::middleware('web') + ->group(base_path('routes/web.php')); + }); } /** - * Define the "web" routes for the application. - * - * These routes all receive session state, CSRF protection, etc. - * - * @return void - */ - protected function mapWebRoutes() - { - Route::middleware('web') - ->namespace($this->namespace) - ->group(base_path('routes/web.php')); - } - - /** - * Define the "api" routes for the application. - * - * These routes are typically stateless. + * Configure the rate limiters for the application. * * @return void */ - protected function mapApiRoutes() + protected function configureRateLimiting() { - // Note: We removed the prefix from here, to have more control - // over it in routes/api.php - Route::middleware('api') - ->namespace($this->namespace) - ->group(base_path('routes/api.php')); + RateLimiter::for('api', function (Request $request) { + return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); + }); } } diff --git a/src/app/Resource.php b/src/app/Resource.php --- a/src/app/Resource.php +++ b/src/app/Resource.php @@ -4,11 +4,11 @@ use App\Traits\BelongsToTenantTrait; use App\Traits\EntitleableTrait; +use App\Traits\EmailPropertyTrait; use App\Traits\ResourceConfigTrait; use App\Traits\SettingsTrait; use App\Traits\StatusPropertyTrait; use App\Traits\UuidIntKeyTrait; -use App\Wallet; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -30,6 +30,7 @@ use SoftDeletes; use StatusPropertyTrait; use UuidIntKeyTrait; + use EmailPropertyTrait; // must be after UuidIntKeyTrait // we've simply never heard of this resource public const STATUS_NEW = 1 << 0; @@ -44,54 +45,16 @@ // resource has been created in IMAP public const STATUS_IMAP_READY = 1 << 8; - protected $fillable = [ - 'email', - 'name', - 'status', - ]; - - /** @var ?string Domain name for a resource to be created */ - public $domain; - - - /** - * Returns the resource domain. - * - * @return ?\App\Domain The domain to which the resource belongs to, NULL if it does not exist - */ - public function domain(): ?Domain - { - if (isset($this->domain)) { - $domainName = $this->domain; - } else { - list($local, $domainName) = explode('@', $this->email); - } - - return Domain::where('namespace', $domainName)->first(); - } + // A template for the email attribute on a resource creation + public const EMAIL_TEMPLATE = 'resource-{id}@{domainName}'; - /** - * Find whether an email address exists as a resource (including deleted resources). - * - * @param string $email Email address - * @param bool $return_resource Return Resource instance instead of boolean - * - * @return \App\Resource|bool True or Resource model object if found, False otherwise - */ - public static function emailExists(string $email, bool $return_resource = false) - { - if (strpos($email, '@') === false) { - return false; - } - - $email = \strtolower($email); - - $resource = self::withTrashed()->where('email', $email)->first(); - - if ($resource) { - return $return_resource ? $resource : true; - } + /** @var array The attributes that should be cast */ + protected $casts = [ + 'created_at' => 'datetime:Y-m-d H:i:s', + 'deleted_at' => 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', + ]; - return false; - } + /** @var array The attributes that are mass assignable */ + protected $fillable = ['email', 'name', 'status']; } diff --git a/src/app/ResourceSetting.php b/src/app/ResourceSetting.php --- a/src/app/ResourceSetting.php +++ b/src/app/ResourceSetting.php @@ -14,9 +14,8 @@ */ class ResourceSetting extends Model { - protected $fillable = [ - 'resource_id', 'key', 'value' - ]; + /** @var array The attributes that are mass assignable */ + protected $fillable = ['resource_id', 'key', 'value']; /** * The resource to which this setting belongs. @@ -25,6 +24,6 @@ */ public function resource() { - return $this->belongsTo(\App\Resource::class, 'resource_id', 'id'); + return $this->belongsTo(Resource::class, 'resource_id', 'id'); } } diff --git a/src/app/SharedFolder.php b/src/app/SharedFolder.php --- a/src/app/SharedFolder.php +++ b/src/app/SharedFolder.php @@ -2,13 +2,14 @@ namespace App; +use App\Traits\AliasesTrait; use App\Traits\BelongsToTenantTrait; use App\Traits\EntitleableTrait; +use App\Traits\EmailPropertyTrait; use App\Traits\SharedFolderConfigTrait; use App\Traits\SettingsTrait; use App\Traits\StatusPropertyTrait; use App\Traits\UuidIntKeyTrait; -use App\Wallet; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -24,6 +25,7 @@ */ class SharedFolder extends Model { + use AliasesTrait; use BelongsToTenantTrait; use EntitleableTrait; use SharedFolderConfigTrait; @@ -31,6 +33,7 @@ use SoftDeletes; use StatusPropertyTrait; use UuidIntKeyTrait; + use EmailPropertyTrait; // must be after UuidIntKeyTrait // we've simply never heard of this folder public const STATUS_NEW = 1 << 0; @@ -48,7 +51,17 @@ /** @const array Supported folder type labels */ public const SUPPORTED_TYPES = ['mail', 'event', 'contact', 'task', 'note', 'file']; - /** @var array Mass-assignable properties */ + /** @const string A template for the email attribute on a folder creation */ + public const EMAIL_TEMPLATE = '{type}-{id}@{domainName}'; + + /** @var array The attributes that should be cast */ + protected $casts = [ + 'created_at' => 'datetime:Y-m-d H:i:s', + 'deleted_at' => 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', + ]; + + /** @var array The attributes that are mass assignable */ protected $fillable = [ 'email', 'name', @@ -56,51 +69,6 @@ 'type', ]; - /** @var ?string Domain name for a shared folder to be created */ - public $domain; - - - /** - * Returns the shared folder domain. - * - * @return ?\App\Domain The domain to which the folder belongs to, NULL if it does not exist - */ - public function domain(): ?Domain - { - if (isset($this->domain)) { - $domainName = $this->domain; - } else { - list($local, $domainName) = explode('@', $this->email); - } - - return Domain::where('namespace', $domainName)->first(); - } - - /** - * Find whether an email address exists as a shared folder (including deleted folders). - * - * @param string $email Email address - * @param bool $return_folder Return SharedFolder instance instead of boolean - * - * @return \App\SharedFolder|bool True or Resource model object if found, False otherwise - */ - public static function emailExists(string $email, bool $return_folder = false) - { - if (strpos($email, '@') === false) { - return false; - } - - $email = \strtolower($email); - - $folder = self::withTrashed()->where('email', $email)->first(); - - if ($folder) { - return $return_folder ? $folder : true; - } - - return false; - } - /** * Folder type mutator * diff --git a/src/app/SharedFolderAlias.php b/src/app/SharedFolderAlias.php new file mode 100644 --- /dev/null +++ b/src/app/SharedFolderAlias.php @@ -0,0 +1,38 @@ + The attributes that are mass assignable */ + protected $fillable = ['shared_folder_id', 'alias']; + + /** + * Ensure the email address is appropriately cased. + * + * @param string $alias Email address + */ + public function setAliasAttribute(string $alias) + { + $this->attributes['alias'] = \strtolower($alias); + } + + /** + * The shared folder to which this alias belongs. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function sharedFolder() + { + return $this->belongsTo(SharedFolder::class, 'shared_folder_id', 'id'); + } +} diff --git a/src/app/SharedFolderSetting.php b/src/app/SharedFolderSetting.php --- a/src/app/SharedFolderSetting.php +++ b/src/app/SharedFolderSetting.php @@ -14,9 +14,8 @@ */ class SharedFolderSetting extends Model { - protected $fillable = [ - 'shared_folder_id', 'key', 'value' - ]; + /** @var array The attributes that are mass assignable */ + protected $fillable = ['shared_folder_id', 'key', 'value']; /** * The folder to which this setting belongs. @@ -25,6 +24,6 @@ */ public function folder() { - return $this->belongsTo(\App\SharedFolder::class, 'shared_folder_id', 'id'); + return $this->belongsTo(SharedFolder::class, 'shared_folder_id', 'id'); } } diff --git a/src/app/SignupCode.php b/src/app/SignupCode.php --- a/src/app/SignupCode.php +++ b/src/app/SignupCode.php @@ -35,32 +35,16 @@ public const CODE_EXP_HOURS = 24; - /** - * The primary key associated with the table. - * - * @var string - */ + /** @var string The primary key associated with the table */ protected $primaryKey = 'code'; - /** - * Indicates if the IDs are auto-incrementing. - * - * @var bool - */ + /** @var bool Indicates if the IDs are auto-incrementing */ public $incrementing = false; - /** - * The "type" of the auto-incrementing ID. - * - * @var string - */ + /** @var string The "type" of the auto-incrementing ID */ protected $keyType = 'string'; - /** - * The attributes that are mass assignable. - * - * @var array - */ + /** @var array The attributes that are mass assignable */ protected $fillable = [ 'code', 'email', @@ -72,14 +56,12 @@ 'voucher' ]; - protected $casts = ['headers' => 'array']; + /** @var array The attributes that should be cast */ + protected $casts = [ + 'expires_at' => 'datetime:Y-m-d H:i:s', + 'headers' => 'array' + ]; - /** - * The attributes that should be mutated to dates. - * - * @var array - */ - protected $dates = ['expires_at']; /** * Check if code is expired. diff --git a/src/app/SignupInvitation.php b/src/app/SignupInvitation.php --- a/src/app/SignupInvitation.php +++ b/src/app/SignupInvitation.php @@ -31,13 +31,10 @@ public const STATUS_COMPLETED = 1 << 3; - /** - * The attributes that are mass assignable. - * - * @var array - */ + /** @var array The attributes that are mass assignable */ protected $fillable = ['email']; + /** * Returns whether this invitation process completed (user signed up) * @@ -85,6 +82,6 @@ */ public function user() { - return $this->belongsTo('App\User', 'user_id', 'id'); + return $this->belongsTo(User::class, 'user_id', 'id'); } } diff --git a/src/app/Sku.php b/src/app/Sku.php --- a/src/app/Sku.php +++ b/src/app/Sku.php @@ -2,10 +2,10 @@ namespace App; -use Illuminate\Database\Eloquent\Model; -use Spatie\Translatable\HasTranslations; use App\Traits\BelongsToTenantTrait; use App\Traits\UuidStrKeyTrait; +use Illuminate\Database\Eloquent\Model; +use Spatie\Translatable\HasTranslations; /** * The eloquent definition of a Stock Keeping Unit (SKU). @@ -28,10 +28,12 @@ use HasTranslations; use UuidStrKeyTrait; + /** @var array The attributes that should be cast */ protected $casts = [ 'units_free' => 'integer' ]; + /** @var array The attributes that are mass assignable */ protected $fillable = [ 'active', 'cost', @@ -45,7 +47,7 @@ 'units_free', ]; - /** @var array Translatable properties */ + /** @var array Translatable properties */ public $translatable = [ 'name', 'description', @@ -58,7 +60,7 @@ */ public function entitlements() { - return $this->hasMany('App\Entitlement'); + return $this->hasMany(Entitlement::class); } /** @@ -68,9 +70,8 @@ */ public function packages() { - return $this->belongsToMany( - 'App\Package', - 'package_skus' - )->using('App\PackageSku')->withPivot(['cost', 'qty']); + return $this->belongsToMany(Package::class, 'package_skus') + ->using(PackageSku::class) + ->withPivot(['cost', 'qty']); } } diff --git a/src/app/Tenant.php b/src/app/Tenant.php --- a/src/app/Tenant.php +++ b/src/app/Tenant.php @@ -15,10 +15,8 @@ { use SettingsTrait; - protected $fillable = [ - 'id', - 'title', - ]; + /** @var array The attributes that are mass assignable */ + protected $fillable = ['id', 'title']; /** @@ -67,7 +65,7 @@ */ public function discounts() { - return $this->hasMany('App\Discount'); + return $this->hasMany(Discount::class); } /** @@ -77,7 +75,7 @@ */ public function signupInvitations() { - return $this->hasMany('App\SignupInvitation'); + return $this->hasMany(SignupInvitation::class); } /* @@ -87,7 +85,7 @@ */ public function wallet(): ?Wallet { - $user = \App\User::where('role', 'reseller')->where('tenant_id', $this->id)->first(); + $user = User::where('role', 'reseller')->where('tenant_id', $this->id)->first(); return $user ? $user->wallets->first() : null; } diff --git a/src/app/TenantSetting.php b/src/app/TenantSetting.php --- a/src/app/TenantSetting.php +++ b/src/app/TenantSetting.php @@ -14,9 +14,8 @@ */ class TenantSetting extends Model { - protected $fillable = [ - 'tenant_id', 'key', 'value' - ]; + /** @var array The attributes that are mass assignable */ + protected $fillable = ['tenant_id', 'key', 'value']; /** * The tenant to which this setting belongs. @@ -25,6 +24,6 @@ */ public function tenant() { - return $this->belongsTo('\App\Tenant', 'tenant_id', 'id'); + return $this->belongsTo(Tenant::class, 'tenant_id', 'id'); } } diff --git a/src/app/Traits/UserAliasesTrait.php b/src/app/Traits/AliasesTrait.php rename from src/app/Traits/UserAliasesTrait.php rename to src/app/Traits/AliasesTrait.php --- a/src/app/Traits/UserAliasesTrait.php +++ b/src/app/Traits/AliasesTrait.php @@ -2,11 +2,22 @@ namespace App\Traits; -trait UserAliasesTrait +use Illuminate\Support\Str; + +trait AliasesTrait { + /** + * Email aliases of this object. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function aliases() + { + return $this->hasMany(static::class . 'Alias'); + } + /** * Find whether an email address exists as an alias - * (including aliases of deleted users). * * @param string $email Email address * @@ -19,14 +30,13 @@ } $email = \strtolower($email); + $class = static::class . 'Alias'; - $count = \App\UserAlias::where('alias', $email)->count(); - - return $count > 0; + return $class::where('alias', $email)->count() > 0; } /** - * A helper to update user aliases list. + * A helper to update object's aliases list. * * Example Usage: * @@ -47,6 +57,7 @@ $existing_aliases = []; foreach ($this->aliases()->get() as $alias) { + /** @var \App\UserAlias|\App\SharedFolderAlias $alias */ if (!in_array($alias->alias, $aliases)) { $alias->delete(); } else { diff --git a/src/app/Traits/EmailPropertyTrait.php b/src/app/Traits/EmailPropertyTrait.php new file mode 100644 --- /dev/null +++ b/src/app/Traits/EmailPropertyTrait.php @@ -0,0 +1,92 @@ +email) && defined('static::EMAIL_TEMPLATE')) { + $template = static::EMAIL_TEMPLATE; // @phpstan-ignore-line + $defaults = [ + 'type' => 'mail', + ]; + + foreach (['id', 'domainName', 'type'] as $prop) { + if (strpos($template, "{{$prop}}") === false) { + continue; + } + + $value = $model->{$prop} ?? ($defaults[$prop] ?? ''); + + if ($value === '' || $value === null) { + throw new \Exception("Missing '{$prop}' property for " . static::class); + } + + $template = str_replace("{{$prop}}", $value, $template); + } + + $model->email = strtolower($template); + } + }); + } + + /** + * Returns the object's domain (including soft-deleted). + * + * @return ?\App\Domain The domain to which the object belongs to, NULL if it does not exist + */ + public function domain(): ?\App\Domain + { + if (empty($this->email) && isset($this->domainName)) { + $domainName = $this->domainName; + } else { + list($local, $domainName) = explode('@', $this->email); + } + + return \App\Domain::withTrashed()->where('namespace', $domainName)->first(); + } + + /** + * Find whether an email address exists as a model object (including soft-deleted). + * + * @param string $email Email address + * @param bool $return_object Return model instance instead of a boolean + * + * @return static|bool True or Model object if found, False otherwise + */ + public static function emailExists(string $email, bool $return_object = false) + { + if (strpos($email, '@') === false) { + return false; + } + + $email = \strtolower($email); + + $object = static::withTrashed()->where('email', $email)->first(); + + if ($object) { + return $return_object ? $object : true; + } + + return false; + } + + /** + * Ensure the email is appropriately cased. + * + * @param string $email Email address + */ + public function setEmailAttribute(string $email): void + { + $this->attributes['email'] = strtolower($email); + } +} diff --git a/src/app/Transaction.php b/src/app/Transaction.php --- a/src/app/Transaction.php +++ b/src/app/Transaction.php @@ -2,8 +2,6 @@ namespace App; -use App\Entitlement; -use App\Wallet; use App\Traits\UuidStrKeyTrait; use Illuminate\Database\Eloquent\Model; @@ -34,27 +32,23 @@ public const WALLET_REFUND = 'refund'; public const WALLET_CHARGEBACK = 'chback'; + /** @var array The attributes that are mass assignable */ protected $fillable = [ // actor, if any 'user_email', - // entitlement, wallet 'object_id', 'object_type', - // entitlement: created, deleted, billed // wallet: debit, credit, award, penalty 'type', - 'amount', - 'description', - // parent, for example wallet debit is parent for entitlements charged. 'transaction_id' ]; - /** @var array Casts properties as type */ + /** @var array Casts properties as type */ protected $casts = [ 'amount' => 'integer', ]; diff --git a/src/app/User.php b/src/app/User.php --- a/src/app/User.php +++ b/src/app/User.php @@ -2,20 +2,19 @@ namespace App; -use App\UserAlias; +use App\Traits\AliasesTrait; use App\Traits\BelongsToTenantTrait; use App\Traits\EntitleableTrait; -use App\Traits\UserAliasesTrait; +use App\Traits\EmailPropertyTrait; use App\Traits\UserConfigTrait; use App\Traits\UuidIntKeyTrait; use App\Traits\SettingsTrait; use App\Traits\StatusPropertyTrait; -use App\Wallet; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Illuminate\Foundation\Auth\User as Authenticatable; -use Iatstuti\Database\Support\NullableFields; +use Dyrynda\Database\Support\NullableFields; use Laravel\Passport\HasApiTokens; use League\OAuth2\Server\Exception\OAuthServerException; @@ -31,12 +30,13 @@ */ class User extends Authenticatable { + use AliasesTrait; use BelongsToTenantTrait; use EntitleableTrait; + use EmailPropertyTrait; use HasApiTokens; use NullableFields; use UserConfigTrait; - use UserAliasesTrait; use UuidIntKeyTrait; use SettingsTrait; use SoftDeletes; @@ -57,11 +57,7 @@ // user in "limited feature-set" state public const STATUS_DEGRADED = 1 << 6; - /** - * The attributes that are mass assignable. - * - * @var array - */ + /** @var array The attributes that are mass assignable */ protected $fillable = [ 'id', 'email', @@ -70,22 +66,26 @@ 'status', ]; - /** - * The attributes that should be hidden for arrays. - * - * @var array - */ + /** @var array The attributes that should be hidden for arrays */ protected $hidden = [ 'password', 'password_ldap', 'role' ]; + /** @var array The attributes that can be null */ protected $nullable = [ 'password', 'password_ldap' ]; + /** @var array The attributes that should be cast */ + protected $casts = [ + 'created_at' => 'datetime:Y-m-d H:i:s', + 'deleted_at' => 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', + ]; + /** * Any wallets on which this user is a controller. * @@ -96,23 +96,13 @@ public function accounts() { return $this->belongsToMany( - 'App\Wallet', // The foreign object definition + Wallet::class, // The foreign object definition 'user_accounts', // The table name 'user_id', // The local foreign key 'wallet_id' // The remote foreign key ); } - /** - * Email aliases of this user. - * - * @return \Illuminate\Database\Eloquent\Relations\HasMany - */ - public function aliases() - { - return $this->hasMany('App\UserAlias', 'user_id'); - } - /** * Assign a package to a user. The user should not have any existing entitlements. * @@ -263,20 +253,6 @@ $this->save(); } - /** - * Return the \App\Domain for this user. - * - * @return \App\Domain|null - */ - public function domain() - { - list($local, $domainName) = explode('@', $this->email); - - $domain = \App\Domain::withTrashed()->where('namespace', $domainName)->first(); - - return $domain; - } - /** * List the domains to which this user is entitled. * @@ -298,39 +274,14 @@ $query->withEnvTenantContext(); } - $query->whereRaw(sprintf('(domains.type & %s)', Domain::TYPE_PUBLIC)) - ->whereRaw(sprintf('(domains.status & %s)', Domain::STATUS_ACTIVE)); + $query->where('domains.type', '&', Domain::TYPE_PUBLIC) + ->where('domains.status', '&', Domain::STATUS_ACTIVE); }); } return $domains; } - /** - * Find whether an email address exists as a user (including deleted users). - * - * @param string $email Email address - * @param bool $return_user Return User instance instead of boolean - * - * @return \App\User|bool True or User model object if found, False otherwise - */ - public static function emailExists(string $email, bool $return_user = false) - { - if (strpos($email, '@') === false) { - return false; - } - - $email = \strtolower($email); - - $user = self::withTrashed()->where('email', $email)->first(); - - if ($user) { - return $return_user ? $user : true; - } - - return false; - } - /** * Return entitleable objects of a specified type controlled by the current user. * @@ -444,7 +395,7 @@ $name = trim($settings['first_name'] . ' ' . $settings['last_name']); if (empty($name) && $fallback) { - return trim(\trans('app.siteuser', ['site' => \App\Tenant::getConfig($this->tenant_id, 'app.name')])); + return trim(\trans('app.siteuser', ['site' => Tenant::getConfig($this->tenant_id, 'app.name')])); } return $name; @@ -457,7 +408,7 @@ */ public function passwords() { - return $this->hasMany('App\UserPassword'); + return $this->hasMany(UserPassword::class); } /** @@ -470,7 +421,7 @@ */ public function resources($with_accounts = true) { - return $this->entitleables(\App\Resource::class, $with_accounts); + return $this->entitleables(Resource::class, $with_accounts); } /** @@ -483,7 +434,7 @@ */ public function sharedFolders($with_accounts = true) { - return $this->entitleables(\App\SharedFolder::class, $with_accounts); + return $this->entitleables(SharedFolder::class, $with_accounts); } public function senderPolicyFrameworkWhitelist($clientName) @@ -561,7 +512,7 @@ */ public function verificationcodes() { - return $this->hasMany('App\VerificationCode', 'user_id', 'id'); + return $this->hasMany(VerificationCode::class, 'user_id', 'id'); } /** @@ -571,7 +522,7 @@ */ public function wallets() { - return $this->hasMany('App\Wallet'); + return $this->hasMany(Wallet::class); } /** diff --git a/src/app/UserAlias.php b/src/app/UserAlias.php --- a/src/app/UserAlias.php +++ b/src/app/UserAlias.php @@ -13,9 +13,18 @@ */ class UserAlias extends Model { - protected $fillable = [ - 'user_id', 'alias' - ]; + /** @var array The attributes that are mass assignable */ + protected $fillable = ['user_id', 'alias']; + + /** + * Ensure the email address is appropriately cased. + * + * @param string $alias Email address + */ + public function setAliasAttribute(string $alias) + { + $this->attributes['alias'] = \strtolower($alias); + } /** * The user to which this alias belongs. @@ -24,6 +33,6 @@ */ public function user() { - return $this->belongsTo('\App\User', 'user_id', 'id'); + return $this->belongsTo(User::class, 'user_id', 'id'); } } diff --git a/src/app/UserPassword.php b/src/app/UserPassword.php --- a/src/app/UserPassword.php +++ b/src/app/UserPassword.php @@ -16,13 +16,15 @@ /** @var bool Indicates if the model should be timestamped. */ public $timestamps = false; - /** @var array The attributes that should be mutated to dates. */ - protected $dates = ['created_at']; + /** @var array The attributes that should be cast. */ + protected $casts = [ + 'created_at' => 'datetime:Y-m-d H:i:s', + ]; - /** @var array The attributes that are mass assignable. */ + /** @var array The attributes that are mass assignable. */ protected $fillable = ['user_id', 'password']; - /** @var array The attributes that should be hidden for arrays. */ + /** @var array The attributes that should be hidden for arrays. */ protected $hidden = ['password']; /** @@ -32,6 +34,6 @@ */ public function user() { - return $this->belongsTo('\App\User', 'user_id', 'id'); + return $this->belongsTo(User::class, 'user_id', 'id'); } } diff --git a/src/app/UserSetting.php b/src/app/UserSetting.php --- a/src/app/UserSetting.php +++ b/src/app/UserSetting.php @@ -14,9 +14,8 @@ */ class UserSetting extends Model { - protected $fillable = [ - 'user_id', 'key', 'value' - ]; + /** @var array The attributes that are mass assignable */ + protected $fillable = ['user_id', 'key', 'value']; /** * The user to which this setting belongs. @@ -25,6 +24,6 @@ */ public function user() { - return $this->belongsTo('\App\User', 'user_id', 'id'); + return $this->belongsTo(User::class, 'user_id', 'id'); } } diff --git a/src/app/VerificationCode.php b/src/app/VerificationCode.php --- a/src/app/VerificationCode.php +++ b/src/app/VerificationCode.php @@ -26,49 +26,25 @@ // Code expires after so many hours public const CODE_EXP_HOURS = 8; - /** - * The primary key associated with the table. - * - * @var string - */ + /** @var string The primary key associated with the table */ protected $primaryKey = 'code'; - /** - * Indicates if the IDs are auto-incrementing. - * - * @var bool - */ + /** @var bool Indicates if the IDs are auto-incrementing */ public $incrementing = false; - /** - * The "type" of the auto-incrementing ID. - * - * @var string - */ + /** @var string The "type" of the auto-incrementing ID */ protected $keyType = 'string'; - /** - * Indicates if the model should be timestamped. - * - * @var bool - */ + /** @var bool Indicates if the model should be timestamped */ public $timestamps = false; - /** - * Casts properties as type - * - * @var array - */ + /** @var array Casts properties as type */ protected $casts = [ 'active' => 'boolean', 'expires_at' => 'datetime', ]; - /** - * The attributes that are mass assignable. - * - * @var array - */ + /** @var array The attributes that are mass assignable */ protected $fillable = ['user_id', 'code', 'short_code', 'mode', 'expires_at', 'active']; @@ -102,6 +78,6 @@ */ public function user() { - return $this->belongsTo('\App\User', 'user_id', 'id'); + return $this->belongsTo(User::class, 'user_id', 'id'); } } diff --git a/src/app/Wallet.php b/src/app/Wallet.php --- a/src/app/Wallet.php +++ b/src/app/Wallet.php @@ -2,11 +2,10 @@ namespace App; -use App\User; use App\Traits\SettingsTrait; use App\Traits\UuidStrKeyTrait; use Carbon\Carbon; -use Iatstuti\Database\Support\NullableFields; +use Dyrynda\Database\Support\NullableFields; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\DB; @@ -30,39 +29,23 @@ public $timestamps = false; - /** - * The attributes' default values. - * - * @var array - */ + /** @var array The attributes' default values */ protected $attributes = [ 'balance' => 0, ]; - /** - * The attributes that are mass assignable. - * - * @var array - */ + /** @var array The attributes that are mass assignable */ protected $fillable = [ 'currency', 'description' ]; - /** - * The attributes that can be not set. - * - * @var array - */ + /** @var array The attributes that can be not set */ protected $nullable = [ 'description', ]; - /** - * The types of attributes to which its values will be cast - * - * @var array - */ + /** @var array The types of attributes to which its values will be cast */ protected $casts = [ 'balance' => 'integer', ]; @@ -160,7 +143,7 @@ } $entitlementTransactions[] = $entitlement->createTransaction( - \App\Transaction::ENTITLEMENT_BILLED, + Transaction::ENTITLEMENT_BILLED, $cost ); } @@ -232,8 +215,8 @@ public function controllers() { return $this->belongsToMany( - 'App\User', // The foreign object definition - 'user_accounts', // The table name + User::class, // The foreign object definition + 'user_accounts', // The table name 'wallet_id', // The local foreign key 'user_id' // The remote foreign key ); @@ -269,11 +252,11 @@ $this->save(); - \App\Transaction::create( + Transaction::create( [ 'object_id' => $this->id, - 'object_type' => \App\Wallet::class, - 'type' => \App\Transaction::WALLET_CREDIT, + 'object_type' => Wallet::class, + 'type' => Transaction::WALLET_CREDIT, 'amount' => $amount, 'description' => $description ] @@ -301,18 +284,18 @@ $this->save(); - $transaction = \App\Transaction::create( + $transaction = Transaction::create( [ 'object_id' => $this->id, - 'object_type' => \App\Wallet::class, - 'type' => \App\Transaction::WALLET_DEBIT, + 'object_type' => Wallet::class, + 'type' => Transaction::WALLET_DEBIT, 'amount' => $amount * -1, 'description' => $description ] ); if (!empty($eTIDs)) { - \App\Transaction::whereIn('id', $eTIDs)->update(['transaction_id' => $transaction->id]); + Transaction::whereIn('id', $eTIDs)->update(['transaction_id' => $transaction->id]); } return $this; @@ -325,7 +308,7 @@ */ public function discount() { - return $this->belongsTo('App\Discount', 'discount_id', 'id'); + return $this->belongsTo(Discount::class, 'discount_id', 'id'); } /** @@ -335,7 +318,7 @@ */ public function entitlements() { - return $this->hasMany('App\Entitlement'); + return $this->hasMany(Entitlement::class); } /** @@ -396,7 +379,7 @@ */ public function owner() { - return $this->belongsTo('App\User', 'user_id', 'id'); + return $this->belongsTo(User::class, 'user_id', 'id'); } /** @@ -406,7 +389,7 @@ */ public function payments() { - return $this->hasMany('App\Payment'); + return $this->hasMany(Payment::class); } /** @@ -430,10 +413,10 @@ */ public function transactions() { - return \App\Transaction::where( + return Transaction::where( [ 'object_id' => $this->id, - 'object_type' => \App\Wallet::class + 'object_type' => Wallet::class ] ); } @@ -492,7 +475,7 @@ // FIXME: Shouldn't we store also cost=0 transactions (to have the full history)? $entitlementTransactions[] = $entitlement->createTransaction( - \App\Transaction::ENTITLEMENT_BILLED, + Transaction::ENTITLEMENT_BILLED, $cost ); } diff --git a/src/app/WalletSetting.php b/src/app/WalletSetting.php --- a/src/app/WalletSetting.php +++ b/src/app/WalletSetting.php @@ -14,9 +14,8 @@ */ class WalletSetting extends Model { - protected $fillable = [ - 'wallet_id', 'key', 'value' - ]; + /** @var array The attributes that are mass assignable */ + protected $fillable = ['wallet_id', 'key', 'value']; /** * The wallet to which this setting belongs. @@ -25,6 +24,6 @@ */ public function wallet() { - return $this->belongsTo('\App\Wallet', 'wallet_id', 'id'); + return $this->belongsTo(Wallet::class, 'wallet_id', 'id'); } } diff --git a/src/composer.json b/src/composer.json --- a/src/composer.json +++ b/src/composer.json @@ -1,7 +1,7 @@ { - "name": "laravel/laravel", + "name": "kolab/kolab4", "type": "project", - "description": "The Laravel Framework.", + "description": "Kolab 4", "keywords": [ "framework", "laravel" @@ -14,37 +14,33 @@ } ], "require": { - "php": "^7.3", - "barryvdh/laravel-dompdf": "^0.8.6", - "doctrine/dbal": "^2.13", - "dyrynda/laravel-nullable-fields": "*", - "fideloper/proxy": "^4.0", - "guzzlehttp/guzzle": "^7.3", + "php": "^8.0", + "barryvdh/laravel-dompdf": "^1.0.0", + "doctrine/dbal": "^3.3.2", + "dyrynda/laravel-nullable-fields": "^4.2.0", + "guzzlehttp/guzzle": "^7.4.1", "kolab/net_ldap3": "dev-master", - "laravel/framework": "6.*", - "laravel/horizon": "^3", - "laravel/passport": "^9", - "laravel/tinker": "^2.4", - "mlocati/spf-lib": "^3.0", - "mollie/laravel-mollie": "^2.9", + "laravel/framework": "^9.2", + "laravel/horizon": "^5.9", + "laravel/octane": "^1.2", + "laravel/passport": "^10.3", + "laravel/tinker": "^2.7", + "mlocati/spf-lib": "^3.1", + "mollie/laravel-mollie": "^2.19", "moontoast/math": "^1.2", - "morrislaptop/laravel-queue-clear": "^1.2", "pear/crypt_gpg": "^1.6.6", - "silviolleite/laravelpwa": "^2.0", - "spatie/laravel-translatable": "^4.2", + "predis/predis": "^1.1.10", + "spatie/laravel-translatable": "^5.2", "spomky-labs/otphp": "~4.0.0", - "stripe/stripe-php": "^7.29", - "swooletw/laravel-swoole": "^2.6" + "stripe/stripe-php": "^7.29" }, "require-dev": { - "beyondcode/laravel-er-diagram-generator": "^1.3", - "code-lts/doctum": "^5.1", - "kirschbaum-development/mail-intercept": "^0.2.4", - "laravel/dusk": "~6.15.0", - "nunomaduro/larastan": "^0.7", - "phpstan/phpstan": "^0.12", + "code-lts/doctum": "^5.5.1", + "laravel/dusk": "~6.22.0", + "nunomaduro/larastan": "^2.0", + "phpstan/phpstan": "^1.4", "phpunit/phpunit": "^9", - "squizlabs/php_codesniffer": "3.*" + "squizlabs/php_codesniffer": "^3.6" }, "config": { "optimize-autoloader": true, @@ -77,6 +73,9 @@ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "@php artisan package:discover --ansi" ], + "post-update-cmd": [ + "@php artisan vendor:publish --tag=laravel-assets --ansi --force" + ], "post-root-package-install": [ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" ], diff --git a/src/config/app.php b/src/config/app.php --- a/src/config/app.php +++ b/src/config/app.php @@ -57,7 +57,7 @@ 'public_url' => env('APP_PUBLIC_URL', env('APP_URL', 'http://localhost')), - 'asset_url' => env('ASSET_URL', null), + 'asset_url' => env('ASSET_URL'), 'support_url' => env('SUPPORT_URL', null), @@ -215,44 +215,9 @@ | */ - 'aliases' => [ - 'App' => Illuminate\Support\Facades\App::class, - 'Arr' => Illuminate\Support\Arr::class, - 'Artisan' => Illuminate\Support\Facades\Artisan::class, - 'Auth' => Illuminate\Support\Facades\Auth::class, - 'Blade' => Illuminate\Support\Facades\Blade::class, - 'Broadcast' => Illuminate\Support\Facades\Broadcast::class, - 'Bus' => Illuminate\Support\Facades\Bus::class, - 'Cache' => Illuminate\Support\Facades\Cache::class, - 'Config' => Illuminate\Support\Facades\Config::class, - 'Cookie' => Illuminate\Support\Facades\Cookie::class, - 'Crypt' => Illuminate\Support\Facades\Crypt::class, - 'DB' => Illuminate\Support\Facades\DB::class, - 'Eloquent' => Illuminate\Database\Eloquent\Model::class, - 'Event' => Illuminate\Support\Facades\Event::class, - 'File' => Illuminate\Support\Facades\File::class, - 'Gate' => Illuminate\Support\Facades\Gate::class, - 'Hash' => Illuminate\Support\Facades\Hash::class, - 'Lang' => Illuminate\Support\Facades\Lang::class, - 'Log' => Illuminate\Support\Facades\Log::class, - 'Mail' => Illuminate\Support\Facades\Mail::class, - 'Notification' => Illuminate\Support\Facades\Notification::class, - 'Password' => Illuminate\Support\Facades\Password::class, + 'aliases' => \Illuminate\Support\Facades\Facade::defaultAliases()->merge([ 'PDF' => Barryvdh\DomPDF\Facade::class, - 'Queue' => Illuminate\Support\Facades\Queue::class, - 'Redirect' => Illuminate\Support\Facades\Redirect::class, - 'Redis' => Illuminate\Support\Facades\Redis::class, - 'Request' => Illuminate\Support\Facades\Request::class, - 'Response' => Illuminate\Support\Facades\Response::class, - 'Route' => Illuminate\Support\Facades\Route::class, - 'Schema' => Illuminate\Support\Facades\Schema::class, - 'Session' => Illuminate\Support\Facades\Session::class, - 'Storage' => Illuminate\Support\Facades\Storage::class, - 'Str' => Illuminate\Support\Str::class, - 'URL' => Illuminate\Support\Facades\URL::class, - 'Validator' => Illuminate\Support\Facades\Validator::class, - 'View' => Illuminate\Support\Facades\View::class, - ], + ])->toArray(), 'headers' => [ 'csp' => env('APP_HEADER_CSP', ""), diff --git a/src/config/auth.php b/src/config/auth.php --- a/src/config/auth.php +++ b/src/config/auth.php @@ -66,7 +66,7 @@ 'providers' => [ 'users' => [ - 'driver' => 'ldap', + 'driver' => 'eloquent', 'model' => App\User::class, ], diff --git a/src/config/broadcasting.php b/src/config/broadcasting.php --- a/src/config/broadcasting.php +++ b/src/config/broadcasting.php @@ -11,7 +11,7 @@ | framework when an event needs to be broadcast. You may set this to | any of the connections defined in the "connections" array below. | - | Supported: "pusher", "redis", "log", "null" + | Supported: "pusher", "ably", "redis", "log", "null" | */ @@ -39,6 +39,14 @@ 'cluster' => env('PUSHER_APP_CLUSTER'), 'useTLS' => true, ], + 'client_options' => [ + // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html + ], + ], + + 'ably' => [ + 'driver' => 'ably', + 'key' => env('ABLY_KEY'), ], 'redis' => [ diff --git a/src/config/cache.php b/src/config/cache.php --- a/src/config/cache.php +++ b/src/config/cache.php @@ -39,12 +39,14 @@ 'array' => [ 'driver' => 'array', + 'serialize' => false, ], 'database' => [ 'driver' => 'database', 'table' => 'cache', 'connection' => null, + 'lock_connection' => null, ], 'file' => [ @@ -74,6 +76,7 @@ 'redis' => [ 'driver' => 'redis', 'connection' => 'cache', + 'lock_connection' => 'default', ], 'dynamodb' => [ @@ -85,6 +88,10 @@ 'endpoint' => env('DYNAMODB_ENDPOINT'), ], + 'octane' => [ + 'driver' => 'octane', + ], + ], /* @@ -98,6 +105,6 @@ | */ - 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache'), + 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache_'), ]; diff --git a/src/config/database.php b/src/config/database.php --- a/src/config/database.php +++ b/src/config/database.php @@ -75,7 +75,7 @@ 'charset' => 'utf8', 'prefix' => '', 'prefix_indexes' => true, - 'schema' => 'public', + 'search_path' => 'public', 'sslmode' => 'prefer', ], @@ -135,7 +135,7 @@ 'default' => [ 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), - 'password' => env('REDIS_PASSWORD', null), + 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', 6379), 'database' => env('REDIS_DB', 0), ], @@ -143,7 +143,7 @@ 'cache' => [ 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), - 'password' => env('REDIS_PASSWORD', null), + 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', 6379), 'database' => env('REDIS_CACHE_DB', 1), ], diff --git a/src/config/filesystems.php b/src/config/filesystems.php --- a/src/config/filesystems.php +++ b/src/config/filesystems.php @@ -13,20 +13,7 @@ | */ - 'default' => env('FILESYSTEM_DRIVER', 'local'), - - /* - |-------------------------------------------------------------------------- - | Default Cloud Filesystem Disk - |-------------------------------------------------------------------------- - | - | Many applications store files both locally and in the cloud. For this - | reason, you may specify a default "cloud" driver here. This driver - | will be bound as the Cloud disk implementation in the container. - | - */ - - 'cloud' => env('FILESYSTEM_CLOUD', 's3'), + 'default' => env('FILESYSTEM_DISK', 'local'), /* |-------------------------------------------------------------------------- @@ -37,7 +24,7 @@ | may even configure multiple disks of the same driver. Defaults have | been setup for each driver as an example of the required options. | - | Supported Drivers: "local", "ftp", "sftp", "s3", "rackspace" + | Supported Drivers: "local", "ftp", "sftp", "s3" | */ @@ -56,7 +43,7 @@ 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), - 'url' => env('APP_URL').'/storage', + 'url' => env('APP_URL') . '/storage', 'visibility' => 'public', ], @@ -67,8 +54,25 @@ 'region' => env('AWS_DEFAULT_REGION'), 'bucket' => env('AWS_BUCKET'), 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), ], ], + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + ]; diff --git a/src/config/hashing.php b/src/config/hashing.php --- a/src/config/hashing.php +++ b/src/config/hashing.php @@ -44,9 +44,9 @@ */ 'argon' => [ - 'memory' => 1024, - 'threads' => 2, - 'time' => 2, + 'memory' => 65536, + 'threads' => 1, + 'time' => 4, ], ]; diff --git a/src/config/logging.php b/src/config/logging.php --- a/src/config/logging.php +++ b/src/config/logging.php @@ -1,5 +1,6 @@ env('LOG_CHANNEL', 'stack'), + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + /* |-------------------------------------------------------------------------- | Log Channels @@ -43,13 +57,13 @@ 'single' => [ 'driver' => 'single', 'path' => storage_path('logs/laravel.log'), - 'level' => 'debug', + 'level' => env('LOG_LEVEL', 'debug'), ], 'daily' => [ 'driver' => 'daily', 'path' => storage_path('logs/laravel.log'), - 'level' => 'debug', + 'level' => env('LOG_LEVEL', 'debug'), 'days' => 14, ], @@ -58,21 +72,23 @@ 'url' => env('LOG_SLACK_WEBHOOK_URL'), 'username' => 'Laravel Log', 'emoji' => ':boom:', - 'level' => 'critical', + 'level' => env('LOG_LEVEL', 'critical'), ], 'papertrail' => [ 'driver' => 'monolog', - 'level' => 'debug', - 'handler' => SyslogUdpHandler::class, + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), 'handler_with' => [ 'host' => env('PAPERTRAIL_URL'), 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://' . env('PAPERTRAIL_URL') . ':' . env('PAPERTRAIL_PORT'), ], ], 'stderr' => [ 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), 'handler' => StreamHandler::class, 'formatter' => env('LOG_STDERR_FORMATTER'), 'with' => [ @@ -82,15 +98,22 @@ 'syslog' => [ 'driver' => 'syslog', - 'level' => 'debug', + 'level' => env('LOG_LEVEL', 'debug'), ], 'errorlog' => [ 'driver' => 'errorlog', - 'level' => 'debug', + 'level' => env('LOG_LEVEL', 'debug'), + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, ], - ], - 'slow_log' => (float) env('LOG_SLOW_REQUESTS', 5), + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + ], ]; diff --git a/src/config/mail.php b/src/config/mail.php --- a/src/config/mail.php +++ b/src/config/mail.php @@ -4,45 +4,80 @@ /* |-------------------------------------------------------------------------- - | Mail Driver + | Default Mailer |-------------------------------------------------------------------------- | - | Laravel supports both SMTP and PHP's "mail" function as drivers for the - | sending of e-mail. You may specify which one you're using throughout - | your application here. By default, Laravel is setup for SMTP mail. - | - | Supported: "smtp", "sendmail", "mailgun", "mandrill", "ses", - | "sparkpost", "postmark", "log", "array" + | This option controls the default mailer that is used to send any email + | messages sent by your application. Alternative mailers may be setup + | and used as needed; however, this mailer will be used by default. | */ - 'driver' => env('MAIL_DRIVER', 'smtp'), + 'default' => env('MAIL_MAILER', 'smtp'), /* |-------------------------------------------------------------------------- - | SMTP Host Address + | Mailer Configurations |-------------------------------------------------------------------------- | - | Here you may provide the host address of the SMTP server used by your - | applications. A default option is provided that is compatible with - | the Mailgun mail service which will provide reliable deliveries. + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. | - */ - - 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), - - /* - |-------------------------------------------------------------------------- - | SMTP Host Port - |-------------------------------------------------------------------------- + | Laravel supports a variety of mail "transport" drivers to be used while + | sending an e-mail. You will specify which one you are using for your + | mailers below. You are free to add additional mailers as required. | - | This is the SMTP port used by your application to deliver e-mails to - | users of the application. Like the host we have set this value to - | stay compatible with the Mailgun e-mail application by default. + | Supported: "smtp", "sendmail", "mailgun", "ses", + | "postmark", "log", "array", "failover" | */ - 'port' => env('MAIL_PORT', 587), + 'mailers' => [ + 'smtp' => [ + 'transport' => 'smtp', + 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + 'port' => env('MAIL_PORT', 587), + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'mailgun' => [ + 'transport' => 'mailgun', + ], + + 'postmark' => [ + 'transport' => 'postmark', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -t -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + ], + ], /* |-------------------------------------------------------------------------- @@ -76,47 +111,6 @@ 'name' => env('MAIL_REPLYTO_NAME', ''), ], - /* - |-------------------------------------------------------------------------- - | E-Mail Encryption Protocol - |-------------------------------------------------------------------------- - | - | Here you may specify the encryption protocol that should be used when - | the application send e-mail messages. A sensible default using the - | transport layer security protocol should provide great security. - | - */ - - 'encryption' => env('MAIL_ENCRYPTION', 'tls'), - - /* - |-------------------------------------------------------------------------- - | SMTP Server Username - |-------------------------------------------------------------------------- - | - | If your SMTP server requires a username for authentication, you should - | set it here. This will get used to authenticate with your server on - | connection. You may also set the "password" value below this one. - | - */ - - 'username' => env('MAIL_USERNAME'), - - 'password' => env('MAIL_PASSWORD'), - - /* - |-------------------------------------------------------------------------- - | Sendmail System Path - |-------------------------------------------------------------------------- - | - | When using the "sendmail" driver to send e-mails, we will need to know - | the path to where Sendmail lives on this server. A default path has - | been provided here, which will work well on most of your systems. - | - */ - - 'sendmail' => '/usr/sbin/sendmail -bs', - /* |-------------------------------------------------------------------------- | Markdown Mail Settings @@ -132,21 +126,8 @@ 'theme' => 'default', 'paths' => [ - resource_path('views/emails'), + resource_path('views/vendor/mail'), ], ], - /* - |-------------------------------------------------------------------------- - | Log Channel - |-------------------------------------------------------------------------- - | - | If you are using the "log" driver, you may specify the logging channel - | if you prefer to keep mail messages separate from other log entries - | for simpler reading. Otherwise, the default channel will be used. - | - */ - - 'log_channel' => env('MAIL_LOG_CHANNEL'), - ]; diff --git a/src/config/octane.php b/src/config/octane.php new file mode 100644 --- /dev/null +++ b/src/config/octane.php @@ -0,0 +1,222 @@ + env('OCTANE_SERVER', 'swoole'), + + /* + |-------------------------------------------------------------------------- + | Force HTTPS + |-------------------------------------------------------------------------- + | + | When this configuration value is set to "true", Octane will inform the + | framework that all absolute links must be generated using the HTTPS + | protocol. Otherwise your links may be generated using plain HTTP. + | + */ + + 'https' => env('OCTANE_HTTPS', true), + + /* + |-------------------------------------------------------------------------- + | Octane Listeners + |-------------------------------------------------------------------------- + | + | All of the event listeners for Octane's events are defined below. These + | listeners are responsible for resetting your application's state for + | the next request. You may even add your own listeners to the list. + | + */ + + 'listeners' => [ + WorkerStarting::class => [ + EnsureUploadedFilesAreValid::class, + EnsureUploadedFilesCanBeMoved::class, + ], + + RequestReceived::class => [ + ...Octane::prepareApplicationForNextOperation(), + ...Octane::prepareApplicationForNextRequest(), + // + ], + + RequestHandled::class => [ + // + ], + + RequestTerminated::class => [ + // FlushUploadedFiles::class, + ], + + TaskReceived::class => [ + ...Octane::prepareApplicationForNextOperation(), + // + ], + + TaskTerminated::class => [ + // + ], + + TickReceived::class => [ + ...Octane::prepareApplicationForNextOperation(), + // + ], + + TickTerminated::class => [ + // + ], + + OperationTerminated::class => [ + FlushTemporaryContainerInstances::class, + // DisconnectFromDatabases::class, + CollectGarbage::class, + ], + + WorkerErrorOccurred::class => [ + ReportException::class, + StopWorkerIfNecessary::class, + ], + + WorkerStopping::class => [ + // + ], + ], + + /* + |-------------------------------------------------------------------------- + | Warm / Flush Bindings + |-------------------------------------------------------------------------- + | + | The bindings listed below will either be pre-warmed when a worker boots + | or they will be flushed before every new request. Flushing a binding + | will force the container to resolve that binding again when asked. + | + */ + + 'warm' => [ + ...Octane::defaultServicesToWarm(), + ], + + 'flush' => [ + ], + + /* + |-------------------------------------------------------------------------- + | Octane Cache Table + |-------------------------------------------------------------------------- + | + | While using Swoole, you may leverage the Octane cache, which is powered + | by a Swoole table. You may set the maximum number of rows as well as + | the number of bytes per row using the configuration options below. + | + */ + + 'cache' => [ + 'rows' => 1000, + 'bytes' => 10000, + ], + + /* + |-------------------------------------------------------------------------- + | Octane Swoole Tables + |-------------------------------------------------------------------------- + | + | While using Swoole, you may define additional tables as required by the + | application. These tables can be used to store data that needs to be + | quickly accessed by other workers on the particular Swoole server. + | + */ + + 'tables' => [ +/* + 'example:1000' => [ + 'name' => 'string:1000', + 'votes' => 'int', + ], +*/ + ], + + /* + |-------------------------------------------------------------------------- + | File Watching + |-------------------------------------------------------------------------- + | + | The following list of files and directories will be watched when using + | the --watch option offered by Octane. If any of the directories and + | files are changed, Octane will automatically reload your workers. + | + */ + + 'watch' => [ + 'app', + 'bootstrap', + 'config', + 'database', + 'public/**/*.php', + 'resources/**/*.php', + 'routes', + 'composer.lock', + '.env', + ], + + /* + |-------------------------------------------------------------------------- + | Garbage Collection Threshold + |-------------------------------------------------------------------------- + | + | When executing long-lived PHP scripts such as Octane, memory can build + | up before being cleared by PHP. You can force Octane to run garbage + | collection if your application consumes this amount of megabytes. + | + */ + + 'garbage' => 64, + + /* + |-------------------------------------------------------------------------- + | Maximum Execution Time + |-------------------------------------------------------------------------- + | + | The following setting configures the maximum execution time for requests + | being handled by Octane. You may set this value to 0 to indicate that + | there isn't a specific time limit on Octane request execution time. + | + */ + + 'max_execution_time' => 30, + +]; diff --git a/src/config/queue.php b/src/config/queue.php --- a/src/config/queue.php +++ b/src/config/queue.php @@ -39,6 +39,7 @@ 'table' => 'jobs', 'queue' => 'default', 'retry_after' => 90, + 'after_commit' => false, ], 'beanstalkd' => [ @@ -47,6 +48,7 @@ 'queue' => 'default', 'retry_after' => 90, 'block_for' => 0, + 'after_commit' => false, ], 'sqs' => [ @@ -54,8 +56,10 @@ 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), - 'queue' => env('SQS_QUEUE', 'your-queue-name'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, ], 'redis' => [ @@ -64,6 +68,7 @@ 'queue' => env('REDIS_QUEUE', 'default'), 'retry_after' => 90, 'block_for' => null, + 'after_commit' => false, ], ], diff --git a/src/config/session.php b/src/config/session.php --- a/src/config/session.php +++ b/src/config/session.php @@ -72,7 +72,7 @@ | */ - 'connection' => env('SESSION_CONNECTION', null), + 'connection' => env('SESSION_CONNECTION'), /* |-------------------------------------------------------------------------- @@ -92,13 +92,15 @@ | Session Cache Store |-------------------------------------------------------------------------- | - | When using the "apc", "memcached", or "dynamodb" session drivers you may + | While using one of the framework's cache driven session backends you may | list a cache store that should be used for these sessions. This value | must match with one of the application's configured cache "stores". | + | Affects: "apc", "dynamodb", "memcached", "redis" + | */ - 'store' => env('SESSION_STORE', null), + 'store' => env('SESSION_STORE'), /* |-------------------------------------------------------------------------- @@ -124,10 +126,7 @@ | */ - 'cookie' => env( - 'SESSION_COOKIE', - Str::slug(env('APP_NAME', 'laravel'), '_') . '_session' - ), + 'cookie' => env('SESSION_COOKIE', Str::slug(env('APP_NAME', 'laravel'), '_') . '_session'), /* |-------------------------------------------------------------------------- @@ -153,7 +152,7 @@ | */ - 'domain' => env('SESSION_DOMAIN', null), + 'domain' => env('SESSION_DOMAIN'), /* |-------------------------------------------------------------------------- @@ -162,11 +161,11 @@ | | By setting this option to true, session cookies will only be sent back | to the server if the browser has a HTTPS connection. This will keep - | the cookie from being sent to you if it can not be done securely. + | the cookie from being sent to you when it can't be done securely. | */ - 'secure' => env('SESSION_SECURE_COOKIE', false), + 'secure' => env('SESSION_SECURE_COOKIE'), /* |-------------------------------------------------------------------------- @@ -188,12 +187,12 @@ | | This option determines how your cookies behave when cross-site requests | take place, and can be used to mitigate CSRF attacks. By default, we - | do not enable this as other CSRF protection services are in place. + | will set this value to "lax" since this is a secure default value. | - | Supported: "lax", "strict" + | Supported: "lax", "strict", "none", null | */ - 'same_site' => null, + 'same_site' => 'lax', ]; diff --git a/src/config/swoole_http.php b/src/config/swoole_http.php deleted file mode 100644 --- a/src/config/swoole_http.php +++ /dev/null @@ -1,141 +0,0 @@ - [ - 'host' => env('SWOOLE_HTTP_HOST', '127.0.0.1'), - 'port' => env('SWOOLE_HTTP_PORT', '1215'), - 'public_path' => base_path('public'), - // Determine if to use swoole to respond request for static files - 'handle_static_files' => env('SWOOLE_HANDLE_STATIC', true), - 'access_log' => env('SWOOLE_HTTP_ACCESS_LOG', false), - // You must add --enable-openssl while compiling Swoole - // Put `SWOOLE_SOCK_TCP | SWOOLE_SSL` if you want to enable SSL - 'socket_type' => SWOOLE_SOCK_TCP, - 'process_type' => SWOOLE_PROCESS, - 'options' => [ - 'pid_file' => env('SWOOLE_HTTP_PID_FILE', base_path('storage/logs/swoole_http.pid')), - 'log_file' => env('SWOOLE_HTTP_LOG_FILE', base_path('storage/logs/swoole_http.log')), - 'daemonize' => env('SWOOLE_HTTP_DAEMONIZE', false), - // Normally this value should be 1~4 times larger according to your cpu cores. - 'reactor_num' => env('SWOOLE_HTTP_REACTOR_NUM', swoole_cpu_num()), - 'worker_num' => env('SWOOLE_HTTP_WORKER_NUM', swoole_cpu_num()), - 'task_worker_num' => env('SWOOLE_HTTP_TASK_WORKER_NUM', swoole_cpu_num()), - // The data to receive can't be larger than buffer_output_size. - 'package_max_length' => 20 * 1024 * 1024, - // The data to send can't be larger than buffer_output_size. - 'buffer_output_size' => 10 * 1024 * 1024, - // Max buffer size for socket connections - 'socket_buffer_size' => 128 * 1024 * 1024, - // Worker will restart after processing this number of requests - 'max_request' => 3000, - // Enable coroutine send - 'send_yield' => true, - // You must add --enable-openssl while compiling Swoole - 'ssl_cert_file' => null, - 'ssl_key_file' => null, - ], - ], - - /* - |-------------------------------------------------------------------------- - | Enable to turn on websocket server. - |-------------------------------------------------------------------------- - */ - 'websocket' => [ - 'enabled' => env('SWOOLE_HTTP_WEBSOCKET', false), - ], - - /* - |-------------------------------------------------------------------------- - | Hot reload configuration - |-------------------------------------------------------------------------- - */ - 'hot_reload' => [ - 'enabled' => env('SWOOLE_HOT_RELOAD_ENABLE', false), - 'recursively' => env('SWOOLE_HOT_RELOAD_RECURSIVELY', true), - 'directory' => env('SWOOLE_HOT_RELOAD_DIRECTORY', base_path()), - 'log' => env('SWOOLE_HOT_RELOAD_LOG', true), - 'filter' => env('SWOOLE_HOT_RELOAD_FILTER', '.php'), - ], - - /* - |-------------------------------------------------------------------------- - | Console output will be transferred to response content if enabled. - |-------------------------------------------------------------------------- - */ - 'ob_output' => env('SWOOLE_OB_OUTPUT', true), - - /* - |-------------------------------------------------------------------------- - | Pre-resolved instances here will be resolved when sandbox created. - |-------------------------------------------------------------------------- - */ - 'pre_resolved' => [ - 'view', 'files', 'session', 'session.store', 'routes', - 'db', 'db.factory', 'cache', 'cache.store', 'config', 'cookie', - 'encrypter', 'hash', 'router', 'translator', 'url', 'log', - ], - - /* - |-------------------------------------------------------------------------- - | Instances here will be cleared on every request. - |-------------------------------------------------------------------------- - */ - 'instances' => [ - 'auth', 'translator' - ], - - /* - |-------------------------------------------------------------------------- - | Providers here will be registered on every request. - |-------------------------------------------------------------------------- - */ - 'providers' => [ - Illuminate\Pagination\PaginationServiceProvider::class, - App\Providers\AuthServiceProvider::class, - //Without this passport will sort of work, - //but PassportServiceProvider will not contain a valid app instance. - App\Providers\PassportServiceProvider::class, - ], - - /* - |-------------------------------------------------------------------------- - | Resetters for sandbox app. - |-------------------------------------------------------------------------- - */ - 'resetters' => [ - SwooleTW\Http\Server\Resetters\ResetConfig::class, - SwooleTW\Http\Server\Resetters\ResetSession::class, - SwooleTW\Http\Server\Resetters\ResetCookie::class, - SwooleTW\Http\Server\Resetters\ClearInstances::class, - SwooleTW\Http\Server\Resetters\BindRequest::class, - SwooleTW\Http\Server\Resetters\RebindKernelContainer::class, - SwooleTW\Http\Server\Resetters\RebindRouterContainer::class, - SwooleTW\Http\Server\Resetters\RebindViewContainer::class, - SwooleTW\Http\Server\Resetters\ResetProviders::class, - ], - - /* - |-------------------------------------------------------------------------- - | Define your swoole tables here. - | - | @see https://www.swoole.co.uk/docs/modules/swoole-table - |-------------------------------------------------------------------------- - */ - 'tables' => [ - // 'table_name' => [ - // 'size' => 1024, - // 'columns' => [ - // ['name' => 'column_name', 'type' => Table::TYPE_STRING, 'size' => 1024], - // ] - // ], - ], -]; diff --git a/src/config/swoole_websocket.php b/src/config/swoole_websocket.php deleted file mode 100644 --- a/src/config/swoole_websocket.php +++ /dev/null @@ -1,107 +0,0 @@ - SwooleTW\Http\Websocket\SocketIO\WebsocketHandler::class, - - /* - |-------------------------------------------------------------------------- - | Default frame parser - | Replace it if you want to customize your websocket payload - |-------------------------------------------------------------------------- - */ - 'parser' => SwooleTW\Http\Websocket\SocketIO\SocketIOParser::class, - - /* - |-------------------------------------------------------------------------- - | Websocket route file path - |-------------------------------------------------------------------------- - */ - 'route_file' => base_path('routes/websocket.php'), - - /* - |-------------------------------------------------------------------------- - | Default middleware for on connect request - |-------------------------------------------------------------------------- - */ - 'middleware' => [ - SwooleTW\Http\Websocket\Middleware\DecryptCookies::class, - SwooleTW\Http\Websocket\Middleware\StartSession::class, - SwooleTW\Http\Websocket\Middleware\Authenticate::class, - ], - - /* - |-------------------------------------------------------------------------- - | Websocket handler for customized onHandShake callback - |-------------------------------------------------------------------------- - */ - 'handshake' => [ - 'enabled' => false, - 'handler' => SwooleTW\Http\Websocket\HandShakeHandler::class, - ], - - /* - |-------------------------------------------------------------------------- - | Default websocket driver - |-------------------------------------------------------------------------- - */ - 'default' => 'table', - - /* - |-------------------------------------------------------------------------- - | Websocket client's heartbeat interval (ms) - |-------------------------------------------------------------------------- - */ - 'ping_interval' => 25000, - - /* - |-------------------------------------------------------------------------- - | Websocket client's heartbeat interval timeout (ms) - |-------------------------------------------------------------------------- - */ - 'ping_timeout' => 60000, - - /* - |-------------------------------------------------------------------------- - | Room drivers mapping - |-------------------------------------------------------------------------- - */ - 'drivers' => [ - 'table' => SwooleTW\Http\Websocket\Rooms\TableRoom::class, - 'redis' => SwooleTW\Http\Websocket\Rooms\RedisRoom::class, - ], - - /* - |-------------------------------------------------------------------------- - | Room drivers settings - |-------------------------------------------------------------------------- - */ - 'settings' => [ - - 'table' => [ - 'room_rows' => 4096, - 'room_size' => 2048, - 'client_rows' => 8192, - 'client_size' => 2048, - ], - - 'redis' => [ - 'server' => [ - 'host' => env('REDIS_HOST', '127.0.0.1'), - 'password' => env('REDIS_PASSWORD', null), - 'port' => env('REDIS_PORT', 6379), - 'database' => 0, - 'persistent' => true, - ], - 'options' => [ - // - ], - 'prefix' => 'swoole:', - ], - ], -]; diff --git a/src/database/migrations/2022_01_25_100000_create_shared_folder_aliases_table.php b/src/database/migrations/2022_01_25_100000_create_shared_folder_aliases_table.php new file mode 100644 --- /dev/null +++ b/src/database/migrations/2022_01_25_100000_create_shared_folder_aliases_table.php @@ -0,0 +1,43 @@ +bigIncrements('id'); + $table->unsignedBigInteger('shared_folder_id'); + $table->string('alias'); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->useCurrent(); + + $table->unique(['alias', 'shared_folder_id']); + + $table->foreign('shared_folder_id')->references('id')->on('shared_folders') + ->onDelete('cascade')->onUpdate('cascade'); + } + ); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('shared_folder_aliases'); + } +} diff --git a/src/phpstan.neon b/src/phpstan.neon --- a/src/phpstan.neon +++ b/src/phpstan.neon @@ -8,7 +8,6 @@ - '#Call to an undefined [a-zA-Z0-9<>\\ ]+::withObjectTenantContext\(\)#' - '#Call to an undefined [a-zA-Z0-9<>\\ ]+::withSubjectTenantContext\(\)#' - '#Call to an undefined method Tests\\Browser::#' - - '#Call to an undefined method Illuminate\\Support\\Fluent::references\(\)#' level: 4 parallel: processTimeout: 300.0 @@ -17,4 +16,5 @@ - config/ - database/ - resources/ + - routes/ - tests/ diff --git a/src/phpunit.xml b/src/phpunit.xml --- a/src/phpunit.xml +++ b/src/phpunit.xml @@ -37,7 +37,7 @@ - + diff --git a/src/public/index.php b/src/public/index.php --- a/src/public/index.php +++ b/src/public/index.php @@ -1,14 +1,14 @@ - */ +use Illuminate\Contracts\Http\Kernel; +use Illuminate\Http\Request; define('LARAVEL_START', microtime(true)); +if (file_exists($maintenance = __DIR__ . '/../storage/framework/maintenance.php')) { + require $maintenance; +} + /* |-------------------------------------------------------------------------- | Register The Auto Loader @@ -21,7 +21,7 @@ | */ -require __DIR__.'/../vendor/autoload.php'; +require __DIR__ . '/../vendor/autoload.php'; /* |-------------------------------------------------------------------------- @@ -35,7 +35,7 @@ | */ -$app = require_once __DIR__.'/../bootstrap/app.php'; +$app = require_once __DIR__ . '/../bootstrap/app.php'; /* |-------------------------------------------------------------------------- @@ -49,10 +49,10 @@ | */ -$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); +$kernel = $app->make(Kernel::class); $response = $kernel->handle( - $request = Illuminate\Http\Request::capture() + $request = Request::capture() ); $response->send(); diff --git a/src/resources/js/app.js b/src/resources/js/app.js --- a/src/resources/js/app.js +++ b/src/resources/js/app.js @@ -494,7 +494,7 @@ } }) - form.find('.is-invalid:not(.listinput-widget)').first().focus() + form.find('.is-invalid:not(.list-input)').first().focus() }) } else if (data.status == 'error') { diff --git a/src/resources/lang/en/auth.php b/src/resources/lang/en/auth.php --- a/src/resources/lang/en/auth.php +++ b/src/resources/lang/en/auth.php @@ -14,6 +14,7 @@ */ 'failed' => 'Invalid username or password.', + 'password' => 'The provided password is incorrect.', 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 'logoutsuccess' => 'Successfully logged out.', diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php --- a/src/resources/lang/en/ui.php +++ b/src/resources/lang/en/ui.php @@ -124,6 +124,7 @@ 'disabled' => "disabled", 'domain' => "Domain", 'email' => "Email Address", + 'emails' => "Email Addresses", 'enabled' => "enabled", 'firstname' => "First Name", 'general' => "General", @@ -294,6 +295,7 @@ ], 'password' => [ + 'link-invalid' => "The password reset code is expired or invalid.", 'reset' => "Password Reset", 'reset-step1' => "Enter your email address to reset your password.", 'reset-step1-hint' => "You may need to check your spam folder or unblock {email}.", @@ -317,6 +319,7 @@ ], 'shf' => [ + 'aliases-none' => "This shared folder has no email aliases.", 'create' => "Create folder", 'delete' => "Delete folder", 'acl-text' => "Defines user permissions to access the shared folder.", @@ -424,6 +427,7 @@ 'pass-input' => "Enter password", 'pass-link' => "Set via link", 'pass-link-label' => "Link:", + 'pass-link-hint' => "Press Submit to activate the link", 'passwordpolicy' => "Password Policy", 'price' => "Price", 'profile-title' => "Your profile", diff --git a/src/resources/lang/en/validation.php b/src/resources/lang/en/validation.php --- a/src/resources/lang/en/validation.php +++ b/src/resources/lang/en/validation.php @@ -14,12 +14,13 @@ */ 'accepted' => 'The :attribute must be accepted.', + 'accepted_if' => 'The :attribute must be accepted when :other is :value.', 'active_url' => 'The :attribute is not a valid URL.', 'after' => 'The :attribute must be a date after :date.', 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', - 'alpha' => 'The :attribute may only contain letters.', - 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.', - 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'alpha' => 'The :attribute must only contain letters.', + 'alpha_dash' => 'The :attribute must only contain letters, numbers, dashes and underscores.', + 'alpha_num' => 'The :attribute must only contain letters and numbers.', 'array' => 'The :attribute must be an array.', 'before' => 'The :attribute must be a date before :date.', 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', @@ -31,9 +32,12 @@ ], 'boolean' => 'The :attribute field must be true or false.', 'confirmed' => 'The :attribute confirmation does not match.', + 'current_password' => 'The password is incorrect.', 'date' => 'The :attribute is not a valid date.', 'date_equals' => 'The :attribute must be a date equal to :date.', 'date_format' => 'The :attribute does not match the format :format.', + 'declined' => 'The :attribute must be declined.', + 'declined_if' => 'The :attribute must be declined when :other is :value.', 'different' => 'The :attribute and :other must be different.', 'digits' => 'The :attribute must be :digits digits.', 'digits_between' => 'The :attribute must be between :min and :max digits.', @@ -41,6 +45,7 @@ 'distinct' => 'The :attribute field has a duplicate value.', 'email' => 'The :attribute must be a valid email address.', 'ends_with' => 'The :attribute must end with one of the following: :values', + 'enum' => 'The selected :attribute is invalid.', 'exists' => 'The selected :attribute is invalid.', 'file' => 'The :attribute must be a file.', 'filled' => 'The :attribute field must have a value.', @@ -82,6 +87,7 @@ 'string' => 'The :attribute may not be greater than :max characters.', 'array' => 'The :attribute may not have more than :max items.', ], + 'mac_address' => 'The :attribute must be a valid MAC address.', 'mimes' => 'The :attribute must be a file of type: :values.', 'mimetypes' => 'The :attribute must be a file of type: :values.', 'min' => [ @@ -90,12 +96,18 @@ 'string' => 'The :attribute must be at least :min characters.', 'array' => 'The :attribute must have at least :min items.', ], + 'multiple_of' => 'The :attribute must be a multiple of :value.', 'not_in' => 'The selected :attribute is invalid.', 'not_regex' => 'The :attribute format is invalid.', 'numeric' => 'The :attribute must be a number.', 'present' => 'The :attribute field must be present.', + 'prohibited' => 'The :attribute field is prohibited.', + 'prohibited_if' => 'The :attribute field is prohibited when :other is :value.', + 'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.', + 'prohibits' => 'The :attribute field prohibits :other from being present.', 'regex' => 'The :attribute format is invalid.', 'required' => 'The :attribute field is required.', + 'required_array_keys' => 'The :attribute field must contain entries for: :values.', 'required_if' => 'The :attribute field is required when :other is :value.', 'required_unless' => 'The :attribute field is required unless :other is in :values.', 'required_with' => 'The :attribute field is required when :values is present.', @@ -111,10 +123,10 @@ ], 'starts_with' => 'The :attribute must start with one of the following: :values', 'string' => 'The :attribute must be a string.', - 'timezone' => 'The :attribute must be a valid zone.', + 'timezone' => 'The :attribute must be a valid timezone.', 'unique' => 'The :attribute has already been taken.', 'uploaded' => 'The :attribute failed to upload.', - 'url' => 'The :attribute format is invalid.', + 'url' => 'The :attribute must be a valid URL.', 'uuid' => 'The :attribute must be a valid UUID.', '2fareq' => 'Second factor code is required.', diff --git a/src/resources/vue/Admin/SharedFolder.vue b/src/resources/vue/Admin/SharedFolder.vue --- a/src/resources/vue/Admin/SharedFolder.vue +++ b/src/resources/vue/Admin/SharedFolder.vue @@ -43,6 +43,11 @@ {{ $t('form.settings') }} +
@@ -66,6 +71,29 @@
+
+
+
+ + + + + + + + + + + + + + + + +
{{ $t('form.email') }}
{{ alias }}
{{ $t('shf.aliases-none') }}
+
+
+
@@ -74,7 +102,7 @@ export default { data() { return { - folder: { config: {} } + folder: { config: {}, aliases: [] } } }, created() { diff --git a/src/resources/vue/PasswordReset.vue b/src/resources/vue/PasswordReset.vue --- a/src/resources/vue/PasswordReset.vue +++ b/src/resources/vue/PasswordReset.vue @@ -85,8 +85,7 @@ this.short_code = RegExp.$1 this.code = RegExp.$2 this.submitStep2(true) - } - else { + } else { this.$root.errorPage(404) } } @@ -109,23 +108,28 @@ }, // Submits the code to the API for verification submitStep2(bylink) { + let post = { + code: this.code, + short_code: this.short_code + } + + let params = {} + if (bylink === true) { - this.displayForm(2, false) + this.$root.startLoading() + params.ignoreErrors = true } this.$root.clearFormValidation($('#step2 form')) - axios.post('/api/auth/password-reset/verify', { - code: this.code, - short_code: this.short_code - }).then(response => { + axios.post('/api/auth/password-reset/verify', post, params).then(response => { + this.$root.stopLoading() this.userId = response.data.userId this.displayForm(3, true) }).catch(error => { if (bylink === true) { - // FIXME: display step 1, user can do nothing about it anyway - // Maybe we should display 404 error page? - this.displayForm(1, true) + this.$root.stopLoading() + this.$root.errorPage(404, '', this.$t('password.link-invalid')) } }) }, diff --git a/src/resources/vue/SharedFolder/Info.vue b/src/resources/vue/SharedFolder/Info.vue --- a/src/resources/vue/SharedFolder/Info.vue +++ b/src/resources/vue/SharedFolder/Info.vue @@ -53,10 +53,10 @@ -
- +
+
- +
{{ $t('btn.submit') }} @@ -85,18 +85,20 @@