PortfolioController   F
last analyzed

Complexity

Total Complexity 406

Size/Duplication

Total Lines 4393
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 2548
c 0
b 0
f 0
dl 0
loc 4393
rs 0.8
wmc 406

58 Methods

Rating   Name   Duplication   Size   Complexity  
C commentVisibilityChooser() 0 131 10
A copyComment() 0 28 1
B processAttachments() 0 63 8
A copyItem() 0 30 1
A showHideCategory() 0 17 2
F editItem() 0 194 16
A createFormTagFilter() 0 49 5
A addCategory() 0 71 4
A deleteCategory() 0 15 2
C addItem() 0 231 10
C itemVisibilityChooser() 0 132 10
A qualifyComment() 0 73 2
A markAsTemplate() 0 21 3
A translateCategory() 0 76 3
A getHighlightedItems() 0 63 4
A markImportantCommentInItem() 0 19 2
A __construct() 0 13 3
A deleteTag() 0 22 2
F listCategories() 0 93 13
B deleteAttachment() 0 63 9
C listTags() 0 150 10
F view() 0 373 42
B editCategory() 0 84 5
B teacherCopyComment() 0 84 5
B editComment() 0 100 5
B teacherCopyItem() 0 80 5
A markAsHighlighted() 0 28 4
B downloadAttachment() 0 47 7
F exportZip() 0 227 21
A renderView() 0 29 5
A deleteItem() 0 20 2
A showHideItem() 0 28 5
F exportPdf() 0 117 15
A addAttachmentsFieldToForm() 0 21 1
A getCategoriesForIndex() 0 14 4
A categoryBelongToOwner() 0 7 2
A blockIsNotAllowed() 0 4 2
B createFormStudentFilter() 0 86 6
F index() 0 151 19
A commentBelongsToOwner() 0 3 1
A itemBelongToOwner() 0 7 2
A deleteComment() 0 20 2
A isAllowed() 0 18 5
F details() 0 361 38
A markAsTemplateComment() 0 21 3
A qualifyItem() 0 65 2
B generateAttachmentList() 0 63 8
A getCommentsInHtmlFormatted() 0 25 2
A translateDisplayName() 0 5 2
D getItemsForIndex() 0 82 15
C fixMediaSourcesToHtml() 0 65 14
B generateItemContent() 0 51 7
B getCommentsForIndex() 0 66 8
A getLabelForCommentDate() 0 26 4
A formatZipIndexFile() 0 41 3
B createCommentForm() 0 109 5
A getLanguageVariable() 0 10 2
B getItemsInHtmlFormatted() 0 56 8

How to fix   Complexity   

Complex Class

Complex classes like PortfolioController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PortfolioController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
6
use Chamilo\CoreBundle\Entity\Course;
7
use Chamilo\CoreBundle\Entity\ExtraField as ExtraFieldEntity;
8
use Chamilo\CoreBundle\Entity\ExtraFieldRelTag;
9
use Chamilo\CoreBundle\Entity\Portfolio;
10
use Chamilo\CoreBundle\Entity\PortfolioAttachment;
11
use Chamilo\CoreBundle\Entity\PortfolioCategory;
12
use Chamilo\CoreBundle\Entity\PortfolioComment;
13
use Chamilo\CoreBundle\Entity\PortfolioRelTag;
14
use Chamilo\CoreBundle\Entity\Session;
15
use Chamilo\CoreBundle\Entity\Tag;
16
use Chamilo\CoreBundle\Entity\User;
17
use Chamilo\CoreBundle\Event\Events;
18
use Chamilo\CoreBundle\Event\PortfolioCommentEditedEvent;
19
use Chamilo\CoreBundle\Event\PortfolioCommentScoredEvent;
20
use Chamilo\CoreBundle\Event\PortfolioItemAddedEvent;
21
use Chamilo\CoreBundle\Event\PortfolioItemCommentedEvent;
22
use Chamilo\CoreBundle\Event\PortfolioItemDeletedEvent;
23
use Chamilo\CoreBundle\Event\PortfolioItemDownloadedEvent;
24
use Chamilo\CoreBundle\Event\PortfolioItemEditedEvent;
25
use Chamilo\CoreBundle\Event\PortfolioItemScoredEvent;
26
use Chamilo\CoreBundle\Event\PortfolioItemViewedEvent;
27
use Chamilo\CoreBundle\Event\PortfolioItemVisibilityChangedEvent;
28
use Chamilo\CoreBundle\Framework\Container;
29
use Chamilo\CourseBundle\Entity\CItemProperty;
30
use Doctrine\ORM\Query\Expr\Join;
31
use Mpdf\MpdfException;
32
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
33
use Symfony\Component\Filesystem\Filesystem;
34
use Symfony\Component\HttpFoundation\Request as HttpRequest;
35
36
/**
37
 * Class PortfolioController.
38
 */
39
class PortfolioController
40
{
41
    public string $baseUrl;
42
    private ?Course $course;
43
    private ?Session $session;
44
    private User $owner;
45
    private \Doctrine\ORM\EntityManagerInterface $em;
46
    private bool $advancedSharingEnabled;
47
48
    /**
49
     * PortfolioController constructor.
50
     */
51
    public function __construct()
52
    {
53
        $this->em = Database::getManager();
54
55
        $this->owner = api_get_user_entity();
56
        $this->course = api_get_course_entity();
57
        $this->session = api_get_session_entity();
58
59
        $cidreq = api_get_cidreq();
60
        $this->baseUrl = api_get_self().'?'.($cidreq ? $cidreq.'&' : '');
61
62
        $this->advancedSharingEnabled = true === api_get_configuration_value('portfolio_advanced_sharing')
63
            && $this->course;
64
    }
65
66
    /**
67
     * @throws Exception
68
     */
69
    public function translateCategory($category, $languages, $languageId): void
70
    {
71
        global $interbreadcrumb;
72
73
        $originalName = $category->getTitle();
74
        $variableLanguage = '$'.$this->getLanguageVariable($originalName);
75
76
        $translateUrl = api_get_path(WEB_AJAX_PATH).'lang.ajax.php?a=translate_portfolio_category&sec_token='.Security::get_token();
77
        $form = new FormValidator('new_lang_variable', 'POST', $translateUrl);
78
        $form->addHeader(get_lang('Add terms to the sub-language'));
79
        $form->addText('variable_language', get_lang('Language variable'), false);
80
        $form->addText('original_name', get_lang('Original name'), false);
81
82
        $languagesOptions = [0 => get_lang('None')];
83
        foreach ($languages as $language) {
84
            $languagesOptions[$language->getId()] = $language->getOriginalName();
85
        }
86
87
        $form->addSelect(
88
            'sub_language',
89
            [get_lang('Sub-language'), get_lang('Only active sub-languages appear in this list')],
90
            $languagesOptions
91
        );
92
93
        if ($languageId) {
94
            $languageInfo = api_get_language_info($languageId);
95
            $form->addText(
96
                'new_language',
97
                [get_lang('Translation'), get_lang('If this term has already been translated, this operation will replace its translation for this sub-language.')]
98
            );
99
100
            $form->addHidden('category_id', $category->getId());
101
            $form->addHidden('id', $languageInfo['parent_id']);
102
            $form->addHidden('sub', $languageInfo['id']);
103
            $form->addHidden('sub_language_id', $languageInfo['id']);
104
            $form->addHidden('redirect', true);
105
            $form->addButtonSave(get_lang('Save'));
106
        }
107
108
        $form->setDefaults([
109
            'variable_language' => $variableLanguage,
110
            'original_name' => $originalName,
111
            'sub_language' => $languageId,
112
        ]);
113
        $form->addRule('sub_language', get_lang('Required'), 'required');
114
        $form->freeze(['variable_language', 'original_name']);
115
116
        $interbreadcrumb[] = [
117
            'name' => get_lang('Portfolio'),
118
            'url' => $this->baseUrl,
119
        ];
120
        $interbreadcrumb[] = [
121
            'name' => get_lang('Categories'),
122
            'url' => $this->baseUrl.'action=list_categories&parent_id='.$category->getParentId(),
123
        ];
124
        $interbreadcrumb[] = [
125
            'name' => Security::remove_XSS($category->getTitle()),
126
            'url' => $this->baseUrl.'action=edit_category&id='.$category->getId(),
127
        ];
128
129
        $actions = [];
130
        $actions[] = Display::url(
131
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
132
            $this->baseUrl.'action=edit_category&id='.$category->getId()
133
        );
134
135
        $js = '<script>
136
            $(function() {
137
              $("select[name=\'sub_language\']").on("change", function () {
138
                    location.href += "&sub_language=" + this.value;
139
                });
140
            });
141
        </script>';
142
        $content = $form->returnForm();
143
144
        $this->renderView($content.$js, get_lang('Translate category'), $actions);
145
    }
146
147
    public function listCategories(): void
148
    {
149
        global $interbreadcrumb;
150
151
        $parentId = isset($_REQUEST['parent_id']) ? (int) $_REQUEST['parent_id'] : 0;
152
        $table = new HTML_Table(['class' => 'table table-hover table-striped data_table']);
153
        $headers = [
154
            get_lang('Title'),
155
            get_lang('Description'),
156
        ];
157
        if ($parentId === 0) {
158
            $headers[] = get_lang('Sub-categories');
159
        }
160
        $headers[] = get_lang('Actions');
161
162
        $column = 0;
163
        foreach ($headers as $header) {
164
            $table->setHeaderContents(0, $column, $header);
165
            $column++;
166
        }
167
        $currentUserId = api_get_user_id();
168
        $row = 1;
169
        $categories = $this->getCategoriesForIndex($parentId);
170
171
        foreach ($categories as $category) {
172
            $column = 0;
173
            $subcategories = $this->getCategoriesForIndex($category->getId());
174
            $linkSubCategories = $category->getTitle();
175
            if (count($subcategories) > 0) {
176
                $linkSubCategories = Display::url(
177
                    $category->getTitle(),
178
                    $this->baseUrl.'action=list_categories&parent_id='.$category->getId()
179
                );
180
            }
181
            $table->setCellContents($row, $column++, $linkSubCategories);
182
            $table->setCellContents($row, $column++, strip_tags($category->getDescription()));
183
            if ($parentId === 0) {
184
                $table->setCellContents($row, $column++, count($subcategories));
185
            }
186
187
            // Actions
188
            $links = null;
189
            // Edit action
190
            $url = $this->baseUrl.'action=edit_category&id='.$category->getId();
191
            $links .= Display::url(Display::return_icon('edit.png', get_lang('Edit')), $url).'&nbsp;';
192
            // Visible action: if active
193
            if ($category->isVisible() != 0) {
194
                $url = $this->baseUrl.'action=hide_category&id='.$category->getId();
195
                $links .= Display::url(Display::return_icon('visible.png', get_lang('Hide')), $url).'&nbsp;';
196
            } else { // else if not active
197
                $url = $this->baseUrl.'action=show_category&id='.$category->getId();
198
                $links .= Display::url(Display::return_icon('invisible.png', get_lang('Show')), $url).'&nbsp;';
199
            }
200
            // Delete action
201
            $url = $this->baseUrl.'action=delete_category&id='.$category->getId();
202
            $links .= Display::url(Display::return_icon('delete.png', get_lang('Delete')), $url, ['onclick' => 'javascript:if(!confirm(\''.get_lang('Are you sure to delete').'?\')) return false;']);
203
204
            $table->setCellContents($row, $column++, $links);
205
            $row++;
206
        }
207
208
        $interbreadcrumb[] = [
209
            'name' => get_lang('Portfolio'),
210
            'url' => $this->baseUrl,
211
        ];
212
        if ($parentId > 0) {
213
            $interbreadcrumb[] = [
214
                'name' => get_lang('Categories'),
215
                'url' => $this->baseUrl.'action=list_categories',
216
            ];
217
        }
218
219
        $actions = [];
220
        $actions[] = Display::url(
221
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
222
            $this->baseUrl.($parentId > 0 ? 'action=list_categories' : '')
223
        );
224
        if ($currentUserId == $this->owner->getId() && $parentId === 0) {
225
            $actions[] = Display::url(
226
                Display::return_icon('new_folder.png', get_lang('Add category'), [], ICON_SIZE_MEDIUM),
227
                $this->baseUrl.'action=add_category'
228
            );
229
        }
230
        $content = $table->toHtml();
231
232
        $pageTitle = get_lang('Categories');
233
        if ($parentId > 0) {
234
            $em = Database::getManager();
235
            $parentCategory = $em->find(PortfolioCategory::class, $parentId);
236
            $pageTitle = $parentCategory->getTitle().' : '.get_lang('Sub-categories');
237
        }
238
239
        $this->renderView($content, $pageTitle, $actions);
240
    }
241
242
    /**
243
     * @throws Exception
244
     */
245
    public function addCategory(): void
246
    {
247
        global $interbreadcrumb;
248
249
        Display::addFlash(
250
            Display::return_message(get_lang('Categories are for organization only in personal portfolio.'), 'info')
251
        );
252
253
        $form = new FormValidator('add_category', 'post', "{$this->baseUrl}&action=add_category");
254
255
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
256
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
257
        } else {
258
            $form->addText('title', get_lang('Title'));
259
            $form->applyFilter('title', 'trim');
260
        }
261
262
        $form->addHtmlEditor('description', get_lang('Description'), false, false, ['ToolbarSet' => 'Minimal']);
263
264
        $parentSelect = $form->addSelect(
265
            'parent_id',
266
            get_lang('Parent category')
267
        );
268
        $parentSelect->addOption(sprintf(get_lang('Level %s'), '0'), 0);
269
        $categories = $this->getCategoriesForIndex(0);
270
271
        foreach ($categories as $category) {
272
            $parentSelect->addOption($category->getTitle(), $category->getId());
273
        }
274
275
        $form->addButtonCreate(get_lang('Create'));
276
277
        if ($form->validate()) {
278
            $values = $form->exportValues();
279
280
            $category = new PortfolioCategory();
281
            $category
282
                ->setTitle($values['title'])
283
                ->setDescription($values['description'])
284
                ->setParentId($values['parent_id'])
285
                ->setUser($this->owner);
286
287
            $this->em->persist($category);
288
            $this->em->flush();
289
290
            Display::addFlash(
291
                Display::return_message(get_lang('Category added'), 'success')
292
            );
293
294
            header("Location: {$this->baseUrl}action=list_categories");
295
            exit;
296
        }
297
298
        $interbreadcrumb[] = [
299
            'name' => get_lang('Portfolio'),
300
            'url' => $this->baseUrl,
301
        ];
302
        $interbreadcrumb[] = [
303
            'name' => get_lang('Categories'),
304
            'url' => $this->baseUrl.'action=list_categories',
305
        ];
306
307
        $actions = [];
308
        $actions[] = Display::url(
309
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
310
            $this->baseUrl.'action=list_categories'
311
        );
312
313
        $content = $form->returnForm();
314
315
        $this->renderView($content, get_lang('Add category'), $actions);
316
    }
317
318
    /**
319
     * @throws \Exception
320
     */
321
    public function editCategory(PortfolioCategory $category): void
322
    {
323
        global $interbreadcrumb;
324
325
        if (!api_is_platform_admin()) {
326
            api_not_allowed(true);
327
        }
328
329
        Display::addFlash(
330
            Display::return_message(get_lang('Categories are for organization only in personal portfolio.'), 'info')
331
        );
332
333
        $form = new FormValidator(
334
            'edit_category',
335
            'post',
336
            $this->baseUrl."action=edit_category&id={$category->getId()}"
337
        );
338
339
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
340
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
341
        } else {
342
            $translateUrl = $this->baseUrl.'action=translate_category&id='.$category->getId();
343
            $translateButton = Display::toolbarButton(get_lang('Translate this term'), $translateUrl, 'language', 'link');
344
            $form->addText(
345
                'title',
346
                [get_lang('Title'), $translateButton]
347
            );
348
            $form->applyFilter('title', 'trim');
349
        }
350
351
        $form->addHtmlEditor('description', get_lang('Description'), false, false, ['ToolbarSet' => 'Minimal']);
352
        $form->addButtonUpdate(get_lang('Update'));
353
        $form->setDefaults(
354
            [
355
                'title' => $category->getTitle(),
356
                'description' => $category->getDescription(),
357
            ]
358
        );
359
360
        if ($form->validate()) {
361
            $values = $form->exportValues();
362
363
            $category
364
                ->setTitle($values['title'])
365
                ->setDescription($values['description']);
366
367
            $this->em->persist($category);
368
            $this->em->flush();
369
370
            Display::addFlash(
371
                Display::return_message(get_lang('Updated'), 'success')
372
            );
373
374
            header("Location: {$this->baseUrl}action=list_categories&parent_id=".$category->getParentId());
375
            exit;
376
        }
377
378
        $interbreadcrumb[] = [
379
            'name' => get_lang('Portfolio'),
380
            'url' => $this->baseUrl,
381
        ];
382
        $interbreadcrumb[] = [
383
            'name' => get_lang('Categories'),
384
            'url' => $this->baseUrl.'action=list_categories',
385
        ];
386
        if ($category->getParentId() > 0) {
387
            $em = Database::getManager();
388
            $parentCategory = $em->find(PortfolioCategory::class, $category->getParentId());
389
            $pageTitle = $parentCategory->getTitle().' : '.get_lang('Sub-categories');
390
            $interbreadcrumb[] = [
391
                'name' => Security::remove_XSS($pageTitle),
392
                'url' => $this->baseUrl.'action=list_categories&parent_id='.$category->getParentId(),
393
            ];
394
        }
395
396
        $actions = [];
397
        $actions[] = Display::url(
398
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
399
            $this->baseUrl.'action=list_categories&parent_id='.$category->getParentId()
400
        );
401
402
        $content = $form->returnForm();
403
404
        $this->renderView($content, get_lang('Edit this category'), $actions);
405
    }
406
407
    /**
408
     * @throws \Doctrine\ORM\OptimisticLockException
409
     * @throws \Doctrine\ORM\Exception\ORMException
410
     */
411
    public function showHideCategory(PortfolioCategory $category): never
412
    {
413
        if (!$this->categoryBelongToOwner($category)) {
414
            api_not_allowed(true);
415
        }
416
417
        $category->setIsVisible(!$category->isVisible());
418
419
        $this->em->persist($category);
420
        $this->em->flush();
421
422
        Display::addFlash(
423
            Display::return_message(get_lang('Post visibility changed'), 'success')
424
        );
425
426
        header("Location: {$this->baseUrl}action=list_categories");
427
        exit;
428
    }
429
430
    /**
431
     * @throws \Doctrine\ORM\OptimisticLockException
432
     * @throws \Doctrine\ORM\Exception\ORMException
433
     */
434
    public function deleteCategory(PortfolioCategory $category): never
435
    {
436
        if (!api_is_platform_admin()) {
437
            api_not_allowed(true);
438
        }
439
440
        $this->em->remove($category);
441
        $this->em->flush();
442
443
        Display::addFlash(
444
            Display::return_message(get_lang('The category has been deleted.'), 'success')
445
        );
446
447
        header("Location: {$this->baseUrl}action=list_categories");
448
        exit;
449
    }
450
451
    /**
452
     * @throws \Exception
453
     */
454
    public function addItem(): void
455
    {
456
        global $interbreadcrumb;
457
458
        $this->blockIsNotAllowed();
459
460
        $templates = Container::getPortfolioRepository()->findTemplates(
461
            $this->owner,
462
            $this->course,
463
            $this->session
464
        );
465
466
        $form = new FormValidator('add_portfolio', 'post', $this->baseUrl.'action=add_item');
467
        $form->addSelectFromCollection(
468
            'template',
469
            [
470
                get_lang('Template'),
471
                null,
472
                '<span id="portfolio-spinner" class="fa fa-fw fa-spinner fa-spin" style="display: none;"
473
                    aria-hidden="true" aria-label="'.get_lang('Loading').'"></span>',
474
            ],
475
            $templates,
476
            [],
477
            true,
478
            'getTitle'
479
        );
480
481
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
482
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
483
        } else {
484
            $form->addText('title', get_lang('Title'));
485
            $form->applyFilter('title', 'trim');
486
        }
487
        $editorConfig = [
488
            'ToolbarSet' => 'Documents',
489
            'Width' => '100%',
490
            'Height' => '400',
491
            'cols-size' => [2, 10, 0],
492
        ];
493
        $form->addHtmlEditor('content', get_lang('Content'), true, false, $editorConfig);
494
495
        $categoriesSelect = $form->addSelect(
496
            'category',
497
            [get_lang('Category'), get_lang('Categories are for organization only in personal portfolio.')]
498
        );
499
        $categoriesSelect->addOption(get_lang('Select a category'), 0);
500
        $parentCategories = $this->getCategoriesForIndex(0);
501
        foreach ($parentCategories as $parentCategory) {
502
            $categoriesSelect->addOption($this->translateDisplayName($parentCategory->getTitle()), $parentCategory->getId());
503
            $subCategories = $this->getCategoriesForIndex($parentCategory->getId());
504
            if (count($subCategories) > 0) {
505
                foreach ($subCategories as $subCategory) {
506
                    $categoriesSelect->addOption(' &mdash; '.$this->translateDisplayName($subCategory->getTitle()), $subCategory->getId());
507
                }
508
            }
509
        }
510
511
        $extraField = new ExtraField('portfolio');
512
        $extra = $extraField->addElements(
513
            $form,
514
            0,
515
            $this->course ? [] : ['tags']
516
        );
517
518
        $this->addAttachmentsFieldToForm($form);
519
520
        $form->addButtonCreate(get_lang('Create'));
521
522
        if ($form->validate()) {
523
            $values = $form->exportValues();
524
525
            $portfolio = new Portfolio();
526
            $portfolio
527
                ->setTitle($values['title'])
528
                ->setContent($values['content'])
529
                ->setCreator($this->owner)
530
                ->setParent($this->owner)
531
                ->addCourseLink($this->course, $this->session)
532
                ->setCategory(
533
                    $this->em->find(PortfolioCategory::class, $values['category'])
534
                )
535
            ;
536
537
            $this->em->persist($portfolio);
538
            $this->em->flush();
539
540
            $values['item_id'] = $portfolio->getId();
541
542
            $extraFieldValue = new ExtraFieldValue('portfolio');
543
            $extraFieldValue->saveFieldValues($values);
544
545
            $this->processAttachments(
546
                $form,
547
                $this->owner,
548
                $portfolio->getId(),
549
                Portfolio::TYPE_ITEM
550
            );
551
552
            Container::getEventDispatcher()->dispatch(
553
                new PortfolioItemAddedEvent(['portfolio' => $portfolio]),
554
                Events::PORTFOLIO_ITEM_ADDED
555
            );
556
557
            if (1 == api_get_course_setting('email_alert_teachers_new_post')) {
558
                if ($this->session) {
559
                    $messageCourseTitle = "{$this->course->getTitle()} ({$this->session->getTitle()})";
560
561
                    $teachers = SessionManager::getCoachesByCourseSession(
562
                        $this->session->getId(),
563
                        $this->course->getId()
564
                    );
565
                    $userIdListToSend = array_values($teachers);
566
                } else {
567
                    $messageCourseTitle = $this->course->getTitle();
568
569
                    $teachers = CourseManager::get_teacher_list_from_course_code($this->course->getCode());
570
571
                    $userIdListToSend = array_keys($teachers);
572
                }
573
574
                $messageSubject = sprintf(get_lang('[Portfolio] New post in course %s'), $messageCourseTitle);
575
                $messageContent = sprintf(
576
                    get_lang("There is a new post by %s in the portfolio of course %s. To view it <a href='%s'>go here</a>."),
577
                    $this->owner->getFullName(),
578
                    $messageCourseTitle,
579
                    $this->baseUrl.http_build_query(['action' => 'view', 'id' => $portfolio->getId()])
580
                );
581
                $messageContent .= '<br><br><dl>'
582
                    .'<dt>'.Security::remove_XSS($portfolio->getTitle()).'</dt>'
583
                    .'<dd>'.$portfolio->getExcerpt().'</dd>'.'</dl>';
584
585
                foreach ($userIdListToSend as $userIdToSend) {
586
                    MessageManager::send_message_simple(
587
                        $userIdToSend,
588
                        $messageSubject,
589
                        $messageContent,
590
                        0,
591
                        false,
592
                        false,
593
                        [],
594
                        false
595
                    );
596
                }
597
            }
598
599
            Display::addFlash(
600
                Display::return_message(get_lang('Portfolio item added'), 'success')
601
            );
602
603
            header("Location: $this->baseUrl");
604
            exit;
605
        }
606
607
        $interbreadcrumb[] = [
608
            'name' => get_lang('Portfolio'),
609
            'url' => $this->baseUrl,
610
        ];
611
612
        $actions = [];
613
        $actions[] = Display::url(
614
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
615
            $this->baseUrl
616
        );
617
        $actions[] = '<a id="hide_bar_template" href="#" role="button">'.
618
            Display::return_icon('expand.png', get_lang('Expand'), ['id' => 'expand'], ICON_SIZE_MEDIUM).
619
            Display::return_icon('contract.png', get_lang('Collapse'), ['id' => 'contract', 'class' => 'hide'], ICON_SIZE_MEDIUM).'</a>';
620
621
        $js = '<script>
622
            $(function() {
623
                $(".scrollbar-light").scrollbar();
624
                $(".scroll-wrapper").css("height", "550px");
625
                expandColumnToogle("#hide_bar_template", {
626
                    selector: "#template_col",
627
                    width: 3
628
                }, {
629
                    selector: "#doc_form",
630
                    width: 9
631
                });
632
                CKEDITOR.on("instanceReady", function (e) {
633
                    showTemplates();
634
                });
635
                $(window).on("load", function () {
636
                    $("input[name=\'title\']").focus();
637
                });
638
                $(\'#add_portfolio_template\').on(\'change\', function () {
639
                    $(\'#portfolio-spinner\').show();
640
641
                    $.getJSON(_p.web_ajax + \'portfolio.ajax.php?a=find_template&item=\' + this.value)
642
                        .done(function(response) {
643
                            if (CKEDITOR.instances.title) {
644
                                CKEDITOR.instances.title.setData(response.title);
645
                            } else {
646
                                document.getElementById(\'add_portfolio_title\').value = response.title;
647
                            }
648
649
                            CKEDITOR.instances.content.setData(response.content);
650
                        })
651
                        .fail(function () {
652
                            if (CKEDITOR.instances.title) {
653
                                CKEDITOR.instances.title.setData(\'\');
654
                            } else {
655
                                document.getElementById(\'add_portfolio_title\').value = \'\';
656
                            }
657
658
                            CKEDITOR.instances.content.setData(\'\');
659
                        })
660
                        .always(function() {
661
                          $(\'#portfolio-spinner\').hide();
662
                        });
663
                });
664
                '.$extra['jquery_ready_content'].'
665
            });
666
        </script>';
667
        $content = '<div class="page-create">
668
            <div class="row" style="overflow:hidden">
669
            <div id="template_col" class="col-md-3">
670
                <div class="panel panel-default">
671
                <div class="panel-body">
672
                    <div id="frmModel" class="items-templates scrollbar-light"></div>
673
                </div>
674
                </div>
675
            </div>
676
            <div id="doc_form" class="col-md-9">
677
                '.$form->returnForm().'
678
            </div>
679
          </div></div>';
680
681
        $this->renderView(
682
            $content.$js,
683
            get_lang('Add item to portfolio'),
684
            $actions
685
        );
686
    }
687
688
    /**
689
     * @throws \Exception
690
     */
691
    public function editItem(Portfolio $item): void
692
    {
693
        global $interbreadcrumb;
694
695
        if (!api_is_allowed_to_edit() && !$this->itemBelongToOwner($item)) {
696
            api_not_allowed(true);
697
        }
698
699
        $itemCourse = $item->getCourse();
700
        $itemSession = $item->getSession();
701
702
        $form = new FormValidator('edit_portfolio', 'post', $this->baseUrl."action=edit_item&id={$item->getId()}");
703
704
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
705
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
706
        } else {
707
            $form->addText('title', get_lang('Title'));
708
            $form->applyFilter('title', 'trim');
709
        }
710
711
        if ($item->getOrigin()) {
712
            if (Portfolio::TYPE_ITEM === $item->getOriginType()) {
713
                $origin = $this->em->find(Portfolio::class, $item->getOrigin());
714
715
                $form->addLabel(
716
                    sprintf(get_lang('Portfolio item by %s'), $origin->getUser()->getFullName()),
717
                    Display::panel(
718
                        Security::remove_XSS($origin->getContent())
719
                    )
720
                );
721
            } elseif (Portfolio::TYPE_COMMENT === $item->getOriginType()) {
722
                $origin = $this->em->find(PortfolioComment::class, $item->getOrigin());
723
724
                $form->addLabel(
725
                    sprintf(get_lang('Comment by %s'), $origin->getAuthor()->getFullName()),
726
                    Display::panel(
727
                        Security::remove_XSS($origin->getContent())
728
                    )
729
                );
730
            }
731
        }
732
        $editorConfig = [
733
            'ToolbarSet' => 'Documents',
734
            'Width' => '100%',
735
            'Height' => '400',
736
            'cols-size' => [2, 10, 0],
737
        ];
738
        $form->addHtmlEditor('content', get_lang('Content'), true, false, $editorConfig);
739
        $categoriesSelect = $form->addSelect(
740
            'category',
741
            [get_lang('Category'), get_lang('Categories are for organization only in personal portfolio.')]
742
        );
743
        $categoriesSelect->addOption(get_lang('Select a category'), 0);
744
        $parentCategories = $this->getCategoriesForIndex(0);
745
        foreach ($parentCategories as $parentCategory) {
746
            $categoriesSelect->addOption($this->translateDisplayName($parentCategory->getTitle()), $parentCategory->getId());
747
            $subCategories = $this->getCategoriesForIndex($parentCategory->getId());
748
            if (count($subCategories) > 0) {
749
                foreach ($subCategories as $subCategory) {
750
                    $categoriesSelect->addOption(' &mdash; '.$this->translateDisplayName($subCategory->getTitle()), $subCategory->getId());
751
                }
752
            }
753
        }
754
755
        $extraField = new ExtraField('portfolio');
756
        $extra = $extraField->addElements(
757
            $form,
758
            $item->getId(),
759
            $this->course ? [] : ['tags']
760
        );
761
762
        $attachmentList = $this->generateAttachmentList($item, false);
763
764
        if (!empty($attachmentList)) {
765
            $form->addLabel(get_lang('Attachments'), $attachmentList);
766
        }
767
768
        $this->addAttachmentsFieldToForm($form);
769
770
        $form->addButtonUpdate(get_lang('Update'));
771
        $form->setDefaults(
772
            [
773
                'title' => $item->getTitle(),
774
                'content' => $item->getContent(),
775
                'category' => $item->getCategory() ? $item->getCategory()->getId() : '',
776
            ]
777
        );
778
779
        if ($form->validate()) {
780
            if ($itemCourse) {
781
                api_item_property_update(
782
                    api_get_course_info($itemCourse->getCode()),
783
                    TOOL_PORTFOLIO,
784
                    $item->getId(),
785
                    'PortfolioUpdated',
786
                    api_get_user_id(),
787
                    [],
788
                    null,
789
                    '',
790
                    '',
791
                    $itemSession ? $itemSession->getId() : 0
792
                );
793
            }
794
795
            $values = $form->exportValues();
796
            $currentTime = new DateTime(api_get_utc_datetime(), new DateTimeZone('UTC'));
797
798
            $item
799
                ->setTitle($values['title'])
800
                ->setContent($values['content'])
801
                ->setUpdateDate($currentTime)
802
                ->setCategory(
803
                    $this->em->find('ChamiloCoreBundle:PortfolioCategory', $values['category'])
804
                );
805
806
            $values['item_id'] = $item->getId();
807
808
            $extraFieldValue = new ExtraFieldValue('portfolio');
809
            $extraFieldValue->saveFieldValues($values);
810
811
            $this->em->persist($item);
812
            $this->em->flush();
813
814
            Container::getEventDispatcher()->dispatch(
815
                new PortfolioItemEditedEvent(['portfolio' => $item]),
816
                Events::PORTFOLIO_ITEM_EDITED
817
            );
818
819
            $this->processAttachments(
820
                $form,
821
                $item->getUser(),
822
                $item->getId(),
823
                Portfolio::TYPE_ITEM
824
            );
825
826
            Display::addFlash(
827
                Display::return_message(get_lang('Item updated'), 'success')
828
            );
829
830
            header("Location: $this->baseUrl");
831
            exit;
832
        }
833
834
        $interbreadcrumb[] = [
835
            'name' => get_lang('Portfolio'),
836
            'url' => $this->baseUrl,
837
        ];
838
        $actions = [];
839
        $actions[] = Display::url(
840
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
841
            $this->baseUrl
842
        );
843
        $actions[] = '<a id="hide_bar_template" href="#" role="button">'.
844
            Display::return_icon('expand.png', get_lang('Expand'), ['id' => 'expand'], ICON_SIZE_MEDIUM).
845
            Display::return_icon('contract.png', get_lang('Collapse'), ['id' => 'contract', 'class' => 'hide'], ICON_SIZE_MEDIUM).'</a>';
846
847
        $js = '<script>
848
            $(function() {
849
                $(".scrollbar-light").scrollbar();
850
                $(".scroll-wrapper").css("height", "550px");
851
                expandColumnToogle("#hide_bar_template", {
852
                    selector: "#template_col",
853
                    width: 3
854
                }, {
855
                    selector: "#doc_form",
856
                    width: 9
857
                });
858
                CKEDITOR.on("instanceReady", function (e) {
859
                    showTemplates();
860
                });
861
                $(window).on("load", function () {
862
                    $("input[name=\'title\']").focus();
863
                });
864
                '.$extra['jquery_ready_content'].'
865
            });
866
        </script>';
867
        $content = '<div class="page-create">
868
            <div class="row" style="overflow:hidden">
869
            <div id="template_col" class="col-md-3">
870
                <div class="panel panel-default">
871
                <div class="panel-body">
872
                    <div id="frmModel" class="items-templates scrollbar-light"></div>
873
                </div>
874
                </div>
875
            </div>
876
            <div id="doc_form" class="col-md-9">
877
                '.$form->returnForm().'
878
            </div>
879
          </div></div>';
880
881
        $this->renderView(
882
            $content.$js,
883
            get_lang('Edit portfolio item'),
884
            $actions
885
        );
886
    }
887
888
    /**
889
     * @throws \Doctrine\ORM\ORMException
890
     * @throws \Doctrine\ORM\OptimisticLockException
891
     */
892
    public function showHideItem(Portfolio $item): never
893
    {
894
        if (!$this->itemBelongToOwner($item)) {
895
            api_not_allowed(true);
896
        }
897
898
        switch ($item->getVisibility()) {
899
            case Portfolio::VISIBILITY_HIDDEN:
900
                $item->setVisibility(Portfolio::VISIBILITY_VISIBLE);
901
                break;
902
            case Portfolio::VISIBILITY_VISIBLE:
903
                $item->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER);
904
                break;
905
            case Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER:
906
            default:
907
                $item->setVisibility(Portfolio::VISIBILITY_HIDDEN);
908
                break;
909
        }
910
911
        $this->em->persist($item);
912
        $this->em->flush();
913
914
        Display::addFlash(
915
            Display::return_message(get_lang('The visibility has been changed.'), 'success')
916
        );
917
918
        header("Location: $this->baseUrl");
919
        exit;
920
    }
921
922
    /**
923
     * @throws \Doctrine\ORM\ORMException
924
     * @throws \Doctrine\ORM\OptimisticLockException
925
     */
926
    public function deleteItem(Portfolio $item)
927
    {
928
        if (!$this->itemBelongToOwner($item)) {
929
            api_not_allowed(true);
930
        }
931
932
        Container::getEventDispatcher()->dispatch(
933
            new PortfolioItemDeletedEvent(['portfolio' => $item]),
934
            Events::PORTFOLIO_ITEM_DELETED
935
        );
936
937
        $this->em->remove($item);
938
        $this->em->flush();
939
940
        Display::addFlash(
941
            Display::return_message(get_lang('Item deleted'), 'success')
942
        );
943
944
        header("Location: $this->baseUrl");
945
        exit;
946
    }
947
948
    /**
949
     * @throws \Exception
950
     */
951
    public function index(HttpRequest $httpRequest): void
952
    {
953
        $listByUser = false;
954
        $listHighlighted = $httpRequest->query->has('list_highlighted');
955
        $listAlphabetical = $httpRequest->query->has('list_alphabetical');
956
957
        if ($httpRequest->query->has('user')) {
958
            $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
959
960
            if (empty($this->owner)) {
961
                api_not_allowed(true);
962
            }
963
964
            $listByUser = true;
965
        }
966
967
        $actions = [];
968
969
        if (api_is_platform_admin()) {
970
            $actions[] = Display::url(
971
                Display::return_icon('add.png', get_lang('Add'), [], ICON_SIZE_MEDIUM),
972
                $this->baseUrl.'action=add_item'
973
            );
974
            $actions[] = Display::url(
975
                Display::return_icon('folder.png', get_lang('Add category'), [], ICON_SIZE_MEDIUM),
976
                $this->baseUrl.'action=list_categories'
977
            );
978
            $actions[] = Display::url(
979
                Display::return_icon('waiting_list.png', get_lang('Portfolio details'), [], ICON_SIZE_MEDIUM),
980
                $this->baseUrl.'action=details'
981
            );
982
        } elseif (api_get_user_entity() === $this->owner) {
983
            if ($this->isAllowed()) {
984
                $actions[] = Display::url(
985
                    Display::return_icon('add.png', get_lang('Add'), [], ICON_SIZE_MEDIUM),
986
                    $this->baseUrl.'action=add_item'
987
                );
988
                $actions[] = Display::url(
989
                    Display::return_icon('waiting_list.png', get_lang('Portfolio details'), [], ICON_SIZE_MEDIUM),
990
                    $this->baseUrl.'action=details'
991
                );
992
            }
993
        } else {
994
            $actions[] = Display::url(
995
                Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
996
                $this->baseUrl
997
            );
998
        }
999
1000
        if (api_is_allowed_to_edit()) {
1001
            $actions[] = Display::url(
1002
                Display::return_icon('tickets.png', get_lang('Tags'), [], ICON_SIZE_MEDIUM),
1003
                $this->baseUrl.'action=tags'
1004
            );
1005
        }
1006
1007
        $frmStudentList = null;
1008
        $frmTagList = null;
1009
1010
        $categories = [];
1011
        $portfolio = [];
1012
        if ($this->course) {
1013
            $frmTagList = $this->createFormTagFilter($listByUser);
1014
            $frmStudentList = $this->createFormStudentFilter($listByUser, $listHighlighted, $listAlphabetical);
1015
            $frmStudentList->setDefaults(['user' => $this->owner->getId()]);
1016
            // it translates the category title with the current user language
1017
            $categories = $this->getCategoriesForIndex(0);
1018
            if (count($categories) > 0) {
1019
                foreach ($categories as &$category) {
1020
                    $translated = $this->translateDisplayName($category->getTitle());
1021
                    $category->setTitle($translated);
1022
                }
1023
            }
1024
        } else {
1025
            // it displays the list in Network Social for the current user
1026
            $portfolio = $this->getCategoriesForIndex();
1027
        }
1028
1029
        $foundComments = [];
1030
1031
        if ($listHighlighted) {
1032
            $items = $this->getHighlightedItems();
1033
        } else {
1034
            $items = $this->getItemsForIndex($listByUser, $frmTagList, $listAlphabetical);
1035
1036
            $foundComments = $this->getCommentsForIndex($frmTagList);
1037
        }
1038
1039
        // it gets and translate the subcategories
1040
        $categoryId = $httpRequest->query->getInt('categoryId');
1041
        $subCategoryIdsReq = isset($_REQUEST['subCategoryIds']) ? Security::remove_XSS($_REQUEST['subCategoryIds']) : '';
1042
        $subCategoryIds = $subCategoryIdsReq;
1043
        if ('all' !== $subCategoryIdsReq) {
1044
            $subCategoryIds = !empty($subCategoryIdsReq) ? explode(',', $subCategoryIdsReq) : [];
1045
        }
1046
        $subcategories = [];
1047
        if ($categoryId > 0) {
1048
            $subcategories = $this->getCategoriesForIndex($categoryId);
1049
            if (count($subcategories) > 0) {
1050
                foreach ($subcategories as &$subcategory) {
1051
                    $translated = $this->translateDisplayName($subcategory->getTitle());
1052
                    $subcategory->setTitle($translated);
1053
                }
1054
            }
1055
        }
1056
1057
        $context = [
1058
            'user' => $this->owner,
1059
            'listByUser' => $listByUser,
1060
            'course' => $this->course,
1061
            'session' => $this->session,
1062
            'portfolio' => $portfolio,
1063
            'categories' => $categories,
1064
            'uncategorized_items' => $items,
1065
            'frm_student_list' => $this->course ? $frmStudentList->returnForm() : '',
1066
            'frm_tag_list' => $this->course ? $frmTagList->returnForm() : '',
1067
            'category_id' => $categoryId,
1068
            'subcategories' => $subcategories,
1069
            'subcategory_ids' => $subCategoryIds,
1070
            'found_comments' => $foundComments,
1071
            '_p' => Template::getLegacyP(),
1072
            '_c' => Template::getLegacyC(),
1073
            'is_allowed_to_edit' => api_is_allowed_to_edit(false),
1074
        ];
1075
1076
        $js = '<script>
1077
            $(function() {
1078
                $(".category-filters").bind("click", function() {
1079
                    var categoryId = parseInt($(this).find("input[type=\'radio\']").val());
1080
                    $("input[name=\'categoryId\']").val(categoryId);
1081
                    $("input[name=\'subCategoryIds\']").val("all");
1082
                    $("#frm_tag_list_submit").trigger("click");
1083
                });
1084
                $(".subcategory-filters").bind("click", function() {
1085
                    var checkedVals = $(".subcategory-filters:checkbox:checked").map(function() {
1086
                        return this.value;
1087
                    }).get();
1088
                    $("input[name=\'subCategoryIds\']").val(checkedVals.join(","));
1089
                    $("#frm_tag_list_submit").trigger("click");
1090
                });
1091
            });
1092
        </script>';
1093
        $context['js_script'] = $js;
1094
1095
        $content = Container::getTwig()->render('@ChamiloCore/Portfolio/list.html.twig', $context);
1096
1097
        Display::addFlash(
1098
            Display::return_message(get_lang('Portfolio tool introduction'), 'info', false)
1099
        );
1100
1101
        $this->renderView($content, get_lang('Portfolio'), $actions);
1102
    }
1103
1104
    /**
1105
     * @throws \Doctrine\ORM\ORMException
1106
     * @throws \Doctrine\ORM\OptimisticLockException
1107
     * @throws \Doctrine\ORM\TransactionRequiredException
1108
     */
1109
    public function view(Portfolio $item, $urlUser)
1110
    {
1111
        global $interbreadcrumb;
1112
1113
        if (!$this->itemBelongToOwner($item)) {
1114
            if ($this->advancedSharingEnabled) {
1115
                $courseInfo = api_get_course_info_by_id($this->course->getId());
1116
                $sessionId = $this->session ? $this->session->getId() : 0;
1117
1118
                $itemPropertyVisiblity = api_get_item_visibility(
1119
                    $courseInfo,
1120
                    TOOL_PORTFOLIO,
1121
                    $item->getId(),
1122
                    $sessionId,
1123
                    $this->owner->getId(),
1124
                    'visible'
1125
                );
1126
1127
                if ($item->getVisibility() === Portfolio::VISIBILITY_PER_USER && 1 !== $itemPropertyVisiblity) {
1128
                    api_not_allowed(true);
1129
                }
1130
            } elseif ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN
1131
                || ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER && !api_is_allowed_to_edit())
1132
            ) {
1133
                api_not_allowed(true);
1134
            }
1135
        }
1136
1137
        Container::getEventDispatcher()->dispatch(
1138
            new PortfolioItemViewedEvent(['portfolio' => $item]),
1139
            Events::PORTFOLIO_ITEM_VIEWED
1140
        );
1141
1142
        $itemCourse = $item->getCourse();
1143
        $itemSession = $item->getSession();
1144
1145
        $form = $this->createCommentForm($item);
1146
1147
        $commentsRepo = $this->em->getRepository(PortfolioComment::class);
1148
1149
        $commentsQueryBuilder = $commentsRepo->createQueryBuilder('comment');
1150
        $commentsQueryBuilder->where('comment.item = :item');
1151
1152
        if ($this->advancedSharingEnabled) {
1153
            $commentsQueryBuilder
1154
                ->leftJoin(
1155
                    CItemProperty::class,
1156
                    'cip',
1157
                    Join::WITH,
1158
                    "cip.ref = comment.id
1159
                        AND cip.tool = :cip_tool
1160
                        AND cip.course = :course
1161
                        AND cip.lasteditType = 'visible'
1162
                        AND cip.toUser = :current_user"
1163
                )
1164
                ->andWhere(
1165
                    sprintf(
1166
                        'comment.visibility = %d
1167
                            OR (
1168
                                comment.visibility = %d AND cip IS NOT NULL OR comment.author = :current_user
1169
                            )',
1170
                        PortfolioComment::VISIBILITY_VISIBLE,
1171
                        PortfolioComment::VISIBILITY_PER_USER
1172
                    )
1173
                )
1174
                ->setParameter('cip_tool', TOOL_PORTFOLIO_COMMENT)
1175
                ->setParameter('current_user', $this->owner->getId())
1176
                ->setParameter('course', $item->getCourse())
1177
            ;
1178
        }
1179
1180
        if (true === api_get_configuration_value('portfolio_show_base_course_post_in_sessions')
1181
            && $this->session && !$item->getSession() && !$item->isDuplicatedInSession($this->session)
1182
        ) {
1183
            $comments = [];
1184
        } else {
1185
            $comments = $commentsQueryBuilder
1186
                ->orderBy('comment.root, comment.lft', 'ASC')
1187
                ->setParameter('item', $item)
1188
                ->getQuery()
1189
                ->getArrayResult()
1190
            ;
1191
        }
1192
1193
        $clockIcon = Display::returnFontAwesomeIcon('clock-o', '', true);
1194
1195
        $commentsHtml = $commentsRepo->buildTree(
1196
            $comments,
1197
            [
1198
                'decorate' => true,
1199
                'rootOpen' => '<div class="media-list">',
1200
                'rootClose' => '</div>',
1201
                'childOpen' => function ($node) use ($commentsRepo) {
1202
                    /** @var PortfolioComment $comment */
1203
                    $comment = $commentsRepo->find($node['id']);
1204
                    $author = $comment->getAuthor();
1205
1206
                    $userPicture = UserManager::getUserPicture(
1207
                        $comment->getAuthor()->getId(),
1208
                        USER_IMAGE_SIZE_SMALL,
1209
                        null,
1210
                        [
1211
                            'picture_uri' => $author->getPictureUri(),
1212
                            'email' => $author->getEmail(),
1213
                        ]
1214
                    );
1215
1216
                    return '<article class="media" id="comment-'.$node['id'].'">
1217
                        <div class="media-left"><img class="media-object thumbnail" src="'.$userPicture.'" alt="'
1218
                        .$author->getFullName().'"></div>
1219
                        <div class="media-body">';
1220
                },
1221
                'childClose' => '</div></article>',
1222
                'nodeDecorator' => function ($node) use ($commentsRepo, $clockIcon, $item) {
1223
                    $commentActions = [];
1224
                    /** @var PortfolioComment $comment */
1225
                    $comment = $commentsRepo->find($node['id']);
1226
1227
                    if ($this->commentBelongsToOwner($comment)) {
1228
                        $commentActions[] = Display::url(
1229
                            Display::return_icon(
1230
                                $comment->isTemplate() ? 'wizard.png' : 'wizard_na.png',
1231
                                $comment->isTemplate() ? get_lang('Remove as template') : get_lang('Add as a template')
1232
                            ),
1233
                            $this->baseUrl.http_build_query(['action' => 'template_comment', 'id' => $comment->getId()])
1234
                        );
1235
                    }
1236
1237
                    $commentActions[] = Display::url(
1238
                        Display::return_icon('discuss.png', get_lang('Reply to this comment')),
1239
                        '#',
1240
                        [
1241
                            'data-comment' => htmlspecialchars(
1242
                                json_encode(['id' => $comment->getId()])
1243
                            ),
1244
                            'role' => 'button',
1245
                            'class' => 'btn-reply-to',
1246
                        ]
1247
                    );
1248
                    $commentActions[] = Display::url(
1249
                        Display::return_icon('copy.png', get_lang('Copy to my portfolio')),
1250
                        $this->baseUrl.http_build_query(
1251
                            [
1252
                                'action' => 'copy',
1253
                                'copy' => 'comment',
1254
                                'id' => $comment->getId(),
1255
                            ]
1256
                        )
1257
                    );
1258
1259
                    $isAllowedToEdit = api_is_allowed_to_edit();
1260
1261
                    if ($isAllowedToEdit) {
1262
                        $commentActions[] = Display::url(
1263
                            Display::return_icon('copy.png', get_lang('Copy to student portfolio')),
1264
                            $this->baseUrl.http_build_query(
1265
                                [
1266
                                    'action' => 'teacher_copy',
1267
                                    'copy' => 'comment',
1268
                                    'id' => $comment->getId(),
1269
                                ]
1270
                            )
1271
                        );
1272
1273
                        if ($comment->isImportant()) {
1274
                            $commentActions[] = Display::url(
1275
                                Display::return_icon('drawing-pin.png', get_lang('Unmark comment as important')),
1276
                                $this->baseUrl.http_build_query(
1277
                                    [
1278
                                        'action' => 'mark_important',
1279
                                        'item' => $item->getId(),
1280
                                        'id' => $comment->getId(),
1281
                                    ]
1282
                                )
1283
                            );
1284
                        } else {
1285
                            $commentActions[] = Display::url(
1286
                                Display::return_icon('drawing-pin.png', get_lang('Mark comment as important')),
1287
                                $this->baseUrl.http_build_query(
1288
                                    [
1289
                                        'action' => 'mark_important',
1290
                                        'item' => $item->getId(),
1291
                                        'id' => $comment->getId(),
1292
                                    ]
1293
                                )
1294
                            );
1295
                        }
1296
1297
                        if ($this->course && '1' === api_get_course_setting('qualify_portfolio_comment')) {
1298
                            $commentActions[] = Display::url(
1299
                                Display::return_icon('quiz.png', get_lang('Grade this comment')),
1300
                                $this->baseUrl.http_build_query(
1301
                                    [
1302
                                        'action' => 'qualify',
1303
                                        'comment' => $comment->getId(),
1304
                                    ]
1305
                                )
1306
                            );
1307
                        }
1308
                    }
1309
1310
                    if ($this->commentBelongsToOwner($comment)) {
1311
                        if ($this->advancedSharingEnabled) {
1312
                            $commentActions[] = Display::url(
1313
                                Display::return_icon('visible.png', get_lang('Choose recipients')),
1314
                                $this->baseUrl.http_build_query(['action' => 'comment_visiblity_choose', 'id' => $comment->getId()])
1315
                            );
1316
                        }
1317
1318
                        $commentActions[] = Display::url(
1319
                            Display::return_icon('edit.png', get_lang('Edit')),
1320
                            $this->baseUrl.http_build_query(['action' => 'edit_comment', 'id' => $comment->getId()])
1321
                        );
1322
                        $commentActions[] = Display::url(
1323
                            Display::return_icon('delete.png', get_lang('Delete')),
1324
                            $this->baseUrl.http_build_query(['action' => 'delete_comment', 'id' => $comment->getId()])
1325
                        );
1326
                    }
1327
1328
                    $nodeHtml = '<div class="pull-right">'.implode(PHP_EOL, $commentActions).'</div>'.PHP_EOL
1329
                        .'<footer class="media-heading h4">'.PHP_EOL
1330
                        .'<p>'.$comment->getAuthor()->getFullName().'</p>'.PHP_EOL;
1331
1332
                    if ($comment->isImportant()
1333
                        && ($this->itemBelongToOwner($comment->getItem()) || $isAllowedToEdit)
1334
                    ) {
1335
                        $nodeHtml .= '<span class="pull-right label label-warning origin-style">'
1336
                            .get_lang('Portfolio item marked as important')
1337
                            .'</span>'.PHP_EOL;
1338
                    }
1339
1340
                    $nodeHtml .= '<small>'.$clockIcon.PHP_EOL
1341
                        .$this->getLabelForCommentDate($comment).'</small>'.PHP_EOL;
1342
1343
                    $nodeHtml .= '</footer>'.PHP_EOL
1344
                        .Security::remove_XSS($comment->getContent()).PHP_EOL;
1345
1346
                    $nodeHtml .= $this->generateAttachmentList($comment);
1347
1348
                    return $nodeHtml;
1349
                },
1350
            ]
1351
        );
1352
1353
        $template = new Template(null, false, false, false, false, false, false);
1354
        $template->assign('baseurl', $this->baseUrl);
1355
        $template->assign('item', $item);
1356
        $template->assign('item_content', $this->generateItemContent($item));
1357
        $template->assign('count_comments', count($comments));
1358
        $template->assign('comments', $commentsHtml);
1359
        $template->assign('form', $form);
1360
        $template->assign('attachment_list', $this->generateAttachmentList($item));
1361
1362
        if ($itemCourse) {
1363
            $propertyInfo = api_get_item_property_info(
1364
                $itemCourse->getId(),
1365
                TOOL_PORTFOLIO,
1366
                $item->getId(),
1367
                $itemSession ? $itemSession->getId() : 0
1368
            );
1369
1370
            if ($propertyInfo && empty($propertyInfo['to_user_id'])) {
1371
                $template->assign(
1372
                    'last_edit',
1373
                    [
1374
                        'date' => $propertyInfo['lastedit_date'],
1375
                        'user' => api_get_user_entity($propertyInfo['lastedit_user_id'])->getFullName(),
1376
                    ]
1377
                );
1378
            }
1379
        }
1380
1381
        $layout = $template->get_template('Portfolio/view.html.twig');
1382
        $content = $template->fetch($layout);
1383
1384
        $interbreadcrumb[] = ['name' => get_lang('Portfolio'), 'url' => $this->baseUrl];
1385
1386
        $editLink = Display::url(
1387
            Display::return_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_MEDIUM),
1388
            $this->baseUrl.http_build_query(['action' => 'edit_item', 'id' => $item->getId()])
1389
        );
1390
1391
        $urlUserString = "";
1392
        if (!empty($urlUser)) {
1393
            $urlUserString = "user=".$urlUser;
1394
        }
1395
1396
        $actions = [];
1397
        $actions[] = Display::url(
1398
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
1399
            $this->baseUrl.$urlUserString
1400
        );
1401
1402
        if ($this->itemBelongToOwner($item)) {
1403
            $actions[] = $editLink;
1404
1405
            $actions[] = Display::url(
1406
                Display::return_icon(
1407
                    $item->isTemplate() ? 'wizard.png' : 'wizard_na.png',
1408
                    $item->isTemplate() ? get_lang('Remove template') : get_lang('Add as a template'),
1409
                    [],
1410
                    ICON_SIZE_MEDIUM
1411
                ),
1412
                $this->baseUrl.http_build_query(['action' => 'template', 'id' => $item->getId()])
1413
            );
1414
1415
            if ($this->advancedSharingEnabled) {
1416
                $actions[] = Display::url(
1417
                    Display::return_icon('visible.png', get_lang('Choose recipients'), [], ICON_SIZE_MEDIUM),
1418
                    $this->baseUrl.http_build_query(['action' => 'item_visiblity_choose', 'id' => $item->getId()])
1419
                );
1420
            } else {
1421
                $visibilityUrl = $this->baseUrl.http_build_query(['action' => 'visibility', 'id' => $item->getId()]);
1422
1423
                if ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN) {
1424
                    $actions[] = Display::url(
1425
                        Display::return_icon('invisible.png', get_lang('Make visible'), [], ICON_SIZE_MEDIUM),
1426
                        $visibilityUrl
1427
                    );
1428
                } elseif ($item->getVisibility() === Portfolio::VISIBILITY_VISIBLE) {
1429
                    $actions[] = Display::url(
1430
                        Display::return_icon('visible.png', get_lang('Make visible for teachers'), [], ICON_SIZE_MEDIUM),
1431
                        $visibilityUrl
1432
                    );
1433
                } elseif ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER) {
1434
                    $actions[] = Display::url(
1435
                        Display::return_icon('eye-slash.png', get_lang('Make invisible'), [], ICON_SIZE_MEDIUM),
1436
                        $visibilityUrl
1437
                    );
1438
                }
1439
            }
1440
1441
            $actions[] = Display::url(
1442
                Display::return_icon('delete.png', get_lang('Delete'), [], ICON_SIZE_MEDIUM),
1443
                $this->baseUrl.http_build_query(['action' => 'delete_item', 'id' => $item->getId()])
1444
            );
1445
        } else {
1446
            $actions[] = Display::url(
1447
                Display::return_icon('copy.png', get_lang('Copy to my portfolio'), [], ICON_SIZE_MEDIUM),
1448
                $this->baseUrl.http_build_query(['action' => 'copy', 'copy' => 'item', 'id' => $item->getId()])
1449
            );
1450
        }
1451
1452
        if (api_is_allowed_to_edit()) {
1453
            $actions[] = Display::url(
1454
                Display::return_icon('copy.png', get_lang('Copy to student portfolio'), [], ICON_SIZE_MEDIUM),
1455
                $this->baseUrl.http_build_query(['action' => 'teacher_copy', 'copy' => 'item', 'id' => $item->getId()])
1456
            );
1457
            $actions[] = $editLink;
1458
1459
            $highlightedUrl = $this->baseUrl.http_build_query(['action' => 'highlighted', 'id' => $item->getId()]);
1460
1461
            if ($item->isHighlighted()) {
1462
                $actions[] = Display::url(
1463
                    Display::return_icon('award_red.png', get_lang('Unmark as highlighted'), [], ICON_SIZE_MEDIUM),
1464
                    $highlightedUrl
1465
                );
1466
            } else {
1467
                $actions[] = Display::url(
1468
                    Display::return_icon('award_red_na.png', get_lang('Mark as highlighted'), [], ICON_SIZE_MEDIUM),
1469
                    $highlightedUrl
1470
                );
1471
            }
1472
1473
            if ($itemCourse && '1' === api_get_course_setting('qualify_portfolio_item')) {
1474
                $actions[] = Display::url(
1475
                    Display::return_icon('quiz.png', get_lang('Grade this item'), [], ICON_SIZE_MEDIUM),
1476
                    $this->baseUrl.http_build_query(['action' => 'qualify', 'item' => $item->getId()])
1477
                );
1478
            }
1479
        }
1480
1481
        $this->renderView($content, $item->getTitle(true), $actions, false);
1482
    }
1483
1484
    /**
1485
     * @throws \Doctrine\ORM\ORMException
1486
     * @throws \Doctrine\ORM\OptimisticLockException
1487
     */
1488
    public function copyItem(Portfolio $originItem)
1489
    {
1490
        $this->blockIsNotAllowed();
1491
1492
        $currentTime = api_get_utc_datetime(null, false, true);
1493
1494
        $portfolio = new Portfolio();
1495
        $portfolio
1496
            ->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER)
1497
            ->setTitle(
1498
                sprintf(get_lang('Portfolio item by %s'), $originItem->getUser()->getFullName())
1499
            )
1500
            ->setContent('')
1501
            ->setUser($this->owner)
1502
            ->setOrigin($originItem->getId())
1503
            ->setOriginType(Portfolio::TYPE_ITEM)
1504
            ->setCourse($this->course)
1505
            ->setSession($this->session)
1506
            ->setCreationDate($currentTime)
1507
            ->setUpdateDate($currentTime);
1508
1509
        $this->em->persist($portfolio);
1510
        $this->em->flush();
1511
1512
        Display::addFlash(
1513
            Display::return_message(get_lang('Portfolio item added'), 'success')
1514
        );
1515
1516
        header("Location: $this->baseUrl".http_build_query(['action' => 'edit_item', 'id' => $portfolio->getId()]));
1517
        exit;
1518
    }
1519
1520
    /**
1521
     * @throws \Doctrine\ORM\ORMException
1522
     * @throws \Doctrine\ORM\OptimisticLockException
1523
     */
1524
    public function copyComment(PortfolioComment $originComment)
1525
    {
1526
        $currentTime = api_get_utc_datetime(null, false, true);
1527
1528
        $portfolio = new Portfolio();
1529
        $portfolio
1530
            ->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER)
1531
            ->setTitle(
1532
                sprintf(get_lang('Comment by %s'), $originComment->getAuthor()->getFullName())
1533
            )
1534
            ->setContent('')
1535
            ->setUser($this->owner)
1536
            ->setOrigin($originComment->getId())
1537
            ->setOriginType(Portfolio::TYPE_COMMENT)
1538
            ->setCourse($this->course)
1539
            ->setSession($this->session)
1540
            ->setCreationDate($currentTime)
1541
            ->setUpdateDate($currentTime);
1542
1543
        $this->em->persist($portfolio);
1544
        $this->em->flush();
1545
1546
        Display::addFlash(
1547
            Display::return_message(get_lang('Portfolio item added'), 'success')
1548
        );
1549
1550
        header("Location: $this->baseUrl".http_build_query(['action' => 'edit_item', 'id' => $portfolio->getId()]));
1551
        exit;
1552
    }
1553
1554
    /**
1555
     * @throws \Doctrine\ORM\ORMException
1556
     * @throws \Doctrine\ORM\OptimisticLockException
1557
     * @throws \Exception
1558
     */
1559
    public function teacherCopyItem(Portfolio $originItem)
1560
    {
1561
        api_protect_teacher_script();
1562
1563
        $actionParams = http_build_query(['action' => 'teacher_copy', 'copy' => 'item', 'id' => $originItem->getId()]);
1564
1565
        $form = new FormValidator('teacher_copy_portfolio', 'post', $this->baseUrl.$actionParams);
1566
1567
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
1568
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
1569
        } else {
1570
            $form->addText('title', get_lang('Title'));
1571
            $form->applyFilter('title', 'trim');
1572
        }
1573
1574
        $form->addLabel(
1575
            sprintf(get_lang('"Portfolio item by %s"'), $originItem->getUser()->getFullName()),
1576
            Display::panel(
1577
                Security::remove_XSS($originItem->getContent())
1578
            )
1579
        );
1580
        $form->addHtmlEditor('content', get_lang('Content'), true, false, ['ToolbarSet' => 'NotebookStudent']);
1581
1582
        $urlParams = http_build_query(
1583
            [
1584
                'a' => 'search_user_by_course',
1585
                'course_id' => $this->course->getId(),
1586
                'session_id' => $this->session ? $this->session->getId() : 0,
1587
            ]
1588
        );
1589
        $form->addSelectAjax(
1590
            'students',
1591
            get_lang('Learners'),
1592
            [],
1593
            [
1594
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1595
                'multiple' => true,
1596
            ]
1597
        );
1598
        $form->addRule('students', get_lang('Required field'), 'required');
1599
        $form->addButtonCreate(get_lang('Save'));
1600
1601
        $toolName = get_lang('Copy to student portfolio');
1602
        $content = $form->returnForm();
1603
1604
        if ($form->validate()) {
1605
            $values = $form->exportValues();
1606
1607
            $currentTime = api_get_utc_datetime(null, false, true);
1608
1609
            foreach ($values['students'] as $studentId) {
1610
                $owner = api_get_user_entity($studentId);
1611
1612
                $portfolio = new Portfolio();
1613
                $portfolio
1614
                    ->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER)
1615
                    ->setTitle($values['title'])
1616
                    ->setContent($values['content'])
1617
                    ->setUser($owner)
1618
                    ->setOrigin($originItem->getId())
1619
                    ->setOriginType(Portfolio::TYPE_ITEM)
1620
                    ->setCourse($this->course)
1621
                    ->setSession($this->session)
1622
                    ->setCreationDate($currentTime)
1623
                    ->setUpdateDate($currentTime);
1624
1625
                $this->em->persist($portfolio);
1626
            }
1627
1628
            $this->em->flush();
1629
1630
            Display::addFlash(
1631
                Display::return_message(get_lang('Item added to students own portfolio'), 'success')
1632
            );
1633
1634
            header("Location: $this->baseUrl");
1635
            exit;
1636
        }
1637
1638
        $this->renderView($content, $toolName);
1639
    }
1640
1641
    /**
1642
     * @throws \Doctrine\ORM\ORMException
1643
     * @throws \Doctrine\ORM\OptimisticLockException
1644
     * @throws \Exception
1645
     */
1646
    public function teacherCopyComment(PortfolioComment $originComment)
1647
    {
1648
        $actionParams = http_build_query(
1649
            [
1650
                'action' => 'teacher_copy',
1651
                'copy' => 'comment',
1652
                'id' => $originComment->getId(),
1653
            ]
1654
        );
1655
1656
        $form = new FormValidator('teacher_copy_portfolio', 'post', $this->baseUrl.$actionParams);
1657
1658
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
1659
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
1660
        } else {
1661
            $form->addText('title', get_lang('Title'));
1662
            $form->applyFilter('title', 'trim');
1663
        }
1664
1665
        $form->addLabel(
1666
            sprintf(get_lang('Comment from %s'), $originComment->getAuthor()->getFullName()),
1667
            Display::panel(
1668
                Security::remove_XSS($originComment->getContent())
1669
            )
1670
        );
1671
        $form->addHtmlEditor('content', get_lang('Content'), true, false, ['ToolbarSet' => 'NotebookStudent']);
1672
1673
        $urlParams = http_build_query(
1674
            [
1675
                'a' => 'search_user_by_course',
1676
                'course_id' => $this->course->getId(),
1677
                'session_id' => $this->session ? $this->session->getId() : 0,
1678
            ]
1679
        );
1680
        $form->addSelectAjax(
1681
            'students',
1682
            get_lang('Learners'),
1683
            [],
1684
            [
1685
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1686
                'multiple' => true,
1687
            ]
1688
        );
1689
        $form->addRule('students', get_lang('Required field'), 'required');
1690
        $form->addButtonCreate(get_lang('Save'));
1691
1692
        $toolName = get_lang('Copy to student portfolio');
1693
        $content = $form->returnForm();
1694
1695
        if ($form->validate()) {
1696
            $values = $form->exportValues();
1697
1698
            $currentTime = api_get_utc_datetime(null, false, true);
1699
1700
            foreach ($values['students'] as $studentId) {
1701
                $owner = api_get_user_entity($studentId);
1702
1703
                $portfolio = new Portfolio();
1704
                $portfolio
1705
                    ->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER)
1706
                    ->setTitle($values['title'])
1707
                    ->setContent($values['content'])
1708
                    ->setUser($owner)
1709
                    ->setOrigin($originComment->getId())
1710
                    ->setOriginType(Portfolio::TYPE_COMMENT)
1711
                    ->setCourse($this->course)
1712
                    ->setSession($this->session)
1713
                    ->setCreationDate($currentTime)
1714
                    ->setUpdateDate($currentTime);
1715
1716
                $this->em->persist($portfolio);
1717
            }
1718
1719
            $this->em->flush();
1720
1721
            Display::addFlash(
1722
                Display::return_message(get_lang('Item added to students own portfolio'), 'success')
1723
            );
1724
1725
            header("Location: $this->baseUrl");
1726
            exit;
1727
        }
1728
1729
        $this->renderView($content, $toolName);
1730
    }
1731
1732
    /**
1733
     * @throws \Doctrine\ORM\ORMException
1734
     * @throws \Doctrine\ORM\OptimisticLockException
1735
     */
1736
    public function markImportantCommentInItem(Portfolio $item, PortfolioComment $comment)
1737
    {
1738
        if ($comment->getItem()->getId() !== $item->getId()) {
1739
            api_not_allowed(true);
1740
        }
1741
1742
        $comment->setIsImportant(
1743
            !$comment->isImportant()
1744
        );
1745
1746
        $this->em->persist($comment);
1747
        $this->em->flush();
1748
1749
        Display::addFlash(
1750
            Display::return_message(get_lang('Portfolio item marked as important'), 'success')
1751
        );
1752
1753
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
1754
        exit;
1755
    }
1756
1757
    /**
1758
     * @throws \Exception
1759
     */
1760
    public function details(HttpRequest $httpRequest)
1761
    {
1762
        $this->blockIsNotAllowed();
1763
1764
        $currentUserId = api_get_user_id();
1765
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
1766
1767
        $actions = [];
1768
        $actions[] = Display::url(
1769
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
1770
            $this->baseUrl
1771
        );
1772
        $actions[] = Display::url(
1773
            Display::return_icon('pdf.png', get_lang('Export my portfolio data in a PDF file'), [], ICON_SIZE_MEDIUM),
1774
            $this->baseUrl.http_build_query(['action' => 'export_pdf'])
1775
        );
1776
        $actions[] = Display::url(
1777
            Display::return_icon('save_pack.png', get_lang('Export my portfolio data in a ZIP file'), [], ICON_SIZE_MEDIUM),
1778
            $this->baseUrl.http_build_query(['action' => 'export_zip'])
1779
        );
1780
1781
        $frmStudent = null;
1782
1783
        if ($isAllowedToFilterStudent) {
1784
            if ($httpRequest->query->has('user')) {
1785
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
1786
1787
                if (empty($this->owner)) {
1788
                    api_not_allowed(true);
1789
                }
1790
1791
                $actions[1] = Display::url(
1792
                    Display::return_icon('pdf.png', get_lang('Export my portfolio data in a PDF file'), [], ICON_SIZE_MEDIUM),
1793
                    $this->baseUrl.http_build_query(['action' => 'export_pdf', 'user' => $this->owner->getId()])
1794
                );
1795
                $actions[2] = Display::url(
1796
                    Display::return_icon('save_pack.png', get_lang('Export my portfolio data in a ZIP file'), [], ICON_SIZE_MEDIUM),
1797
                    $this->baseUrl.http_build_query(['action' => 'export_zip', 'user' => $this->owner->getId()])
1798
                );
1799
            }
1800
1801
            $frmStudent = new FormValidator('frm_student_list', 'get');
1802
1803
            $urlParams = http_build_query(
1804
                [
1805
                    'a' => 'search_user_by_course',
1806
                    'course_id' => $this->course->getId(),
1807
                    'session_id' => $this->session ? $this->session->getId() : 0,
1808
                ]
1809
            );
1810
1811
            $frmStudent
1812
                ->addSelectAjax(
1813
                    'user',
1814
                    get_lang('Select a learner portfolio'),
1815
                    [],
1816
                    [
1817
                        'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1818
                        'placeholder' => get_lang('Search users'),
1819
                        'formatResult' => SelectAjax::templateResultForUsersInCourse(),
1820
                        'formatSelection' => SelectAjax::templateSelectionForUsersInCourse(),
1821
                    ]
1822
                )
1823
                ->addOption(
1824
                    $this->owner->getFullName(),
1825
                    $this->owner->getId(),
1826
                    [
1827
                        'data-avatarurl' => UserManager::getUserPicture($this->owner->getId()),
1828
                        'data-username' => $this->owner->getUsername(),
1829
                    ]
1830
                )
1831
            ;
1832
            $frmStudent->setDefaults(['user' => $this->owner->getId()]);
1833
            $frmStudent->addHidden('action', 'details');
1834
            $frmStudent->addHidden('cidReq', $this->course->getCode());
1835
            $frmStudent->addHidden('id_session', $this->session ? $this->session->getId() : 0);
1836
            $frmStudent->addButtonFilter(get_lang('Filter'));
1837
        }
1838
1839
        $itemsRepo = Container::getPortfolioRepository();
1840
        $commentsRepo = $this->em->getRepository(PortfolioComment::class);
1841
1842
        $getItemsTotalNumber = function () use ($itemsRepo, $isAllowedToFilterStudent, $currentUserId) {
1843
            $qb = $itemsRepo->createQueryBuilder('i');
1844
            $qb
1845
                ->select('COUNT(i)')
1846
                ->where('i.user = :user')
1847
                ->setParameter('user', $this->owner);
1848
1849
            if ($this->course) {
1850
                $qb
1851
                    ->andWhere('i.course = :course')
1852
                    ->setParameter('course', $this->course);
1853
1854
                if ($this->session) {
1855
                    $qb
1856
                        ->andWhere('i.session = :session')
1857
                        ->setParameter('session', $this->session);
1858
                } else {
1859
                    $qb->andWhere('i.session IS NULL');
1860
                }
1861
            }
1862
1863
            if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
1864
                $visibilityCriteria = [
1865
                    Portfolio::VISIBILITY_VISIBLE,
1866
                    Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER,
1867
                ];
1868
1869
                $qb->andWhere(
1870
                    $qb->expr()->in('i.visibility', $visibilityCriteria)
1871
                );
1872
            }
1873
1874
            return $qb->getQuery()->getSingleScalarResult();
1875
        };
1876
        $getItemsData = function ($from, $limit, $columnNo, $orderDirection) use ($itemsRepo, $isAllowedToFilterStudent, $currentUserId) {
1877
            $qb = $itemsRepo->createQueryBuilder('item')
1878
                ->where('item.user = :user')
1879
                ->leftJoin('item.category', 'category')
1880
                ->leftJoin('item.course', 'course')
1881
                ->leftJoin('item.session', 'session')
1882
                ->setParameter('user', $this->owner);
1883
1884
            if ($this->course) {
1885
                $qb
1886
                    ->andWhere('item.course = :course_id')
1887
                    ->setParameter('course_id', $this->course);
1888
1889
                if ($this->session) {
1890
                    $qb
1891
                        ->andWhere('item.session = :session')
1892
                        ->setParameter('session', $this->session);
1893
                } else {
1894
                    $qb->andWhere('item.session IS NULL');
1895
                }
1896
            }
1897
1898
            if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
1899
                $visibilityCriteria = [
1900
                    Portfolio::VISIBILITY_VISIBLE,
1901
                    Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER,
1902
                ];
1903
1904
                $qb->andWhere(
1905
                    $qb->expr()->in('item.visibility', $visibilityCriteria)
1906
                );
1907
            }
1908
1909
            if (0 == $columnNo) {
1910
                $qb->orderBy('item.title', $orderDirection);
1911
            } elseif (1 == $columnNo) {
1912
                $qb->orderBy('item.creationDate', $orderDirection);
1913
            } elseif (2 == $columnNo) {
1914
                $qb->orderBy('item.updateDate', $orderDirection);
1915
            } elseif (3 == $columnNo) {
1916
                $qb->orderBy('category.title', $orderDirection);
1917
            } elseif (5 == $columnNo) {
1918
                $qb->orderBy('item.score', $orderDirection);
1919
            } elseif (6 == $columnNo) {
1920
                $qb->orderBy('course.title', $orderDirection);
1921
            } elseif (7 == $columnNo) {
1922
                $qb->orderBy('session.name', $orderDirection);
1923
            }
1924
1925
            $qb->setFirstResult($from)->setMaxResults($limit);
1926
1927
            return array_map(
1928
                function (Portfolio $item) {
1929
                    $category = $item->getCategory();
1930
1931
                    $row = [];
1932
                    $row[] = $item;
1933
                    $row[] = $item->getCreationDate();
1934
                    $row[] = $item->getUpdateDate();
1935
                    $row[] = $category ? $item->getCategory()->getTitle() : null;
1936
                    $row[] = $item->getComments()->count();
1937
                    $row[] = $item->getScore();
1938
1939
                    if (!$this->course) {
1940
                        $row[] = $item->getCourse();
1941
                        $row[] = $item->getSession();
1942
                    }
1943
1944
                    return $row;
1945
                },
1946
                $qb->getQuery()->getResult()
1947
            );
1948
        };
1949
1950
        $portfolioItemColumnFilter = function (Portfolio $item) {
1951
            return Display::url(
1952
                $item->getTitle(true),
1953
                $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
1954
            );
1955
        };
1956
        $convertFormatDateColumnFilter = function (DateTime $date) {
1957
            return api_convert_and_format_date($date);
1958
        };
1959
1960
        $tblItems = new SortableTable('tbl_items', $getItemsTotalNumber, $getItemsData, 1, 20, 'DESC');
1961
        $tblItems->set_additional_parameters(['action' => 'details', 'user' => $this->owner->getId()]);
1962
        $tblItems->set_header(0, get_lang('Title'));
1963
        $tblItems->set_column_filter(0, $portfolioItemColumnFilter);
1964
        $tblItems->set_header(1, get_lang('Creation date'), true, [], ['class' => 'text-center']);
1965
        $tblItems->set_column_filter(1, $convertFormatDateColumnFilter);
1966
        $tblItems->set_header(2, get_lang('Last update'), true, [], ['class' => 'text-center']);
1967
        $tblItems->set_column_filter(2, $convertFormatDateColumnFilter);
1968
        $tblItems->set_header(3, get_lang('Category'));
1969
        $tblItems->set_header(4, get_lang('Comments'), false, [], ['class' => 'text-right']);
1970
        $tblItems->set_header(5, get_lang('Score'), true, [], ['class' => 'text-right']);
1971
1972
        if (!$this->course) {
1973
            $tblItems->set_header(6, get_lang('Course'));
1974
            $tblItems->set_header(7, get_lang('Session'));
1975
        }
1976
1977
        $getCommentsTotalNumber = function () use ($commentsRepo) {
1978
            $qb = $commentsRepo->createQueryBuilder('c');
1979
            $qb
1980
                ->select('COUNT(c)')
1981
                ->where('c.author = :author')
1982
                ->setParameter('author', $this->owner);
1983
1984
            if ($this->course) {
1985
                $qb
1986
                    ->innerJoin('c.item', 'i')
1987
                    ->andWhere('i.course = :course')
1988
                    ->setParameter('course', $this->course);
1989
1990
                if ($this->session) {
1991
                    $qb
1992
                        ->andWhere('i.session = :session')
1993
                        ->setParameter('session', $this->session);
1994
                } else {
1995
                    $qb->andWhere('i.session IS NULL');
1996
                }
1997
            }
1998
1999
            return $qb->getQuery()->getSingleScalarResult();
2000
        };
2001
        $getCommentsData = function ($from, $limit, $columnNo, $orderDirection) use ($commentsRepo) {
2002
            $qb = $commentsRepo->createQueryBuilder('comment');
2003
            $qb
2004
                ->where('comment.author = :user')
2005
                ->innerJoin('comment.item', 'item')
2006
                ->setParameter('user', $this->owner);
2007
2008
            if ($this->course) {
2009
                $qb
2010
                    ->innerJoin('comment.item', 'i')
2011
                    ->andWhere('item.course = :course')
2012
                    ->setParameter('course', $this->course);
2013
2014
                if ($this->session) {
2015
                    $qb
2016
                        ->andWhere('item.session = :session')
2017
                        ->setParameter('session', $this->session);
2018
                } else {
2019
                    $qb->andWhere('item.session IS NULL');
2020
                }
2021
            }
2022
2023
            if (0 == $columnNo) {
2024
                $qb->orderBy('comment.content', $orderDirection);
2025
            } elseif (1 == $columnNo) {
2026
                $qb->orderBy('comment.date', $orderDirection);
2027
            } elseif (2 == $columnNo) {
2028
                $qb->orderBy('item.title', $orderDirection);
2029
            } elseif (3 == $columnNo) {
2030
                $qb->orderBy('comment.score', $orderDirection);
2031
            }
2032
2033
            $qb->setFirstResult($from)->setMaxResults($limit);
2034
2035
            return array_map(
2036
                function (PortfolioComment $comment) {
2037
                    return [
2038
                        $comment,
2039
                        $comment->getDate(),
2040
                        $comment->getItem(),
2041
                        $comment->getScore(),
2042
                    ];
2043
                },
2044
                $qb->getQuery()->getResult()
2045
            );
2046
        };
2047
2048
        $tblComments = new SortableTable('tbl_comments', $getCommentsTotalNumber, $getCommentsData, 1, 20, 'DESC');
2049
        $tblComments->set_additional_parameters(['action' => 'details', 'user' => $this->owner->getId()]);
2050
        $tblComments->set_header(0, get_lang('Resume'));
2051
        $tblComments->set_column_filter(
2052
            0,
2053
            function (PortfolioComment $comment) {
2054
                return Display::url(
2055
                    $comment->getExcerpt(),
2056
                    $this->baseUrl.http_build_query(['action' => 'view', 'id' => $comment->getItem()->getId()])
2057
                    .'#comment-'.$comment->getId()
2058
                );
2059
            }
2060
        );
2061
        $tblComments->set_header(1, get_lang('Date'), true, [], ['class' => 'text-center']);
2062
        $tblComments->set_column_filter(1, $convertFormatDateColumnFilter);
2063
        $tblComments->set_header(2, get_lang('Item title'));
2064
        $tblComments->set_column_filter(2, $portfolioItemColumnFilter);
2065
        $tblComments->set_header(3, get_lang('Score'), true, [], ['class' => 'text-right']);
2066
2067
        $content = '';
2068
2069
        if ($frmStudent) {
2070
            $content .= $frmStudent->returnForm();
2071
        }
2072
2073
        $totalNumberOfItems = $tblItems->get_total_number_of_items();
2074
        $totalNumberOfComments = $tblComments->get_total_number_of_items();
2075
        $requiredNumberOfItems = (int) api_get_course_setting('portfolio_number_items');
2076
        $requiredNumberOfComments = (int) api_get_course_setting('portfolio_number_comments');
2077
2078
        $itemsSubtitle = '';
2079
2080
        if ($requiredNumberOfItems > 0) {
2081
            $itemsSubtitle = sprintf(
2082
                get_lang('%d added / %d required'),
2083
                $totalNumberOfItems,
2084
                $requiredNumberOfItems
2085
            );
2086
        }
2087
2088
        $content .= Display::page_subheader2(
2089
            get_lang('Portfolio items'),
2090
            $itemsSubtitle
2091
        ).PHP_EOL;
2092
2093
        if ($totalNumberOfItems > 0) {
2094
            $content .= $tblItems->return_table().PHP_EOL;
2095
        } else {
2096
            $content .= Display::return_message(get_lang('No items in your portfolio'), 'warning');
2097
        }
2098
2099
        $commentsSubtitle = '';
2100
2101
        if ($requiredNumberOfComments > 0) {
2102
            $commentsSubtitle = sprintf(
2103
                get_lang('%d added / %d required'),
2104
                $totalNumberOfComments,
2105
                $requiredNumberOfComments
2106
            );
2107
        }
2108
2109
        $content .= Display::page_subheader2(
2110
            get_lang('Comments made'),
2111
            $commentsSubtitle
2112
        ).PHP_EOL;
2113
2114
        if ($totalNumberOfComments > 0) {
2115
            $content .= $tblComments->return_table().PHP_EOL;
2116
        } else {
2117
            $content .= Display::return_message(get_lang('You have not commented'), 'warning');
2118
        }
2119
2120
        $this->renderView($content, get_lang('Portfolio details'), $actions);
2121
    }
2122
2123
    /**
2124
     * @throws MpdfException
2125
     */
2126
    public function exportPdf(HttpRequest $httpRequest)
2127
    {
2128
        $currentUserId = api_get_user_id();
2129
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
2130
2131
        if ($isAllowedToFilterStudent) {
2132
            if ($httpRequest->query->has('user')) {
2133
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
2134
2135
                if (empty($this->owner)) {
2136
                    api_not_allowed(true);
2137
                }
2138
            }
2139
        }
2140
2141
        $pdfContent = Display::page_header($this->owner->getFullName());
2142
2143
        if ($this->course) {
2144
            $pdfContent .= '<p>'.get_lang('Course').': ';
2145
2146
            if ($this->session) {
2147
                $pdfContent .= $this->session->getTitle().' ('.$this->course->getTitle().')';
2148
            } else {
2149
                $pdfContent .= $this->course->getTitle();
2150
            }
2151
2152
            $pdfContent .= '</p>';
2153
        }
2154
2155
        $visibility = [];
2156
2157
        if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
2158
            $visibility[] = Portfolio::VISIBILITY_VISIBLE;
2159
            $visibility[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
2160
        }
2161
2162
        $items = Container::getPortfolioRepository()
2163
            ->findItemsByUser(
2164
                $this->owner,
2165
                $this->course,
2166
                $this->session,
2167
                null,
2168
                $visibility
2169
            );
2170
        $comments = $this->em
2171
            ->getRepository(PortfolioComment::class)
2172
            ->findCommentsByUser($this->owner, $this->course, $this->session);
2173
2174
        $itemsHtml = $this->getItemsInHtmlFormatted($items);
2175
        $commentsHtml = $this->getCommentsInHtmlFormatted($comments);
2176
2177
        $totalNumberOfItems = count($itemsHtml);
2178
        $totalNumberOfComments = count($commentsHtml);
2179
        $requiredNumberOfItems = (int) api_get_course_setting('portfolio_number_items');
2180
        $requiredNumberOfComments = (int) api_get_course_setting('portfolio_number_comments');
2181
2182
        $itemsSubtitle = '';
2183
        $commentsSubtitle = '';
2184
2185
        if ($requiredNumberOfItems > 0) {
2186
            $itemsSubtitle = sprintf(
2187
                get_lang('%d added / %d required'),
2188
                $totalNumberOfItems,
2189
                $requiredNumberOfItems
2190
            );
2191
        }
2192
2193
        if ($requiredNumberOfComments > 0) {
2194
            $commentsSubtitle = sprintf(
2195
                get_lang('%d added / %d required'),
2196
                $totalNumberOfComments,
2197
                $requiredNumberOfComments
2198
            );
2199
        }
2200
2201
        $pdfContent .= Display::page_subheader2(
2202
            get_lang('Portfolio items'),
2203
            $itemsSubtitle
2204
        );
2205
2206
        if ($totalNumberOfItems > 0) {
2207
            $pdfContent .= implode(PHP_EOL, $itemsHtml);
2208
        } else {
2209
            $pdfContent .= Display::return_message(get_lang('No items in your portfolio'), 'warning');
2210
        }
2211
2212
        $pdfContent .= Display::page_subheader2(
2213
            get_lang('Comments made'),
2214
            $commentsSubtitle
2215
        );
2216
2217
        if ($totalNumberOfComments > 0) {
2218
            $pdfContent .= implode(PHP_EOL, $commentsHtml);
2219
        } else {
2220
            $pdfContent .= Display::return_message(get_lang('You have not commented'), 'warning');
2221
        }
2222
2223
        $pdfName = $this->owner->getFullName()
2224
            .($this->course ? '_'.$this->course->getCode() : '')
2225
            .'_'.get_lang('Portfolio');
2226
2227
        Container::getEventDispatcher()->dispatch(
2228
            new PortfolioItemDownloadedEvent(['owner' => $this->owner]),
2229
            Events::PORTFOLIO_DOWNLOADED,
2230
        );
2231
2232
        $pdf = new PDF();
2233
        $pdf->content_to_pdf(
2234
            $pdfContent,
2235
            null,
2236
            $pdfName,
2237
            $this->course ? $this->course->getCode() : null,
2238
            'D',
2239
            false,
2240
            null,
2241
            false,
2242
            true
2243
        );
2244
    }
2245
2246
    public function exportZip(HttpRequest $httpRequest)
2247
    {
2248
        $currentUserId = api_get_user_id();
2249
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
2250
2251
        if ($isAllowedToFilterStudent) {
2252
            if ($httpRequest->query->has('user')) {
2253
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
2254
2255
                if (empty($this->owner)) {
2256
                    api_not_allowed(true);
2257
                }
2258
            }
2259
        }
2260
2261
        $commentsRepo = $this->em->getRepository(PortfolioComment::class);
2262
        $attachmentsRepo = $this->em->getRepository(PortfolioAttachment::class);
2263
2264
        $visibility = [];
2265
2266
        if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
2267
            $visibility[] = Portfolio::VISIBILITY_VISIBLE;
2268
            $visibility[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
2269
        }
2270
2271
        $items = Container::getPortfolioRepository()->findItemsByUser(
2272
            $this->owner,
2273
            $this->course,
2274
            $this->session,
2275
            null,
2276
            $visibility
2277
        );
2278
        $comments = $commentsRepo->findCommentsByUser($this->owner, $this->course, $this->session);
2279
2280
        $itemsHtml = $this->getItemsInHtmlFormatted($items);
2281
        $commentsHtml = $this->getCommentsInHtmlFormatted($comments);
2282
2283
        $sysArchivePath = api_get_path(SYS_ARCHIVE_PATH);
2284
        $tempPortfolioDirectory = $sysArchivePath."portfolio/{$this->owner->getId()}";
2285
2286
        $userDirectory = UserManager::getUserPathById($this->owner->getId(), 'system');
2287
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2288
2289
        $tblItemsHeaders = [];
2290
        $tblItemsHeaders[] = get_lang('Title');
2291
        $tblItemsHeaders[] = get_lang('Creation date');
2292
        $tblItemsHeaders[] = get_lang('Last update');
2293
        $tblItemsHeaders[] = get_lang('Category');
2294
        $tblItemsHeaders[] = get_lang('Category');
2295
        $tblItemsHeaders[] = get_lang('Score');
2296
        $tblItemsHeaders[] = get_lang('Course');
2297
        $tblItemsHeaders[] = get_lang('Session');
2298
        $tblItemsData = [];
2299
2300
        $tblCommentsHeaders = [];
2301
        $tblCommentsHeaders[] = get_lang('Resume');
2302
        $tblCommentsHeaders[] = get_lang('Date');
2303
        $tblCommentsHeaders[] = get_lang('Item title');
2304
        $tblCommentsHeaders[] = get_lang('Score');
2305
        $tblCommentsData = [];
2306
2307
        $filenames = [];
2308
2309
        $fs = new Filesystem();
2310
2311
        /**
2312
         * @var int       $i
2313
         * @var Portfolio $item
2314
         */
2315
        foreach ($items as $i => $item) {
2316
            $itemCategory = $item->getCategory();
2317
            $itemCourse = $item->getCourse();
2318
            $itemSession = $item->getSession();
2319
2320
            $itemDirectory = $item->getCreationDate()->format('Y-m-d-H-i-s');
2321
2322
            $itemFilename = sprintf('%s/items/%s/item.html', $tempPortfolioDirectory, $itemDirectory);
2323
            $imagePaths = [];
2324
            $itemFileContent = $this->fixMediaSourcesToHtml($itemsHtml[$i], $imagePaths);
2325
2326
            $fs->dumpFile($itemFilename, $itemFileContent);
2327
2328
            $filenames[] = $itemFilename;
2329
2330
            foreach ($imagePaths as $imagePath) {
2331
                $inlineFile = dirname($itemFilename).'/'.basename($imagePath);
2332
2333
                try {
2334
                    $filenames[] = $inlineFile;
2335
                    $fs->copy($imagePath, $inlineFile);
2336
                } catch (FileNotFoundException $notFoundException) {
2337
                    continue;
2338
                }
2339
            }
2340
2341
            $attachments = $attachmentsRepo->findFromItem($item);
2342
2343
            /** @var PortfolioAttachment $attachment */
2344
            foreach ($attachments as $attachment) {
2345
                $attachmentFilename = sprintf(
2346
                    '%s/items/%s/attachments/%s',
2347
                    $tempPortfolioDirectory,
2348
                    $itemDirectory,
2349
                    $attachment->getFilename()
2350
                );
2351
2352
                try {
2353
                    $fs->copy(
2354
                        $attachmentsDirectory.$attachment->getPath(),
2355
                        $attachmentFilename
2356
                    );
2357
                    $filenames[] = $attachmentFilename;
2358
                } catch (FileNotFoundException $notFoundException) {
2359
                    continue;
2360
                }
2361
            }
2362
2363
            $tblItemsData[] = [
2364
                Display::url(
2365
                    Security::remove_XSS($item->getTitle()),
2366
                    sprintf('items/%s/item.html', $itemDirectory)
2367
                ),
2368
                api_convert_and_format_date($item->getCreationDate()),
2369
                api_convert_and_format_date($item->getUpdateDate()),
2370
                $itemCategory ? $itemCategory->getTitle() : null,
2371
                $item->getComments()->count(),
2372
                $item->getScore(),
2373
                $itemCourse->getTitle(),
2374
                $itemSession ? $itemSession->getTitle() : null,
2375
            ];
2376
        }
2377
2378
        /**
2379
         * @var int              $i
2380
         * @var PortfolioComment $comment
2381
         */
2382
        foreach ($comments as $i => $comment) {
2383
            $commentDirectory = $comment->getDate()->format('Y-m-d-H-i-s');
2384
2385
            $imagePaths = [];
2386
            $commentFileContent = $this->fixMediaSourcesToHtml($commentsHtml[$i], $imagePaths);
2387
            $commentFilename = sprintf('%s/comments/%s/comment.html', $tempPortfolioDirectory, $commentDirectory);
2388
2389
            $fs->dumpFile($commentFilename, $commentFileContent);
2390
2391
            $filenames[] = $commentFilename;
2392
2393
            foreach ($imagePaths as $imagePath) {
2394
                $inlineFile = dirname($commentFilename).'/'.basename($imagePath);
2395
2396
                try {
2397
                    $filenames[] = $inlineFile;
2398
                    $fs->copy($imagePath, $inlineFile);
2399
                } catch (FileNotFoundException $notFoundException) {
2400
                    continue;
2401
                }
2402
            }
2403
2404
            $attachments = $attachmentsRepo->findFromComment($comment);
2405
2406
            /** @var PortfolioAttachment $attachment */
2407
            foreach ($attachments as $attachment) {
2408
                $attachmentFilename = sprintf(
2409
                    '%s/comments/%s/attachments/%s',
2410
                    $tempPortfolioDirectory,
2411
                    $commentDirectory,
2412
                    $attachment->getFilename()
2413
                );
2414
2415
                try {
2416
                    $fs->copy(
2417
                        $attachmentsDirectory.$attachment->getPath(),
2418
                        $attachmentFilename
2419
                    );
2420
                    $filenames[] = $attachmentFilename;
2421
                } catch (FileNotFoundException $notFoundException) {
2422
                    continue;
2423
                }
2424
            }
2425
2426
            $tblCommentsData[] = [
2427
                Display::url(
2428
                    $comment->getExcerpt(),
2429
                    sprintf('comments/%s/comment.html', $commentDirectory)
2430
                ),
2431
                api_convert_and_format_date($comment->getDate()),
2432
                Security::remove_XSS($comment->getItem()->getTitle()),
2433
                $comment->getScore(),
2434
            ];
2435
        }
2436
2437
        $tblItems = new HTML_Table(['class' => 'table table-hover table-striped table-bordered data_table']);
2438
        $tblItems->setHeaders($tblItemsHeaders);
2439
        $tblItems->setData($tblItemsData);
2440
2441
        $tblComments = new HTML_Table(['class' => 'table table-hover table-striped table-bordered data_table']);
2442
        $tblComments->setHeaders($tblCommentsHeaders);
2443
        $tblComments->setData($tblCommentsData);
2444
2445
        $itemFilename = sprintf('%s/index.html', $tempPortfolioDirectory);
2446
2447
        $filenames[] = $itemFilename;
2448
2449
        $fs->dumpFile(
2450
            $itemFilename,
2451
            $this->formatZipIndexFile($tblItems, $tblComments)
2452
        );
2453
2454
        $zipName = $this->owner->getFullName()
2455
            .($this->course ? '_'.$this->course->getCode() : '')
2456
            .'_'.get_lang('Portfolio');
2457
        $tempZipFile = $sysArchivePath."portfolio/$zipName.zip";
2458
        $zip = new PclZip($tempZipFile);
2459
2460
        foreach ($filenames as $filename) {
2461
            $zip->add($filename, PCLZIP_OPT_REMOVE_PATH, $tempPortfolioDirectory);
2462
        }
2463
2464
        Container::getEventDispatcher()->dispatch(
2465
            new PortfolioItemDownloadedEvent(['owner' => $this->owner]),
2466
            Events::PORTFOLIO_DOWNLOADED,
2467
        );
2468
2469
        DocumentManager::file_send_for_download($tempZipFile, true, "$zipName.zip");
2470
2471
        $fs->remove($tempPortfolioDirectory);
2472
        $fs->remove($tempZipFile);
2473
    }
2474
2475
    public function qualifyItem(Portfolio $item)
2476
    {
2477
        global $interbreadcrumb;
2478
2479
        $em = Database::getManager();
2480
2481
        $formAction = $this->baseUrl.http_build_query(['action' => 'qualify', 'item' => $item->getId()]);
2482
2483
        $form = new FormValidator('frm_qualify', 'post', $formAction);
2484
        $form->addUserAvatar('user', get_lang('Author'));
2485
        $form->addLabel(get_lang('Title'), $item->getTitle());
2486
2487
        $itemContent = $this->generateItemContent($item);
2488
2489
        $form->addLabel(get_lang('Content'), $itemContent);
2490
        $form->addNumeric(
2491
            'score',
2492
            [get_lang('Score'), null, ' / '.api_get_course_setting('portfolio_max_score')]
2493
        );
2494
        $form->addButtonSave(get_lang('Grade this item'));
2495
2496
        if ($form->validate()) {
2497
            $values = $form->exportValues();
2498
2499
            $item->setScore($values['score']);
2500
2501
            $em->persist($item);
2502
            $em->flush();
2503
2504
            Container::getEventDispatcher()->dispatch(
2505
                new PortfolioItemScoredEvent(['portfolio' => $item]),
2506
                Events::PORTFOLIO_ITEM_SCORED
2507
            );
2508
2509
            Display::addFlash(
2510
                Display::return_message(get_lang('Portfolio item was graded'), 'success')
2511
            );
2512
2513
            header("Location: $formAction");
2514
            exit();
2515
        }
2516
2517
        $form->setDefaults(
2518
            [
2519
                'user' => $item->getUser(),
2520
                'score' => (float) $item->getScore(),
2521
            ]
2522
        );
2523
2524
        $interbreadcrumb[] = [
2525
            'name' => get_lang('Portfolio'),
2526
            'url' => $this->baseUrl,
2527
        ];
2528
        $interbreadcrumb[] = [
2529
            'name' => $item->getTitle(true),
2530
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
2531
        ];
2532
2533
        $actions = [];
2534
        $actions[] = Display::url(
2535
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
2536
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
2537
        );
2538
2539
        $this->renderView($form->returnForm(), get_lang('Qualify'), $actions);
2540
    }
2541
2542
    public function qualifyComment(PortfolioComment $comment)
2543
    {
2544
        global $interbreadcrumb;
2545
2546
        $em = Database::getManager();
2547
2548
        $item = $comment->getItem();
2549
        $commentPath = $em->getRepository(PortfolioComment::class)->getPath($comment);
2550
2551
        $commentContext = Container::getTwig()->render(
2552
            '@ChamiloCore/Portfolio/comment_context.html.twig',
2553
            [
2554
                'item' => $item,
2555
                'comment_path' => $commentPath,
2556
            ]
2557
        );
2558
2559
        $formAction = $this->baseUrl.http_build_query(['action' => 'qualify', 'comment' => $comment->getId()]);
2560
2561
        $form = new FormValidator('frm_qualify', 'post', $formAction);
2562
        $form->addHtml($commentContext);
2563
        $form->addUserAvatar('user', get_lang('Author'));
2564
        $form->addLabel(get_lang('Comment'), $comment->getContent());
2565
        $form->addNumeric(
2566
            'score',
2567
            [get_lang('Score'), null, '/ '.api_get_course_setting('portfolio_max_score')]
2568
        );
2569
        $form->addButtonSave(get_lang('Grade this comment'));
2570
2571
        if ($form->validate()) {
2572
            $values = $form->exportValues();
2573
2574
            $comment->setScore($values['score']);
2575
2576
            $em->persist($comment);
2577
            $em->flush();
2578
2579
            Container::getEventDispatcher()->dispatch(
2580
                new PortfolioCommentScoredEvent(['comment' => $comment]),
2581
                Events::PORTFOLIO_COMMENT_SCORED
2582
            );
2583
2584
            Display::addFlash(
2585
                Display::return_message(get_lang('Portfolio comment was graded'), 'success')
2586
            );
2587
2588
            header("Location: $formAction");
2589
            exit();
2590
        }
2591
2592
        $form->setDefaults(
2593
            [
2594
                'user' => $comment->getAuthor(),
2595
                'score' => (float) $comment->getScore(),
2596
            ]
2597
        );
2598
2599
        $interbreadcrumb[] = [
2600
            'name' => get_lang('Portfolio'),
2601
            'url' => $this->baseUrl,
2602
        ];
2603
        $interbreadcrumb[] = [
2604
            'name' => $item->getTitle(true),
2605
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
2606
        ];
2607
2608
        $actions = [];
2609
        $actions[] = Display::url(
2610
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
2611
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
2612
        );
2613
2614
        $this->renderView($form->returnForm(), get_lang('Qualify'), $actions);
2615
    }
2616
2617
    public function downloadAttachment(HttpRequest $httpRequest)
2618
    {
2619
        $path = $httpRequest->query->get('file');
2620
2621
        if (empty($path)) {
2622
            api_not_allowed(true);
2623
        }
2624
2625
        $em = Database::getManager();
2626
        $attachmentRepo = $em->getRepository(PortfolioAttachment::class);
2627
2628
        $attachment = $attachmentRepo->findOneByPath($path);
2629
2630
        if (empty($attachment)) {
2631
            api_not_allowed(true);
2632
        }
2633
2634
        $originOwnerId = 0;
2635
2636
        if (Portfolio::TYPE_ITEM === $attachment->getOriginType()) {
2637
            $item = $em->find(Portfolio::class, $attachment->getOrigin());
2638
2639
            $originOwnerId = $item->getUser()->getId();
2640
        } elseif (Portfolio::TYPE_COMMENT === $attachment->getOriginType()) {
2641
            $comment = $em->find(PortfolioComment::class, $attachment->getOrigin());
2642
2643
            $originOwnerId = $comment->getAuthor()->getId();
2644
        } else {
2645
            api_not_allowed(true);
2646
        }
2647
2648
        $userDirectory = UserManager::getUserPathById($originOwnerId, 'system');
2649
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2650
        $attachmentFilename = $attachmentsDirectory.$attachment->getPath();
2651
2652
        if (!Security::check_abs_path($attachmentFilename, $attachmentsDirectory)) {
2653
            api_not_allowed(true);
2654
        }
2655
2656
        $downloaded = DocumentManager::file_send_for_download(
2657
            $attachmentFilename,
2658
            true,
2659
            $attachment->getFilename()
2660
        );
2661
2662
        if (!$downloaded) {
2663
            api_not_allowed(true);
2664
        }
2665
    }
2666
2667
    public function deleteAttachment(HttpRequest $httpRequest)
2668
    {
2669
        $currentUserId = api_get_user_id();
2670
2671
        $path = $httpRequest->query->get('file');
2672
2673
        if (empty($path)) {
2674
            api_not_allowed(true);
2675
        }
2676
2677
        $em = Database::getManager();
2678
        $fs = new Filesystem();
2679
2680
        $attachmentRepo = $em->getRepository(PortfolioAttachment::class);
2681
        $attachment = $attachmentRepo->findOneByPath($path);
2682
2683
        if (empty($attachment)) {
2684
            api_not_allowed(true);
2685
        }
2686
2687
        $originOwnerId = 0;
2688
        $itemId = 0;
2689
2690
        if (Portfolio::TYPE_ITEM === $attachment->getOriginType()) {
2691
            $item = $em->find(Portfolio::class, $attachment->getOrigin());
2692
            $originOwnerId = $item->getUser()->getId();
2693
            $itemId = $item->getId();
2694
        } elseif (Portfolio::TYPE_COMMENT === $attachment->getOriginType()) {
2695
            $comment = $em->find(PortfolioComment::class, $attachment->getOrigin());
2696
            $originOwnerId = $comment->getAuthor()->getId();
2697
            $itemId = $comment->getItem()->getId();
2698
        }
2699
2700
        if ($currentUserId !== $originOwnerId) {
2701
            api_not_allowed(true);
2702
        }
2703
2704
        $em->remove($attachment);
2705
        $em->flush();
2706
2707
        $userDirectory = UserManager::getUserPathById($originOwnerId, 'system');
2708
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2709
        $attachmentFilename = $attachmentsDirectory.$attachment->getPath();
2710
2711
        $fs->remove($attachmentFilename);
2712
2713
        if ($httpRequest->isXmlHttpRequest()) {
2714
            echo Display::return_message(get_lang('The attached file has been deleted'), 'success');
2715
        } else {
2716
            Display::addFlash(
2717
                Display::return_message(get_lang('The attached file has been deleted'), 'success')
2718
            );
2719
2720
            $url = $this->baseUrl.http_build_query(['action' => 'view', 'id' => $itemId]);
2721
2722
            if (Portfolio::TYPE_COMMENT === $attachment->getOriginType() && isset($comment)) {
2723
                $url .= '#comment-'.$comment->getId();
2724
            }
2725
2726
            header("Location: $url");
2727
        }
2728
2729
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
2730
    }
2731
2732
    /**
2733
     * @throws \Doctrine\ORM\OptimisticLockException
2734
     * @throws \Doctrine\ORM\ORMException
2735
     */
2736
    public function markAsHighlighted(Portfolio $item)
2737
    {
2738
        if ($item->getCourse()->getId() !== (int) api_get_course_int_id()) {
2739
            api_not_allowed(true);
2740
        }
2741
2742
        $item->setIsHighlighted(
2743
            !$item->isHighlighted()
2744
        );
2745
2746
        Database::getManager()->flush();
2747
2748
        if ($item->isHighlighted()) {
2749
            Container::getEventDispatcher()->dispatch(
2750
                new \Chamilo\CoreBundle\Event\PortfolioItemHighlightedEvent(['portfolio' => $item]),
2751
                Events::PORTFOLIO_ITEM_HIGHLIGHTED
2752
            );
2753
        }
2754
2755
        Display::addFlash(
2756
            Display::return_message(
2757
                $item->isHighlighted() ? get_lang('Marked as highlighted') : get_lang('Unmarked as highlighted'),
2758
                'success'
2759
            )
2760
        );
2761
2762
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
2763
        exit;
2764
    }
2765
2766
    public function markAsTemplate(Portfolio $item)
2767
    {
2768
        if (!$this->itemBelongToOwner($item)) {
2769
            api_not_allowed(true);
2770
        }
2771
2772
        $item->setIsTemplate(
2773
            !$item->isTemplate()
2774
        );
2775
2776
        Database::getManager()->flush($item);
2777
2778
        Display::addFlash(
2779
            Display::return_message(
2780
                $item->isTemplate() ? get_lang('Portfolio item set as a new template') : get_lang('Portfolio item unset as template'),
2781
                'success'
2782
            )
2783
        );
2784
2785
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
2786
        exit;
2787
    }
2788
2789
    public function markAsTemplateComment(PortfolioComment $comment)
2790
    {
2791
        if (!$this->commentBelongsToOwner($comment)) {
2792
            api_not_allowed(true);
2793
        }
2794
2795
        $comment->setIsTemplate(
2796
            !$comment->isTemplate()
2797
        );
2798
2799
        Database::getManager()->flush();
2800
2801
        Display::addFlash(
2802
            Display::return_message(
2803
                $comment->isTemplate() ? get_lang('Portfolio comment set as a new template') : get_lang('Portfolio comment unset as template'),
2804
                'success'
2805
            )
2806
        );
2807
2808
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $comment->getItem()->getId()]));
2809
        exit;
2810
    }
2811
2812
    public function listTags(HttpRequest $request)
2813
    {
2814
        global $interbreadcrumb;
2815
2816
        api_protect_course_script();
2817
        api_protect_teacher_script();
2818
2819
        $em = Database::getManager();
2820
        $tagRepo = $em->getRepository(Tag::class);
2821
2822
        $tagsQuery = $tagRepo->findForPortfolioInCourseQuery($this->course, $this->session);
2823
2824
        $tag = $request->query->has('id')
2825
            ? $tagRepo->find($request->query->getInt('id'))
2826
            : null;
2827
2828
        $formAction = ['action' => $request->query->get('action')];
2829
2830
        if ($tag) {
2831
            $formAction['id'] = $tag->getId();
2832
        }
2833
2834
        $form = new FormValidator('frm_add_tag', 'post', $this->baseUrl.http_build_query($formAction));
2835
        $form->addText('name', get_lang('Tag'));
2836
2837
        if ($tag) {
2838
            $form->addButtonUpdate(get_lang('Edit'));
2839
        } else {
2840
            $form->addButtonCreate(get_lang('Add'));
2841
        }
2842
2843
        if ($form->validate()) {
2844
            $values = $form->exportValues();
2845
2846
            $extraFieldInfo = (new ExtraField('portfolio'))->get_handler_field_info_by_field_variable('tags');
2847
2848
            if (!$tag) {
2849
                $tag = (new Tag())->setCount(0);
2850
2851
                $portfolioRelTag = (new PortfolioRelTag())
2852
                    ->setTag($tag)
2853
                    ->setCourse($this->course)
2854
                    ->setSession($this->session)
2855
                ;
2856
2857
                $em->persist($tag);
2858
                $em->persist($portfolioRelTag);
2859
            }
2860
2861
            $tag
2862
                ->setTag($values['name'])
2863
                ->setFieldId((int) $extraFieldInfo['id'])
2864
            ;
2865
2866
            $em->flush();
2867
2868
            Display::addFlash(
2869
                Display::return_message(get_lang('Tag saved'), 'success')
2870
            );
2871
2872
            header('Location: '.$this->baseUrl.http_build_query($formAction));
2873
            exit();
2874
        } else {
2875
            $form->protect();
2876
2877
            if ($tag) {
2878
                $form->setDefaults(['name' => $tag->getTag()]);
2879
            }
2880
        }
2881
2882
        $langTags = get_lang('Tags');
2883
        $langEdit = get_lang('Edit');
2884
2885
        $deleteIcon = Display::return_icon('delete.png', get_lang('Delete'));
2886
        $editIcon = Display::return_icon('edit.png', $langEdit);
2887
2888
        $table = new SortableTable(
2889
            'portfolio_tags',
2890
            function () use ($tagsQuery) {
2891
                return (int) $tagsQuery
2892
                    ->select('COUNT(t)')
2893
                    ->getQuery()
2894
                    ->getSingleScalarResult()
2895
                ;
2896
            },
2897
            function ($from, $limit, $column, $direction) use ($tagsQuery) {
2898
                $data = [];
2899
2900
                /** @var array<int, Tag> $tags */
2901
                $tags = $tagsQuery
2902
                    ->select('t')
2903
                    ->orderBy('t.tag', $direction)
2904
                    ->setFirstResult($from)
2905
                    ->setMaxResults($limit)
2906
                    ->getQuery()
2907
                    ->getResult();
2908
2909
                foreach ($tags as $tag) {
2910
                    $data[] = [
2911
                        $tag->getTag(),
2912
                        $tag->getId(),
2913
                    ];
2914
                }
2915
2916
                return $data;
2917
            },
2918
            0,
2919
            40
2920
        );
2921
        $table->set_header(0, get_lang('Name'));
2922
        $table->set_header(1, get_lang('Actions'), false, ['class' => 'text-right'], ['class' => 'text-right']);
2923
        $table->set_column_filter(
2924
            1,
2925
            function ($id) use ($editIcon, $deleteIcon) {
2926
                $editParams = http_build_query(['action' => 'edit_tag', 'id' => $id]);
2927
                $deleteParams = http_build_query(['action' => 'delete_tag', 'id' => $id]);
2928
2929
                return Display::url($editIcon, $this->baseUrl.$editParams).PHP_EOL
2930
                    .Display::url($deleteIcon, $this->baseUrl.$deleteParams).PHP_EOL;
2931
            }
2932
        );
2933
        $table->set_additional_parameters(
2934
            [
2935
                'action' => 'tags',
2936
                'cidReq' => $this->course->getCode(),
2937
                'id_session' => $this->session ? $this->session->getId() : 0,
2938
                'gidReq' => 0,
2939
            ]
2940
        );
2941
2942
        $content = $form->returnForm().PHP_EOL
2943
            .$table->return_table();
2944
2945
        $interbreadcrumb[] = [
2946
            'name' => get_lang('Portfolio'),
2947
            'url' => $this->baseUrl,
2948
        ];
2949
2950
        $pageTitle = $langTags;
2951
2952
        if ($tag) {
2953
            $pageTitle = $langEdit;
2954
2955
            $interbreadcrumb[] = [
2956
                'name' => $langTags,
2957
                'url' => $this->baseUrl.'action=tags',
2958
            ];
2959
        }
2960
2961
        $this->renderView($content, $pageTitle);
2962
    }
2963
2964
    public function deleteTag(Tag $tag)
2965
    {
2966
        api_protect_course_script();
2967
        api_protect_teacher_script();
2968
2969
        $em = Database::getManager();
2970
        $portfolioTagRepo = $em->getRepository(PortfolioRelTag::class);
2971
2972
        $portfolioTag = $portfolioTagRepo
2973
            ->findOneBy(['tag' => $tag, 'course' => $this->course, 'session' => $this->session]);
2974
2975
        if ($portfolioTag) {
2976
            $em->remove($portfolioTag);
2977
            $em->flush();
2978
2979
            Display::addFlash(
2980
                Display::return_message(get_lang('Tag deleted'), 'success')
2981
            );
2982
        }
2983
2984
        header('Location: '.$this->baseUrl.http_build_query(['action' => 'tags']));
2985
        exit();
2986
    }
2987
2988
    /**
2989
     * @throws \Doctrine\ORM\OptimisticLockException
2990
     * @throws \Doctrine\ORM\ORMException
2991
     */
2992
    public function editComment(PortfolioComment $comment)
2993
    {
2994
        global $interbreadcrumb;
2995
2996
        if (!$this->commentBelongsToOwner($comment)) {
2997
            api_not_allowed(true);
2998
        }
2999
3000
        $item = $comment->getItem();
3001
        $commmentCourse = $item->getCourse();
3002
        $commmentSession = $item->getSession();
3003
3004
        $formAction = $this->baseUrl.http_build_query(['action' => 'edit_comment', 'id' => $comment->getId()]);
3005
3006
        $form = new FormValidator('frm_comment', 'post', $formAction);
3007
        $form->addLabel(
3008
            get_lang('Date'),
3009
            $this->getLabelForCommentDate($comment)
3010
        );
3011
        $form->addHtmlEditor('content', get_lang('Comments'), true, false, ['ToolbarSet' => 'Minimal']);
3012
        $form->applyFilter('content', 'trim');
3013
3014
        $this->addAttachmentsFieldToForm($form);
3015
3016
        $form->addButtonUpdate(get_lang('Update'));
3017
3018
        if ($form->validate()) {
3019
            if ($commmentCourse) {
3020
                api_item_property_update(
3021
                    api_get_course_info($commmentCourse->getCode()),
3022
                    TOOL_PORTFOLIO_COMMENT,
3023
                    $comment->getId(),
3024
                    'PortfolioCommentUpdated',
3025
                    api_get_user_id(),
3026
                    [],
3027
                    null,
3028
                    '',
3029
                    '',
3030
                    $commmentSession ? $commmentSession->getId() : 0
3031
                );
3032
            }
3033
3034
            $values = $form->exportValues();
3035
3036
            $comment->setContent($values['content']);
3037
3038
            $this->em->flush();
3039
3040
            $this->processAttachments(
3041
                $form,
3042
                $comment->getAuthor(),
3043
                $comment->getId(),
3044
                Portfolio::TYPE_COMMENT
3045
            );
3046
3047
            Container::getEventDispatcher()->dispatch(
3048
                new PortfolioCommentEditedEvent(['comment' => $comment]),
3049
                Events::PORTFOLIO_COMMENT_EDITED
3050
            );
3051
3052
            Display::addFlash(
3053
                Display::return_message(get_lang('Item updated'), 'success')
3054
            );
3055
3056
            header("Location: $this->baseUrl"
3057
                .http_build_query(['action' => 'view', 'id' => $item->getId()])
3058
                .'#comment-'.$comment->getId()
3059
            );
3060
            exit;
3061
        }
3062
3063
        $form->setDefaults([
3064
            'content' => $comment->getContent(),
3065
        ]);
3066
3067
        $interbreadcrumb[] = [
3068
            'name' => get_lang('Portfolio'),
3069
            'url' => $this->baseUrl,
3070
        ];
3071
        $interbreadcrumb[] = [
3072
            'name' => $item->getTitle(true),
3073
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
3074
        ];
3075
3076
        $actions = [];
3077
        $actions[] = Display::url(
3078
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
3079
            $this->baseUrl
3080
        );
3081
3082
        $content = $form->returnForm()
3083
            .PHP_EOL
3084
            .'<div class="row"> <div class="col-sm-8 col-sm-offset-2">'
3085
            .$this->generateAttachmentList($comment)
3086
            .'</div></div>';
3087
3088
        $this->renderView(
3089
            $content,
3090
            get_lang('Edit portfolio comment'),
3091
            $actions
3092
        );
3093
    }
3094
3095
    /**
3096
     * @throws \Doctrine\ORM\OptimisticLockException
3097
     * @throws \Doctrine\ORM\ORMException
3098
     */
3099
    public function deleteComment(PortfolioComment $comment)
3100
    {
3101
        if (!$this->commentBelongsToOwner($comment)) {
3102
            api_not_allowed(true);
3103
        }
3104
3105
        $this->em->remove($comment);
3106
3107
        $this->em
3108
            ->getRepository(PortfolioAttachment::class)
3109
            ->removeFromComment($comment);
3110
3111
        $this->em->flush();
3112
3113
        Display::addFlash(
3114
            Display::return_message(get_lang('The comment has been deleted.'), 'success')
3115
        );
3116
3117
        header("Location: $this->baseUrl");
3118
        exit;
3119
    }
3120
3121
    public function itemVisibilityChooser(Portfolio $item)
3122
    {
3123
        global $interbreadcrumb;
3124
3125
        if (!$this->itemBelongToOwner($item)) {
3126
            api_not_allowed(true);
3127
        }
3128
3129
        $em = Database::getManager();
3130
        $tblItemProperty = Database::get_course_table(TABLE_ITEM_PROPERTY);
3131
3132
        $courseId = $this->course->getId();
3133
        $sessionId = $this->session ? $this->session->getId() : 0;
3134
3135
        $formAction = $this->baseUrl.http_build_query(['action' => 'item_visiblity_choose', 'id' => $item->getId()]);
3136
3137
        $form = new FormValidator('visibility', 'post', $formAction);
3138
        CourseManager::addUserGroupMultiSelect($form, ['USER:'.$this->owner->getId()]);
3139
        $form->addLabel(
3140
            '',
3141
            Display::return_message(
3142
                get_lang('Only selected users will see the content')
3143
                    .'<br>'.get_lang('Leave empty to enable the content for everyone'),
3144
                'info',
3145
                false
3146
            )
3147
        );
3148
        $form->addCheckBox('hidden', '', get_lang('Hidden but visible for me'));
3149
        $form->addButtonSave(get_lang('Save'));
3150
3151
        if ($form->validate()) {
3152
            $values = $form->exportValues();
3153
            $recipients = CourseManager::separateUsersGroups($values['users'])['users'];
3154
            $courseInfo = api_get_course_info_by_id($courseId);
3155
3156
            Database::delete(
3157
                $tblItemProperty,
3158
                [
3159
                    'c_id = ? ' => [$courseId],
3160
                    'AND tool = ? AND ref = ? ' => [TOOL_PORTFOLIO, $item->getId()],
3161
                    'AND lastedit_type = ? ' => ['visible'],
3162
                ]
3163
            );
3164
3165
            if (empty($recipients) && empty($values['hidden'])) {
3166
                $item->setVisibility(Portfolio::VISIBILITY_VISIBLE);
3167
            } else {
3168
                if (empty($values['hidden'])) {
3169
                    foreach ($recipients as $userId) {
3170
                        api_item_property_update(
3171
                            $courseInfo,
3172
                            TOOL_PORTFOLIO,
3173
                            $item->getId(),
3174
                            'visible',
3175
                            api_get_user_id(),
3176
                            [],
3177
                            $userId,
3178
                            '',
3179
                            '',
3180
                            $sessionId
3181
                        );
3182
                    }
3183
                }
3184
3185
                $item->setVisibility(Portfolio::VISIBILITY_PER_USER);
3186
            }
3187
3188
            $em->flush();
3189
3190
            Container::getEventDispatcher()->dispatch(
3191
                new PortfolioItemVisibilityChangedEvent([
3192
                    'portfolio' => $item,
3193
                    'recipients' => array_values($recipients),
3194
                ]),
3195
                Events::PORTFOLIO_ITEM_VISIBILITY_CHANGED
3196
            );
3197
3198
            Display::addFlash(
3199
                Display::return_message(get_lang('Post visibility changed'), 'success')
3200
            );
3201
3202
            header("Location: $formAction");
3203
            exit;
3204
        }
3205
3206
        $result = Database::select(
3207
            'to_user_id',
3208
            $tblItemProperty,
3209
            [
3210
                'where' => [
3211
                    'c_id = ? ' => [$courseId],
3212
                    'AND tool = ? AND ref = ? ' => [TOOL_PORTFOLIO, $item->getId()],
3213
                    'AND to_user_id IS NOT NULL ' => [],
3214
                ],
3215
            ]
3216
        );
3217
3218
        $recipients = array_map(
3219
            function (array $item): string {
3220
                return 'USER:'.$item['to_user_id'];
3221
            },
3222
            $result
3223
        );
3224
3225
        $defaults = ['users' => $recipients];
3226
3227
        if (empty($recipients) && Portfolio::VISIBILITY_PER_USER === $item->getVisibility()) {
3228
            $defaults['hidden'] = true;
3229
        }
3230
3231
        $form->setDefaults($defaults);
3232
        $form->protect();
3233
3234
        $interbreadcrumb[] = [
3235
            'name' => get_lang('Portfolio'),
3236
            'url' => $this->baseUrl,
3237
        ];
3238
        $interbreadcrumb[] = [
3239
            'name' => $item->getTitle(true),
3240
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
3241
        ];
3242
3243
        $actions = [];
3244
        $actions[] = Display::url(
3245
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
3246
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
3247
        );
3248
3249
        $this->renderView(
3250
            $form->returnForm(),
3251
            get_lang('Choose recipients'),
3252
            $actions
3253
        );
3254
    }
3255
3256
    public function commentVisibilityChooser(PortfolioComment $comment): void
3257
    {
3258
        global $interbreadcrumb;
3259
3260
        if (!$this->commentBelongsToOwner($comment)) {
3261
            api_not_allowed(true);
3262
        }
3263
3264
        $em = Database::getManager();
3265
        $tblItemProperty = Database::get_course_table(TABLE_ITEM_PROPERTY);
3266
3267
        $courseId = $this->course->getId();
3268
        $sessionId = $this->session ? $this->session->getId() : 0;
3269
        $item = $comment->getItem();
3270
3271
        $formAction = $this->baseUrl.http_build_query(['action' => 'comment_visiblity_choose', 'id' => $comment->getId()]);
3272
3273
        $form = new FormValidator('visibility', 'post', $formAction);
3274
        CourseManager::addUserGroupMultiSelect($form, ['USER:'.$this->owner->getId()]);
3275
        $form->addLabel(
3276
            '',
3277
            Display::return_message(
3278
                get_lang('Only selected users will see the content')
3279
                    .'<br>'.get_lang('Leave empty to enable the content for everyone'),
3280
                'info',
3281
                false
3282
            )
3283
        );
3284
        $form->addCheckBox('hidden', '', get_lang('Hidden but visible for me'));
3285
        $form->addButtonSave(get_lang('Save'));
3286
3287
        if ($form->validate()) {
3288
            $values = $form->exportValues();
3289
            $recipients = CourseManager::separateUsersGroups($values['users'])['users'];
3290
            $courseInfo = api_get_course_info_by_id($courseId);
3291
3292
            Database::delete(
3293
                $tblItemProperty,
3294
                [
3295
                    'c_id = ? ' => [$courseId],
3296
                    'AND tool = ? AND ref = ? ' => [TOOL_PORTFOLIO_COMMENT, $comment->getId()],
3297
                    'AND lastedit_type = ? ' => ['visible'],
3298
                ]
3299
            );
3300
3301
            if (empty($recipients) && empty($values['hidden'])) {
3302
                $comment->setVisibility(PortfolioComment::VISIBILITY_VISIBLE);
3303
            } else {
3304
                if (empty($values['hidden'])) {
3305
                    foreach ($recipients as $userId) {
3306
                        api_item_property_update(
3307
                            $courseInfo,
3308
                            TOOL_PORTFOLIO_COMMENT,
3309
                            $comment->getId(),
3310
                            'visible',
3311
                            api_get_user_id(),
3312
                            [],
3313
                            $userId,
3314
                            '',
3315
                            '',
3316
                            $sessionId
3317
                        );
3318
                    }
3319
                }
3320
3321
                $comment->setVisibility(PortfolioComment::VISIBILITY_PER_USER);
3322
            }
3323
3324
            $em->flush();
3325
3326
            Display::addFlash(
3327
                Display::return_message(get_lang('The visibility has been changed.'), 'success')
3328
            );
3329
3330
            header("Location: $formAction");
3331
            exit;
3332
        }
3333
3334
        $result = Database::select(
3335
            'to_user_id',
3336
            $tblItemProperty,
3337
            [
3338
                'where' => [
3339
                    'c_id = ? ' => [$courseId],
3340
                    'AND tool = ? AND ref = ? ' => [TOOL_PORTFOLIO_COMMENT, $comment->getId()],
3341
                    'AND to_user_id IS NOT NULL ' => [],
3342
                ],
3343
            ]
3344
        );
3345
3346
        $recipients = array_map(
3347
            function (array $itemProperty): string {
3348
                return 'USER:'.$itemProperty['to_user_id'];
3349
            },
3350
            $result
3351
        );
3352
3353
        $defaults = ['users' => $recipients];
3354
3355
        if (empty($recipients) && PortfolioComment::VISIBILITY_PER_USER === $comment->getVisibility()) {
3356
            $defaults['hidden'] = true;
3357
        }
3358
3359
        $form->setDefaults($defaults);
3360
        $form->protect();
3361
3362
        $interbreadcrumb[] = [
3363
            'name' => get_lang('Portfolio'),
3364
            'url' => $this->baseUrl,
3365
        ];
3366
        $interbreadcrumb[] = [
3367
            'name' => $item->getTitle(true),
3368
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
3369
        ];
3370
        $interbreadcrumb[] = [
3371
            'name' => $comment->getExcerpt(40),
3372
            'url' => $this->baseUrl
3373
                .http_build_query(['action' => 'view', 'id' => $item->getId()])
3374
                .'#comment-'.$comment->getId(),
3375
        ];
3376
3377
        $actions = [];
3378
        $actions[] = Display::url(
3379
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
3380
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
3381
        );
3382
3383
        $this->renderView(
3384
            $form->returnForm(),
3385
            get_lang('Choose recipients'),
3386
            $actions
3387
        );
3388
    }
3389
3390
    private function isAllowed(): bool
3391
    {
3392
        $isSubscribedInCourse = false;
3393
3394
        if ($this->course) {
3395
            $isSubscribedInCourse = CourseManager::is_user_subscribed_in_course(
3396
                api_get_user_id(),
3397
                $this->course->getCode(),
3398
                (bool) $this->session,
3399
                $this->session ? $this->session->getId() : 0
3400
            );
3401
        }
3402
3403
        if (!$this->course || $isSubscribedInCourse) {
3404
            return true;
3405
        }
3406
3407
        return false;
3408
    }
3409
3410
    private function blockIsNotAllowed(): void
3411
    {
3412
        if (!$this->isAllowed()) {
3413
            api_not_allowed(true);
3414
        }
3415
    }
3416
3417
    /**
3418
     * @param  bool  $showHeader
3419
     */
3420
    private function renderView(string $content, string $toolName, array $actions = [], bool $showHeader = true): void
3421
    {
3422
        global $this_section;
3423
3424
        $this_section = $this->course ? SECTION_COURSES : SECTION_SOCIAL;
3425
3426
        $view = new Template($toolName);
3427
3428
        if ($showHeader) {
3429
            $view->assign('header', $toolName);
3430
        }
3431
3432
        $actionsStr = '';
3433
3434
        if ($this->course) {
3435
            $actionsStr .= Display::return_introduction_section(TOOL_PORTFOLIO);
3436
        }
3437
3438
        if ($actions) {
3439
            $actions = implode('', $actions);
3440
3441
            $actionsStr .= Display::toolbarAction('portfolio-toolbar', [$actions]);
3442
        }
3443
3444
        $view->assign('baseurl', $this->baseUrl);
3445
        $view->assign('actions', $actionsStr);
3446
3447
        $view->assign('content', $content);
3448
        $view->display_one_col_template();
3449
    }
3450
3451
    private function categoryBelongToOwner(PortfolioCategory $category): bool
3452
    {
3453
        if ($category->getUser()->getId() != $this->owner->getId()) {
3454
            return false;
3455
        }
3456
3457
        return true;
3458
    }
3459
3460
    private function addAttachmentsFieldToForm(FormValidator $form): void
3461
    {
3462
        $form->addButton('add_attachment', get_lang('Add attachment'), 'plus');
3463
        $form->addHtml('<div id="container-attachments" style="display: none;">');
3464
        $form->addFile('attachment_file[]', get_lang('Files attachments'));
3465
        $form->addText('attachment_comment[]', get_lang('Description'), false);
3466
        $form->addHtml('</div>');
3467
3468
        $script = "$(function () {
3469
            var attachmentsTemplate = $('#container-attachments').html();
3470
            var \$btnAdd = $('[name=\"add_attachment\"]');
3471
            var \$reference = \$btnAdd.parents('.form-group');
3472
3473
            \$btnAdd.on('click', function (e) {
3474
                e.preventDefault();
3475
3476
                $(attachmentsTemplate).insertBefore(\$reference);
3477
            });
3478
        })";
3479
3480
        $form->addHtml("<script>$script</script>");
3481
    }
3482
3483
    private function processAttachments(
3484
        FormValidator $form,
3485
        User $user,
3486
        int $originId,
3487
        int $originType
3488
    ): void {
3489
        $em = Database::getManager();
3490
        $fs = new Filesystem();
3491
3492
        $comments = $form->getSubmitValue('attachment_comment');
3493
3494
        foreach ($_FILES['attachment_file']['error'] as $i => $attachmentFileError) {
3495
            if ($attachmentFileError != UPLOAD_ERR_OK) {
3496
                continue;
3497
            }
3498
3499
            $_file = [
3500
                'name' => $_FILES['attachment_file']['name'][$i],
3501
                'type' => $_FILES['attachment_file']['type'][$i],
3502
                'tmp_name' => $_FILES['attachment_file']['tmp_name'][$i],
3503
                'size' => $_FILES['attachment_file']['size'][$i],
3504
            ];
3505
3506
            if (empty($_file['type'])) {
3507
                $_file['type'] = DocumentManager::file_get_mime_type($_file['name']);
3508
            }
3509
3510
            $newFileName = add_ext_on_mime(stripslashes($_file['name']), $_file['type']);
3511
3512
            if (!filter_extension($newFileName)) {
3513
                Display::addFlash(Display::return_message(get_lang('File upload failed: this file extension or file type is prohibited'), 'error'));
3514
                continue;
3515
            }
3516
3517
            $newFileName = uniqid();
3518
            $attachmentsDirectory = UserManager::getUserPathById($user->getId(), 'system').'portfolio_attachments/';
3519
3520
            if (!$fs->exists($attachmentsDirectory)) {
3521
                $fs->mkdir($attachmentsDirectory, api_get_permissions_for_new_directories());
3522
            }
3523
3524
            $attachmentFilename = $attachmentsDirectory.$newFileName;
3525
3526
            if (is_uploaded_file($_file['tmp_name'])) {
3527
                $moved = move_uploaded_file($_file['tmp_name'], $attachmentFilename);
3528
3529
                if (!$moved) {
3530
                    Display::addFlash(Display::return_message(get_lang('The uploaded file could not be saved (perhaps a permission problem?)'), 'error'));
3531
                    continue;
3532
                }
3533
            }
3534
3535
            $attachment = new PortfolioAttachment();
3536
            $attachment
3537
                ->setFilename($_file['name'])
3538
                ->setComment($comments[$i])
3539
                ->setPath($newFileName)
3540
                ->setOrigin($originId)
3541
                ->setOriginType($originType)
3542
                ->setSize($_file['size']);
3543
3544
            $em->persist($attachment);
3545
            $em->flush();
3546
        }
3547
    }
3548
3549
    private function itemBelongToOwner(Portfolio $item): bool
3550
    {
3551
        if ($item->getUser()->getId() != $this->owner->getId()) {
3552
            return false;
3553
        }
3554
3555
        return true;
3556
    }
3557
3558
    private function commentBelongsToOwner(PortfolioComment $comment): bool
3559
    {
3560
        return $comment->getAuthor() === $this->owner;
3561
    }
3562
3563
    private function createFormTagFilter(bool $listByUser = false): FormValidator
3564
    {
3565
        $tags = Database::getManager()
3566
            ->getRepository(Tag::class)
3567
            ->findForPortfolioInCourseQuery($this->course, $this->session)
3568
            ->getQuery()
3569
            ->getResult()
3570
        ;
3571
3572
        $frmTagList = new FormValidator(
3573
            'frm_tag_list',
3574
            'get',
3575
            $this->baseUrl.($listByUser ? 'user='.$this->owner->getId() : ''),
3576
            '',
3577
            [],
3578
            FormValidator::LAYOUT_BOX
3579
        );
3580
3581
        $frmTagList->addDatePicker('date', get_lang('Creation date'));
3582
3583
        $frmTagList->addSelectFromCollection(
3584
            'tags',
3585
            get_lang('Tags'),
3586
            $tags,
3587
            ['multiple' => 'multiple'],
3588
            false,
3589
            'getTag'
3590
        );
3591
3592
        $frmTagList->addText('text', get_lang('Search'), false)->setIcon('search');
3593
        $frmTagList->applyFilter('text', 'trim');
3594
        $frmTagList->addHtml('<br>');
3595
        $frmTagList->addButtonFilter(get_lang('Filter'));
3596
3597
        if ($this->course) {
3598
            $frmTagList->addHidden('cidReq', $this->course->getCode());
3599
            $frmTagList->addHidden('id_session', $this->session ? $this->session->getId() : 0);
3600
            $frmTagList->addHidden('gidReq', 0);
3601
            $frmTagList->addHidden('gradebook', 0);
3602
            $frmTagList->addHidden('origin', '');
3603
            $frmTagList->addHidden('categoryId', 0);
3604
            $frmTagList->addHidden('subCategoryIds', '');
3605
3606
            if ($listByUser) {
3607
                $frmTagList->addHidden('user', $this->owner->getId());
3608
            }
3609
        }
3610
3611
        return $frmTagList;
3612
    }
3613
3614
    /**
3615
     * @throws Exception
3616
     */
3617
    private function createFormStudentFilter(bool $listByUser = false, bool $listHighlighted = false, bool $listAlphabeticalOrder = false): FormValidator
3618
    {
3619
        $frmStudentList = new FormValidator(
3620
            'frm_student_list',
3621
            'get',
3622
            $this->baseUrl,
3623
            '',
3624
            [],
3625
            FormValidator::LAYOUT_BOX
3626
        );
3627
3628
        $urlParams = http_build_query(
3629
            [
3630
                'a' => 'search_user_by_course',
3631
                'course_id' => $this->course->getId(),
3632
                'session_id' => $this->session ? $this->session->getId() : 0,
3633
            ]
3634
        );
3635
3636
        /** @var SelectAjax $slctUser */
3637
        $slctUser = $frmStudentList->addSelectAjax(
3638
            'user',
3639
            get_lang('Select a learner portfolio'),
3640
            [],
3641
            [
3642
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
3643
                'placeholder' => get_lang('Search users'),
3644
                'formatResult' => SelectAjax::templateResultForUsersInCourse(),
3645
                'formatSelection' => SelectAjax::templateSelectionForUsersInCourse(),
3646
            ]
3647
        );
3648
3649
        if ($listByUser) {
3650
            $slctUser->addOption(
3651
                $this->owner->getFullName(),
3652
                $this->owner->getId(),
3653
                [
3654
                    'data-avatarurl' => UserManager::getUserPicture($this->owner->getId()),
3655
                    'data-username' => $this->owner->getUsername(),
3656
                ]
3657
            );
3658
3659
            $link = Display::url(
3660
                get_lang('Back to the main course portfolio'),
3661
                $this->baseUrl
3662
            );
3663
        } else {
3664
            $link = Display::url(
3665
                get_lang('See my portfolio in this course'),
3666
                $this->baseUrl.http_build_query(['user' => api_get_user_id()])
3667
            );
3668
        }
3669
3670
        $frmStudentList->addHtml("<p>$link</p>");
3671
3672
        if ($listHighlighted) {
3673
            $link = Display::url(
3674
                get_lang('Back to the main course portfolio'),
3675
                $this->baseUrl
3676
            );
3677
        } else {
3678
            $link = Display::url(
3679
                get_lang('See highlights'),
3680
                $this->baseUrl.http_build_query(['list_highlighted' => true])
3681
            );
3682
        }
3683
3684
        $frmStudentList->addHtml("<p>$link</p>");
3685
3686
        if (true !== api_get_configuration_value('portfolio_order_post_by_alphabetical_order')) {
3687
            if ($listAlphabeticalOrder) {
3688
                $link = Display::url(
3689
                    get_lang('View in chronological order'),
3690
                    $this->baseUrl
3691
                );
3692
            } else {
3693
                $link = Display::url(
3694
                    get_lang('View in alphabetical order'),
3695
                    $this->baseUrl.http_build_query(['list_alphabetical' => true])
3696
                );
3697
            }
3698
3699
            $frmStudentList->addHtml("<p>$link</p>");
3700
        }
3701
3702
        return $frmStudentList;
3703
    }
3704
3705
    private function getCategoriesForIndex(?int $parentId = null): array
3706
    {
3707
        $categoriesCriteria = [];
3708
3709
        if (!api_is_platform_admin() && null !== $this->owner->getId()) {
3710
            $categoriesCriteria['isVisible'] = true;
3711
        }
3712
        if (isset($parentId)) {
3713
            $categoriesCriteria['parent'] = $parentId;
3714
        }
3715
3716
        return $this->em
3717
            ->getRepository(PortfolioCategory::class)
3718
            ->findBy($categoriesCriteria);
3719
    }
3720
3721
    private function getHighlightedItems()
3722
    {
3723
        $queryBuilder = $this->em->createQueryBuilder();
3724
        $queryBuilder
3725
            ->select('pi')
3726
            ->from(Portfolio::class, 'pi')
3727
            ->where('pi.course = :course')
3728
            ->andWhere('pi.isHighlighted = TRUE')
3729
            ->setParameter('course', $this->course);
3730
3731
        if ($this->session) {
3732
            $queryBuilder->andWhere('pi.session = :session');
3733
            $queryBuilder->setParameter('session', $this->session);
3734
        } else {
3735
            $queryBuilder->andWhere('pi.session IS NULL');
3736
        }
3737
3738
        if ($this->advancedSharingEnabled) {
3739
            $queryBuilder
3740
                ->leftJoin(
3741
                    CItemProperty::class,
3742
                    'cip',
3743
                    Join::WITH,
3744
                    "cip.ref = pi.id
3745
                        AND cip.tool = :cip_tool
3746
                        AND cip.course = pi.course
3747
                        AND cip.lasteditType = 'visible'
3748
                        AND cip.toUser = :current_user"
3749
                )
3750
                ->andWhere(
3751
                    sprintf(
3752
                        'pi.visibility = %d
3753
                            OR (
3754
                                pi.visibility = %d AND cip IS NOT NULL OR pi.user = :current_user
3755
                            )',
3756
                        Portfolio::VISIBILITY_VISIBLE,
3757
                        Portfolio::VISIBILITY_PER_USER
3758
                    )
3759
                )
3760
                ->setParameter('cip_tool', TOOL_PORTFOLIO)
3761
            ;
3762
        } else {
3763
            $visibilityCriteria = [Portfolio::VISIBILITY_VISIBLE];
3764
3765
            if (api_is_allowed_to_edit()) {
3766
                $visibilityCriteria[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
3767
            }
3768
3769
            $queryBuilder->andWhere(
3770
                $queryBuilder->expr()->orX(
3771
                    'pi.user = :current_user',
3772
                    $queryBuilder->expr()->andX(
3773
                        'pi.user != :current_user',
3774
                        $queryBuilder->expr()->in('pi.visibility', $visibilityCriteria)
3775
                    )
3776
                )
3777
            );
3778
        }
3779
3780
        $queryBuilder->setParameter('current_user', api_get_user_id());
3781
        $queryBuilder->orderBy('pi.creationDate', 'DESC');
3782
3783
        return $queryBuilder->getQuery()->getResult();
3784
    }
3785
3786
    private function getItemsForIndex(
3787
        bool $listByUser = false,
3788
        FormValidator $frmFilterList = null,
3789
        bool $alphabeticalOrder = false
3790
    ) {
3791
        $currentUserId = api_get_user_id();
3792
3793
        if ($this->course) {
3794
            $showBaseContentInSession = $this->session
3795
                && true === api_get_configuration_value('portfolio_show_base_course_post_in_sessions');
3796
3797
            $portfolioRepo = Container::getPortfolioRepository();
3798
            $portfolioCategoryHelper = Container::getPortfolioCategoryHelper();
3799
3800
            $filters = $frmFilterList && $frmFilterList->validate() ? $frmFilterList->exportValues() : [];
3801
3802
            $searchInCategories = [];
3803
3804
            if ($categoryId = $filters['categoryId'] ?? null) {
3805
                $searchInCategories[] = $categoryId;
3806
3807
                foreach ($portfolioCategoryHelper->getListForIndex($categoryId) as $subCategory) {
3808
                    $searchInCategories[] = $subCategory->getId();
3809
                }
3810
            }
3811
3812
            $searchNotInCategories = [];
3813
3814
            if ($subCategoryIdList = $filters['subCategoryIds'] ?? '') {
3815
                $diff = [];
3816
3817
                if ('all' !== $subCategoryIdList) {
3818
                    $subCategoryIds = explode(',', $subCategoryIdList);
3819
                    $diff = array_diff($searchInCategories, $subCategoryIds);
3820
                } elseif (trim($subCategoryIdList) === '') {
3821
                    $diff = $searchInCategories;
3822
                }
3823
3824
                if (!empty($diff)) {
3825
                    unset($diff[0]);
3826
3827
                    $searchNotInCategories = $diff;
3828
                }
3829
            }
3830
3831
            $items = $portfolioRepo->getIndexCourseItems(
3832
                api_get_user_entity(),
3833
                $this->owner,
3834
                $this->course,
3835
                $this->session,
3836
                $showBaseContentInSession,
3837
                $listByUser,
3838
                $filters['date'] ?? null,
3839
                $filters['tags'] ?? [],
3840
                $filters['text'] ?? '',
3841
                $searchInCategories,
3842
                $searchNotInCategories,
3843
                $this->advancedSharingEnabled
3844
            );
3845
3846
            if ($showBaseContentInSession) {
3847
                $items = array_filter(
3848
                    $items,
3849
                    fn (Portfolio $item) => !($this->session && !$item->getSession() && $item->isDuplicatedInSession($this->session))
3850
                );
3851
            }
3852
3853
            return $items;
3854
        } else {
3855
            $itemsCriteria = [];
3856
            $itemsCriteria['category'] = null;
3857
            $itemsCriteria['user'] = $this->owner;
3858
3859
            if ($currentUserId !== $this->owner->getId()) {
3860
                $itemsCriteria['visibility'] = Portfolio::VISIBILITY_VISIBLE;
3861
            }
3862
3863
            $items = Container::getPortfolioRepository()
3864
                ->findBy($itemsCriteria, ['creationDate' => 'DESC']);
3865
        }
3866
3867
        return $items;
3868
    }
3869
3870
    /**
3871
     * @throws \Doctrine\ORM\ORMException
3872
     * @throws \Doctrine\ORM\OptimisticLockException
3873
     * @throws \Doctrine\ORM\TransactionRequiredException
3874
     */
3875
    private function createCommentForm(Portfolio $item): string
3876
    {
3877
        $formAction = $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]);
3878
3879
        $templates = $this->em
3880
            ->getRepository(PortfolioComment::class)
3881
            ->findBy(
3882
                [
3883
                    'isTemplate' => true,
3884
                    'author' => $this->owner,
3885
                ]
3886
            );
3887
3888
        $form = new FormValidator('frm_comment', 'post', $formAction);
3889
        $form->addHeader(get_lang('Add a new comment'));
3890
        $form->addSelectFromCollection(
3891
            'template',
3892
            [
3893
                get_lang('Template'),
3894
                null,
3895
                '<span id="portfolio-spinner" class="fa fa-fw fa-spinner fa-spin" style="display: none;"
3896
                    aria-hidden="true" aria-label="'.get_lang('Loading').'"></span>',
3897
            ],
3898
            $templates,
3899
            [],
3900
            true,
3901
            'getExcerpt'
3902
        );
3903
        $form->addHtmlEditor('content', get_lang('Comments'), true, false, ['ToolbarSet' => 'Minimal']);
3904
        $form->addHidden('item', $item->getId());
3905
        $form->addHidden('parent', 0);
3906
        $form->applyFilter('content', 'trim');
3907
3908
        $this->addAttachmentsFieldToForm($form);
3909
3910
        $form->addButtonSave(get_lang('Save'));
3911
3912
        if ($form->validate()) {
3913
            if ($this->session
3914
                && true === api_get_configuration_value('portfolio_show_base_course_post_in_sessions')
3915
                && !$item->getSession()
3916
            ) {
3917
                $duplicate = $item->duplicateInSession($this->session);
3918
3919
                $this->em->persist($duplicate);
3920
                $this->em->flush();
3921
3922
                $item = $duplicate;
3923
3924
                $formAction = $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]);
3925
            }
3926
3927
            $values = $form->exportValues();
3928
3929
            $parentComment = $this->em->find(PortfolioComment::class, $values['parent']);
3930
3931
            $comment = new PortfolioComment();
3932
            $comment
3933
                ->setAuthor($this->owner)
3934
                ->setParent($parentComment)
3935
                ->setContent($values['content'])
3936
                ->setDate(api_get_utc_datetime(null, false, true))
3937
                ->setItem($item);
3938
3939
            $this->em->persist($comment);
3940
            $this->em->flush();
3941
3942
            $this->processAttachments(
3943
                $form,
3944
                $comment->getAuthor(),
3945
                $comment->getId(),
3946
                Portfolio::TYPE_COMMENT
3947
            );
3948
3949
            Container::getEventDispatcher()->dispatch(
3950
                new PortfolioItemCommentedEvent(['comment' => $comment]),
3951
                Events::PORTFOLIO_ITEM_COMMENTED
3952
            );
3953
3954
            PortfolioNotifier::notifyTeachersAndAuthor($comment);
3955
3956
            Display::addFlash(
3957
                Display::return_message(get_lang('You comment has been added'), 'success')
3958
            );
3959
3960
            header("Location: $formAction");
3961
            exit;
3962
        }
3963
3964
        $js = '<script>
3965
            $(function() {
3966
                $(\'#frm_comment_template\').on(\'change\', function () {
3967
                    $(\'#portfolio-spinner\').show();
3968
3969
                    $.getJSON(_p.web_ajax + \'portfolio.ajax.php?a=find_template_comment&comment=\' + this.value)
3970
                        .done(function(response) {
3971
                            CKEDITOR.instances.content.setData(response.content);
3972
                        })
3973
                        .fail(function () {
3974
                            CKEDITOR.instances.content.setData(\'\');
3975
                        })
3976
                        .always(function() {
3977
                          $(\'#portfolio-spinner\').hide();
3978
                        });
3979
                });
3980
            });
3981
        </script>';
3982
3983
        return $form->returnForm().$js;
3984
    }
3985
3986
    private function generateAttachmentList($post, bool $includeHeader = true): string
3987
    {
3988
        $attachmentsRepo = $this->em->getRepository(PortfolioAttachment::class);
3989
3990
        $postOwnerId = 0;
3991
3992
        if ($post instanceof Portfolio) {
3993
            $attachments = $attachmentsRepo->findFromItem($post);
3994
3995
            $postOwnerId = $post->getUser()->getId();
3996
        } elseif ($post instanceof PortfolioComment) {
3997
            $attachments = $attachmentsRepo->findFromComment($post);
3998
3999
            $postOwnerId = $post->getAuthor()->getId();
4000
        }
4001
4002
        if (empty($attachments)) {
4003
            return '';
4004
        }
4005
4006
        $currentUserId = api_get_user_id();
4007
4008
        $listItems = '<ul class="fa-ul">';
4009
4010
        $deleteIcon = Display::return_icon(
4011
            'delete.png',
4012
            get_lang('Delete attachment'),
4013
            ['style' => 'display: inline-block'],
4014
            ICON_SIZE_TINY
4015
        );
4016
        $deleteAttrs = ['class' => 'btn-portfolio-delete'];
4017
4018
        /** @var PortfolioAttachment $attachment */
4019
        foreach ($attachments as $attachment) {
4020
            $downloadParams = http_build_query(['action' => 'download_attachment', 'file' => $attachment->getPath()]);
4021
            $deleteParams = http_build_query(['action' => 'delete_attachment', 'file' => $attachment->getPath()]);
4022
4023
            $listItems .= '<li>'
4024
                .'<span class="fa-li fa fa-paperclip" aria-hidden="true"></span>'
4025
                .Display::url(
4026
                    Security::remove_XSS($attachment->getFilename()),
4027
                    $this->baseUrl.$downloadParams
4028
                );
4029
4030
            if ($currentUserId === $postOwnerId) {
4031
                $listItems .= PHP_EOL.Display::url($deleteIcon, $this->baseUrl.$deleteParams, $deleteAttrs);
4032
            }
4033
4034
            if ($attachment->getComment()) {
4035
                $listItems .= '<p class="text-muted">'.Security::remove_XSS($attachment->getComment()).'</p>';
4036
            }
4037
4038
            $listItems .= '</li>';
4039
        }
4040
4041
        $listItems .= '</ul>';
4042
4043
        if ($includeHeader) {
4044
            $listItems = '<h1 class="h4">'.get_lang('Files attachments').'</h1>'
4045
                .$listItems;
4046
        }
4047
4048
        return $listItems;
4049
    }
4050
4051
    private function generateItemContent(Portfolio $item): string
4052
    {
4053
        $originId = $item->getOrigin();
4054
4055
        if (empty($originId)) {
4056
            return $item->getContent();
4057
        }
4058
4059
        $em = Database::getManager();
4060
4061
        $originContent = '';
4062
        $originContentFooter = '';
4063
4064
        if (Portfolio::TYPE_ITEM === $item->getOriginType()) {
4065
            $origin = $em->find(Portfolio::class, $item->getOrigin());
4066
4067
            if ($origin) {
4068
                $originContent = Security::remove_XSS($origin->getContent());
4069
                $originContentFooter = vsprintf(
4070
                    get_lang('Originally published as "%s" by %s'),
4071
                    [
4072
                        "<cite>{$origin->getTitle(true)}</cite>",
4073
                        $origin->getUser()->getFullName(),
4074
                    ]
4075
                );
4076
            }
4077
        } elseif (Portfolio::TYPE_COMMENT === $item->getOriginType()) {
4078
            $origin = $em->find(PortfolioComment::class, $item->getOrigin());
4079
4080
            if ($origin) {
4081
                $originContent = Security::remove_XSS($origin->getContent());
4082
                $originContentFooter = vsprintf(
4083
                    get_lang('Originally commented by %s in "%s"'),
4084
                    [
4085
                        $origin->getAuthor()->getFullName(),
4086
                        "<cite>{$origin->getItem()->getTitle(true)}</cite>",
4087
                    ]
4088
                );
4089
            }
4090
        }
4091
4092
        if ($originContent) {
4093
            return "<figure>
4094
                    <blockquote>$originContent</blockquote>
4095
                    <figcaption style=\"margin-bottom: 10px;\">$originContentFooter</figcaption>
4096
                </figure>
4097
                <div class=\"clearfix\">".Security::remove_XSS($item->getContent()).'</div>'
4098
            ;
4099
        }
4100
4101
        return Security::remove_XSS($item->getContent());
4102
    }
4103
4104
    private function getItemsInHtmlFormatted(array $items): array
4105
    {
4106
        $itemsHtml = [];
4107
4108
        /** @var Portfolio $item */
4109
        foreach ($items as $item) {
4110
            $itemCourse = $item->getCourse();
4111
            $itemSession = $item->getSession();
4112
4113
            $creationDate = api_convert_and_format_date($item->getCreationDate());
4114
            $updateDate = api_convert_and_format_date($item->getUpdateDate());
4115
4116
            $metadata = '<ul class="list-unstyled text-muted">';
4117
4118
            if ($itemSession) {
4119
                $metadata .= '<li>'.get_lang('Course').': '.$itemSession->getTitle().' ('
4120
                    .$itemCourse->getTitle().') </li>';
4121
            } elseif ($itemCourse) {
4122
                $metadata .= '<li>'.get_lang('Course').': '.$itemCourse->getTitle().'</li>';
4123
            }
4124
4125
            $metadata .= '<li>'.sprintf(get_lang('Creation date: %s'), $creationDate).'</li>';
4126
4127
            if ($itemCourse) {
4128
                $propertyInfo = api_get_item_property_info(
4129
                    $itemCourse->getId(),
4130
                    TOOL_PORTFOLIO,
4131
                    $item->getId(),
4132
                    $itemSession ? $itemSession->getId() : 0
4133
                );
4134
4135
                if ($propertyInfo) {
4136
                    $metadata .= '<li>'
4137
                        .sprintf(
4138
                            get_lang('Updated on %s by %s'),
4139
                            api_convert_and_format_date($propertyInfo['lastedit_date'], DATE_TIME_FORMAT_LONG),
4140
                            api_get_user_entity($propertyInfo['lastedit_user_id'])->getFullName()
4141
                        )
4142
                        .'</li>';
4143
                }
4144
            } else {
4145
                $metadata .= '<li>'.sprintf(get_lang('Update date: %s'), $updateDate).'</li>';
4146
            }
4147
4148
            if ($item->getCategory()) {
4149
                $metadata .= '<li>'.sprintf(get_lang('Category: %s'), $item->getCategory()->getTitle()).'</li>';
4150
            }
4151
4152
            $metadata .= '</ul>';
4153
4154
            $itemContent = $this->generateItemContent($item);
4155
4156
            $itemsHtml[] = Display::panel($itemContent, Security::remove_XSS($item->getTitle()), '', 'info', $metadata);
4157
        }
4158
4159
        return $itemsHtml;
4160
    }
4161
4162
    private function getCommentsInHtmlFormatted(array $comments): array
4163
    {
4164
        $commentsHtml = [];
4165
4166
        /** @var PortfolioComment $comment */
4167
        foreach ($comments as $comment) {
4168
            $item = $comment->getItem();
4169
            $date = api_convert_and_format_date($comment->getDate());
4170
4171
            $metadata = '<ul class="list-unstyled text-muted">';
4172
            $metadata .= '<li>'.sprintf(get_lang('Date: %s'), $date).'</li>';
4173
            $metadata .= '<li>'.sprintf(get_lang('Item title: %s'), Security::remove_XSS($item->getTitle()))
4174
                .'</li>';
4175
            $metadata .= '</ul>';
4176
4177
            $commentsHtml[] = Display::panel(
4178
                Security::remove_XSS($comment->getContent()),
4179
                '',
4180
                '',
4181
                'default',
4182
                $metadata
4183
            );
4184
        }
4185
4186
        return $commentsHtml;
4187
    }
4188
4189
    /**
4190
     * @param string $htmlContent
4191
     * @param array $imagePaths Relative paths found in $htmlContent
4192
     *
4193
     * @return string
4194
     */
4195
    private function fixMediaSourcesToHtml(string $htmlContent, array &$imagePaths): string
4196
    {
4197
        $doc = new DOMDocument();
4198
        @$doc->loadHTML($htmlContent);
4199
4200
        $tagsWithSrc = ['img', 'video', 'audio', 'source'];
4201
        /** @var array<int, \DOMElement> $elements */
4202
        $elements = [];
4203
4204
        foreach ($tagsWithSrc as $tag) {
4205
            foreach ($doc->getElementsByTagName($tag) as $element) {
4206
                if ($element->hasAttribute('src')) {
4207
                    $elements[] = $element;
4208
                }
4209
            }
4210
        }
4211
4212
        if (empty($elements)) {
4213
            return $htmlContent;
4214
        }
4215
4216
        /** @var array<int, \DOMElement> $anchorElements */
4217
        $anchorElements = $doc->getElementsByTagName('a');
4218
4219
        $webPath = api_get_path(WEB_PATH);
4220
        $sysPath = rtrim(api_get_path(SYS_PATH), '/');
4221
4222
        $paths = [
4223
            '/app/upload/' => $sysPath,
4224
            '/courses/' => $sysPath.'/app'
4225
        ];
4226
4227
        foreach ($elements as $element) {
4228
            $src = trim($element->getAttribute('src'));
4229
4230
            if (!str_starts_with($src, '/')
4231
                && !str_starts_with($src, $webPath)
4232
            ) {
4233
                continue;
4234
            }
4235
4236
            // to search anchors linking to files
4237
            if ($anchorElements->length > 0) {
4238
                foreach ($anchorElements as $anchorElement) {
4239
                    if (!$anchorElement->hasAttribute('href')) {
4240
                        continue;
4241
                    }
4242
4243
                    if ($src === $anchorElement->getAttribute('href')) {
4244
                        $anchorElement->setAttribute('href', basename($src));
4245
                    }
4246
                }
4247
            }
4248
4249
            $src = str_replace($webPath, '/', $src);
4250
4251
            foreach ($paths as $prefix => $basePath) {
4252
                if (str_starts_with($src, $prefix)) {
4253
                    $imagePaths[] = $basePath.urldecode($src);
4254
                    $element->setAttribute('src', basename($src));
4255
                }
4256
            }
4257
        }
4258
4259
        return $doc->saveHTML();
4260
    }
4261
4262
    private function formatZipIndexFile(HTML_Table $tblItems, HTML_Table $tblComments): string
4263
    {
4264
        $htmlContent = Display::page_header($this->owner->getFullNameWithUsername());
4265
        $htmlContent .= Display::page_subheader2(get_lang('Portfolio items'));
4266
4267
        $htmlContent .= $tblItems->getRowCount() > 0
4268
            ? $tblItems->toHtml()
4269
            : Display::return_message(get_lang('No items in your portfolio'), 'warning');
4270
4271
        $htmlContent .= Display::page_subheader2(get_lang('Comments made'));
4272
4273
        $htmlContent .= $tblComments->getRowCount() > 0
4274
            ? $tblComments->toHtml()
4275
            : Display::return_message(get_lang('You have not commented'), 'warning');
4276
4277
        $webAssetsPath = api_get_path(WEB_PUBLIC_PATH).'assets/';
4278
4279
        $doc = new DOMDocument();
4280
        @$doc->loadHTML($htmlContent);
4281
4282
        $stylesheet1 = $doc->createElement('link');
4283
        $stylesheet1->setAttribute('rel', 'stylesheet');
4284
        $stylesheet1->setAttribute('href', $webAssetsPath.'bootstrap/dist/css/bootstrap.min.css');
4285
        $stylesheet2 = $doc->createElement('link');
4286
        $stylesheet2->setAttribute('rel', 'stylesheet');
4287
        $stylesheet2->setAttribute('href', $webAssetsPath.'fontawesome/css/font-awesome.min.css');
4288
        $stylesheet3 = $doc->createElement('link');
4289
        $stylesheet3->setAttribute('rel', 'stylesheet');
4290
        $stylesheet3->setAttribute('href', ChamiloApi::getEditorDocStylePath());
4291
4292
        $head = $doc->createElement('head');
4293
        $head->appendChild($stylesheet1);
4294
        $head->appendChild($stylesheet2);
4295
        $head->appendChild($stylesheet3);
4296
4297
        $doc->documentElement->insertBefore(
4298
            $head,
4299
            $doc->getElementsByTagName('body')->item(0)
4300
        );
4301
4302
        return $doc->saveHTML();
4303
    }
4304
4305
    /**
4306
     * It parsers a title for a variable in lang.
4307
     *
4308
     * @param $defaultDisplayText
4309
     *
4310
     * @return string
4311
     */
4312
    private function getLanguageVariable($defaultDisplayText)
4313
    {
4314
        $variableLanguage = api_replace_dangerous_char(strtolower($defaultDisplayText));
4315
        $variableLanguage = preg_replace('/[^A-Za-z0-9\_]/', '', $variableLanguage); // Removes special chars except underscore.
4316
        if (is_numeric($variableLanguage[0])) {
4317
            $variableLanguage = '_'.$variableLanguage;
4318
        }
4319
        $variableLanguage = api_underscore_to_camel_case($variableLanguage);
4320
4321
        return $variableLanguage;
4322
    }
4323
4324
    /**
4325
     * It translates the text as parameter.
4326
     *
4327
     * @param $defaultDisplayText
4328
     *
4329
     * @return mixed
4330
     */
4331
    private function translateDisplayName($defaultDisplayText)
4332
    {
4333
        $variableLanguage = $this->getLanguageVariable($defaultDisplayText);
4334
4335
        return isset($GLOBALS[$variableLanguage]) ? $GLOBALS[$variableLanguage] : $defaultDisplayText;
4336
    }
4337
4338
    private function getCommentsForIndex(FormValidator $frmFilterList = null): array
4339
    {
4340
        if (null === $frmFilterList) {
4341
            return [];
4342
        }
4343
4344
        if (!$frmFilterList->validate()) {
4345
            return [];
4346
        }
4347
4348
        $values = $frmFilterList->exportValues();
4349
4350
        if (empty($values['date']) && empty($values['text'])) {
4351
            return [];
4352
        }
4353
4354
        $queryBuilder = $this->em->createQueryBuilder()
4355
            ->select('c')
4356
            ->from(PortfolioComment::class, 'c')
4357
        ;
4358
4359
        if (!empty($values['date'])) {
4360
            $queryBuilder
4361
                ->andWhere('c.date >= :date')
4362
                ->setParameter(':date', api_get_utc_datetime($values['date'], false, true))
4363
            ;
4364
        }
4365
4366
        if (!empty($values['text'])) {
4367
            $queryBuilder
4368
                ->andWhere('c.content LIKE :text')
4369
                ->setParameter('text', '%'.$values['text'].'%')
4370
            ;
4371
        }
4372
4373
        if ($this->advancedSharingEnabled) {
4374
            $queryBuilder
4375
                ->leftJoin(
4376
                    CItemProperty::class,
4377
                    'cip',
4378
                    Join::WITH,
4379
                    "cip.ref = c.id
4380
                        AND cip.tool = :cip_tool
4381
                        AND cip.course = :course
4382
                        AND cip.lasteditType = 'visible'
4383
                        AND cip.toUser = :current_user"
4384
                )
4385
                ->andWhere(
4386
                    sprintf(
4387
                        'c.visibility = %d
4388
                            OR (
4389
                                c.visibility = %d AND cip IS NOT NULL OR c.author = :current_user
4390
                            )',
4391
                        PortfolioComment::VISIBILITY_VISIBLE,
4392
                        PortfolioComment::VISIBILITY_PER_USER
4393
                    )
4394
                )
4395
                ->setParameter('cip_tool', TOOL_PORTFOLIO_COMMENT)
4396
                ->setParameter('current_user', $this->owner->getId())
4397
                ->setParameter('course', $this->course)
4398
            ;
4399
        }
4400
4401
        $queryBuilder->orderBy('c.date', 'DESC');
4402
4403
        return $queryBuilder->getQuery()->getResult();
4404
    }
4405
4406
    private function getLabelForCommentDate(PortfolioComment $comment): string
4407
    {
4408
        $item = $comment->getItem();
4409
        $commmentCourse = $item->getCourse();
4410
        $commmentSession = $item->getSession();
4411
4412
        $dateLabel = Display::dateToStringAgoAndLongDate($comment->getDate()).PHP_EOL;
4413
4414
        if ($commmentCourse) {
4415
            $propertyInfo = api_get_item_property_info(
4416
                $commmentCourse->getId(),
4417
                TOOL_PORTFOLIO_COMMENT,
4418
                $comment->getId(),
4419
                $commmentSession ? $commmentSession->getId() : 0
4420
            );
4421
4422
            if ($propertyInfo) {
4423
                $dateLabel .= '|'.PHP_EOL
4424
                    .sprintf(
4425
                        get_lang('Updated %s'),
4426
                        Display::dateToStringAgoAndLongDate($propertyInfo['lastedit_date'])
4427
                    );
4428
            }
4429
        }
4430
4431
        return $dateLabel;
4432
    }
4433
}
4434