EntitySqlBuilder   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 384
Duplicated Lines 0 %

Test Coverage

Coverage 83.96%

Importance

Changes 0
Metric Value
eloc 140
dl 0
loc 384
ccs 157
cts 187
cp 0.8396
rs 9.44
c 0
b 0
f 0
wmc 37

22 Methods

Rating   Name   Duplication   Size   Complexity  
A entity() 0 31 3
A canBaseUser() 0 11 2
A canBaseTeam() 0 7 1
A canBaseUserOnly() 0 8 1
A usersTeams() 0 16 1
A teamEvents() 0 20 3
A getCanFilter() 0 21 2
A canBaseOrg() 0 6 1
A status() 0 5 1
A steps() 0 8 1
A links() 0 13 3
A canAnon() 0 6 1
A canTeamGroups() 0 13 2
A canTeams() 0 14 2
A canBasePub() 0 6 1
A tags() 0 8 1
A category() 0 11 2
A canUsers() 0 3 1
A uploads() 0 17 2
A comments() 0 5 1
A getReadSqlBeforeWhere() 0 32 4
A __construct() 0 2 1
1
<?php declare(strict_types=1);
2
/**
3
 * @author Nicolas CARPi <[email protected]>
4
 * @copyright 2022 Nicolas CARPi
5
 * @see https://www.elabftw.net Official website
6
 * @license AGPL-3.0
7
 * @package elabftw
8
 */
9
10
namespace Elabftw\Elabftw;
11
12
use function array_column;
13
use function array_unique;
14
use Elabftw\Enums\BasePermissions;
15
use Elabftw\Enums\EntityType;
16
use Elabftw\Exceptions\IllegalActionException;
17
use Elabftw\Models\AbstractEntity;
18
use Elabftw\Models\Experiments;
19
use Elabftw\Models\Items;
20
use Elabftw\Services\UsersHelper;
21
use function implode;
22
23
class EntitySqlBuilder
24
{
25
    private array $selectSql = array();
26
27
    private array $joinsSql = array();
28
29 179
    public function __construct(private AbstractEntity $entity)
30
    {
31 179
    }
32
33
    /**
34
     * Get the SQL string for read before the WHERE
35
     *
36
     * @param bool $getTags do we get the tags too?
37
     * @param bool $fullSelect select all the columns of entity
38
     * @param null|EntityType $relatedOrigin Are we looking for related entries, what is the origin, experiments or items?
39
     */
40 177
    public function getReadSqlBeforeWhere(
41
        bool $getTags = true,
42
        bool $fullSelect = false,
43
        ?EntityType $relatedOrigin = null,
44
    ): string {
45 177
        $this->entity($fullSelect);
46 177
        $this->status();
47 177
        $this->category();
48 177
        $this->comments();
49 177
        if ($getTags) {
50 177
            $this->tags();
51
        }
52 177
        if ($fullSelect) {
53 177
            $this->teamEvents();
54
        }
55 177
        $this->steps();
56
        // The links tables are only joined if we want to show related entities
57 177
        if ($relatedOrigin !== null) {
58
            $this->links($relatedOrigin);
59
        }
60 177
        $this->usersTeams();
61 177
        $this->uploads();
62
63 177
        $sql = array(
64 177
            'SELECT DISTINCT',
65 177
            implode(', ', $this->selectSql),
66 177
            'FROM %1$s AS entity',
67 177
            implode(' ', $this->joinsSql),
68 177
        );
69
70
        // replace all %1$s by 'experiments' or 'items', there are many more than the one in FROM
71 177
        return sprintf(implode(' ', $sql), $this->entity->type);
72
    }
73
74 8
    public function getCanFilter(string $can): string
75
    {
76 8
        $sql = '';
77 8
        if ($this->entity->isAnon) {
78
            $sql .= ' AND ' . $this->canAnon($can);
79
        }
80 8
        $sql .= sprintf(
81 8
            ' AND (%s)',
82 8
            implode(' OR ', array(
83 8
                $this->canBasePub($can),
84 8
                $this->canBaseOrg($can),
85 8
                $this->canBaseTeam($can),
86 8
                $this->canBaseUser($can),
87 8
                $this->canBaseUserOnly($can),
88 8
                $this->canTeams($can),
89 8
                $this->canTeamGroups($can),
90 8
                $this->canUsers($can),
91 8
            )),
92 8
        );
93
94 8
        return $sql;
95
    }
96
97 177
    private function entity(bool $fullSelect): void
98
    {
99 177
        if ($fullSelect) {
100
            // get all the columns of entity table
101 177
            $this->selectSql[] = 'entity.*';
102
            // add a literal string for the page that can be used by the mention tinymce plugin code
103 177
            $this->selectSql[] = sprintf(
104 177
                "'%s' AS page, '%s' AS type",
105 177
                $this->entity->page,
106 177
                $this->entity->type,
107 177
            );
108
        } else {
109
            // only get the columns interesting for show mode
110
            $this->selectSql[] = 'entity.id,
111
                entity.title,
112
                entity.custom_id,
113
                entity.date,
114
                entity.category,
115
                entity.team,
116
                entity.status,
117
                entity.rating,
118
                entity.userid,
119
                entity.locked,
120
                entity.state,
121
                entity.canread,
122
                entity.canwrite,
123
                entity.modified_at,
124
                entity.timestamped';
125
            // only include columns (created_at, locked_at, timestamped_at, entity.metadata) if actually searching for it
126
            if (!empty(array_column($this->entity->extendedValues, 'additional_columns'))) {
127
                $this->selectSql[] = implode(', ', array_unique(array_column($this->entity->extendedValues, 'additional_columns')));
128
            }
129
        }
130
    }
131
132 177
    private function tags(): void
133
    {
134 177
        $this->selectSql[] = "GROUP_CONCAT(
135
                DISTINCT tags.tag
136
                ORDER BY tags.id SEPARATOR '|'
137
            ) as tags,
138 177
            GROUP_CONCAT(DISTINCT tags.id) as tags_id";
139 177
        $this->joinsSql[] = 'LEFT JOIN tags2entity
140
                ON (tags2entity.item_id = entity.id
141
                    AND tags2entity.item_type = \'%1$s\')
142
            LEFT JOIN tags
143 177
                ON (tags.id = tags2entity.tag_id)';
144
    }
145
146 177
    private function teamEvents(): void
147
    {
148 177
        $this->selectSql[] = "GROUP_CONCAT(
149
                DISTINCT team_events.start
150
                ORDER BY team_events.start
151
                SEPARATOR '|'
152 177
            ) AS events_start";
153
154 177
        if ($this->entity instanceof Experiments) {
155 117
            $eventsColumn = 'experiment';
156 72
        } elseif ($this->entity instanceof Items) {
157 72
            $this->selectSql[] = 'entity.is_bookable';
158 72
            $eventsColumn = 'item_link = entity.id OR team_events.item';
159
        } else {
160
            throw new IllegalActionException('Nope.');
161
        }
162
163
        // only select events from the future
164 177
        $this->joinsSql[] = "LEFT JOIN team_events
165 177
            ON ((team_events.$eventsColumn = entity.id)
166 177
                AND team_events.start > NOW())";
167
    }
168
169 177
    private function usersTeams(): void
170
    {
171 177
        $this->selectSql[] = "users.firstname,
172
            users.lastname,
173
            users.orcid,
174
            CONCAT(users.firstname, ' ', users.lastname) AS fullname,
175 177
            teams.name AS team_name";
176
177 177
        $this->joinsSql[] = 'LEFT JOIN users
178 177
            ON (users.userid = entity.userid)';
179 177
        $this->joinsSql[] = sprintf(
180 177
            'LEFT JOIN users2teams
181
                ON (users2teams.users_id = users.userid
182
                    AND users2teams.teams_id = %d)
183 177
            LEFT JOIN teams ON (entity.team = teams.id)',
184 177
            $this->entity->Users->userData['team'],
185 177
        );
186
    }
187
188 177
    private function category(): void
189
    {
190 177
        $this->selectSql[] = 'categoryt.title AS category_title,
191 177
            categoryt.color AS category_color';
192
193 177
        $this->joinsSql[] = sprintf(
194 177
            'LEFT JOIN %s AS categoryt
195 177
                ON (categoryt.id = entity.category)',
196 177
            $this->entity->type === 'experiments'
197 117
                ? 'experiments_categories'
198 177
                : 'items_types',
199 177
        );
200
    }
201
202 177
    private function status(): void
203
    {
204 177
        $this->selectSql[] = 'statust.title AS status_title,
205 177
            statust.color AS status_color';
206 177
        $this->joinsSql[] = 'LEFT JOIN %1$s_status AS statust
207 177
            ON (statust.id = entity.status)';
208
    }
209
210 177
    private function uploads(): void
211
    {
212 177
        $this->selectSql[] = 'uploads.up_item_id,
213 177
            uploads.has_attachment';
214
215
        // only include columns if actually searching for comments/filenames
216 177
        $searchAttachments = '';
217 177
        if (!empty(array_column($this->entity->extendedValues, 'searchAttachments'))) {
218
            $searchAttachments = ', GROUP_CONCAT(comment) AS comments
219
                , GROUP_CONCAT(real_name) AS real_names';
220
        }
221
222 177
        $this->joinsSql[] = 'LEFT JOIN (
223
                SELECT item_id AS up_item_id,
224
                    (item_id IS NOT NULL) AS has_attachment,
225
                    type
226 177
                    ' . $searchAttachments . '
227
                FROM uploads
228
                GROUP BY item_id, type
229
            ) AS uploads
230
                ON (uploads.up_item_id = entity.id
231 177
                    AND uploads.type = \'%1$s\')';
232
    }
233
234
    private function links(EntityType $relatedOrigin): void
235
    {
236
        $table = 'items';
237
        if ($this->entity->entityType === EntityType::Experiments) {
238
            $table = 'experiments';
239
        }
240
241
        $related = '_links';
242
        if ($relatedOrigin === EntityType::Experiments) {
243
            $related = '2experiments';
244
        }
245
246
        $this->joinsSql[] = "LEFT JOIN $table$related AS linkst
247
            ON (linkst.item_id = entity.id)";
248
    }
249
250 177
    private function steps(): void
251
    {
252 177
        $this->selectSql[] = "SUBSTRING_INDEX(GROUP_CONCAT(
253
                stepst.next_step
254
                ORDER BY steps_ordering, steps_id
255
                SEPARATOR '|'
256 177
            ), '|', 1) AS next_step";
257 177
        $this->joinsSql[] = 'LEFT JOIN (
258
                SELECT item_id AS steps_item_id,
259
                    body AS next_step,
260
                    ordering AS steps_ordering,
261
                    id AS steps_id,
262
                    finished AS finished
263
                FROM %1$s_steps
264
                WHERE finished = 0
265
            ) AS stepst
266 177
                ON (stepst.steps_item_id = entity.id)';
267
    }
268
269 177
    private function comments(): void
270
    {
271 177
        $this->selectSql[] = 'commentst.recent_comment,
272 177
            (commentst.recent_comment IS NOT NULL) AS has_comment';
273 177
        $this->joinsSql[] = 'LEFT JOIN (
274
                SELECT MAX(created_at) AS recent_comment,
275
                    item_id
276
                FROM %1$s_comments
277
                GROUP BY item_id
278
            ) AS commentst
279 177
                ON (commentst.item_id = entity.id)';
280
    }
281
282
    /**
283
     * anon filter
284
     */
285
    private function canAnon(string $can): string
286
    {
287
        return sprintf(
288
            "entity.%s->'$.base' = %s",
289
            $can,
290
            BasePermissions::Full->value,
291
        );
292
    }
293
294
    /**
295
     * base pub filter
296
     */
297 8
    private function canBasePub(string $can): string
298
    {
299 8
        return sprintf(
300 8
            "entity.%s->'$.base' = %d",
301 8
            $can,
302 8
            BasePermissions::Full->value,
303 8
        );
304
    }
305
306
    /**
307
     * base org filter
308
     */
309 8
    private function canBaseOrg(string $can): string
310
    {
311 8
        return sprintf(
312 8
            "entity.%s->'$.base' = %d",
313 8
            $can,
314 8
            BasePermissions::Organization->value,
315 8
        );
316
    }
317
318
    /**
319
     * base team filter
320
     */
321 8
    private function canBaseTeam(string $can): string
322
    {
323 8
        return sprintf(
324 8
            "(entity.%s->'$.base' = %d
325 8
                AND users2teams.teams_id = entity.team)",
326 8
            $can,
327 8
            BasePermissions::Team->value,
328 8
        );
329
    }
330
331
    /**
332
     * base user filter
333
     * entities are accessible for admins too
334
     */
335 8
    private function canBaseUser(string $can): string
336
    {
337 8
        return sprintf(
338 8
            "(entity.%s->'$.base' = %d
339
                AND entity.userid = %s
340 8
                AND users2teams.teams_id = entity.team)",
341 8
            $can,
342 8
            BasePermissions::User->value,
343 8
            $this->entity->Users->isAdmin
344 8
                ? 'users2teams.users_id'
345 8
                : ':userid',
346 8
        );
347
    }
348
349
    /**
350
     * base user only filter
351
     * entities are listed only if we own them
352
     */
353 8
    private function canBaseUserOnly(string $can): string
354
    {
355 8
        return sprintf(
356 8
            "(entity.%s->'$.base' = %d
357
                AND entity.userid = :userid
358 8
                AND users2teams.teams_id = entity.team)",
359 8
            $can,
360 8
            BasePermissions::UserOnly->value,
361 8
        );
362
    }
363
364
    /**
365
     * teams filter
366
     */
367 8
    private function canTeams(string $can): string
368
    {
369 8
        $UsersHelper = new UsersHelper((int) $this->entity->Users->userData['userid']);
370 8
        $teamsOfUser = $UsersHelper->getTeamsIdFromUserid();
371 8
        if (!empty($teamsOfUser)) {
372
            // JSON_OVERLAPS checks for the intersection of two arrays
373
            // for instance [4,5,6] vs [2,6] has 6 in common -> 1 (true)
374 8
            return sprintf(
375 8
                "JSON_OVERLAPS(entity.%s->'$.teams', CAST('[%s]' AS JSON))",
376 8
                $can,
377 8
                implode(', ', $teamsOfUser),
378 8
            );
379
        }
380
        return '0';
381
    }
382
383
    /**
384
     * teamgroups filter
385
     */
386 8
    private function canTeamGroups(string $can): string
387
    {
388 8
        $teamgroupsOfUser = array_column($this->entity->TeamGroups->readGroupsFromUser(), 'id');
389 8
        if (!empty($teamgroupsOfUser)) {
390
            // JSON_OVERLAPS checks for the intersection of two arrays
391
            // for instance [4,5,6] vs [2,6] has 6 in common -> 1 (true)
392
            return sprintf(
393
                "JSON_OVERLAPS(entity.%s->'$.teamgroups', CAST('[%s]' AS JSON))",
394
                $can,
395
                implode(', ', $teamgroupsOfUser),
396
            );
397
        }
398 8
        return '0';
399
    }
400
401
    /**
402
     * users filter
403
     */
404 8
    private function canUsers(string $can): string
405
    {
406 8
        return ":userid MEMBER OF (entity.$can->>'$.users')";
407
    }
408
}
409