Passed
Pull Request — 1.11.x (#4368)
by Angel Fernando Quiroz
08:07
created

PortfolioController::fixImagesSourcesToHtml()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 42
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 22
nc 6
nop 1
dl 0
loc 42
rs 8.9457
c 0
b 0
f 0
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\Tag;
14
use Chamilo\UserBundle\Entity\User;
15
use Doctrine\ORM\Query\Expr\Join;
16
use Symfony\Component\Filesystem\Filesystem;
17
use Symfony\Component\HttpFoundation\Request as HttpRequest;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, HttpRequest. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
18
19
/**
20
 * Class PortfolioController.
21
 */
22
class PortfolioController
23
{
24
    /**
25
     * @var string
26
     */
27
    public $baseUrl;
28
    /**
29
     * @var CourseEntity|null
30
     */
31
    private $course;
32
    /**
33
     * @var \Chamilo\CoreBundle\Entity\Session|null
34
     */
35
    private $session;
36
    /**
37
     * @var \Chamilo\UserBundle\Entity\User
38
     */
39
    private $owner;
40
    /**
41
     * @var \Doctrine\ORM\EntityManager
42
     */
43
    private $em;
44
45
    /**
46
     * PortfolioController constructor.
47
     */
48
    public function __construct()
49
    {
50
        $this->em = Database::getManager();
51
52
        $this->owner = api_get_user_entity(api_get_user_id());
53
        $this->course = api_get_course_entity(api_get_course_int_id());
54
        $this->session = api_get_session_entity(api_get_session_id());
55
56
        $cidreq = api_get_cidreq();
57
        $this->baseUrl = api_get_self().'?'.($cidreq ? $cidreq.'&' : '');
58
    }
59
60
    /**
61
     * @throws \Doctrine\ORM\ORMException
62
     * @throws \Doctrine\ORM\OptimisticLockException
63
     */
64
    public function translateCategory($category, $languages, $languageId)
65
    {
66
        global $interbreadcrumb;
67
68
        $originalName = $category->getTitle();
69
        $variableLanguage = '$'.$this->getLanguageVariable($originalName);
70
71
        $translateUrl = api_get_path(WEB_AJAX_PATH).'lang.ajax.php?a=translate_portfolio_category&sec_token='.Security::get_token();
72
        $form = new FormValidator('new_lang_variable', 'POST', $translateUrl);
73
        $form->addHeader(get_lang('AddWordForTheSubLanguage'));
74
        $form->addText('variable_language', get_lang('LanguageVariable'), false);
75
        $form->addText('original_name', get_lang('OriginalName'), false);
76
77
        $languagesOptions = [0 => get_lang('None')];
78
        foreach ($languages as $language) {
79
            $languagesOptions[$language->getId()] = $language->getOriginalName();
80
        }
81
82
        $form->addSelect(
83
            'sub_language',
84
            [get_lang('SubLanguage'), get_lang('OnlyActiveSubLanguagesAreListed')],
85
            $languagesOptions
86
        );
87
88
        if ($languageId) {
89
            $languageInfo = api_get_language_info($languageId);
90
            $form->addText(
91
                'new_language',
92
                [get_lang('Translation'), get_lang('IfThisTranslationExistsThisWillReplaceTheTerm')]
93
            );
94
95
            $form->addHidden('category_id', $category->getId());
96
            $form->addHidden('id', $languageInfo['parent_id']);
97
            $form->addHidden('sub', $languageInfo['id']);
98
            $form->addHidden('sub_language_id', $languageInfo['id']);
99
            $form->addHidden('redirect', true);
100
            $form->addButtonSave(get_lang('Save'));
101
        }
102
103
        $form->setDefaults([
104
            'variable_language' => $variableLanguage,
105
            'original_name' => $originalName,
106
            'sub_language' => $languageId,
107
        ]);
108
        $form->addRule('sub_language', get_lang('Required'), 'required');
109
        $form->freeze(['variable_language', 'original_name']);
110
111
        $interbreadcrumb[] = [
112
            'name' => get_lang('Portfolio'),
113
            'url' => $this->baseUrl,
114
        ];
115
        $interbreadcrumb[] = [
116
            'name' => get_lang('Categories'),
117
            'url' => $this->baseUrl.'action=list_categories&parent_id='.$category->getParentId(),
118
        ];
119
        $interbreadcrumb[] = [
120
            'name' => Security::remove_XSS($category->getTitle()),
121
            'url' => $this->baseUrl.'action=edit_category&id='.$category->getId(),
122
        ];
123
124
        $actions = [];
125
        $actions[] = Display::url(
126
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
127
            $this->baseUrl.'action=edit_category&id='.$category->getId()
128
        );
129
130
        $js = '<script>
131
            $(function() {
132
              $("select[name=\'sub_language\']").on("change", function () {
133
                    location.href += "&sub_language=" + this.value;
134
                });
135
            });
136
        </script>';
137
        $content = $form->returnForm();
138
139
        $this->renderView($content.$js, get_lang('TranslateCategory'), $actions);
140
    }
141
142
    /**
143
     * @throws \Doctrine\ORM\ORMException
144
     * @throws \Doctrine\ORM\OptimisticLockException
145
     */
146
    public function listCategories()
147
    {
148
        global $interbreadcrumb;
149
150
        $parentId = isset($_REQUEST['parent_id']) ? (int) $_REQUEST['parent_id'] : 0;
151
        $table = new HTML_Table(['class' => 'table table-hover table-striped data_table']);
152
        $headers = [
153
            get_lang('Title'),
154
            get_lang('Description'),
155
        ];
156
        if ($parentId === 0) {
157
            $headers[] = get_lang('SubCategories');
158
        }
159
        $headers[] = get_lang('Actions');
160
161
        $column = 0;
162
        foreach ($headers as $header) {
163
            $table->setHeaderContents(0, $column, $header);
164
            $column++;
165
        }
166
        $currentUserId = api_get_user_id();
167
        $row = 1;
168
        $categories = $this->getCategoriesForIndex(null, $parentId);
169
170
        foreach ($categories as $category) {
171
            $column = 0;
172
            $subcategories = $this->getCategoriesForIndex(null, $category->getId());
173
            $linkSubCategories = $category->getTitle();
174
            if (count($subcategories) > 0) {
175
                $linkSubCategories = Display::url(
176
                    $category->getTitle(),
177
                    $this->baseUrl.'action=list_categories&parent_id='.$category->getId()
178
                );
179
            }
180
            $table->setCellContents($row, $column++, $linkSubCategories);
181
            $table->setCellContents($row, $column++, strip_tags($category->getDescription()));
182
            if ($parentId === 0) {
183
                $table->setCellContents($row, $column++, count($subcategories));
184
            }
185
186
            // Actions
187
            $links = null;
188
            // Edit action
189
            $url = $this->baseUrl.'action=edit_category&id='.$category->getId();
190
            $links .= Display::url(Display::return_icon('edit.png', get_lang('Edit')), $url).'&nbsp;';
191
            // Visible action : if active
192
            if ($category->isVisible() != 0) {
193
                $url = $this->baseUrl.'action=hide_category&id='.$category->getId();
194
                $links .= Display::url(Display::return_icon('visible.png', get_lang('Hide')), $url).'&nbsp;';
195
            } else { // else if not active
196
                $url = $this->baseUrl.'action=show_category&id='.$category->getId();
197
                $links .= Display::url(Display::return_icon('invisible.png', get_lang('Show')), $url).'&nbsp;';
198
            }
199
            // Delete action
200
            $url = $this->baseUrl.'action=delete_category&id='.$category->getId();
201
            $links .= Display::url(Display::return_icon('delete.png', get_lang('Delete')), $url, ['onclick' => 'javascript:if(!confirm(\''.get_lang('AreYouSureToDeleteJS').'\')) return false;']);
202
203
            $table->setCellContents($row, $column++, $links);
204
            $row++;
205
        }
206
207
        $interbreadcrumb[] = [
208
            'name' => get_lang('Portfolio'),
209
            'url' => $this->baseUrl,
210
        ];
211
        if ($parentId > 0) {
212
            $interbreadcrumb[] = [
213
                'name' => get_lang('Categories'),
214
                'url' => $this->baseUrl.'action=list_categories',
215
            ];
216
        }
217
218
        $actions = [];
219
        $actions[] = Display::url(
220
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
221
            $this->baseUrl.($parentId > 0 ? 'action=list_categories' : '')
222
        );
223
        if ($currentUserId == $this->owner->getId() && $parentId === 0) {
224
            $actions[] = Display::url(
225
                Display::return_icon('new_folder.png', get_lang('AddCategory'), [], ICON_SIZE_MEDIUM),
226
                $this->baseUrl.'action=add_category'
227
            );
228
        }
229
        $content = $table->toHtml();
230
231
        $pageTitle = get_lang('Categories');
232
        if ($parentId > 0) {
233
            $em = Database::getManager();
234
            $parentCategory = $em->find('ChamiloCoreBundle:PortfolioCategory', $parentId);
235
            $pageTitle = $parentCategory->getTitle().' : '.get_lang('SubCategories');
236
        }
237
238
        $this->renderView($content, $pageTitle, $actions);
239
    }
240
241
    /**
242
     * @throws \Doctrine\ORM\ORMException
243
     * @throws \Doctrine\ORM\OptimisticLockException
244
     */
245
    public function addCategory()
246
    {
247
        global $interbreadcrumb;
248
249
        Display::addFlash(
250
            Display::return_message(get_lang('PortfolioCategoryFieldHelp'), 'info')
251
        );
252
253
        $form = new FormValidator('add_category', 'post', "{$this->baseUrl}&action=add_category");
254
255
        if (api_get_configuration_value('save_titles_as_html')) {
256
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
257
        } else {
258
            $form->addText('title', get_lang('Title'));
259
            $form->applyFilter('title', 'trim');
260
        }
261
262
        $form->addHtmlEditor('description', get_lang('Description'), false, false, ['ToolbarSet' => 'Minimal']);
263
264
        $parentSelect = $form->addSelect(
265
            'parent_id',
266
            get_lang('ParentCategory')
267
        );
268
        $parentSelect->addOption(get_lang('Level0'), 0);
269
        $currentUserId = api_get_user_id();
270
        $categories = $this->getCategoriesForIndex(null, 0);
271
        foreach ($categories as $category) {
272
            $parentSelect->addOption($category->getTitle(), $category->getId());
273
        }
274
275
        $form->addButtonCreate(get_lang('Create'));
276
277
        if ($form->validate()) {
278
            $values = $form->exportValues();
279
280
            $category = new PortfolioCategory();
281
            $category
282
                ->setTitle($values['title'])
283
                ->setDescription($values['description'])
284
                ->setParentId($values['parent_id'])
285
                ->setUser($this->owner);
286
287
            $this->em->persist($category);
288
            $this->em->flush();
289
290
            Display::addFlash(
291
                Display::return_message(get_lang('CategoryAdded'), 'success')
292
            );
293
294
            header("Location: {$this->baseUrl}action=list_categories");
295
            exit;
296
        }
297
298
        $interbreadcrumb[] = [
299
            'name' => get_lang('Portfolio'),
300
            'url' => $this->baseUrl,
301
        ];
302
        $interbreadcrumb[] = [
303
            'name' => get_lang('Categories'),
304
            'url' => $this->baseUrl.'action=list_categories',
305
        ];
306
307
        $actions = [];
308
        $actions[] = Display::url(
309
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
310
            $this->baseUrl.'action=list_categories'
311
        );
312
313
        $content = $form->returnForm();
314
315
        $this->renderView($content, get_lang('AddCategory'), $actions);
316
    }
317
318
    /**
319
     * @throws \Doctrine\ORM\ORMException
320
     * @throws \Doctrine\ORM\OptimisticLockException
321
     * @throws \Exception
322
     */
323
    public function editCategory(PortfolioCategory $category)
324
    {
325
        global $interbreadcrumb;
326
327
        if (!api_is_platform_admin()) {
328
            api_not_allowed(true);
329
        }
330
331
        Display::addFlash(
332
            Display::return_message(get_lang('PortfolioCategoryFieldHelp'), 'info')
333
        );
334
335
        $form = new FormValidator(
336
            'edit_category',
337
            'post',
338
            $this->baseUrl."action=edit_category&id={$category->getId()}"
339
        );
340
341
        if (api_get_configuration_value('save_titles_as_html')) {
342
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
343
        } else {
344
            $translateUrl = $this->baseUrl.'action=translate_category&id='.$category->getId();
345
            $translateButton = Display::toolbarButton(get_lang('TranslateThisTerm'), $translateUrl, 'language', 'link');
346
            $form->addText(
347
                'title',
348
                [get_lang('Title'), $translateButton]
349
            );
350
            $form->applyFilter('title', 'trim');
351
        }
352
353
        $form->addHtmlEditor('description', get_lang('Description'), false, false, ['ToolbarSet' => 'Minimal']);
354
        $form->addButtonUpdate(get_lang('Update'));
355
        $form->setDefaults(
356
            [
357
                'title' => $category->getTitle(),
358
                'description' => $category->getDescription(),
359
            ]
360
        );
361
362
        if ($form->validate()) {
363
            $values = $form->exportValues();
364
365
            $category
366
                ->setTitle($values['title'])
367
                ->setDescription($values['description']);
368
369
            $this->em->persist($category);
370
            $this->em->flush();
371
372
            Display::addFlash(
373
                Display::return_message(get_lang('Updated'), 'success')
374
            );
375
376
            header("Location: {$this->baseUrl}action=list_categories&parent_id=".$category->getParentId());
377
            exit;
378
        }
379
380
        $interbreadcrumb[] = [
381
            'name' => get_lang('Portfolio'),
382
            'url' => $this->baseUrl,
383
        ];
384
        $interbreadcrumb[] = [
385
            'name' => get_lang('Categories'),
386
            'url' => $this->baseUrl.'action=list_categories',
387
        ];
388
        if ($category->getParentId() > 0) {
389
            $em = Database::getManager();
390
            $parentCategory = $em->find('ChamiloCoreBundle:PortfolioCategory', $category->getParentId());
391
            $pageTitle = $parentCategory->getTitle().' : '.get_lang('SubCategories');
392
            $interbreadcrumb[] = [
393
                'name' => Security::remove_XSS($pageTitle),
394
                'url' => $this->baseUrl.'action=list_categories&parent_id='.$category->getParentId(),
395
            ];
396
        }
397
398
        $actions = [];
399
        $actions[] = Display::url(
400
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
401
            $this->baseUrl.'action=list_categories&parent_id='.$category->getParentId()
402
        );
403
404
        $content = $form->returnForm();
405
406
        $this->renderView($content, get_lang('EditCategory'), $actions);
407
    }
408
409
    /**
410
     * @throws \Doctrine\ORM\ORMException
411
     * @throws \Doctrine\ORM\OptimisticLockException
412
     */
413
    public function showHideCategory(PortfolioCategory $category)
414
    {
415
        if (!$this->categoryBelongToOwner($category)) {
416
            api_not_allowed(true);
417
        }
418
419
        $category->setIsVisible(!$category->isVisible());
420
421
        $this->em->persist($category);
422
        $this->em->flush();
423
424
        Display::addFlash(
425
            Display::return_message(get_lang('VisibilityChanged'), 'success')
426
        );
427
428
        header("Location: {$this->baseUrl}action=list_categories");
429
        exit;
430
    }
431
432
    /**
433
     * @throws \Doctrine\ORM\ORMException
434
     * @throws \Doctrine\ORM\OptimisticLockException
435
     */
436
    public function deleteCategory(PortfolioCategory $category)
437
    {
438
        if (!api_is_platform_admin()) {
439
            api_not_allowed(true);
440
        }
441
442
        $this->em->remove($category);
443
        $this->em->flush();
444
445
        Display::addFlash(
446
            Display::return_message(get_lang('CategoryDeleted'), 'success')
447
        );
448
449
        header("Location: {$this->baseUrl}action=list_categories");
450
        exit;
451
    }
452
453
    /**
454
     * @throws \Doctrine\ORM\ORMException
455
     * @throws \Doctrine\ORM\OptimisticLockException
456
     * @throws \Doctrine\ORM\TransactionRequiredException
457
     * @throws \Exception
458
     */
459
    public function addItem()
460
    {
461
        global $interbreadcrumb;
462
463
        $templates = $this->em
464
            ->getRepository(Portfolio::class)
465
            ->findBy(
466
                [
467
                    'isTemplate' => true,
468
                    'course' => $this->course,
469
                    'session' => $this->session,
470
                    'user' => $this->owner,
471
                ]
472
            );
473
474
        $form = new FormValidator('add_portfolio', 'post', $this->baseUrl.'action=add_item');
475
        $form->addSelectFromCollection(
476
            'template',
477
            [
478
                get_lang('Template'),
479
                null,
480
                '<span id="portfolio-spinner" class="fa fa-fw fa-spinner fa-spin" style="display: none;"
481
                    aria-hidden="true" aria-label="'.get_lang('Loading').'"></span>',
482
            ],
483
            $templates,
484
            [],
485
            true,
486
            'getTitle'
487
        );
488
489
        if (api_get_configuration_value('save_titles_as_html')) {
490
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
491
        } else {
492
            $form->addText('title', get_lang('Title'));
493
            $form->applyFilter('title', 'trim');
494
        }
495
        $editorConfig = [
496
            'ToolbarSet' => 'NotebookStudent',
497
            'Width' => '100%',
498
            'Height' => '400',
499
            'cols-size' => [2, 10, 0],
500
        ];
501
        $form->addHtmlEditor('content', get_lang('Content'), true, false, $editorConfig);
502
503
        $categoriesSelect = $form->addSelect(
504
            'category',
505
            [get_lang('Category'), get_lang('PortfolioCategoryFieldHelp')]
506
        );
507
        $categoriesSelect->addOption(get_lang('SelectACategory'), 0);
508
        $parentCategories = $this->getCategoriesForIndex(null, 0);
509
        foreach ($parentCategories as $parentCategory) {
510
            $categoriesSelect->addOption($this->translateDisplayName($parentCategory->getTitle()), $parentCategory->getId());
511
            $subCategories = $this->getCategoriesForIndex(null, $parentCategory->getId());
512
            if (count($subCategories) > 0) {
513
                foreach ($subCategories as $subCategory) {
514
                    $categoriesSelect->addOption(' &mdash; '.$this->translateDisplayName($subCategory->getTitle()), $subCategory->getId());
515
                }
516
            }
517
        }
518
519
        $extraField = new ExtraField('portfolio');
520
        $extra = $extraField->addElements($form);
521
522
        $this->addAttachmentsFieldToForm($form);
523
524
        $form->addButtonCreate(get_lang('Create'));
525
526
        if ($form->validate()) {
527
            $values = $form->exportValues();
528
            $currentTime = new DateTime(
529
                api_get_utc_datetime(),
530
                new DateTimeZone('UTC')
531
            );
532
533
            $portfolio = new Portfolio();
534
            $portfolio
535
                ->setTitle($values['title'])
536
                ->setContent($values['content'])
537
                ->setUser($this->owner)
538
                ->setCourse($this->course)
539
                ->setSession($this->session)
540
                ->setCategory(
541
                    $this->em->find('ChamiloCoreBundle:PortfolioCategory', $values['category'])
542
                )
543
                ->setCreationDate($currentTime)
544
                ->setUpdateDate($currentTime);
545
546
            $this->em->persist($portfolio);
547
            $this->em->flush();
548
549
            $values['item_id'] = $portfolio->getId();
550
551
            $extraFieldValue = new ExtraFieldValue('portfolio');
552
            $extraFieldValue->saveFieldValues($values);
553
554
            $this->processAttachments(
555
                $form,
556
                $portfolio->getUser(),
557
                $portfolio->getId(),
558
                PortfolioAttachment::TYPE_ITEM
559
            );
560
561
            $hook = HookPortfolioItemAdded::create();
562
            $hook->setEventData(['portfolio' => $portfolio]);
563
            $hook->notifyItemAdded();
564
565
            if (1 == api_get_course_setting('email_alert_teachers_new_post')) {
566
                if ($this->session) {
567
                    $messageCourseTitle = "{$this->course->getTitle()} ({$this->session->getName()})";
0 ignored issues
show
Bug introduced by
The method getTitle() does not exist on null. ( Ignorable by Annotation )

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

567
                    $messageCourseTitle = "{$this->course->/** @scrutinizer ignore-call */ getTitle()} ({$this->session->getName()})";

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

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

Loading history...
568
569
                    $teachers = SessionManager::getCoachesByCourseSession(
570
                        $this->session->getId(),
571
                        $this->course->getId()
572
                    );
573
                    $userIdListToSend = array_values($teachers);
574
                } else {
575
                    $messageCourseTitle = $this->course->getTitle();
576
577
                    $teachers = CourseManager::get_teacher_list_from_course_code($this->course->getCode());
578
579
                    $userIdListToSend = array_keys($teachers);
580
                }
581
582
                $messageSubject = sprintf(get_lang('PortfolioAlertNewPostSubject'), $messageCourseTitle);
583
                $messageContent = sprintf(
584
                    get_lang('PortfolioAlertNewPostContent'),
585
                    $this->owner->getCompleteName(),
586
                    $messageCourseTitle,
587
                    $this->baseUrl.http_build_query(['action' => 'view', 'id' => $portfolio->getId()])
588
                );
589
                $messageContent .= '<br><br><dl>'
590
                    .'<dt>'.Security::remove_XSS($portfolio->getTitle()).'</dt>'
591
                    .'<dd>'.$portfolio->getExcerpt().'</dd>'.'</dl>';
592
593
                foreach ($userIdListToSend as $userIdToSend) {
594
                    MessageManager::send_message_simple(
595
                        $userIdToSend,
596
                        $messageSubject,
597
                        $messageContent,
598
                        0,
599
                        false,
600
                        false,
601
                        [],
602
                        false
603
                    );
604
                }
605
            }
606
607
            Display::addFlash(
608
                Display::return_message(get_lang('PortfolioItemAdded'), 'success')
609
            );
610
611
            header("Location: $this->baseUrl");
612
            exit;
613
        }
614
615
        $interbreadcrumb[] = [
616
            'name' => get_lang('Portfolio'),
617
            'url' => $this->baseUrl,
618
        ];
619
620
        $actions = [];
621
        $actions[] = Display::url(
622
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
623
            $this->baseUrl
624
        );
625
        $actions[] = '<a id="hide_bar_template" href="#" role="button">'.
626
            Display::return_icon('expand.png', get_lang('Expand'), ['id' => 'expand'], ICON_SIZE_MEDIUM).
627
            Display::return_icon('contract.png', get_lang('Collapse'), ['id' => 'contract', 'class' => 'hide'], ICON_SIZE_MEDIUM).'</a>';
628
629
        $js = '<script>
630
            $(function() {
631
                $(".scrollbar-light").scrollbar();
632
                $(".scroll-wrapper").css("height", "550px");
633
                expandColumnToogle("#hide_bar_template", {
634
                    selector: "#template_col",
635
                    width: 3
636
                }, {
637
                    selector: "#doc_form",
638
                    width: 9
639
                });
640
                CKEDITOR.on("instanceReady", function (e) {
641
                    showTemplates();
642
                });
643
                $(window).on("load", function () {
644
                    $("input[name=\'title\']").focus();
645
                });
646
                $(\'#add_portfolio_template\').on(\'change\', function () {
647
                    $(\'#portfolio-spinner\').show();
648
                
649
                    $.getJSON(_p.web_ajax + \'portfolio.ajax.php?a=find_template&item=\' + this.value)
650
                        .done(function(response) {
651
                            if (CKEDITOR.instances.title) {
652
                                CKEDITOR.instances.title.setData(response.title);
653
                            } else {
654
                                document.getElementById(\'add_portfolio_title\').value = response.title;
655
                            }
656
657
                            CKEDITOR.instances.content.setData(response.content);
658
                        })
659
                        .fail(function () {
660
                            if (CKEDITOR.instances.title) {
661
                                CKEDITOR.instances.title.setData(\'\');
662
                            } else {
663
                                document.getElementById(\'add_portfolio_title\').value = \'\';
664
                            }
665
666
                            CKEDITOR.instances.content.setData(\'\');
667
                        })
668
                        .always(function() {
669
                          $(\'#portfolio-spinner\').hide();
670
                        });
671
                });
672
                '.$extra['jquery_ready_content'].'
673
            });
674
        </script>';
675
        $content = '<div class="page-create">
676
            <div class="row" style="overflow:hidden">
677
            <div id="template_col" class="col-md-3">
678
                <div class="panel panel-default">
679
                <div class="panel-body">
680
                    <div id="frmModel" class="items-templates scrollbar-light"></div>
681
                </div>
682
                </div>
683
            </div>
684
            <div id="doc_form" class="col-md-9">
685
                '.$form->returnForm().'
686
            </div>
687
          </div></div>';
688
689
        $this->renderView(
690
            $content.$js,
691
            get_lang('AddPortfolioItem'),
692
            $actions
693
        );
694
    }
695
696
    /**
697
     * @throws \Doctrine\ORM\ORMException
698
     * @throws \Doctrine\ORM\OptimisticLockException
699
     * @throws \Doctrine\ORM\TransactionRequiredException
700
     * @throws \Exception
701
     */
702
    public function editItem(Portfolio $item)
703
    {
704
        global $interbreadcrumb;
705
706
        if (!api_is_allowed_to_edit() && !$this->itemBelongToOwner($item)) {
707
            api_not_allowed(true);
708
        }
709
710
        $itemCourse = $item->getCourse();
711
        $itemSession = $item->getSession();
712
713
        $form = new FormValidator('edit_portfolio', 'post', $this->baseUrl."action=edit_item&id={$item->getId()}");
714
715
        if (api_get_configuration_value('save_titles_as_html')) {
716
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
717
        } else {
718
            $form->addText('title', get_lang('Title'));
719
            $form->applyFilter('title', 'trim');
720
        }
721
722
        if ($item->getOrigin()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $item->getOrigin() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
723
            if (Portfolio::TYPE_ITEM === $item->getOriginType()) {
724
                $origin = $this->em->find(Portfolio::class, $item->getOrigin());
725
726
                $form->addLabel(
727
                    sprintf(get_lang('PortfolioItemFromXUser'), $origin->getUser()->getCompleteName()),
728
                    Display::panel(
729
                        Security::remove_XSS($origin->getContent())
730
                    )
731
                );
732
            } elseif (Portfolio::TYPE_COMMENT === $item->getOriginType()) {
733
                $origin = $this->em->find(PortfolioComment::class, $item->getOrigin());
734
735
                $form->addLabel(
736
                    sprintf(get_lang('PortfolioCommentFromXUser'), $origin->getAuthor()->getCompleteName()),
737
                    Display::panel(
738
                        Security::remove_XSS($origin->getContent())
739
                    )
740
                );
741
            }
742
        }
743
        $editorConfig = [
744
            'ToolbarSet' => 'NotebookStudent',
745
            'Width' => '100%',
746
            'Height' => '400',
747
            'cols-size' => [2, 10, 0],
748
        ];
749
        $form->addHtmlEditor('content', get_lang('Content'), true, false, $editorConfig);
750
        $categoriesSelect = $form->addSelect(
751
            'category',
752
            [get_lang('Category'), get_lang('PortfolioCategoryFieldHelp')]
753
        );
754
        $categoriesSelect->addOption(get_lang('SelectACategory'), 0);
755
        $parentCategories = $this->getCategoriesForIndex(null, 0);
756
        foreach ($parentCategories as $parentCategory) {
757
            $categoriesSelect->addOption($this->translateDisplayName($parentCategory->getTitle()), $parentCategory->getId());
758
            $subCategories = $this->getCategoriesForIndex(null, $parentCategory->getId());
759
            if (count($subCategories) > 0) {
760
                foreach ($subCategories as $subCategory) {
761
                    $categoriesSelect->addOption(' &mdash; '.$this->translateDisplayName($subCategory->getTitle()), $subCategory->getId());
762
                }
763
            }
764
        }
765
766
        $extraField = new ExtraField('portfolio');
767
        $extra = $extraField->addElements($form, $item->getId());
768
769
        $attachmentList = $this->generateAttachmentList($item, false);
770
771
        if (!empty($attachmentList)) {
772
            $form->addLabel(get_lang('AttachmentFiles'), $attachmentList);
773
        }
774
775
        $this->addAttachmentsFieldToForm($form);
776
777
        $form->addButtonUpdate(get_lang('Update'));
778
        $form->setDefaults(
779
            [
780
                'title' => $item->getTitle(),
781
                'content' => $item->getContent(),
782
                'category' => $item->getCategory() ? $item->getCategory()->getId() : '',
783
            ]
784
        );
785
786
        if ($form->validate()) {
787
            if ($itemCourse) {
0 ignored issues
show
introduced by
$itemCourse is of type Chamilo\CoreBundle\Entity\Course, thus it always evaluated to true.
Loading history...
788
                api_item_property_update(
789
                    api_get_course_info($itemCourse->getCode()),
790
                    TOOL_PORTFOLIO,
791
                    $item->getId(),
792
                    'PortfolioUpdated',
793
                    api_get_user_id(),
794
                    [],
795
                    null,
796
                    '',
797
                    '',
798
                    $itemSession ? $itemSession->getId() : 0
0 ignored issues
show
introduced by
$itemSession is of type Chamilo\CoreBundle\Entity\Session, thus it always evaluated to true.
Loading history...
799
                );
800
            }
801
802
            $values = $form->exportValues();
803
            $currentTime = new DateTime(api_get_utc_datetime(), new DateTimeZone('UTC'));
804
805
            $item
806
                ->setTitle($values['title'])
807
                ->setContent($values['content'])
808
                ->setUpdateDate($currentTime)
809
                ->setCategory(
810
                    $this->em->find('ChamiloCoreBundle:PortfolioCategory', $values['category'])
811
                );
812
813
            $values['item_id'] = $item->getId();
814
815
            $extraFieldValue = new ExtraFieldValue('portfolio');
816
            $extraFieldValue->saveFieldValues($values);
817
818
            $this->em->persist($item);
819
            $this->em->flush();
820
821
            $this->processAttachments(
822
                $form,
823
                $item->getUser(),
824
                $item->getId(),
825
                PortfolioAttachment::TYPE_ITEM
826
            );
827
828
            Display::addFlash(
829
                Display::return_message(get_lang('ItemUpdated'), 'success')
830
            );
831
832
            header("Location: $this->baseUrl");
833
            exit;
834
        }
835
836
        $interbreadcrumb[] = [
837
            'name' => get_lang('Portfolio'),
838
            'url' => $this->baseUrl,
839
        ];
840
        $actions = [];
841
        $actions[] = Display::url(
842
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
843
            $this->baseUrl
844
        );
845
        $actions[] = '<a id="hide_bar_template" href="#" role="button">'.
846
            Display::return_icon('expand.png', get_lang('Expand'), ['id' => 'expand'], ICON_SIZE_MEDIUM).
847
            Display::return_icon('contract.png', get_lang('Collapse'), ['id' => 'contract', 'class' => 'hide'], ICON_SIZE_MEDIUM).'</a>';
848
849
        $js = '<script>
850
            $(function() {
851
                $(".scrollbar-light").scrollbar();
852
                $(".scroll-wrapper").css("height", "550px");
853
                expandColumnToogle("#hide_bar_template", {
854
                    selector: "#template_col",
855
                    width: 3
856
                }, {
857
                    selector: "#doc_form",
858
                    width: 9
859
                });
860
                CKEDITOR.on("instanceReady", function (e) {
861
                    showTemplates();
862
                });
863
                $(window).on("load", function () {
864
                    $("input[name=\'title\']").focus();
865
                });
866
                '.$extra['jquery_ready_content'].'
867
            });
868
        </script>';
869
        $content = '<div class="page-create">
870
            <div class="row" style="overflow:hidden">
871
            <div id="template_col" class="col-md-3">
872
                <div class="panel panel-default">
873
                <div class="panel-body">
874
                    <div id="frmModel" class="items-templates scrollbar-light"></div>
875
                </div>
876
                </div>
877
            </div>
878
            <div id="doc_form" class="col-md-9">
879
                '.$form->returnForm().'
880
            </div>
881
          </div></div>';
882
883
        $this->renderView(
884
            $content.$js,
885
            get_lang('EditPortfolioItem'),
886
            $actions
887
        );
888
    }
889
890
    /**
891
     * @throws \Doctrine\ORM\ORMException
892
     * @throws \Doctrine\ORM\OptimisticLockException
893
     */
894
    public function showHideItem(Portfolio $item)
895
    {
896
        if (!$this->itemBelongToOwner($item)) {
897
            api_not_allowed(true);
898
        }
899
900
        switch ($item->getVisibility()) {
901
            case Portfolio::VISIBILITY_HIDDEN:
902
                $item->setVisibility(Portfolio::VISIBILITY_VISIBLE);
903
                break;
904
            case Portfolio::VISIBILITY_VISIBLE:
905
                $item->setVisibility(Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER);
906
                break;
907
            case Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER:
908
            default:
909
                $item->setVisibility(Portfolio::VISIBILITY_HIDDEN);
910
                break;
911
        }
912
913
        $this->em->persist($item);
914
        $this->em->flush();
915
916
        Display::addFlash(
917
            Display::return_message(get_lang('VisibilityChanged'), 'success')
918
        );
919
920
        header("Location: $this->baseUrl");
921
        exit;
922
    }
923
924
    /**
925
     * @throws \Doctrine\ORM\ORMException
926
     * @throws \Doctrine\ORM\OptimisticLockException
927
     */
928
    public function deleteItem(Portfolio $item)
929
    {
930
        if (!$this->itemBelongToOwner($item)) {
931
            api_not_allowed(true);
932
        }
933
934
        $this->em->remove($item);
935
        $this->em->flush();
936
937
        Display::addFlash(
938
            Display::return_message(get_lang('ItemDeleted'), 'success')
939
        );
940
941
        header("Location: $this->baseUrl");
942
        exit;
943
    }
944
945
    /**
946
     * @throws \Exception
947
     */
948
    public function index(HttpRequest $httpRequest)
949
    {
950
        $listByUser = false;
951
        $listHighlighted = $httpRequest->query->has('list_highlighted');
952
953
        if ($httpRequest->query->has('user')) {
954
            $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
955
956
            if (empty($this->owner)) {
957
                api_not_allowed(true);
958
            }
959
960
            $listByUser = true;
961
        }
962
963
        $currentUserId = api_get_user_id();
964
965
        $actions = [];
966
967
        if (api_is_platform_admin()) {
968
            $actions[] = Display::url(
969
                Display::return_icon('add.png', get_lang('Add'), [], ICON_SIZE_MEDIUM),
970
                $this->baseUrl.'action=add_item'
971
            );
972
            $actions[] = Display::url(
973
                Display::return_icon('folder.png', get_lang('AddCategory'), [], ICON_SIZE_MEDIUM),
974
                $this->baseUrl.'action=list_categories'
975
            );
976
            $actions[] = Display::url(
977
                Display::return_icon('waiting_list.png', get_lang('PortfolioDetails'), [], ICON_SIZE_MEDIUM),
978
                $this->baseUrl.'action=details'
979
            );
980
        } else {
981
            if ($currentUserId == $this->owner->getId()) {
982
                $actions[] = Display::url(
983
                    Display::return_icon('add.png', get_lang('Add'), [], ICON_SIZE_MEDIUM),
984
                    $this->baseUrl.'action=add_item'
985
                );
986
                $actions[] = Display::url(
987
                    Display::return_icon('waiting_list.png', get_lang('PortfolioDetails'), [], ICON_SIZE_MEDIUM),
988
                    $this->baseUrl.'action=details'
989
                );
990
            } else {
991
                $actions[] = Display::url(
992
                    Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
993
                    $this->baseUrl
994
                );
995
            }
996
        }
997
998
        $frmStudentList = null;
999
        $frmTagList = null;
1000
1001
        $categories = [];
1002
        $portfolio = [];
1003
        if ($this->course) {
1004
            $frmTagList = $this->createFormTagFilter($listByUser);
1005
            $frmStudentList = $this->createFormStudentFilter($listByUser, $listHighlighted);
1006
            $frmStudentList->setDefaults(['user' => $this->owner->getId()]);
1007
            // it translates the category title with the current user language
1008
            $categories = $this->getCategoriesForIndex(null, 0);
1009
            if (count($categories) > 0) {
1010
                foreach ($categories as &$category) {
1011
                    $translated = $this->translateDisplayName($category->getTitle());
1012
                    $category->setTitle($translated);
1013
                }
1014
            }
1015
        } else {
1016
            // it displays the list in Network Social for the current user
1017
            $portfolio = $this->getCategoriesForIndex();
1018
        }
1019
1020
        if ($listHighlighted) {
1021
            $items = $this->getHighlightedItems();
1022
        } else {
1023
            $items = $this->getItemsForIndex($listByUser, $frmTagList);
1024
        }
1025
1026
        // it gets and translate the sub-categories
1027
        $categoryId = $httpRequest->query->getInt('categoryId');
1028
        $subCategoryIdsReq = isset($_REQUEST['subCategoryIds']) ? Security::remove_XSS($_REQUEST['subCategoryIds']) : '';
1029
        $subCategoryIds = $subCategoryIdsReq;
1030
        if ('all' !== $subCategoryIdsReq) {
1031
            $subCategoryIds = !empty($subCategoryIdsReq) ? explode(',', $subCategoryIdsReq) : [];
1032
        }
1033
        $subcategories = [];
1034
        if ($categoryId > 0) {
1035
            $subcategories = $this->getCategoriesForIndex(null, $categoryId);
1036
            if (count($subcategories) > 0) {
1037
                foreach ($subcategories as &$subcategory) {
1038
                    $translated = $this->translateDisplayName($subcategory->getTitle());
1039
                    $subcategory->setTitle($translated);
1040
                }
1041
            }
1042
        }
1043
1044
        $template = new Template(null, false, false, false, false, false, false);
1045
        $template->assign('user', $this->owner);
1046
        $template->assign('course', $this->course);
1047
        $template->assign('session', $this->session);
1048
        $template->assign('portfolio', $portfolio);
1049
        $template->assign('categories', $categories);
1050
        $template->assign('uncategorized_items', $items);
1051
        $template->assign('frm_student_list', $this->course ? $frmStudentList->returnForm() : '');
1052
        $template->assign('frm_tag_list', $this->course ? $frmTagList->returnForm() : '');
1053
        $template->assign('category_id', $categoryId);
1054
        $template->assign('subcategories', $subcategories);
1055
        $template->assign('subcategory_ids', $subCategoryIds);
1056
1057
        $js = '<script>
1058
            $(function() {
1059
                $(".category-filters").bind("click", function() {
1060
                    var categoryId = parseInt($(this).find("input[type=\'radio\']").val());
1061
                    $("input[name=\'categoryId\']").val(categoryId);
1062
                    $("input[name=\'subCategoryIds\']").val("all");
1063
                    $("#frm_tag_list_submit").trigger("click");
1064
                });
1065
                $(".subcategory-filters").bind("click", function() {
1066
                    var checkedVals = $(".subcategory-filters:checkbox:checked").map(function() {
1067
                        return this.value;
1068
                    }).get();
1069
                    $("input[name=\'subCategoryIds\']").val(checkedVals.join(","));
1070
                    $("#frm_tag_list_submit").trigger("click");
1071
                });
1072
            });
1073
        </script>';
1074
        $template->assign('js_script', $js);
1075
        $layout = $template->get_template('portfolio/list.html.twig');
1076
1077
        Display::addFlash(
1078
            Display::return_message(get_lang('PortfolioPostAddHelp'), 'info', false)
1079
        );
1080
1081
        $content = $template->fetch($layout);
1082
1083
        $this->renderView($content, get_lang('Portfolio'), $actions);
1084
    }
1085
1086
    /**
1087
     * @throws \Doctrine\ORM\ORMException
1088
     * @throws \Doctrine\ORM\OptimisticLockException
1089
     * @throws \Doctrine\ORM\TransactionRequiredException
1090
     */
1091
    public function view(Portfolio $item)
1092
    {
1093
        global $interbreadcrumb;
1094
1095
        if (!$this->itemBelongToOwner($item)) {
1096
            if ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN
1097
                || ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER && !api_is_allowed_to_edit())
1098
            ) {
1099
                api_not_allowed(true);
1100
            }
1101
        }
1102
1103
        HookPortfolioItemViewed::create()
1104
            ->setEventData(['portfolio' => $item])
1105
            ->notifyItemViewed()
1106
        ;
1107
1108
        $itemCourse = $item->getCourse();
1109
        $itemSession = $item->getSession();
1110
1111
        $form = $this->createCommentForm($item);
1112
1113
        $commentsRepo = $this->em->getRepository(PortfolioComment::class);
1114
1115
        $query = $commentsRepo->createQueryBuilder('comment')
1116
            ->where('comment.item = :item')
1117
            ->orderBy('comment.root, comment.lft', 'ASC')
1118
            ->setParameter('item', $item)
1119
            ->getQuery();
1120
1121
        $clockIcon = Display::returnFontAwesomeIcon('clock-o', '', true);
1122
1123
        $commentsHtml = $commentsRepo->buildTree(
1124
            $query->getArrayResult(),
1125
            [
1126
                'decorate' => true,
1127
                'rootOpen' => '<div class="media-list">',
1128
                'rootClose' => '</div>',
1129
                'childOpen' => function ($node) use ($commentsRepo) {
1130
                    /** @var PortfolioComment $comment */
1131
                    $comment = $commentsRepo->find($node['id']);
1132
                    $author = $comment->getAuthor();
1133
1134
                    $userPicture = UserManager::getUserPicture(
1135
                        $comment->getAuthor()->getId(),
1136
                        USER_IMAGE_SIZE_SMALL,
1137
                        null,
1138
                        [
1139
                            'picture_uri' => $author->getPictureUri(),
1140
                            'email' => $author->getEmail(),
1141
                        ]
1142
                    );
1143
1144
                    return '<article class="media" id="comment-'.$node['id'].'">
1145
                        <div class="media-left"><img class="media-object thumbnail" src="'.$userPicture.'" alt="'
1146
                        .$author->getCompleteName().'"></div>
1147
                        <div class="media-body">';
1148
                },
1149
                'childClose' => '</div></article>',
1150
                'nodeDecorator' => function ($node) use ($commentsRepo, $clockIcon, $item) {
1151
                    /** @var PortfolioComment $comment */
1152
                    $comment = $commentsRepo->find($node['id']);
1153
1154
                    $commentActions[] = Display::url(
0 ignored issues
show
Comprehensibility Best Practice introduced by
$commentActions was never initialized. Although not strictly required by PHP, it is generally a good practice to add $commentActions = array(); before regardless.
Loading history...
1155
                        Display::return_icon('discuss.png', get_lang('ReplyToThisComment')),
1156
                        '#',
1157
                        [
1158
                            'data-comment' => htmlspecialchars(
1159
                                json_encode(['id' => $comment->getId()])
1160
                            ),
1161
                            'role' => 'button',
1162
                            'class' => 'btn-reply-to',
1163
                        ]
1164
                    );
1165
                    $commentActions[] = Display::url(
1166
                        Display::return_icon('copy.png', get_lang('CopyToMyPortfolio')),
1167
                        $this->baseUrl.http_build_query(
1168
                            [
1169
                                'action' => 'copy',
1170
                                'copy' => 'comment',
1171
                                'id' => $comment->getId(),
1172
                            ]
1173
                        )
1174
                    );
1175
1176
                    $isAllowedToEdit = api_is_allowed_to_edit();
1177
1178
                    if ($isAllowedToEdit) {
1179
                        $commentActions[] = Display::url(
1180
                            Display::return_icon('copy.png', get_lang('CopyToStudentPortfolio')),
1181
                            $this->baseUrl.http_build_query(
1182
                                [
1183
                                    'action' => 'teacher_copy',
1184
                                    'copy' => 'comment',
1185
                                    'id' => $comment->getId(),
1186
                                ]
1187
                            )
1188
                        );
1189
1190
                        if ($comment->isImportant()) {
1191
                            $commentActions[] = Display::url(
1192
                                Display::return_icon('drawing-pin.png', get_lang('UnmarkCommentAsImportant')),
1193
                                $this->baseUrl.http_build_query(
1194
                                    [
1195
                                        'action' => 'mark_important',
1196
                                        'item' => $item->getId(),
1197
                                        'id' => $comment->getId(),
1198
                                    ]
1199
                                )
1200
                            );
1201
                        } else {
1202
                            $commentActions[] = Display::url(
1203
                                Display::return_icon('drawing-pin.png', get_lang('MarkCommentAsImportant')),
1204
                                $this->baseUrl.http_build_query(
1205
                                    [
1206
                                        'action' => 'mark_important',
1207
                                        'item' => $item->getId(),
1208
                                        'id' => $comment->getId(),
1209
                                    ]
1210
                                )
1211
                            );
1212
                        }
1213
1214
                        if ($this->course && '1' === api_get_course_setting('qualify_portfolio_comment')) {
1215
                            $commentActions[] = Display::url(
1216
                                Display::return_icon('quiz.png', get_lang('QualifyThisPortfolioComment')),
1217
                                $this->baseUrl.http_build_query(
1218
                                    [
1219
                                        'action' => 'qualify',
1220
                                        'comment' => $comment->getId(),
1221
                                    ]
1222
                                )
1223
                            );
1224
                        }
1225
                    }
1226
1227
                    $nodeHtml = '<div class="pull-right">'.implode(PHP_EOL, $commentActions).'</div>'.PHP_EOL
1228
                        .'<footer class="media-heading h4">'.PHP_EOL
1229
                        .'<p>'.$comment->getAuthor()->getCompleteName().'</p>'.PHP_EOL;
1230
1231
                    if ($comment->isImportant()
1232
                        && ($this->itemBelongToOwner($comment->getItem()) || $isAllowedToEdit)
1233
                    ) {
1234
                        $nodeHtml .= '<span class="pull-right label label-warning origin-style">'
1235
                            .get_lang('CommentMarkedAsImportant')
1236
                            .'</span>'.PHP_EOL;
1237
                    }
1238
1239
                    $nodeHtml .= '<p class="small">'.$clockIcon.PHP_EOL
1240
                        .Display::dateToStringAgoAndLongDate($comment->getDate()).'</p>'.PHP_EOL;
1241
1242
                    $nodeHtml .= '</footer>'.PHP_EOL
1243
                        .Security::remove_XSS($comment->getContent()).PHP_EOL;
1244
1245
                    $nodeHtml .= $this->generateAttachmentList($comment);
1246
1247
                    return $nodeHtml;
1248
                },
1249
            ]
1250
        );
1251
1252
        $template = new Template(null, false, false, false, false, false, false);
1253
        $template->assign('baseurl', $this->baseUrl);
1254
        $template->assign('item', $item);
1255
        $template->assign('item_content', $this->generateItemContent($item));
1256
        $template->assign('comments', $commentsHtml);
1257
        $template->assign('form', $form);
1258
        $template->assign('attachment_list', $this->generateAttachmentList($item));
1259
1260
        if ($itemCourse) {
1261
            $propertyInfo = api_get_item_property_info(
1262
                $itemCourse->getId(),
1263
                TOOL_PORTFOLIO,
1264
                $item->getId(),
1265
                $itemSession ? $itemSession->getId() : 0
1266
            );
1267
1268
            if ($propertyInfo) {
1269
                $template->assign(
1270
                    'last_edit',
1271
                    [
1272
                        'date' => $propertyInfo['lastedit_date'],
1273
                        'user' => api_get_user_entity($propertyInfo['lastedit_user_id'])->getCompleteName(),
1274
                    ]
1275
                );
1276
            }
1277
        }
1278
1279
        $layout = $template->get_template('portfolio/view.html.twig');
1280
        $content = $template->fetch($layout);
1281
1282
        $interbreadcrumb[] = ['name' => get_lang('Portfolio'), 'url' => $this->baseUrl];
1283
1284
        $editLink = Display::url(
1285
            Display::return_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_MEDIUM),
1286
            $this->baseUrl.http_build_query(['action' => 'edit_item', 'id' => $item->getId()])
1287
        );
1288
1289
        $actions = [];
1290
        $actions[] = Display::url(
1291
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
1292
            $this->baseUrl
1293
        );
1294
1295
        if ($this->itemBelongToOwner($item)) {
1296
            $actions[] = $editLink;
1297
1298
            $actions[] = Display::url(
1299
                Display::return_icon(
1300
                    $item->isTemplate() ? 'wizard.png' : 'wizard_na.png',
1301
                    $item->isTemplate() ? get_lang('RemoveAsTemplate') : get_lang('AddAsTemplate'),
1302
                    [],
1303
                    ICON_SIZE_MEDIUM
1304
                ),
1305
                $this->baseUrl.http_build_query(['action' => 'template', 'id' => $item->getId()])
1306
            );
1307
1308
            $visibilityUrl = $this->baseUrl.http_build_query(['action' => 'visibility', 'id' => $item->getId()]);
1309
1310
            if ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN) {
1311
                $actions[] = Display::url(
1312
                    Display::return_icon('invisible.png', get_lang('MakeVisible'), [], ICON_SIZE_MEDIUM),
1313
                    $visibilityUrl
1314
                );
1315
            } elseif ($item->getVisibility() === Portfolio::VISIBILITY_VISIBLE) {
1316
                $actions[] = Display::url(
1317
                    Display::return_icon('visible.png', get_lang('MakeVisibleForTeachers'), [], ICON_SIZE_MEDIUM),
1318
                    $visibilityUrl
1319
                );
1320
            } elseif ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER) {
1321
                $actions[] = Display::url(
1322
                    Display::return_icon('eye-slash.png', get_lang('MakeInvisible'), [], ICON_SIZE_MEDIUM),
1323
                    $visibilityUrl
1324
                );
1325
            }
1326
1327
            $actions[] = Display::url(
1328
                Display::return_icon('delete.png', get_lang('Delete'), [], ICON_SIZE_MEDIUM),
1329
                $this->baseUrl.http_build_query(['action' => 'delete_item', 'id' => $item->getId()])
1330
            );
1331
        } else {
1332
            $actions[] = Display::url(
1333
                Display::return_icon('copy.png', get_lang('CopyToMyPortfolio'), [], ICON_SIZE_MEDIUM),
1334
                $this->baseUrl.http_build_query(['action' => 'copy', 'id' => $item->getId()])
1335
            );
1336
        }
1337
1338
        if (api_is_allowed_to_edit()) {
1339
            $actions[] = Display::url(
1340
                Display::return_icon('copy.png', get_lang('CopyToStudentPortfolio'), [], ICON_SIZE_MEDIUM),
1341
                $this->baseUrl.http_build_query(['action' => 'teacher_copy', 'copy' => 'item', 'id' => $item->getId()])
1342
            );
1343
            $actions[] = $editLink;
1344
1345
            $highlightedUrl = $this->baseUrl.http_build_query(['action' => 'highlighted', 'id' => $item->getId()]);
1346
1347
            if ($item->isHighlighted()) {
1348
                $actions[] = Display::url(
1349
                    Display::return_icon('award_red.png', get_lang('UnmarkAsHighlighted'), [], ICON_SIZE_MEDIUM),
1350
                    $highlightedUrl
1351
                );
1352
            } else {
1353
                $actions[] = Display::url(
1354
                    Display::return_icon('award_red_na.png', get_lang('MarkAsHighlighted'), [], ICON_SIZE_MEDIUM),
1355
                    $highlightedUrl
1356
                );
1357
            }
1358
1359
            if ($itemCourse && '1' === api_get_course_setting('qualify_portfolio_item')) {
1360
                $actions[] = Display::url(
1361
                    Display::return_icon('quiz.png', get_lang('QualifyThisPortfolioItem'), [], ICON_SIZE_MEDIUM),
1362
                    $this->baseUrl.http_build_query(['action' => 'qualify', 'item' => $item->getId()])
1363
                );
1364
            }
1365
        }
1366
1367
        $this->renderView($content, $item->getTitle(true), $actions, false);
1368
    }
1369
1370
    /**
1371
     * @throws \Doctrine\ORM\ORMException
1372
     * @throws \Doctrine\ORM\OptimisticLockException
1373
     */
1374
    public function copyItem(Portfolio $originItem)
1375
    {
1376
        $currentTime = api_get_utc_datetime(null, false, true);
1377
1378
        $portfolio = new Portfolio();
1379
        $portfolio
1380
            ->setVisibility(Portfolio::VISIBILITY_HIDDEN)
1381
            ->setTitle(
1382
                sprintf(get_lang('PortfolioItemFromXUser'), $originItem->getUser()->getCompleteName())
1383
            )
1384
            ->setContent('')
1385
            ->setUser($this->owner)
1386
            ->setOrigin($originItem->getId())
1387
            ->setOriginType(Portfolio::TYPE_ITEM)
1388
            ->setCourse($this->course)
1389
            ->setSession($this->session)
1390
            ->setCreationDate($currentTime)
1391
            ->setUpdateDate($currentTime);
1392
1393
        $this->em->persist($portfolio);
1394
        $this->em->flush();
1395
1396
        Display::addFlash(
1397
            Display::return_message(get_lang('PortfolioItemAdded'), 'success')
1398
        );
1399
1400
        header("Location: $this->baseUrl".http_build_query(['action' => 'edit_item', 'id' => $portfolio->getId()]));
1401
        exit;
1402
    }
1403
1404
    /**
1405
     * @throws \Doctrine\ORM\ORMException
1406
     * @throws \Doctrine\ORM\OptimisticLockException
1407
     */
1408
    public function copyComment(PortfolioComment $originComment)
1409
    {
1410
        $currentTime = api_get_utc_datetime(null, false, true);
1411
1412
        $portfolio = new Portfolio();
1413
        $portfolio
1414
            ->setVisibility(Portfolio::VISIBILITY_HIDDEN)
1415
            ->setTitle(
1416
                sprintf(get_lang('PortfolioCommentFromXUser'), $originComment->getAuthor()->getCompleteName())
1417
            )
1418
            ->setContent('')
1419
            ->setUser($this->owner)
1420
            ->setOrigin($originComment->getId())
1421
            ->setOriginType(Portfolio::TYPE_COMMENT)
1422
            ->setCourse($this->course)
1423
            ->setSession($this->session)
1424
            ->setCreationDate($currentTime)
1425
            ->setUpdateDate($currentTime);
1426
1427
        $this->em->persist($portfolio);
1428
        $this->em->flush();
1429
1430
        Display::addFlash(
1431
            Display::return_message(get_lang('PortfolioItemAdded'), 'success')
1432
        );
1433
1434
        header("Location: $this->baseUrl".http_build_query(['action' => 'edit_item', 'id' => $portfolio->getId()]));
1435
        exit;
1436
    }
1437
1438
    /**
1439
     * @throws \Doctrine\ORM\ORMException
1440
     * @throws \Doctrine\ORM\OptimisticLockException
1441
     * @throws \Exception
1442
     */
1443
    public function teacherCopyItem(Portfolio $originItem)
1444
    {
1445
        $actionParams = http_build_query(['action' => 'teacher_copy', 'copy' => 'item', 'id' => $originItem->getId()]);
1446
1447
        $form = new FormValidator('teacher_copy_portfolio', 'post', $this->baseUrl.$actionParams);
1448
1449
        if (api_get_configuration_value('save_titles_as_html')) {
1450
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
1451
        } else {
1452
            $form->addText('title', get_lang('Title'));
1453
            $form->applyFilter('title', 'trim');
1454
        }
1455
1456
        $form->addLabel(
1457
            sprintf(get_lang('PortfolioItemFromXUser'), $originItem->getUser()->getCompleteName()),
1458
            Display::panel(
1459
                Security::remove_XSS($originItem->getContent())
1460
            )
1461
        );
1462
        $form->addHtmlEditor('content', get_lang('Content'), true, false, ['ToolbarSet' => 'NotebookStudent']);
1463
1464
        $urlParams = http_build_query(
1465
            [
1466
                'a' => 'search_user_by_course',
1467
                'course_id' => $this->course->getId(),
1468
                'session_id' => $this->session ? $this->session->getId() : 0,
1469
            ]
1470
        );
1471
        $form->addSelectAjax(
1472
            'students',
1473
            get_lang('Students'),
1474
            [],
1475
            [
1476
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1477
                'multiple' => true,
1478
            ]
1479
        );
1480
        $form->addRule('students', get_lang('ThisFieldIsRequired'), 'required');
1481
        $form->addButtonCreate(get_lang('Save'));
1482
1483
        $toolName = get_lang('CopyToStudentPortfolio');
1484
        $content = $form->returnForm();
1485
1486
        if ($form->validate()) {
1487
            $values = $form->exportValues();
1488
1489
            $currentTime = api_get_utc_datetime(null, false, true);
1490
1491
            foreach ($values['students'] as $studentId) {
1492
                $owner = api_get_user_entity($studentId);
1493
1494
                $portfolio = new Portfolio();
1495
                $portfolio
1496
                    ->setVisibility(Portfolio::VISIBILITY_HIDDEN)
1497
                    ->setTitle($values['title'])
1498
                    ->setContent($values['content'])
1499
                    ->setUser($owner)
1500
                    ->setOrigin($originItem->getId())
1501
                    ->setOriginType(Portfolio::TYPE_ITEM)
1502
                    ->setCourse($this->course)
1503
                    ->setSession($this->session)
1504
                    ->setCreationDate($currentTime)
1505
                    ->setUpdateDate($currentTime);
1506
1507
                $this->em->persist($portfolio);
1508
            }
1509
1510
            $this->em->flush();
1511
1512
            Display::addFlash(
1513
                Display::return_message(get_lang('PortfolioItemAddedToStudents'), 'success')
1514
            );
1515
1516
            header("Location: $this->baseUrl");
1517
            exit;
1518
        }
1519
1520
        $this->renderView($content, $toolName);
1521
    }
1522
1523
    /**
1524
     * @throws \Doctrine\ORM\ORMException
1525
     * @throws \Doctrine\ORM\OptimisticLockException
1526
     * @throws \Exception
1527
     */
1528
    public function teacherCopyComment(PortfolioComment $originComment)
1529
    {
1530
        $actionParams = http_build_query(
1531
            [
1532
                'action' => 'teacher_copy',
1533
                'copy' => 'comment',
1534
                'id' => $originComment->getId(),
1535
            ]
1536
        );
1537
1538
        $form = new FormValidator('teacher_copy_portfolio', 'post', $this->baseUrl.$actionParams);
1539
1540
        if (api_get_configuration_value('save_titles_as_html')) {
1541
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
1542
        } else {
1543
            $form->addText('title', get_lang('Title'));
1544
            $form->applyFilter('title', 'trim');
1545
        }
1546
1547
        $form->addLabel(
1548
            sprintf(get_lang('PortfolioCommentFromXUser'), $originComment->getAuthor()->getCompleteName()),
1549
            Display::panel(
1550
                Security::remove_XSS($originComment->getContent())
1551
            )
1552
        );
1553
        $form->addHtmlEditor('content', get_lang('Content'), true, false, ['ToolbarSet' => 'NotebookStudent']);
1554
1555
        $urlParams = http_build_query(
1556
            [
1557
                'a' => 'search_user_by_course',
1558
                'course_id' => $this->course->getId(),
1559
                'session_id' => $this->session ? $this->session->getId() : 0,
1560
            ]
1561
        );
1562
        $form->addSelectAjax(
1563
            'students',
1564
            get_lang('Students'),
1565
            [],
1566
            [
1567
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1568
                'multiple' => true,
1569
            ]
1570
        );
1571
        $form->addRule('students', get_lang('ThisFieldIsRequired'), 'required');
1572
        $form->addButtonCreate(get_lang('Save'));
1573
1574
        $toolName = get_lang('CopyToStudentPortfolio');
1575
        $content = $form->returnForm();
1576
1577
        if ($form->validate()) {
1578
            $values = $form->exportValues();
1579
1580
            $currentTime = api_get_utc_datetime(null, false, true);
1581
1582
            foreach ($values['students'] as $studentId) {
1583
                $owner = api_get_user_entity($studentId);
1584
1585
                $portfolio = new Portfolio();
1586
                $portfolio
1587
                    ->setVisibility(Portfolio::VISIBILITY_HIDDEN)
1588
                    ->setTitle($values['title'])
1589
                    ->setContent($values['content'])
1590
                    ->setUser($owner)
1591
                    ->setOrigin($originComment->getId())
1592
                    ->setOriginType(Portfolio::TYPE_COMMENT)
1593
                    ->setCourse($this->course)
1594
                    ->setSession($this->session)
1595
                    ->setCreationDate($currentTime)
1596
                    ->setUpdateDate($currentTime);
1597
1598
                $this->em->persist($portfolio);
1599
            }
1600
1601
            $this->em->flush();
1602
1603
            Display::addFlash(
1604
                Display::return_message(get_lang('PortfolioItemAddedToStudents'), 'success')
1605
            );
1606
1607
            header("Location: $this->baseUrl");
1608
            exit;
1609
        }
1610
1611
        $this->renderView($content, $toolName);
1612
    }
1613
1614
    /**
1615
     * @throws \Doctrine\ORM\ORMException
1616
     * @throws \Doctrine\ORM\OptimisticLockException
1617
     */
1618
    public function markImportantCommentInItem(Portfolio $item, PortfolioComment $comment)
1619
    {
1620
        if ($comment->getItem()->getId() !== $item->getId()) {
1621
            api_not_allowed(true);
1622
        }
1623
1624
        $comment->setIsImportant(
1625
            !$comment->isImportant()
1626
        );
1627
1628
        $this->em->persist($comment);
1629
        $this->em->flush();
1630
1631
        Display::addFlash(
1632
            Display::return_message(get_lang('CommentMarkedAsImportant'), 'success')
1633
        );
1634
1635
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
1636
        exit;
1637
    }
1638
1639
    /**
1640
     * @throws \Exception
1641
     */
1642
    public function details(HttpRequest $httpRequest)
1643
    {
1644
        $currentUserId = api_get_user_id();
1645
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
1646
1647
        $actions = [];
1648
        $actions[] = Display::url(
1649
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
1650
            $this->baseUrl
1651
        );
1652
        $actions[] = Display::url(
1653
            Display::return_icon('pdf.png', get_lang('ExportMyPortfolioDataPdf'), [], ICON_SIZE_MEDIUM),
1654
            $this->baseUrl.http_build_query(['action' => 'export_pdf'])
1655
        );
1656
        $actions[] = Display::url(
1657
            Display::return_icon('save_pack.png', get_lang('ExportMyPortfolioDataZip'), [], ICON_SIZE_MEDIUM),
1658
            $this->baseUrl.http_build_query(['action' => 'export_zip'])
1659
        );
1660
1661
        $frmStudent = null;
1662
1663
        if ($isAllowedToFilterStudent) {
1664
            if ($httpRequest->query->has('user')) {
1665
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
1666
1667
                if (empty($this->owner)) {
1668
                    api_not_allowed(true);
1669
                }
1670
1671
                $actions[1] = Display::url(
1672
                    Display::return_icon('pdf.png', get_lang('ExportMyPortfolioDataPdf'), [], ICON_SIZE_MEDIUM),
1673
                    $this->baseUrl.http_build_query(['action' => 'export_pdf', 'user' => $this->owner->getId()])
1674
                );
1675
                $actions[2] = Display::url(
1676
                    Display::return_icon('save_pack.png', get_lang('ExportMyPortfolioDataZip'), [], ICON_SIZE_MEDIUM),
1677
                    $this->baseUrl.http_build_query(['action' => 'export_zip', 'user' => $this->owner->getId()])
1678
                );
1679
            }
1680
1681
            $frmStudent = new FormValidator('frm_student_list', 'get');
1682
1683
            $urlParams = http_build_query(
1684
                [
1685
                    'a' => 'search_user_by_course',
1686
                    'course_id' => $this->course->getId(),
1687
                    'session_id' => $this->session ? $this->session->getId() : 0,
1688
                ]
1689
            );
1690
1691
            $frmStudent
1692
                ->addSelectAjax(
1693
                    'user',
1694
                    get_lang('SelectLearnerPortfolio'),
1695
                    [],
1696
                    [
1697
                        'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1698
                        'placeholder' => get_lang('SearchStudent'),
1699
                        'formatResult' => SelectAjax::templateResultForUsersInCourse(),
1700
                        'formatSelection' => SelectAjax::templateSelectionForUsersInCourse(),
1701
                    ]
1702
                )
1703
                ->addOption(
1704
                    $this->owner->getCompleteName(),
1705
                    $this->owner->getId(),
1706
                    [
1707
                        'data-avatarurl' => UserManager::getUserPicture($this->owner->getId()),
1708
                        'data-username' => $this->owner->getUsername(),
1709
                    ]
1710
                )
1711
            ;
1712
            $frmStudent->setDefaults(['user' => $this->owner->getId()]);
1713
            $frmStudent->addHidden('action', 'details');
1714
            $frmStudent->addHidden('cidReq', $this->course->getCode());
1715
            $frmStudent->addHidden('id_session', $this->session ? $this->session->getId() : 0);
1716
            $frmStudent->addButtonFilter(get_lang('Filter'));
1717
        }
1718
1719
        $itemsRepo = $this->em->getRepository(Portfolio::class);
1720
        $commentsRepo = $this->em->getRepository(PortfolioComment::class);
1721
1722
        $getItemsTotalNumber = function () use ($itemsRepo, $isAllowedToFilterStudent, $currentUserId) {
1723
            $qb = $itemsRepo->createQueryBuilder('i');
1724
            $qb
1725
                ->select('COUNT(i)')
1726
                ->where('i.user = :user')
1727
                ->setParameter('user', $this->owner);
1728
1729
            if ($this->course) {
1730
                $qb
1731
                    ->andWhere('i.course = :course')
1732
                    ->setParameter('course', $this->course);
1733
1734
                if ($this->session) {
1735
                    $qb
1736
                        ->andWhere('i.session = :session')
1737
                        ->setParameter('session', $this->session);
1738
                } else {
1739
                    $qb->andWhere('i.session IS NULL');
1740
                }
1741
            }
1742
1743
            if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
1744
                $visibilityCriteria = [
1745
                    Portfolio::VISIBILITY_VISIBLE,
1746
                    Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER,
1747
                ];
1748
1749
                $qb->andWhere(
1750
                    $qb->expr()->in('i.visibility', $visibilityCriteria)
1751
                );
1752
            }
1753
1754
            return $qb->getQuery()->getSingleScalarResult();
1755
        };
1756
        $getItemsData = function ($from, $limit, $columnNo, $orderDirection) use ($itemsRepo, $isAllowedToFilterStudent, $currentUserId) {
1757
            $qb = $itemsRepo->createQueryBuilder('item')
1758
                ->where('item.user = :user')
1759
                ->leftJoin('item.category', 'category')
1760
                ->leftJoin('item.course', 'course')
1761
                ->leftJoin('item.session', 'session')
1762
                ->setParameter('user', $this->owner);
1763
1764
            if ($this->course) {
1765
                $qb
1766
                    ->andWhere('item.course = :course_id')
1767
                    ->setParameter('course_id', $this->course);
1768
1769
                if ($this->session) {
1770
                    $qb
1771
                        ->andWhere('item.session = :session')
1772
                        ->setParameter('session', $this->session);
1773
                } else {
1774
                    $qb->andWhere('item.session IS NULL');
1775
                }
1776
            }
1777
1778
            if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
1779
                $visibilityCriteria = [
1780
                    Portfolio::VISIBILITY_VISIBLE,
1781
                    Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER,
1782
                ];
1783
1784
                $qb->andWhere(
1785
                    $qb->expr()->in('item.visibility', $visibilityCriteria)
1786
                );
1787
            }
1788
1789
            if (0 == $columnNo) {
1790
                $qb->orderBy('item.title', $orderDirection);
1791
            } elseif (1 == $columnNo) {
1792
                $qb->orderBy('item.creationDate', $orderDirection);
1793
            } elseif (2 == $columnNo) {
1794
                $qb->orderBy('item.updateDate', $orderDirection);
1795
            } elseif (3 == $columnNo) {
1796
                $qb->orderBy('category.title', $orderDirection);
1797
            } elseif (5 == $columnNo) {
1798
                $qb->orderBy('item.score', $orderDirection);
1799
            } elseif (6 == $columnNo) {
1800
                $qb->orderBy('course.title', $orderDirection);
1801
            } elseif (7 == $columnNo) {
1802
                $qb->orderBy('session.name', $orderDirection);
1803
            }
1804
1805
            $qb->setFirstResult($from)->setMaxResults($limit);
1806
1807
            return array_map(
1808
                function (Portfolio $item) {
1809
                    $category = $item->getCategory();
1810
1811
                    $row = [];
1812
                    $row[] = $item;
1813
                    $row[] = $item->getCreationDate();
1814
                    $row[] = $item->getUpdateDate();
1815
                    $row[] = $category ? $item->getCategory()->getTitle() : null;
0 ignored issues
show
introduced by
$category is of type Chamilo\CoreBundle\Entity\PortfolioCategory, thus it always evaluated to true.
Loading history...
1816
                    $row[] = $item->getComments()->count();
1817
                    $row[] = $item->getScore();
1818
1819
                    if (!$this->course) {
1820
                        $row[] = $item->getCourse();
1821
                        $row[] = $item->getSession();
1822
                    }
1823
1824
                    return $row;
1825
                },
1826
                $qb->getQuery()->getResult()
1827
            );
1828
        };
1829
1830
        $portfolioItemColumnFilter = function (Portfolio $item) {
1831
            return Display::url(
1832
                $item->getTitle(true),
1833
                $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
1834
            );
1835
        };
1836
        $convertFormatDateColumnFilter = function (DateTime $date) {
1837
            return api_convert_and_format_date($date);
1838
        };
1839
1840
        $tblItems = new SortableTable('tbl_items', $getItemsTotalNumber, $getItemsData, 1, 20, 'DESC');
1841
        $tblItems->set_additional_parameters(['action' => 'details', 'user' => $this->owner->getId()]);
1842
        $tblItems->set_header(0, get_lang('Title'));
1843
        $tblItems->set_column_filter(0, $portfolioItemColumnFilter);
1844
        $tblItems->set_header(1, get_lang('CreationDate'), true, [], ['class' => 'text-center']);
1845
        $tblItems->set_column_filter(1, $convertFormatDateColumnFilter);
1846
        $tblItems->set_header(2, get_lang('LastUpdate'), true, [], ['class' => 'text-center']);
1847
        $tblItems->set_column_filter(2, $convertFormatDateColumnFilter);
1848
        $tblItems->set_header(3, get_lang('Category'));
1849
        $tblItems->set_header(4, get_lang('Comments'), false, [], ['class' => 'text-right']);
1850
        $tblItems->set_header(5, get_lang('Score'), true, [], ['class' => 'text-right']);
1851
1852
        if (!$this->course) {
1853
            $tblItems->set_header(6, get_lang('Course'));
1854
            $tblItems->set_header(7, get_lang('Session'));
1855
        }
1856
1857
        $getCommentsTotalNumber = function () use ($commentsRepo) {
1858
            $qb = $commentsRepo->createQueryBuilder('c');
1859
            $qb
1860
                ->select('COUNT(c)')
1861
                ->where('c.author = :author')
1862
                ->setParameter('author', $this->owner);
1863
1864
            if ($this->course) {
1865
                $qb
1866
                    ->innerJoin('c.item', 'i')
1867
                    ->andWhere('i.course = :course')
1868
                    ->setParameter('course', $this->course);
1869
1870
                if ($this->session) {
1871
                    $qb
1872
                        ->andWhere('i.session = :session')
1873
                        ->setParameter('session', $this->session);
1874
                } else {
1875
                    $qb->andWhere('i.session IS NULL');
1876
                }
1877
            }
1878
1879
            return $qb->getQuery()->getSingleScalarResult();
1880
        };
1881
        $getCommentsData = function ($from, $limit, $columnNo, $orderDirection) use ($commentsRepo) {
1882
            $qb = $commentsRepo->createQueryBuilder('comment');
1883
            $qb
1884
                ->where('comment.author = :user')
1885
                ->innerJoin('comment.item', 'item')
1886
                ->setParameter('user', $this->owner);
1887
1888
            if ($this->course) {
1889
                $qb
1890
                    ->innerJoin('comment.item', 'i')
1891
                    ->andWhere('item.course = :course')
1892
                    ->setParameter('course', $this->course);
1893
1894
                if ($this->session) {
1895
                    $qb
1896
                        ->andWhere('item.session = :session')
1897
                        ->setParameter('session', $this->session);
1898
                } else {
1899
                    $qb->andWhere('item.session IS NULL');
1900
                }
1901
            }
1902
1903
            if (0 == $columnNo) {
1904
                $qb->orderBy('comment.content', $orderDirection);
1905
            } elseif (1 == $columnNo) {
1906
                $qb->orderBy('comment.date', $orderDirection);
1907
            } elseif (2 == $columnNo) {
1908
                $qb->orderBy('item.title', $orderDirection);
1909
            } elseif (3 == $columnNo) {
1910
                $qb->orderBy('comment.score', $orderDirection);
1911
            }
1912
1913
            $qb->setFirstResult($from)->setMaxResults($limit);
1914
1915
            return array_map(
1916
                function (PortfolioComment $comment) {
1917
                    return [
1918
                        $comment,
1919
                        $comment->getDate(),
1920
                        $comment->getItem(),
1921
                        $comment->getScore(),
1922
                    ];
1923
                },
1924
                $qb->getQuery()->getResult()
1925
            );
1926
        };
1927
1928
        $tblComments = new SortableTable('tbl_comments', $getCommentsTotalNumber, $getCommentsData, 1, 20, 'DESC');
1929
        $tblComments->set_additional_parameters(['action' => 'details', 'user' => $this->owner->getId()]);
1930
        $tblComments->set_header(0, get_lang('Resume'));
1931
        $tblComments->set_column_filter(
1932
            0,
1933
            function (PortfolioComment $comment) {
1934
                return Display::url(
1935
                    $comment->getExcerpt(),
1936
                    $this->baseUrl.http_build_query(['action' => 'view', 'id' => $comment->getItem()->getId()])
1937
                    .'#comment-'.$comment->getId()
1938
                );
1939
            }
1940
        );
1941
        $tblComments->set_header(1, get_lang('Date'), true, [], ['class' => 'text-center']);
1942
        $tblComments->set_column_filter(1, $convertFormatDateColumnFilter);
1943
        $tblComments->set_header(2, get_lang('PortfolioItemTitle'));
1944
        $tblComments->set_column_filter(2, $portfolioItemColumnFilter);
1945
        $tblComments->set_header(3, get_lang('Score'), true, [], ['class' => 'text-right']);
1946
1947
        $content = '';
1948
1949
        if ($frmStudent) {
1950
            $content .= $frmStudent->returnForm();
1951
        }
1952
1953
        $totalNumberOfItems = $tblItems->get_total_number_of_items();
1954
        $totalNumberOfComments = $tblComments->get_total_number_of_items();
1955
        $requiredNumberOfItems = (int) api_get_course_setting('portfolio_number_items');
1956
        $requiredNumberOfComments = (int) api_get_course_setting('portfolio_number_comments');
1957
1958
        $itemsSubtitle = '';
1959
1960
        if ($requiredNumberOfItems > 0) {
1961
            $itemsSubtitle = sprintf(
1962
                get_lang('XAddedYRequired'),
1963
                $totalNumberOfItems,
1964
                $requiredNumberOfItems
1965
            );
1966
        }
1967
1968
        $content .= Display::page_subheader2(
1969
            get_lang('PortfolioItems'),
1970
            $itemsSubtitle
1971
        ).PHP_EOL;
1972
1973
        if ($totalNumberOfItems > 0) {
1974
            $content .= $tblItems->return_table().PHP_EOL;
1975
        } else {
1976
            $content .= Display::return_message(get_lang('NoItemsInYourPortfolio'), 'warning');
1977
        }
1978
1979
        $commentsSubtitle = '';
1980
1981
        if ($requiredNumberOfComments > 0) {
1982
            $commentsSubtitle = sprintf(
1983
                get_lang('XAddedYRequired'),
1984
                $totalNumberOfComments,
1985
                $requiredNumberOfComments
1986
            );
1987
        }
1988
1989
        $content .= Display::page_subheader2(
1990
            get_lang('PortfolioCommentsMade'),
1991
            $commentsSubtitle
1992
        ).PHP_EOL;
1993
1994
        if ($totalNumberOfComments > 0) {
1995
            $content .= $tblComments->return_table().PHP_EOL;
1996
        } else {
1997
            $content .= Display::return_message(get_lang('YouHaveNotCommented'), 'warning');
1998
        }
1999
2000
        $this->renderView($content, get_lang('PortfolioDetails'), $actions);
2001
    }
2002
2003
    /**
2004
     * @throws \MpdfException
2005
     */
2006
    public function exportPdf(HttpRequest $httpRequest)
2007
    {
2008
        $currentUserId = api_get_user_id();
2009
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
2010
2011
        if ($isAllowedToFilterStudent) {
2012
            if ($httpRequest->query->has('user')) {
2013
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
2014
2015
                if (empty($this->owner)) {
2016
                    api_not_allowed(true);
2017
                }
2018
            }
2019
        }
2020
2021
        $pdfContent = Display::page_header($this->owner->getCompleteName());
2022
2023
        if ($this->course) {
2024
            $pdfContent .= '<p>'.get_lang('Course').': ';
2025
2026
            if ($this->session) {
2027
                $pdfContent .= $this->session->getName().' ('.$this->course->getTitle().')';
2028
            } else {
2029
                $pdfContent .= $this->course->getTitle();
2030
            }
2031
2032
            $pdfContent .= '</p>';
2033
        }
2034
2035
        $visibility = [];
2036
2037
        if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
2038
            $visibility[] = Portfolio::VISIBILITY_VISIBLE;
2039
            $visibility[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
2040
        }
2041
2042
        $items = $this->em
2043
            ->getRepository(Portfolio::class)
2044
            ->findItemsByUser(
2045
                $this->owner,
2046
                $this->course,
2047
                $this->session,
2048
                null,
2049
                $visibility
2050
            );
2051
        $comments = $this->em
2052
            ->getRepository(PortfolioComment::class)
2053
            ->findCommentsByUser($this->owner, $this->course, $this->session);
2054
2055
        $itemsHtml = $this->getItemsInHtmlFormatted($items);
2056
        $commentsHtml = $this->getCommentsInHtmlFormatted($comments);
2057
2058
        $totalNumberOfItems = count($itemsHtml);
2059
        $totalNumberOfComments = count($commentsHtml);
2060
        $requiredNumberOfItems = (int) api_get_course_setting('portfolio_number_items');
2061
        $requiredNumberOfComments = (int) api_get_course_setting('portfolio_number_comments');
2062
2063
        $itemsSubtitle = '';
2064
        $commentsSubtitle = '';
2065
2066
        if ($requiredNumberOfItems > 0) {
2067
            $itemsSubtitle = sprintf(
2068
                get_lang('XAddedYRequired'),
2069
                $totalNumberOfItems,
2070
                $requiredNumberOfItems
2071
            );
2072
        }
2073
2074
        if ($requiredNumberOfComments > 0) {
2075
            $commentsSubtitle = sprintf(
2076
                get_lang('XAddedYRequired'),
2077
                $totalNumberOfComments,
2078
                $requiredNumberOfComments
2079
            );
2080
        }
2081
2082
        $pdfContent .= Display::page_subheader2(
2083
            get_lang('PortfolioItems'),
2084
            $itemsSubtitle
2085
        );
2086
2087
        if ($totalNumberOfItems > 0) {
2088
            $pdfContent .= implode(PHP_EOL, $itemsHtml);
2089
        } else {
2090
            $pdfContent .= Display::return_message(get_lang('NoItemsInYourPortfolio'), 'warning');
2091
        }
2092
2093
        $pdfContent .= Display::page_subheader2(
2094
            get_lang('PortfolioCommentsMade'),
2095
            $commentsSubtitle
2096
        );
2097
2098
        if ($totalNumberOfComments > 0) {
2099
            $pdfContent .= implode(PHP_EOL, $commentsHtml);
2100
        } else {
2101
            $pdfContent .= Display::return_message(get_lang('YouHaveNotCommented'), 'warning');
2102
        }
2103
2104
        $pdfName = $this->owner->getCompleteName()
2105
            .($this->course ? '_'.$this->course->getCode() : '')
2106
            .'_'.get_lang('Portfolio');
2107
2108
        $pdf = new PDF();
2109
        $pdf->content_to_pdf(
2110
            $pdfContent,
2111
            null,
2112
            $pdfName,
2113
            $this->course ? $this->course->getCode() : null,
2114
            'D',
2115
            false,
2116
            null,
2117
            false,
2118
            true
2119
        );
2120
    }
2121
2122
    public function exportZip(HttpRequest $httpRequest)
2123
    {
2124
        $currentUserId = api_get_user_id();
2125
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
2126
2127
        if ($isAllowedToFilterStudent) {
2128
            if ($httpRequest->query->has('user')) {
2129
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
2130
2131
                if (empty($this->owner)) {
2132
                    api_not_allowed(true);
2133
                }
2134
            }
2135
        }
2136
2137
        $itemsRepo = $this->em->getRepository(Portfolio::class);
2138
        $commentsRepo = $this->em->getRepository(PortfolioComment::class);
2139
        $attachmentsRepo = $this->em->getRepository(PortfolioAttachment::class);
2140
2141
        $visibility = [];
2142
2143
        if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
2144
            $visibility[] = Portfolio::VISIBILITY_VISIBLE;
2145
            $visibility[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
2146
        }
2147
2148
        $items = $itemsRepo->findItemsByUser(
2149
            $this->owner,
2150
            $this->course,
2151
            $this->session,
2152
            null,
2153
            $visibility
2154
        );
2155
        $comments = $commentsRepo->findCommentsByUser($this->owner, $this->course, $this->session);
2156
2157
        $itemsHtml = $this->getItemsInHtmlFormatted($items);
2158
        $commentsHtml = $this->getCommentsInHtmlFormatted($comments);
2159
2160
        $sysArchivePath = api_get_path(SYS_ARCHIVE_PATH);
2161
        $tempPortfolioDirectory = $sysArchivePath."portfolio/{$this->owner->getId()}";
2162
2163
        $userDirectory = UserManager::getUserPathById($this->owner->getId(), 'system');
2164
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2165
2166
        $tblItemsHeaders = [];
2167
        $tblItemsHeaders[] = get_lang('Title');
2168
        $tblItemsHeaders[] = get_lang('CreationDate');
2169
        $tblItemsHeaders[] = get_lang('LastUpdate');
2170
        $tblItemsHeaders[] = get_lang('Category');
2171
        $tblItemsHeaders[] = get_lang('Category');
2172
        $tblItemsHeaders[] = get_lang('Score');
2173
        $tblItemsHeaders[] = get_lang('Course');
2174
        $tblItemsHeaders[] = get_lang('Session');
2175
        $tblItemsData = [];
2176
2177
        $tblCommentsHeaders = [];
2178
        $tblCommentsHeaders[] = get_lang('Resume');
2179
        $tblCommentsHeaders[] = get_lang('Date');
2180
        $tblCommentsHeaders[] = get_lang('PortfolioItemTitle');
2181
        $tblCommentsHeaders[] = get_lang('Score');
2182
        $tblCommentsData = [];
2183
2184
        $filenames = [];
2185
2186
        $fs = new Filesystem();
2187
2188
        /**
2189
         * @var int       $i
2190
         * @var Portfolio $item
2191
         */
2192
        foreach ($items as $i => $item) {
2193
            $itemCategory = $item->getCategory();
2194
            $itemCourse = $item->getCourse();
2195
            $itemSession = $item->getSession();
2196
2197
            $itemDirectory = $item->getCreationDate()->format('Y-m-d-H-i-s');
2198
2199
            $itemFilename = sprintf('%s/items/%s/item.html', $tempPortfolioDirectory, $itemDirectory);
2200
            $itemFileContent = $this->fixImagesSourcesToHtml($itemsHtml[$i]);
2201
2202
            $fs->dumpFile($itemFilename, $itemFileContent);
2203
2204
            $filenames[] = $itemFilename;
2205
2206
            $attachments = $attachmentsRepo->findFromItem($item);
2207
2208
            /** @var PortfolioAttachment $attachment */
2209
            foreach ($attachments as $attachment) {
2210
                $attachmentFilename = sprintf(
2211
                    '%s/items/%s/attachments/%s',
2212
                    $tempPortfolioDirectory,
2213
                    $itemDirectory,
2214
                    $attachment->getFilename()
2215
                );
2216
2217
                $fs->copy(
2218
                    $attachmentsDirectory.$attachment->getPath(),
2219
                    $attachmentFilename
2220
                );
2221
2222
                $filenames[] = $attachmentFilename;
2223
            }
2224
2225
            $tblItemsData[] = [
2226
                Display::url(
2227
                    Security::remove_XSS($item->getTitle()),
2228
                    sprintf('items/%s/item.html', $itemDirectory)
2229
                ),
2230
                api_convert_and_format_date($item->getCreationDate()),
2231
                api_convert_and_format_date($item->getUpdateDate()),
2232
                $itemCategory ? $itemCategory->getTitle() : null,
2233
                $item->getComments()->count(),
2234
                $item->getScore(),
2235
                $itemCourse->getTitle(),
2236
                $itemSession ? $itemSession->getName() : null,
2237
            ];
2238
        }
2239
2240
        /**
2241
         * @var int              $i
2242
         * @var PortfolioComment $comment
2243
         */
2244
        foreach ($comments as $i => $comment) {
2245
            $commentDirectory = $comment->getDate()->format('Y-m-d-H-i-s');
2246
2247
            $commentFileContent = $this->fixImagesSourcesToHtml($commentsHtml[$i]);
2248
            $commentFilename = sprintf('%s/comments/%s/comment.html', $tempPortfolioDirectory, $commentDirectory);
2249
2250
            $fs->dumpFile($commentFilename, $commentFileContent);
2251
2252
            $filenames[] = $commentFilename;
2253
2254
            $attachments = $attachmentsRepo->findFromComment($comment);
2255
2256
            /** @var PortfolioAttachment $attachment */
2257
            foreach ($attachments as $attachment) {
2258
                $attachmentFilename = sprintf(
2259
                    '%s/comments/%s/attachments/%s',
2260
                    $tempPortfolioDirectory,
2261
                    $commentDirectory,
2262
                    $attachment->getFilename()
2263
                );
2264
2265
                $fs->copy(
2266
                    $attachmentsDirectory.$attachment->getPath(),
2267
                    $attachmentFilename
2268
                );
2269
2270
                $filenames[] = $attachmentFilename;
2271
            }
2272
2273
            $tblCommentsData[] = [
2274
                Display::url(
2275
                    $comment->getExcerpt(),
2276
                    sprintf('comments/%s/comment.html', $commentDirectory)
2277
                ),
2278
                api_convert_and_format_date($comment->getDate()),
2279
                Security::remove_XSS($comment->getItem()->getTitle()),
2280
                $comment->getScore(),
2281
            ];
2282
        }
2283
2284
        $tblItems = new HTML_Table(['class' => 'table table-hover table-striped table-bordered data_table']);
2285
        $tblItems->setHeaders($tblItemsHeaders);
2286
        $tblItems->setData($tblItemsData);
2287
2288
        $tblComments = new HTML_Table(['class' => 'table table-hover table-striped table-bordered data_table']);
2289
        $tblComments->setHeaders($tblCommentsHeaders);
2290
        $tblComments->setData($tblCommentsData);
2291
2292
        $itemFilename = sprintf('%s/index.html', $tempPortfolioDirectory);
2293
2294
        $filenames[] = $itemFilename;
2295
2296
        $fs->dumpFile(
2297
            $itemFilename,
2298
            $this->formatZipIndexFile($tblItems, $tblComments)
2299
        );
2300
2301
        $zipName = $this->owner->getCompleteName()
2302
            .($this->course ? '_'.$this->course->getCode() : '')
2303
            .'_'.get_lang('Portfolio');
2304
        $tempZipFile = $sysArchivePath."portfolio/$zipName.zip";
2305
        $zip = new PclZip($tempZipFile);
2306
2307
        foreach ($filenames as $filename) {
2308
            $zip->add($filename, PCLZIP_OPT_REMOVE_PATH, $tempPortfolioDirectory);
0 ignored issues
show
Bug introduced by
The constant PCLZIP_OPT_REMOVE_PATH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
2309
        }
2310
2311
        DocumentManager::file_send_for_download($tempZipFile, true, "$zipName.zip");
2312
2313
        $fs->remove($tempPortfolioDirectory);
2314
        $fs->remove($tempZipFile);
2315
    }
2316
2317
    public function qualifyItem(Portfolio $item)
2318
    {
2319
        global $interbreadcrumb;
2320
2321
        $em = Database::getManager();
2322
2323
        $formAction = $this->baseUrl.http_build_query(['action' => 'qualify', 'item' => $item->getId()]);
2324
2325
        $form = new FormValidator('frm_qualify', 'post', $formAction);
2326
        $form->addUserAvatar('user', get_lang('Author'));
2327
        $form->addLabel(get_lang('Title'), $item->getTitle());
2328
2329
        $itemContent = $this->generateItemContent($item);
2330
2331
        $form->addLabel(get_lang('Content'), $itemContent);
2332
        $form->addNumeric(
2333
            'score',
2334
            [get_lang('QualifyNumeric'), null, ' / '.api_get_course_setting('portfolio_max_score')]
2335
        );
2336
        $form->addButtonSave(get_lang('QualifyThisPortfolioItem'));
2337
2338
        if ($form->validate()) {
2339
            $values = $form->exportValues();
2340
2341
            $item->setScore($values['score']);
2342
2343
            $em->persist($item);
2344
            $em->flush();
2345
2346
            Display::addFlash(
2347
                Display::return_message(get_lang('PortfolioItemGraded'), 'success')
2348
            );
2349
2350
            header("Location: $formAction");
2351
            exit();
2352
        }
2353
2354
        $form->setDefaults(
2355
            [
2356
                'user' => $item->getUser(),
2357
                'score' => (float) $item->getScore(),
2358
            ]
2359
        );
2360
2361
        $interbreadcrumb[] = [
2362
            'name' => get_lang('Portfolio'),
2363
            'url' => $this->baseUrl,
2364
        ];
2365
        $interbreadcrumb[] = [
2366
            'name' => $item->getTitle(true),
2367
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
2368
        ];
2369
2370
        $actions = [];
2371
        $actions[] = Display::url(
2372
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
2373
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
2374
        );
2375
2376
        $this->renderView($form->returnForm(), get_lang('Qualify'), $actions);
2377
    }
2378
2379
    public function qualifyComment(PortfolioComment $comment)
2380
    {
2381
        global $interbreadcrumb;
2382
2383
        $em = Database::getManager();
2384
2385
        $item = $comment->getItem();
2386
        $commentPath = $em->getRepository(PortfolioComment::class)->getPath($comment);
2387
2388
        $template = new Template('', false, false, false, true, false, false);
2389
        $template->assign('item', $item);
2390
        $template->assign('comments_path', $commentPath);
2391
        $commentContext = $template->fetch(
2392
            $template->get_template('portfolio/comment_context.html.twig')
2393
        );
2394
2395
        $formAction = $this->baseUrl.http_build_query(['action' => 'qualify', 'comment' => $comment->getId()]);
2396
2397
        $form = new FormValidator('frm_qualify', 'post', $formAction);
2398
        $form->addHtml($commentContext);
2399
        $form->addUserAvatar('user', get_lang('Author'));
2400
        $form->addLabel(get_lang('Comment'), $comment->getContent());
2401
        $form->addNumeric(
2402
            'score',
2403
            [get_lang('QualifyNumeric'), null, '/ '.api_get_course_setting('portfolio_max_score')]
2404
        );
2405
        $form->addButtonSave(get_lang('QualifyThisPortfolioComment'));
2406
2407
        if ($form->validate()) {
2408
            $values = $form->exportValues();
2409
2410
            $comment->setScore($values['score']);
2411
2412
            $em->persist($comment);
2413
            $em->flush();
2414
2415
            Display::addFlash(
2416
                Display::return_message(get_lang('PortfolioCommentGraded'), 'success')
2417
            );
2418
2419
            header("Location: $formAction");
2420
            exit();
2421
        }
2422
2423
        $form->setDefaults(
2424
            [
2425
                'user' => $comment->getAuthor(),
2426
                'score' => (float) $comment->getScore(),
2427
            ]
2428
        );
2429
2430
        $interbreadcrumb[] = [
2431
            'name' => get_lang('Portfolio'),
2432
            'url' => $this->baseUrl,
2433
        ];
2434
        $interbreadcrumb[] = [
2435
            'name' => $item->getTitle(true),
2436
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
2437
        ];
2438
2439
        $actions = [];
2440
        $actions[] = Display::url(
2441
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
2442
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
2443
        );
2444
2445
        $this->renderView($form->returnForm(), get_lang('Qualify'), $actions);
2446
    }
2447
2448
    public function downloadAttachment(HttpRequest $httpRequest)
2449
    {
2450
        $path = $httpRequest->query->get('file');
2451
2452
        if (empty($path)) {
2453
            api_not_allowed(true);
2454
        }
2455
2456
        $em = Database::getManager();
2457
        $attachmentRepo = $em->getRepository(PortfolioAttachment::class);
2458
2459
        $attachment = $attachmentRepo->findOneByPath($path);
2460
2461
        if (empty($attachment)) {
2462
            api_not_allowed(true);
2463
        }
2464
2465
        $originOwnerId = 0;
2466
2467
        if (PortfolioAttachment::TYPE_ITEM === $attachment->getOriginType()) {
2468
            $item = $em->find(Portfolio::class, $attachment->getOrigin());
2469
2470
            $originOwnerId = $item->getUser()->getId();
2471
        } elseif (PortfolioAttachment::TYPE_COMMENT === $attachment->getOriginType()) {
2472
            $comment = $em->find(PortfolioComment::class, $attachment->getOrigin());
2473
2474
            $originOwnerId = $comment->getAuthor()->getId();
2475
        } else {
2476
            api_not_allowed(true);
2477
        }
2478
2479
        $userDirectory = UserManager::getUserPathById($originOwnerId, 'system');
2480
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2481
        $attachmentFilename = $attachmentsDirectory.$attachment->getPath();
2482
2483
        if (!Security::check_abs_path($attachmentFilename, $attachmentsDirectory)) {
2484
            api_not_allowed(true);
2485
        }
2486
2487
        $downloaded = DocumentManager::file_send_for_download(
2488
            $attachmentFilename,
2489
            true,
2490
            $attachment->getFilename()
2491
        );
2492
2493
        if (!$downloaded) {
2494
            api_not_allowed(true);
2495
        }
2496
    }
2497
2498
    public function deleteAttachment(HttpRequest $httpRequest)
2499
    {
2500
        $currentUserId = api_get_user_id();
2501
2502
        $path = $httpRequest->query->get('file');
2503
2504
        if (empty($path)) {
2505
            api_not_allowed(true);
2506
        }
2507
2508
        $em = Database::getManager();
2509
        $fs = new Filesystem();
2510
2511
        $attachmentRepo = $em->getRepository(PortfolioAttachment::class);
2512
        $attachment = $attachmentRepo->findOneByPath($path);
2513
2514
        if (empty($attachment)) {
2515
            api_not_allowed(true);
2516
        }
2517
2518
        $originOwnerId = 0;
2519
        $itemId = 0;
2520
2521
        if (PortfolioAttachment::TYPE_ITEM === $attachment->getOriginType()) {
2522
            $item = $em->find(Portfolio::class, $attachment->getOrigin());
2523
            $originOwnerId = $item->getUser()->getId();
2524
            $itemId = $item->getId();
2525
        } elseif (PortfolioAttachment::TYPE_COMMENT === $attachment->getOriginType()) {
2526
            $comment = $em->find(PortfolioComment::class, $attachment->getOrigin());
2527
            $originOwnerId = $comment->getAuthor()->getId();
2528
            $itemId = $comment->getItem()->getId();
2529
        }
2530
2531
        if ($currentUserId !== $originOwnerId) {
2532
            api_not_allowed(true);
2533
        }
2534
2535
        $em->remove($attachment);
2536
        $em->flush();
2537
2538
        $userDirectory = UserManager::getUserPathById($originOwnerId, 'system');
2539
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2540
        $attachmentFilename = $attachmentsDirectory.$attachment->getPath();
2541
2542
        $fs->remove($attachmentFilename);
2543
2544
        if ($httpRequest->isXmlHttpRequest()) {
2545
            echo Display::return_message(get_lang('AttachmentFileDeleteSuccess'), 'success');
2546
        } else {
2547
            Display::addFlash(
2548
                Display::return_message(get_lang('AttachmentFileDeleteSuccess'), 'success')
2549
            );
2550
2551
            header('Location: '.$this->baseUrl.http_build_query(['action' => 'view', 'id' => $itemId]));
2552
        }
2553
2554
        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...
2555
    }
2556
2557
    /**
2558
     * @throws \Doctrine\ORM\OptimisticLockException
2559
     * @throws \Doctrine\ORM\ORMException
2560
     */
2561
    public function markAsHighlighted(Portfolio $item)
2562
    {
2563
        if ($item->getCourse()->getId() !== (int) api_get_course_int_id()) {
2564
            api_not_allowed(true);
2565
        }
2566
2567
        $item->setIsHighlighted(
2568
            !$item->isHighlighted()
2569
        );
2570
2571
        Database::getManager()->flush();
2572
2573
        Display::addFlash(
2574
            Display::return_message(
2575
                $item->isHighlighted() ? get_lang('MarkedAsHighlighted') : get_lang('UnmarkedAsHighlighted'),
2576
                'success'
2577
            )
2578
        );
2579
2580
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
2581
        exit;
2582
    }
2583
2584
    public function markAsTemplate(Portfolio $item)
2585
    {
2586
        if (!$this->itemBelongToOwner($item)) {
2587
            api_not_allowed(true);
2588
        }
2589
2590
        $item->setIsTemplate(
2591
            !$item->isTemplate()
2592
        );
2593
2594
        Database::getManager()->flush($item);
2595
2596
        Display::addFlash(
2597
            Display::return_message(
2598
                $item->isTemplate() ? get_lang('PortfolioItemSetAsTemplate') : get_lang('PortfolioItemUnsetAsTemplate'),
2599
                'success'
2600
            )
2601
        );
2602
2603
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
2604
        exit;
2605
    }
2606
2607
    /**
2608
     * @param bool $showHeader
2609
     */
2610
    private function renderView(string $content, string $toolName, array $actions = [], $showHeader = true)
2611
    {
2612
        global $this_section;
2613
2614
        $this_section = $this->course ? SECTION_COURSES : SECTION_SOCIAL;
2615
2616
        $view = new Template($toolName);
2617
2618
        if ($showHeader) {
2619
            $view->assign('header', $toolName);
2620
        }
2621
2622
        $actionsStr = '';
2623
2624
        if ($this->course) {
2625
            $actionsStr .= Display::return_introduction_section(TOOL_PORTFOLIO);
0 ignored issues
show
Bug introduced by
Are you sure the usage of Display::return_introduc...section(TOOL_PORTFOLIO) targeting Display::return_introduction_section() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
2626
        }
2627
2628
        if ($actions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $actions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2629
            $actions = implode('', $actions);
2630
2631
            $actionsStr .= Display::toolbarAction('portfolio-toolbar', [$actions]);
2632
        }
2633
2634
        $view->assign('baseurl', $this->baseUrl);
2635
        $view->assign('actions', $actionsStr);
2636
2637
        $view->assign('content', $content);
2638
        $view->display_one_col_template();
2639
    }
2640
2641
    private function categoryBelongToOwner(PortfolioCategory $category): bool
2642
    {
2643
        if ($category->getUser()->getId() != $this->owner->getId()) {
2644
            return false;
2645
        }
2646
2647
        return true;
2648
    }
2649
2650
    private function addAttachmentsFieldToForm(FormValidator $form)
2651
    {
2652
        $form->addButton('add_attachment', get_lang('AddAttachment'), 'plus');
2653
        $form->addHtml('<div id="container-attachments" style="display: none;">');
2654
        $form->addFile('attachment_file[]', get_lang('FilesAttachment'));
2655
        $form->addText('attachment_comment[]', get_lang('Description'), false);
2656
        $form->addHtml('</div>');
2657
2658
        $script = "$(function () {
2659
            var attachmentsTemplate = $('#container-attachments').html();
2660
            var \$btnAdd = $('[name=\"add_attachment\"]');
2661
            var \$reference = \$btnAdd.parents('.form-group');
2662
2663
            \$btnAdd.on('click', function (e) {
2664
                e.preventDefault();
2665
2666
                $(attachmentsTemplate).insertBefore(\$reference);
2667
            });
2668
        })";
2669
2670
        $form->addHtml("<script>$script</script>");
2671
    }
2672
2673
    private function processAttachments(
2674
        FormValidator $form,
2675
        User $user,
2676
        int $originId,
2677
        int $originType
2678
    ) {
2679
        $em = Database::getManager();
2680
        $fs = new Filesystem();
2681
2682
        $comments = $form->getSubmitValue('attachment_comment');
2683
2684
        foreach ($_FILES['attachment_file']['error'] as $i => $attachmentFileError) {
2685
            if ($attachmentFileError != UPLOAD_ERR_OK) {
2686
                continue;
2687
            }
2688
2689
            $_file = [
2690
                'name' => $_FILES['attachment_file']['name'][$i],
2691
                'type' => $_FILES['attachment_file']['type'][$i],
2692
                'tmp_name' => $_FILES['attachment_file']['tmp_name'][$i],
2693
                'size' => $_FILES['attachment_file']['size'][$i],
2694
            ];
2695
2696
            if (empty($_file['type'])) {
2697
                $_file['type'] = DocumentManager::file_get_mime_type($_file['name']);
2698
            }
2699
2700
            $newFileName = add_ext_on_mime(stripslashes($_file['name']), $_file['type']);
2701
2702
            if (!filter_extension($newFileName)) {
2703
                Display::addFlash(Display::return_message(get_lang('UplUnableToSaveFileFilteredExtension'), 'error'));
2704
                continue;
2705
            }
2706
2707
            $newFileName = uniqid();
2708
            $attachmentsDirectory = UserManager::getUserPathById($user->getId(), 'system').'portfolio_attachments/';
2709
2710
            if (!$fs->exists($attachmentsDirectory)) {
2711
                $fs->mkdir($attachmentsDirectory, api_get_permissions_for_new_directories());
2712
            }
2713
2714
            $attachmentFilename = $attachmentsDirectory.$newFileName;
2715
2716
            if (is_uploaded_file($_file['tmp_name'])) {
2717
                $moved = move_uploaded_file($_file['tmp_name'], $attachmentFilename);
2718
2719
                if (!$moved) {
2720
                    Display::addFlash(Display::return_message(get_lang('UplUnableToSaveFile'), 'error'));
2721
                    continue;
2722
                }
2723
            }
2724
2725
            $attachment = new PortfolioAttachment();
2726
            $attachment
2727
                ->setFilename($_file['name'])
2728
                ->setComment($comments[$i])
2729
                ->setPath($newFileName)
2730
                ->setOrigin($originId)
2731
                ->setOriginType($originType)
2732
                ->setSize($_file['size']);
2733
2734
            $em->persist($attachment);
2735
            $em->flush();
2736
        }
2737
    }
2738
2739
    private function itemBelongToOwner(Portfolio $item): bool
2740
    {
2741
        if ($item->getUser()->getId() != $this->owner->getId()) {
2742
            return false;
2743
        }
2744
2745
        return true;
2746
    }
2747
2748
    private function createFormTagFilter(bool $listByUser = false): FormValidator
2749
    {
2750
        $em = Database::getManager();
2751
        $tagTepo = $em->getRepository(Tag::class);
2752
2753
        $extraField = new ExtraField('portfolio');
2754
        $tagFieldInfo = $extraField->get_handler_field_info_by_tags('tags');
2755
2756
        $selectTagOptions = array_map(
2757
            function (array $tagOption) {
2758
                return $tagOption['tag'];
2759
            },
2760
            $tagFieldInfo['options'] ?? []
2761
        );
2762
2763
        $frmTagList = new FormValidator(
2764
            'frm_tag_list',
2765
            'get',
2766
            $this->baseUrl.($listByUser ? 'user='.$this->owner->getId() : ''),
2767
            '',
2768
            [],
2769
            FormValidator::LAYOUT_BOX
2770
        );
2771
2772
        $frmTagList->addDatePicker('date', get_lang('CreationDate'));
2773
2774
        $frmTagList->addSelect(
2775
            'tags',
2776
            get_lang('Tags'),
2777
            $selectTagOptions,
2778
            ['multiple' => 'multiple']
2779
        );
2780
2781
        $frmTagList->addText('text', get_lang('Search'), false)->setIcon('search');
2782
        $frmTagList->applyFilter('text', 'trim');
2783
        $frmTagList->addHtml('<br>');
2784
        $frmTagList->addButtonFilter(get_lang('Filter'));
2785
2786
        if ($this->course) {
2787
            $frmTagList->addHidden('cidReq', $this->course->getCode());
2788
            $frmTagList->addHidden('id_session', $this->session ? $this->session->getId() : 0);
2789
            $frmTagList->addHidden('gidReq', 0);
2790
            $frmTagList->addHidden('gradebook', 0);
2791
            $frmTagList->addHidden('origin', '');
2792
            $frmTagList->addHidden('categoryId', 0);
2793
            $frmTagList->addHidden('subCategoryIds', '');
2794
2795
            if ($listByUser) {
2796
                $frmTagList->addHidden('user', $this->owner->getId());
2797
            }
2798
        }
2799
2800
        return $frmTagList;
2801
    }
2802
2803
    /**
2804
     * @throws Exception
2805
     */
2806
    private function createFormStudentFilter(bool $listByUser = false, bool $listHighlighted = false): FormValidator
2807
    {
2808
        $frmStudentList = new FormValidator(
2809
            'frm_student_list',
2810
            'get',
2811
            $this->baseUrl,
2812
            '',
2813
            [],
2814
            FormValidator::LAYOUT_BOX
2815
        );
2816
2817
        $urlParams = http_build_query(
2818
            [
2819
                'a' => 'search_user_by_course',
2820
                'course_id' => $this->course->getId(),
2821
                'session_id' => $this->session ? $this->session->getId() : 0,
2822
            ]
2823
        );
2824
2825
        /** @var SelectAjax $slctUser */
2826
        $slctUser = $frmStudentList->addSelectAjax(
2827
            'user',
2828
            get_lang('SelectLearnerPortfolio'),
2829
            [],
2830
            [
2831
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
2832
                'placeholder' => get_lang('SearchStudent'),
2833
                'formatResult' => SelectAjax::templateResultForUsersInCourse(),
2834
                'formatSelection' => SelectAjax::templateSelectionForUsersInCourse(),
2835
            ]
2836
        );
2837
2838
        if ($listByUser) {
2839
            $slctUser->addOption(
2840
                $this->owner->getCompleteName(),
2841
                $this->owner->getId(),
2842
                [
2843
                    'data-avatarurl' => UserManager::getUserPicture($this->owner->getId()),
2844
                    'data-username' => $this->owner->getUsername(),
2845
                ]
2846
            );
2847
2848
            $link = Display::url(
2849
                get_lang('BackToMainPortfolio'),
2850
                $this->baseUrl
2851
            );
2852
        } else {
2853
            $link = Display::url(
2854
                get_lang('SeeMyPortfolio'),
2855
                $this->baseUrl.http_build_query(['user' => api_get_user_id()])
2856
            );
2857
        }
2858
2859
        $frmStudentList->addHtml("<p>$link</p>");
2860
2861
        if ($listHighlighted) {
2862
            $link = Display::url(
2863
                get_lang('BackToMainPortfolio'),
2864
                $this->baseUrl
2865
            );
2866
        } else {
2867
            $link = Display::url(
2868
                get_lang('SeeHighlights'),
2869
                $this->baseUrl.http_build_query(['list_highlighted' => true])
2870
            );
2871
        }
2872
2873
        $frmStudentList->addHtml("<p>$link</p>");
2874
2875
        return $frmStudentList;
2876
    }
2877
2878
    private function getCategoriesForIndex(?int $currentUserId = null, ?int $parentId = null): array
2879
    {
2880
        $categoriesCriteria = [];
2881
        if (isset($currentUserId)) {
2882
            $categoriesCriteria['user'] = $this->owner;
2883
        }
2884
        if (!api_is_platform_admin() && $currentUserId !== $this->owner->getId()) {
2885
            $categoriesCriteria['isVisible'] = true;
2886
        }
2887
        if (isset($parentId)) {
2888
            $categoriesCriteria['parentId'] = $parentId;
2889
        }
2890
2891
        return $this->em
2892
            ->getRepository(PortfolioCategory::class)
2893
            ->findBy($categoriesCriteria);
2894
    }
2895
2896
    private function getHighlightedItems()
2897
    {
2898
        $queryBuilder = $this->em->createQueryBuilder();
2899
        $queryBuilder
2900
            ->select('pi')
2901
            ->from(Portfolio::class, 'pi')
2902
            ->where('pi.course = :course')
2903
            ->andWhere('pi.isHighlighted = TRUE')
2904
            ->setParameter('course', $this->course);
2905
2906
        if ($this->session) {
2907
            $queryBuilder->andWhere('pi.session = :session');
2908
            $queryBuilder->setParameter('session', $this->session);
2909
        } else {
2910
            $queryBuilder->andWhere('pi.session IS NULL');
2911
        }
2912
2913
        $visibilityCriteria = [Portfolio::VISIBILITY_VISIBLE];
2914
2915
        if (api_is_allowed_to_edit()) {
2916
            $visibilityCriteria[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
2917
        }
2918
2919
        $queryBuilder
2920
            ->andWhere(
2921
                $queryBuilder->expr()->orX(
2922
                    'pi.user = :current_user',
2923
                    $queryBuilder->expr()->andX(
2924
                        'pi.user != :current_user',
2925
                        $queryBuilder->expr()->in('pi.visibility', $visibilityCriteria)
2926
                    )
2927
                )
2928
            )
2929
            ->setParameter('current_user', api_get_user_id());
2930
2931
        $queryBuilder->orderBy('pi.creationDate', 'DESC');
2932
2933
        return $queryBuilder->getQuery()->getResult();
2934
    }
2935
2936
    private function getItemsForIndex(
2937
        bool $listByUser = false,
2938
        FormValidator $frmFilterList = null
2939
    ) {
2940
        $currentUserId = api_get_user_id();
2941
2942
        if ($this->course) {
2943
            $queryBuilder = $this->em->createQueryBuilder();
2944
            $queryBuilder
2945
                ->select('pi')
2946
                ->from(Portfolio::class, 'pi')
2947
                ->where('pi.course = :course');
2948
2949
            $queryBuilder->setParameter('course', $this->course);
2950
2951
            if ($this->session) {
2952
                $queryBuilder->andWhere('pi.session = :session');
2953
                $queryBuilder->setParameter('session', $this->session);
2954
            } else {
2955
                $queryBuilder->andWhere('pi.session IS NULL');
2956
            }
2957
2958
            if ($frmFilterList && $frmFilterList->validate()) {
2959
                $values = $frmFilterList->exportValues();
2960
2961
                if (!empty($values['date'])) {
2962
                    $queryBuilder
2963
                        ->andWhere('pi.creationDate >= :date')
2964
                        ->setParameter(':date', api_get_utc_datetime($values['date'], false, true))
2965
                    ;
2966
                }
2967
2968
                if (!empty($values['tags'])) {
2969
                    $queryBuilder
2970
                        ->innerJoin(ExtraFieldRelTag::class, 'efrt', Join::WITH, 'efrt.itemId = pi.id')
2971
                        ->innerJoin(ExtraFieldEntity::class, 'ef', Join::WITH, 'ef.id = efrt.fieldId')
2972
                        ->andWhere('ef.extraFieldType = :efType')
2973
                        ->andWhere('ef.variable = :variable')
2974
                        ->andWhere('efrt.tagId IN (:tags)');
2975
2976
                    $queryBuilder->setParameter('efType', ExtraFieldEntity::PORTFOLIO_TYPE);
2977
                    $queryBuilder->setParameter('variable', 'tags');
2978
                    $queryBuilder->setParameter('tags', $values['tags']);
2979
                }
2980
2981
                if (!empty($values['text'])) {
2982
                    $queryBuilder->andWhere(
2983
                        $queryBuilder->expr()->orX(
2984
                            $queryBuilder->expr()->like('pi.title', ':text'),
2985
                            $queryBuilder->expr()->like('pi.content', ':text')
2986
                        )
2987
                    );
2988
2989
                    $queryBuilder->setParameter('text', '%'.$values['text'].'%');
2990
                }
2991
2992
                // Filters by category level 0
2993
                $searchCategories = [];
2994
                if (!empty($values['categoryId'])) {
2995
                    $searchCategories[] = $values['categoryId'];
2996
                    $subCategories = $this->getCategoriesForIndex(null, $values['categoryId']);
2997
                    if (count($subCategories) > 0) {
2998
                        foreach ($subCategories as $subCategory) {
2999
                            $searchCategories[] = $subCategory->getId();
3000
                        }
3001
                    }
3002
                    $queryBuilder->andWhere('pi.category IN('.implode(',', $searchCategories).')');
3003
                }
3004
3005
                // Filters by sub-category, don't show the selected values
3006
                $diff = [];
3007
                if (!empty($values['subCategoryIds']) && !('all' === $values['subCategoryIds'])) {
3008
                    $subCategoryIds = explode(',', $values['subCategoryIds']);
3009
                    $diff = array_diff($searchCategories, $subCategoryIds);
3010
                } else {
3011
                    if (trim($values['subCategoryIds']) === '') {
3012
                        $diff = $searchCategories;
3013
                    }
3014
                }
3015
                if (!empty($diff)) {
3016
                    unset($diff[0]);
3017
                    if (!empty($diff)) {
3018
                        $queryBuilder->andWhere('pi.category NOT IN('.implode(',', $diff).')');
3019
                    }
3020
                }
3021
            }
3022
3023
            if ($listByUser) {
3024
                $queryBuilder
3025
                    ->andWhere('pi.user = :user')
3026
                    ->setParameter('user', $this->owner);
3027
            }
3028
3029
            $visibilityCriteria = [Portfolio::VISIBILITY_VISIBLE];
3030
3031
            if (api_is_allowed_to_edit()) {
3032
                $visibilityCriteria[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
3033
            }
3034
3035
            $queryBuilder
3036
                ->andWhere(
3037
                    $queryBuilder->expr()->orX(
3038
                        'pi.user = :current_user',
3039
                        $queryBuilder->expr()->andX(
3040
                            'pi.user != :current_user',
3041
                            $queryBuilder->expr()->in('pi.visibility', $visibilityCriteria)
3042
                        )
3043
                    )
3044
                )
3045
                ->setParameter('current_user', $currentUserId);
3046
3047
            $queryBuilder->orderBy('pi.creationDate', 'DESC');
3048
3049
            $items = $queryBuilder->getQuery()->getResult();
3050
        } else {
3051
            $itemsCriteria = [];
3052
            $itemsCriteria['category'] = null;
3053
            $itemsCriteria['user'] = $this->owner;
3054
3055
            if ($currentUserId !== $this->owner->getId()) {
3056
                $itemsCriteria['visibility'] = Portfolio::VISIBILITY_VISIBLE;
3057
            }
3058
3059
            $items = $this->em
3060
                ->getRepository(Portfolio::class)
3061
                ->findBy($itemsCriteria, ['creationDate' => 'DESC']);
3062
        }
3063
3064
        return $items;
3065
    }
3066
3067
    /**
3068
     * @throws \Doctrine\ORM\ORMException
3069
     * @throws \Doctrine\ORM\OptimisticLockException
3070
     * @throws \Doctrine\ORM\TransactionRequiredException
3071
     */
3072
    private function createCommentForm(Portfolio $item): string
3073
    {
3074
        $formAction = $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]);
3075
3076
        $form = new FormValidator('frm_comment', 'post', $formAction);
3077
        $form->addHeader(get_lang('AddNewComment'));
3078
        $form->addHtmlEditor('content', get_lang('Comments'), true, false, ['ToolbarSet' => 'Minimal']);
3079
        $form->addHidden('item', $item->getId());
3080
        $form->addHidden('parent', 0);
3081
        $form->applyFilter('content', 'trim');
3082
3083
        $this->addAttachmentsFieldToForm($form);
3084
3085
        $form->addButtonSave(get_lang('Save'));
3086
3087
        if ($form->validate()) {
3088
            $values = $form->exportValues();
3089
3090
            $parentComment = $this->em->find(PortfolioComment::class, $values['parent']);
3091
3092
            $comment = new PortfolioComment();
3093
            $comment
3094
                ->setAuthor($this->owner)
3095
                ->setParent($parentComment)
3096
                ->setContent($values['content'])
3097
                ->setDate(api_get_utc_datetime(null, false, true))
3098
                ->setItem($item);
3099
3100
            $this->em->persist($comment);
3101
            $this->em->flush();
3102
3103
            $this->processAttachments(
3104
                $form,
3105
                $comment->getAuthor(),
3106
                $comment->getId(),
3107
                PortfolioAttachment::TYPE_COMMENT
3108
            );
3109
3110
            $hook = HookPortfolioItemCommented::create();
3111
            $hook->setEventData(['comment' => $comment]);
3112
            $hook->notifyItemCommented();
3113
3114
            PortfolioNotifier::notifyTeachersAndAuthor($comment);
3115
3116
            Display::addFlash(
3117
                Display::return_message(get_lang('CommentAdded'), 'success')
3118
            );
3119
3120
            header("Location: $formAction");
3121
            exit;
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return string. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
3122
        }
3123
3124
        return $form->returnForm();
3125
    }
3126
3127
    private function generateAttachmentList($post, bool $includeHeader = true): string
3128
    {
3129
        $attachmentsRepo = $this->em->getRepository(PortfolioAttachment::class);
3130
3131
        $postOwnerId = 0;
3132
3133
        if ($post instanceof Portfolio) {
3134
            $attachments = $attachmentsRepo->findFromItem($post);
3135
3136
            $postOwnerId = $post->getUser()->getId();
3137
        } elseif ($post instanceof PortfolioComment) {
3138
            $attachments = $attachmentsRepo->findFromComment($post);
3139
3140
            $postOwnerId = $post->getAuthor()->getId();
3141
        }
3142
3143
        if (empty($attachments)) {
3144
            return '';
3145
        }
3146
3147
        $currentUserId = api_get_user_id();
3148
3149
        $listItems = '<ul class="fa-ul">';
3150
3151
        $deleteIcon = Display::return_icon(
3152
            'delete.png',
3153
            get_lang('DeleteAttachment'),
3154
            ['style' => 'display: inline-block'],
3155
            ICON_SIZE_TINY
3156
        );
3157
        $deleteAttrs = ['class' => 'btn-portfolio-delete'];
3158
3159
        /** @var PortfolioAttachment $attachment */
3160
        foreach ($attachments as $attachment) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $attachments does not seem to be defined for all execution paths leading up to this point.
Loading history...
3161
            $downloadParams = http_build_query(['action' => 'download_attachment', 'file' => $attachment->getPath()]);
3162
            $deleteParams = http_build_query(['action' => 'delete_attachment', 'file' => $attachment->getPath()]);
3163
3164
            $listItems .= '<li>'
3165
                .'<span class="fa-li fa fa-paperclip" aria-hidden="true"></span>'
3166
                .Display::url(
3167
                    Security::remove_XSS($attachment->getFilename()),
3168
                    $this->baseUrl.$downloadParams
3169
                );
3170
3171
            if ($currentUserId === $postOwnerId) {
3172
                $listItems .= PHP_EOL.Display::url($deleteIcon, $this->baseUrl.$deleteParams, $deleteAttrs);
3173
            }
3174
3175
            if ($attachment->getComment()) {
3176
                $listItems .= '<p class="text-muted">'.Security::remove_XSS($attachment->getComment()).'</p>';
3177
            }
3178
3179
            $listItems .= '</li>';
3180
        }
3181
3182
        $listItems .= '</ul>';
3183
3184
        if ($includeHeader) {
3185
            $listItems = '<h1 class="h4">'.get_lang('AttachmentFiles').'</h1>'
3186
                .$listItems;
3187
        }
3188
3189
        return $listItems;
3190
    }
3191
3192
    private function generateItemContent(Portfolio $item): string
3193
    {
3194
        $originId = $item->getOrigin();
3195
3196
        if (empty($originId)) {
3197
            return $item->getContent();
3198
        }
3199
3200
        $em = Database::getManager();
3201
3202
        $originContent = '';
3203
        $originContentFooter = '';
3204
3205
        if (Portfolio::TYPE_ITEM === $item->getOriginType()) {
3206
            $origin = $em->find(Portfolio::class, $item->getOrigin());
3207
3208
            if ($origin) {
3209
                $originContent = $origin->getContent();
3210
                $originContentFooter = vsprintf(
3211
                    get_lang('OriginallyPublishedAsXTitleByYUser'),
3212
                    [
3213
                        "<cite>{$origin->getTitle(true)}</cite>",
3214
                        $origin->getUser()->getCompleteName(),
3215
                    ]
3216
                );
3217
            }
3218
        } elseif (Portfolio::TYPE_COMMENT === $item->getOriginType()) {
3219
            $origin = $em->find(PortfolioComment::class, $item->getOrigin());
3220
3221
            if ($origin) {
3222
                $originContent = $origin->getContent();
3223
                $originContentFooter = vsprintf(
3224
                    get_lang('OriginallyCommentedByXUserInYItem'),
3225
                    [
3226
                        $origin->getAuthor()->getCompleteName(),
3227
                        "<cite>{$origin->getItem()->getTitle(true)}</cite>",
3228
                    ]
3229
                );
3230
            }
3231
        }
3232
3233
        if ($originContent) {
3234
            return "<figure>
3235
                    <blockquote>$originContent</blockquote>
3236
                    <figcaption style=\"margin-bottom: 10px;\">$originContentFooter</figcaption>
3237
                </figure>
3238
                <div class=\"clearfix\">".Security::remove_XSS($item->getContent()).'</div>'
3239
            ;
3240
        }
3241
3242
        return Security::remove_XSS($item->getContent());
3243
    }
3244
3245
    private function getItemsInHtmlFormatted(array $items): array
3246
    {
3247
        $itemsHtml = [];
3248
3249
        /** @var Portfolio $item */
3250
        foreach ($items as $item) {
3251
            $itemCourse = $item->getCourse();
3252
            $itemSession = $item->getSession();
3253
3254
            $creationDate = api_convert_and_format_date($item->getCreationDate());
3255
            $updateDate = api_convert_and_format_date($item->getUpdateDate());
3256
3257
            $metadata = '<ul class="list-unstyled text-muted">';
3258
3259
            if ($itemSession) {
3260
                $metadata .= '<li>'.get_lang('Course').': '.$itemSession->getName().' ('
3261
                    .$itemCourse->getTitle().') </li>';
3262
            } elseif ($itemCourse) {
3263
                $metadata .= '<li>'.get_lang('Course').': '.$itemCourse->getTitle().'</li>';
3264
            }
3265
3266
            $metadata .= '<li>'.sprintf(get_lang('CreationDateXDate'), $creationDate).'</li>';
3267
3268
            if ($itemCourse) {
3269
                $propertyInfo = api_get_item_property_info(
3270
                    $itemCourse->getId(),
3271
                    TOOL_PORTFOLIO,
3272
                    $item->getId(),
3273
                    $itemSession ? $itemSession->getId() : 0
3274
                );
3275
3276
                if ($propertyInfo) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $propertyInfo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
3277
                    $metadata .= '<li>'
3278
                        .sprintf(
3279
                            get_lang('UpdatedOnDateXByUserY'),
3280
                            api_convert_and_format_date($propertyInfo['lastedit_date'], DATE_TIME_FORMAT_LONG),
3281
                            api_get_user_entity($propertyInfo['lastedit_user_id'])->getCompleteName()
3282
                        )
3283
                        .'</li>';
3284
                }
3285
            } else {
3286
                $metadata .= '<li>'.sprintf(get_lang('UpdateDateXDate'), $updateDate).'</li>';
3287
            }
3288
3289
            if ($item->getCategory()) {
3290
                $metadata .= '<li>'.sprintf(get_lang('CategoryXName'), $item->getCategory()->getTitle()).'</li>';
3291
            }
3292
3293
            $metadata .= '</ul>';
3294
3295
            $itemContent = $this->generateItemContent($item);
3296
3297
            $itemsHtml[] = Display::panel($itemContent, Security::remove_XSS($item->getTitle()), '', 'info', $metadata);
3298
        }
3299
3300
        return $itemsHtml;
3301
    }
3302
3303
    private function getCommentsInHtmlFormatted(array $comments): array
3304
    {
3305
        $commentsHtml = [];
3306
3307
        /** @var PortfolioComment $comment */
3308
        foreach ($comments as $comment) {
3309
            $item = $comment->getItem();
3310
            $date = api_convert_and_format_date($comment->getDate());
3311
3312
            $metadata = '<ul class="list-unstyled text-muted">';
3313
            $metadata .= '<li>'.sprintf(get_lang('DateXDate'), $date).'</li>';
3314
            $metadata .= '<li>'.sprintf(get_lang('PortfolioItemTitleXName'), Security::remove_XSS($item->getTitle()))
3315
                .'</li>';
3316
            $metadata .= '</ul>';
3317
3318
            $commentsHtml[] = Display::panel(
3319
                Security::remove_XSS($comment->getContent()),
3320
                '',
3321
                '',
3322
                'default',
3323
                $metadata
3324
            );
3325
        }
3326
3327
        return $commentsHtml;
3328
    }
3329
3330
    private function fixImagesSourcesToHtml(string $htmlContent): string
3331
    {
3332
        $doc = new DOMDocument();
3333
        @$doc->loadHTML($htmlContent);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for loadHTML(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

3333
        /** @scrutinizer ignore-unhandled */ @$doc->loadHTML($htmlContent);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3334
3335
        $elements = $doc->getElementsByTagName('img');
3336
3337
        if (empty($elements->length)) {
3338
            return $htmlContent;
3339
        }
3340
3341
        $webCoursePath = api_get_path(WEB_COURSE_PATH);
3342
        $webUploadPath = api_get_path(WEB_UPLOAD_PATH);
3343
3344
        /** @var \DOMElement $element */
3345
        foreach ($elements as $element) {
3346
            $src = trim($element->getAttribute('src'));
3347
3348
            if (strpos($src, 'http') === 0) {
3349
                continue;
3350
            }
3351
3352
            if (strpos($src, '/app/upload/') === 0) {
3353
                $element->setAttribute(
3354
                    'src',
3355
                    preg_replace('/\/app/upload\//', $webUploadPath, $src, 1)
3356
                );
3357
3358
                continue;
3359
            }
3360
3361
            if (strpos($src, '/courses/') === 0) {
3362
                $element->setAttribute(
3363
                    'src',
3364
                    preg_replace('/\/courses\//', $webCoursePath, $src, 1)
3365
                );
3366
3367
                continue;
3368
            }
3369
        }
3370
3371
        return $doc->saveHTML();
3372
    }
3373
3374
    private function formatZipIndexFile(HTML_Table $tblItems, HTML_Table $tblComments): string
3375
    {
3376
        $htmlContent = Display::page_header($this->owner->getCompleteNameWithUsername());
3377
        $htmlContent .= Display::page_subheader2(get_lang('PortfolioItems'));
3378
3379
        $htmlContent .= $tblItems->getRowCount() > 0
3380
            ? $tblItems->toHtml()
3381
            : Display::return_message(get_lang('NoItemsInYourPortfolio'), 'warning');
3382
3383
        $htmlContent .= Display::page_subheader2(get_lang('PortfolioCommentsMade'));
3384
3385
        $htmlContent .= $tblComments->getRowCount() > 0
3386
            ? $tblComments->toHtml()
3387
            : Display::return_message(get_lang('YouHaveNotCommented'), 'warning');
3388
3389
        $webAssetsPath = api_get_path(WEB_PUBLIC_PATH).'assets/';
3390
3391
        $doc = new DOMDocument();
3392
        @$doc->loadHTML($htmlContent);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for loadHTML(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

3392
        /** @scrutinizer ignore-unhandled */ @$doc->loadHTML($htmlContent);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3393
3394
        $stylesheet1 = $doc->createElement('link');
3395
        $stylesheet1->setAttribute('rel', 'stylesheet');
3396
        $stylesheet1->setAttribute('href', $webAssetsPath.'bootstrap/dist/css/bootstrap.min.css');
3397
        $stylesheet2 = $doc->createElement('link');
3398
        $stylesheet2->setAttribute('rel', 'stylesheet');
3399
        $stylesheet2->setAttribute('href', $webAssetsPath.'fontawesome/css/font-awesome.min.css');
3400
        $stylesheet3 = $doc->createElement('link');
3401
        $stylesheet3->setAttribute('rel', 'stylesheet');
3402
        $stylesheet3->setAttribute('href', ChamiloApi::getEditorDocStylePath());
3403
3404
        $head = $doc->createElement('head');
3405
        $head->appendChild($stylesheet1);
3406
        $head->appendChild($stylesheet2);
3407
        $head->appendChild($stylesheet3);
3408
3409
        $doc->documentElement->insertBefore(
3410
            $head,
3411
            $doc->getElementsByTagName('body')->item(0)
3412
        );
3413
3414
        return $doc->saveHTML();
3415
    }
3416
3417
    /**
3418
     * It parsers a title for a variable in lang.
3419
     *
3420
     * @param $defaultDisplayText
3421
     *
3422
     * @return string
3423
     */
3424
    private function getLanguageVariable($defaultDisplayText)
3425
    {
3426
        $variableLanguage = api_replace_dangerous_char(strtolower($defaultDisplayText));
3427
        $variableLanguage = preg_replace('/[^A-Za-z0-9\_]/', '', $variableLanguage); // Removes special chars except underscore.
3428
        if (is_numeric($variableLanguage[0])) {
3429
            $variableLanguage = '_'.$variableLanguage;
3430
        }
3431
        $variableLanguage = api_underscore_to_camel_case($variableLanguage);
3432
3433
        return $variableLanguage;
3434
    }
3435
3436
    /**
3437
     * It translates the text as parameter.
3438
     *
3439
     * @param $defaultDisplayText
3440
     *
3441
     * @return mixed
3442
     */
3443
    private function translateDisplayName($defaultDisplayText)
3444
    {
3445
        $variableLanguage = $this->getLanguageVariable($defaultDisplayText);
3446
3447
        return isset($GLOBALS[$variableLanguage]) ? $GLOBALS[$variableLanguage] : $defaultDisplayText;
3448
    }
3449
}
3450