PortfolioController   F
last analyzed

Complexity

Total Complexity 419

Size/Duplication

Total Lines 4528
Duplicated Lines 0 %

Importance

Changes 6
Bugs 1 Features 2
Metric Value
wmc 419
eloc 2624
c 6
b 1
f 2
dl 0
loc 4528
rs 0.8

58 Methods

Rating   Name   Duplication   Size   Complexity  
B generateAttachmentList() 0 63 8
C commentVisibilityChooser() 0 131 10
A copyComment() 0 28 1
B processAttachments() 0 63 8
A getCommentsInHtmlFormatted() 0 25 2
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 240 10
A translateDisplayName() 0 5 2
C itemVisibilityChooser() 0 132 10
F getItemsForIndex() 0 173 27
A qualifyComment() 0 72 2
A markAsTemplate() 0 21 3
A translateCategory() 0 76 3
C fixMediaSourcesToHtml() 0 65 14
B generateItemContent() 0 51 7
A getHighlightedItems() 0 63 4
A markImportantCommentInItem() 0 19 2
B getCommentsForIndex() 0 66 8
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 228 21
A getLabelForCommentDate() 0 26 4
A renderView() 0 29 5
A deleteItem() 0 20 2
A showHideItem() 0 28 5
A formatZipIndexFile() 0 41 3
F exportPdf() 0 118 15
B createCommentForm() 0 108 5
A addAttachmentsFieldToForm() 0 21 1
A getCategoriesForIndex() 0 16 5
A categoryBelongToOwner() 0 7 2
A blockIsNotAllowed() 0 4 2
B createFormStudentFilter() 0 86 6
F index() 0 152 19
A getLanguageVariable() 0 10 2
A commentBelongsToOwner() 0 3 1
A itemBelongToOwner() 0 7 2
B getItemsInHtmlFormatted() 0 56 8
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

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 as CourseEntity;
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\Tag;
15
use Chamilo\CourseBundle\Entity\CItemProperty;
16
use Chamilo\UserBundle\Entity\User;
17
use Doctrine\ORM\Query\Expr\Join;
18
use Mpdf\MpdfException;
19
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
20
use Symfony\Component\Filesystem\Filesystem;
21
use Symfony\Component\HttpFoundation\Request as HttpRequest;
22
23
/**
24
 * Class PortfolioController.
25
 */
26
class PortfolioController
27
{
28
    /**
29
     * @var string
30
     */
31
    public $baseUrl;
32
    /**
33
     * @var CourseEntity|null
34
     */
35
    private $course;
36
    /**
37
     * @var \Chamilo\CoreBundle\Entity\Session|null
38
     */
39
    private $session;
40
    /**
41
     * @var \Chamilo\UserBundle\Entity\User
42
     */
43
    private $owner;
44
    /**
45
     * @var \Doctrine\ORM\EntityManager
46
     */
47
    private $em;
48
    /**
49
     * @var bool
50
     */
51
    private $advancedSharingEnabled;
52
53
    /**
54
     * PortfolioController constructor.
55
     */
56
    public function __construct()
57
    {
58
        $this->em = Database::getManager();
59
60
        $this->owner = api_get_user_entity(api_get_user_id());
61
        $this->course = api_get_course_entity(api_get_course_int_id());
62
        $this->session = api_get_session_entity(api_get_session_id());
63
64
        $cidreq = api_get_cidreq();
65
        $this->baseUrl = api_get_self().'?'.($cidreq ? $cidreq.'&' : '');
66
67
        $this->advancedSharingEnabled = true === api_get_configuration_value('portfolio_advanced_sharing')
68
            && $this->course;
69
    }
70
71
    /**
72
     * @throws \Doctrine\ORM\ORMException
73
     * @throws \Doctrine\ORM\OptimisticLockException
74
     */
75
    public function translateCategory($category, $languages, $languageId)
76
    {
77
        global $interbreadcrumb;
78
79
        $originalName = $category->getTitle();
80
        $variableLanguage = '$'.$this->getLanguageVariable($originalName);
81
82
        $translateUrl = api_get_path(WEB_AJAX_PATH).'lang.ajax.php?a=translate_portfolio_category&sec_token='.Security::get_token();
83
        $form = new FormValidator('new_lang_variable', 'POST', $translateUrl);
84
        $form->addHeader(get_lang('AddWordForTheSubLanguage'));
85
        $form->addText('variable_language', get_lang('LanguageVariable'), false);
86
        $form->addText('original_name', get_lang('OriginalName'), false);
87
88
        $languagesOptions = [0 => get_lang('None')];
89
        foreach ($languages as $language) {
90
            $languagesOptions[$language->getId()] = $language->getOriginalName();
91
        }
92
93
        $form->addSelect(
94
            'sub_language',
95
            [get_lang('SubLanguage'), get_lang('OnlyActiveSubLanguagesAreListed')],
96
            $languagesOptions
97
        );
98
99
        if ($languageId) {
100
            $languageInfo = api_get_language_info($languageId);
101
            $form->addText(
102
                'new_language',
103
                [get_lang('Translation'), get_lang('IfThisTranslationExistsThisWillReplaceTheTerm')]
104
            );
105
106
            $form->addHidden('category_id', $category->getId());
107
            $form->addHidden('id', $languageInfo['parent_id']);
108
            $form->addHidden('sub', $languageInfo['id']);
109
            $form->addHidden('sub_language_id', $languageInfo['id']);
110
            $form->addHidden('redirect', true);
111
            $form->addButtonSave(get_lang('Save'));
112
        }
113
114
        $form->setDefaults([
115
            'variable_language' => $variableLanguage,
116
            'original_name' => $originalName,
117
            'sub_language' => $languageId,
118
        ]);
119
        $form->addRule('sub_language', get_lang('Required'), 'required');
120
        $form->freeze(['variable_language', 'original_name']);
121
122
        $interbreadcrumb[] = [
123
            'name' => get_lang('Portfolio'),
124
            'url' => $this->baseUrl,
125
        ];
126
        $interbreadcrumb[] = [
127
            'name' => get_lang('Categories'),
128
            'url' => $this->baseUrl.'action=list_categories&parent_id='.$category->getParentId(),
129
        ];
130
        $interbreadcrumb[] = [
131
            'name' => Security::remove_XSS($category->getTitle()),
132
            'url' => $this->baseUrl.'action=edit_category&id='.$category->getId(),
133
        ];
134
135
        $actions = [];
136
        $actions[] = Display::url(
137
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
138
            $this->baseUrl.'action=edit_category&id='.$category->getId()
139
        );
140
141
        $js = '<script>
142
            $(function() {
143
              $("select[name=\'sub_language\']").on("change", function () {
144
                    location.href += "&sub_language=" + this.value;
145
                });
146
            });
147
        </script>';
148
        $content = $form->returnForm();
149
150
        $this->renderView($content.$js, get_lang('TranslateCategory'), $actions);
151
    }
152
153
    /**
154
     * @throws \Doctrine\ORM\ORMException
155
     * @throws \Doctrine\ORM\OptimisticLockException
156
     */
157
    public function listCategories()
158
    {
159
        global $interbreadcrumb;
160
161
        $parentId = isset($_REQUEST['parent_id']) ? (int) $_REQUEST['parent_id'] : 0;
162
        $table = new HTML_Table(['class' => 'table table-hover table-striped data_table']);
163
        $headers = [
164
            get_lang('Title'),
165
            get_lang('Description'),
166
        ];
167
        if ($parentId === 0) {
168
            $headers[] = get_lang('SubCategories');
169
        }
170
        $headers[] = get_lang('Actions');
171
172
        $column = 0;
173
        foreach ($headers as $header) {
174
            $table->setHeaderContents(0, $column, $header);
175
            $column++;
176
        }
177
        $currentUserId = api_get_user_id();
178
        $row = 1;
179
        $categories = $this->getCategoriesForIndex(null, $parentId);
180
181
        foreach ($categories as $category) {
182
            $column = 0;
183
            $subcategories = $this->getCategoriesForIndex(null, $category->getId());
184
            $linkSubCategories = $category->getTitle();
185
            if (count($subcategories) > 0) {
186
                $linkSubCategories = Display::url(
187
                    $category->getTitle(),
188
                    $this->baseUrl.'action=list_categories&parent_id='.$category->getId()
189
                );
190
            }
191
            $table->setCellContents($row, $column++, $linkSubCategories);
192
            $table->setCellContents($row, $column++, strip_tags($category->getDescription()));
193
            if ($parentId === 0) {
194
                $table->setCellContents($row, $column++, count($subcategories));
195
            }
196
197
            // Actions
198
            $links = null;
199
            // Edit action
200
            $url = $this->baseUrl.'action=edit_category&id='.$category->getId();
201
            $links .= Display::url(Display::return_icon('edit.png', get_lang('Edit')), $url).'&nbsp;';
202
            // Visible action : if active
203
            if ($category->isVisible() != 0) {
204
                $url = $this->baseUrl.'action=hide_category&id='.$category->getId();
205
                $links .= Display::url(Display::return_icon('visible.png', get_lang('Hide')), $url).'&nbsp;';
206
            } else { // else if not active
207
                $url = $this->baseUrl.'action=show_category&id='.$category->getId();
208
                $links .= Display::url(Display::return_icon('invisible.png', get_lang('Show')), $url).'&nbsp;';
209
            }
210
            // Delete action
211
            $url = $this->baseUrl.'action=delete_category&id='.$category->getId();
212
            $links .= Display::url(Display::return_icon('delete.png', get_lang('Delete')), $url, ['onclick' => 'javascript:if(!confirm(\''.get_lang('AreYouSureToDeleteJS').'\')) return false;']);
213
214
            $table->setCellContents($row, $column++, $links);
215
            $row++;
216
        }
217
218
        $interbreadcrumb[] = [
219
            'name' => get_lang('Portfolio'),
220
            'url' => $this->baseUrl,
221
        ];
222
        if ($parentId > 0) {
223
            $interbreadcrumb[] = [
224
                'name' => get_lang('Categories'),
225
                'url' => $this->baseUrl.'action=list_categories',
226
            ];
227
        }
228
229
        $actions = [];
230
        $actions[] = Display::url(
231
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
232
            $this->baseUrl.($parentId > 0 ? 'action=list_categories' : '')
233
        );
234
        if ($currentUserId == $this->owner->getId() && $parentId === 0) {
235
            $actions[] = Display::url(
236
                Display::return_icon('new_folder.png', get_lang('AddCategory'), [], ICON_SIZE_MEDIUM),
237
                $this->baseUrl.'action=add_category'
238
            );
239
        }
240
        $content = $table->toHtml();
241
242
        $pageTitle = get_lang('Categories');
243
        if ($parentId > 0) {
244
            $em = Database::getManager();
245
            $parentCategory = $em->find('ChamiloCoreBundle:PortfolioCategory', $parentId);
246
            $pageTitle = $parentCategory->getTitle().' : '.get_lang('SubCategories');
247
        }
248
249
        $this->renderView($content, $pageTitle, $actions);
250
    }
251
252
    /**
253
     * @throws \Doctrine\ORM\ORMException
254
     * @throws \Doctrine\ORM\OptimisticLockException
255
     */
256
    public function addCategory()
257
    {
258
        global $interbreadcrumb;
259
260
        Display::addFlash(
261
            Display::return_message(get_lang('PortfolioCategoryFieldHelp'), 'info')
262
        );
263
264
        $form = new FormValidator('add_category', 'post', "{$this->baseUrl}&action=add_category");
265
266
        if (api_get_configuration_value('save_titles_as_html')) {
267
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
268
        } else {
269
            $form->addText('title', get_lang('Title'));
270
            $form->applyFilter('title', 'trim');
271
        }
272
273
        $form->addHtmlEditor('description', get_lang('Description'), false, false, ['ToolbarSet' => 'Minimal']);
274
275
        $parentSelect = $form->addSelect(
276
            'parent_id',
277
            get_lang('ParentCategory')
278
        );
279
        $parentSelect->addOption(get_lang('Level0'), 0);
280
        $currentUserId = api_get_user_id();
281
        $categories = $this->getCategoriesForIndex(null, 0);
282
        foreach ($categories as $category) {
283
            $parentSelect->addOption($category->getTitle(), $category->getId());
284
        }
285
286
        $form->addButtonCreate(get_lang('Create'));
287
288
        if ($form->validate()) {
289
            $values = $form->exportValues();
290
291
            $category = new PortfolioCategory();
292
            $category
293
                ->setTitle($values['title'])
294
                ->setDescription($values['description'])
295
                ->setParentId($values['parent_id'])
296
                ->setUser($this->owner);
297
298
            $this->em->persist($category);
299
            $this->em->flush();
300
301
            Display::addFlash(
302
                Display::return_message(get_lang('CategoryAdded'), 'success')
303
            );
304
305
            header("Location: {$this->baseUrl}action=list_categories");
306
            exit;
307
        }
308
309
        $interbreadcrumb[] = [
310
            'name' => get_lang('Portfolio'),
311
            'url' => $this->baseUrl,
312
        ];
313
        $interbreadcrumb[] = [
314
            'name' => get_lang('Categories'),
315
            'url' => $this->baseUrl.'action=list_categories',
316
        ];
317
318
        $actions = [];
319
        $actions[] = Display::url(
320
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
321
            $this->baseUrl.'action=list_categories'
322
        );
323
324
        $content = $form->returnForm();
325
326
        $this->renderView($content, get_lang('AddCategory'), $actions);
327
    }
328
329
    /**
330
     * @throws \Doctrine\ORM\ORMException
331
     * @throws \Doctrine\ORM\OptimisticLockException
332
     * @throws \Exception
333
     */
334
    public function editCategory(PortfolioCategory $category)
335
    {
336
        global $interbreadcrumb;
337
338
        if (!api_is_platform_admin()) {
339
            api_not_allowed(true);
340
        }
341
342
        Display::addFlash(
343
            Display::return_message(get_lang('PortfolioCategoryFieldHelp'), 'info')
344
        );
345
346
        $form = new FormValidator(
347
            'edit_category',
348
            'post',
349
            $this->baseUrl."action=edit_category&id={$category->getId()}"
350
        );
351
352
        if (api_get_configuration_value('save_titles_as_html')) {
353
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
354
        } else {
355
            $translateUrl = $this->baseUrl.'action=translate_category&id='.$category->getId();
356
            $translateButton = Display::toolbarButton(get_lang('TranslateThisTerm'), $translateUrl, 'language', 'link');
357
            $form->addText(
358
                'title',
359
                [get_lang('Title'), $translateButton]
360
            );
361
            $form->applyFilter('title', 'trim');
362
        }
363
364
        $form->addHtmlEditor('description', get_lang('Description'), false, false, ['ToolbarSet' => 'Minimal']);
365
        $form->addButtonUpdate(get_lang('Update'));
366
        $form->setDefaults(
367
            [
368
                'title' => $category->getTitle(),
369
                'description' => $category->getDescription(),
370
            ]
371
        );
372
373
        if ($form->validate()) {
374
            $values = $form->exportValues();
375
376
            $category
377
                ->setTitle($values['title'])
378
                ->setDescription($values['description']);
379
380
            $this->em->persist($category);
381
            $this->em->flush();
382
383
            Display::addFlash(
384
                Display::return_message(get_lang('Updated'), 'success')
385
            );
386
387
            header("Location: {$this->baseUrl}action=list_categories&parent_id=".$category->getParentId());
388
            exit;
389
        }
390
391
        $interbreadcrumb[] = [
392
            'name' => get_lang('Portfolio'),
393
            'url' => $this->baseUrl,
394
        ];
395
        $interbreadcrumb[] = [
396
            'name' => get_lang('Categories'),
397
            'url' => $this->baseUrl.'action=list_categories',
398
        ];
399
        if ($category->getParentId() > 0) {
400
            $em = Database::getManager();
401
            $parentCategory = $em->find('ChamiloCoreBundle:PortfolioCategory', $category->getParentId());
402
            $pageTitle = $parentCategory->getTitle().' : '.get_lang('SubCategories');
403
            $interbreadcrumb[] = [
404
                'name' => Security::remove_XSS($pageTitle),
405
                'url' => $this->baseUrl.'action=list_categories&parent_id='.$category->getParentId(),
406
            ];
407
        }
408
409
        $actions = [];
410
        $actions[] = Display::url(
411
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
412
            $this->baseUrl.'action=list_categories&parent_id='.$category->getParentId()
413
        );
414
415
        $content = $form->returnForm();
416
417
        $this->renderView($content, get_lang('EditCategory'), $actions);
418
    }
419
420
    /**
421
     * @throws \Doctrine\ORM\ORMException
422
     * @throws \Doctrine\ORM\OptimisticLockException
423
     */
424
    public function showHideCategory(PortfolioCategory $category)
425
    {
426
        if (!$this->categoryBelongToOwner($category)) {
427
            api_not_allowed(true);
428
        }
429
430
        $category->setIsVisible(!$category->isVisible());
431
432
        $this->em->persist($category);
433
        $this->em->flush();
434
435
        Display::addFlash(
436
            Display::return_message(get_lang('VisibilityChanged'), 'success')
437
        );
438
439
        header("Location: {$this->baseUrl}action=list_categories");
440
        exit;
441
    }
442
443
    /**
444
     * @throws \Doctrine\ORM\ORMException
445
     * @throws \Doctrine\ORM\OptimisticLockException
446
     */
447
    public function deleteCategory(PortfolioCategory $category)
448
    {
449
        if (!api_is_platform_admin()) {
450
            api_not_allowed(true);
451
        }
452
453
        $this->em->remove($category);
454
        $this->em->flush();
455
456
        Display::addFlash(
457
            Display::return_message(get_lang('CategoryDeleted'), 'success')
458
        );
459
460
        header("Location: {$this->baseUrl}action=list_categories");
461
        exit;
462
    }
463
464
    /**
465
     * @throws \Doctrine\ORM\ORMException
466
     * @throws \Doctrine\ORM\OptimisticLockException
467
     * @throws \Doctrine\ORM\TransactionRequiredException
468
     * @throws \Exception
469
     */
470
    public function addItem()
471
    {
472
        global $interbreadcrumb;
473
474
        $this->blockIsNotAllowed();
475
476
        $templates = $this->em
477
            ->getRepository(Portfolio::class)
478
            ->findBy(
479
                [
480
                    'isTemplate' => true,
481
                    'course' => $this->course,
482
                    'session' => $this->session,
483
                    'user' => $this->owner,
484
                ]
485
            );
486
487
        $form = new FormValidator('add_portfolio', 'post', $this->baseUrl.'action=add_item');
488
        $form->addSelectFromCollection(
489
            'template',
490
            [
491
                get_lang('Template'),
492
                null,
493
                '<span id="portfolio-spinner" class="fa fa-fw fa-spinner fa-spin" style="display: none;"
494
                    aria-hidden="true" aria-label="'.get_lang('Loading').'"></span>',
495
            ],
496
            $templates,
497
            [],
498
            true,
499
            'getTitle'
500
        );
501
502
        if (api_get_configuration_value('save_titles_as_html')) {
503
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
504
        } else {
505
            $form->addText('title', get_lang('Title'));
506
            $form->applyFilter('title', 'trim');
507
        }
508
        $editorConfig = [
509
            'ToolbarSet' => 'Documents',
510
            'Width' => '100%',
511
            'Height' => '400',
512
            'cols-size' => [2, 10, 0],
513
        ];
514
        $form->addHtmlEditor('content', get_lang('Content'), true, false, $editorConfig);
515
516
        $categoriesSelect = $form->addSelect(
517
            'category',
518
            [get_lang('Category'), get_lang('PortfolioCategoryFieldHelp')]
519
        );
520
        $categoriesSelect->addOption(get_lang('SelectACategory'), 0);
521
        $parentCategories = $this->getCategoriesForIndex(null, 0);
522
        foreach ($parentCategories as $parentCategory) {
523
            $categoriesSelect->addOption($this->translateDisplayName($parentCategory->getTitle()), $parentCategory->getId());
524
            $subCategories = $this->getCategoriesForIndex(null, $parentCategory->getId());
525
            if (count($subCategories) > 0) {
526
                foreach ($subCategories as $subCategory) {
527
                    $categoriesSelect->addOption(' &mdash; '.$this->translateDisplayName($subCategory->getTitle()), $subCategory->getId());
528
                }
529
            }
530
        }
531
532
        $extraField = new ExtraField('portfolio');
533
        $extra = $extraField->addElements(
534
            $form,
535
            0,
536
            $this->course ? [] : ['tags']
537
        );
538
539
        $this->addAttachmentsFieldToForm($form);
540
541
        $form->addButtonCreate(get_lang('Create'));
542
543
        if ($form->validate()) {
544
            $values = $form->exportValues();
545
            $currentTime = new DateTime(
546
                api_get_utc_datetime(),
547
                new DateTimeZone('UTC')
548
            );
549
550
            $portfolio = new Portfolio();
551
            $portfolio
552
                ->setTitle($values['title'])
553
                ->setContent($values['content'])
554
                ->setUser($this->owner)
555
                ->setCourse($this->course)
556
                ->setSession($this->session)
557
                ->setCategory(
558
                    $this->em->find('ChamiloCoreBundle:PortfolioCategory', $values['category'])
559
                )
560
                ->setCreationDate($currentTime)
561
                ->setUpdateDate($currentTime);
562
563
            $this->em->persist($portfolio);
564
            $this->em->flush();
565
566
            $values['item_id'] = $portfolio->getId();
567
568
            $extraFieldValue = new ExtraFieldValue('portfolio');
569
            $extraFieldValue->saveFieldValues($values);
570
571
            $this->processAttachments(
572
                $form,
573
                $portfolio->getUser(),
574
                $portfolio->getId(),
575
                PortfolioAttachment::TYPE_ITEM
576
            );
577
578
            $hook = HookPortfolioItemAdded::create();
579
            $hook->setEventData(['portfolio' => $portfolio]);
580
            $hook->notifyItemAdded();
581
582
            if (1 == api_get_course_setting('email_alert_teachers_new_post')) {
583
                if ($this->session) {
584
                    $messageCourseTitle = "{$this->course->getTitle()} ({$this->session->getName()})";
585
586
                    $teachers = SessionManager::getCoachesByCourseSession(
587
                        $this->session->getId(),
588
                        $this->course->getId()
589
                    );
590
                    $userIdListToSend = array_values($teachers);
591
                } else {
592
                    $messageCourseTitle = $this->course->getTitle();
593
594
                    $teachers = CourseManager::get_teacher_list_from_course_code($this->course->getCode());
595
596
                    $userIdListToSend = array_keys($teachers);
597
                }
598
599
                $messageSubject = sprintf(get_lang('PortfolioAlertNewPostSubject'), $messageCourseTitle);
600
                $messageContent = sprintf(
601
                    get_lang('PortfolioAlertNewPostContent'),
602
                    $this->owner->getCompleteName(),
603
                    $messageCourseTitle,
604
                    $this->baseUrl.http_build_query(['action' => 'view', 'id' => $portfolio->getId()])
605
                );
606
                $messageContent .= '<br><br><dl>'
607
                    .'<dt>'.Security::remove_XSS($portfolio->getTitle()).'</dt>'
608
                    .'<dd>'.$portfolio->getExcerpt().'</dd>'.'</dl>';
609
610
                foreach ($userIdListToSend as $userIdToSend) {
611
                    MessageManager::send_message_simple(
612
                        $userIdToSend,
613
                        $messageSubject,
614
                        $messageContent,
615
                        0,
616
                        false,
617
                        false,
618
                        [],
619
                        false
620
                    );
621
                }
622
            }
623
624
            Display::addFlash(
625
                Display::return_message(get_lang('PortfolioItemAdded'), 'success')
626
            );
627
628
            header("Location: $this->baseUrl");
629
            exit;
630
        }
631
632
        $interbreadcrumb[] = [
633
            'name' => get_lang('Portfolio'),
634
            'url' => $this->baseUrl,
635
        ];
636
637
        $actions = [];
638
        $actions[] = Display::url(
639
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
640
            $this->baseUrl
641
        );
642
        $actions[] = '<a id="hide_bar_template" href="#" role="button">'.
643
            Display::return_icon('expand.png', get_lang('Expand'), ['id' => 'expand'], ICON_SIZE_MEDIUM).
644
            Display::return_icon('contract.png', get_lang('Collapse'), ['id' => 'contract', 'class' => 'hide'], ICON_SIZE_MEDIUM).'</a>';
645
646
        $js = '<script>
647
            $(function() {
648
                $(".scrollbar-light").scrollbar();
649
                $(".scroll-wrapper").css("height", "550px");
650
                expandColumnToogle("#hide_bar_template", {
651
                    selector: "#template_col",
652
                    width: 3
653
                }, {
654
                    selector: "#doc_form",
655
                    width: 9
656
                });
657
                CKEDITOR.on("instanceReady", function (e) {
658
                    showTemplates();
659
                });
660
                $(window).on("load", function () {
661
                    $("input[name=\'title\']").focus();
662
                });
663
                $(\'#add_portfolio_template\').on(\'change\', function () {
664
                    $(\'#portfolio-spinner\').show();
665
666
                    $.getJSON(_p.web_ajax + \'portfolio.ajax.php?a=find_template&item=\' + this.value)
667
                        .done(function(response) {
668
                            if (CKEDITOR.instances.title) {
669
                                CKEDITOR.instances.title.setData(response.title);
670
                            } else {
671
                                document.getElementById(\'add_portfolio_title\').value = response.title;
672
                            }
673
674
                            CKEDITOR.instances.content.setData(response.content);
675
                        })
676
                        .fail(function () {
677
                            if (CKEDITOR.instances.title) {
678
                                CKEDITOR.instances.title.setData(\'\');
679
                            } else {
680
                                document.getElementById(\'add_portfolio_title\').value = \'\';
681
                            }
682
683
                            CKEDITOR.instances.content.setData(\'\');
684
                        })
685
                        .always(function() {
686
                          $(\'#portfolio-spinner\').hide();
687
                        });
688
                });
689
                '.$extra['jquery_ready_content'].'
690
            });
691
        </script>';
692
        $content = '<div class="page-create">
693
            <div class="row" style="overflow:hidden">
694
            <div id="template_col" class="col-md-3">
695
                <div class="panel panel-default">
696
                <div class="panel-body">
697
                    <div id="frmModel" class="items-templates scrollbar-light"></div>
698
                </div>
699
                </div>
700
            </div>
701
            <div id="doc_form" class="col-md-9">
702
                '.$form->returnForm().'
703
            </div>
704
          </div></div>';
705
706
        $this->renderView(
707
            $content.$js,
708
            get_lang('AddPortfolioItem'),
709
            $actions
710
        );
711
    }
712
713
    /**
714
     * @throws \Doctrine\ORM\ORMException
715
     * @throws \Doctrine\ORM\OptimisticLockException
716
     * @throws \Doctrine\ORM\TransactionRequiredException
717
     * @throws \Exception
718
     */
719
    public function editItem(Portfolio $item)
720
    {
721
        global $interbreadcrumb;
722
723
        if (!api_is_allowed_to_edit() && !$this->itemBelongToOwner($item)) {
724
            api_not_allowed(true);
725
        }
726
727
        $itemCourse = $item->getCourse();
728
        $itemSession = $item->getSession();
729
730
        $form = new FormValidator('edit_portfolio', 'post', $this->baseUrl."action=edit_item&id={$item->getId()}");
731
732
        if (api_get_configuration_value('save_titles_as_html')) {
733
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
734
        } else {
735
            $form->addText('title', get_lang('Title'));
736
            $form->applyFilter('title', 'trim');
737
        }
738
739
        if ($item->getOrigin()) {
740
            if (Portfolio::TYPE_ITEM === $item->getOriginType()) {
741
                $origin = $this->em->find(Portfolio::class, $item->getOrigin());
742
743
                $form->addLabel(
744
                    sprintf(get_lang('PortfolioItemFromXUser'), $origin->getUser()->getCompleteName()),
745
                    Display::panel(
746
                        Security::remove_XSS($origin->getContent())
747
                    )
748
                );
749
            } elseif (Portfolio::TYPE_COMMENT === $item->getOriginType()) {
750
                $origin = $this->em->find(PortfolioComment::class, $item->getOrigin());
751
752
                $form->addLabel(
753
                    sprintf(get_lang('PortfolioCommentFromXUser'), $origin->getAuthor()->getCompleteName()),
754
                    Display::panel(
755
                        Security::remove_XSS($origin->getContent())
756
                    )
757
                );
758
            }
759
        }
760
        $editorConfig = [
761
            'ToolbarSet' => 'Documents',
762
            'Width' => '100%',
763
            'Height' => '400',
764
            'cols-size' => [2, 10, 0],
765
        ];
766
        $form->addHtmlEditor('content', get_lang('Content'), true, false, $editorConfig);
767
        $categoriesSelect = $form->addSelect(
768
            'category',
769
            [get_lang('Category'), get_lang('PortfolioCategoryFieldHelp')]
770
        );
771
        $categoriesSelect->addOption(get_lang('SelectACategory'), 0);
772
        $parentCategories = $this->getCategoriesForIndex(null, 0);
773
        foreach ($parentCategories as $parentCategory) {
774
            $categoriesSelect->addOption($this->translateDisplayName($parentCategory->getTitle()), $parentCategory->getId());
775
            $subCategories = $this->getCategoriesForIndex(null, $parentCategory->getId());
776
            if (count($subCategories) > 0) {
777
                foreach ($subCategories as $subCategory) {
778
                    $categoriesSelect->addOption(' &mdash; '.$this->translateDisplayName($subCategory->getTitle()), $subCategory->getId());
779
                }
780
            }
781
        }
782
783
        $extraField = new ExtraField('portfolio');
784
        $extra = $extraField->addElements(
785
            $form,
786
            $item->getId(),
787
            $this->course ? [] : ['tags']
788
        );
789
790
        $attachmentList = $this->generateAttachmentList($item, false);
791
792
        if (!empty($attachmentList)) {
793
            $form->addLabel(get_lang('AttachmentFiles'), $attachmentList);
794
        }
795
796
        $this->addAttachmentsFieldToForm($form);
797
798
        $form->addButtonUpdate(get_lang('Update'));
799
        $form->setDefaults(
800
            [
801
                'title' => $item->getTitle(),
802
                'content' => $item->getContent(),
803
                'category' => $item->getCategory() ? $item->getCategory()->getId() : '',
804
            ]
805
        );
806
807
        if ($form->validate()) {
808
            if ($itemCourse) {
809
                api_item_property_update(
810
                    api_get_course_info($itemCourse->getCode()),
811
                    TOOL_PORTFOLIO,
812
                    $item->getId(),
813
                    'PortfolioUpdated',
814
                    api_get_user_id(),
815
                    [],
816
                    null,
817
                    '',
818
                    '',
819
                    $itemSession ? $itemSession->getId() : 0
820
                );
821
            }
822
823
            $values = $form->exportValues();
824
            $currentTime = new DateTime(api_get_utc_datetime(), new DateTimeZone('UTC'));
825
826
            $item
827
                ->setTitle($values['title'])
828
                ->setContent($values['content'])
829
                ->setUpdateDate($currentTime)
830
                ->setCategory(
831
                    $this->em->find('ChamiloCoreBundle:PortfolioCategory', $values['category'])
832
                );
833
834
            $values['item_id'] = $item->getId();
835
836
            $extraFieldValue = new ExtraFieldValue('portfolio');
837
            $extraFieldValue->saveFieldValues($values);
838
839
            $this->em->persist($item);
840
            $this->em->flush();
841
842
            HookPortfolioItemEdited::create()
843
                ->setEventData(['item' => $item])
844
                ->notifyItemEdited()
845
            ;
846
847
            $this->processAttachments(
848
                $form,
849
                $item->getUser(),
850
                $item->getId(),
851
                PortfolioAttachment::TYPE_ITEM
852
            );
853
854
            Display::addFlash(
855
                Display::return_message(get_lang('ItemUpdated'), 'success')
856
            );
857
858
            header("Location: $this->baseUrl");
859
            exit;
860
        }
861
862
        $interbreadcrumb[] = [
863
            'name' => get_lang('Portfolio'),
864
            'url' => $this->baseUrl,
865
        ];
866
        $actions = [];
867
        $actions[] = Display::url(
868
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
869
            $this->baseUrl
870
        );
871
        $actions[] = '<a id="hide_bar_template" href="#" role="button">'.
872
            Display::return_icon('expand.png', get_lang('Expand'), ['id' => 'expand'], ICON_SIZE_MEDIUM).
873
            Display::return_icon('contract.png', get_lang('Collapse'), ['id' => 'contract', 'class' => 'hide'], ICON_SIZE_MEDIUM).'</a>';
874
875
        $js = '<script>
876
            $(function() {
877
                $(".scrollbar-light").scrollbar();
878
                $(".scroll-wrapper").css("height", "550px");
879
                expandColumnToogle("#hide_bar_template", {
880
                    selector: "#template_col",
881
                    width: 3
882
                }, {
883
                    selector: "#doc_form",
884
                    width: 9
885
                });
886
                CKEDITOR.on("instanceReady", function (e) {
887
                    showTemplates();
888
                });
889
                $(window).on("load", function () {
890
                    $("input[name=\'title\']").focus();
891
                });
892
                '.$extra['jquery_ready_content'].'
893
            });
894
        </script>';
895
        $content = '<div class="page-create">
896
            <div class="row" style="overflow:hidden">
897
            <div id="template_col" class="col-md-3">
898
                <div class="panel panel-default">
899
                <div class="panel-body">
900
                    <div id="frmModel" class="items-templates scrollbar-light"></div>
901
                </div>
902
                </div>
903
            </div>
904
            <div id="doc_form" class="col-md-9">
905
                '.$form->returnForm().'
906
            </div>
907
          </div></div>';
908
909
        $this->renderView(
910
            $content.$js,
911
            get_lang('EditPortfolioItem'),
912
            $actions
913
        );
914
    }
915
916
    /**
917
     * @throws \Doctrine\ORM\ORMException
918
     * @throws \Doctrine\ORM\OptimisticLockException
919
     */
920
    public function showHideItem(Portfolio $item)
921
    {
922
        if (!$this->itemBelongToOwner($item)) {
923
            api_not_allowed(true);
924
        }
925
926
        switch ($item->getVisibility()) {
927
            case Portfolio::VISIBILITY_HIDDEN:
928
                $item->setVisibility(Portfolio::VISIBILITY_VISIBLE);
929
                break;
930
            case Portfolio::VISIBILITY_VISIBLE:
931
                $item->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER);
932
                break;
933
            case Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER:
934
            default:
935
                $item->setVisibility(Portfolio::VISIBILITY_HIDDEN);
936
                break;
937
        }
938
939
        $this->em->persist($item);
940
        $this->em->flush();
941
942
        Display::addFlash(
943
            Display::return_message(get_lang('VisibilityChanged'), 'success')
944
        );
945
946
        header("Location: $this->baseUrl");
947
        exit;
948
    }
949
950
    /**
951
     * @throws \Doctrine\ORM\ORMException
952
     * @throws \Doctrine\ORM\OptimisticLockException
953
     */
954
    public function deleteItem(Portfolio $item)
955
    {
956
        if (!$this->itemBelongToOwner($item)) {
957
            api_not_allowed(true);
958
        }
959
960
        HookPortfolioItemDeleted::create()
961
            ->setEventData(['item' => $item])
962
            ->notifyItemDeleted()
963
        ;
964
965
        $this->em->remove($item);
966
        $this->em->flush();
967
968
        Display::addFlash(
969
            Display::return_message(get_lang('ItemDeleted'), 'success')
970
        );
971
972
        header("Location: $this->baseUrl");
973
        exit;
974
    }
975
976
    /**
977
     * @throws \Exception
978
     */
979
    public function index(HttpRequest $httpRequest)
980
    {
981
        $listByUser = false;
982
        $listHighlighted = $httpRequest->query->has('list_highlighted');
983
        $listAlphabetical = $httpRequest->query->has('list_alphabetical');
984
985
        if ($httpRequest->query->has('user')) {
986
            $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
987
988
            if (empty($this->owner)) {
989
                api_not_allowed(true);
990
            }
991
992
            $listByUser = true;
993
        }
994
995
        $currentUserId = api_get_user_id();
996
997
        $actions = [];
998
999
        if (api_is_platform_admin()) {
1000
            $actions[] = Display::url(
1001
                Display::return_icon('add.png', get_lang('Add'), [], ICON_SIZE_MEDIUM),
1002
                $this->baseUrl.'action=add_item'
1003
            );
1004
            $actions[] = Display::url(
1005
                Display::return_icon('folder.png', get_lang('AddCategory'), [], ICON_SIZE_MEDIUM),
1006
                $this->baseUrl.'action=list_categories'
1007
            );
1008
            $actions[] = Display::url(
1009
                Display::return_icon('waiting_list.png', get_lang('PortfolioDetails'), [], ICON_SIZE_MEDIUM),
1010
                $this->baseUrl.'action=details'
1011
            );
1012
        } else {
1013
            if ($currentUserId == $this->owner->getId()) {
1014
                if ($this->isAllowed()) {
1015
                    $actions[] = Display::url(
1016
                        Display::return_icon('add.png', get_lang('Add'), [], ICON_SIZE_MEDIUM),
1017
                        $this->baseUrl.'action=add_item'
1018
                    );
1019
                    $actions[] = Display::url(
1020
                        Display::return_icon('waiting_list.png', get_lang('PortfolioDetails'), [], ICON_SIZE_MEDIUM),
1021
                        $this->baseUrl.'action=details'
1022
                    );
1023
                }
1024
            } else {
1025
                $actions[] = Display::url(
1026
                    Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
1027
                    $this->baseUrl
1028
                );
1029
            }
1030
        }
1031
1032
        if (api_is_allowed_to_edit()) {
1033
            $actions[] = Display::url(
1034
                Display::return_icon('tickets.png', get_lang('Tags'), [], ICON_SIZE_MEDIUM),
1035
                $this->baseUrl.'action=tags'
1036
            );
1037
        }
1038
1039
        $frmStudentList = null;
1040
        $frmTagList = null;
1041
1042
        $categories = [];
1043
        $portfolio = [];
1044
        if ($this->course) {
1045
            $frmTagList = $this->createFormTagFilter($listByUser);
1046
            $frmStudentList = $this->createFormStudentFilter($listByUser, $listHighlighted, $listAlphabetical);
1047
            $frmStudentList->setDefaults(['user' => $this->owner->getId()]);
1048
            // it translates the category title with the current user language
1049
            $categories = $this->getCategoriesForIndex(null, 0);
1050
            if (count($categories) > 0) {
1051
                foreach ($categories as &$category) {
1052
                    $translated = $this->translateDisplayName($category->getTitle());
1053
                    $category->setTitle($translated);
1054
                }
1055
            }
1056
        } else {
1057
            // it displays the list in Network Social for the current user
1058
            $portfolio = $this->getCategoriesForIndex();
1059
        }
1060
1061
        $foundComments = [];
1062
1063
        if ($listHighlighted) {
1064
            $items = $this->getHighlightedItems();
1065
        } else {
1066
            $items = $this->getItemsForIndex($listByUser, $frmTagList, $listAlphabetical);
1067
1068
            $foundComments = $this->getCommentsForIndex($frmTagList);
1069
        }
1070
1071
        // it gets and translate the sub-categories
1072
        $categoryId = $httpRequest->query->getInt('categoryId');
1073
        $subCategoryIdsReq = isset($_REQUEST['subCategoryIds']) ? Security::remove_XSS($_REQUEST['subCategoryIds']) : '';
1074
        $subCategoryIds = $subCategoryIdsReq;
1075
        if ('all' !== $subCategoryIdsReq) {
1076
            $subCategoryIds = !empty($subCategoryIdsReq) ? explode(',', $subCategoryIdsReq) : [];
1077
        }
1078
        $subcategories = [];
1079
        if ($categoryId > 0) {
1080
            $subcategories = $this->getCategoriesForIndex(null, $categoryId);
1081
            if (count($subcategories) > 0) {
1082
                foreach ($subcategories as &$subcategory) {
1083
                    $translated = $this->translateDisplayName($subcategory->getTitle());
1084
                    $subcategory->setTitle($translated);
1085
                }
1086
            }
1087
        }
1088
1089
        $template = new Template(null, false, false, false, false, false, false);
1090
        $template->assign('user', $this->owner);
1091
        $template->assign('listByUser', $listByUser);
1092
        $template->assign('course', $this->course);
1093
        $template->assign('session', $this->session);
1094
        $template->assign('portfolio', $portfolio);
1095
        $template->assign('categories', $categories);
1096
        $template->assign('uncategorized_items', $items);
1097
        $template->assign('frm_student_list', $this->course ? $frmStudentList->returnForm() : '');
1098
        $template->assign('frm_tag_list', $this->course ? $frmTagList->returnForm() : '');
1099
        $template->assign('category_id', $categoryId);
1100
        $template->assign('subcategories', $subcategories);
1101
        $template->assign('subcategory_ids', $subCategoryIds);
1102
        $template->assign('found_comments', $foundComments);
1103
1104
        $js = '<script>
1105
            $(function() {
1106
                $(".category-filters").bind("click", function() {
1107
                    var categoryId = parseInt($(this).find("input[type=\'radio\']").val());
1108
                    $("input[name=\'categoryId\']").val(categoryId);
1109
                    $("input[name=\'subCategoryIds\']").val("all");
1110
                    $("#frm_tag_list_submit").trigger("click");
1111
                });
1112
                $(".subcategory-filters").bind("click", function() {
1113
                    var checkedVals = $(".subcategory-filters:checkbox:checked").map(function() {
1114
                        return this.value;
1115
                    }).get();
1116
                    $("input[name=\'subCategoryIds\']").val(checkedVals.join(","));
1117
                    $("#frm_tag_list_submit").trigger("click");
1118
                });
1119
            });
1120
        </script>';
1121
        $template->assign('js_script', $js);
1122
        $layout = $template->get_template('portfolio/list.html.twig');
1123
1124
        Display::addFlash(
1125
            Display::return_message(get_lang('PortfolioPostAddHelp'), 'info', false)
1126
        );
1127
1128
        $content = $template->fetch($layout);
1129
1130
        $this->renderView($content, get_lang('Portfolio'), $actions);
1131
    }
1132
1133
    /**
1134
     * @throws \Doctrine\ORM\ORMException
1135
     * @throws \Doctrine\ORM\OptimisticLockException
1136
     * @throws \Doctrine\ORM\TransactionRequiredException
1137
     */
1138
    public function view(Portfolio $item, $urlUser)
1139
    {
1140
        global $interbreadcrumb;
1141
1142
        if (!$this->itemBelongToOwner($item)) {
1143
            if ($this->advancedSharingEnabled) {
1144
                $courseInfo = api_get_course_info_by_id($this->course->getId());
1145
                $sessionId = $this->session ? $this->session->getId() : 0;
1146
1147
                $itemPropertyVisiblity = api_get_item_visibility(
1148
                    $courseInfo,
1149
                    TOOL_PORTFOLIO,
1150
                    $item->getId(),
1151
                    $sessionId,
1152
                    $this->owner->getId(),
1153
                    'visible'
1154
                );
1155
1156
                if ($item->getVisibility() === Portfolio::VISIBILITY_PER_USER && 1 !== $itemPropertyVisiblity) {
1157
                    api_not_allowed(true);
1158
                }
1159
            } elseif ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN
1160
                || ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER && !api_is_allowed_to_edit())
1161
            ) {
1162
                api_not_allowed(true);
1163
            }
1164
        }
1165
1166
        HookPortfolioItemViewed::create()
1167
            ->setEventData(['portfolio' => $item])
1168
            ->notifyItemViewed()
1169
        ;
1170
1171
        $itemCourse = $item->getCourse();
1172
        $itemSession = $item->getSession();
1173
1174
        $form = $this->createCommentForm($item);
1175
1176
        $commentsRepo = $this->em->getRepository(PortfolioComment::class);
1177
1178
        $commentsQueryBuilder = $commentsRepo->createQueryBuilder('comment');
1179
        $commentsQueryBuilder->where('comment.item = :item');
1180
1181
        if ($this->advancedSharingEnabled) {
1182
            $commentsQueryBuilder
1183
                ->leftJoin(
1184
                    CItemProperty::class,
1185
                    'cip',
1186
                    Join::WITH,
1187
                    "cip.ref = comment.id
1188
                        AND cip.tool = :cip_tool
1189
                        AND cip.course = :course
1190
                        AND cip.lasteditType = 'visible'
1191
                        AND cip.toUser = :current_user"
1192
                )
1193
                ->andWhere(
1194
                    sprintf(
1195
                        'comment.visibility = %d
1196
                            OR (
1197
                                comment.visibility = %d AND cip IS NOT NULL OR comment.author = :current_user
1198
                            )',
1199
                        PortfolioComment::VISIBILITY_VISIBLE,
1200
                        PortfolioComment::VISIBILITY_PER_USER
1201
                    )
1202
                )
1203
                ->setParameter('cip_tool', TOOL_PORTFOLIO_COMMENT)
1204
                ->setParameter('current_user', $this->owner->getId())
1205
                ->setParameter('course', $item->getCourse())
1206
            ;
1207
        }
1208
1209
        if (true === api_get_configuration_value('portfolio_show_base_course_post_in_sessions')
1210
            && $this->session && !$item->getSession() && !$item->isDuplicatedInSession($this->session)
1211
        ) {
1212
            $comments = [];
1213
        } else {
1214
            $comments = $commentsQueryBuilder
1215
                ->orderBy('comment.root, comment.lft', 'ASC')
1216
                ->setParameter('item', $item)
1217
                ->getQuery()
1218
                ->getArrayResult()
1219
            ;
1220
        }
1221
1222
        $clockIcon = Display::returnFontAwesomeIcon('clock-o', '', true);
1223
1224
        $commentsHtml = $commentsRepo->buildTree(
1225
            $comments,
1226
            [
1227
                'decorate' => true,
1228
                'rootOpen' => '<div class="media-list">',
1229
                'rootClose' => '</div>',
1230
                'childOpen' => function ($node) use ($commentsRepo) {
1231
                    /** @var PortfolioComment $comment */
1232
                    $comment = $commentsRepo->find($node['id']);
1233
                    $author = $comment->getAuthor();
1234
1235
                    $userPicture = UserManager::getUserPicture(
1236
                        $comment->getAuthor()->getId(),
1237
                        USER_IMAGE_SIZE_SMALL,
1238
                        null,
1239
                        [
1240
                            'picture_uri' => $author->getPictureUri(),
1241
                            'email' => $author->getEmail(),
1242
                        ]
1243
                    );
1244
1245
                    return '<article class="media" id="comment-'.$node['id'].'">
1246
                        <div class="media-left"><img class="media-object thumbnail" src="'.$userPicture.'" alt="'
1247
                        .$author->getCompleteName().'"></div>
1248
                        <div class="media-body">';
1249
                },
1250
                'childClose' => '</div></article>',
1251
                'nodeDecorator' => function ($node) use ($commentsRepo, $clockIcon, $item) {
1252
                    $commentActions = [];
1253
                    /** @var PortfolioComment $comment */
1254
                    $comment = $commentsRepo->find($node['id']);
1255
1256
                    if ($this->commentBelongsToOwner($comment)) {
1257
                        $commentActions[] = Display::url(
1258
                            Display::return_icon(
1259
                                $comment->isTemplate() ? 'wizard.png' : 'wizard_na.png',
1260
                                $comment->isTemplate() ? get_lang('RemoveAsTemplate') : get_lang('AddAsTemplate')
1261
                            ),
1262
                            $this->baseUrl.http_build_query(['action' => 'template_comment', 'id' => $comment->getId()])
1263
                        );
1264
                    }
1265
1266
                    $commentActions[] = Display::url(
1267
                        Display::return_icon('discuss.png', get_lang('ReplyToThisComment')),
1268
                        '#',
1269
                        [
1270
                            'data-comment' => htmlspecialchars(
1271
                                json_encode(['id' => $comment->getId()])
1272
                            ),
1273
                            'role' => 'button',
1274
                            'class' => 'btn-reply-to',
1275
                        ]
1276
                    );
1277
                    $commentActions[] = Display::url(
1278
                        Display::return_icon('copy.png', get_lang('CopyToMyPortfolio')),
1279
                        $this->baseUrl.http_build_query(
1280
                            [
1281
                                'action' => 'copy',
1282
                                'copy' => 'comment',
1283
                                'id' => $comment->getId(),
1284
                            ]
1285
                        )
1286
                    );
1287
1288
                    $isAllowedToEdit = api_is_allowed_to_edit();
1289
1290
                    if ($isAllowedToEdit) {
1291
                        $commentActions[] = Display::url(
1292
                            Display::return_icon('copy.png', get_lang('CopyToStudentPortfolio')),
1293
                            $this->baseUrl.http_build_query(
1294
                                [
1295
                                    'action' => 'teacher_copy',
1296
                                    'copy' => 'comment',
1297
                                    'id' => $comment->getId(),
1298
                                ]
1299
                            )
1300
                        );
1301
1302
                        if ($comment->isImportant()) {
1303
                            $commentActions[] = Display::url(
1304
                                Display::return_icon('drawing-pin.png', get_lang('UnmarkCommentAsImportant')),
1305
                                $this->baseUrl.http_build_query(
1306
                                    [
1307
                                        'action' => 'mark_important',
1308
                                        'item' => $item->getId(),
1309
                                        'id' => $comment->getId(),
1310
                                    ]
1311
                                )
1312
                            );
1313
                        } else {
1314
                            $commentActions[] = Display::url(
1315
                                Display::return_icon('drawing-pin.png', get_lang('MarkCommentAsImportant')),
1316
                                $this->baseUrl.http_build_query(
1317
                                    [
1318
                                        'action' => 'mark_important',
1319
                                        'item' => $item->getId(),
1320
                                        'id' => $comment->getId(),
1321
                                    ]
1322
                                )
1323
                            );
1324
                        }
1325
1326
                        if ($this->course && '1' === api_get_course_setting('qualify_portfolio_comment')) {
1327
                            $commentActions[] = Display::url(
1328
                                Display::return_icon('quiz.png', get_lang('QualifyThisPortfolioComment')),
1329
                                $this->baseUrl.http_build_query(
1330
                                    [
1331
                                        'action' => 'qualify',
1332
                                        'comment' => $comment->getId(),
1333
                                    ]
1334
                                )
1335
                            );
1336
                        }
1337
                    }
1338
1339
                    if ($this->commentBelongsToOwner($comment)) {
1340
                        if ($this->advancedSharingEnabled) {
1341
                            $commentActions[] = Display::url(
1342
                                Display::return_icon('visible.png', get_lang('ChooseRecipients')),
1343
                                $this->baseUrl.http_build_query(['action' => 'comment_visiblity_choose', 'id' => $comment->getId()])
1344
                            );
1345
                        }
1346
1347
                        $commentActions[] = Display::url(
1348
                            Display::return_icon('edit.png', get_lang('Edit')),
1349
                            $this->baseUrl.http_build_query(['action' => 'edit_comment', 'id' => $comment->getId()])
1350
                        );
1351
                        $commentActions[] = Display::url(
1352
                            Display::return_icon('delete.png', get_lang('Delete')),
1353
                            $this->baseUrl.http_build_query(['action' => 'delete_comment', 'id' => $comment->getId()])
1354
                        );
1355
                    }
1356
1357
                    $nodeHtml = '<div class="pull-right">'.implode(PHP_EOL, $commentActions).'</div>'.PHP_EOL
1358
                        .'<footer class="media-heading h4">'.PHP_EOL
1359
                        .'<p>'.$comment->getAuthor()->getCompleteName().'</p>'.PHP_EOL;
1360
1361
                    if ($comment->isImportant()
1362
                        && ($this->itemBelongToOwner($comment->getItem()) || $isAllowedToEdit)
1363
                    ) {
1364
                        $nodeHtml .= '<span class="pull-right label label-warning origin-style">'
1365
                            .get_lang('CommentMarkedAsImportant')
1366
                            .'</span>'.PHP_EOL;
1367
                    }
1368
1369
                    $nodeHtml .= '<small>'.$clockIcon.PHP_EOL
1370
                        .$this->getLabelForCommentDate($comment).'</small>'.PHP_EOL;
1371
1372
                    $nodeHtml .= '</footer>'.PHP_EOL
1373
                        .Security::remove_XSS($comment->getContent()).PHP_EOL;
1374
1375
                    $nodeHtml .= $this->generateAttachmentList($comment);
1376
1377
                    return $nodeHtml;
1378
                },
1379
            ]
1380
        );
1381
1382
        $template = new Template(null, false, false, false, false, false, false);
1383
        $template->assign('baseurl', $this->baseUrl);
1384
        $template->assign('item', $item);
1385
        $template->assign('item_content', $this->generateItemContent($item));
1386
        $template->assign('count_comments', count($comments));
1387
        $template->assign('comments', $commentsHtml);
1388
        $template->assign('form', $form);
1389
        $template->assign('attachment_list', $this->generateAttachmentList($item));
1390
1391
        if ($itemCourse) {
1392
            $propertyInfo = api_get_item_property_info(
1393
                $itemCourse->getId(),
1394
                TOOL_PORTFOLIO,
1395
                $item->getId(),
1396
                $itemSession ? $itemSession->getId() : 0
1397
            );
1398
1399
            if ($propertyInfo && empty($propertyInfo['to_user_id'])) {
1400
                $template->assign(
1401
                    'last_edit',
1402
                    [
1403
                        'date' => $propertyInfo['lastedit_date'],
1404
                        'user' => api_get_user_entity($propertyInfo['lastedit_user_id'])->getCompleteName(),
1405
                    ]
1406
                );
1407
            }
1408
        }
1409
1410
        $layout = $template->get_template('portfolio/view.html.twig');
1411
        $content = $template->fetch($layout);
1412
1413
        $interbreadcrumb[] = ['name' => get_lang('Portfolio'), 'url' => $this->baseUrl];
1414
1415
        $editLink = Display::url(
1416
            Display::return_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_MEDIUM),
1417
            $this->baseUrl.http_build_query(['action' => 'edit_item', 'id' => $item->getId()])
1418
        );
1419
1420
        $urlUserString = "";
1421
        if (!empty($urlUser)) {
1422
            $urlUserString = "user=".$urlUser;
1423
        }
1424
1425
        $actions = [];
1426
        $actions[] = Display::url(
1427
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
1428
            $this->baseUrl.$urlUserString
1429
        );
1430
1431
        if ($this->itemBelongToOwner($item)) {
1432
            $actions[] = $editLink;
1433
1434
            $actions[] = Display::url(
1435
                Display::return_icon(
1436
                    $item->isTemplate() ? 'wizard.png' : 'wizard_na.png',
1437
                    $item->isTemplate() ? get_lang('RemoveAsTemplate') : get_lang('AddAsTemplate'),
1438
                    [],
1439
                    ICON_SIZE_MEDIUM
1440
                ),
1441
                $this->baseUrl.http_build_query(['action' => 'template', 'id' => $item->getId()])
1442
            );
1443
1444
            if ($this->advancedSharingEnabled) {
1445
                $actions[] = Display::url(
1446
                    Display::return_icon('visible.png', get_lang('ChooseRecipients'), [], ICON_SIZE_MEDIUM),
1447
                    $this->baseUrl.http_build_query(['action' => 'item_visiblity_choose', 'id' => $item->getId()])
1448
                );
1449
            } else {
1450
                $visibilityUrl = $this->baseUrl.http_build_query(['action' => 'visibility', 'id' => $item->getId()]);
1451
1452
                if ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN) {
1453
                    $actions[] = Display::url(
1454
                        Display::return_icon('invisible.png', get_lang('MakeVisible'), [], ICON_SIZE_MEDIUM),
1455
                        $visibilityUrl
1456
                    );
1457
                } elseif ($item->getVisibility() === Portfolio::VISIBILITY_VISIBLE) {
1458
                    $actions[] = Display::url(
1459
                        Display::return_icon('visible.png', get_lang('MakeVisibleForTeachers'), [], ICON_SIZE_MEDIUM),
1460
                        $visibilityUrl
1461
                    );
1462
                } elseif ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER) {
1463
                    $actions[] = Display::url(
1464
                        Display::return_icon('eye-slash.png', get_lang('MakeInvisible'), [], ICON_SIZE_MEDIUM),
1465
                        $visibilityUrl
1466
                    );
1467
                }
1468
            }
1469
1470
            $actions[] = Display::url(
1471
                Display::return_icon('delete.png', get_lang('Delete'), [], ICON_SIZE_MEDIUM),
1472
                $this->baseUrl.http_build_query(['action' => 'delete_item', 'id' => $item->getId()])
1473
            );
1474
        } else {
1475
            $actions[] = Display::url(
1476
                Display::return_icon('copy.png', get_lang('CopyToMyPortfolio'), [], ICON_SIZE_MEDIUM),
1477
                $this->baseUrl.http_build_query(['action' => 'copy', 'copy' => 'item', 'id' => $item->getId()])
1478
            );
1479
        }
1480
1481
        if (api_is_allowed_to_edit()) {
1482
            $actions[] = Display::url(
1483
                Display::return_icon('copy.png', get_lang('CopyToStudentPortfolio'), [], ICON_SIZE_MEDIUM),
1484
                $this->baseUrl.http_build_query(['action' => 'teacher_copy', 'copy' => 'item', 'id' => $item->getId()])
1485
            );
1486
            $actions[] = $editLink;
1487
1488
            $highlightedUrl = $this->baseUrl.http_build_query(['action' => 'highlighted', 'id' => $item->getId()]);
1489
1490
            if ($item->isHighlighted()) {
1491
                $actions[] = Display::url(
1492
                    Display::return_icon('award_red.png', get_lang('UnmarkAsHighlighted'), [], ICON_SIZE_MEDIUM),
1493
                    $highlightedUrl
1494
                );
1495
            } else {
1496
                $actions[] = Display::url(
1497
                    Display::return_icon('award_red_na.png', get_lang('MarkAsHighlighted'), [], ICON_SIZE_MEDIUM),
1498
                    $highlightedUrl
1499
                );
1500
            }
1501
1502
            if ($itemCourse && '1' === api_get_course_setting('qualify_portfolio_item')) {
1503
                $actions[] = Display::url(
1504
                    Display::return_icon('quiz.png', get_lang('QualifyThisPortfolioItem'), [], ICON_SIZE_MEDIUM),
1505
                    $this->baseUrl.http_build_query(['action' => 'qualify', 'item' => $item->getId()])
1506
                );
1507
            }
1508
        }
1509
1510
        $this->renderView($content, $item->getTitle(true), $actions, false);
1511
    }
1512
1513
    /**
1514
     * @throws \Doctrine\ORM\ORMException
1515
     * @throws \Doctrine\ORM\OptimisticLockException
1516
     */
1517
    public function copyItem(Portfolio $originItem)
1518
    {
1519
        $this->blockIsNotAllowed();
1520
1521
        $currentTime = api_get_utc_datetime(null, false, true);
1522
1523
        $portfolio = new Portfolio();
1524
        $portfolio
1525
            ->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER)
1526
            ->setTitle(
1527
                sprintf(get_lang('PortfolioItemFromXUser'), $originItem->getUser()->getCompleteName())
1528
            )
1529
            ->setContent('')
1530
            ->setUser($this->owner)
1531
            ->setOrigin($originItem->getId())
1532
            ->setOriginType(Portfolio::TYPE_ITEM)
1533
            ->setCourse($this->course)
1534
            ->setSession($this->session)
1535
            ->setCreationDate($currentTime)
1536
            ->setUpdateDate($currentTime);
1537
1538
        $this->em->persist($portfolio);
1539
        $this->em->flush();
1540
1541
        Display::addFlash(
1542
            Display::return_message(get_lang('PortfolioItemAdded'), 'success')
1543
        );
1544
1545
        header("Location: $this->baseUrl".http_build_query(['action' => 'edit_item', 'id' => $portfolio->getId()]));
1546
        exit;
1547
    }
1548
1549
    /**
1550
     * @throws \Doctrine\ORM\ORMException
1551
     * @throws \Doctrine\ORM\OptimisticLockException
1552
     */
1553
    public function copyComment(PortfolioComment $originComment)
1554
    {
1555
        $currentTime = api_get_utc_datetime(null, false, true);
1556
1557
        $portfolio = new Portfolio();
1558
        $portfolio
1559
            ->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER)
1560
            ->setTitle(
1561
                sprintf(get_lang('PortfolioCommentFromXUser'), $originComment->getAuthor()->getCompleteName())
1562
            )
1563
            ->setContent('')
1564
            ->setUser($this->owner)
1565
            ->setOrigin($originComment->getId())
1566
            ->setOriginType(Portfolio::TYPE_COMMENT)
1567
            ->setCourse($this->course)
1568
            ->setSession($this->session)
1569
            ->setCreationDate($currentTime)
1570
            ->setUpdateDate($currentTime);
1571
1572
        $this->em->persist($portfolio);
1573
        $this->em->flush();
1574
1575
        Display::addFlash(
1576
            Display::return_message(get_lang('PortfolioItemAdded'), 'success')
1577
        );
1578
1579
        header("Location: $this->baseUrl".http_build_query(['action' => 'edit_item', 'id' => $portfolio->getId()]));
1580
        exit;
1581
    }
1582
1583
    /**
1584
     * @throws \Doctrine\ORM\ORMException
1585
     * @throws \Doctrine\ORM\OptimisticLockException
1586
     * @throws \Exception
1587
     */
1588
    public function teacherCopyItem(Portfolio $originItem)
1589
    {
1590
        api_protect_teacher_script();
1591
1592
        $actionParams = http_build_query(['action' => 'teacher_copy', 'copy' => 'item', 'id' => $originItem->getId()]);
1593
1594
        $form = new FormValidator('teacher_copy_portfolio', 'post', $this->baseUrl.$actionParams);
1595
1596
        if (api_get_configuration_value('save_titles_as_html')) {
1597
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
1598
        } else {
1599
            $form->addText('title', get_lang('Title'));
1600
            $form->applyFilter('title', 'trim');
1601
        }
1602
1603
        $form->addLabel(
1604
            sprintf(get_lang('PortfolioItemFromXUser'), $originItem->getUser()->getCompleteName()),
1605
            Display::panel(
1606
                Security::remove_XSS($originItem->getContent())
1607
            )
1608
        );
1609
        $form->addHtmlEditor('content', get_lang('Content'), true, false, ['ToolbarSet' => 'NotebookStudent']);
1610
1611
        $urlParams = http_build_query(
1612
            [
1613
                'a' => 'search_user_by_course',
1614
                'course_id' => $this->course->getId(),
1615
                'session_id' => $this->session ? $this->session->getId() : 0,
1616
            ]
1617
        );
1618
        $form->addSelectAjax(
1619
            'students',
1620
            get_lang('Students'),
1621
            [],
1622
            [
1623
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1624
                'multiple' => true,
1625
            ]
1626
        );
1627
        $form->addRule('students', get_lang('ThisFieldIsRequired'), 'required');
1628
        $form->addButtonCreate(get_lang('Save'));
1629
1630
        $toolName = get_lang('CopyToStudentPortfolio');
1631
        $content = $form->returnForm();
1632
1633
        if ($form->validate()) {
1634
            $values = $form->exportValues();
1635
1636
            $currentTime = api_get_utc_datetime(null, false, true);
1637
1638
            foreach ($values['students'] as $studentId) {
1639
                $owner = api_get_user_entity($studentId);
1640
1641
                $portfolio = new Portfolio();
1642
                $portfolio
1643
                    ->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER)
1644
                    ->setTitle($values['title'])
1645
                    ->setContent($values['content'])
1646
                    ->setUser($owner)
1647
                    ->setOrigin($originItem->getId())
1648
                    ->setOriginType(Portfolio::TYPE_ITEM)
1649
                    ->setCourse($this->course)
1650
                    ->setSession($this->session)
1651
                    ->setCreationDate($currentTime)
1652
                    ->setUpdateDate($currentTime);
1653
1654
                $this->em->persist($portfolio);
1655
            }
1656
1657
            $this->em->flush();
1658
1659
            Display::addFlash(
1660
                Display::return_message(get_lang('PortfolioItemAddedToStudents'), 'success')
1661
            );
1662
1663
            header("Location: $this->baseUrl");
1664
            exit;
1665
        }
1666
1667
        $this->renderView($content, $toolName);
1668
    }
1669
1670
    /**
1671
     * @throws \Doctrine\ORM\ORMException
1672
     * @throws \Doctrine\ORM\OptimisticLockException
1673
     * @throws \Exception
1674
     */
1675
    public function teacherCopyComment(PortfolioComment $originComment)
1676
    {
1677
        $actionParams = http_build_query(
1678
            [
1679
                'action' => 'teacher_copy',
1680
                'copy' => 'comment',
1681
                'id' => $originComment->getId(),
1682
            ]
1683
        );
1684
1685
        $form = new FormValidator('teacher_copy_portfolio', 'post', $this->baseUrl.$actionParams);
1686
1687
        if (api_get_configuration_value('save_titles_as_html')) {
1688
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
1689
        } else {
1690
            $form->addText('title', get_lang('Title'));
1691
            $form->applyFilter('title', 'trim');
1692
        }
1693
1694
        $form->addLabel(
1695
            sprintf(get_lang('PortfolioCommentFromXUser'), $originComment->getAuthor()->getCompleteName()),
1696
            Display::panel(
1697
                Security::remove_XSS($originComment->getContent())
1698
            )
1699
        );
1700
        $form->addHtmlEditor('content', get_lang('Content'), true, false, ['ToolbarSet' => 'NotebookStudent']);
1701
1702
        $urlParams = http_build_query(
1703
            [
1704
                'a' => 'search_user_by_course',
1705
                'course_id' => $this->course->getId(),
1706
                'session_id' => $this->session ? $this->session->getId() : 0,
1707
            ]
1708
        );
1709
        $form->addSelectAjax(
1710
            'students',
1711
            get_lang('Students'),
1712
            [],
1713
            [
1714
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1715
                'multiple' => true,
1716
            ]
1717
        );
1718
        $form->addRule('students', get_lang('ThisFieldIsRequired'), 'required');
1719
        $form->addButtonCreate(get_lang('Save'));
1720
1721
        $toolName = get_lang('CopyToStudentPortfolio');
1722
        $content = $form->returnForm();
1723
1724
        if ($form->validate()) {
1725
            $values = $form->exportValues();
1726
1727
            $currentTime = api_get_utc_datetime(null, false, true);
1728
1729
            foreach ($values['students'] as $studentId) {
1730
                $owner = api_get_user_entity($studentId);
1731
1732
                $portfolio = new Portfolio();
1733
                $portfolio
1734
                    ->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER)
1735
                    ->setTitle($values['title'])
1736
                    ->setContent($values['content'])
1737
                    ->setUser($owner)
1738
                    ->setOrigin($originComment->getId())
1739
                    ->setOriginType(Portfolio::TYPE_COMMENT)
1740
                    ->setCourse($this->course)
1741
                    ->setSession($this->session)
1742
                    ->setCreationDate($currentTime)
1743
                    ->setUpdateDate($currentTime);
1744
1745
                $this->em->persist($portfolio);
1746
            }
1747
1748
            $this->em->flush();
1749
1750
            Display::addFlash(
1751
                Display::return_message(get_lang('PortfolioItemAddedToStudents'), 'success')
1752
            );
1753
1754
            header("Location: $this->baseUrl");
1755
            exit;
1756
        }
1757
1758
        $this->renderView($content, $toolName);
1759
    }
1760
1761
    /**
1762
     * @throws \Doctrine\ORM\ORMException
1763
     * @throws \Doctrine\ORM\OptimisticLockException
1764
     */
1765
    public function markImportantCommentInItem(Portfolio $item, PortfolioComment $comment)
1766
    {
1767
        if ($comment->getItem()->getId() !== $item->getId()) {
1768
            api_not_allowed(true);
1769
        }
1770
1771
        $comment->setIsImportant(
1772
            !$comment->isImportant()
1773
        );
1774
1775
        $this->em->persist($comment);
1776
        $this->em->flush();
1777
1778
        Display::addFlash(
1779
            Display::return_message(get_lang('CommentMarkedAsImportant'), 'success')
1780
        );
1781
1782
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
1783
        exit;
1784
    }
1785
1786
    /**
1787
     * @throws \Exception
1788
     */
1789
    public function details(HttpRequest $httpRequest)
1790
    {
1791
        $this->blockIsNotAllowed();
1792
1793
        $currentUserId = api_get_user_id();
1794
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
1795
1796
        $actions = [];
1797
        $actions[] = Display::url(
1798
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
1799
            $this->baseUrl
1800
        );
1801
        $actions[] = Display::url(
1802
            Display::return_icon('pdf.png', get_lang('ExportMyPortfolioDataPdf'), [], ICON_SIZE_MEDIUM),
1803
            $this->baseUrl.http_build_query(['action' => 'export_pdf'])
1804
        );
1805
        $actions[] = Display::url(
1806
            Display::return_icon('save_pack.png', get_lang('ExportMyPortfolioDataZip'), [], ICON_SIZE_MEDIUM),
1807
            $this->baseUrl.http_build_query(['action' => 'export_zip'])
1808
        );
1809
1810
        $frmStudent = null;
1811
1812
        if ($isAllowedToFilterStudent) {
1813
            if ($httpRequest->query->has('user')) {
1814
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
1815
1816
                if (empty($this->owner)) {
1817
                    api_not_allowed(true);
1818
                }
1819
1820
                $actions[1] = Display::url(
1821
                    Display::return_icon('pdf.png', get_lang('ExportMyPortfolioDataPdf'), [], ICON_SIZE_MEDIUM),
1822
                    $this->baseUrl.http_build_query(['action' => 'export_pdf', 'user' => $this->owner->getId()])
1823
                );
1824
                $actions[2] = Display::url(
1825
                    Display::return_icon('save_pack.png', get_lang('ExportMyPortfolioDataZip'), [], ICON_SIZE_MEDIUM),
1826
                    $this->baseUrl.http_build_query(['action' => 'export_zip', 'user' => $this->owner->getId()])
1827
                );
1828
            }
1829
1830
            $frmStudent = new FormValidator('frm_student_list', 'get');
1831
1832
            $urlParams = http_build_query(
1833
                [
1834
                    'a' => 'search_user_by_course',
1835
                    'course_id' => $this->course->getId(),
1836
                    'session_id' => $this->session ? $this->session->getId() : 0,
1837
                ]
1838
            );
1839
1840
            $frmStudent
1841
                ->addSelectAjax(
1842
                    'user',
1843
                    get_lang('SelectLearnerPortfolio'),
1844
                    [],
1845
                    [
1846
                        'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1847
                        'placeholder' => get_lang('SearchStudent'),
1848
                        'formatResult' => SelectAjax::templateResultForUsersInCourse(),
1849
                        'formatSelection' => SelectAjax::templateSelectionForUsersInCourse(),
1850
                    ]
1851
                )
1852
                ->addOption(
1853
                    $this->owner->getCompleteName(),
1854
                    $this->owner->getId(),
1855
                    [
1856
                        'data-avatarurl' => UserManager::getUserPicture($this->owner->getId()),
1857
                        'data-username' => $this->owner->getUsername(),
1858
                    ]
1859
                )
1860
            ;
1861
            $frmStudent->setDefaults(['user' => $this->owner->getId()]);
1862
            $frmStudent->addHidden('action', 'details');
1863
            $frmStudent->addHidden('cidReq', $this->course->getCode());
1864
            $frmStudent->addHidden('id_session', $this->session ? $this->session->getId() : 0);
1865
            $frmStudent->addButtonFilter(get_lang('Filter'));
1866
        }
1867
1868
        $itemsRepo = $this->em->getRepository(Portfolio::class);
1869
        $commentsRepo = $this->em->getRepository(PortfolioComment::class);
1870
1871
        $getItemsTotalNumber = function () use ($itemsRepo, $isAllowedToFilterStudent, $currentUserId) {
1872
            $qb = $itemsRepo->createQueryBuilder('i');
1873
            $qb
1874
                ->select('COUNT(i)')
1875
                ->where('i.user = :user')
1876
                ->setParameter('user', $this->owner);
1877
1878
            if ($this->course) {
1879
                $qb
1880
                    ->andWhere('i.course = :course')
1881
                    ->setParameter('course', $this->course);
1882
1883
                if ($this->session) {
1884
                    $qb
1885
                        ->andWhere('i.session = :session')
1886
                        ->setParameter('session', $this->session);
1887
                } else {
1888
                    $qb->andWhere('i.session IS NULL');
1889
                }
1890
            }
1891
1892
            if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
1893
                $visibilityCriteria = [
1894
                    Portfolio::VISIBILITY_VISIBLE,
1895
                    Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER,
1896
                ];
1897
1898
                $qb->andWhere(
1899
                    $qb->expr()->in('i.visibility', $visibilityCriteria)
1900
                );
1901
            }
1902
1903
            return $qb->getQuery()->getSingleScalarResult();
1904
        };
1905
        $getItemsData = function ($from, $limit, $columnNo, $orderDirection) use ($itemsRepo, $isAllowedToFilterStudent, $currentUserId) {
1906
            $qb = $itemsRepo->createQueryBuilder('item')
1907
                ->where('item.user = :user')
1908
                ->leftJoin('item.category', 'category')
1909
                ->leftJoin('item.course', 'course')
1910
                ->leftJoin('item.session', 'session')
1911
                ->setParameter('user', $this->owner);
1912
1913
            if ($this->course) {
1914
                $qb
1915
                    ->andWhere('item.course = :course_id')
1916
                    ->setParameter('course_id', $this->course);
1917
1918
                if ($this->session) {
1919
                    $qb
1920
                        ->andWhere('item.session = :session')
1921
                        ->setParameter('session', $this->session);
1922
                } else {
1923
                    $qb->andWhere('item.session IS NULL');
1924
                }
1925
            }
1926
1927
            if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
1928
                $visibilityCriteria = [
1929
                    Portfolio::VISIBILITY_VISIBLE,
1930
                    Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER,
1931
                ];
1932
1933
                $qb->andWhere(
1934
                    $qb->expr()->in('item.visibility', $visibilityCriteria)
1935
                );
1936
            }
1937
1938
            if (0 == $columnNo) {
1939
                $qb->orderBy('item.title', $orderDirection);
1940
            } elseif (1 == $columnNo) {
1941
                $qb->orderBy('item.creationDate', $orderDirection);
1942
            } elseif (2 == $columnNo) {
1943
                $qb->orderBy('item.updateDate', $orderDirection);
1944
            } elseif (3 == $columnNo) {
1945
                $qb->orderBy('category.title', $orderDirection);
1946
            } elseif (5 == $columnNo) {
1947
                $qb->orderBy('item.score', $orderDirection);
1948
            } elseif (6 == $columnNo) {
1949
                $qb->orderBy('course.title', $orderDirection);
1950
            } elseif (7 == $columnNo) {
1951
                $qb->orderBy('session.name', $orderDirection);
1952
            }
1953
1954
            $qb->setFirstResult($from)->setMaxResults($limit);
1955
1956
            return array_map(
1957
                function (Portfolio $item) {
1958
                    $category = $item->getCategory();
1959
1960
                    $row = [];
1961
                    $row[] = $item;
1962
                    $row[] = $item->getCreationDate();
1963
                    $row[] = $item->getUpdateDate();
1964
                    $row[] = $category ? $item->getCategory()->getTitle() : null;
1965
                    $row[] = $item->getComments()->count();
1966
                    $row[] = $item->getScore();
1967
1968
                    if (!$this->course) {
1969
                        $row[] = $item->getCourse();
1970
                        $row[] = $item->getSession();
1971
                    }
1972
1973
                    return $row;
1974
                },
1975
                $qb->getQuery()->getResult()
1976
            );
1977
        };
1978
1979
        $portfolioItemColumnFilter = function (Portfolio $item) {
1980
            return Display::url(
1981
                $item->getTitle(true),
1982
                $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
1983
            );
1984
        };
1985
        $convertFormatDateColumnFilter = function (DateTime $date) {
1986
            return api_convert_and_format_date($date);
1987
        };
1988
1989
        $tblItems = new SortableTable('tbl_items', $getItemsTotalNumber, $getItemsData, 1, 20, 'DESC');
1990
        $tblItems->set_additional_parameters(['action' => 'details', 'user' => $this->owner->getId()]);
1991
        $tblItems->set_header(0, get_lang('Title'));
1992
        $tblItems->set_column_filter(0, $portfolioItemColumnFilter);
1993
        $tblItems->set_header(1, get_lang('CreationDate'), true, [], ['class' => 'text-center']);
1994
        $tblItems->set_column_filter(1, $convertFormatDateColumnFilter);
1995
        $tblItems->set_header(2, get_lang('LastUpdate'), true, [], ['class' => 'text-center']);
1996
        $tblItems->set_column_filter(2, $convertFormatDateColumnFilter);
1997
        $tblItems->set_header(3, get_lang('Category'));
1998
        $tblItems->set_header(4, get_lang('Comments'), false, [], ['class' => 'text-right']);
1999
        $tblItems->set_header(5, get_lang('Score'), true, [], ['class' => 'text-right']);
2000
2001
        if (!$this->course) {
2002
            $tblItems->set_header(6, get_lang('Course'));
2003
            $tblItems->set_header(7, get_lang('Session'));
2004
        }
2005
2006
        $getCommentsTotalNumber = function () use ($commentsRepo) {
2007
            $qb = $commentsRepo->createQueryBuilder('c');
2008
            $qb
2009
                ->select('COUNT(c)')
2010
                ->where('c.author = :author')
2011
                ->setParameter('author', $this->owner);
2012
2013
            if ($this->course) {
2014
                $qb
2015
                    ->innerJoin('c.item', 'i')
2016
                    ->andWhere('i.course = :course')
2017
                    ->setParameter('course', $this->course);
2018
2019
                if ($this->session) {
2020
                    $qb
2021
                        ->andWhere('i.session = :session')
2022
                        ->setParameter('session', $this->session);
2023
                } else {
2024
                    $qb->andWhere('i.session IS NULL');
2025
                }
2026
            }
2027
2028
            return $qb->getQuery()->getSingleScalarResult();
2029
        };
2030
        $getCommentsData = function ($from, $limit, $columnNo, $orderDirection) use ($commentsRepo) {
2031
            $qb = $commentsRepo->createQueryBuilder('comment');
2032
            $qb
2033
                ->where('comment.author = :user')
2034
                ->innerJoin('comment.item', 'item')
2035
                ->setParameter('user', $this->owner);
2036
2037
            if ($this->course) {
2038
                $qb
2039
                    ->innerJoin('comment.item', 'i')
2040
                    ->andWhere('item.course = :course')
2041
                    ->setParameter('course', $this->course);
2042
2043
                if ($this->session) {
2044
                    $qb
2045
                        ->andWhere('item.session = :session')
2046
                        ->setParameter('session', $this->session);
2047
                } else {
2048
                    $qb->andWhere('item.session IS NULL');
2049
                }
2050
            }
2051
2052
            if (0 == $columnNo) {
2053
                $qb->orderBy('comment.content', $orderDirection);
2054
            } elseif (1 == $columnNo) {
2055
                $qb->orderBy('comment.date', $orderDirection);
2056
            } elseif (2 == $columnNo) {
2057
                $qb->orderBy('item.title', $orderDirection);
2058
            } elseif (3 == $columnNo) {
2059
                $qb->orderBy('comment.score', $orderDirection);
2060
            }
2061
2062
            $qb->setFirstResult($from)->setMaxResults($limit);
2063
2064
            return array_map(
2065
                function (PortfolioComment $comment) {
2066
                    return [
2067
                        $comment,
2068
                        $comment->getDate(),
2069
                        $comment->getItem(),
2070
                        $comment->getScore(),
2071
                    ];
2072
                },
2073
                $qb->getQuery()->getResult()
2074
            );
2075
        };
2076
2077
        $tblComments = new SortableTable('tbl_comments', $getCommentsTotalNumber, $getCommentsData, 1, 20, 'DESC');
2078
        $tblComments->set_additional_parameters(['action' => 'details', 'user' => $this->owner->getId()]);
2079
        $tblComments->set_header(0, get_lang('Resume'));
2080
        $tblComments->set_column_filter(
2081
            0,
2082
            function (PortfolioComment $comment) {
2083
                return Display::url(
2084
                    $comment->getExcerpt(),
2085
                    $this->baseUrl.http_build_query(['action' => 'view', 'id' => $comment->getItem()->getId()])
2086
                    .'#comment-'.$comment->getId()
2087
                );
2088
            }
2089
        );
2090
        $tblComments->set_header(1, get_lang('Date'), true, [], ['class' => 'text-center']);
2091
        $tblComments->set_column_filter(1, $convertFormatDateColumnFilter);
2092
        $tblComments->set_header(2, get_lang('PortfolioItemTitle'));
2093
        $tblComments->set_column_filter(2, $portfolioItemColumnFilter);
2094
        $tblComments->set_header(3, get_lang('Score'), true, [], ['class' => 'text-right']);
2095
2096
        $content = '';
2097
2098
        if ($frmStudent) {
2099
            $content .= $frmStudent->returnForm();
2100
        }
2101
2102
        $totalNumberOfItems = $tblItems->get_total_number_of_items();
2103
        $totalNumberOfComments = $tblComments->get_total_number_of_items();
2104
        $requiredNumberOfItems = (int) api_get_course_setting('portfolio_number_items');
2105
        $requiredNumberOfComments = (int) api_get_course_setting('portfolio_number_comments');
2106
2107
        $itemsSubtitle = '';
2108
2109
        if ($requiredNumberOfItems > 0) {
2110
            $itemsSubtitle = sprintf(
2111
                get_lang('XAddedYRequired'),
2112
                $totalNumberOfItems,
2113
                $requiredNumberOfItems
2114
            );
2115
        }
2116
2117
        $content .= Display::page_subheader2(
2118
            get_lang('PortfolioItems'),
2119
            $itemsSubtitle
2120
        ).PHP_EOL;
2121
2122
        if ($totalNumberOfItems > 0) {
2123
            $content .= $tblItems->return_table().PHP_EOL;
2124
        } else {
2125
            $content .= Display::return_message(get_lang('NoItemsInYourPortfolio'), 'warning');
2126
        }
2127
2128
        $commentsSubtitle = '';
2129
2130
        if ($requiredNumberOfComments > 0) {
2131
            $commentsSubtitle = sprintf(
2132
                get_lang('XAddedYRequired'),
2133
                $totalNumberOfComments,
2134
                $requiredNumberOfComments
2135
            );
2136
        }
2137
2138
        $content .= Display::page_subheader2(
2139
            get_lang('PortfolioCommentsMade'),
2140
            $commentsSubtitle
2141
        ).PHP_EOL;
2142
2143
        if ($totalNumberOfComments > 0) {
2144
            $content .= $tblComments->return_table().PHP_EOL;
2145
        } else {
2146
            $content .= Display::return_message(get_lang('YouHaveNotCommented'), 'warning');
2147
        }
2148
2149
        $this->renderView($content, get_lang('PortfolioDetails'), $actions);
2150
    }
2151
2152
    /**
2153
     * @throws MpdfException
2154
     */
2155
    public function exportPdf(HttpRequest $httpRequest)
2156
    {
2157
        $currentUserId = api_get_user_id();
2158
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
2159
2160
        if ($isAllowedToFilterStudent) {
2161
            if ($httpRequest->query->has('user')) {
2162
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
2163
2164
                if (empty($this->owner)) {
2165
                    api_not_allowed(true);
2166
                }
2167
            }
2168
        }
2169
2170
        $pdfContent = Display::page_header($this->owner->getCompleteName());
2171
2172
        if ($this->course) {
2173
            $pdfContent .= '<p>'.get_lang('Course').': ';
2174
2175
            if ($this->session) {
2176
                $pdfContent .= $this->session->getName().' ('.$this->course->getTitle().')';
2177
            } else {
2178
                $pdfContent .= $this->course->getTitle();
2179
            }
2180
2181
            $pdfContent .= '</p>';
2182
        }
2183
2184
        $visibility = [];
2185
2186
        if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
2187
            $visibility[] = Portfolio::VISIBILITY_VISIBLE;
2188
            $visibility[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
2189
        }
2190
2191
        $items = $this->em
2192
            ->getRepository(Portfolio::class)
2193
            ->findItemsByUser(
2194
                $this->owner,
2195
                $this->course,
2196
                $this->session,
2197
                null,
2198
                $visibility
2199
            );
2200
        $comments = $this->em
2201
            ->getRepository(PortfolioComment::class)
2202
            ->findCommentsByUser($this->owner, $this->course, $this->session);
2203
2204
        $itemsHtml = $this->getItemsInHtmlFormatted($items);
2205
        $commentsHtml = $this->getCommentsInHtmlFormatted($comments);
2206
2207
        $totalNumberOfItems = count($itemsHtml);
2208
        $totalNumberOfComments = count($commentsHtml);
2209
        $requiredNumberOfItems = (int) api_get_course_setting('portfolio_number_items');
2210
        $requiredNumberOfComments = (int) api_get_course_setting('portfolio_number_comments');
2211
2212
        $itemsSubtitle = '';
2213
        $commentsSubtitle = '';
2214
2215
        if ($requiredNumberOfItems > 0) {
2216
            $itemsSubtitle = sprintf(
2217
                get_lang('XAddedYRequired'),
2218
                $totalNumberOfItems,
2219
                $requiredNumberOfItems
2220
            );
2221
        }
2222
2223
        if ($requiredNumberOfComments > 0) {
2224
            $commentsSubtitle = sprintf(
2225
                get_lang('XAddedYRequired'),
2226
                $totalNumberOfComments,
2227
                $requiredNumberOfComments
2228
            );
2229
        }
2230
2231
        $pdfContent .= Display::page_subheader2(
2232
            get_lang('PortfolioItems'),
2233
            $itemsSubtitle
2234
        );
2235
2236
        if ($totalNumberOfItems > 0) {
2237
            $pdfContent .= implode(PHP_EOL, $itemsHtml);
2238
        } else {
2239
            $pdfContent .= Display::return_message(get_lang('NoItemsInYourPortfolio'), 'warning');
2240
        }
2241
2242
        $pdfContent .= Display::page_subheader2(
2243
            get_lang('PortfolioCommentsMade'),
2244
            $commentsSubtitle
2245
        );
2246
2247
        if ($totalNumberOfComments > 0) {
2248
            $pdfContent .= implode(PHP_EOL, $commentsHtml);
2249
        } else {
2250
            $pdfContent .= Display::return_message(get_lang('YouHaveNotCommented'), 'warning');
2251
        }
2252
2253
        $pdfName = $this->owner->getCompleteName()
2254
            .($this->course ? '_'.$this->course->getCode() : '')
2255
            .'_'.get_lang('Portfolio');
2256
2257
        HookPortfolioDownloaded::create()
2258
            ->setEventData(['owner' => $this->owner])
2259
            ->notifyPortfolioDownloaded()
2260
        ;
2261
2262
        $pdf = new PDF();
2263
        $pdf->content_to_pdf(
2264
            $pdfContent,
2265
            null,
2266
            $pdfName,
2267
            $this->course ? $this->course->getCode() : null,
2268
            'D',
2269
            false,
2270
            null,
2271
            false,
2272
            true
2273
        );
2274
    }
2275
2276
    public function exportZip(HttpRequest $httpRequest)
2277
    {
2278
        $currentUserId = api_get_user_id();
2279
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
2280
2281
        if ($isAllowedToFilterStudent) {
2282
            if ($httpRequest->query->has('user')) {
2283
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
2284
2285
                if (empty($this->owner)) {
2286
                    api_not_allowed(true);
2287
                }
2288
            }
2289
        }
2290
2291
        $itemsRepo = $this->em->getRepository(Portfolio::class);
2292
        $commentsRepo = $this->em->getRepository(PortfolioComment::class);
2293
        $attachmentsRepo = $this->em->getRepository(PortfolioAttachment::class);
2294
2295
        $visibility = [];
2296
2297
        if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
2298
            $visibility[] = Portfolio::VISIBILITY_VISIBLE;
2299
            $visibility[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
2300
        }
2301
2302
        $items = $itemsRepo->findItemsByUser(
2303
            $this->owner,
2304
            $this->course,
2305
            $this->session,
2306
            null,
2307
            $visibility
2308
        );
2309
        $comments = $commentsRepo->findCommentsByUser($this->owner, $this->course, $this->session);
2310
2311
        $itemsHtml = $this->getItemsInHtmlFormatted($items);
2312
        $commentsHtml = $this->getCommentsInHtmlFormatted($comments);
2313
2314
        $sysArchivePath = api_get_path(SYS_ARCHIVE_PATH);
2315
        $tempPortfolioDirectory = $sysArchivePath."portfolio/{$this->owner->getId()}";
2316
2317
        $userDirectory = UserManager::getUserPathById($this->owner->getId(), 'system');
2318
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2319
2320
        $tblItemsHeaders = [];
2321
        $tblItemsHeaders[] = get_lang('Title');
2322
        $tblItemsHeaders[] = get_lang('CreationDate');
2323
        $tblItemsHeaders[] = get_lang('LastUpdate');
2324
        $tblItemsHeaders[] = get_lang('Category');
2325
        $tblItemsHeaders[] = get_lang('Category');
2326
        $tblItemsHeaders[] = get_lang('Score');
2327
        $tblItemsHeaders[] = get_lang('Course');
2328
        $tblItemsHeaders[] = get_lang('Session');
2329
        $tblItemsData = [];
2330
2331
        $tblCommentsHeaders = [];
2332
        $tblCommentsHeaders[] = get_lang('Resume');
2333
        $tblCommentsHeaders[] = get_lang('Date');
2334
        $tblCommentsHeaders[] = get_lang('PortfolioItemTitle');
2335
        $tblCommentsHeaders[] = get_lang('Score');
2336
        $tblCommentsData = [];
2337
2338
        $filenames = [];
2339
2340
        $fs = new Filesystem();
2341
2342
        /**
2343
         * @var int       $i
2344
         * @var Portfolio $item
2345
         */
2346
        foreach ($items as $i => $item) {
2347
            $itemCategory = $item->getCategory();
2348
            $itemCourse = $item->getCourse();
2349
            $itemSession = $item->getSession();
2350
2351
            $itemDirectory = $item->getCreationDate()->format('Y-m-d-H-i-s');
2352
2353
            $itemFilename = sprintf('%s/items/%s/item.html', $tempPortfolioDirectory, $itemDirectory);
2354
            $imagePaths = [];
2355
            $itemFileContent = $this->fixMediaSourcesToHtml($itemsHtml[$i], $imagePaths);
2356
2357
            $fs->dumpFile($itemFilename, $itemFileContent);
2358
2359
            $filenames[] = $itemFilename;
2360
2361
            foreach ($imagePaths as $imagePath) {
2362
                $inlineFile = dirname($itemFilename).'/'.basename($imagePath);
2363
2364
                try {
2365
                    $filenames[] = $inlineFile;
2366
                    $fs->copy($imagePath, $inlineFile);
2367
                } catch (FileNotFoundException $notFoundException) {
2368
                    continue;
2369
                }
2370
            }
2371
2372
            $attachments = $attachmentsRepo->findFromItem($item);
2373
2374
            /** @var PortfolioAttachment $attachment */
2375
            foreach ($attachments as $attachment) {
2376
                $attachmentFilename = sprintf(
2377
                    '%s/items/%s/attachments/%s',
2378
                    $tempPortfolioDirectory,
2379
                    $itemDirectory,
2380
                    $attachment->getFilename()
2381
                );
2382
2383
                try {
2384
                    $fs->copy(
2385
                        $attachmentsDirectory.$attachment->getPath(),
2386
                        $attachmentFilename
2387
                    );
2388
                    $filenames[] = $attachmentFilename;
2389
                } catch (FileNotFoundException $notFoundException) {
2390
                    continue;
2391
                }
2392
            }
2393
2394
            $tblItemsData[] = [
2395
                Display::url(
2396
                    Security::remove_XSS($item->getTitle()),
2397
                    sprintf('items/%s/item.html', $itemDirectory)
2398
                ),
2399
                api_convert_and_format_date($item->getCreationDate()),
2400
                api_convert_and_format_date($item->getUpdateDate()),
2401
                $itemCategory ? $itemCategory->getTitle() : null,
2402
                $item->getComments()->count(),
2403
                $item->getScore(),
2404
                $itemCourse->getTitle(),
2405
                $itemSession ? $itemSession->getName() : null,
2406
            ];
2407
        }
2408
2409
        /**
2410
         * @var int              $i
2411
         * @var PortfolioComment $comment
2412
         */
2413
        foreach ($comments as $i => $comment) {
2414
            $commentDirectory = $comment->getDate()->format('Y-m-d-H-i-s');
2415
2416
            $imagePaths = [];
2417
            $commentFileContent = $this->fixMediaSourcesToHtml($commentsHtml[$i], $imagePaths);
2418
            $commentFilename = sprintf('%s/comments/%s/comment.html', $tempPortfolioDirectory, $commentDirectory);
2419
2420
            $fs->dumpFile($commentFilename, $commentFileContent);
2421
2422
            $filenames[] = $commentFilename;
2423
2424
            foreach ($imagePaths as $imagePath) {
2425
                $inlineFile = dirname($commentFilename).'/'.basename($imagePath);
2426
2427
                try {
2428
                    $filenames[] = $inlineFile;
2429
                    $fs->copy($imagePath, $inlineFile);
2430
                } catch (FileNotFoundException $notFoundException) {
2431
                    continue;
2432
                }
2433
            }
2434
2435
            $attachments = $attachmentsRepo->findFromComment($comment);
2436
2437
            /** @var PortfolioAttachment $attachment */
2438
            foreach ($attachments as $attachment) {
2439
                $attachmentFilename = sprintf(
2440
                    '%s/comments/%s/attachments/%s',
2441
                    $tempPortfolioDirectory,
2442
                    $commentDirectory,
2443
                    $attachment->getFilename()
2444
                );
2445
2446
                try {
2447
                    $fs->copy(
2448
                        $attachmentsDirectory.$attachment->getPath(),
2449
                        $attachmentFilename
2450
                    );
2451
                    $filenames[] = $attachmentFilename;
2452
                } catch (FileNotFoundException $notFoundException) {
2453
                    continue;
2454
                }
2455
            }
2456
2457
            $tblCommentsData[] = [
2458
                Display::url(
2459
                    $comment->getExcerpt(),
2460
                    sprintf('comments/%s/comment.html', $commentDirectory)
2461
                ),
2462
                api_convert_and_format_date($comment->getDate()),
2463
                Security::remove_XSS($comment->getItem()->getTitle()),
2464
                $comment->getScore(),
2465
            ];
2466
        }
2467
2468
        $tblItems = new HTML_Table(['class' => 'table table-hover table-striped table-bordered data_table']);
2469
        $tblItems->setHeaders($tblItemsHeaders);
2470
        $tblItems->setData($tblItemsData);
2471
2472
        $tblComments = new HTML_Table(['class' => 'table table-hover table-striped table-bordered data_table']);
2473
        $tblComments->setHeaders($tblCommentsHeaders);
2474
        $tblComments->setData($tblCommentsData);
2475
2476
        $itemFilename = sprintf('%s/index.html', $tempPortfolioDirectory);
2477
2478
        $filenames[] = $itemFilename;
2479
2480
        $fs->dumpFile(
2481
            $itemFilename,
2482
            $this->formatZipIndexFile($tblItems, $tblComments)
2483
        );
2484
2485
        $zipName = $this->owner->getCompleteName()
2486
            .($this->course ? '_'.$this->course->getCode() : '')
2487
            .'_'.get_lang('Portfolio');
2488
        $tempZipFile = $sysArchivePath."portfolio/$zipName.zip";
2489
        $zip = new PclZip($tempZipFile);
2490
2491
        foreach ($filenames as $filename) {
2492
            $zip->add($filename, PCLZIP_OPT_REMOVE_PATH, $tempPortfolioDirectory);
2493
        }
2494
2495
        HookPortfolioDownloaded::create()
2496
            ->setEventData(['owner' => $this->owner])
2497
            ->notifyPortfolioDownloaded()
2498
        ;
2499
2500
        DocumentManager::file_send_for_download($tempZipFile, true, "$zipName.zip");
2501
2502
        $fs->remove($tempPortfolioDirectory);
2503
        $fs->remove($tempZipFile);
2504
    }
2505
2506
    public function qualifyItem(Portfolio $item)
2507
    {
2508
        global $interbreadcrumb;
2509
2510
        $em = Database::getManager();
2511
2512
        $formAction = $this->baseUrl.http_build_query(['action' => 'qualify', 'item' => $item->getId()]);
2513
2514
        $form = new FormValidator('frm_qualify', 'post', $formAction);
2515
        $form->addUserAvatar('user', get_lang('Author'));
2516
        $form->addLabel(get_lang('Title'), $item->getTitle());
2517
2518
        $itemContent = $this->generateItemContent($item);
2519
2520
        $form->addLabel(get_lang('Content'), $itemContent);
2521
        $form->addNumeric(
2522
            'score',
2523
            [get_lang('QualifyNumeric'), null, ' / '.api_get_course_setting('portfolio_max_score')]
2524
        );
2525
        $form->addButtonSave(get_lang('QualifyThisPortfolioItem'));
2526
2527
        if ($form->validate()) {
2528
            $values = $form->exportValues();
2529
2530
            $item->setScore($values['score']);
2531
2532
            $em->persist($item);
2533
            $em->flush();
2534
2535
            HookPortfolioItemScored::create()
2536
                ->setEventData(['item' => $item])
2537
                ->notifyItemScored()
2538
            ;
2539
2540
            Display::addFlash(
2541
                Display::return_message(get_lang('PortfolioItemGraded'), 'success')
2542
            );
2543
2544
            header("Location: $formAction");
2545
            exit();
2546
        }
2547
2548
        $form->setDefaults(
2549
            [
2550
                'user' => $item->getUser(),
2551
                'score' => (float) $item->getScore(),
2552
            ]
2553
        );
2554
2555
        $interbreadcrumb[] = [
2556
            'name' => get_lang('Portfolio'),
2557
            'url' => $this->baseUrl,
2558
        ];
2559
        $interbreadcrumb[] = [
2560
            'name' => $item->getTitle(true),
2561
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
2562
        ];
2563
2564
        $actions = [];
2565
        $actions[] = Display::url(
2566
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
2567
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
2568
        );
2569
2570
        $this->renderView($form->returnForm(), get_lang('Qualify'), $actions);
2571
    }
2572
2573
    public function qualifyComment(PortfolioComment $comment)
2574
    {
2575
        global $interbreadcrumb;
2576
2577
        $em = Database::getManager();
2578
2579
        $item = $comment->getItem();
2580
        $commentPath = $em->getRepository(PortfolioComment::class)->getPath($comment);
2581
2582
        $template = new Template('', false, false, false, true, false, false);
2583
        $template->assign('item', $item);
2584
        $template->assign('comments_path', $commentPath);
2585
        $commentContext = $template->fetch(
2586
            $template->get_template('portfolio/comment_context.html.twig')
2587
        );
2588
2589
        $formAction = $this->baseUrl.http_build_query(['action' => 'qualify', 'comment' => $comment->getId()]);
2590
2591
        $form = new FormValidator('frm_qualify', 'post', $formAction);
2592
        $form->addHtml($commentContext);
2593
        $form->addUserAvatar('user', get_lang('Author'));
2594
        $form->addLabel(get_lang('Comment'), $comment->getContent());
2595
        $form->addNumeric(
2596
            'score',
2597
            [get_lang('QualifyNumeric'), null, '/ '.api_get_course_setting('portfolio_max_score')]
2598
        );
2599
        $form->addButtonSave(get_lang('QualifyThisPortfolioComment'));
2600
2601
        if ($form->validate()) {
2602
            $values = $form->exportValues();
2603
2604
            $comment->setScore($values['score']);
2605
2606
            $em->persist($comment);
2607
            $em->flush();
2608
2609
            HookPortfolioCommentScored::create()
2610
                ->setEventData(['comment' => $comment])
2611
                ->notifyCommentScored()
2612
            ;
2613
2614
            Display::addFlash(
2615
                Display::return_message(get_lang('PortfolioCommentGraded'), 'success')
2616
            );
2617
2618
            header("Location: $formAction");
2619
            exit();
2620
        }
2621
2622
        $form->setDefaults(
2623
            [
2624
                'user' => $comment->getAuthor(),
2625
                'score' => (float) $comment->getScore(),
2626
            ]
2627
        );
2628
2629
        $interbreadcrumb[] = [
2630
            'name' => get_lang('Portfolio'),
2631
            'url' => $this->baseUrl,
2632
        ];
2633
        $interbreadcrumb[] = [
2634
            'name' => $item->getTitle(true),
2635
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
2636
        ];
2637
2638
        $actions = [];
2639
        $actions[] = Display::url(
2640
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
2641
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
2642
        );
2643
2644
        $this->renderView($form->returnForm(), get_lang('Qualify'), $actions);
2645
    }
2646
2647
    public function downloadAttachment(HttpRequest $httpRequest)
2648
    {
2649
        $path = $httpRequest->query->get('file');
2650
2651
        if (empty($path)) {
2652
            api_not_allowed(true);
2653
        }
2654
2655
        $em = Database::getManager();
2656
        $attachmentRepo = $em->getRepository(PortfolioAttachment::class);
2657
2658
        $attachment = $attachmentRepo->findOneByPath($path);
2659
2660
        if (empty($attachment)) {
2661
            api_not_allowed(true);
2662
        }
2663
2664
        $originOwnerId = 0;
2665
2666
        if (PortfolioAttachment::TYPE_ITEM === $attachment->getOriginType()) {
2667
            $item = $em->find(Portfolio::class, $attachment->getOrigin());
2668
2669
            $originOwnerId = $item->getUser()->getId();
2670
        } elseif (PortfolioAttachment::TYPE_COMMENT === $attachment->getOriginType()) {
2671
            $comment = $em->find(PortfolioComment::class, $attachment->getOrigin());
2672
2673
            $originOwnerId = $comment->getAuthor()->getId();
2674
        } else {
2675
            api_not_allowed(true);
2676
        }
2677
2678
        $userDirectory = UserManager::getUserPathById($originOwnerId, 'system');
2679
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2680
        $attachmentFilename = $attachmentsDirectory.$attachment->getPath();
2681
2682
        if (!Security::check_abs_path($attachmentFilename, $attachmentsDirectory)) {
2683
            api_not_allowed(true);
2684
        }
2685
2686
        $downloaded = DocumentManager::file_send_for_download(
2687
            $attachmentFilename,
2688
            true,
2689
            $attachment->getFilename()
2690
        );
2691
2692
        if (!$downloaded) {
2693
            api_not_allowed(true);
2694
        }
2695
    }
2696
2697
    public function deleteAttachment(HttpRequest $httpRequest)
2698
    {
2699
        $currentUserId = api_get_user_id();
2700
2701
        $path = $httpRequest->query->get('file');
2702
2703
        if (empty($path)) {
2704
            api_not_allowed(true);
2705
        }
2706
2707
        $em = Database::getManager();
2708
        $fs = new Filesystem();
2709
2710
        $attachmentRepo = $em->getRepository(PortfolioAttachment::class);
2711
        $attachment = $attachmentRepo->findOneByPath($path);
2712
2713
        if (empty($attachment)) {
2714
            api_not_allowed(true);
2715
        }
2716
2717
        $originOwnerId = 0;
2718
        $itemId = 0;
2719
2720
        if (PortfolioAttachment::TYPE_ITEM === $attachment->getOriginType()) {
2721
            $item = $em->find(Portfolio::class, $attachment->getOrigin());
2722
            $originOwnerId = $item->getUser()->getId();
2723
            $itemId = $item->getId();
2724
        } elseif (PortfolioAttachment::TYPE_COMMENT === $attachment->getOriginType()) {
2725
            $comment = $em->find(PortfolioComment::class, $attachment->getOrigin());
2726
            $originOwnerId = $comment->getAuthor()->getId();
2727
            $itemId = $comment->getItem()->getId();
2728
        }
2729
2730
        if ($currentUserId !== $originOwnerId) {
2731
            api_not_allowed(true);
2732
        }
2733
2734
        $em->remove($attachment);
2735
        $em->flush();
2736
2737
        $userDirectory = UserManager::getUserPathById($originOwnerId, 'system');
2738
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2739
        $attachmentFilename = $attachmentsDirectory.$attachment->getPath();
2740
2741
        $fs->remove($attachmentFilename);
2742
2743
        if ($httpRequest->isXmlHttpRequest()) {
2744
            echo Display::return_message(get_lang('AttachmentFileDeleteSuccess'), 'success');
2745
        } else {
2746
            Display::addFlash(
2747
                Display::return_message(get_lang('AttachmentFileDeleteSuccess'), 'success')
2748
            );
2749
2750
            $url = $this->baseUrl.http_build_query(['action' => 'view', 'id' => $itemId]);
2751
2752
            if (PortfolioAttachment::TYPE_COMMENT === $attachment->getOriginType() && isset($comment)) {
2753
                $url .= '#comment-'.$comment->getId();
2754
            }
2755
2756
            header("Location: $url");
2757
        }
2758
2759
        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...
2760
    }
2761
2762
    /**
2763
     * @throws \Doctrine\ORM\OptimisticLockException
2764
     * @throws \Doctrine\ORM\ORMException
2765
     */
2766
    public function markAsHighlighted(Portfolio $item)
2767
    {
2768
        if ($item->getCourse()->getId() !== (int) api_get_course_int_id()) {
2769
            api_not_allowed(true);
2770
        }
2771
2772
        $item->setIsHighlighted(
2773
            !$item->isHighlighted()
2774
        );
2775
2776
        Database::getManager()->flush();
2777
2778
        if ($item->isHighlighted()) {
2779
            HookPortfolioItemHighlighted::create()
2780
                ->setEventData(['item' => $item])
2781
                ->notifyItemHighlighted()
2782
            ;
2783
        }
2784
2785
        Display::addFlash(
2786
            Display::return_message(
2787
                $item->isHighlighted() ? get_lang('MarkedAsHighlighted') : get_lang('UnmarkedAsHighlighted'),
2788
                'success'
2789
            )
2790
        );
2791
2792
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
2793
        exit;
2794
    }
2795
2796
    public function markAsTemplate(Portfolio $item)
2797
    {
2798
        if (!$this->itemBelongToOwner($item)) {
2799
            api_not_allowed(true);
2800
        }
2801
2802
        $item->setIsTemplate(
2803
            !$item->isTemplate()
2804
        );
2805
2806
        Database::getManager()->flush($item);
2807
2808
        Display::addFlash(
2809
            Display::return_message(
2810
                $item->isTemplate() ? get_lang('PortfolioItemSetAsTemplate') : get_lang('PortfolioItemUnsetAsTemplate'),
2811
                'success'
2812
            )
2813
        );
2814
2815
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
2816
        exit;
2817
    }
2818
2819
    public function markAsTemplateComment(PortfolioComment $comment)
2820
    {
2821
        if (!$this->commentBelongsToOwner($comment)) {
2822
            api_not_allowed(true);
2823
        }
2824
2825
        $comment->setIsTemplate(
2826
            !$comment->isTemplate()
2827
        );
2828
2829
        Database::getManager()->flush();
2830
2831
        Display::addFlash(
2832
            Display::return_message(
2833
                $comment->isTemplate() ? get_lang('PortfolioCommentSetAsTemplate') : get_lang('PortfolioCommentUnsetAsTemplate'),
2834
                'success'
2835
            )
2836
        );
2837
2838
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $comment->getItem()->getId()]));
2839
        exit;
2840
    }
2841
2842
    public function listTags(HttpRequest $request)
2843
    {
2844
        global $interbreadcrumb;
2845
2846
        api_protect_course_script();
2847
        api_protect_teacher_script();
2848
2849
        $em = Database::getManager();
2850
        $tagRepo = $em->getRepository(Tag::class);
2851
2852
        $tagsQuery = $tagRepo->findForPortfolioInCourseQuery($this->course, $this->session);
2853
2854
        $tag = $request->query->has('id')
2855
            ? $tagRepo->find($request->query->getInt('id'))
2856
            : null;
2857
2858
        $formAction = ['action' => $request->query->get('action')];
2859
2860
        if ($tag) {
2861
            $formAction['id'] = $tag->getId();
2862
        }
2863
2864
        $form = new FormValidator('frm_add_tag', 'post', $this->baseUrl.http_build_query($formAction));
2865
        $form->addText('name', get_lang('Tag'));
2866
2867
        if ($tag) {
2868
            $form->addButtonUpdate(get_lang('Edit'));
2869
        } else {
2870
            $form->addButtonCreate(get_lang('Add'));
2871
        }
2872
2873
        if ($form->validate()) {
2874
            $values = $form->exportValues();
2875
2876
            $extraFieldInfo = (new ExtraField('portfolio'))->get_handler_field_info_by_field_variable('tags');
2877
2878
            if (!$tag) {
2879
                $tag = (new Tag())->setCount(0);
2880
2881
                $portfolioRelTag = (new PortfolioRelTag())
2882
                    ->setTag($tag)
2883
                    ->setCourse($this->course)
2884
                    ->setSession($this->session)
2885
                ;
2886
2887
                $em->persist($tag);
2888
                $em->persist($portfolioRelTag);
2889
            }
2890
2891
            $tag
2892
                ->setTag($values['name'])
2893
                ->setFieldId((int) $extraFieldInfo['id'])
2894
            ;
2895
2896
            $em->flush();
2897
2898
            Display::addFlash(
2899
                Display::return_message(get_lang('TagSaved'), 'success')
2900
            );
2901
2902
            header('Location: '.$this->baseUrl.http_build_query($formAction));
2903
            exit();
2904
        } else {
2905
            $form->protect();
2906
2907
            if ($tag) {
2908
                $form->setDefaults(['name' => $tag->getTag()]);
2909
            }
2910
        }
2911
2912
        $langTags = get_lang('Tags');
2913
        $langEdit = get_lang('Edit');
2914
2915
        $deleteIcon = Display::return_icon('delete.png', get_lang('Delete'));
2916
        $editIcon = Display::return_icon('edit.png', $langEdit);
2917
2918
        $table = new SortableTable(
2919
            'portfolio_tags',
2920
            function () use ($tagsQuery) {
2921
                return (int) $tagsQuery
2922
                    ->select('COUNT(t)')
2923
                    ->getQuery()
2924
                    ->getSingleScalarResult()
2925
                ;
2926
            },
2927
            function ($from, $limit, $column, $direction) use ($tagsQuery) {
2928
                $data = [];
2929
2930
                /** @var array<int, Tag> $tags */
2931
                $tags = $tagsQuery
2932
                    ->select('t')
2933
                    ->orderBy('t.tag', $direction)
2934
                    ->setFirstResult($from)
2935
                    ->setMaxResults($limit)
2936
                    ->getQuery()
2937
                    ->getResult();
2938
2939
                foreach ($tags as $tag) {
2940
                    $data[] = [
2941
                        $tag->getTag(),
2942
                        $tag->getId(),
2943
                    ];
2944
                }
2945
2946
                return $data;
2947
            },
2948
            0,
2949
            40
2950
        );
2951
        $table->set_header(0, get_lang('Name'));
2952
        $table->set_header(1, get_lang('Actions'), false, ['class' => 'text-right'], ['class' => 'text-right']);
2953
        $table->set_column_filter(
2954
            1,
2955
            function ($id) use ($editIcon, $deleteIcon) {
2956
                $editParams = http_build_query(['action' => 'edit_tag', 'id' => $id]);
2957
                $deleteParams = http_build_query(['action' => 'delete_tag', 'id' => $id]);
2958
2959
                return Display::url($editIcon, $this->baseUrl.$editParams).PHP_EOL
2960
                    .Display::url($deleteIcon, $this->baseUrl.$deleteParams).PHP_EOL;
2961
            }
2962
        );
2963
        $table->set_additional_parameters(
2964
            [
2965
                'action' => 'tags',
2966
                'cidReq' => $this->course->getCode(),
2967
                'id_session' => $this->session ? $this->session->getId() : 0,
2968
                'gidReq' => 0,
2969
            ]
2970
        );
2971
2972
        $content = $form->returnForm().PHP_EOL
2973
            .$table->return_table();
2974
2975
        $interbreadcrumb[] = [
2976
            'name' => get_lang('Portfolio'),
2977
            'url' => $this->baseUrl,
2978
        ];
2979
2980
        $pageTitle = $langTags;
2981
2982
        if ($tag) {
2983
            $pageTitle = $langEdit;
2984
2985
            $interbreadcrumb[] = [
2986
                'name' => $langTags,
2987
                'url' => $this->baseUrl.'action=tags',
2988
            ];
2989
        }
2990
2991
        $this->renderView($content, $pageTitle);
2992
    }
2993
2994
    public function deleteTag(Tag $tag)
2995
    {
2996
        api_protect_course_script();
2997
        api_protect_teacher_script();
2998
2999
        $em = Database::getManager();
3000
        $portfolioTagRepo = $em->getRepository(PortfolioRelTag::class);
3001
3002
        $portfolioTag = $portfolioTagRepo
3003
            ->findOneBy(['tag' => $tag, 'course' => $this->course, 'session' => $this->session]);
3004
3005
        if ($portfolioTag) {
3006
            $em->remove($portfolioTag);
3007
            $em->flush();
3008
3009
            Display::addFlash(
3010
                Display::return_message(get_lang('TagDeleted'), 'success')
3011
            );
3012
        }
3013
3014
        header('Location: '.$this->baseUrl.http_build_query(['action' => 'tags']));
3015
        exit();
3016
    }
3017
3018
    /**
3019
     * @throws \Doctrine\ORM\OptimisticLockException
3020
     * @throws \Doctrine\ORM\ORMException
3021
     */
3022
    public function editComment(PortfolioComment $comment)
3023
    {
3024
        global $interbreadcrumb;
3025
3026
        if (!$this->commentBelongsToOwner($comment)) {
3027
            api_not_allowed(true);
3028
        }
3029
3030
        $item = $comment->getItem();
3031
        $commmentCourse = $item->getCourse();
3032
        $commmentSession = $item->getSession();
3033
3034
        $formAction = $this->baseUrl.http_build_query(['action' => 'edit_comment', 'id' => $comment->getId()]);
3035
3036
        $form = new FormValidator('frm_comment', 'post', $formAction);
3037
        $form->addLabel(
3038
            get_lang('Date'),
3039
            $this->getLabelForCommentDate($comment)
3040
        );
3041
        $form->addHtmlEditor('content', get_lang('Comments'), true, false, ['ToolbarSet' => 'Minimal']);
3042
        $form->applyFilter('content', 'trim');
3043
3044
        $this->addAttachmentsFieldToForm($form);
3045
3046
        $form->addButtonUpdate(get_lang('Update'));
3047
3048
        if ($form->validate()) {
3049
            if ($commmentCourse) {
3050
                api_item_property_update(
3051
                    api_get_course_info($commmentCourse->getCode()),
3052
                    TOOL_PORTFOLIO_COMMENT,
3053
                    $comment->getId(),
3054
                    'PortfolioCommentUpdated',
3055
                    api_get_user_id(),
3056
                    [],
3057
                    null,
3058
                    '',
3059
                    '',
3060
                    $commmentSession ? $commmentSession->getId() : 0
3061
                );
3062
            }
3063
3064
            $values = $form->exportValues();
3065
3066
            $comment->setContent($values['content']);
3067
3068
            $this->em->flush();
3069
3070
            $this->processAttachments(
3071
                $form,
3072
                $comment->getAuthor(),
3073
                $comment->getId(),
3074
                PortfolioAttachment::TYPE_COMMENT
3075
            );
3076
3077
            HookPortfolioCommentEdited::create()
3078
                ->setEventData(['comment' => $comment])
3079
                ->notifyCommentEdited()
3080
            ;
3081
3082
            Display::addFlash(
3083
                Display::return_message(get_lang('ItemUpdated'), 'success')
3084
            );
3085
3086
            header("Location: $this->baseUrl"
3087
                .http_build_query(['action' => 'view', 'id' => $item->getId()])
3088
                .'#comment-'.$comment->getId()
3089
            );
3090
            exit;
3091
        }
3092
3093
        $form->setDefaults([
3094
            'content' => $comment->getContent(),
3095
        ]);
3096
3097
        $interbreadcrumb[] = [
3098
            'name' => get_lang('Portfolio'),
3099
            'url' => $this->baseUrl,
3100
        ];
3101
        $interbreadcrumb[] = [
3102
            'name' => $item->getTitle(true),
3103
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
3104
        ];
3105
3106
        $actions = [];
3107
        $actions[] = Display::url(
3108
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
3109
            $this->baseUrl
3110
        );
3111
3112
        $content = $form->returnForm()
3113
            .PHP_EOL
3114
            .'<div class="row"> <div class="col-sm-8 col-sm-offset-2">'
3115
            .$this->generateAttachmentList($comment)
3116
            .'</div></div>';
3117
3118
        $this->renderView(
3119
            $content,
3120
            get_lang('EditPortfolioComment'),
3121
            $actions
3122
        );
3123
    }
3124
3125
    /**
3126
     * @throws \Doctrine\ORM\OptimisticLockException
3127
     * @throws \Doctrine\ORM\ORMException
3128
     */
3129
    public function deleteComment(PortfolioComment $comment)
3130
    {
3131
        if (!$this->commentBelongsToOwner($comment)) {
3132
            api_not_allowed(true);
3133
        }
3134
3135
        $this->em->remove($comment);
3136
3137
        $this->em
3138
            ->getRepository(PortfolioAttachment::class)
3139
            ->removeFromComment($comment);
3140
3141
        $this->em->flush();
3142
3143
        Display::addFlash(
3144
            Display::return_message(get_lang('CommentDeleted'), 'success')
3145
        );
3146
3147
        header("Location: $this->baseUrl");
3148
        exit;
3149
    }
3150
3151
    public function itemVisibilityChooser(Portfolio $item)
3152
    {
3153
        global $interbreadcrumb;
3154
3155
        if (!$this->itemBelongToOwner($item)) {
3156
            api_not_allowed(true);
3157
        }
3158
3159
        $em = Database::getManager();
3160
        $tblItemProperty = Database::get_course_table(TABLE_ITEM_PROPERTY);
3161
3162
        $courseId = $this->course->getId();
3163
        $sessionId = $this->session ? $this->session->getId() : 0;
3164
3165
        $formAction = $this->baseUrl.http_build_query(['action' => 'item_visiblity_choose', 'id' => $item->getId()]);
3166
3167
        $form = new FormValidator('visibility', 'post', $formAction);
3168
        CourseManager::addUserGroupMultiSelect($form, ['USER:'.$this->owner->getId()]);
3169
        $form->addLabel(
3170
            '',
3171
            Display::return_message(
3172
                get_lang('OnlySelectedUsersWillSeeTheContent')
3173
                    .'<br>'.get_lang('LeaveEmptyToEnableTheContentForEveryone'),
3174
                'info',
3175
                false
3176
            )
3177
        );
3178
        $form->addCheckBox('hidden', '', get_lang('HiddenButVisibleForMe'));
3179
        $form->addButtonSave(get_lang('Save'));
3180
3181
        if ($form->validate()) {
3182
            $values = $form->exportValues();
3183
            $recipients = CourseManager::separateUsersGroups($values['users'])['users'];
3184
            $courseInfo = api_get_course_info_by_id($courseId);
3185
3186
            Database::delete(
3187
                $tblItemProperty,
3188
                [
3189
                    'c_id = ? ' => [$courseId],
3190
                    'AND tool = ? AND ref = ? ' => [TOOL_PORTFOLIO, $item->getId()],
3191
                    'AND lastedit_type = ? ' => ['visible'],
3192
                ]
3193
            );
3194
3195
            if (empty($recipients) && empty($values['hidden'])) {
3196
                $item->setVisibility(Portfolio::VISIBILITY_VISIBLE);
3197
            } else {
3198
                if (empty($values['hidden'])) {
3199
                    foreach ($recipients as $userId) {
3200
                        api_item_property_update(
3201
                            $courseInfo,
3202
                            TOOL_PORTFOLIO,
3203
                            $item->getId(),
3204
                            'visible',
3205
                            api_get_user_id(),
3206
                            [],
3207
                            $userId,
3208
                            '',
3209
                            '',
3210
                            $sessionId
3211
                        );
3212
                    }
3213
                }
3214
3215
                $item->setVisibility(Portfolio::VISIBILITY_PER_USER);
3216
            }
3217
3218
            $em->flush();
3219
3220
            HookPortfolioItemVisibility::create()
3221
                ->setEventData([
3222
                    'item' => $item,
3223
                    'recipients' => array_values($recipients),
3224
                ])
3225
                ->notifyItemVisibility()
3226
            ;
3227
3228
            Display::addFlash(
3229
                Display::return_message(get_lang('VisibilityChanged'), 'success')
3230
            );
3231
3232
            header("Location: $formAction");
3233
            exit;
3234
        }
3235
3236
        $result = Database::select(
3237
            'to_user_id',
3238
            $tblItemProperty,
3239
            [
3240
                'where' => [
3241
                    'c_id = ? ' => [$courseId],
3242
                    'AND tool = ? AND ref = ? ' => [TOOL_PORTFOLIO, $item->getId()],
3243
                    'AND to_user_id IS NOT NULL ' => [],
3244
                ],
3245
            ]
3246
        );
3247
3248
        $recipients = array_map(
3249
            function (array $item): string {
3250
                return 'USER:'.$item['to_user_id'];
3251
            },
3252
            $result
3253
        );
3254
3255
        $defaults = ['users' => $recipients];
3256
3257
        if (empty($recipients) && Portfolio::VISIBILITY_PER_USER === $item->getVisibility()) {
3258
            $defaults['hidden'] = true;
3259
        }
3260
3261
        $form->setDefaults($defaults);
3262
        $form->protect();
3263
3264
        $interbreadcrumb[] = [
3265
            'name' => get_lang('Portfolio'),
3266
            'url' => $this->baseUrl,
3267
        ];
3268
        $interbreadcrumb[] = [
3269
            'name' => $item->getTitle(true),
3270
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
3271
        ];
3272
3273
        $actions = [];
3274
        $actions[] = Display::url(
3275
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
3276
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
3277
        );
3278
3279
        $this->renderView(
3280
            $form->returnForm(),
3281
            get_lang('ChooseRecipients'),
3282
            $actions
3283
        );
3284
    }
3285
3286
    public function commentVisibilityChooser(PortfolioComment $comment)
3287
    {
3288
        global $interbreadcrumb;
3289
3290
        if (!$this->commentBelongsToOwner($comment)) {
3291
            api_not_allowed(true);
3292
        }
3293
3294
        $em = Database::getManager();
3295
        $tblItemProperty = Database::get_course_table(TABLE_ITEM_PROPERTY);
3296
3297
        $courseId = $this->course->getId();
3298
        $sessionId = $this->session ? $this->session->getId() : 0;
3299
        $item = $comment->getItem();
3300
3301
        $formAction = $this->baseUrl.http_build_query(['action' => 'comment_visiblity_choose', 'id' => $comment->getId()]);
3302
3303
        $form = new FormValidator('visibility', 'post', $formAction);
3304
        CourseManager::addUserGroupMultiSelect($form, ['USER:'.$this->owner->getId()]);
3305
        $form->addLabel(
3306
            '',
3307
            Display::return_message(
3308
                get_lang('OnlySelectedUsersWillSeeTheContent')
3309
                    .'<br>'.get_lang('LeaveEmptyToEnableTheContentForEveryone'),
3310
                'info',
3311
                false
3312
            )
3313
        );
3314
        $form->addCheckBox('hidden', '', get_lang('HiddenButVisibleForMe'));
3315
        $form->addButtonSave(get_lang('Save'));
3316
3317
        if ($form->validate()) {
3318
            $values = $form->exportValues();
3319
            $recipients = CourseManager::separateUsersGroups($values['users'])['users'];
3320
            $courseInfo = api_get_course_info_by_id($courseId);
3321
3322
            Database::delete(
3323
                $tblItemProperty,
3324
                [
3325
                    'c_id = ? ' => [$courseId],
3326
                    'AND tool = ? AND ref = ? ' => [TOOL_PORTFOLIO_COMMENT, $comment->getId()],
3327
                    'AND lastedit_type = ? ' => ['visible'],
3328
                ]
3329
            );
3330
3331
            if (empty($recipients) && empty($values['hidden'])) {
3332
                $comment->setVisibility(PortfolioComment::VISIBILITY_VISIBLE);
3333
            } else {
3334
                if (empty($values['hidden'])) {
3335
                    foreach ($recipients as $userId) {
3336
                        api_item_property_update(
3337
                            $courseInfo,
3338
                            TOOL_PORTFOLIO_COMMENT,
3339
                            $comment->getId(),
3340
                            'visible',
3341
                            api_get_user_id(),
3342
                            [],
3343
                            $userId,
3344
                            '',
3345
                            '',
3346
                            $sessionId
3347
                        );
3348
                    }
3349
                }
3350
3351
                $comment->setVisibility(PortfolioComment::VISIBILITY_PER_USER);
3352
            }
3353
3354
            $em->flush();
3355
3356
            Display::addFlash(
3357
                Display::return_message(get_lang('VisibilityChanged'), 'success')
3358
            );
3359
3360
            header("Location: $formAction");
3361
            exit;
3362
        }
3363
3364
        $result = Database::select(
3365
            'to_user_id',
3366
            $tblItemProperty,
3367
            [
3368
                'where' => [
3369
                    'c_id = ? ' => [$courseId],
3370
                    'AND tool = ? AND ref = ? ' => [TOOL_PORTFOLIO_COMMENT, $comment->getId()],
3371
                    'AND to_user_id IS NOT NULL ' => [],
3372
                ],
3373
            ]
3374
        );
3375
3376
        $recipients = array_map(
3377
            function (array $itemProperty): string {
3378
                return 'USER:'.$itemProperty['to_user_id'];
3379
            },
3380
            $result
3381
        );
3382
3383
        $defaults = ['users' => $recipients];
3384
3385
        if (empty($recipients) && PortfolioComment::VISIBILITY_PER_USER === $comment->getVisibility()) {
3386
            $defaults['hidden'] = true;
3387
        }
3388
3389
        $form->setDefaults($defaults);
3390
        $form->protect();
3391
3392
        $interbreadcrumb[] = [
3393
            'name' => get_lang('Portfolio'),
3394
            'url' => $this->baseUrl,
3395
        ];
3396
        $interbreadcrumb[] = [
3397
            'name' => $item->getTitle(true),
3398
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
3399
        ];
3400
        $interbreadcrumb[] = [
3401
            'name' => $comment->getExcerpt(40),
3402
            'url' => $this->baseUrl
3403
                .http_build_query(['action' => 'view', 'id' => $item->getId()])
3404
                .'#comment-'.$comment->getId(),
3405
        ];
3406
3407
        $actions = [];
3408
        $actions[] = Display::url(
3409
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
3410
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
3411
        );
3412
3413
        $this->renderView(
3414
            $form->returnForm(),
3415
            get_lang('ChooseRecipients'),
3416
            $actions
3417
        );
3418
    }
3419
3420
    private function isAllowed(): bool
3421
    {
3422
        $isSubscribedInCourse = false;
3423
3424
        if ($this->course) {
3425
            $isSubscribedInCourse = CourseManager::is_user_subscribed_in_course(
3426
                api_get_user_id(),
3427
                $this->course->getCode(),
3428
                (bool) $this->session,
3429
                $this->session ? $this->session->getId() : 0
3430
            );
3431
        }
3432
3433
        if (!$this->course || $isSubscribedInCourse) {
3434
            return true;
3435
        }
3436
3437
        return false;
3438
    }
3439
3440
    private function blockIsNotAllowed()
3441
    {
3442
        if (!$this->isAllowed()) {
3443
            api_not_allowed(true);
3444
        }
3445
    }
3446
3447
    /**
3448
     * @param bool $showHeader
3449
     */
3450
    private function renderView(string $content, string $toolName, array $actions = [], $showHeader = true)
3451
    {
3452
        global $this_section;
3453
3454
        $this_section = $this->course ? SECTION_COURSES : SECTION_SOCIAL;
3455
3456
        $view = new Template($toolName);
3457
3458
        if ($showHeader) {
3459
            $view->assign('header', $toolName);
3460
        }
3461
3462
        $actionsStr = '';
3463
3464
        if ($this->course) {
3465
            $actionsStr .= Display::return_introduction_section(TOOL_PORTFOLIO);
3466
        }
3467
3468
        if ($actions) {
3469
            $actions = implode('', $actions);
3470
3471
            $actionsStr .= Display::toolbarAction('portfolio-toolbar', [$actions]);
3472
        }
3473
3474
        $view->assign('baseurl', $this->baseUrl);
3475
        $view->assign('actions', $actionsStr);
3476
3477
        $view->assign('content', $content);
3478
        $view->display_one_col_template();
3479
    }
3480
3481
    private function categoryBelongToOwner(PortfolioCategory $category): bool
3482
    {
3483
        if ($category->getUser()->getId() != $this->owner->getId()) {
3484
            return false;
3485
        }
3486
3487
        return true;
3488
    }
3489
3490
    private function addAttachmentsFieldToForm(FormValidator $form)
3491
    {
3492
        $form->addButton('add_attachment', get_lang('AddAttachment'), 'plus');
3493
        $form->addHtml('<div id="container-attachments" style="display: none;">');
3494
        $form->addFile('attachment_file[]', get_lang('FilesAttachment'));
3495
        $form->addText('attachment_comment[]', get_lang('Description'), false);
3496
        $form->addHtml('</div>');
3497
3498
        $script = "$(function () {
3499
            var attachmentsTemplate = $('#container-attachments').html();
3500
            var \$btnAdd = $('[name=\"add_attachment\"]');
3501
            var \$reference = \$btnAdd.parents('.form-group');
3502
3503
            \$btnAdd.on('click', function (e) {
3504
                e.preventDefault();
3505
3506
                $(attachmentsTemplate).insertBefore(\$reference);
3507
            });
3508
        })";
3509
3510
        $form->addHtml("<script>$script</script>");
3511
    }
3512
3513
    private function processAttachments(
3514
        FormValidator $form,
3515
        User $user,
3516
        int $originId,
3517
        int $originType
3518
    ) {
3519
        $em = Database::getManager();
3520
        $fs = new Filesystem();
3521
3522
        $comments = $form->getSubmitValue('attachment_comment');
3523
3524
        foreach ($_FILES['attachment_file']['error'] as $i => $attachmentFileError) {
3525
            if ($attachmentFileError != UPLOAD_ERR_OK) {
3526
                continue;
3527
            }
3528
3529
            $_file = [
3530
                'name' => $_FILES['attachment_file']['name'][$i],
3531
                'type' => $_FILES['attachment_file']['type'][$i],
3532
                'tmp_name' => $_FILES['attachment_file']['tmp_name'][$i],
3533
                'size' => $_FILES['attachment_file']['size'][$i],
3534
            ];
3535
3536
            if (empty($_file['type'])) {
3537
                $_file['type'] = DocumentManager::file_get_mime_type($_file['name']);
3538
            }
3539
3540
            $newFileName = add_ext_on_mime(stripslashes($_file['name']), $_file['type']);
3541
3542
            if (!filter_extension($newFileName)) {
3543
                Display::addFlash(Display::return_message(get_lang('UplUnableToSaveFileFilteredExtension'), 'error'));
3544
                continue;
3545
            }
3546
3547
            $newFileName = uniqid();
3548
            $attachmentsDirectory = UserManager::getUserPathById($user->getId(), 'system').'portfolio_attachments/';
3549
3550
            if (!$fs->exists($attachmentsDirectory)) {
3551
                $fs->mkdir($attachmentsDirectory, api_get_permissions_for_new_directories());
3552
            }
3553
3554
            $attachmentFilename = $attachmentsDirectory.$newFileName;
3555
3556
            if (is_uploaded_file($_file['tmp_name'])) {
3557
                $moved = move_uploaded_file($_file['tmp_name'], $attachmentFilename);
3558
3559
                if (!$moved) {
3560
                    Display::addFlash(Display::return_message(get_lang('UplUnableToSaveFile'), 'error'));
3561
                    continue;
3562
                }
3563
            }
3564
3565
            $attachment = new PortfolioAttachment();
3566
            $attachment
3567
                ->setFilename($_file['name'])
3568
                ->setComment($comments[$i])
3569
                ->setPath($newFileName)
3570
                ->setOrigin($originId)
3571
                ->setOriginType($originType)
3572
                ->setSize($_file['size']);
3573
3574
            $em->persist($attachment);
3575
            $em->flush();
3576
        }
3577
    }
3578
3579
    private function itemBelongToOwner(Portfolio $item): bool
3580
    {
3581
        if ($item->getUser()->getId() != $this->owner->getId()) {
3582
            return false;
3583
        }
3584
3585
        return true;
3586
    }
3587
3588
    private function commentBelongsToOwner(PortfolioComment $comment): bool
3589
    {
3590
        return $comment->getAuthor() === $this->owner;
3591
    }
3592
3593
    private function createFormTagFilter(bool $listByUser = false): FormValidator
3594
    {
3595
        $tags = Database::getManager()
3596
            ->getRepository(Tag::class)
3597
            ->findForPortfolioInCourseQuery($this->course, $this->session)
3598
            ->getQuery()
3599
            ->getResult()
3600
        ;
3601
3602
        $frmTagList = new FormValidator(
3603
            'frm_tag_list',
3604
            'get',
3605
            $this->baseUrl.($listByUser ? 'user='.$this->owner->getId() : ''),
3606
            '',
3607
            [],
3608
            FormValidator::LAYOUT_BOX
3609
        );
3610
3611
        $frmTagList->addDatePicker('date', get_lang('CreationDate'));
3612
3613
        $frmTagList->addSelectFromCollection(
3614
            'tags',
3615
            get_lang('Tags'),
3616
            $tags,
3617
            ['multiple' => 'multiple'],
3618
            false,
3619
            'getTag'
3620
        );
3621
3622
        $frmTagList->addText('text', get_lang('Search'), false)->setIcon('search');
3623
        $frmTagList->applyFilter('text', 'trim');
3624
        $frmTagList->addHtml('<br>');
3625
        $frmTagList->addButtonFilter(get_lang('Filter'));
3626
3627
        if ($this->course) {
3628
            $frmTagList->addHidden('cidReq', $this->course->getCode());
3629
            $frmTagList->addHidden('id_session', $this->session ? $this->session->getId() : 0);
3630
            $frmTagList->addHidden('gidReq', 0);
3631
            $frmTagList->addHidden('gradebook', 0);
3632
            $frmTagList->addHidden('origin', '');
3633
            $frmTagList->addHidden('categoryId', 0);
3634
            $frmTagList->addHidden('subCategoryIds', '');
3635
3636
            if ($listByUser) {
3637
                $frmTagList->addHidden('user', $this->owner->getId());
3638
            }
3639
        }
3640
3641
        return $frmTagList;
3642
    }
3643
3644
    /**
3645
     * @throws Exception
3646
     */
3647
    private function createFormStudentFilter(bool $listByUser = false, bool $listHighlighted = false, bool $listAlphabeticalOrder = false): FormValidator
3648
    {
3649
        $frmStudentList = new FormValidator(
3650
            'frm_student_list',
3651
            'get',
3652
            $this->baseUrl,
3653
            '',
3654
            [],
3655
            FormValidator::LAYOUT_BOX
3656
        );
3657
3658
        $urlParams = http_build_query(
3659
            [
3660
                'a' => 'search_user_by_course',
3661
                'course_id' => $this->course->getId(),
3662
                'session_id' => $this->session ? $this->session->getId() : 0,
3663
            ]
3664
        );
3665
3666
        /** @var SelectAjax $slctUser */
3667
        $slctUser = $frmStudentList->addSelectAjax(
3668
            'user',
3669
            get_lang('SelectLearnerPortfolio'),
3670
            [],
3671
            [
3672
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
3673
                'placeholder' => get_lang('SearchStudent'),
3674
                'formatResult' => SelectAjax::templateResultForUsersInCourse(),
3675
                'formatSelection' => SelectAjax::templateSelectionForUsersInCourse(),
3676
            ]
3677
        );
3678
3679
        if ($listByUser) {
3680
            $slctUser->addOption(
3681
                $this->owner->getCompleteName(),
3682
                $this->owner->getId(),
3683
                [
3684
                    'data-avatarurl' => UserManager::getUserPicture($this->owner->getId()),
3685
                    'data-username' => $this->owner->getUsername(),
3686
                ]
3687
            );
3688
3689
            $link = Display::url(
3690
                get_lang('BackToMainPortfolio'),
3691
                $this->baseUrl
3692
            );
3693
        } else {
3694
            $link = Display::url(
3695
                get_lang('SeeMyPortfolio'),
3696
                $this->baseUrl.http_build_query(['user' => api_get_user_id()])
3697
            );
3698
        }
3699
3700
        $frmStudentList->addHtml("<p>$link</p>");
3701
3702
        if ($listHighlighted) {
3703
            $link = Display::url(
3704
                get_lang('BackToMainPortfolio'),
3705
                $this->baseUrl
3706
            );
3707
        } else {
3708
            $link = Display::url(
3709
                get_lang('SeeHighlights'),
3710
                $this->baseUrl.http_build_query(['list_highlighted' => true])
3711
            );
3712
        }
3713
3714
        $frmStudentList->addHtml("<p>$link</p>");
3715
3716
        if (true !== api_get_configuration_value('portfolio_order_post_by_alphabetical_order')) {
3717
            if ($listAlphabeticalOrder) {
3718
                $link = Display::url(
3719
                    get_lang('BackToDateOrder'),
3720
                    $this->baseUrl
3721
                );
3722
            } else {
3723
                $link = Display::url(
3724
                    get_lang('SeeAlphabeticalOrder'),
3725
                    $this->baseUrl.http_build_query(['list_alphabetical' => true])
3726
                );
3727
            }
3728
3729
            $frmStudentList->addHtml("<p>$link</p>");
3730
        }
3731
3732
        return $frmStudentList;
3733
    }
3734
3735
    private function getCategoriesForIndex(?int $currentUserId = null, ?int $parentId = null): array
3736
    {
3737
        $categoriesCriteria = [];
3738
        if (isset($currentUserId)) {
3739
            $categoriesCriteria['user'] = $this->owner;
3740
        }
3741
        if (!api_is_platform_admin() && $currentUserId !== $this->owner->getId()) {
3742
            $categoriesCriteria['isVisible'] = true;
3743
        }
3744
        if (isset($parentId)) {
3745
            $categoriesCriteria['parentId'] = $parentId;
3746
        }
3747
3748
        return $this->em
3749
            ->getRepository(PortfolioCategory::class)
3750
            ->findBy($categoriesCriteria);
3751
    }
3752
3753
    private function getHighlightedItems()
3754
    {
3755
        $queryBuilder = $this->em->createQueryBuilder();
3756
        $queryBuilder
3757
            ->select('pi')
3758
            ->from(Portfolio::class, 'pi')
3759
            ->where('pi.course = :course')
3760
            ->andWhere('pi.isHighlighted = TRUE')
3761
            ->setParameter('course', $this->course);
3762
3763
        if ($this->session) {
3764
            $queryBuilder->andWhere('pi.session = :session');
3765
            $queryBuilder->setParameter('session', $this->session);
3766
        } else {
3767
            $queryBuilder->andWhere('pi.session IS NULL');
3768
        }
3769
3770
        if ($this->advancedSharingEnabled) {
3771
            $queryBuilder
3772
                ->leftJoin(
3773
                    CItemProperty::class,
3774
                    'cip',
3775
                    Join::WITH,
3776
                    "cip.ref = pi.id
3777
                        AND cip.tool = :cip_tool
3778
                        AND cip.course = pi.course
3779
                        AND cip.lasteditType = 'visible'
3780
                        AND cip.toUser = :current_user"
3781
                )
3782
                ->andWhere(
3783
                    sprintf(
3784
                        'pi.visibility = %d
3785
                            OR (
3786
                                pi.visibility = %d AND cip IS NOT NULL OR pi.user = :current_user
3787
                            )',
3788
                        Portfolio::VISIBILITY_VISIBLE,
3789
                        Portfolio::VISIBILITY_PER_USER
3790
                    )
3791
                )
3792
                ->setParameter('cip_tool', TOOL_PORTFOLIO)
3793
            ;
3794
        } else {
3795
            $visibilityCriteria = [Portfolio::VISIBILITY_VISIBLE];
3796
3797
            if (api_is_allowed_to_edit()) {
3798
                $visibilityCriteria[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
3799
            }
3800
3801
            $queryBuilder->andWhere(
3802
                $queryBuilder->expr()->orX(
3803
                    'pi.user = :current_user',
3804
                    $queryBuilder->expr()->andX(
3805
                        'pi.user != :current_user',
3806
                        $queryBuilder->expr()->in('pi.visibility', $visibilityCriteria)
3807
                    )
3808
                )
3809
            );
3810
        }
3811
3812
        $queryBuilder->setParameter('current_user', api_get_user_id());
3813
        $queryBuilder->orderBy('pi.creationDate', 'DESC');
3814
3815
        return $queryBuilder->getQuery()->getResult();
3816
    }
3817
3818
    private function getItemsForIndex(
3819
        bool $listByUser = false,
3820
        FormValidator $frmFilterList = null,
3821
        bool $alphabeticalOrder = false
3822
    ) {
3823
        $currentUserId = api_get_user_id();
3824
3825
        if ($this->course) {
3826
            $showBaseContentInSession = $this->session
3827
                && true === api_get_configuration_value('portfolio_show_base_course_post_in_sessions');
3828
3829
            $queryBuilder = $this->em->createQueryBuilder();
3830
            $queryBuilder
3831
                ->select('pi')
3832
                ->from(Portfolio::class, 'pi')
3833
                ->where('pi.course = :course');
3834
3835
            $queryBuilder->setParameter('course', $this->course);
3836
3837
            if ($this->session) {
3838
                $queryBuilder->andWhere(
3839
                    $showBaseContentInSession ? 'pi.session = :session OR pi.session IS NULL' : 'pi.session = :session'
3840
                );
3841
                $queryBuilder->setParameter('session', $this->session);
3842
            } else {
3843
                $queryBuilder->andWhere('pi.session IS NULL');
3844
            }
3845
3846
            if ($frmFilterList && $frmFilterList->validate()) {
3847
                $values = $frmFilterList->exportValues();
3848
3849
                if (!empty($values['date'])) {
3850
                    $queryBuilder
3851
                        ->andWhere('pi.creationDate >= :date')
3852
                        ->setParameter(':date', api_get_utc_datetime($values['date'], false, true))
3853
                    ;
3854
                }
3855
3856
                if (!empty($values['tags'])) {
3857
                    $queryBuilder
3858
                        ->innerJoin(ExtraFieldRelTag::class, 'efrt', Join::WITH, 'efrt.itemId = pi.id')
3859
                        ->innerJoin(ExtraFieldEntity::class, 'ef', Join::WITH, 'ef.id = efrt.fieldId')
3860
                        ->andWhere('ef.extraFieldType = :efType')
3861
                        ->andWhere('ef.variable = :variable')
3862
                        ->andWhere('efrt.tagId IN (:tags)');
3863
3864
                    $queryBuilder->setParameter('efType', ExtraFieldEntity::PORTFOLIO_TYPE);
3865
                    $queryBuilder->setParameter('variable', 'tags');
3866
                    $queryBuilder->setParameter('tags', $values['tags']);
3867
                }
3868
3869
                if (!empty($values['text'])) {
3870
                    $queryBuilder->andWhere(
3871
                        $queryBuilder->expr()->orX(
3872
                            $queryBuilder->expr()->like('pi.title', ':text'),
3873
                            $queryBuilder->expr()->like('pi.content', ':text')
3874
                        )
3875
                    );
3876
3877
                    $queryBuilder->setParameter('text', '%'.$values['text'].'%');
3878
                }
3879
3880
                // Filters by category level 0
3881
                $searchCategories = [];
3882
                if (!empty($values['categoryId'])) {
3883
                    $searchCategories[] = $values['categoryId'];
3884
                    $subCategories = $this->getCategoriesForIndex(null, $values['categoryId']);
3885
                    if (count($subCategories) > 0) {
3886
                        foreach ($subCategories as $subCategory) {
3887
                            $searchCategories[] = $subCategory->getId();
3888
                        }
3889
                    }
3890
                    $queryBuilder->andWhere('pi.category IN('.implode(',', $searchCategories).')');
3891
                }
3892
3893
                // Filters by sub-category, don't show the selected values
3894
                $diff = [];
3895
                if (!empty($values['subCategoryIds']) && !('all' === $values['subCategoryIds'])) {
3896
                    $subCategoryIds = explode(',', $values['subCategoryIds']);
3897
                    $diff = array_diff($searchCategories, $subCategoryIds);
3898
                } else {
3899
                    if (trim($values['subCategoryIds']) === '') {
3900
                        $diff = $searchCategories;
3901
                    }
3902
                }
3903
                if (!empty($diff)) {
3904
                    unset($diff[0]);
3905
                    if (!empty($diff)) {
3906
                        $queryBuilder->andWhere('pi.category NOT IN('.implode(',', $diff).')');
3907
                    }
3908
                }
3909
            }
3910
3911
            if ($listByUser) {
3912
                $queryBuilder
3913
                    ->andWhere('pi.user = :user')
3914
                    ->setParameter('user', $this->owner);
3915
            }
3916
3917
            if ($this->advancedSharingEnabled) {
3918
                $queryBuilder
3919
                    ->leftJoin(
3920
                        CItemProperty::class,
3921
                        'cip',
3922
                        Join::WITH,
3923
                        "cip.ref = pi.id
3924
                            AND cip.tool = :cip_tool
3925
                            AND cip.course = pi.course
3926
                            AND cip.lasteditType = 'visible'
3927
                            AND cip.toUser = :current_user"
3928
                    )
3929
                    ->andWhere(
3930
                        sprintf(
3931
                            'pi.visibility = %d
3932
                            OR (
3933
                                pi.visibility = %d AND cip IS NOT NULL OR pi.user = :current_user
3934
                            )',
3935
                            Portfolio::VISIBILITY_VISIBLE,
3936
                            Portfolio::VISIBILITY_PER_USER
3937
                        )
3938
                    )
3939
                    ->setParameter('cip_tool', TOOL_PORTFOLIO)
3940
                ;
3941
            } else {
3942
                $visibilityCriteria = [Portfolio::VISIBILITY_VISIBLE];
3943
3944
                if (api_is_allowed_to_edit()) {
3945
                    $visibilityCriteria[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
3946
                }
3947
3948
                $queryBuilder->andWhere(
3949
                    $queryBuilder->expr()->orX(
3950
                        'pi.user = :current_user',
3951
                        $queryBuilder->expr()->andX(
3952
                            'pi.user != :current_user',
3953
                            $queryBuilder->expr()->in('pi.visibility', $visibilityCriteria)
3954
                        )
3955
                    )
3956
                );
3957
            }
3958
3959
            $queryBuilder->setParameter('current_user', $currentUserId);
3960
            if ($alphabeticalOrder || true === api_get_configuration_value('portfolio_order_post_by_alphabetical_order')) {
3961
                $queryBuilder->orderBy('pi.title', 'ASC');
3962
            } else {
3963
                $queryBuilder->orderBy('pi.creationDate', 'DESC');
3964
            }
3965
3966
            $items = $queryBuilder->getQuery()->getResult();
3967
3968
            if ($showBaseContentInSession) {
3969
                $items = array_filter(
3970
                    $items,
3971
                    fn (Portfolio $item) => !($this->session && !$item->getSession() && $item->isDuplicatedInSession($this->session))
3972
                );
3973
            }
3974
3975
            return $items;
3976
        } else {
3977
            $itemsCriteria = [];
3978
            $itemsCriteria['category'] = null;
3979
            $itemsCriteria['user'] = $this->owner;
3980
3981
            if ($currentUserId !== $this->owner->getId()) {
3982
                $itemsCriteria['visibility'] = Portfolio::VISIBILITY_VISIBLE;
3983
            }
3984
3985
            $items = $this->em
3986
                ->getRepository(Portfolio::class)
3987
                ->findBy($itemsCriteria, ['creationDate' => 'DESC']);
3988
        }
3989
3990
        return $items;
3991
    }
3992
3993
    /**
3994
     * @throws \Doctrine\ORM\ORMException
3995
     * @throws \Doctrine\ORM\OptimisticLockException
3996
     * @throws \Doctrine\ORM\TransactionRequiredException
3997
     */
3998
    private function createCommentForm(Portfolio $item): string
3999
    {
4000
        $formAction = $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]);
4001
4002
        $templates = $this->em
4003
            ->getRepository(PortfolioComment::class)
4004
            ->findBy(
4005
                [
4006
                    'isTemplate' => true,
4007
                    'author' => $this->owner,
4008
                ]
4009
            );
4010
4011
        $form = new FormValidator('frm_comment', 'post', $formAction);
4012
        $form->addHeader(get_lang('AddNewComment'));
4013
        $form->addSelectFromCollection(
4014
            'template',
4015
            [
4016
                get_lang('Template'),
4017
                null,
4018
                '<span id="portfolio-spinner" class="fa fa-fw fa-spinner fa-spin" style="display: none;"
4019
                    aria-hidden="true" aria-label="'.get_lang('Loading').'"></span>',
4020
            ],
4021
            $templates,
4022
            [],
4023
            true,
4024
            'getExcerpt'
4025
        );
4026
        $form->addHtmlEditor('content', get_lang('Comments'), true, false, ['ToolbarSet' => 'Minimal']);
4027
        $form->addHidden('item', $item->getId());
4028
        $form->addHidden('parent', 0);
4029
        $form->applyFilter('content', 'trim');
4030
4031
        $this->addAttachmentsFieldToForm($form);
4032
4033
        $form->addButtonSave(get_lang('Save'));
4034
4035
        if ($form->validate()) {
4036
            if ($this->session
4037
                && true === api_get_configuration_value('portfolio_show_base_course_post_in_sessions')
4038
                && !$item->getSession()
4039
            ) {
4040
                $duplicate = $item->duplicateInSession($this->session);
4041
4042
                $this->em->persist($duplicate);
4043
                $this->em->flush();
4044
4045
                $item = $duplicate;
4046
4047
                $formAction = $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]);
4048
            }
4049
4050
            $values = $form->exportValues();
4051
4052
            $parentComment = $this->em->find(PortfolioComment::class, $values['parent']);
4053
4054
            $comment = new PortfolioComment();
4055
            $comment
4056
                ->setAuthor($this->owner)
4057
                ->setParent($parentComment)
4058
                ->setContent($values['content'])
4059
                ->setDate(api_get_utc_datetime(null, false, true))
4060
                ->setItem($item);
4061
4062
            $this->em->persist($comment);
4063
            $this->em->flush();
4064
4065
            $this->processAttachments(
4066
                $form,
4067
                $comment->getAuthor(),
4068
                $comment->getId(),
4069
                PortfolioAttachment::TYPE_COMMENT
4070
            );
4071
4072
            $hook = HookPortfolioItemCommented::create();
4073
            $hook->setEventData(['comment' => $comment]);
4074
            $hook->notifyItemCommented();
4075
4076
            PortfolioNotifier::notifyTeachersAndAuthor($comment);
4077
4078
            Display::addFlash(
4079
                Display::return_message(get_lang('CommentAdded'), 'success')
4080
            );
4081
4082
            header("Location: $formAction");
4083
            exit;
4084
        }
4085
4086
        $js = '<script>
4087
            $(function() {
4088
                $(\'#frm_comment_template\').on(\'change\', function () {
4089
                    $(\'#portfolio-spinner\').show();
4090
4091
                    $.getJSON(_p.web_ajax + \'portfolio.ajax.php?a=find_template_comment&comment=\' + this.value)
4092
                        .done(function(response) {
4093
                            CKEDITOR.instances.content.setData(response.content);
4094
                        })
4095
                        .fail(function () {
4096
                            CKEDITOR.instances.content.setData(\'\');
4097
                        })
4098
                        .always(function() {
4099
                          $(\'#portfolio-spinner\').hide();
4100
                        });
4101
                });
4102
            });
4103
        </script>';
4104
4105
        return $form->returnForm().$js;
4106
    }
4107
4108
    private function generateAttachmentList($post, bool $includeHeader = true): string
4109
    {
4110
        $attachmentsRepo = $this->em->getRepository(PortfolioAttachment::class);
4111
4112
        $postOwnerId = 0;
4113
4114
        if ($post instanceof Portfolio) {
4115
            $attachments = $attachmentsRepo->findFromItem($post);
4116
4117
            $postOwnerId = $post->getUser()->getId();
4118
        } elseif ($post instanceof PortfolioComment) {
4119
            $attachments = $attachmentsRepo->findFromComment($post);
4120
4121
            $postOwnerId = $post->getAuthor()->getId();
4122
        }
4123
4124
        if (empty($attachments)) {
4125
            return '';
4126
        }
4127
4128
        $currentUserId = api_get_user_id();
4129
4130
        $listItems = '<ul class="fa-ul">';
4131
4132
        $deleteIcon = Display::return_icon(
4133
            'delete.png',
4134
            get_lang('DeleteAttachment'),
4135
            ['style' => 'display: inline-block'],
4136
            ICON_SIZE_TINY
4137
        );
4138
        $deleteAttrs = ['class' => 'btn-portfolio-delete'];
4139
4140
        /** @var PortfolioAttachment $attachment */
4141
        foreach ($attachments as $attachment) {
4142
            $downloadParams = http_build_query(['action' => 'download_attachment', 'file' => $attachment->getPath()]);
4143
            $deleteParams = http_build_query(['action' => 'delete_attachment', 'file' => $attachment->getPath()]);
4144
4145
            $listItems .= '<li>'
4146
                .'<span class="fa-li fa fa-paperclip" aria-hidden="true"></span>'
4147
                .Display::url(
4148
                    Security::remove_XSS($attachment->getFilename()),
4149
                    $this->baseUrl.$downloadParams
4150
                );
4151
4152
            if ($currentUserId === $postOwnerId) {
4153
                $listItems .= PHP_EOL.Display::url($deleteIcon, $this->baseUrl.$deleteParams, $deleteAttrs);
4154
            }
4155
4156
            if ($attachment->getComment()) {
4157
                $listItems .= '<p class="text-muted">'.Security::remove_XSS($attachment->getComment()).'</p>';
4158
            }
4159
4160
            $listItems .= '</li>';
4161
        }
4162
4163
        $listItems .= '</ul>';
4164
4165
        if ($includeHeader) {
4166
            $listItems = '<h1 class="h4">'.get_lang('FilesAttachment').'</h1>'
4167
                .$listItems;
4168
        }
4169
4170
        return $listItems;
4171
    }
4172
4173
    private function generateItemContent(Portfolio $item): string
4174
    {
4175
        $originId = $item->getOrigin();
4176
4177
        if (empty($originId)) {
4178
            return $item->getContent();
4179
        }
4180
4181
        $em = Database::getManager();
4182
4183
        $originContent = '';
4184
        $originContentFooter = '';
4185
4186
        if (Portfolio::TYPE_ITEM === $item->getOriginType()) {
4187
            $origin = $em->find(Portfolio::class, $item->getOrigin());
4188
4189
            if ($origin) {
4190
                $originContent = Security::remove_XSS($origin->getContent());
4191
                $originContentFooter = vsprintf(
4192
                    get_lang('OriginallyPublishedAsXTitleByYUser'),
4193
                    [
4194
                        "<cite>{$origin->getTitle(true)}</cite>",
4195
                        $origin->getUser()->getCompleteName(),
4196
                    ]
4197
                );
4198
            }
4199
        } elseif (Portfolio::TYPE_COMMENT === $item->getOriginType()) {
4200
            $origin = $em->find(PortfolioComment::class, $item->getOrigin());
4201
4202
            if ($origin) {
4203
                $originContent = Security::remove_XSS($origin->getContent());
4204
                $originContentFooter = vsprintf(
4205
                    get_lang('OriginallyCommentedByXUserInYItem'),
4206
                    [
4207
                        $origin->getAuthor()->getCompleteName(),
4208
                        "<cite>{$origin->getItem()->getTitle(true)}</cite>",
4209
                    ]
4210
                );
4211
            }
4212
        }
4213
4214
        if ($originContent) {
4215
            return "<figure>
4216
                    <blockquote>$originContent</blockquote>
4217
                    <figcaption style=\"margin-bottom: 10px;\">$originContentFooter</figcaption>
4218
                </figure>
4219
                <div class=\"clearfix\">".Security::remove_XSS($item->getContent()).'</div>'
4220
            ;
4221
        }
4222
4223
        return Security::remove_XSS($item->getContent());
4224
    }
4225
4226
    private function getItemsInHtmlFormatted(array $items): array
4227
    {
4228
        $itemsHtml = [];
4229
4230
        /** @var Portfolio $item */
4231
        foreach ($items as $item) {
4232
            $itemCourse = $item->getCourse();
4233
            $itemSession = $item->getSession();
4234
4235
            $creationDate = api_convert_and_format_date($item->getCreationDate());
4236
            $updateDate = api_convert_and_format_date($item->getUpdateDate());
4237
4238
            $metadata = '<ul class="list-unstyled text-muted">';
4239
4240
            if ($itemSession) {
4241
                $metadata .= '<li>'.get_lang('Course').': '.$itemSession->getName().' ('
4242
                    .$itemCourse->getTitle().') </li>';
4243
            } elseif ($itemCourse) {
4244
                $metadata .= '<li>'.get_lang('Course').': '.$itemCourse->getTitle().'</li>';
4245
            }
4246
4247
            $metadata .= '<li>'.sprintf(get_lang('CreationDateXDate'), $creationDate).'</li>';
4248
4249
            if ($itemCourse) {
4250
                $propertyInfo = api_get_item_property_info(
4251
                    $itemCourse->getId(),
4252
                    TOOL_PORTFOLIO,
4253
                    $item->getId(),
4254
                    $itemSession ? $itemSession->getId() : 0
4255
                );
4256
4257
                if ($propertyInfo) {
4258
                    $metadata .= '<li>'
4259
                        .sprintf(
4260
                            get_lang('UpdatedOnDateXByUserY'),
4261
                            api_convert_and_format_date($propertyInfo['lastedit_date'], DATE_TIME_FORMAT_LONG),
4262
                            api_get_user_entity($propertyInfo['lastedit_user_id'])->getCompleteName()
4263
                        )
4264
                        .'</li>';
4265
                }
4266
            } else {
4267
                $metadata .= '<li>'.sprintf(get_lang('UpdateDateXDate'), $updateDate).'</li>';
4268
            }
4269
4270
            if ($item->getCategory()) {
4271
                $metadata .= '<li>'.sprintf(get_lang('CategoryXName'), $item->getCategory()->getTitle()).'</li>';
4272
            }
4273
4274
            $metadata .= '</ul>';
4275
4276
            $itemContent = $this->generateItemContent($item);
4277
4278
            $itemsHtml[] = Display::panel($itemContent, Security::remove_XSS($item->getTitle()), '', 'info', $metadata);
4279
        }
4280
4281
        return $itemsHtml;
4282
    }
4283
4284
    private function getCommentsInHtmlFormatted(array $comments): array
4285
    {
4286
        $commentsHtml = [];
4287
4288
        /** @var PortfolioComment $comment */
4289
        foreach ($comments as $comment) {
4290
            $item = $comment->getItem();
4291
            $date = api_convert_and_format_date($comment->getDate());
4292
4293
            $metadata = '<ul class="list-unstyled text-muted">';
4294
            $metadata .= '<li>'.sprintf(get_lang('DateXDate'), $date).'</li>';
4295
            $metadata .= '<li>'.sprintf(get_lang('PortfolioItemTitleXName'), Security::remove_XSS($item->getTitle()))
4296
                .'</li>';
4297
            $metadata .= '</ul>';
4298
4299
            $commentsHtml[] = Display::panel(
4300
                Security::remove_XSS($comment->getContent()),
4301
                '',
4302
                '',
4303
                'default',
4304
                $metadata
4305
            );
4306
        }
4307
4308
        return $commentsHtml;
4309
    }
4310
4311
    /**
4312
     * @param string $htmlContent
4313
     * @param array $imagePaths Relative paths found in $htmlContent
4314
     *
4315
     * @return string
4316
     */
4317
    private function fixMediaSourcesToHtml(string $htmlContent, array &$imagePaths): string
4318
    {
4319
        $doc = new DOMDocument();
4320
        @$doc->loadHTML($htmlContent);
4321
4322
        $tagsWithSrc = ['img', 'video', 'audio', 'source'];
4323
        /** @var array<int, \DOMElement> $elements */
4324
        $elements = [];
4325
4326
        foreach ($tagsWithSrc as $tag) {
4327
            foreach ($doc->getElementsByTagName($tag) as $element) {
4328
                if ($element->hasAttribute('src')) {
4329
                    $elements[] = $element;
4330
                }
4331
            }
4332
        }
4333
4334
        if (empty($elements)) {
4335
            return $htmlContent;
4336
        }
4337
4338
        /** @var array<int, \DOMElement> $anchorElements */
4339
        $anchorElements = $doc->getElementsByTagName('a');
4340
4341
        $webPath = api_get_path(WEB_PATH);
4342
        $sysPath = rtrim(api_get_path(SYS_PATH), '/');
4343
4344
        $paths = [
4345
            '/app/upload/' => $sysPath,
4346
            '/courses/' => $sysPath.'/app'
4347
        ];
4348
4349
        foreach ($elements as $element) {
4350
            $src = trim($element->getAttribute('src'));
4351
4352
            if (!str_starts_with($src, '/')
4353
                && !str_starts_with($src, $webPath)
4354
            ) {
4355
                continue;
4356
            }
4357
4358
            // to search anchors linking to files
4359
            if ($anchorElements->length > 0) {
4360
                foreach ($anchorElements as $anchorElement) {
4361
                    if (!$anchorElement->hasAttribute('href')) {
4362
                        continue;
4363
                    }
4364
4365
                    if ($src === $anchorElement->getAttribute('href')) {
4366
                        $anchorElement->setAttribute('href', basename($src));
4367
                    }
4368
                }
4369
            }
4370
4371
            $src = str_replace($webPath, '/', $src);
4372
4373
            foreach ($paths as $prefix => $basePath) {
4374
                if (str_starts_with($src, $prefix)) {
4375
                    $imagePaths[] = $basePath.urldecode($src);
4376
                    $element->setAttribute('src', basename($src));
4377
                }
4378
            }
4379
        }
4380
4381
        return $doc->saveHTML();
4382
    }
4383
4384
    private function formatZipIndexFile(HTML_Table $tblItems, HTML_Table $tblComments): string
4385
    {
4386
        $htmlContent = Display::page_header($this->owner->getCompleteNameWithUsername());
4387
        $htmlContent .= Display::page_subheader2(get_lang('PortfolioItems'));
4388
4389
        $htmlContent .= $tblItems->getRowCount() > 0
4390
            ? $tblItems->toHtml()
4391
            : Display::return_message(get_lang('NoItemsInYourPortfolio'), 'warning');
4392
4393
        $htmlContent .= Display::page_subheader2(get_lang('PortfolioCommentsMade'));
4394
4395
        $htmlContent .= $tblComments->getRowCount() > 0
4396
            ? $tblComments->toHtml()
4397
            : Display::return_message(get_lang('YouHaveNotCommented'), 'warning');
4398
4399
        $webAssetsPath = api_get_path(WEB_PUBLIC_PATH).'assets/';
4400
4401
        $doc = new DOMDocument();
4402
        @$doc->loadHTML($htmlContent);
4403
4404
        $stylesheet1 = $doc->createElement('link');
4405
        $stylesheet1->setAttribute('rel', 'stylesheet');
4406
        $stylesheet1->setAttribute('href', $webAssetsPath.'bootstrap/dist/css/bootstrap.min.css');
4407
        $stylesheet2 = $doc->createElement('link');
4408
        $stylesheet2->setAttribute('rel', 'stylesheet');
4409
        $stylesheet2->setAttribute('href', $webAssetsPath.'fontawesome/css/font-awesome.min.css');
4410
        $stylesheet3 = $doc->createElement('link');
4411
        $stylesheet3->setAttribute('rel', 'stylesheet');
4412
        $stylesheet3->setAttribute('href', ChamiloApi::getEditorDocStylePath());
4413
4414
        $head = $doc->createElement('head');
4415
        $head->appendChild($stylesheet1);
4416
        $head->appendChild($stylesheet2);
4417
        $head->appendChild($stylesheet3);
4418
4419
        $doc->documentElement->insertBefore(
4420
            $head,
4421
            $doc->getElementsByTagName('body')->item(0)
4422
        );
4423
4424
        return $doc->saveHTML();
4425
    }
4426
4427
    /**
4428
     * It parsers a title for a variable in lang.
4429
     *
4430
     * @param $defaultDisplayText
4431
     *
4432
     * @return string
4433
     */
4434
    private function getLanguageVariable($defaultDisplayText)
4435
    {
4436
        $variableLanguage = api_replace_dangerous_char(strtolower($defaultDisplayText));
4437
        $variableLanguage = preg_replace('/[^A-Za-z0-9\_]/', '', $variableLanguage); // Removes special chars except underscore.
4438
        if (is_numeric($variableLanguage[0])) {
4439
            $variableLanguage = '_'.$variableLanguage;
4440
        }
4441
        $variableLanguage = api_underscore_to_camel_case($variableLanguage);
4442
4443
        return $variableLanguage;
4444
    }
4445
4446
    /**
4447
     * It translates the text as parameter.
4448
     *
4449
     * @param $defaultDisplayText
4450
     *
4451
     * @return mixed
4452
     */
4453
    private function translateDisplayName($defaultDisplayText)
4454
    {
4455
        $variableLanguage = $this->getLanguageVariable($defaultDisplayText);
4456
4457
        return isset($GLOBALS[$variableLanguage]) ? $GLOBALS[$variableLanguage] : $defaultDisplayText;
4458
    }
4459
4460
    private function getCommentsForIndex(FormValidator $frmFilterList = null): array
4461
    {
4462
        if (null === $frmFilterList) {
4463
            return [];
4464
        }
4465
4466
        if (!$frmFilterList->validate()) {
4467
            return [];
4468
        }
4469
4470
        $values = $frmFilterList->exportValues();
4471
4472
        if (empty($values['date']) && empty($values['text'])) {
4473
            return [];
4474
        }
4475
4476
        $queryBuilder = $this->em->createQueryBuilder()
4477
            ->select('c')
4478
            ->from(PortfolioComment::class, 'c')
4479
        ;
4480
4481
        if (!empty($values['date'])) {
4482
            $queryBuilder
4483
                ->andWhere('c.date >= :date')
4484
                ->setParameter(':date', api_get_utc_datetime($values['date'], false, true))
4485
            ;
4486
        }
4487
4488
        if (!empty($values['text'])) {
4489
            $queryBuilder
4490
                ->andWhere('c.content LIKE :text')
4491
                ->setParameter('text', '%'.$values['text'].'%')
4492
            ;
4493
        }
4494
4495
        if ($this->advancedSharingEnabled) {
4496
            $queryBuilder
4497
                ->leftJoin(
4498
                    CItemProperty::class,
4499
                    'cip',
4500
                    Join::WITH,
4501
                    "cip.ref = c.id
4502
                        AND cip.tool = :cip_tool
4503
                        AND cip.course = :course
4504
                        AND cip.lasteditType = 'visible'
4505
                        AND cip.toUser = :current_user"
4506
                )
4507
                ->andWhere(
4508
                    sprintf(
4509
                        'c.visibility = %d
4510
                            OR (
4511
                                c.visibility = %d AND cip IS NOT NULL OR c.author = :current_user
4512
                            )',
4513
                        PortfolioComment::VISIBILITY_VISIBLE,
4514
                        PortfolioComment::VISIBILITY_PER_USER
4515
                    )
4516
                )
4517
                ->setParameter('cip_tool', TOOL_PORTFOLIO_COMMENT)
4518
                ->setParameter('current_user', $this->owner->getId())
4519
                ->setParameter('course', $this->course)
4520
            ;
4521
        }
4522
4523
        $queryBuilder->orderBy('c.date', 'DESC');
4524
4525
        return $queryBuilder->getQuery()->getResult();
4526
    }
4527
4528
    private function getLabelForCommentDate(PortfolioComment $comment): string
4529
    {
4530
        $item = $comment->getItem();
4531
        $commmentCourse = $item->getCourse();
4532
        $commmentSession = $item->getSession();
4533
4534
        $dateLabel = Display::dateToStringAgoAndLongDate($comment->getDate()).PHP_EOL;
4535
4536
        if ($commmentCourse) {
4537
            $propertyInfo = api_get_item_property_info(
4538
                $commmentCourse->getId(),
4539
                TOOL_PORTFOLIO_COMMENT,
4540
                $comment->getId(),
4541
                $commmentSession ? $commmentSession->getId() : 0
4542
            );
4543
4544
            if ($propertyInfo) {
4545
                $dateLabel .= '|'.PHP_EOL
4546
                    .sprintf(
4547
                        get_lang('UpdatedDateX'),
4548
                        Display::dateToStringAgoAndLongDate($propertyInfo['lastedit_date'])
4549
                    );
4550
            }
4551
        }
4552
4553
        return $dateLabel;
4554
    }
4555
}
4556