Passed
Pull Request — 1.11.x (#6001)
by Angel Fernando Quiroz
11:43
created

PortfolioController::qualifyComment()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 72
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 44
c 0
b 0
f 0
dl 0
loc 72
rs 9.216
cc 2
nc 2
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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