Page MenuHomePhorge

D2906.1775256017.diff
No OneTemporary

Authored By
Unknown
Size
25 KB
Referenced Files
None
Subscribers
None

D2906.1775256017.diff

diff --git a/src/app/Domain.php b/src/app/Domain.php
--- a/src/app/Domain.php
+++ b/src/app/Domain.php
@@ -381,6 +381,31 @@
}
/**
+ * Checks if there are any objects (users/aliases/groups) in a domain.
+ * Note: Public domains are always reported not empty.
+ *
+ * @return bool True if there are no objects assigned, False otherwise
+ */
+ public function isEmpty(): bool
+ {
+ if ($this->isPublic()) {
+ return false;
+ }
+
+ // FIXME: These queries will not use indexes, so maybe we should consider
+ // wallet/entitlements to search in objects that belong to this domain account?
+
+ $suffix = '@' . $this->namespace;
+ $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()
+ );
+ }
+
+ /**
* Any (additional) properties of this domain.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
@@ -436,8 +461,9 @@
/**
* List the users of a domain, so long as the domain is not a public registration domain.
+ * Note: It returns only users with a mailbox.
*
- * @return array
+ * @return \App\User[] A list of users
*/
public function users(): array
{
diff --git a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
--- a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
@@ -9,6 +9,18 @@
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
diff --git a/src/app/Http/Controllers/API/V4/DomainsController.php b/src/app/Http/Controllers/API/V4/DomainsController.php
--- a/src/app/Http/Controllers/API/V4/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/DomainsController.php
@@ -39,7 +39,7 @@
}
/**
- * Show the form for creating a new resource.
+ * Show the form for creating a new domain.
*
* @return \Illuminate\Http\JsonResponse
*/
@@ -82,21 +82,42 @@
}
/**
- * Remove the specified resource from storage.
+ * Remove the specified domain.
*
- * @param int $id
+ * @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse
*/
public function destroy($id)
{
- return $this->errorResponse(404);
+ $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 resource.
+ * Show the form for editing the specified domain.
*
- * @param int $id
+ * @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse
*/
@@ -168,9 +189,15 @@
$namespace = \strtolower(request()->input('namespace'));
// Domain already exists
- if (Domain::withTrashed()->where('namespace', $namespace)->exists()) {
- $errors = ['namespace' => \trans('validation.domainnotavailable')];
- return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ 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))) {
@@ -185,7 +212,10 @@
DB::beginTransaction();
- // TODO: Force-delete domain if it is soft-deleted and belongs to the same user
+ // 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([
@@ -309,10 +339,10 @@
}
/**
- * Update the specified resource in storage.
+ * Update the specified domain.
*
* @param \Illuminate\Http\Request $request
- * @param int $id
+ * @param int $id Domain identifier
*
* @return \Illuminate\Http\JsonResponse
*/
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
@@ -452,9 +452,6 @@
return response
},
error => {
- let error_msg
- let status = error.response ? error.response.status : 200
-
// Do not display the error in a toast message, pass the error as-is
if (error.config.ignoreErrors) {
return Promise.reject(error)
@@ -464,15 +461,20 @@
error.config.onFinish()
}
- if (error.response && status == 422) {
- error_msg = "Form validation error"
+ let error_msg
+
+ const status = error.response ? error.response.status : 200
+ const data = error.response ? error.response.data : {}
+
+ if (status == 422 && data.errors) {
+ error_msg = app.$t('error.form')
const modal = $('div.modal.show')
$(modal.length ? modal : 'form').each((i, form) => {
form = $(form)
- $.each(error.response.data.errors || {}, (idx, msg) => {
+ $.each(data.errors, (idx, msg) => {
const input_name = (form.data('validation-prefix') || form.find('form').first().data('validation-prefix') || '') + idx
let input = form.find('#' + input_name)
@@ -526,8 +528,8 @@
form.find('.is-invalid:not(.listinput-widget)').first().focus()
})
}
- else if (error.response && error.response.data) {
- error_msg = error.response.data.message
+ else if (data.status == 'error') {
+ error_msg = data.message
}
else {
error_msg = error.request ? error.request.statusText : error.message
diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php
--- a/src/resources/lang/en/app.php
+++ b/src/resources/lang/en/app.php
@@ -51,6 +51,8 @@
'distlist-unsuspend-success' => 'Distribution list unsuspended successfully.',
'domain-create-success' => 'Domain created successfully.',
+ 'domain-delete-success' => 'Domain deleted successfully.',
+ 'domain-notempty-error' => 'Unable to delete a domain with assigned users or other objects.',
'domain-verify-success' => 'Domain verified successfully.',
'domain-verify-error' => 'Domain ownership verification failed.',
'domain-suspend-success' => 'Domain suspended successfully.',
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
@@ -61,6 +61,11 @@
],
'domain' => [
+ 'delete' => "Delete domain",
+ 'delete-domain' => "Delete {domain}",
+ 'delete-text' => "Do you really want to delete this domain permanently?"
+ . " This is only possible if there are no users, aliases or other objects in this domain."
+ . " Please note that this action cannot be undone.",
'dns-verify' => "Domain DNS verification sample:",
'dns-config' => "Domain DNS configuration sample:",
'namespace' => "Namespace",
@@ -92,6 +97,7 @@
'500' => "Internal server error",
'unknown' => "Unknown Error",
'server' => "Server Error",
+ 'form' => "Form validation error",
],
'form' => [
diff --git a/src/resources/vue/Domain/Info.vue b/src/resources/vue/Domain/Info.vue
--- a/src/resources/vue/Domain/Info.vue
+++ b/src/resources/vue/Domain/Info.vue
@@ -5,7 +5,14 @@
<div class="card">
<div class="card-body">
<div class="card-title" v-if="domain_id === 'new'">{{ $t('domain.new') }}</div>
- <div class="card-title" v-else>{{ $t('form.domain') }}</div>
+ <div class="card-title" v-else>{{ $t('form.domain') }}
+ <button
+ class="btn btn-outline-danger button-delete float-end"
+ @click="showDeleteConfirmation()" type="button"
+ >
+ <svg-icon icon="trash-alt"></svg-icon> {{ $t('domain.delete') }}
+ </button>
+ </div>
<div class="card-text">
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item">
@@ -93,10 +100,30 @@
</div>
</div>
</div>
+ <div id="delete-warning" class="modal" tabindex="-1" role="dialog">
+ <div class="modal-dialog" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title">{{ $t('domain.delete-domain', { domain: domain.namespace }) }}</h5>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
+ </div>
+ <div class="modal-body">
+ <p>{{ $t('domain.delete-text') }}</p>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
+ <button type="button" class="btn btn-danger modal-action" @click="deleteDomain()">
+ <svg-icon icon="trash-alt"></svg-icon> {{ $t('btn.delete') }}
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
</div>
</template>
<script>
+ import { Modal } from 'bootstrap'
import ListInput from '../Widgets/ListInput'
import PackageSelect from '../Widgets/PackageSelect'
import StatusComponent from '../Widgets/Status'
@@ -140,6 +167,9 @@
},
mounted() {
$('#namespace').focus()
+ $('#delete-warning')[0].addEventListener('shown.bs.modal', event => {
+ $(event.target).find('button.modal-cancel').focus()
+ })
},
methods: {
confirm() {
@@ -155,6 +185,20 @@
}
})
},
+ deleteDomain() {
+ // Delete the domain from the confirm dialog
+ axios.delete('/api/v4/domains/' + this.domain_id)
+ .then(response => {
+ if (response.data.status == 'success') {
+ this.$toast.success(response.data.message)
+ this.$router.push({ name: 'domains' })
+ }
+ })
+ },
+ showDeleteConfirmation() {
+ // Display the warning
+ new Modal('#delete-warning').show()
+ },
statusUpdate(domain) {
this.domain = Object.assign({}, this.domain, domain)
},
diff --git a/src/tests/Browser/DomainTest.php b/src/tests/Browser/DomainTest.php
--- a/src/tests/Browser/DomainTest.php
+++ b/src/tests/Browser/DomainTest.php
@@ -5,6 +5,7 @@
use App\Domain;
use App\User;
use Tests\Browser;
+use Tests\Browser\Components\Dialog;
use Tests\Browser\Components\ListInput;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
@@ -295,4 +296,58 @@
->assertSeeIn('@table tr:nth-child(2) a', 'testdomain.com');
});
}
+
+ /**
+ * Test domain deletion
+ */
+ public function testDomainDelete(): void
+ {
+ // Create the domain to delete
+ $john = $this->getTestUser('john@kolab.org');
+ $domain = $this->getTestDomain('testdomain.com', ['type' => Domain::TYPE_EXTERNAL]);
+ $packageDomain = \App\Package::withEnvTenantContext()->where('title', 'domain-hosting')->first();
+ $domain->assignPackage($packageDomain, $john);
+
+ $this->browse(function ($browser) {
+ $browser->visit('/login')
+ ->on(new Home())
+ ->submitLogon('john@kolab.org', 'simple123')
+ ->visit('/domains')
+ ->on(new DomainList())
+ ->assertElementsCount('@table tbody tr', 2)
+ ->assertSeeIn('@table tr:nth-child(2) a', 'testdomain.com')
+ ->click('@table tbody tr:nth-child(2) a')
+ ->on(new DomainInfo())
+ ->waitFor('button.button-delete')
+ ->assertSeeIn('button.button-delete', 'Delete domain')
+ ->click('button.button-delete')
+ ->with(new Dialog('#delete-warning'), function ($browser) {
+ $browser->assertSeeIn('@title', 'Delete testdomain.com')
+ ->assertFocused('@button-cancel')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Delete')
+ ->click('@button-cancel');
+ })
+ ->waitUntilMissing('#delete-warning')
+ ->click('button.button-delete')
+ ->with(new Dialog('#delete-warning'), function (Browser $browser) {
+ $browser->click('@button-action');
+ })
+ ->waitUntilMissing('#delete-warning')
+ ->assertToast(Toast::TYPE_SUCCESS, 'Domain deleted successfully.')
+ ->on(new DomainList())
+ ->assertElementsCount('@table tbody tr', 1);
+
+ // Test error handling on deleting a non-empty domain
+ $err = 'Unable to delete a domain with assigned users or other objects.';
+ $browser->click('@table tbody tr:nth-child(1) a')
+ ->on(new DomainInfo())
+ ->waitFor('button.button-delete')
+ ->click('button.button-delete')
+ ->with(new Dialog('#delete-warning'), function ($browser) {
+ $browser->click('@button-action');
+ })
+ ->assertToast(Toast::TYPE_ERROR, $err);
+ });
+ }
}
diff --git a/src/tests/Feature/Controller/Admin/DomainsTest.php b/src/tests/Feature/Controller/Admin/DomainsTest.php
--- a/src/tests/Feature/Controller/Admin/DomainsTest.php
+++ b/src/tests/Feature/Controller/Admin/DomainsTest.php
@@ -26,6 +26,7 @@
*/
public function tearDown(): void
{
+ $this->deleteTestUser('test1@domainscontroller.com');
$this->deleteTestDomain('domainscontroller.com');
parent::tearDown();
@@ -45,6 +46,19 @@
}
/**
+ * Test deleting a domain (DELETE /api/v4/domains/<id>)
+ */
+ public function testDestroy(): void
+ {
+ $admin = $this->getTestUser('jeroen@jeroen.jeroen');
+ $domain = $this->getTestDomain('kolab.org');
+
+ // This end-point does not exist for admins
+ $response = $this->actingAs($admin)->delete("api/v4/domains/{$domain->id}");
+ $response->assertStatus(404);
+ }
+
+ /**
* Test domains searching (/api/v4/domains)
*/
public function testIndex(): void
@@ -178,7 +192,7 @@
'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_EXTERNAL,
]);
- $user = $this->getTestUser('test@domainscontroller.com');
+ $user = $this->getTestUser('test1@domainscontroller.com');
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
// Test unauthorized access to admin API
@@ -211,7 +225,7 @@
'status' => Domain::STATUS_NEW | Domain::STATUS_SUSPENDED,
'type' => Domain::TYPE_EXTERNAL,
]);
- $user = $this->getTestUser('test@domainscontroller.com');
+ $user = $this->getTestUser('test1@domainscontroller.com');
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
// Test unauthorized access to admin API
diff --git a/src/tests/Feature/Controller/DomainsTest.php b/src/tests/Feature/Controller/DomainsTest.php
--- a/src/tests/Feature/Controller/DomainsTest.php
+++ b/src/tests/Feature/Controller/DomainsTest.php
@@ -20,12 +20,16 @@
{
parent::setUp();
+ $this->deleteTestUser('test1@' . \config('app.domain'));
+ $this->deleteTestUser('test2@' . \config('app.domain'));
$this->deleteTestUser('test1@domainscontroller.com');
$this->deleteTestDomain('domainscontroller.com');
}
public function tearDown(): void
{
+ $this->deleteTestUser('test1@' . \config('app.domain'));
+ $this->deleteTestUser('test2@' . \config('app.domain'));
$this->deleteTestUser('test1@domainscontroller.com');
$this->deleteTestDomain('domainscontroller.com');
@@ -40,6 +44,8 @@
*/
public function testConfirm(): void
{
+ Queue::fake();
+
$sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first();
$john = $this->getTestUser('john@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
@@ -88,6 +94,81 @@
}
/**
+ * Test domain delete request (DELETE /api/v4/domains/<id>)
+ */
+ public function testDestroy(): void
+ {
+ Queue::fake();
+
+ $sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first();
+ $john = $this->getTestUser('john@kolab.org');
+ $johns_domain = $this->getTestDomain('kolab.org');
+ $user1 = $this->getTestUser('test1@' . \config('app.domain'));
+ $user2 = $this->getTestUser('test2@' . \config('app.domain'));
+ $domain = $this->getTestDomain('domainscontroller.com', [
+ 'status' => Domain::STATUS_NEW,
+ 'type' => Domain::TYPE_EXTERNAL,
+ ]);
+
+ Entitlement::create([
+ 'wallet_id' => $user1->wallets()->first()->id,
+ 'sku_id' => $sku_domain->id,
+ 'entitleable_id' => $domain->id,
+ 'entitleable_type' => Domain::class
+ ]);
+
+ // Not authorized access
+ $response = $this->actingAs($john)->delete("api/v4/domains/{$domain->id}");
+ $response->assertStatus(403);
+
+ // Can't delete non-empty domain
+ $response = $this->actingAs($john)->delete("api/v4/domains/{$johns_domain->id}");
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertEquals('error', $json['status']);
+ $this->assertEquals('Unable to delete a domain with assigned users or other objects.', $json['message']);
+
+ // Successful deletion
+ $response = $this->actingAs($user1)->delete("api/v4/domains/{$domain->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertEquals('success', $json['status']);
+ $this->assertEquals('Domain deleted successfully.', $json['message']);
+ $this->assertTrue($domain->fresh()->trashed());
+
+ // Authorized access by additional account controller
+ $this->deleteTestDomain('domainscontroller.com');
+ $domain = $this->getTestDomain('domainscontroller.com', [
+ 'status' => Domain::STATUS_NEW,
+ 'type' => Domain::TYPE_EXTERNAL,
+ ]);
+
+ Entitlement::create([
+ 'wallet_id' => $user1->wallets()->first()->id,
+ 'sku_id' => $sku_domain->id,
+ 'entitleable_id' => $domain->id,
+ 'entitleable_type' => Domain::class
+ ]);
+
+ $user1->wallets()->first()->addController($user2);
+
+ $response = $this->actingAs($user2)->delete("api/v4/domains/{$domain->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+ $this->assertCount(2, $json);
+ $this->assertEquals('success', $json['status']);
+ $this->assertEquals('Domain deleted successfully.', $json['message']);
+ $this->assertTrue($domain->fresh()->trashed());
+ }
+
+ /**
* Test fetching domains list
*/
public function testIndex(): void
@@ -436,6 +517,37 @@
$wallet = $domain->wallet();
$this->assertSame($john->wallets->first()->id, $wallet->id);
+ // Test re-creating a domain
+ $domain->delete();
+
+ $response = $this->actingAs($john)->post("/api/v4/domains", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Domain created successfully.", $json['message']);
+ $this->assertCount(2, $json);
+
+ $domain = Domain::where('namespace', $post['namespace'])->first();
+ $this->assertInstanceOf(Domain::class, $domain);
+ $this->assertEntitlements($domain, ['domain-hosting']);
+ $wallet = $domain->wallet();
+ $this->assertSame($john->wallets->first()->id, $wallet->id);
+
+ // Test creating a domain that is soft-deleted and belongs to another user
+ $domain->delete();
+ $domain->entitlement()->withTrashed()->update(['wallet_id' => $jack->wallets->first()->id]);
+
+ $response = $this->actingAs($john)->post("/api/v4/domains", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(2, $json);
+ $this->assertSame('The specified domain is not available.', $json['errors']['namespace']);
+
// Test acting as account controller (not owner)
$this->markTestIncomplete();
diff --git a/src/tests/Feature/Controller/Reseller/DomainsTest.php b/src/tests/Feature/Controller/Reseller/DomainsTest.php
--- a/src/tests/Feature/Controller/Reseller/DomainsTest.php
+++ b/src/tests/Feature/Controller/Reseller/DomainsTest.php
@@ -27,6 +27,7 @@
*/
public function tearDown(): void
{
+ $this->deleteTestUser('test1@domainscontroller.com');
$this->deleteTestDomain('domainscontroller.com');
parent::tearDown();
@@ -49,6 +50,19 @@
}
/**
+ * Test deleting a domain (DELETE /api/v4/domains/<id>)
+ */
+ public function testDestroy(): void
+ {
+ $reseller1 = $this->getTestUser('reseller@' . \config('app.domain'));
+ $domain = $this->getTestDomain('kolab.org');
+
+ // This end-point does not exist for resellers
+ $response = $this->actingAs($reseller1)->delete("api/v4/domains/{$domain->id}");
+ $response->assertStatus(404);
+ }
+
+ /**
* Test domains searching (/api/v4/domains)
*/
public function testIndex(): void
@@ -213,7 +227,7 @@
'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_EXTERNAL,
]);
- $user = $this->getTestUser('test@domainscontroller.com');
+ $user = $this->getTestUser('test1@domainscontroller.com');
// Test unauthorized access to the reseller API (user)
$response = $this->actingAs($user)->post("/api/v4/domains/{$domain->id}/suspend", []);
@@ -263,7 +277,7 @@
'status' => Domain::STATUS_NEW | Domain::STATUS_SUSPENDED,
'type' => Domain::TYPE_EXTERNAL,
]);
- $user = $this->getTestUser('test@domainscontroller.com');
+ $user = $this->getTestUser('test1@domainscontroller.com');
// Test unauthorized access to reseller API (user)
$response = $this->actingAs($user)->post("/api/v4/domains/{$domain->id}/unsuspend", []);
diff --git a/src/tests/Feature/DomainTest.php b/src/tests/Feature/DomainTest.php
--- a/src/tests/Feature/DomainTest.php
+++ b/src/tests/Feature/DomainTest.php
@@ -230,6 +230,34 @@
}
/**
+ * Test isEmpty() method
+ */
+ public function testIsEmpty(): void
+ {
+ Queue::fake();
+
+ // Empty domain
+ $domain = $this->getTestDomain('gmail.com', [
+ 'status' => Domain::STATUS_NEW,
+ 'type' => Domain::TYPE_EXTERNAL,
+ ]);
+
+ $this->assertTrue($domain->isEmpty());
+
+ // TODO: Test with adding a group/alias/user, each separately
+
+ // Empty public domain
+ $domain = Domain::where('namespace', 'libertymail.net')->first();
+
+ $this->assertFalse($domain->isEmpty());
+
+ // Non-empty private domain
+ $domain = Domain::where('namespace', 'kolab.org')->first();
+
+ $this->assertFalse($domain->isEmpty());
+ }
+
+ /**
* Test domain restoring
*/
public function testRestore(): void

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 10:40 PM (53 m, 6 s ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18827122
Default Alt Text
D2906.1775256017.diff (25 KB)

Event Timeline