Passed
Push — master ( f88eb1...1417f0 )
by Angel Fernando Quiroz
22:09 queued 14:11
created

ExternalTool::setJwksUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 5
rs 10
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
declare(strict_types=1);
6
7
namespace Chamilo\LtiBundle\Entity;
8
9
use Chamilo\CoreBundle\Entity\AbstractResource;
10
use Chamilo\CoreBundle\Entity\GradebookEvaluation;
11
use Chamilo\CoreBundle\Entity\ResourceInterface;
12
use Chamilo\CoreBundle\Entity\ResourceLink;
13
use Chamilo\CoreBundle\Entity\ResourceNode;
14
use Chamilo\CoreBundle\Entity\ResourceToRootInterface;
15
use Doctrine\Common\Collections\ArrayCollection;
16
use Doctrine\Common\Collections\Collection;
17
use Doctrine\Common\Collections\Criteria;
18
use Doctrine\ORM\Mapping as ORM;
19
use LtiAssignmentGradesService;
20
use LtiNamesRoleProvisioningService;
21
use Stringable;
22
use UnserializeApi;
23
24
#[ORM\Table(name: 'lti_external_tool')]
25
#[ORM\Entity]
26
class ExternalTool extends AbstractResource implements ResourceInterface, ResourceToRootInterface, Stringable
27
{
28
    public const V_1P1 = 'lti1p1';
29
    public const V_1P3 = 'lti1p3';
30
31
    #[ORM\Id]
32
    #[ORM\GeneratedValue]
33
    #[ORM\Column(name: 'id', type: 'integer')]
34
    protected ?int $id = null;
35
36
    #[ORM\Column(name: 'title', type: 'string')]
37
    protected string $title;
38
39
    #[ORM\Column(name: 'description', type: 'text', nullable: true)]
40
    protected ?string $description;
41
42
    #[ORM\Column(name: 'public_key', type: 'text', nullable: true)]
43
    public ?string $publicKey;
44
45
    #[ORM\Column(name: 'launch_url', type: 'string')]
46
    protected string $launchUrl;
47
48
    #[ORM\Column(name: 'consumer_key', type: 'string', nullable: true)]
49
    protected ?string $consumerKey;
50
51
    #[ORM\Column(name: 'shared_secret', type: 'string', nullable: true)]
52
    protected ?string $sharedSecret;
53
54
    #[ORM\Column(name: 'custom_params', type: 'text', nullable: true)]
55
    protected ?string $customParams;
56
57
    #[ORM\Column(name: 'active_deep_linking', type: 'boolean', nullable: false, options: ['default' => false])]
58
    protected bool $activeDeepLinking;
59
60
    #[ORM\Column(name: 'privacy', type: 'text', nullable: true, options: ['default' => null])]
61
    protected ?string $privacy;
62
63
    #[ORM\ManyToOne(targetEntity: GradebookEvaluation::class)]
64
    #[ORM\JoinColumn(name: 'gradebook_eval_id', referencedColumnName: 'id', onDelete: 'SET NULL')]
65
    private ?GradebookEvaluation $gradebookEval;
66
67
    #[ORM\Column(name: 'client_id', type: 'string', nullable: true)]
68
    private ?string $clientId;
69
70
    #[ORM\Column(name: 'login_url', type: 'string', nullable: true)]
71
    private ?string $loginUrl;
72
73
    #[ORM\Column(name: 'redirect_url', type: 'string', nullable: true)]
74
    private ?string $redirectUrl;
75
76
    #[ORM\Column(name: 'jwks_url', type: 'string', nullable: true)]
77
    private ?string $jwksUrl;
78
79
    #[ORM\Column(name: 'advantage_services', type: 'json', nullable: true)]
80
    private ?array $advantageServices;
81
82
    #[ORM\OneToMany(mappedBy: 'tool', targetEntity: LineItem::class)]
83
    private Collection $lineItems;
84
85
    #[ORM\Column(name: 'version', type: 'string', options: ['default' => self::V_1P3])]
86
    private string $version;
87
88
    #[ORM\Column(name: 'launch_presentation', type: 'json')]
89
    private array $launchPresentation;
90
91
    #[ORM\Column(name: 'replacement_params', type: 'json')]
92
    private array $replacementParams;
93
94
    public function __construct()
95
    {
96
        $this->description = null;
97
        $this->customParams = null;
98
        $this->activeDeepLinking = false;
99
        $this->gradebookEval = null;
100
        $this->privacy = null;
101
        $this->consumerKey = null;
102
        $this->sharedSecret = null;
103
        $this->lineItems = new ArrayCollection();
104
        $this->version = self::V_1P3;
105
        $this->launchPresentation = [
106
            'document_target' => 'iframe',
107
        ];
108
        $this->replacementParams = [];
109
    }
110
111
    public function __toString(): string
112
    {
113
        return $this->getTitle();
114
    }
115
116
    public function getId(): ?int
117
    {
118
        return $this->id;
119
    }
120
121
    public function getTitle(): string
122
    {
123
        return $this->title;
124
    }
125
126
    public function setTitle(string $title): static
127
    {
128
        $this->title = $title;
129
130
        return $this;
131
    }
132
133
    public function getDescription(): ?string
134
    {
135
        return $this->description;
136
    }
137
138
    public function setDescription(?string $description): static
139
    {
140
        $this->description = $description;
141
142
        return $this;
143
    }
144
145
    public function getLaunchUrl(): string
146
    {
147
        return $this->launchUrl;
148
    }
149
150
    public function setLaunchUrl(string $launchUrl): static
151
    {
152
        $this->launchUrl = $launchUrl;
153
154
        return $this;
155
    }
156
157
    public function getCustomParams(): ?string
158
    {
159
        return $this->customParams;
160
    }
161
162
    public function setCustomParams(?string $customParams): static
163
    {
164
        $this->customParams = $customParams;
165
166
        return $this;
167
    }
168
169
    public function isGlobal(): bool
170
    {
171
        $resourceNode = $this->resourceNode->getResourceLinks()->first();
0 ignored issues
show
Bug introduced by
The method getResourceLinks() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

171
        $resourceNode = $this->resourceNode->/** @scrutinizer ignore-call */ getResourceLinks()->first();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
172
173
        return !$resourceNode;
174
    }
175
176
    public function encodeCustomParams(array $params): ?string
177
    {
178
        if (empty($params)) {
179
            return null;
180
        }
181
182
        $pairs = [];
183
184
        foreach ($params as $key => $value) {
185
            $pairs[] = "$key=$value";
186
        }
187
188
        return implode("\n", $pairs);
189
    }
190
191
    public function getCustomParamsAsArray(): array
192
    {
193
        $params = [];
194
        $lines = explode("\n", $this->customParams);
195
        $lines = array_filter($lines);
196
197
        foreach ($lines as $line) {
198
            [$key, $value] = explode('=', $line, 2);
199
200
            $key = self::filterSpecialChars($key);
201
            $value = self::filterSpaces($value);
202
203
            $params[$key] = $value;
204
        }
205
206
        return $params;
207
    }
208
209
    public function parseCustomParams(): array
210
    {
211
        if (empty($this->customParams)) {
212
            return [];
213
        }
214
215
        $params = [];
216
        $strings = explode("\n", $this->customParams);
217
218
        foreach ($strings as $string) {
219
            if (empty($string)) {
220
                continue;
221
            }
222
223
            $pairs = explode('=', $string, 2);
224
            $key = self::filterSpecialChars($pairs[0]);
225
            $value = $pairs[1];
226
227
            $params['custom_'.$key] = $value;
228
        }
229
230
        return $params;
231
    }
232
233
    public function isActiveDeepLinking(): bool
234
    {
235
        return $this->activeDeepLinking;
236
    }
237
238
    public function setActiveDeepLinking(bool $activeDeepLinking): static
239
    {
240
        $this->activeDeepLinking = $activeDeepLinking;
241
242
        return $this;
243
    }
244
245
    public function getGradebookEval(): ?GradebookEvaluation
246
    {
247
        return $this->gradebookEval;
248
    }
249
250
    public function setGradebookEval(?GradebookEvaluation $gradebookEval): static
251
    {
252
        $this->gradebookEval = $gradebookEval;
253
254
        return $this;
255
    }
256
257
    public function getPrivacy(): ?string
258
    {
259
        return $this->privacy;
260
    }
261
262
    public function setPrivacy(bool $shareName = false, bool $shareEmail = false, bool $sharePicture = false): static
263
    {
264
        $this->privacy = serialize(
265
            [
266
                'share_name' => $shareName,
267
                'share_email' => $shareEmail,
268
                'share_picture' => $sharePicture,
269
            ]
270
        );
271
272
        return $this;
273
    }
274
275
    public function isSharingName(): bool
276
    {
277
        $unserialize = $this->unserializePrivacy();
278
279
        return (bool) $unserialize['share_name'];
280
    }
281
282
    public function unserializePrivacy(): array
283
    {
284
        return UnserializeApi::unserialize('not_allowed_classes', $this->privacy);
285
    }
286
287
    public function isSharingEmail(): bool
288
    {
289
        $unserialize = $this->unserializePrivacy();
290
291
        if (!$unserialize) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $unserialize of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
292
            return false;
293
        }
294
295
        return (bool) $unserialize['share_email'];
296
    }
297
298
    public function isSharingPicture(): bool
299
    {
300
        $unserialize = $this->unserializePrivacy();
301
302
        if (!$unserialize) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $unserialize of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
303
            return false;
304
        }
305
306
        return (bool) $unserialize['share_picture'];
307
    }
308
309
    public function getToolParent(): ?self
310
    {
311
        return $this->parent;
0 ignored issues
show
Bug introduced by
The property parent does not exist on Chamilo\LtiBundle\Entity\ExternalTool. Did you mean parentResource?
Loading history...
312
    }
313
314
    public function setToolParent(self $parent): static
315
    {
316
        $this->resourceNode->setParent($parent->getResourceNode());
317
        $this->sharedSecret = $parent->getSharedSecret();
318
        $this->consumerKey = $parent->getConsumerKey();
319
        $this->privacy = $parent->getPrivacy();
320
321
        return $this;
322
    }
323
324
    public function getSharedSecret(): ?string
325
    {
326
        return $this->sharedSecret;
327
    }
328
329
    public function setSharedSecret(?string $sharedSecret): static
330
    {
331
        $this->sharedSecret = $sharedSecret;
332
333
        return $this;
334
    }
335
336
    public function getConsumerKey(): ?string
337
    {
338
        return $this->consumerKey;
339
    }
340
341
    public function setConsumerKey(?string $consumerKey): static
342
    {
343
        $this->consumerKey = $consumerKey;
344
345
        return $this;
346
    }
347
348
    public function getLoginUrl(): ?string
349
    {
350
        return $this->loginUrl;
351
    }
352
353
    public function setLoginUrl(?string $loginUrl): static
354
    {
355
        $this->loginUrl = $loginUrl;
356
357
        return $this;
358
    }
359
360
    public function getRedirectUrl(): ?string
361
    {
362
        return $this->redirectUrl;
363
    }
364
365
    public function getJwksUrl(): ?string
366
    {
367
        return $this->jwksUrl;
368
    }
369
370
    public function setJwksUrl(?string $jwksUrl): static
371
    {
372
        $this->jwksUrl = $jwksUrl;
373
374
        return $this;
375
    }
376
377
    public function setRedirectUrl(?string $redirectUrl): static
378
    {
379
        $this->redirectUrl = $redirectUrl;
380
381
        return $this;
382
    }
383
384
    public function getClientId(): ?string
385
    {
386
        return $this->clientId;
387
    }
388
389
    public function setClientId(?string $clientId): static
390
    {
391
        $this->clientId = $clientId;
392
393
        return $this;
394
    }
395
396
    public function getAdvantageServices(): array
397
    {
398
        if (empty($this->advantageServices)) {
399
            $this->advantageServices = [];
400
        }
401
402
        return array_merge(
403
            [
404
                'ags' => LtiAssignmentGradesService::AGS_NONE,
405
                'nrps' => LtiNamesRoleProvisioningService::NRPS_NONE,
406
            ],
407
            $this->advantageServices
408
        );
409
    }
410
411
    public function setAdvantageServices(array $advantageServices): static
412
    {
413
        $this->advantageServices = $advantageServices;
414
415
        return $this;
416
    }
417
418
    public function addLineItem(LineItem $lineItem): static
419
    {
420
        $lineItem->setTool($this);
421
422
        $this->lineItems[] = $lineItem;
423
424
        return $this;
425
    }
426
427
    public function getLineItems(
428
        int $resourceLinkId = 0,
429
        int $resourceId = 0,
430
        string $tag = '',
431
        int $limit = 0,
432
        int $page = 1
433
    ): Collection {
434
        $criteria = Criteria::create();
435
436
        if ($resourceLinkId) {
437
            $criteria->andWhere(Criteria::expr()->eq('tool', $resourceId));
438
        }
439
440
        if ($resourceId) {
441
            $criteria->andWhere(Criteria::expr()->eq('tool', $resourceId));
442
        }
443
444
        if (!empty($tag)) {
445
            $criteria->andWhere(Criteria::expr()->eq('tag', $tag));
446
        }
447
448
        if ($limit > 0) {
449
            $criteria->setMaxResults($limit);
450
451
            if ($page > 0) {
452
                $criteria->setFirstResult($page * $limit);
453
            }
454
        }
455
456
        return $this->lineItems->matching($criteria);
457
    }
458
459
    public function setLineItems(Collection $lineItems): static
460
    {
461
        $this->lineItems = $lineItems;
462
463
        return $this;
464
    }
465
466
    public function getVersion(): string
467
    {
468
        return $this->version;
469
    }
470
471
    public function setVersion(string $version): static
472
    {
473
        $this->version = $version;
474
475
        return $this;
476
    }
477
478
    public function getVersionName(): string
479
    {
480
        if (self::V_1P1 === $this->version) {
481
            return 'LTI 1.0 / 1.1';
482
        }
483
484
        return 'LTI 1.3';
485
    }
486
487
    public function setDocumenTarget(string $target): static
488
    {
489
        $this->launchPresentation['document_target'] = \in_array($target, ['iframe', 'window'], true) ? $target : 'iframe';
490
491
        return $this;
492
    }
493
494
    public function getDocumentTarget(): string
495
    {
496
        return $this->launchPresentation['document_target'] ?: 'iframe';
497
    }
498
499
    public function getLaunchPresentation(): array
500
    {
501
        return $this->launchPresentation;
502
    }
503
504
    public function setReplacementForUserId(string $replacement): static
505
    {
506
        $this->replacementParams['user_id'] = $replacement;
507
508
        return $this;
509
    }
510
511
    public function getReplacementForUserId(): ?string
512
    {
513
        if (!empty($this->replacementParams['user_id'])) {
514
            return $this->replacementParams['user_id'];
515
        }
516
517
        return null;
518
    }
519
520
    // @TODO: move it to repository
521
    public function getChildrenInCourses(array $coursesId): Collection
522
    {
523
        return $this->resourceNode->getChildren()->filter(
524
            fn(ResourceNode $child) => $child->getResourceLinks()->exists(
525
                fn(ResourceLink $link) => ($course = $link->getCourse()) && in_array($course->getId(), $coursesId)
526
            )
527
        );
528
    }
529
530
    public function getResourceName(): string
531
    {
532
        return $this->getTitle();
533
    }
534
535
    public function setResourceName(string $name): static
536
    {
537
        return $this->setTitle($name);
538
    }
539
540
    public function getResourceIdentifier(): int
541
    {
542
        return $this->getId();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getId() could return the type null which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
543
    }
544
545
    private static function filterSpaces(string $value): string
546
    {
547
        $newValue = preg_replace('/\s+/', ' ', $value);
548
549
        return trim($newValue);
550
    }
551
552
    private static function filterSpecialChars(string $key): string
553
    {
554
        $newKey = '';
555
        $key = strtolower($key);
556
        $split = str_split($key);
557
558
        foreach ($split as $char) {
559
            if (
560
                ($char >= 'a' && $char <= 'z') || ($char >= '0' && $char <= '9')
561
            ) {
562
                $newKey .= $char;
563
564
                continue;
565
            }
566
567
            $newKey .= '_';
568
        }
569
570
        return $newKey;
571
    }
572
}
573