Test Failed
Push — hypernext ( 50ecff...6ccff6 )
by Nico
13:45
created

EntitySqlBuilder::canBaseTeam()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 5
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 7
ccs 6
cts 6
cp 1
crap 1
rs 10
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
    public function __construct(private AbstractEntity $entity)
30 177
    {
31
    }
32 177
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
    public function getReadSqlBeforeWhere(
41 175
        bool $getTags = true,
42
        bool $fullSelect = false,
43
        ?EntityType $relatedOrigin = null,
44
    ): string {
45
        $this->entity($fullSelect);
46 175
        $this->status();
47 175
        $this->category();
48 175
        $this->comments();
49 175
        if ($getTags) {
50 175
            $this->tags();
51 175
        }
52
        if ($fullSelect) {
53 175
            $this->teamEvents();
54 175
        }
55
        $this->steps();
56 175
        // The links tables are only joined if we want to show related entities
57
        if ($relatedOrigin !== null) {
58 175
            $this->links($relatedOrigin);
59
        }
60
        $this->usersTeams();
61 175
        $this->uploads();
62 175
63
        $sql = array(
64 175
            'SELECT DISTINCT',
65 175
            implode(', ', $this->selectSql),
66 175
            'FROM %1$s AS entity',
67 175
            implode(' ', $this->joinsSql),
68 175
        );
69 175
70
        // replace all %1$s by 'experiments' or 'items', there are many more than the one in FROM
71
        return sprintf(implode(' ', $sql), $this->entity->type);
72 175
    }
73
74
    public function getCanFilter(string $can): string
75 8
    {
76
        $sql = '';
77 8
        if ($this->entity->isAnon) {
78 8
            $sql .= ' AND ' . $this->canAnon($can);
79
        }
80
        $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 175
    private function entity(bool $fullSelect): void
98
    {
99 175
        if ($fullSelect) {
100
            // get all the columns of entity table
101 175
            $this->selectSql[] = 'entity.*';
102
            // add a literal string for the page that can be used by the mention tinymce plugin code
103 175
            $this->selectSql[] = sprintf(
104 175
                "'%s' AS page, '%s' AS type",
105 175
                $this->entity->page,
106 175
                $this->entity->type,
107 175
            );
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 175
132
    private function tags(): void
133 175
    {
134
        $this->selectSql[] = "GROUP_CONCAT(
135
                DISTINCT tags.tag
136
                ORDER BY tags.id SEPARATOR '|'
137 175
            ) as tags,
138 175
            GROUP_CONCAT(DISTINCT tags.id) as tags_id";
139
        $this->joinsSql[] = 'LEFT JOIN tags2entity
140
                ON (tags2entity.item_id = entity.id
141
                    AND tags2entity.item_type = \'%1$s\')
142 175
            LEFT JOIN tags
143
                ON (tags.id = tags2entity.tag_id)';
144
    }
145 175
146
    private function teamEvents(): void
147 175
    {
148
        $this->selectSql[] = "GROUP_CONCAT(
149
                DISTINCT team_events.start
150
                ORDER BY team_events.start
151 175
                SEPARATOR '|'
152
            ) AS events_start";
153 175
154 115
        if ($this->entity instanceof Experiments) {
155 72
            $eventsColumn = 'experiment';
156 72
        } elseif ($this->entity instanceof Items) {
157 72
            $this->selectSql[] = 'entity.is_bookable';
158
            $eventsColumn = 'item_link = entity.id OR team_events.item';
159
        } else {
160
            throw new IllegalActionException('Nope.');
161
        }
162
163 175
        // only select events from the future
164 175
        $this->joinsSql[] = "LEFT JOIN team_events
165 175
            ON ((team_events.$eventsColumn = entity.id)
166
                AND team_events.start > NOW())";
167
    }
168 175
169
    private function usersTeams(): void
170 175
    {
171
        $this->selectSql[] = "users.firstname,
172
            users.lastname,
173 175
            users.orcid,
174
            CONCAT(users.firstname, ' ', users.lastname) AS fullname,
175 175
            teams.name AS team_name";
176 175
177 175
        $this->joinsSql[] = 'LEFT JOIN users
178 175
            ON (users.userid = entity.userid)';
179
        $this->joinsSql[] = sprintf(
180 175
            'LEFT JOIN users2teams
181 175
                ON (users2teams.users_id = users.userid
182 175
                    AND users2teams.teams_id = %d)
183
            LEFT JOIN teams ON (entity.team = teams.id)',
184
            $this->entity->Users->userData['team'],
185 175
        );
186
    }
187 175
188 175
    private function category(): void
189
    {
190 175
        $this->selectSql[] = 'categoryt.title AS category_title,
191 175
            categoryt.color AS category_color';
192 175
193 175
        $this->joinsSql[] = sprintf(
194 115
            'LEFT JOIN %s AS categoryt
195 175
                ON (categoryt.id = entity.category)',
196 175
            $this->entity->type === 'experiments'
197
                ? 'experiments_categories'
198
                : 'items_types',
199 175
        );
200
    }
201 175
202 175
    private function status(): void
203 175
    {
204 175
        $this->selectSql[] = 'statust.title AS status_title,
205
            statust.color AS status_color';
206
        $this->joinsSql[] = 'LEFT JOIN %1$s_status AS statust
207 175
            ON (statust.id = entity.status)';
208
    }
209 175
210 175
    private function uploads(): void
211
    {
212
        $this->selectSql[] = 'uploads.up_item_id,
213 175
            uploads.has_attachment';
214 175
215
        // only include columns if actually searching for comments/filenames
216
        $searchAttachments = '';
217
        if (!empty(array_column($this->entity->extendedValues, 'searchAttachments'))) {
218
            $searchAttachments = ', GROUP_CONCAT(comment) AS comments
219 175
                , GROUP_CONCAT(real_name) AS real_names';
220
        }
221
222
        $this->joinsSql[] = 'LEFT JOIN (
223 175
                SELECT item_id AS up_item_id,
224
                    (item_id IS NOT NULL) AS has_attachment,
225
                    type
226
                    ' . $searchAttachments . '
227
                FROM uploads
228 175
                GROUP BY item_id, type
229
            ) AS uploads
230
                ON (uploads.up_item_id = entity.id
231
                    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 175
            ON (linkst.item_id = entity.id)";
248
    }
249 175
250
    private function steps(): void
251
    {
252
        $this->selectSql[] = "SUBSTRING_INDEX(GROUP_CONCAT(
253 175
                stepst.next_step
254 175
                ORDER BY steps_ordering, steps_id
255
                SEPARATOR '|'
256
            ), '|', 1) AS next_step";
257
        $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 175
                FROM %1$s_steps
264
                WHERE finished = 0
265
            ) AS stepst
266 175
                ON (stepst.steps_item_id = entity.id)';
267
    }
268 175
269 175
    private function comments(): void
270 175
    {
271
        $this->selectSql[] = 'commentst.recent_comment,
272
            (commentst.recent_comment IS NOT NULL) AS has_comment';
273
        $this->joinsSql[] = 'LEFT JOIN (
274
                SELECT MAX(created_at) AS recent_comment,
275
                    item_id
276 175
                FROM %1$s_comments
277
                GROUP BY item_id
278
            ) AS commentst
279
                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 8
     * base pub filter
296
     */
297 8
    private function canBasePub(string $can): string
298 8
    {
299 8
        return sprintf(
300 8
            "entity.%s->'$.base' = %d",
301 8
            $can,
302 8
            BasePermissions::Full->value,
303 8
        );
304 8
    }
305 8
306
    /**
307 8
     * base org filter
308
     */
309
    private function canBaseOrg(string $can): string
310
    {
311
        return sprintf(
312
            "entity.%s->'$.base' = %d",
313
            $can,
314
            BasePermissions::Organization->value,
315 8
        );
316
    }
317 8
318 8
    /**
319
     * base team filter
320 8
     */
321 8
    private function canBaseTeam(string $can): string
322 8
    {
323
        return sprintf(
324 8
            "(entity.%s->'$.base' = %d
325 4
                AND users2teams.teams_id = entity.team)",
326 8
            $can,
327 8
            BasePermissions::Team->value,
328
        );
329
    }
330
331
    /**
332
     * base user filter
333
     * entities are accessible for admins too
334 8
     */
335
    private function canBaseUser(string $can): string
336 8
    {
337 8
        return sprintf(
338 8
            "(entity.%s->'$.base' = %d
339 8
                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
                : ':userid',
346
        );
347
    }
348
349
    /**
350
     * base user only filter
351 8
     * entities are listed only if we own them
352
     */
353 8
    private function canBaseUserOnly(string $can): string
354 8
    {
355 8
        return sprintf(
356 8
            "(entity.%s->'$.base' = %d
357 8
                AND entity.userid = :userid
358 8
                AND users2teams.teams_id = entity.team)",
359
            $can,
360
            BasePermissions::UserOnly->value,
361
        );
362
    }
363
364 8
    /**
365
     * teams filter
366 8
     */
367 8
    private function canTeams(string $can): string
368 8
    {
369
        $UsersHelper = new UsersHelper((int) $this->entity->Users->userData['userid']);
370
        $teamsOfUser = $UsersHelper->getTeamsIdFromUserid();
371 8
        if (!empty($teamsOfUser)) {
372 8
            // JSON_OVERLAPS checks for the intersection of two arrays
373 8
            // 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
                $can,
377
                implode(', ', $teamsOfUser),
378
            );
379
        }
380
        return '0';
381
    }
382
383 8
    /**
384
     * teamgroups filter
385 8
     */
386 8
    private function canTeamGroups(string $can): string
387
    {
388
        $teamgroupsOfUser = array_column($this->entity->TeamGroups->readGroupsFromUser(), 'id');
389
        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 8
                implode(', ', $teamgroupsOfUser),
396
            );
397
        }
398
        return '0';
399
    }
400
401 8
    /**
402
     * users filter
403 8
     */
404
    private function canUsers(string $can): string
405
    {
406
        return ":userid MEMBER OF (entity.$can->>'$.users')";
407
    }
408
}
409