Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2528870
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
99 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
index 7ad9c324..e1b4b78d 100644
--- a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
@@ -1,128 +1,128 @@
<?php
namespace App\Http\Controllers\API\V4\Admin;
use App\Domain;
use App\User;
use Illuminate\Http\Request;
class DomainsController extends \App\Http\Controllers\API\V4\DomainsController
{
/**
* Remove the specified domain.
*
* @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function destroy($id)
{
return $this->errorResponse(404);
}
/**
* Search for domains
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$search = trim(request()->input('search'));
$owner = trim(request()->input('owner'));
$result = collect([]);
if ($owner) {
if ($owner = User::find($owner)) {
foreach ($owner->wallets as $wallet) {
$entitlements = $wallet->entitlements()->where('entitleable_type', Domain::class)->get();
foreach ($entitlements as $entitlement) {
$domain = $entitlement->entitleable;
$result->push($domain);
}
}
$result = $result->sortBy('namespace')->values();
}
} elseif (!empty($search)) {
if ($domain = Domain::where('namespace', $search)->first()) {
$result->push($domain);
}
}
// Process the result
- $result = $result->map(function ($domain) {
- $data = $domain->toArray();
- $data = array_merge($data, self::domainStatuses($domain));
- return $data;
- });
+ $result = $result->map(
+ function ($domain) {
+ return $this->objectToClient($domain);
+ }
+ );
$result = [
'list' => $result,
'count' => count($result),
'message' => \trans('app.search-foundxdomains', ['x' => count($result)]),
];
return response()->json($result);
}
/**
* Create a domain.
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function store(Request $request)
{
return $this->errorResponse(404);
}
/**
* Suspend the domain
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function suspend(Request $request, $id)
{
$domain = Domain::find($id);
if (!$this->checkTenant($domain) || $domain->isPublic()) {
return $this->errorResponse(404);
}
$domain->suspend();
return response()->json([
'status' => 'success',
'message' => \trans('app.domain-suspend-success'),
]);
}
/**
* Un-Suspend the domain
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function unsuspend(Request $request, $id)
{
$domain = Domain::find($id);
if (!$this->checkTenant($domain) || $domain->isPublic()) {
return $this->errorResponse(404);
}
$domain->unsuspend();
return response()->json([
'status' => 'success',
'message' => \trans('app.domain-unsuspend-success'),
]);
}
}
diff --git a/src/app/Http/Controllers/API/V4/Admin/GroupsController.php b/src/app/Http/Controllers/API/V4/Admin/GroupsController.php
index 303add4f..00f9a055 100644
--- a/src/app/Http/Controllers/API/V4/Admin/GroupsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/GroupsController.php
@@ -1,119 +1,114 @@
<?php
namespace App\Http\Controllers\API\V4\Admin;
use App\Group;
use App\User;
use Illuminate\Http\Request;
class GroupsController extends \App\Http\Controllers\API\V4\GroupsController
{
/**
* Search for groups
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$search = trim(request()->input('search'));
$owner = trim(request()->input('owner'));
$result = collect([]);
if ($owner) {
if ($owner = User::find($owner)) {
foreach ($owner->wallets as $wallet) {
$wallet->entitlements()->where('entitleable_type', Group::class)->get()
->each(function ($entitlement) use ($result) {
$result->push($entitlement->entitleable);
});
}
$result = $result->sortBy('name')->values();
}
} elseif (!empty($search)) {
if ($group = Group::where('email', $search)->first()) {
$result->push($group);
}
}
// Process the result
- $result = $result->map(function ($group) {
- $data = [
- 'id' => $group->id,
- 'email' => $group->email,
- 'name' => $group->name,
- ];
-
- $data = array_merge($data, self::groupStatuses($group));
- return $data;
- });
+ $result = $result->map(
+ function ($group) {
+ return $this->objectToClient($group);
+ }
+ );
$result = [
'list' => $result,
'count' => count($result),
'message' => \trans('app.search-foundxdistlists', ['x' => count($result)]),
];
return response()->json($result);
}
/**
* Create a new group.
*
* @param \Illuminate\Http\Request $request The API request.
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function store(Request $request)
{
return $this->errorResponse(404);
}
/**
* Suspend a group
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id Group identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function suspend(Request $request, $id)
{
$group = Group::find($id);
if (!$this->checkTenant($group)) {
return $this->errorResponse(404);
}
$group->suspend();
return response()->json([
'status' => 'success',
'message' => \trans('app.distlist-suspend-success'),
]);
}
/**
* Un-Suspend a group
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id Group identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function unsuspend(Request $request, $id)
{
$group = Group::find($id);
if (!$this->checkTenant($group)) {
return $this->errorResponse(404);
}
$group->unsuspend();
return response()->json([
'status' => 'success',
'message' => \trans('app.distlist-unsuspend-success'),
]);
}
}
diff --git a/src/app/Http/Controllers/API/V4/Admin/UsersController.php b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
index b09f9026..b702e09c 100644
--- a/src/app/Http/Controllers/API/V4/Admin/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
@@ -1,383 +1,381 @@
<?php
namespace App\Http\Controllers\API\V4\Admin;
use App\Domain;
use App\Group;
use App\Sku;
use App\User;
use App\UserAlias;
use App\UserSetting;
use App\Wallet;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class UsersController extends \App\Http\Controllers\API\V4\UsersController
{
/**
* Delete a user.
*
* @param int $id User identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function destroy($id)
{
return $this->errorResponse(404);
}
/**
* Searching of user accounts.
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$search = trim(request()->input('search'));
$owner = trim(request()->input('owner'));
$result = collect([]);
if ($owner) {
$owner = User::find($owner);
if ($owner) {
$result = $owner->users(false)->orderBy('email')->get();
}
} elseif (strpos($search, '@')) {
// Search by email
$result = User::withTrashed()->where('email', $search)
->orderBy('email')
->get();
if ($result->isEmpty()) {
// Search by an alias
$user_ids = UserAlias::where('alias', $search)->get()->pluck('user_id');
// Search by an external email
$ext_user_ids = UserSetting::where('key', 'external_email')
->where('value', $search)
->get()
->pluck('user_id');
$user_ids = $user_ids->merge($ext_user_ids)->unique();
// Search by a distribution list email
if ($group = Group::withTrashed()->where('email', $search)->first()) {
$user_ids = $user_ids->merge([$group->wallet()->user_id])->unique();
}
if (!$user_ids->isEmpty()) {
$result = User::withTrashed()->whereIn('id', $user_ids)
->orderBy('email')
->get();
}
}
} elseif (is_numeric($search)) {
// Search by user ID
$user = User::withTrashed()->where('id', $search)
->first();
if ($user) {
$result->push($user);
}
} elseif (strpos($search, '.') !== false) {
// Search by domain
$domain = Domain::withTrashed()->where('namespace', $search)
->first();
if ($domain) {
if (($wallet = $domain->wallet()) && ($owner = $wallet->owner()->withTrashed()->first())) {
$result->push($owner);
}
}
// A mollie customer ID
} elseif (substr($search, 0, 4) == 'cst_') {
$setting = \App\WalletSetting::where(
[
'key' => 'mollie_id',
'value' => $search
]
)->first();
if ($setting) {
if ($wallet = $setting->wallet) {
if ($owner = $wallet->owner()->withTrashed()->first()) {
$result->push($owner);
}
}
}
// A mollie transaction ID
} elseif (substr($search, 0, 3) == 'tr_') {
$payment = \App\Payment::find($search);
if ($payment) {
if ($owner = $payment->wallet->owner()->withTrashed()->first()) {
$result->push($owner);
}
}
} elseif (!empty($search)) {
$wallet = Wallet::find($search);
if ($wallet) {
if ($owner = $wallet->owner()->withTrashed()->first()) {
$result->push($owner);
}
}
}
// Process the result
$result = $result->map(
function ($user) {
- $data = $user->toArray();
- $data = array_merge($data, self::userStatuses($user));
- return $data;
+ return $this->objectToClient($user, true);
}
);
$result = [
'list' => $result,
'count' => count($result),
'message' => \trans('app.search-foundxusers', ['x' => count($result)]),
];
return response()->json($result);
}
/**
* Reset 2-Factor Authentication for the user
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id User identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function reset2FA(Request $request, $id)
{
$user = User::find($id);
if (!$this->checkTenant($user)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canUpdate($user)) {
return $this->errorResponse(403);
}
$sku = Sku::withObjectTenantContext($user)->where('title', '2fa')->first();
// Note: we do select first, so the observer can delete
// 2FA preferences from Roundcube database, so don't
// be tempted to replace first() with delete() below
$entitlement = $user->entitlements()->where('sku_id', $sku->id)->first();
$entitlement->delete();
return response()->json([
'status' => 'success',
'message' => \trans('app.user-reset-2fa-success'),
]);
}
/**
* Set/Add a SKU for the user
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id User identifier
* @param string $sku SKU title
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function setSku(Request $request, $id, $sku)
{
// For now we allow adding the 'beta' SKU only
if ($sku != 'beta') {
return $this->errorResponse(404);
}
$user = User::find($id);
if (!$this->checkTenant($user)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canUpdate($user)) {
return $this->errorResponse(403);
}
$sku = Sku::withObjectTenantContext($user)->where('title', $sku)->first();
if (!$sku) {
return $this->errorResponse(404);
}
if ($user->entitlements()->where('sku_id', $sku->id)->first()) {
return $this->errorResponse(422, \trans('app.user-set-sku-already-exists'));
}
$user->assignSku($sku);
$entitlement = $user->entitlements()->where('sku_id', $sku->id)->first();
return response()->json([
'status' => 'success',
'message' => \trans('app.user-set-sku-success'),
'sku' => [
'cost' => $entitlement->cost,
'name' => $sku->name,
'id' => $sku->id,
]
]);
}
/**
* Display information on the user account specified by $id.
*
* @param int $id The account to show information for.
*
* @return \Illuminate\Http\JsonResponse
*/
public function show($id)
{
$user = User::find($id);
if (!$this->checkTenant($user)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($user)) {
return $this->errorResponse(403);
}
$response = $this->userResponse($user);
// Simplified Entitlement/SKU information,
// TODO: I agree this format may need to be extended in future
$response['skus'] = [];
foreach ($user->entitlements as $ent) {
$sku = $ent->sku;
if (!isset($response['skus'][$sku->id])) {
$response['skus'][$sku->id] = ['costs' => [], 'count' => 0];
}
$response['skus'][$sku->id]['count']++;
$response['skus'][$sku->id]['costs'][] = $ent->cost;
}
$response['config'] = $user->getConfig();
return response()->json($response);
}
/**
* Create a new user record.
*
* @param \Illuminate\Http\Request $request The API request.
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function store(Request $request)
{
return $this->errorResponse(404);
}
/**
* Suspend the user
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id User identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function suspend(Request $request, $id)
{
$user = User::find($id);
if (!$this->checkTenant($user)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canUpdate($user)) {
return $this->errorResponse(403);
}
$user->suspend();
return response()->json([
'status' => 'success',
'message' => \trans('app.user-suspend-success'),
]);
}
/**
* Un-Suspend the user
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id User identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function unsuspend(Request $request, $id)
{
$user = User::find($id);
if (!$this->checkTenant($user)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canUpdate($user)) {
return $this->errorResponse(403);
}
$user->unsuspend();
return response()->json([
'status' => 'success',
'message' => \trans('app.user-unsuspend-success'),
]);
}
/**
* Update user data.
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id User identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function update(Request $request, $id)
{
$user = User::find($id);
if (!$this->checkTenant($user)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canUpdate($user)) {
return $this->errorResponse(403);
}
// For now admins can change only user external email address
$rules = [];
if (array_key_exists('external_email', $request->input())) {
$rules['external_email'] = 'email';
}
// Validate input
$v = Validator::make($request->all(), $rules);
if ($v->fails()) {
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
}
// Update user settings
$settings = $request->only(array_keys($rules));
if (!empty($settings)) {
$user->setSettings($settings);
}
return response()->json([
'status' => 'success',
'message' => \trans('app.user-update-success'),
]);
}
}
diff --git a/src/app/Http/Controllers/API/V4/DomainsController.php b/src/app/Http/Controllers/API/V4/DomainsController.php
index e1223006..639e1bd6 100644
--- a/src/app/Http/Controllers/API/V4/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/DomainsController.php
@@ -1,526 +1,463 @@
<?php
namespace App\Http\Controllers\API\V4;
use App\Domain;
use App\Http\Controllers\Controller;
use App\Backends\LDAP;
use App\Rules\UserEmailDomain;
-use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
class DomainsController extends Controller
{
+ /** @var array Common object properties in the API response */
+ protected static $objectProps = ['namespace', 'status', 'type'];
+
+
/**
* Return a list of domains owned by the current user
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$user = $this->guard()->user();
- $list = [];
-
- foreach ($user->domains() as $domain) {
- if (!$domain->isPublic()) {
- $data = $domain->toArray();
- $data = array_merge($data, self::domainStatuses($domain));
- $list[] = $data;
- }
- }
- usort($list, function ($a, $b) {
- return strcmp($a['namespace'], $b['namespace']);
- });
+ $list = \collect($user->domains())
+ ->filter(function ($domain) {
+ return !$domain->isPublic();
+ })
+ ->map(function ($domain) {
+ return $this->objectToClient($domain);
+ })
+ ->sortBy('namespace')
+ ->values()
+ ->all();
return response()->json($list);
}
/**
* Show the form for creating a new domain.
*
* @return \Illuminate\Http\JsonResponse
*/
public function create()
{
return $this->errorResponse(404);
}
/**
* Confirm ownership of the specified domain (via DNS check).
*
* @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse|void
*/
public function confirm($id)
{
$domain = Domain::find($id);
if (!$this->checkTenant($domain)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($domain)) {
return $this->errorResponse(403);
}
if (!$domain->confirm()) {
return response()->json([
'status' => 'error',
'message' => \trans('app.domain-verify-error'),
]);
}
return response()->json([
'status' => 'success',
'statusInfo' => self::statusInfo($domain),
'message' => \trans('app.domain-verify-success'),
]);
}
/**
* Remove the specified domain.
*
* @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function destroy($id)
{
$domain = Domain::withEnvTenantContext()->find($id);
if (empty($domain)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canDelete($domain)) {
return $this->errorResponse(403);
}
// It is possible to delete domain only if there are no users/aliases/groups using it.
if (!$domain->isEmpty()) {
$response = ['status' => 'error', 'message' => \trans('app.domain-notempty-error')];
return response()->json($response, 422);
}
$domain->delete();
return response()->json([
'status' => 'success',
'message' => \trans('app.domain-delete-success'),
]);
}
/**
* Show the form for editing the specified domain.
*
* @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function edit($id)
{
return $this->errorResponse(404);
}
/**
* Set the domain configuration.
*
* @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse|void
*/
public function setConfig($id)
{
$domain = Domain::find($id);
if (empty($domain)) {
return $this->errorResponse(404);
}
// Only owner (or admin) has access to the domain
if (!$this->guard()->user()->canUpdate($domain)) {
return $this->errorResponse(403);
}
$errors = $domain->setConfig(request()->input());
if (!empty($errors)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
return response()->json([
'status' => 'success',
'message' => \trans('app.domain-setconfig-success'),
]);
}
/**
* Create a domain.
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function store(Request $request)
{
$current_user = $this->guard()->user();
$owner = $current_user->wallet()->owner;
if ($owner->id != $current_user->id) {
return $this->errorResponse(403);
}
// Validate the input
$v = Validator::make(
$request->all(),
[
'namespace' => ['required', 'string', new UserEmailDomain()]
]
);
if ($v->fails()) {
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
}
$namespace = \strtolower(request()->input('namespace'));
// Domain already exists
if ($domain = Domain::withTrashed()->where('namespace', $namespace)->first()) {
// Check if the domain is soft-deleted and belongs to the same user
$deleteBeforeCreate = $domain->trashed() && ($wallet = $domain->wallet())
&& $wallet->owner && $wallet->owner->id == $owner->id;
if (!$deleteBeforeCreate) {
$errors = ['namespace' => \trans('validation.domainnotavailable')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
}
if (empty($request->package) || !($package = \App\Package::withEnvTenantContext()->find($request->package))) {
$errors = ['package' => \trans('validation.packagerequired')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
if (!$package->isDomain()) {
$errors = ['package' => \trans('validation.packageinvalid')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
DB::beginTransaction();
// Force-delete the existing domain if it is soft-deleted and belongs to the same user
if (!empty($deleteBeforeCreate)) {
$domain->forceDelete();
}
// Create the domain
$domain = Domain::create([
'namespace' => $namespace,
'type' => \App\Domain::TYPE_EXTERNAL,
]);
$domain->assignPackage($package, $owner);
DB::commit();
return response()->json([
'status' => 'success',
'message' => __('app.domain-create-success'),
]);
}
/**
* Get the information about the specified domain.
*
* @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse|void
*/
public function show($id)
{
$domain = Domain::find($id);
if (!$this->checkTenant($domain)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($domain)) {
return $this->errorResponse(403);
}
- $response = $domain->toArray();
+ $response = $this->objectToClient($domain, true);
// Add hash information to the response
$response['hash_text'] = $domain->hash(Domain::HASH_TEXT);
$response['hash_cname'] = $domain->hash(Domain::HASH_CNAME);
$response['hash_code'] = $domain->hash(Domain::HASH_CODE);
// Add DNS/MX configuration for the domain
$response['dns'] = self::getDNSConfig($domain);
$response['mx'] = self::getMXConfig($domain->namespace);
// Domain configuration, e.g. spf whitelist
$response['config'] = $domain->getConfig();
// Status info
$response['statusInfo'] = self::statusInfo($domain);
// Entitlements info
$response['skus'] = \App\Entitlement::objectEntitlementsSummary($domain);
- $response = array_merge($response, self::domainStatuses($domain));
-
// Some basic information about the domain wallet
$wallet = $domain->wallet();
$response['wallet'] = $wallet->toArray();
if ($wallet->discount) {
$response['wallet']['discount'] = $wallet->discount->discount;
$response['wallet']['discount_description'] = $wallet->discount->description;
}
return response()->json($response);
}
/**
* Fetch domain status (and reload setup process)
*
* @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function status($id)
{
$domain = Domain::find($id);
if (!$this->checkTenant($domain)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($domain)) {
return $this->errorResponse(403);
}
- $response = self::statusInfo($domain);
-
- if (!empty(request()->input('refresh'))) {
- $updated = false;
- $last_step = 'none';
-
- foreach ($response['process'] as $idx => $step) {
- $last_step = $step['label'];
-
- if (!$step['state']) {
- if (!$this->execProcessStep($domain, $step['label'])) {
- break;
- }
-
- $updated = true;
- }
- }
-
- if ($updated) {
- $response = self::statusInfo($domain);
- }
-
- $success = $response['isReady'];
- $suffix = $success ? 'success' : 'error-' . $last_step;
-
- $response['status'] = $success ? 'success' : 'error';
- $response['message'] = \trans('app.process-' . $suffix);
- }
-
- $response = array_merge($response, self::domainStatuses($domain));
+ $response = $this->processStateUpdate($domain);
+ $response = array_merge($response, self::objectState($domain));
return response()->json($response);
}
/**
* Update the specified domain.
*
* @param \Illuminate\Http\Request $request
* @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function update(Request $request, $id)
{
return $this->errorResponse(404);
}
/**
* Provide DNS MX information to configure specified domain for
*/
protected static function getMXConfig(string $namespace): array
{
$entries = [];
// copy MX entries from an existing domain
if ($master = \config('dns.copyfrom')) {
// TODO: cache this lookup
foreach ((array) dns_get_record($master, DNS_MX) as $entry) {
$entries[] = sprintf(
"@\t%s\t%s\tMX\t%d %s.",
\config('dns.ttl', $entry['ttl']),
$entry['class'],
$entry['pri'],
$entry['target']
);
}
} elseif ($static = \config('dns.static')) {
$entries[] = strtr($static, array('\n' => "\n", '%s' => $namespace));
}
// display SPF settings
if ($spf = \config('dns.spf')) {
$entries[] = ';';
foreach (['TXT', 'SPF'] as $type) {
$entries[] = sprintf(
"@\t%s\tIN\t%s\t\"%s\"",
\config('dns.ttl'),
$type,
$spf
);
}
}
return $entries;
}
/**
* Provide sample DNS config for domain confirmation
*/
protected static function getDNSConfig(Domain $domain): array
{
$serial = date('Ymd01');
$hash_txt = $domain->hash(Domain::HASH_TEXT);
$hash_cname = $domain->hash(Domain::HASH_CNAME);
$hash = $domain->hash(Domain::HASH_CODE);
return [
"@ IN SOA ns1.dnsservice.com. hostmaster.{$domain->namespace}. (",
" {$serial} 10800 3600 604800 86400 )",
";",
"@ IN A <some-ip>",
"www IN A <some-ip>",
";",
"{$hash_cname}.{$domain->namespace}. IN CNAME {$hash}.{$domain->namespace}.",
"@ 3600 TXT \"{$hash_txt}\"",
];
}
/**
* Prepare domain statuses for the UI
*
* @param \App\Domain $domain Domain object
*
* @return array Statuses array
*/
- protected static function domainStatuses(Domain $domain): array
+ protected static function objectState(Domain $domain): array
{
return [
'isLdapReady' => $domain->isLdapReady(),
'isConfirmed' => $domain->isConfirmed(),
'isVerified' => $domain->isVerified(),
'isSuspended' => $domain->isSuspended(),
'isActive' => $domain->isActive(),
'isDeleted' => $domain->isDeleted() || $domain->trashed(),
];
}
/**
* Domain status (extended) information.
*
* @param \App\Domain $domain Domain object
*
* @return array Status information
*/
public static function statusInfo(Domain $domain): array
{
- $process = [];
-
// If that is not a public domain, add domain specific steps
- $steps = [
- 'domain-new' => true,
- 'domain-ldap-ready' => $domain->isLdapReady(),
- 'domain-verified' => $domain->isVerified(),
- 'domain-confirmed' => $domain->isConfirmed(),
- ];
-
- $count = count($steps);
-
- // Create a process check list
- foreach ($steps as $step_name => $state) {
- $step = [
- 'label' => $step_name,
- 'title' => \trans("app.process-{$step_name}"),
- 'state' => $state,
- ];
-
- if ($step_name == 'domain-confirmed' && !$state) {
- $step['link'] = "/domain/{$domain->id}";
- }
-
- $process[] = $step;
-
- if ($state) {
- $count--;
- }
- }
-
- $state = $count === 0 ? 'done' : 'running';
-
- // After 180 seconds assume the process is in failed state,
- // this should unlock the Refresh button in the UI
- if ($count > 0 && $domain->created_at->diffInSeconds(Carbon::now()) > 180) {
- $state = 'failed';
- }
-
- return [
- 'process' => $process,
- 'processState' => $state,
- 'isReady' => $count === 0,
- ];
+ return self::processStateInfo(
+ $domain,
+ [
+ 'domain-new' => true,
+ 'domain-ldap-ready' => $domain->isLdapReady(),
+ 'domain-verified' => $domain->isVerified(),
+ 'domain-confirmed' => [$domain->isConfirmed(), "/domain/{$domain->id}"],
+ ]
+ );
}
/**
* Execute (synchronously) specified step in a domain setup process.
*
* @param \App\Domain $domain Domain object
* @param string $step Step identifier (as in self::statusInfo())
*
* @return bool True if the execution succeeded, False otherwise
*/
public static function execProcessStep(Domain $domain, string $step): bool
{
try {
switch ($step) {
case 'domain-ldap-ready':
// Domain not in LDAP, create it
if (!$domain->isLdapReady()) {
LDAP::createDomain($domain);
$domain->status |= Domain::STATUS_LDAP_READY;
$domain->save();
}
return $domain->isLdapReady();
case 'domain-verified':
// Domain existence not verified
$domain->verify();
return $domain->isVerified();
case 'domain-confirmed':
// Domain ownership confirmation
$domain->confirm();
return $domain->isConfirmed();
}
} catch (\Exception $e) {
\Log::error($e);
}
return false;
}
}
diff --git a/src/app/Http/Controllers/API/V4/GroupsController.php b/src/app/Http/Controllers/API/V4/GroupsController.php
index 5022bf8d..d7a01ea9 100644
--- a/src/app/Http/Controllers/API/V4/GroupsController.php
+++ b/src/app/Http/Controllers/API/V4/GroupsController.php
@@ -1,570 +1,488 @@
<?php
namespace App\Http\Controllers\API\V4;
use App\Http\Controllers\Controller;
use App\Domain;
use App\Group;
use App\Rules\GroupName;
use App\User;
-use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
class GroupsController extends Controller
{
+ /** @var array Common object properties in the API response */
+ protected static $objectProps = ['email', 'name', 'status'];
+
+
/**
* Show the form for creating a new group.
*
* @return \Illuminate\Http\JsonResponse
*/
public function create()
{
return $this->errorResponse(404);
}
/**
* Delete a group.
*
* @param int $id Group identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function destroy($id)
{
$group = Group::find($id);
if (!$this->checkTenant($group)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canDelete($group)) {
return $this->errorResponse(403);
}
$group->delete();
return response()->json([
'status' => 'success',
'message' => \trans('app.distlist-delete-success'),
]);
}
/**
* Show the form for editing the specified group.
*
* @param int $id Group identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function edit($id)
{
return $this->errorResponse(404);
}
/**
* Listing of groups belonging to the authenticated user.
*
* The group-entitlements billed to the current user wallet(s)
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$user = $this->guard()->user();
$result = $user->groups()->orderBy('name')->orderBy('email')->get()
- ->map(function (Group $group) {
- $data = [
- 'id' => $group->id,
- 'email' => $group->email,
- 'name' => $group->name,
- ];
-
- $data = array_merge($data, self::groupStatuses($group));
- return $data;
+ ->map(function ($group) {
+ return $this->objectToClient($group);
});
return response()->json($result);
}
/**
* Set the group configuration.
*
* @param int $id Group identifier
*
* @return \Illuminate\Http\JsonResponse|void
*/
public function setConfig($id)
{
$group = Group::find($id);
if (!$this->checkTenant($group)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canUpdate($group)) {
return $this->errorResponse(403);
}
$errors = $group->setConfig(request()->input());
if (!empty($errors)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
return response()->json([
'status' => 'success',
'message' => \trans('app.distlist-setconfig-success'),
]);
}
/**
* Display information of a group specified by $id.
*
* @param int $id The group to show information for.
*
* @return \Illuminate\Http\JsonResponse
*/
public function show($id)
{
$group = Group::find($id);
if (!$this->checkTenant($group)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($group)) {
return $this->errorResponse(403);
}
- $response = $group->toArray();
+ $response = $this->objectToClient($group, true);
- $response = array_merge($response, self::groupStatuses($group));
$response['statusInfo'] = self::statusInfo($group);
// Group configuration, e.g. sender_policy
$response['config'] = $group->getConfig();
return response()->json($response);
}
/**
* Fetch group status (and reload setup process)
*
* @param int $id Group identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function status($id)
{
$group = Group::find($id);
if (!$this->checkTenant($group)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($group)) {
return $this->errorResponse(403);
}
- $response = self::statusInfo($group);
-
- if (!empty(request()->input('refresh'))) {
- $updated = false;
- $async = false;
- $last_step = 'none';
-
- foreach ($response['process'] as $idx => $step) {
- $last_step = $step['label'];
-
- if (!$step['state']) {
- $exec = $this->execProcessStep($group, $step['label']);
-
- if (!$exec) {
- if ($exec === null) {
- $async = true;
- }
-
- break;
- }
-
- $updated = true;
- }
- }
-
- if ($updated) {
- $response = self::statusInfo($group);
- }
-
- $success = $response['isReady'];
- $suffix = $success ? 'success' : 'error-' . $last_step;
-
- $response['status'] = $success ? 'success' : 'error';
- $response['message'] = \trans('app.process-' . $suffix);
-
- if ($async && !$success) {
- $response['processState'] = 'waiting';
- $response['status'] = 'success';
- $response['message'] = \trans('app.process-async');
- }
- }
-
- $response = array_merge($response, self::groupStatuses($group));
+ $response = $this->processStateUpdate($group);
+ $response = array_merge($response, self::objectState($group));
return response()->json($response);
}
/**
* Group status (extended) information
*
* @param \App\Group $group Group object
*
* @return array Status information
*/
public static function statusInfo(Group $group): array
{
- $process = [];
- $steps = [
- 'distlist-new' => true,
- 'distlist-ldap-ready' => $group->isLdapReady(),
- ];
-
- // Create a process check list
- foreach ($steps as $step_name => $state) {
- $step = [
- 'label' => $step_name,
- 'title' => \trans("app.process-{$step_name}"),
- 'state' => $state,
- ];
-
- $process[] = $step;
- }
-
- $domain = $group->domain();
-
- // If that is not a public domain, add domain specific steps
- if ($domain && !$domain->isPublic()) {
- $domain_status = DomainsController::statusInfo($domain);
- $process = array_merge($process, $domain_status['process']);
- }
-
- $all = count($process);
- $checked = count(array_filter($process, function ($v) {
- return $v['state'];
- }));
-
- $state = $all === $checked ? 'done' : 'running';
-
- // After 180 seconds assume the process is in failed state,
- // this should unlock the Refresh button in the UI
- if ($all !== $checked && $group->created_at->diffInSeconds(Carbon::now()) > 180) {
- $state = 'failed';
- }
-
- return [
- 'process' => $process,
- 'processState' => $state,
- 'isReady' => $all === $checked,
- ];
+ return self::processStateInfo(
+ $group,
+ [
+ 'distlist-new' => true,
+ 'distlist-ldap-ready' => $group->isLdapReady(),
+ ]
+ );
}
/**
* Create a new group record.
*
* @param \Illuminate\Http\Request $request The API request.
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function store(Request $request)
{
$current_user = $this->guard()->user();
$owner = $current_user->wallet()->owner;
if ($owner->id != $current_user->id) {
return $this->errorResponse(403);
}
$email = $request->input('email');
$members = $request->input('members');
$errors = [];
$rules = [
'name' => 'required|string|max:191',
];
// Validate group address
if ($error = GroupsController::validateGroupEmail($email, $owner)) {
$errors['email'] = $error;
} else {
list(, $domainName) = explode('@', $email);
$rules['name'] = ['required', 'string', new GroupName($owner, $domainName)];
}
// Validate the group name
$v = Validator::make($request->all(), $rules);
if ($v->fails()) {
$errors = array_merge($errors, $v->errors()->toArray());
}
// Validate members' email addresses
if (empty($members) || !is_array($members)) {
$errors['members'] = \trans('validation.listmembersrequired');
} else {
foreach ($members as $i => $member) {
if (is_string($member) && !empty($member)) {
if ($error = GroupsController::validateMemberEmail($member, $owner)) {
$errors['members'][$i] = $error;
} elseif (\strtolower($member) === \strtolower($email)) {
$errors['members'][$i] = \trans('validation.memberislist');
}
} else {
unset($members[$i]);
}
}
}
if (!empty($errors)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
DB::beginTransaction();
// Create the group
$group = new Group();
$group->name = $request->input('name');
$group->email = $email;
$group->members = $members;
$group->save();
$group->assignToWallet($owner->wallets->first());
DB::commit();
return response()->json([
'status' => 'success',
'message' => \trans('app.distlist-create-success'),
]);
}
/**
* Update a group.
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id Group identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function update(Request $request, $id)
{
$group = Group::find($id);
if (!$this->checkTenant($group)) {
return $this->errorResponse(404);
}
$current_user = $this->guard()->user();
if (!$current_user->canUpdate($group)) {
return $this->errorResponse(403);
}
$owner = $group->wallet()->owner;
$name = $request->input('name');
$members = $request->input('members');
$errors = [];
// Validate the group name
if ($name !== null && $name != $group->name) {
list(, $domainName) = explode('@', $group->email);
$rules = ['name' => ['required', 'string', new GroupName($owner, $domainName)]];
$v = Validator::make($request->all(), $rules);
if ($v->fails()) {
$errors = array_merge($errors, $v->errors()->toArray());
} else {
$group->name = $name;
}
}
// Validate members' email addresses
if (empty($members) || !is_array($members)) {
$errors['members'] = \trans('validation.listmembersrequired');
} else {
foreach ((array) $members as $i => $member) {
if (is_string($member) && !empty($member)) {
if ($error = GroupsController::validateMemberEmail($member, $owner)) {
$errors['members'][$i] = $error;
} elseif (\strtolower($member) === $group->email) {
$errors['members'][$i] = \trans('validation.memberislist');
}
} else {
unset($members[$i]);
}
}
}
if (!empty($errors)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
$group->members = $members;
$group->save();
return response()->json([
'status' => 'success',
'message' => \trans('app.distlist-update-success'),
]);
}
/**
* Execute (synchronously) specified step in a group setup process.
*
* @param \App\Group $group Group object
* @param string $step Step identifier (as in self::statusInfo())
*
* @return bool|null True if the execution succeeded, False if not, Null when
* the job has been sent to the worker (result unknown)
*/
public static function execProcessStep(Group $group, string $step): ?bool
{
try {
if (strpos($step, 'domain-') === 0) {
return DomainsController::execProcessStep($group->domain(), $step);
}
switch ($step) {
case 'distlist-ldap-ready':
// Group not in LDAP, create it
$job = new \App\Jobs\Group\CreateJob($group->id);
$job->handle();
$group->refresh();
return $group->isLdapReady();
}
} catch (\Exception $e) {
\Log::error($e);
}
return false;
}
/**
* Prepare group statuses for the UI
*
* @param \App\Group $group Group object
*
* @return array Statuses array
*/
- protected static function groupStatuses(Group $group): array
+ protected static function objectState(Group $group): array
{
return [
'isLdapReady' => $group->isLdapReady(),
'isSuspended' => $group->isSuspended(),
'isActive' => $group->isActive(),
'isDeleted' => $group->isDeleted() || $group->trashed(),
];
}
/**
* Validate an email address for use as a group email
*
* @param string $email Email address
* @param \App\User $user The group owner
*
* @return ?string Error message on validation error
*/
public static function validateGroupEmail($email, \App\User $user): ?string
{
if (empty($email)) {
return \trans('validation.required', ['attribute' => 'email']);
}
if (strpos($email, '@') === false) {
return \trans('validation.entryinvalid', ['attribute' => 'email']);
}
list($login, $domain) = explode('@', \strtolower($email));
if (strlen($login) === 0 || strlen($domain) === 0) {
return \trans('validation.entryinvalid', ['attribute' => 'email']);
}
// Check if domain exists
$domain = Domain::where('namespace', $domain)->first();
if (empty($domain)) {
return \trans('validation.domaininvalid');
}
$wallet = $domain->wallet();
// The domain must be owned by the user
if (!$wallet || !$user->wallets()->find($wallet->id)) {
return \trans('validation.domainnotavailable');
}
// Validate login part alone
$v = Validator::make(
['email' => $login],
['email' => [new \App\Rules\UserEmailLocal(true)]]
);
if ($v->fails()) {
return $v->errors()->toArray()['email'][0];
}
// Check if a user with specified address already exists
if (User::emailExists($email)) {
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']);
}
if (Group::emailExists($email)) {
return \trans('validation.entryexists', ['attribute' => 'email']);
}
return null;
}
/**
* Validate an email address for use as a group member
*
* @param string $email Email address
* @param \App\User $user The group owner
*
* @return ?string Error message on validation error
*/
public static function validateMemberEmail($email, \App\User $user): ?string
{
$v = Validator::make(
['email' => $email],
['email' => [new \App\Rules\ExternalEmail()]]
);
if ($v->fails()) {
return $v->errors()->toArray()['email'][0];
}
// A local domain user must exist
if (!User::where('email', \strtolower($email))->first()) {
list($login, $domain) = explode('@', \strtolower($email));
$domain = Domain::where('namespace', $domain)->first();
// We return an error only if the domain belongs to the group owner
if ($domain && ($wallet = $domain->wallet()) && $user->wallets()->find($wallet->id)) {
return \trans('validation.notalocaluser');
}
}
return null;
}
}
diff --git a/src/app/Http/Controllers/API/V4/Reseller/DomainsController.php b/src/app/Http/Controllers/API/V4/Reseller/DomainsController.php
index b24b5839..2c2e8332 100644
--- a/src/app/Http/Controllers/API/V4/Reseller/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/Reseller/DomainsController.php
@@ -1,55 +1,55 @@
<?php
namespace App\Http\Controllers\API\V4\Reseller;
use App\Domain;
use App\User;
class DomainsController extends \App\Http\Controllers\API\V4\Admin\DomainsController
{
/**
* Search for domains
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$search = trim(request()->input('search'));
$owner = trim(request()->input('owner'));
$result = collect([]);
if ($owner) {
if ($owner = User::withSubjectTenantContext()->find($owner)) {
foreach ($owner->wallets as $wallet) {
$entitlements = $wallet->entitlements()->where('entitleable_type', Domain::class)->get();
foreach ($entitlements as $entitlement) {
$domain = $entitlement->entitleable;
$result->push($domain);
}
}
$result = $result->sortBy('namespace')->values();
}
} elseif (!empty($search)) {
if ($domain = Domain::withSubjectTenantContext()->where('namespace', $search)->first()) {
$result->push($domain);
}
}
// Process the result
- $result = $result->map(function ($domain) {
- $data = $domain->toArray();
- $data = array_merge($data, self::domainStatuses($domain));
- return $data;
- });
+ $result = $result->map(
+ function ($domain) {
+ return $this->objectToClient($domain);
+ }
+ );
$result = [
'list' => $result,
'count' => count($result),
'message' => \trans('app.search-foundxdomains', ['x' => count($result)]),
];
return response()->json($result);
}
}
diff --git a/src/app/Http/Controllers/API/V4/Reseller/GroupsController.php b/src/app/Http/Controllers/API/V4/Reseller/GroupsController.php
index b08509b2..ee73ddf2 100644
--- a/src/app/Http/Controllers/API/V4/Reseller/GroupsController.php
+++ b/src/app/Http/Controllers/API/V4/Reseller/GroupsController.php
@@ -1,58 +1,53 @@
<?php
namespace App\Http\Controllers\API\V4\Reseller;
use App\Group;
use App\User;
class GroupsController extends \App\Http\Controllers\API\V4\Admin\GroupsController
{
/**
* Search for groups
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$search = trim(request()->input('search'));
$owner = trim(request()->input('owner'));
$result = collect([]);
if ($owner) {
if ($owner = User::withSubjectTenantContext()->find($owner)) {
foreach ($owner->wallets as $wallet) {
$wallet->entitlements()->where('entitleable_type', Group::class)->get()
->each(function ($entitlement) use ($result) {
$result->push($entitlement->entitleable);
});
}
$result = $result->sortBy('name')->values();
}
} elseif (!empty($search)) {
if ($group = Group::withSubjectTenantContext()->where('email', $search)->first()) {
$result->push($group);
}
}
// Process the result
- $result = $result->map(function ($group) {
- $data = [
- 'id' => $group->id,
- 'email' => $group->email,
- 'name' => $group->name,
- ];
-
- $data = array_merge($data, self::groupStatuses($group));
- return $data;
- });
+ $result = $result->map(
+ function ($group) {
+ return $this->objectToClient($group);
+ }
+ );
$result = [
'list' => $result,
'count' => count($result),
'message' => \trans('app.search-foundxdistlists', ['x' => count($result)]),
];
return response()->json($result);
}
}
diff --git a/src/app/Http/Controllers/API/V4/Reseller/UsersController.php b/src/app/Http/Controllers/API/V4/Reseller/UsersController.php
index 14281701..01104e0c 100644
--- a/src/app/Http/Controllers/API/V4/Reseller/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/Reseller/UsersController.php
@@ -1,108 +1,108 @@
<?php
namespace App\Http\Controllers\API\V4\Reseller;
use App\Domain;
use App\Group;
use App\User;
use App\UserAlias;
use App\UserSetting;
class UsersController extends \App\Http\Controllers\API\V4\Admin\UsersController
{
/**
* Searching of user accounts.
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$search = trim(request()->input('search'));
$owner = trim(request()->input('owner'));
$result = collect([]);
if ($owner) {
$owner = User::where('id', $owner)
->withSubjectTenantContext()
->whereNull('role')
->first();
if ($owner) {
$result = $owner->users(false)->whereNull('role')->orderBy('email')->get();
}
} elseif (strpos($search, '@')) {
// Search by email
$result = User::withTrashed()->where('email', $search)
->withSubjectTenantContext()
->whereNull('role')
->orderBy('email')
->get();
if ($result->isEmpty()) {
// Search by an alias
$user_ids = UserAlias::where('alias', $search)->get()->pluck('user_id');
// Search by an external email
$ext_user_ids = UserSetting::where('key', 'external_email')
->where('value', $search)
->get()
->pluck('user_id');
$user_ids = $user_ids->merge($ext_user_ids)->unique();
// Search by a distribution list email
if ($group = Group::withTrashed()->where('email', $search)->first()) {
$user_ids = $user_ids->merge([$group->wallet()->user_id])->unique();
}
if (!$user_ids->isEmpty()) {
$result = User::withTrashed()->whereIn('id', $user_ids)
->withSubjectTenantContext()
->whereNull('role')
->orderBy('email')
->get();
}
}
} elseif (is_numeric($search)) {
// Search by user ID
$user = User::withTrashed()->where('id', $search)
->withSubjectTenantContext()
->whereNull('role')
->first();
if ($user) {
$result->push($user);
}
} elseif (!empty($search)) {
// Search by domain
$domain = Domain::withTrashed()->where('namespace', $search)
->withSubjectTenantContext()
->first();
if ($domain) {
if (
($wallet = $domain->wallet())
&& ($owner = $wallet->owner()->withTrashed()->withSubjectTenantContext()->first())
&& empty($owner->role)
) {
$result->push($owner);
}
}
}
// Process the result
- $result = $result->map(function ($user) {
- $data = $user->toArray();
- $data = array_merge($data, self::userStatuses($user));
- return $data;
- });
+ $result = $result->map(
+ function ($user) {
+ return $this->objectToClient($user, true);
+ }
+ );
$result = [
'list' => $result,
'count' => count($result),
'message' => \trans('app.search-foundxusers', ['x' => count($result)]),
];
return response()->json($result);
}
}
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
index 0687e308..789a5747 100644
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -1,878 +1,803 @@
<?php
namespace App\Http\Controllers\API\V4;
use App\Http\Controllers\Controller;
use App\Domain;
use App\Group;
use App\Rules\UserEmailDomain;
use App\Rules\UserEmailLocal;
use App\Sku;
use App\User;
-use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
class UsersController extends Controller
{
/** @const array List of user setting keys available for modification in UI */
public const USER_SETTINGS = [
'billing_address',
'country',
'currency',
'external_email',
'first_name',
'last_name',
'organization',
'phone',
];
/**
* On user create it is filled with a user or group object to force-delete
* before the creation of a new user record is possible.
*
* @var \App\User|\App\Group|null
*/
protected $deleteBeforeCreate;
+ /** @var array Common object properties in the API response */
+ protected static $objectProps = ['email', 'status'];
+
/**
* Delete a user.
*
* @param int $id User identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function destroy($id)
{
$user = User::withEnvTenantContext()->find($id);
if (empty($user)) {
return $this->errorResponse(404);
}
// User can't remove himself until he's the controller
if (!$this->guard()->user()->canDelete($user)) {
return $this->errorResponse(403);
}
$user->delete();
return response()->json([
'status' => 'success',
'message' => \trans('app.user-delete-success'),
]);
}
/**
* Listing of users.
*
* The user-entitlements billed to the current user wallet(s)
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$user = $this->guard()->user();
$search = trim(request()->input('search'));
$page = intval(request()->input('page')) ?: 1;
$pageSize = 20;
$hasMore = false;
$result = $user->users();
// Search by user email, alias or name
if (strlen($search) > 0) {
// thanks to cloning we skip some extra queries in $user->users()
$allUsers1 = clone $result;
$allUsers2 = clone $result;
$result->whereLike('email', $search)
->union(
$allUsers1->join('user_aliases', 'users.id', '=', 'user_aliases.user_id')
->whereLike('alias', $search)
)
->union(
$allUsers2->join('user_settings', 'users.id', '=', 'user_settings.user_id')
->whereLike('value', $search)
->whereIn('key', ['first_name', 'last_name'])
);
}
$result = $result->orderBy('email')
->limit($pageSize + 1)
->offset($pageSize * ($page - 1))
->get();
if (count($result) > $pageSize) {
$result->pop();
$hasMore = true;
}
// Process the result
$result = $result->map(
function ($user) {
- $data = $user->toArray();
- $data = array_merge($data, self::userStatuses($user));
- return $data;
+ return $this->objectToClient($user);
}
);
$result = [
'list' => $result,
'count' => count($result),
'hasMore' => $hasMore,
];
return response()->json($result);
}
/**
* Set user config.
*
* @param int $id The user
*
* @return \Illuminate\Http\JsonResponse
*/
public function setConfig($id)
{
$user = User::find($id);
if (empty($user)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canUpdate($user)) {
return $this->errorResponse(403);
}
$errors = $user->setConfig(request()->input());
if (!empty($errors)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
return response()->json([
'status' => 'success',
'message' => \trans('app.user-setconfig-success'),
]);
}
/**
* Display information on the user account specified by $id.
*
* @param int $id The account to show information for.
*
* @return \Illuminate\Http\JsonResponse
*/
public function show($id)
{
$user = User::withEnvTenantContext()->find($id);
if (empty($user)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($user)) {
return $this->errorResponse(403);
}
$response = $this->userResponse($user);
$response['skus'] = \App\Entitlement::objectEntitlementsSummary($user);
$response['config'] = $user->getConfig();
return response()->json($response);
}
/**
* Fetch user status (and reload setup process)
*
* @param int $id User identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function status($id)
{
$user = User::withEnvTenantContext()->find($id);
if (empty($user)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($user)) {
return $this->errorResponse(403);
}
- $response = self::statusInfo($user);
-
- if (!empty(request()->input('refresh'))) {
- $updated = false;
- $async = false;
- $last_step = 'none';
-
- foreach ($response['process'] as $idx => $step) {
- $last_step = $step['label'];
-
- if (!$step['state']) {
- $exec = $this->execProcessStep($user, $step['label']);
-
- if (!$exec) {
- if ($exec === null) {
- $async = true;
- }
-
- break;
- }
-
- $updated = true;
- }
- }
-
- if ($updated) {
- $response = self::statusInfo($user);
- }
-
- $success = $response['isReady'];
- $suffix = $success ? 'success' : 'error-' . $last_step;
-
- $response['status'] = $success ? 'success' : 'error';
- $response['message'] = \trans('app.process-' . $suffix);
-
- if ($async && !$success) {
- $response['processState'] = 'waiting';
- $response['status'] = 'success';
- $response['message'] = \trans('app.process-async');
- }
- }
-
- $response = array_merge($response, self::userStatuses($user));
+ $response = $this->processStateUpdate($user);
+ $response = array_merge($response, self::objectState($user));
return response()->json($response);
}
/**
* User status (extended) information
*
* @param \App\User $user User object
*
* @return array Status information
*/
public static function statusInfo(User $user): array
{
- $process = [];
- $steps = [
- 'user-new' => true,
- 'user-ldap-ready' => $user->isLdapReady(),
- 'user-imap-ready' => $user->isImapReady(),
- ];
-
- // Create a process check list
- foreach ($steps as $step_name => $state) {
- $step = [
- 'label' => $step_name,
- 'title' => \trans("app.process-{$step_name}"),
- 'state' => $state,
- ];
-
- $process[] = $step;
- }
-
- list ($local, $domain) = explode('@', $user->email);
- $domain = Domain::where('namespace', $domain)->first();
-
- // If that is not a public domain, add domain specific steps
- if ($domain && !$domain->isPublic()) {
- $domain_status = DomainsController::statusInfo($domain);
- $process = array_merge($process, $domain_status['process']);
- }
-
- $all = count($process);
- $checked = count(array_filter($process, function ($v) {
- return $v['state'];
- }));
-
- $state = $all === $checked ? 'done' : 'running';
-
- // After 180 seconds assume the process is in failed state,
- // this should unlock the Refresh button in the UI
- if ($all !== $checked && $user->created_at->diffInSeconds(Carbon::now()) > 180) {
- $state = 'failed';
- }
+ $process = self::processStateInfo(
+ $user,
+ [
+ 'user-new' => true,
+ 'user-ldap-ready' => $user->isLdapReady(),
+ 'user-imap-ready' => $user->isImapReady(),
+ ]
+ );
// Check if the user is a controller of his wallet
$isController = $user->canDelete($user);
$hasCustomDomain = $user->wallet()->entitlements()
->where('entitleable_type', Domain::class)
->count() > 0;
// Get user's entitlements titles
$skus = $user->entitlements()->select('skus.title')
->join('skus', 'skus.id', '=', 'entitlements.sku_id')
->get()
->pluck('title')
->sort()
->unique()
->values()
->all();
- return [
+ $result = [
'skus' => $skus,
// TODO: This will change when we enable all users to create domains
'enableDomains' => $isController && $hasCustomDomain,
// TODO: Make 'enableDistlists' working for wallet controllers that aren't account owners
'enableDistlists' => $isController && $hasCustomDomain && in_array('distlist', $skus),
'enableUsers' => $isController,
'enableWallets' => $isController,
- 'process' => $process,
- 'processState' => $state,
- 'isReady' => $all === $checked,
];
+
+ return array_merge($process, $result);
}
/**
* Create a new user record.
*
* @param \Illuminate\Http\Request $request The API request.
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function store(Request $request)
{
$current_user = $this->guard()->user();
$owner = $current_user->wallet()->owner;
if ($owner->id != $current_user->id) {
return $this->errorResponse(403);
}
$this->deleteBeforeCreate = null;
if ($error_response = $this->validateUserRequest($request, null, $settings)) {
return $error_response;
}
if (empty($request->package) || !($package = \App\Package::withEnvTenantContext()->find($request->package))) {
$errors = ['package' => \trans('validation.packagerequired')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
if ($package->isDomain()) {
$errors = ['package' => \trans('validation.packageinvalid')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
DB::beginTransaction();
// @phpstan-ignore-next-line
if ($this->deleteBeforeCreate) {
$this->deleteBeforeCreate->forceDelete();
}
// Create user record
$user = User::create([
'email' => $request->email,
'password' => $request->password,
]);
$owner->assignPackage($package, $user);
if (!empty($settings)) {
$user->setSettings($settings);
}
if (!empty($request->aliases)) {
$user->setAliases($request->aliases);
}
DB::commit();
return response()->json([
'status' => 'success',
'message' => \trans('app.user-create-success'),
]);
}
/**
* Update user data.
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id User identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function update(Request $request, $id)
{
$user = User::withEnvTenantContext()->find($id);
if (empty($user)) {
return $this->errorResponse(404);
}
$current_user = $this->guard()->user();
// TODO: Decide what attributes a user can change on his own profile
if (!$current_user->canUpdate($user)) {
return $this->errorResponse(403);
}
if ($error_response = $this->validateUserRequest($request, $user, $settings)) {
return $error_response;
}
// Entitlements, only controller can do that
if ($request->skus !== null && !$current_user->canDelete($user)) {
return $this->errorResponse(422, "You have no permission to change entitlements");
}
DB::beginTransaction();
$this->updateEntitlements($user, $request->skus);
if (!empty($settings)) {
$user->setSettings($settings);
}
if (!empty($request->password)) {
$user->password = $request->password;
$user->save();
}
if (isset($request->aliases)) {
$user->setAliases($request->aliases);
}
// TODO: Make sure that UserUpdate job is created in case of entitlements update
// and no password change. So, for example quota change is applied to LDAP
// TODO: Review use of $user->save() in the above context
DB::commit();
$response = [
'status' => 'success',
'message' => \trans('app.user-update-success'),
];
// For self-update refresh the statusInfo in the UI
if ($user->id == $current_user->id) {
$response['statusInfo'] = self::statusInfo($user);
}
return response()->json($response);
}
/**
* Update user entitlements.
*
* @param \App\User $user The user
* @param array $rSkus List of SKU IDs requested for the user in the form [id=>qty]
*/
protected function updateEntitlements(User $user, $rSkus)
{
if (!is_array($rSkus)) {
return;
}
// list of skus, [id=>obj]
$skus = Sku::withEnvTenantContext()->get()->mapWithKeys(
function ($sku) {
return [$sku->id => $sku];
}
);
// existing entitlement's SKUs
$eSkus = [];
$user->entitlements()->groupBy('sku_id')
->selectRaw('count(*) as total, sku_id')->each(
function ($e) use (&$eSkus) {
$eSkus[$e->sku_id] = $e->total;
}
);
foreach ($skus as $skuID => $sku) {
$e = array_key_exists($skuID, $eSkus) ? $eSkus[$skuID] : 0;
$r = array_key_exists($skuID, $rSkus) ? $rSkus[$skuID] : 0;
if ($sku->handler_class == \App\Handlers\Mailbox::class) {
if ($r != 1) {
throw new \Exception("Invalid quantity of mailboxes");
}
}
if ($e > $r) {
// remove those entitled more than existing
$user->removeSku($sku, ($e - $r));
} elseif ($e < $r) {
// add those requested more than entitled
$user->assignSku($sku, ($r - $e));
}
}
}
/**
* Create a response data array for specified user.
*
* @param \App\User $user User object
*
* @return array Response data
*/
public static function userResponse(User $user): array
{
- $response = $user->toArray();
+ $response = self::objectToClient($user, true);
// Settings
$response['settings'] = [];
foreach ($user->settings()->whereIn('key', self::USER_SETTINGS)->get() as $item) {
$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);
- $response = array_merge($response, self::userStatuses($user));
-
// Add more info to the wallet object output
$map_func = function ($wallet) use ($user) {
$result = $wallet->toArray();
if ($wallet->discount) {
$result['discount'] = $wallet->discount->discount;
$result['discount_description'] = $wallet->discount->description;
}
if ($wallet->user_id != $user->id) {
$result['user_email'] = $wallet->owner->email;
}
$provider = \App\Providers\PaymentProvider::factory($wallet);
$result['provider'] = $provider->name();
return $result;
};
// Information about wallets and accounts for access checks
$response['wallets'] = $user->wallets->map($map_func)->toArray();
$response['accounts'] = $user->accounts->map($map_func)->toArray();
$response['wallet'] = $map_func($user->wallet());
return $response;
}
/**
* Prepare user statuses for the UI
*
* @param \App\User $user User object
*
* @return array Statuses array
*/
- protected static function userStatuses(User $user): array
+ protected static function objectState(User $user): array
{
return [
'isImapReady' => $user->isImapReady(),
'isLdapReady' => $user->isLdapReady(),
'isSuspended' => $user->isSuspended(),
'isActive' => $user->isActive(),
'isDeleted' => $user->isDeleted() || $user->trashed(),
];
}
/**
* Validate user input
*
* @param \Illuminate\Http\Request $request The API request.
* @param \App\User|null $user User identifier
* @param array $settings User settings (from the request)
*
* @return \Illuminate\Http\JsonResponse|null The error response on error
*/
protected function validateUserRequest(Request $request, $user, &$settings = [])
{
$rules = [
'external_email' => 'nullable|email',
'phone' => 'string|nullable|max:64|regex:/^[0-9+() -]+$/',
'first_name' => 'string|nullable|max:128',
'last_name' => 'string|nullable|max:128',
'organization' => 'string|nullable|max:512',
'billing_address' => 'string|nullable|max:1024',
'country' => 'string|nullable|alpha|size:2',
'currency' => 'string|nullable|alpha|size:3',
'aliases' => 'array|nullable',
];
if (empty($user) || !empty($request->password) || !empty($request->password_confirmation)) {
$rules['password'] = 'required|min:4|max:2048|confirmed';
}
$errors = [];
// Validate input
$v = Validator::make($request->all(), $rules);
if ($v->fails()) {
$errors = $v->errors()->toArray();
}
$controller = $user ? $user->wallet()->owner : $this->guard()->user();
// For new user validate email address
if (empty($user)) {
$email = $request->email;
if (empty($email)) {
$errors['email'] = \trans('validation.required', ['attribute' => 'email']);
} elseif ($error = self::validateEmail($email, $controller, $this->deleteBeforeCreate)) {
$errors['email'] = $error;
}
}
// Validate aliases input
if (isset($request->aliases)) {
$aliases = [];
$existing_aliases = $user ? $user->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 (new user)
if (!empty($email) && Str::lower($alias) == Str::lower($email)) {
continue;
}
// validate new aliases
if (
!in_array($alias, $existing_aliases)
&& ($error = self::validateAlias($alias, $controller))
) {
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);
}
// Update user settings
$settings = $request->only(array_keys($rules));
unset($settings['password'], $settings['aliases'], $settings['email']);
return null;
}
/**
* Execute (synchronously) specified step in a user setup process.
*
* @param \App\User $user User object
* @param string $step Step identifier (as in self::statusInfo())
*
* @return bool|null True if the execution succeeded, False if not, Null when
* the job has been sent to the worker (result unknown)
*/
public static function execProcessStep(User $user, string $step): ?bool
{
try {
if (strpos($step, 'domain-') === 0) {
list ($local, $domain) = explode('@', $user->email);
$domain = Domain::where('namespace', $domain)->first();
return DomainsController::execProcessStep($domain, $step);
}
switch ($step) {
case 'user-ldap-ready':
// User not in LDAP, create it
$job = new \App\Jobs\User\CreateJob($user->id);
$job->handle();
$user->refresh();
return $user->isLdapReady();
case 'user-imap-ready':
// User not in IMAP? Verify again
// Do it synchronously if the imap admin credentials are available
// otherwise let the worker do the job
if (!\config('imap.admin_password')) {
\App\Jobs\User\VerifyJob::dispatch($user->id);
return null;
}
$job = new \App\Jobs\User\VerifyJob($user->id);
$job->handle();
$user->refresh();
return $user->isImapReady();
}
} catch (\Exception $e) {
\Log::error($e);
}
return false;
}
/**
* Email address validation for use as a user mailbox (login).
*
* @param string $email Email address
* @param \App\User $user The account owner
* @param null|\App\User|\App\Group $deleted Filled with an instance of a deleted user or group
* with the specified email address, if exists
*
* @return ?string Error message on validation error
*/
public static function validateEmail(string $email, \App\User $user, &$deleted = null): ?string
{
$deleted = null;
if (strpos($email, '@') === false) {
return \trans('validation.entryinvalid', ['attribute' => 'email']);
}
list($login, $domain) = explode('@', Str::lower($email));
if (strlen($login) === 0 || strlen($domain) === 0) {
return \trans('validation.entryinvalid', ['attribute' => 'email']);
}
// Check if domain exists
$domain = Domain::withEnvTenantContext()->where('namespace', $domain)->first();
if (empty($domain)) {
return \trans('validation.domaininvalid');
}
// Validate login part alone
$v = Validator::make(
['email' => $login],
['email' => ['required', new UserEmailLocal(!$domain->isPublic())]]
);
if ($v->fails()) {
return $v->errors()->toArray()['email'][0];
}
// Check if it is one of domains available to the user
$domains = \collect($user->domains())->pluck('namespace')->all();
if (!in_array($domain->namespace, $domains)) {
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 with specified address already exists
if ($existing_group = Group::emailExists($email, true)) {
// If this is a deleted group in the same custom domain
// we'll force delete it before
if (!$domain->isPublic() && $existing_group->trashed()) {
$deleted = $existing_group;
} else {
return \trans('validation.entryexists', ['attribute' => 'email']);
}
}
return null;
}
/**
* Email address validation for use as an alias.
*
* @param string $email Email address
* @param \App\User $user The account owner
*
* @return ?string Error message on validation error
*/
public static function validateAlias(string $email, \App\User $user): ?string
{
if (strpos($email, '@') === false) {
return \trans('validation.entryinvalid', ['attribute' => 'alias']);
}
list($login, $domain) = explode('@', Str::lower($email));
if (strlen($login) === 0 || strlen($domain) === 0) {
return \trans('validation.entryinvalid', ['attribute' => 'alias']);
}
// Check if domain exists
$domain = Domain::withEnvTenantContext()->where('namespace', $domain)->first();
if (empty($domain)) {
return \trans('validation.domaininvalid');
}
// Validate login part alone
$v = Validator::make(
['alias' => $login],
['alias' => ['required', new UserEmailLocal(!$domain->isPublic())]]
);
if ($v->fails()) {
return $v->errors()->toArray()['alias'][0];
}
// Check if it is one of domains available to the user
$domains = \collect($user->domains())->pluck('namespace')->all();
if (!in_array($domain->namespace, $domains)) {
return \trans('validation.entryexists', ['attribute' => 'domain']);
}
// Check if a user with specified address already exists
if ($existing_user = User::emailExists($email, true)) {
// Allow an alias in a custom domain to an address that was a user before
if ($domain->isPublic() || !$existing_user->trashed()) {
return \trans('validation.entryexists', ['attribute' => 'alias']);
}
}
// Check if an alias with specified address already exists
if (User::aliasExists($email)) {
// Allow assigning the same alias to a user in the same group account,
// but only for non-public domains
if ($domain->isPublic()) {
return \trans('validation.entryexists', ['attribute' => 'alias']);
}
}
// 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/Controller.php b/src/app/Http/Controllers/Controller.php
index 73e7c7d5..6bbe4b58 100644
--- a/src/app/Http/Controllers/Controller.php
+++ b/src/app/Http/Controllers/Controller.php
@@ -1,84 +1,226 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
class Controller extends BaseController
{
use AuthorizesRequests;
use DispatchesJobs;
use ValidatesRequests;
+ /** @var array Common object properties in the API response */
+ protected static $objectProps = [];
+
/**
* Common error response builder for API (JSON) responses
*
* @param int $code Error code
* @param string $message Error message
* @param array $data Additional response data
*
* @return \Illuminate\Http\JsonResponse
*/
public static function errorResponse(int $code, string $message = null, array $data = [])
{
$errors = [
400 => "Bad request",
401 => "Unauthorized",
403 => "Access denied",
404 => "Not found",
405 => "Method not allowed",
422 => "Input validation error",
429 => "Too many requests",
500 => "Internal server error",
];
$response = [
'status' => 'error',
'message' => $message ?: (isset($errors[$code]) ? $errors[$code] : "Server error"),
];
if (!empty($data)) {
$response = $response + $data;
}
return response()->json($response, $code);
}
/**
* Check if current user has access to the specified object
* by being an admin or existing in the same tenant context.
*
* @param ?object $object Model object
*
* @return bool
*/
protected function checkTenant(object $object = null): bool
{
if (empty($object)) {
return false;
}
$user = $this->guard()->user();
if ($user->role == 'admin') {
return true;
}
return $object->tenant_id == $user->tenant_id;
}
/**
* Get the guard to be used during authentication.
*
* @return \Illuminate\Contracts\Auth\Guard
*/
protected function guard()
{
return Auth::guard();
}
+
+ /**
+ * Object status' process information.
+ *
+ * @param object $object The object to process
+ * @param array $steps The steps definition
+ *
+ * @return array Process state information
+ */
+ protected static function processStateInfo($object, array $steps): array
+ {
+ $process = [];
+
+ // Create a process check list
+ foreach ($steps as $step_name => $state) {
+ $step = [
+ 'label' => $step_name,
+ 'title' => \trans("app.process-{$step_name}"),
+ ];
+
+ if (is_array($state)) {
+ $step['link'] = $state[1];
+ $state = $state[0];
+ }
+
+ $step['state'] = $state;
+
+ $process[] = $step;
+ }
+
+ // Add domain specific steps
+ if (method_exists($object, 'domain')) {
+ $domain = $object->domain();
+
+ // If that is not a public domain
+ if ($domain && !$domain->isPublic()) {
+ $domain_status = API\V4\DomainsController::statusInfo($domain);
+ $process = array_merge($process, $domain_status['process']);
+ }
+ }
+
+ $all = count($process);
+ $checked = count(array_filter($process, function ($v) {
+ return $v['state'];
+ }));
+
+ $state = $all === $checked ? 'done' : 'running';
+
+ // After 180 seconds assume the process is in failed state,
+ // this should unlock the Refresh button in the UI
+ if ($all !== $checked && $object->created_at->diffInSeconds(\Carbon\Carbon::now()) > 180) {
+ $state = 'failed';
+ }
+
+ return [
+ 'process' => $process,
+ 'processState' => $state,
+ 'isReady' => $all === $checked,
+ ];
+ }
+
+ /**
+ * Object status' process information update.
+ *
+ * @param object $object The object to process
+ *
+ * @return array Process state information
+ */
+ protected function processStateUpdate($object): array
+ {
+ $response = $this->statusInfo($object); // @phpstan-ignore-line
+
+ if (!empty(request()->input('refresh'))) {
+ $updated = false;
+ $async = false;
+ $last_step = 'none';
+
+ foreach ($response['process'] as $idx => $step) {
+ $last_step = $step['label'];
+
+ if (!$step['state']) {
+ $exec = $this->execProcessStep($object, $step['label']); // @phpstan-ignore-line
+
+ if (!$exec) {
+ if ($exec === null) {
+ $async = true;
+ }
+
+ break;
+ }
+
+ $updated = true;
+ }
+ }
+
+ if ($updated) {
+ $response = $this->statusInfo($object); // @phpstan-ignore-line
+ }
+
+ $success = $response['isReady'];
+ $suffix = $success ? 'success' : 'error-' . $last_step;
+
+ $response['status'] = $success ? 'success' : 'error';
+ $response['message'] = \trans('app.process-' . $suffix);
+
+ if ($async && !$success) {
+ $response['processState'] = 'waiting';
+ $response['status'] = 'success';
+ $response['message'] = \trans('app.process-async');
+ }
+ }
+
+ return $response;
+ }
+
+ /**
+ * Prepare an object for the UI.
+ *
+ * @param object $object An object
+ * @param bool $full Include all object properties
+ *
+ * @return array Object information
+ */
+ protected static function objectToClient($object, bool $full = false): array
+ {
+ if ($full) {
+ $result = $object->toArray();
+ } else {
+ $result = ['id' => $object->id];
+
+ foreach (static::$objectProps as $prop) {
+ $result[$prop] = $object->{$prop};
+ }
+ }
+
+ $result = array_merge($result, static::objectState($object)); // @phpstan-ignore-line
+
+ return $result;
+ }
}
diff --git a/src/tests/Unit/Controller/DomainsTest.php b/src/tests/Unit/Controller/DomainsTest.php
deleted file mode 100644
index dd676c73..00000000
--- a/src/tests/Unit/Controller/DomainsTest.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-namespace Tests\Unit\Controller;
-
-use App\Domain;
-use Tests\TestCase;
-
-class DomainsTest extends TestCase
-{
- /**
- * Test DomainsController::domainStatuses()
- */
- public function testDomainStatuses(): void
- {
- $this->markTestIncomplete();
- }
-
- /**
- * Test DomainsController::statusInfo()
- */
- public function testStatusInfo(): void
- {
- $this->markTestIncomplete();
- }
-}
diff --git a/src/tests/Unit/Controller/UsersTest.php b/src/tests/Unit/Controller/UsersTest.php
deleted file mode 100644
index 89e0e5ae..00000000
--- a/src/tests/Unit/Controller/UsersTest.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-namespace Tests\Unit\Controller;
-
-use App\User;
-use Tests\TestCase;
-
-class UsersTest extends TestCase
-{
- /**
- * Test UsersController::userStatuses()
- */
- public function testUserStatuses(): void
- {
- $this->markTestIncomplete();
- }
-
- /**
- * Test UsersController::statusInfo()
- */
- public function testStatusInfo(): void
- {
- $this->markTestIncomplete();
- }
-}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Feb 2, 1:04 AM (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
426799
Default Alt Text
(99 KB)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment