Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F16505121
D4710.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
12 KB
Referenced Files
None
Subscribers
None
D4710.diff
View Options
diff --git a/src/app/Http/Controllers/API/V4/SearchController.php b/src/app/Http/Controllers/API/V4/SearchController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/SearchController.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace App\Http\Controllers\API\V4;
+
+use App\Http\Controllers\Controller;
+use App\User;
+use App\UserSetting;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+
+/*
+ * Note: We use a separate controller for search, as this will
+ * be different that just a user search/listing functionality,
+ * it includes aliases (and contacts), how we do the search is different too.
+ */
+
+class SearchController extends Controller
+{
+ /**
+ * Search request for user's email addresses
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function searchSelf(Request $request)
+ {
+ $user = $this->guard()->user();
+ $search = trim(request()->input('search'));
+ $with_aliases = !empty(request()->input('alias'));
+ $limit = intval(request()->input('limit'));
+
+ if ($limit <= 0) {
+ $limit = 15;
+ } elseif ($limit > 100) {
+ $limit = 100;
+ }
+
+ // Prepare the query
+ $query = User::select('email', 'id')->where('id', $user->id);
+ $aliases = DB::table('user_aliases')->select(DB::raw('alias as email, user_id as id'))
+ ->where('user_id', $user->id);
+
+ if (strlen($search)) {
+ $aliases->whereLike('alias', $search);
+ $query->whereLike('email', $search);
+ }
+
+ if ($with_aliases) {
+ $query->union($aliases);
+ }
+
+ // Execute the query
+ $result = $query->orderBy('email')->limit($limit)->get();
+
+ $result = $this->resultFormat($result);
+
+ return response()->json([
+ 'list' => $result,
+ 'count' => count($result),
+ ]);
+ }
+
+ /**
+ * Search request for addresses of all users (in an account)
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function searchUser(Request $request)
+ {
+ $user = $this->guard()->user();
+ $search = trim(request()->input('search'));
+ $with_aliases = !empty(request()->input('alias'));
+ $limit = intval(request()->input('limit'));
+
+ if ($limit <= 0) {
+ $limit = 15;
+ } elseif ($limit > 100) {
+ $limit = 100;
+ }
+
+ $wallet = $user->wallet();
+
+ // Limit users to the user's account
+ $allUsers = $wallet->entitlements()->where('entitleable_type', User::class)->select('entitleable_id')->distinct();
+
+ // Sub-query for user IDs who's names match the search criteria
+ $foundUserIds = UserSetting::select('user_id')
+ ->whereIn('key', ['first_name', 'last_name'])
+ ->whereLike('value', $search)
+ ->whereIn('user_id', $allUsers);
+
+ // Prepare the query
+ $query = User::select('email', 'id')->whereIn('id', $allUsers);
+ $aliases = DB::table('user_aliases')->select(DB::raw('alias as email, user_id as id'))
+ ->whereIn('user_id', $allUsers);
+
+ if (strlen($search)) {
+ $query->where(function ($query) use ($foundUserIds, $search) {
+ $query->whereLike('email', $search)
+ ->orWhereIn('id', $foundUserIds);
+ });
+
+ $aliases->where(function ($query) use ($foundUserIds, $search) {
+ $query->whereLike('alias', $search)
+ ->orWhereIn('user_id', $foundUserIds);
+ });
+ }
+
+ if ($with_aliases) {
+ $query->union($aliases);
+ }
+
+ // Execute the query
+ $result = $query->orderBy('email')->limit($limit)->get();
+
+ $result = $this->resultFormat($result);
+
+ return response()->json([
+ 'list' => $result,
+ 'count' => count($result),
+ ]);
+ }
+
+ /**
+ * Format the search result, inject user names
+ */
+ protected function resultFormat($result)
+ {
+ if ($result->count()) {
+ // Get user names
+ $settings = UserSetting::whereIn('key', ['first_name', 'last_name'])
+ ->whereIn('user_id', $result->pluck('id'))
+ ->get()
+ ->mapWithKeys(function ($item) {
+ return [($item->user_id . ':' . $item->key) => $item->value];
+ })
+ ->all();
+
+ // "Format" the result, include user names
+ $result = $result->map(function ($record) use ($settings) {
+ return [
+ 'email' => $record->email,
+ 'name' => trim(
+ ($settings["{$record->id}:first_name"] ?? '')
+ . ' '
+ . ($settings["{$record->id}:last_name"] ?? '')
+ ),
+ ];
+ })
+ ->sortBy(['name', 'email'])
+ ->values();
+ }
+
+ return $result;
+ }
+}
diff --git a/src/config/app.php b/src/config/app.php
--- a/src/config/app.php
+++ b/src/config/app.php
@@ -257,6 +257,7 @@
'with_resources' => (bool) env('APP_WITH_RESOURCES', true),
'with_meet' => (bool) env('APP_WITH_MEET', true),
'with_companion_app' => (bool) env('APP_WITH_COMPANION_APP', true),
+ 'with_user_search' => (bool) env('APP_WITH_USER_SEARCH', false),
'signup' => [
'email_limit' => (int) env('SIGNUP_LIMIT_EMAIL', 0),
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -183,6 +183,11 @@
Route::get('payments/has-pending', [API\V4\PaymentsController::class, 'hasPayments']);
Route::get('payments/status', [API\V4\PaymentsController::class, 'paymentStatus']);
+ Route::get('search/self', [API\V4\SearchController::class, 'searchSelf']);
+ if (\config('app.with_user_search')) {
+ Route::get('search/user', [API\V4\SearchController::class, 'searchUser']);
+ }
+
Route::post('support/request', [API\V4\SupportController::class, 'request'])
->withoutMiddleware(['auth:api', 'scope:api'])
->middleware(['api']);
diff --git a/src/tests/Feature/Controller/SearchTest.php b/src/tests/Feature/Controller/SearchTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/SearchTest.php
@@ -0,0 +1,192 @@
+<?php
+
+namespace Tests\Feature\Controller;
+
+use Tests\TestCase;
+
+class SearchTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('jane@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('jane@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test searching
+ */
+ public function testSearchSelf(): void
+ {
+ // Unauth access not allowed
+ $response = $this->get("api/v4/search/self");
+ $response->assertStatus(401);
+
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+
+ // w/o aliases, w/o search
+ $response = $this->actingAs($john)->get("api/v4/search/self");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame(['email' => 'john@kolab.org', 'name' => 'John Doe'], $json['list'][0]);
+
+ // with aliases, w/o search
+ $response = $this->actingAs($john)->get("api/v4/search/self?alias=1");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(2, $json['count']);
+ $this->assertCount(2, $json['list']);
+ $this->assertSame(['email' => 'john.doe@kolab.org', 'name' => 'John Doe'], $json['list'][0]);
+ $this->assertSame(['email' => 'john@kolab.org', 'name' => 'John Doe'], $json['list'][1]);
+
+ // with aliases and search
+ $response = $this->actingAs($john)->get("api/v4/search/self?alias=1&search=doe@");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame(['email' => 'john.doe@kolab.org', 'name' => 'John Doe'], $json['list'][0]);
+
+ // User no account owner - with aliases, w/o search
+ $response = $this->actingAs($jack)->get("api/v4/search/self?alias=1");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(2, $json['count']);
+ $this->assertCount(2, $json['list']);
+ $this->assertSame(['email' => 'jack.daniels@kolab.org', 'name' => 'Jack Daniels'], $json['list'][0]);
+ $this->assertSame(['email' => 'jack@kolab.org', 'name' => 'Jack Daniels'], $json['list'][1]);
+ }
+
+ /**
+ * Test searching
+ */
+ public function testSearchUser(): void
+ {
+ \putenv('APP_WITH_USER_SEARCH=false'); // can't be done using \config()
+ $this->refreshApplication(); // reload routes
+
+ // User search route disabled
+ $response = $this->get("api/v4/search/user");
+ $response->assertStatus(404);
+
+ \putenv('APP_WITH_USER_SEARCH=true'); // can't be done using \config()
+ $this->refreshApplication(); // reload routes
+
+ // Unauth access not allowed
+ $response = $this->get("api/v4/search/user");
+ $response->assertStatus(401);
+
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $jane = $this->getTestUser('jane@kolabnow.com');
+
+ // Account owner - without aliases, w/o search
+ $response = $this->actingAs($john)->get("api/v4/search/user");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $users = [
+ [
+ 'email' => 'joe@kolab.org',
+ 'name' => '',
+ ],
+ [
+ 'email' => 'ned@kolab.org',
+ 'name' => 'Edward Flanders',
+ ],
+ [
+ 'email' => 'jack@kolab.org',
+ 'name' => 'Jack Daniels',
+ ],
+ [
+ 'email' => 'john@kolab.org',
+ 'name' => 'John Doe',
+ ],
+ ];
+
+
+ $this->assertSame(count($users), $json['count']);
+ $this->assertSame($users, $json['list']);
+
+ // User no account owner, without aliases w/o search
+ $response = $this->actingAs($jack)->get("api/v4/search/user");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(count($users), $json['count']);
+ $this->assertSame($users, $json['list']);
+
+ // with aliases, w/o search
+ $response = $this->actingAs($john)->get("api/v4/search/user?alias=1");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $expected = [
+ [
+ 'email' => 'joe.monster@kolab.org',
+ 'name' => '',
+ ],
+ $users[0],
+ $users[1],
+ [
+ 'email' => 'jack.daniels@kolab.org',
+ 'name' => 'Jack Daniels',
+ ],
+ $users[2],
+ [
+ 'email' => 'john.doe@kolab.org',
+ 'name' => 'John Doe',
+ ],
+ $users[3],
+ ];
+
+ $this->assertSame(count($expected), $json['count']);
+ $this->assertSame($expected, $json['list']);
+
+ // with aliases and search
+ $response = $this->actingAs($john)->get("api/v4/search/user?alias=1&search=john");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(2, $json['count']);
+ $this->assertCount(2, $json['list']);
+ $this->assertSame(['email' => 'john.doe@kolab.org', 'name' => 'John Doe'], $json['list'][0]);
+ $this->assertSame(['email' => 'john@kolab.org', 'name' => 'John Doe'], $json['list'][1]);
+
+ // Make sure we can't find users from outside of an account
+ $response = $this->actingAs($john)->get("api/v4/search/user?alias=1&search=jane");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(0, $json['count']);
+ }
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Mon, Oct 28, 2:23 PM (21 h, 53 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
10049268
Default Alt Text
D4710.diff (12 KB)
Attached To
Mode
D4710: User search API
Attached
Detach File
Event Timeline
Log In to Comment