Passed
Push — 1.11.x ( df8c16...f46d87 )
by Angel Fernando Quiroz
08:09 queued 13s
created

PortfolioController::copyItem()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 28
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 20
nc 1
nop 1
dl 0
loc 28
rs 9.6
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\UserBundle\Entity\User;
14
use Doctrine\ORM\Query\Expr\Join;
15
use Mpdf\MpdfException;
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
                    $commentActions = [];
1152
                    /** @var PortfolioComment $comment */
1153
                    $comment = $commentsRepo->find($node['id']);
1154
1155
                    if ($this->commentBelongsToOwner($comment)) {
1156
                        $commentActions[] = Display::url(
1157
                            Display::return_icon(
1158
                                $comment->isTemplate() ? 'wizard.png' : 'wizard_na.png',
1159
                                $comment->isTemplate() ? get_lang('RemoveAsTemplate') : get_lang('AddAsTemplate')
1160
                            ),
1161
                            $this->baseUrl.http_build_query(['action' => 'template_comment', 'id' => $comment->getId()])
1162
                        );
1163
                    }
1164
1165
                    $commentActions[] = Display::url(
1166
                        Display::return_icon('discuss.png', get_lang('ReplyToThisComment')),
1167
                        '#',
1168
                        [
1169
                            'data-comment' => htmlspecialchars(
1170
                                json_encode(['id' => $comment->getId()])
1171
                            ),
1172
                            'role' => 'button',
1173
                            'class' => 'btn-reply-to',
1174
                        ]
1175
                    );
1176
                    $commentActions[] = Display::url(
1177
                        Display::return_icon('copy.png', get_lang('CopyToMyPortfolio')),
1178
                        $this->baseUrl.http_build_query(
1179
                            [
1180
                                'action' => 'copy',
1181
                                'copy' => 'comment',
1182
                                'id' => $comment->getId(),
1183
                            ]
1184
                        )
1185
                    );
1186
1187
                    $isAllowedToEdit = api_is_allowed_to_edit();
1188
1189
                    if ($isAllowedToEdit) {
1190
                        $commentActions[] = Display::url(
1191
                            Display::return_icon('copy.png', get_lang('CopyToStudentPortfolio')),
1192
                            $this->baseUrl.http_build_query(
1193
                                [
1194
                                    'action' => 'teacher_copy',
1195
                                    'copy' => 'comment',
1196
                                    'id' => $comment->getId(),
1197
                                ]
1198
                            )
1199
                        );
1200
1201
                        if ($comment->isImportant()) {
1202
                            $commentActions[] = Display::url(
1203
                                Display::return_icon('drawing-pin.png', get_lang('UnmarkCommentAsImportant')),
1204
                                $this->baseUrl.http_build_query(
1205
                                    [
1206
                                        'action' => 'mark_important',
1207
                                        'item' => $item->getId(),
1208
                                        'id' => $comment->getId(),
1209
                                    ]
1210
                                )
1211
                            );
1212
                        } else {
1213
                            $commentActions[] = Display::url(
1214
                                Display::return_icon('drawing-pin.png', get_lang('MarkCommentAsImportant')),
1215
                                $this->baseUrl.http_build_query(
1216
                                    [
1217
                                        'action' => 'mark_important',
1218
                                        'item' => $item->getId(),
1219
                                        'id' => $comment->getId(),
1220
                                    ]
1221
                                )
1222
                            );
1223
                        }
1224
1225
                        if ($this->course && '1' === api_get_course_setting('qualify_portfolio_comment')) {
1226
                            $commentActions[] = Display::url(
1227
                                Display::return_icon('quiz.png', get_lang('QualifyThisPortfolioComment')),
1228
                                $this->baseUrl.http_build_query(
1229
                                    [
1230
                                        'action' => 'qualify',
1231
                                        'comment' => $comment->getId(),
1232
                                    ]
1233
                                )
1234
                            );
1235
                        }
1236
                    }
1237
1238
                    $nodeHtml = '<div class="pull-right">'.implode(PHP_EOL, $commentActions).'</div>'.PHP_EOL
1239
                        .'<footer class="media-heading h4">'.PHP_EOL
1240
                        .'<p>'.$comment->getAuthor()->getCompleteName().'</p>'.PHP_EOL;
1241
1242
                    if ($comment->isImportant()
1243
                        && ($this->itemBelongToOwner($comment->getItem()) || $isAllowedToEdit)
1244
                    ) {
1245
                        $nodeHtml .= '<span class="pull-right label label-warning origin-style">'
1246
                            .get_lang('CommentMarkedAsImportant')
1247
                            .'</span>'.PHP_EOL;
1248
                    }
1249
1250
                    $nodeHtml .= '<p class="small">'.$clockIcon.PHP_EOL
1251
                        .Display::dateToStringAgoAndLongDate($comment->getDate()).'</p>'.PHP_EOL;
1252
1253
                    $nodeHtml .= '</footer>'.PHP_EOL
1254
                        .Security::remove_XSS($comment->getContent()).PHP_EOL;
1255
1256
                    $nodeHtml .= $this->generateAttachmentList($comment);
1257
1258
                    return $nodeHtml;
1259
                },
1260
            ]
1261
        );
1262
1263
        $template = new Template(null, false, false, false, false, false, false);
1264
        $template->assign('baseurl', $this->baseUrl);
1265
        $template->assign('item', $item);
1266
        $template->assign('item_content', $this->generateItemContent($item));
1267
        $template->assign('comments', $commentsHtml);
1268
        $template->assign('form', $form);
1269
        $template->assign('attachment_list', $this->generateAttachmentList($item));
1270
1271
        if ($itemCourse) {
1272
            $propertyInfo = api_get_item_property_info(
1273
                $itemCourse->getId(),
1274
                TOOL_PORTFOLIO,
1275
                $item->getId(),
1276
                $itemSession ? $itemSession->getId() : 0
1277
            );
1278
1279
            if ($propertyInfo) {
1280
                $template->assign(
1281
                    'last_edit',
1282
                    [
1283
                        'date' => $propertyInfo['lastedit_date'],
1284
                        'user' => api_get_user_entity($propertyInfo['lastedit_user_id'])->getCompleteName(),
1285
                    ]
1286
                );
1287
            }
1288
        }
1289
1290
        $layout = $template->get_template('portfolio/view.html.twig');
1291
        $content = $template->fetch($layout);
1292
1293
        $interbreadcrumb[] = ['name' => get_lang('Portfolio'), 'url' => $this->baseUrl];
1294
1295
        $editLink = Display::url(
1296
            Display::return_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_MEDIUM),
1297
            $this->baseUrl.http_build_query(['action' => 'edit_item', 'id' => $item->getId()])
1298
        );
1299
1300
        $actions = [];
1301
        $actions[] = Display::url(
1302
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
1303
            $this->baseUrl
1304
        );
1305
1306
        if ($this->itemBelongToOwner($item)) {
1307
            $actions[] = $editLink;
1308
1309
            $actions[] = Display::url(
1310
                Display::return_icon(
1311
                    $item->isTemplate() ? 'wizard.png' : 'wizard_na.png',
1312
                    $item->isTemplate() ? get_lang('RemoveAsTemplate') : get_lang('AddAsTemplate'),
1313
                    [],
1314
                    ICON_SIZE_MEDIUM
1315
                ),
1316
                $this->baseUrl.http_build_query(['action' => 'template', 'id' => $item->getId()])
1317
            );
1318
1319
            $visibilityUrl = $this->baseUrl.http_build_query(['action' => 'visibility', 'id' => $item->getId()]);
1320
1321
            if ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN) {
1322
                $actions[] = Display::url(
1323
                    Display::return_icon('invisible.png', get_lang('MakeVisible'), [], ICON_SIZE_MEDIUM),
1324
                    $visibilityUrl
1325
                );
1326
            } elseif ($item->getVisibility() === Portfolio::VISIBILITY_VISIBLE) {
1327
                $actions[] = Display::url(
1328
                    Display::return_icon('visible.png', get_lang('MakeVisibleForTeachers'), [], ICON_SIZE_MEDIUM),
1329
                    $visibilityUrl
1330
                );
1331
            } elseif ($item->getVisibility() === Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER) {
1332
                $actions[] = Display::url(
1333
                    Display::return_icon('eye-slash.png', get_lang('MakeInvisible'), [], ICON_SIZE_MEDIUM),
1334
                    $visibilityUrl
1335
                );
1336
            }
1337
1338
            $actions[] = Display::url(
1339
                Display::return_icon('delete.png', get_lang('Delete'), [], ICON_SIZE_MEDIUM),
1340
                $this->baseUrl.http_build_query(['action' => 'delete_item', 'id' => $item->getId()])
1341
            );
1342
        } else {
1343
            $actions[] = Display::url(
1344
                Display::return_icon('copy.png', get_lang('CopyToMyPortfolio'), [], ICON_SIZE_MEDIUM),
1345
                $this->baseUrl.http_build_query(['action' => 'copy', 'id' => $item->getId()])
1346
            );
1347
        }
1348
1349
        if (api_is_allowed_to_edit()) {
1350
            $actions[] = Display::url(
1351
                Display::return_icon('copy.png', get_lang('CopyToStudentPortfolio'), [], ICON_SIZE_MEDIUM),
1352
                $this->baseUrl.http_build_query(['action' => 'teacher_copy', 'copy' => 'item', 'id' => $item->getId()])
1353
            );
1354
            $actions[] = $editLink;
1355
1356
            $highlightedUrl = $this->baseUrl.http_build_query(['action' => 'highlighted', 'id' => $item->getId()]);
1357
1358
            if ($item->isHighlighted()) {
1359
                $actions[] = Display::url(
1360
                    Display::return_icon('award_red.png', get_lang('UnmarkAsHighlighted'), [], ICON_SIZE_MEDIUM),
1361
                    $highlightedUrl
1362
                );
1363
            } else {
1364
                $actions[] = Display::url(
1365
                    Display::return_icon('award_red_na.png', get_lang('MarkAsHighlighted'), [], ICON_SIZE_MEDIUM),
1366
                    $highlightedUrl
1367
                );
1368
            }
1369
1370
            if ($itemCourse && '1' === api_get_course_setting('qualify_portfolio_item')) {
1371
                $actions[] = Display::url(
1372
                    Display::return_icon('quiz.png', get_lang('QualifyThisPortfolioItem'), [], ICON_SIZE_MEDIUM),
1373
                    $this->baseUrl.http_build_query(['action' => 'qualify', 'item' => $item->getId()])
1374
                );
1375
            }
1376
        }
1377
1378
        $this->renderView($content, $item->getTitle(true), $actions, false);
1379
    }
1380
1381
    /**
1382
     * @throws \Doctrine\ORM\ORMException
1383
     * @throws \Doctrine\ORM\OptimisticLockException
1384
     */
1385
    public function copyItem(Portfolio $originItem)
1386
    {
1387
        $currentTime = api_get_utc_datetime(null, false, true);
1388
1389
        $portfolio = new Portfolio();
1390
        $portfolio
1391
            ->setVisibility(Portfolio::VISIBILITY_HIDDEN)
1392
            ->setTitle(
1393
                sprintf(get_lang('PortfolioItemFromXUser'), $originItem->getUser()->getCompleteName())
1394
            )
1395
            ->setContent('')
1396
            ->setUser($this->owner)
1397
            ->setOrigin($originItem->getId())
1398
            ->setOriginType(Portfolio::TYPE_ITEM)
1399
            ->setCourse($this->course)
1400
            ->setSession($this->session)
1401
            ->setCreationDate($currentTime)
1402
            ->setUpdateDate($currentTime);
1403
1404
        $this->em->persist($portfolio);
1405
        $this->em->flush();
1406
1407
        Display::addFlash(
1408
            Display::return_message(get_lang('PortfolioItemAdded'), 'success')
1409
        );
1410
1411
        header("Location: $this->baseUrl".http_build_query(['action' => 'edit_item', 'id' => $portfolio->getId()]));
1412
        exit;
1413
    }
1414
1415
    /**
1416
     * @throws \Doctrine\ORM\ORMException
1417
     * @throws \Doctrine\ORM\OptimisticLockException
1418
     */
1419
    public function copyComment(PortfolioComment $originComment)
1420
    {
1421
        $currentTime = api_get_utc_datetime(null, false, true);
1422
1423
        $portfolio = new Portfolio();
1424
        $portfolio
1425
            ->setVisibility(Portfolio::VISIBILITY_HIDDEN)
1426
            ->setTitle(
1427
                sprintf(get_lang('PortfolioCommentFromXUser'), $originComment->getAuthor()->getCompleteName())
1428
            )
1429
            ->setContent('')
1430
            ->setUser($this->owner)
1431
            ->setOrigin($originComment->getId())
1432
            ->setOriginType(Portfolio::TYPE_COMMENT)
1433
            ->setCourse($this->course)
1434
            ->setSession($this->session)
1435
            ->setCreationDate($currentTime)
1436
            ->setUpdateDate($currentTime);
1437
1438
        $this->em->persist($portfolio);
1439
        $this->em->flush();
1440
1441
        Display::addFlash(
1442
            Display::return_message(get_lang('PortfolioItemAdded'), 'success')
1443
        );
1444
1445
        header("Location: $this->baseUrl".http_build_query(['action' => 'edit_item', 'id' => $portfolio->getId()]));
1446
        exit;
1447
    }
1448
1449
    /**
1450
     * @throws \Doctrine\ORM\ORMException
1451
     * @throws \Doctrine\ORM\OptimisticLockException
1452
     * @throws \Exception
1453
     */
1454
    public function teacherCopyItem(Portfolio $originItem)
1455
    {
1456
        $actionParams = http_build_query(['action' => 'teacher_copy', 'copy' => 'item', 'id' => $originItem->getId()]);
1457
1458
        $form = new FormValidator('teacher_copy_portfolio', 'post', $this->baseUrl.$actionParams);
1459
1460
        if (api_get_configuration_value('save_titles_as_html')) {
1461
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
1462
        } else {
1463
            $form->addText('title', get_lang('Title'));
1464
            $form->applyFilter('title', 'trim');
1465
        }
1466
1467
        $form->addLabel(
1468
            sprintf(get_lang('PortfolioItemFromXUser'), $originItem->getUser()->getCompleteName()),
1469
            Display::panel(
1470
                Security::remove_XSS($originItem->getContent())
1471
            )
1472
        );
1473
        $form->addHtmlEditor('content', get_lang('Content'), true, false, ['ToolbarSet' => 'NotebookStudent']);
1474
1475
        $urlParams = http_build_query(
1476
            [
1477
                'a' => 'search_user_by_course',
1478
                'course_id' => $this->course->getId(),
1479
                'session_id' => $this->session ? $this->session->getId() : 0,
1480
            ]
1481
        );
1482
        $form->addSelectAjax(
1483
            'students',
1484
            get_lang('Students'),
1485
            [],
1486
            [
1487
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1488
                'multiple' => true,
1489
            ]
1490
        );
1491
        $form->addRule('students', get_lang('ThisFieldIsRequired'), 'required');
1492
        $form->addButtonCreate(get_lang('Save'));
1493
1494
        $toolName = get_lang('CopyToStudentPortfolio');
1495
        $content = $form->returnForm();
1496
1497
        if ($form->validate()) {
1498
            $values = $form->exportValues();
1499
1500
            $currentTime = api_get_utc_datetime(null, false, true);
1501
1502
            foreach ($values['students'] as $studentId) {
1503
                $owner = api_get_user_entity($studentId);
1504
1505
                $portfolio = new Portfolio();
1506
                $portfolio
1507
                    ->setVisibility(Portfolio::VISIBILITY_HIDDEN)
1508
                    ->setTitle($values['title'])
1509
                    ->setContent($values['content'])
1510
                    ->setUser($owner)
1511
                    ->setOrigin($originItem->getId())
1512
                    ->setOriginType(Portfolio::TYPE_ITEM)
1513
                    ->setCourse($this->course)
1514
                    ->setSession($this->session)
1515
                    ->setCreationDate($currentTime)
1516
                    ->setUpdateDate($currentTime);
1517
1518
                $this->em->persist($portfolio);
1519
            }
1520
1521
            $this->em->flush();
1522
1523
            Display::addFlash(
1524
                Display::return_message(get_lang('PortfolioItemAddedToStudents'), 'success')
1525
            );
1526
1527
            header("Location: $this->baseUrl");
1528
            exit;
1529
        }
1530
1531
        $this->renderView($content, $toolName);
1532
    }
1533
1534
    /**
1535
     * @throws \Doctrine\ORM\ORMException
1536
     * @throws \Doctrine\ORM\OptimisticLockException
1537
     * @throws \Exception
1538
     */
1539
    public function teacherCopyComment(PortfolioComment $originComment)
1540
    {
1541
        $actionParams = http_build_query(
1542
            [
1543
                'action' => 'teacher_copy',
1544
                'copy' => 'comment',
1545
                'id' => $originComment->getId(),
1546
            ]
1547
        );
1548
1549
        $form = new FormValidator('teacher_copy_portfolio', 'post', $this->baseUrl.$actionParams);
1550
1551
        if (api_get_configuration_value('save_titles_as_html')) {
1552
            $form->addHtmlEditor('title', get_lang('Title'), true, false, ['ToolbarSet' => 'TitleAsHtml']);
1553
        } else {
1554
            $form->addText('title', get_lang('Title'));
1555
            $form->applyFilter('title', 'trim');
1556
        }
1557
1558
        $form->addLabel(
1559
            sprintf(get_lang('PortfolioCommentFromXUser'), $originComment->getAuthor()->getCompleteName()),
1560
            Display::panel(
1561
                Security::remove_XSS($originComment->getContent())
1562
            )
1563
        );
1564
        $form->addHtmlEditor('content', get_lang('Content'), true, false, ['ToolbarSet' => 'NotebookStudent']);
1565
1566
        $urlParams = http_build_query(
1567
            [
1568
                'a' => 'search_user_by_course',
1569
                'course_id' => $this->course->getId(),
1570
                'session_id' => $this->session ? $this->session->getId() : 0,
1571
            ]
1572
        );
1573
        $form->addSelectAjax(
1574
            'students',
1575
            get_lang('Students'),
1576
            [],
1577
            [
1578
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1579
                'multiple' => true,
1580
            ]
1581
        );
1582
        $form->addRule('students', get_lang('ThisFieldIsRequired'), 'required');
1583
        $form->addButtonCreate(get_lang('Save'));
1584
1585
        $toolName = get_lang('CopyToStudentPortfolio');
1586
        $content = $form->returnForm();
1587
1588
        if ($form->validate()) {
1589
            $values = $form->exportValues();
1590
1591
            $currentTime = api_get_utc_datetime(null, false, true);
1592
1593
            foreach ($values['students'] as $studentId) {
1594
                $owner = api_get_user_entity($studentId);
1595
1596
                $portfolio = new Portfolio();
1597
                $portfolio
1598
                    ->setVisibility(Portfolio::VISIBILITY_HIDDEN)
1599
                    ->setTitle($values['title'])
1600
                    ->setContent($values['content'])
1601
                    ->setUser($owner)
1602
                    ->setOrigin($originComment->getId())
1603
                    ->setOriginType(Portfolio::TYPE_COMMENT)
1604
                    ->setCourse($this->course)
1605
                    ->setSession($this->session)
1606
                    ->setCreationDate($currentTime)
1607
                    ->setUpdateDate($currentTime);
1608
1609
                $this->em->persist($portfolio);
1610
            }
1611
1612
            $this->em->flush();
1613
1614
            Display::addFlash(
1615
                Display::return_message(get_lang('PortfolioItemAddedToStudents'), 'success')
1616
            );
1617
1618
            header("Location: $this->baseUrl");
1619
            exit;
1620
        }
1621
1622
        $this->renderView($content, $toolName);
1623
    }
1624
1625
    /**
1626
     * @throws \Doctrine\ORM\ORMException
1627
     * @throws \Doctrine\ORM\OptimisticLockException
1628
     */
1629
    public function markImportantCommentInItem(Portfolio $item, PortfolioComment $comment)
1630
    {
1631
        if ($comment->getItem()->getId() !== $item->getId()) {
1632
            api_not_allowed(true);
1633
        }
1634
1635
        $comment->setIsImportant(
1636
            !$comment->isImportant()
1637
        );
1638
1639
        $this->em->persist($comment);
1640
        $this->em->flush();
1641
1642
        Display::addFlash(
1643
            Display::return_message(get_lang('CommentMarkedAsImportant'), 'success')
1644
        );
1645
1646
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
1647
        exit;
1648
    }
1649
1650
    /**
1651
     * @throws \Exception
1652
     */
1653
    public function details(HttpRequest $httpRequest)
1654
    {
1655
        $currentUserId = api_get_user_id();
1656
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
1657
1658
        $actions = [];
1659
        $actions[] = Display::url(
1660
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
1661
            $this->baseUrl
1662
        );
1663
        $actions[] = Display::url(
1664
            Display::return_icon('pdf.png', get_lang('ExportMyPortfolioDataPdf'), [], ICON_SIZE_MEDIUM),
1665
            $this->baseUrl.http_build_query(['action' => 'export_pdf'])
1666
        );
1667
        $actions[] = Display::url(
1668
            Display::return_icon('save_pack.png', get_lang('ExportMyPortfolioDataZip'), [], ICON_SIZE_MEDIUM),
1669
            $this->baseUrl.http_build_query(['action' => 'export_zip'])
1670
        );
1671
1672
        $frmStudent = null;
1673
1674
        if ($isAllowedToFilterStudent) {
1675
            if ($httpRequest->query->has('user')) {
1676
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
1677
1678
                if (empty($this->owner)) {
1679
                    api_not_allowed(true);
1680
                }
1681
1682
                $actions[1] = Display::url(
1683
                    Display::return_icon('pdf.png', get_lang('ExportMyPortfolioDataPdf'), [], ICON_SIZE_MEDIUM),
1684
                    $this->baseUrl.http_build_query(['action' => 'export_pdf', 'user' => $this->owner->getId()])
1685
                );
1686
                $actions[2] = Display::url(
1687
                    Display::return_icon('save_pack.png', get_lang('ExportMyPortfolioDataZip'), [], ICON_SIZE_MEDIUM),
1688
                    $this->baseUrl.http_build_query(['action' => 'export_zip', 'user' => $this->owner->getId()])
1689
                );
1690
            }
1691
1692
            $frmStudent = new FormValidator('frm_student_list', 'get');
1693
1694
            $urlParams = http_build_query(
1695
                [
1696
                    'a' => 'search_user_by_course',
1697
                    'course_id' => $this->course->getId(),
1698
                    'session_id' => $this->session ? $this->session->getId() : 0,
1699
                ]
1700
            );
1701
1702
            $frmStudent
1703
                ->addSelectAjax(
1704
                    'user',
1705
                    get_lang('SelectLearnerPortfolio'),
1706
                    [],
1707
                    [
1708
                        'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
1709
                        'placeholder' => get_lang('SearchStudent'),
1710
                        'formatResult' => SelectAjax::templateResultForUsersInCourse(),
1711
                        'formatSelection' => SelectAjax::templateSelectionForUsersInCourse(),
1712
                    ]
1713
                )
1714
                ->addOption(
1715
                    $this->owner->getCompleteName(),
1716
                    $this->owner->getId(),
1717
                    [
1718
                        'data-avatarurl' => UserManager::getUserPicture($this->owner->getId()),
1719
                        'data-username' => $this->owner->getUsername(),
1720
                    ]
1721
                )
1722
            ;
1723
            $frmStudent->setDefaults(['user' => $this->owner->getId()]);
1724
            $frmStudent->addHidden('action', 'details');
1725
            $frmStudent->addHidden('cidReq', $this->course->getCode());
1726
            $frmStudent->addHidden('id_session', $this->session ? $this->session->getId() : 0);
1727
            $frmStudent->addButtonFilter(get_lang('Filter'));
1728
        }
1729
1730
        $itemsRepo = $this->em->getRepository(Portfolio::class);
1731
        $commentsRepo = $this->em->getRepository(PortfolioComment::class);
1732
1733
        $getItemsTotalNumber = function () use ($itemsRepo, $isAllowedToFilterStudent, $currentUserId) {
1734
            $qb = $itemsRepo->createQueryBuilder('i');
1735
            $qb
1736
                ->select('COUNT(i)')
1737
                ->where('i.user = :user')
1738
                ->setParameter('user', $this->owner);
1739
1740
            if ($this->course) {
1741
                $qb
1742
                    ->andWhere('i.course = :course')
1743
                    ->setParameter('course', $this->course);
1744
1745
                if ($this->session) {
1746
                    $qb
1747
                        ->andWhere('i.session = :session')
1748
                        ->setParameter('session', $this->session);
1749
                } else {
1750
                    $qb->andWhere('i.session IS NULL');
1751
                }
1752
            }
1753
1754
            if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
1755
                $visibilityCriteria = [
1756
                    Portfolio::VISIBILITY_VISIBLE,
1757
                    Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER,
1758
                ];
1759
1760
                $qb->andWhere(
1761
                    $qb->expr()->in('i.visibility', $visibilityCriteria)
1762
                );
1763
            }
1764
1765
            return $qb->getQuery()->getSingleScalarResult();
1766
        };
1767
        $getItemsData = function ($from, $limit, $columnNo, $orderDirection) use ($itemsRepo, $isAllowedToFilterStudent, $currentUserId) {
1768
            $qb = $itemsRepo->createQueryBuilder('item')
1769
                ->where('item.user = :user')
1770
                ->leftJoin('item.category', 'category')
1771
                ->leftJoin('item.course', 'course')
1772
                ->leftJoin('item.session', 'session')
1773
                ->setParameter('user', $this->owner);
1774
1775
            if ($this->course) {
1776
                $qb
1777
                    ->andWhere('item.course = :course_id')
1778
                    ->setParameter('course_id', $this->course);
1779
1780
                if ($this->session) {
1781
                    $qb
1782
                        ->andWhere('item.session = :session')
1783
                        ->setParameter('session', $this->session);
1784
                } else {
1785
                    $qb->andWhere('item.session IS NULL');
1786
                }
1787
            }
1788
1789
            if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
1790
                $visibilityCriteria = [
1791
                    Portfolio::VISIBILITY_VISIBLE,
1792
                    Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER,
1793
                ];
1794
1795
                $qb->andWhere(
1796
                    $qb->expr()->in('item.visibility', $visibilityCriteria)
1797
                );
1798
            }
1799
1800
            if (0 == $columnNo) {
1801
                $qb->orderBy('item.title', $orderDirection);
1802
            } elseif (1 == $columnNo) {
1803
                $qb->orderBy('item.creationDate', $orderDirection);
1804
            } elseif (2 == $columnNo) {
1805
                $qb->orderBy('item.updateDate', $orderDirection);
1806
            } elseif (3 == $columnNo) {
1807
                $qb->orderBy('category.title', $orderDirection);
1808
            } elseif (5 == $columnNo) {
1809
                $qb->orderBy('item.score', $orderDirection);
1810
            } elseif (6 == $columnNo) {
1811
                $qb->orderBy('course.title', $orderDirection);
1812
            } elseif (7 == $columnNo) {
1813
                $qb->orderBy('session.name', $orderDirection);
1814
            }
1815
1816
            $qb->setFirstResult($from)->setMaxResults($limit);
1817
1818
            return array_map(
1819
                function (Portfolio $item) {
1820
                    $category = $item->getCategory();
1821
1822
                    $row = [];
1823
                    $row[] = $item;
1824
                    $row[] = $item->getCreationDate();
1825
                    $row[] = $item->getUpdateDate();
1826
                    $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...
1827
                    $row[] = $item->getComments()->count();
1828
                    $row[] = $item->getScore();
1829
1830
                    if (!$this->course) {
1831
                        $row[] = $item->getCourse();
1832
                        $row[] = $item->getSession();
1833
                    }
1834
1835
                    return $row;
1836
                },
1837
                $qb->getQuery()->getResult()
1838
            );
1839
        };
1840
1841
        $portfolioItemColumnFilter = function (Portfolio $item) {
1842
            return Display::url(
1843
                $item->getTitle(true),
1844
                $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
1845
            );
1846
        };
1847
        $convertFormatDateColumnFilter = function (DateTime $date) {
1848
            return api_convert_and_format_date($date);
1849
        };
1850
1851
        $tblItems = new SortableTable('tbl_items', $getItemsTotalNumber, $getItemsData, 1, 20, 'DESC');
1852
        $tblItems->set_additional_parameters(['action' => 'details', 'user' => $this->owner->getId()]);
1853
        $tblItems->set_header(0, get_lang('Title'));
1854
        $tblItems->set_column_filter(0, $portfolioItemColumnFilter);
1855
        $tblItems->set_header(1, get_lang('CreationDate'), true, [], ['class' => 'text-center']);
1856
        $tblItems->set_column_filter(1, $convertFormatDateColumnFilter);
1857
        $tblItems->set_header(2, get_lang('LastUpdate'), true, [], ['class' => 'text-center']);
1858
        $tblItems->set_column_filter(2, $convertFormatDateColumnFilter);
1859
        $tblItems->set_header(3, get_lang('Category'));
1860
        $tblItems->set_header(4, get_lang('Comments'), false, [], ['class' => 'text-right']);
1861
        $tblItems->set_header(5, get_lang('Score'), true, [], ['class' => 'text-right']);
1862
1863
        if (!$this->course) {
1864
            $tblItems->set_header(6, get_lang('Course'));
1865
            $tblItems->set_header(7, get_lang('Session'));
1866
        }
1867
1868
        $getCommentsTotalNumber = function () use ($commentsRepo) {
1869
            $qb = $commentsRepo->createQueryBuilder('c');
1870
            $qb
1871
                ->select('COUNT(c)')
1872
                ->where('c.author = :author')
1873
                ->setParameter('author', $this->owner);
1874
1875
            if ($this->course) {
1876
                $qb
1877
                    ->innerJoin('c.item', 'i')
1878
                    ->andWhere('i.course = :course')
1879
                    ->setParameter('course', $this->course);
1880
1881
                if ($this->session) {
1882
                    $qb
1883
                        ->andWhere('i.session = :session')
1884
                        ->setParameter('session', $this->session);
1885
                } else {
1886
                    $qb->andWhere('i.session IS NULL');
1887
                }
1888
            }
1889
1890
            return $qb->getQuery()->getSingleScalarResult();
1891
        };
1892
        $getCommentsData = function ($from, $limit, $columnNo, $orderDirection) use ($commentsRepo) {
1893
            $qb = $commentsRepo->createQueryBuilder('comment');
1894
            $qb
1895
                ->where('comment.author = :user')
1896
                ->innerJoin('comment.item', 'item')
1897
                ->setParameter('user', $this->owner);
1898
1899
            if ($this->course) {
1900
                $qb
1901
                    ->innerJoin('comment.item', 'i')
1902
                    ->andWhere('item.course = :course')
1903
                    ->setParameter('course', $this->course);
1904
1905
                if ($this->session) {
1906
                    $qb
1907
                        ->andWhere('item.session = :session')
1908
                        ->setParameter('session', $this->session);
1909
                } else {
1910
                    $qb->andWhere('item.session IS NULL');
1911
                }
1912
            }
1913
1914
            if (0 == $columnNo) {
1915
                $qb->orderBy('comment.content', $orderDirection);
1916
            } elseif (1 == $columnNo) {
1917
                $qb->orderBy('comment.date', $orderDirection);
1918
            } elseif (2 == $columnNo) {
1919
                $qb->orderBy('item.title', $orderDirection);
1920
            } elseif (3 == $columnNo) {
1921
                $qb->orderBy('comment.score', $orderDirection);
1922
            }
1923
1924
            $qb->setFirstResult($from)->setMaxResults($limit);
1925
1926
            return array_map(
1927
                function (PortfolioComment $comment) {
1928
                    return [
1929
                        $comment,
1930
                        $comment->getDate(),
1931
                        $comment->getItem(),
1932
                        $comment->getScore(),
1933
                    ];
1934
                },
1935
                $qb->getQuery()->getResult()
1936
            );
1937
        };
1938
1939
        $tblComments = new SortableTable('tbl_comments', $getCommentsTotalNumber, $getCommentsData, 1, 20, 'DESC');
1940
        $tblComments->set_additional_parameters(['action' => 'details', 'user' => $this->owner->getId()]);
1941
        $tblComments->set_header(0, get_lang('Resume'));
1942
        $tblComments->set_column_filter(
1943
            0,
1944
            function (PortfolioComment $comment) {
1945
                return Display::url(
1946
                    $comment->getExcerpt(),
1947
                    $this->baseUrl.http_build_query(['action' => 'view', 'id' => $comment->getItem()->getId()])
1948
                    .'#comment-'.$comment->getId()
1949
                );
1950
            }
1951
        );
1952
        $tblComments->set_header(1, get_lang('Date'), true, [], ['class' => 'text-center']);
1953
        $tblComments->set_column_filter(1, $convertFormatDateColumnFilter);
1954
        $tblComments->set_header(2, get_lang('PortfolioItemTitle'));
1955
        $tblComments->set_column_filter(2, $portfolioItemColumnFilter);
1956
        $tblComments->set_header(3, get_lang('Score'), true, [], ['class' => 'text-right']);
1957
1958
        $content = '';
1959
1960
        if ($frmStudent) {
1961
            $content .= $frmStudent->returnForm();
1962
        }
1963
1964
        $totalNumberOfItems = $tblItems->get_total_number_of_items();
1965
        $totalNumberOfComments = $tblComments->get_total_number_of_items();
1966
        $requiredNumberOfItems = (int) api_get_course_setting('portfolio_number_items');
1967
        $requiredNumberOfComments = (int) api_get_course_setting('portfolio_number_comments');
1968
1969
        $itemsSubtitle = '';
1970
1971
        if ($requiredNumberOfItems > 0) {
1972
            $itemsSubtitle = sprintf(
1973
                get_lang('XAddedYRequired'),
1974
                $totalNumberOfItems,
1975
                $requiredNumberOfItems
1976
            );
1977
        }
1978
1979
        $content .= Display::page_subheader2(
1980
            get_lang('PortfolioItems'),
1981
            $itemsSubtitle
1982
        ).PHP_EOL;
1983
1984
        if ($totalNumberOfItems > 0) {
1985
            $content .= $tblItems->return_table().PHP_EOL;
1986
        } else {
1987
            $content .= Display::return_message(get_lang('NoItemsInYourPortfolio'), 'warning');
1988
        }
1989
1990
        $commentsSubtitle = '';
1991
1992
        if ($requiredNumberOfComments > 0) {
1993
            $commentsSubtitle = sprintf(
1994
                get_lang('XAddedYRequired'),
1995
                $totalNumberOfComments,
1996
                $requiredNumberOfComments
1997
            );
1998
        }
1999
2000
        $content .= Display::page_subheader2(
2001
            get_lang('PortfolioCommentsMade'),
2002
            $commentsSubtitle
2003
        ).PHP_EOL;
2004
2005
        if ($totalNumberOfComments > 0) {
2006
            $content .= $tblComments->return_table().PHP_EOL;
2007
        } else {
2008
            $content .= Display::return_message(get_lang('YouHaveNotCommented'), 'warning');
2009
        }
2010
2011
        $this->renderView($content, get_lang('PortfolioDetails'), $actions);
2012
    }
2013
2014
    /**
2015
     * @throws MpdfException
2016
     */
2017
    public function exportPdf(HttpRequest $httpRequest)
2018
    {
2019
        $currentUserId = api_get_user_id();
2020
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
2021
2022
        if ($isAllowedToFilterStudent) {
2023
            if ($httpRequest->query->has('user')) {
2024
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
2025
2026
                if (empty($this->owner)) {
2027
                    api_not_allowed(true);
2028
                }
2029
            }
2030
        }
2031
2032
        $pdfContent = Display::page_header($this->owner->getCompleteName());
2033
2034
        if ($this->course) {
2035
            $pdfContent .= '<p>'.get_lang('Course').': ';
2036
2037
            if ($this->session) {
2038
                $pdfContent .= $this->session->getName().' ('.$this->course->getTitle().')';
2039
            } else {
2040
                $pdfContent .= $this->course->getTitle();
2041
            }
2042
2043
            $pdfContent .= '</p>';
2044
        }
2045
2046
        $visibility = [];
2047
2048
        if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
2049
            $visibility[] = Portfolio::VISIBILITY_VISIBLE;
2050
            $visibility[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
2051
        }
2052
2053
        $items = $this->em
2054
            ->getRepository(Portfolio::class)
2055
            ->findItemsByUser(
2056
                $this->owner,
2057
                $this->course,
2058
                $this->session,
2059
                null,
2060
                $visibility
2061
            );
2062
        $comments = $this->em
2063
            ->getRepository(PortfolioComment::class)
2064
            ->findCommentsByUser($this->owner, $this->course, $this->session);
2065
2066
        $itemsHtml = $this->getItemsInHtmlFormatted($items);
2067
        $commentsHtml = $this->getCommentsInHtmlFormatted($comments);
2068
2069
        $totalNumberOfItems = count($itemsHtml);
2070
        $totalNumberOfComments = count($commentsHtml);
2071
        $requiredNumberOfItems = (int) api_get_course_setting('portfolio_number_items');
2072
        $requiredNumberOfComments = (int) api_get_course_setting('portfolio_number_comments');
2073
2074
        $itemsSubtitle = '';
2075
        $commentsSubtitle = '';
2076
2077
        if ($requiredNumberOfItems > 0) {
2078
            $itemsSubtitle = sprintf(
2079
                get_lang('XAddedYRequired'),
2080
                $totalNumberOfItems,
2081
                $requiredNumberOfItems
2082
            );
2083
        }
2084
2085
        if ($requiredNumberOfComments > 0) {
2086
            $commentsSubtitle = sprintf(
2087
                get_lang('XAddedYRequired'),
2088
                $totalNumberOfComments,
2089
                $requiredNumberOfComments
2090
            );
2091
        }
2092
2093
        $pdfContent .= Display::page_subheader2(
2094
            get_lang('PortfolioItems'),
2095
            $itemsSubtitle
2096
        );
2097
2098
        if ($totalNumberOfItems > 0) {
2099
            $pdfContent .= implode(PHP_EOL, $itemsHtml);
2100
        } else {
2101
            $pdfContent .= Display::return_message(get_lang('NoItemsInYourPortfolio'), 'warning');
2102
        }
2103
2104
        $pdfContent .= Display::page_subheader2(
2105
            get_lang('PortfolioCommentsMade'),
2106
            $commentsSubtitle
2107
        );
2108
2109
        if ($totalNumberOfComments > 0) {
2110
            $pdfContent .= implode(PHP_EOL, $commentsHtml);
2111
        } else {
2112
            $pdfContent .= Display::return_message(get_lang('YouHaveNotCommented'), 'warning');
2113
        }
2114
2115
        $pdfName = $this->owner->getCompleteName()
2116
            .($this->course ? '_'.$this->course->getCode() : '')
2117
            .'_'.get_lang('Portfolio');
2118
2119
        $pdf = new PDF();
2120
        $pdf->content_to_pdf(
2121
            $pdfContent,
2122
            null,
2123
            $pdfName,
2124
            $this->course ? $this->course->getCode() : null,
2125
            'D',
2126
            false,
2127
            null,
2128
            false,
2129
            true
2130
        );
2131
    }
2132
2133
    public function exportZip(HttpRequest $httpRequest)
2134
    {
2135
        $currentUserId = api_get_user_id();
2136
        $isAllowedToFilterStudent = $this->course && api_is_allowed_to_edit();
2137
2138
        if ($isAllowedToFilterStudent) {
2139
            if ($httpRequest->query->has('user')) {
2140
                $this->owner = api_get_user_entity($httpRequest->query->getInt('user'));
2141
2142
                if (empty($this->owner)) {
2143
                    api_not_allowed(true);
2144
                }
2145
            }
2146
        }
2147
2148
        $itemsRepo = $this->em->getRepository(Portfolio::class);
2149
        $commentsRepo = $this->em->getRepository(PortfolioComment::class);
2150
        $attachmentsRepo = $this->em->getRepository(PortfolioAttachment::class);
2151
2152
        $visibility = [];
2153
2154
        if ($isAllowedToFilterStudent && $currentUserId !== $this->owner->getId()) {
2155
            $visibility[] = Portfolio::VISIBILITY_VISIBLE;
2156
            $visibility[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
2157
        }
2158
2159
        $items = $itemsRepo->findItemsByUser(
2160
            $this->owner,
2161
            $this->course,
2162
            $this->session,
2163
            null,
2164
            $visibility
2165
        );
2166
        $comments = $commentsRepo->findCommentsByUser($this->owner, $this->course, $this->session);
2167
2168
        $itemsHtml = $this->getItemsInHtmlFormatted($items);
2169
        $commentsHtml = $this->getCommentsInHtmlFormatted($comments);
2170
2171
        $sysArchivePath = api_get_path(SYS_ARCHIVE_PATH);
2172
        $tempPortfolioDirectory = $sysArchivePath."portfolio/{$this->owner->getId()}";
2173
2174
        $userDirectory = UserManager::getUserPathById($this->owner->getId(), 'system');
2175
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2176
2177
        $tblItemsHeaders = [];
2178
        $tblItemsHeaders[] = get_lang('Title');
2179
        $tblItemsHeaders[] = get_lang('CreationDate');
2180
        $tblItemsHeaders[] = get_lang('LastUpdate');
2181
        $tblItemsHeaders[] = get_lang('Category');
2182
        $tblItemsHeaders[] = get_lang('Category');
2183
        $tblItemsHeaders[] = get_lang('Score');
2184
        $tblItemsHeaders[] = get_lang('Course');
2185
        $tblItemsHeaders[] = get_lang('Session');
2186
        $tblItemsData = [];
2187
2188
        $tblCommentsHeaders = [];
2189
        $tblCommentsHeaders[] = get_lang('Resume');
2190
        $tblCommentsHeaders[] = get_lang('Date');
2191
        $tblCommentsHeaders[] = get_lang('PortfolioItemTitle');
2192
        $tblCommentsHeaders[] = get_lang('Score');
2193
        $tblCommentsData = [];
2194
2195
        $filenames = [];
2196
2197
        $fs = new Filesystem();
2198
2199
        /**
2200
         * @var int       $i
2201
         * @var Portfolio $item
2202
         */
2203
        foreach ($items as $i => $item) {
2204
            $itemCategory = $item->getCategory();
2205
            $itemCourse = $item->getCourse();
2206
            $itemSession = $item->getSession();
2207
2208
            $itemDirectory = $item->getCreationDate()->format('Y-m-d-H-i-s');
2209
2210
            $itemFilename = sprintf('%s/items/%s/item.html', $tempPortfolioDirectory, $itemDirectory);
2211
            $itemFileContent = $this->fixImagesSourcesToHtml($itemsHtml[$i]);
2212
2213
            $fs->dumpFile($itemFilename, $itemFileContent);
2214
2215
            $filenames[] = $itemFilename;
2216
2217
            $attachments = $attachmentsRepo->findFromItem($item);
2218
2219
            /** @var PortfolioAttachment $attachment */
2220
            foreach ($attachments as $attachment) {
2221
                $attachmentFilename = sprintf(
2222
                    '%s/items/%s/attachments/%s',
2223
                    $tempPortfolioDirectory,
2224
                    $itemDirectory,
2225
                    $attachment->getFilename()
2226
                );
2227
2228
                $fs->copy(
2229
                    $attachmentsDirectory.$attachment->getPath(),
2230
                    $attachmentFilename
2231
                );
2232
2233
                $filenames[] = $attachmentFilename;
2234
            }
2235
2236
            $tblItemsData[] = [
2237
                Display::url(
2238
                    Security::remove_XSS($item->getTitle()),
2239
                    sprintf('items/%s/item.html', $itemDirectory)
2240
                ),
2241
                api_convert_and_format_date($item->getCreationDate()),
2242
                api_convert_and_format_date($item->getUpdateDate()),
2243
                $itemCategory ? $itemCategory->getTitle() : null,
2244
                $item->getComments()->count(),
2245
                $item->getScore(),
2246
                $itemCourse->getTitle(),
2247
                $itemSession ? $itemSession->getName() : null,
2248
            ];
2249
        }
2250
2251
        /**
2252
         * @var int              $i
2253
         * @var PortfolioComment $comment
2254
         */
2255
        foreach ($comments as $i => $comment) {
2256
            $commentDirectory = $comment->getDate()->format('Y-m-d-H-i-s');
2257
2258
            $commentFileContent = $this->fixImagesSourcesToHtml($commentsHtml[$i]);
2259
            $commentFilename = sprintf('%s/comments/%s/comment.html', $tempPortfolioDirectory, $commentDirectory);
2260
2261
            $fs->dumpFile($commentFilename, $commentFileContent);
2262
2263
            $filenames[] = $commentFilename;
2264
2265
            $attachments = $attachmentsRepo->findFromComment($comment);
2266
2267
            /** @var PortfolioAttachment $attachment */
2268
            foreach ($attachments as $attachment) {
2269
                $attachmentFilename = sprintf(
2270
                    '%s/comments/%s/attachments/%s',
2271
                    $tempPortfolioDirectory,
2272
                    $commentDirectory,
2273
                    $attachment->getFilename()
2274
                );
2275
2276
                $fs->copy(
2277
                    $attachmentsDirectory.$attachment->getPath(),
2278
                    $attachmentFilename
2279
                );
2280
2281
                $filenames[] = $attachmentFilename;
2282
            }
2283
2284
            $tblCommentsData[] = [
2285
                Display::url(
2286
                    $comment->getExcerpt(),
2287
                    sprintf('comments/%s/comment.html', $commentDirectory)
2288
                ),
2289
                api_convert_and_format_date($comment->getDate()),
2290
                Security::remove_XSS($comment->getItem()->getTitle()),
2291
                $comment->getScore(),
2292
            ];
2293
        }
2294
2295
        $tblItems = new HTML_Table(['class' => 'table table-hover table-striped table-bordered data_table']);
2296
        $tblItems->setHeaders($tblItemsHeaders);
2297
        $tblItems->setData($tblItemsData);
2298
2299
        $tblComments = new HTML_Table(['class' => 'table table-hover table-striped table-bordered data_table']);
2300
        $tblComments->setHeaders($tblCommentsHeaders);
2301
        $tblComments->setData($tblCommentsData);
2302
2303
        $itemFilename = sprintf('%s/index.html', $tempPortfolioDirectory);
2304
2305
        $filenames[] = $itemFilename;
2306
2307
        $fs->dumpFile(
2308
            $itemFilename,
2309
            $this->formatZipIndexFile($tblItems, $tblComments)
2310
        );
2311
2312
        $zipName = $this->owner->getCompleteName()
2313
            .($this->course ? '_'.$this->course->getCode() : '')
2314
            .'_'.get_lang('Portfolio');
2315
        $tempZipFile = $sysArchivePath."portfolio/$zipName.zip";
2316
        $zip = new PclZip($tempZipFile);
2317
2318
        foreach ($filenames as $filename) {
2319
            $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...
2320
        }
2321
2322
        DocumentManager::file_send_for_download($tempZipFile, true, "$zipName.zip");
2323
2324
        $fs->remove($tempPortfolioDirectory);
2325
        $fs->remove($tempZipFile);
2326
    }
2327
2328
    public function qualifyItem(Portfolio $item)
2329
    {
2330
        global $interbreadcrumb;
2331
2332
        $em = Database::getManager();
2333
2334
        $formAction = $this->baseUrl.http_build_query(['action' => 'qualify', 'item' => $item->getId()]);
2335
2336
        $form = new FormValidator('frm_qualify', 'post', $formAction);
2337
        $form->addUserAvatar('user', get_lang('Author'));
2338
        $form->addLabel(get_lang('Title'), $item->getTitle());
2339
2340
        $itemContent = $this->generateItemContent($item);
2341
2342
        $form->addLabel(get_lang('Content'), $itemContent);
2343
        $form->addNumeric(
2344
            'score',
2345
            [get_lang('QualifyNumeric'), null, ' / '.api_get_course_setting('portfolio_max_score')]
2346
        );
2347
        $form->addButtonSave(get_lang('QualifyThisPortfolioItem'));
2348
2349
        if ($form->validate()) {
2350
            $values = $form->exportValues();
2351
2352
            $item->setScore($values['score']);
2353
2354
            $em->persist($item);
2355
            $em->flush();
2356
2357
            Display::addFlash(
2358
                Display::return_message(get_lang('PortfolioItemGraded'), 'success')
2359
            );
2360
2361
            header("Location: $formAction");
2362
            exit();
2363
        }
2364
2365
        $form->setDefaults(
2366
            [
2367
                'user' => $item->getUser(),
2368
                'score' => (float) $item->getScore(),
2369
            ]
2370
        );
2371
2372
        $interbreadcrumb[] = [
2373
            'name' => get_lang('Portfolio'),
2374
            'url' => $this->baseUrl,
2375
        ];
2376
        $interbreadcrumb[] = [
2377
            'name' => $item->getTitle(true),
2378
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
2379
        ];
2380
2381
        $actions = [];
2382
        $actions[] = Display::url(
2383
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
2384
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
2385
        );
2386
2387
        $this->renderView($form->returnForm(), get_lang('Qualify'), $actions);
2388
    }
2389
2390
    public function qualifyComment(PortfolioComment $comment)
2391
    {
2392
        global $interbreadcrumb;
2393
2394
        $em = Database::getManager();
2395
2396
        $item = $comment->getItem();
2397
        $commentPath = $em->getRepository(PortfolioComment::class)->getPath($comment);
2398
2399
        $template = new Template('', false, false, false, true, false, false);
2400
        $template->assign('item', $item);
2401
        $template->assign('comments_path', $commentPath);
2402
        $commentContext = $template->fetch(
2403
            $template->get_template('portfolio/comment_context.html.twig')
2404
        );
2405
2406
        $formAction = $this->baseUrl.http_build_query(['action' => 'qualify', 'comment' => $comment->getId()]);
2407
2408
        $form = new FormValidator('frm_qualify', 'post', $formAction);
2409
        $form->addHtml($commentContext);
2410
        $form->addUserAvatar('user', get_lang('Author'));
2411
        $form->addLabel(get_lang('Comment'), $comment->getContent());
2412
        $form->addNumeric(
2413
            'score',
2414
            [get_lang('QualifyNumeric'), null, '/ '.api_get_course_setting('portfolio_max_score')]
2415
        );
2416
        $form->addButtonSave(get_lang('QualifyThisPortfolioComment'));
2417
2418
        if ($form->validate()) {
2419
            $values = $form->exportValues();
2420
2421
            $comment->setScore($values['score']);
2422
2423
            $em->persist($comment);
2424
            $em->flush();
2425
2426
            Display::addFlash(
2427
                Display::return_message(get_lang('PortfolioCommentGraded'), 'success')
2428
            );
2429
2430
            header("Location: $formAction");
2431
            exit();
2432
        }
2433
2434
        $form->setDefaults(
2435
            [
2436
                'user' => $comment->getAuthor(),
2437
                'score' => (float) $comment->getScore(),
2438
            ]
2439
        );
2440
2441
        $interbreadcrumb[] = [
2442
            'name' => get_lang('Portfolio'),
2443
            'url' => $this->baseUrl,
2444
        ];
2445
        $interbreadcrumb[] = [
2446
            'name' => $item->getTitle(true),
2447
            'url' => $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]),
2448
        ];
2449
2450
        $actions = [];
2451
        $actions[] = Display::url(
2452
            Display::return_icon('back.png', get_lang('Back'), [], ICON_SIZE_MEDIUM),
2453
            $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()])
2454
        );
2455
2456
        $this->renderView($form->returnForm(), get_lang('Qualify'), $actions);
2457
    }
2458
2459
    public function downloadAttachment(HttpRequest $httpRequest)
2460
    {
2461
        $path = $httpRequest->query->get('file');
2462
2463
        if (empty($path)) {
2464
            api_not_allowed(true);
2465
        }
2466
2467
        $em = Database::getManager();
2468
        $attachmentRepo = $em->getRepository(PortfolioAttachment::class);
2469
2470
        $attachment = $attachmentRepo->findOneByPath($path);
2471
2472
        if (empty($attachment)) {
2473
            api_not_allowed(true);
2474
        }
2475
2476
        $originOwnerId = 0;
2477
2478
        if (PortfolioAttachment::TYPE_ITEM === $attachment->getOriginType()) {
2479
            $item = $em->find(Portfolio::class, $attachment->getOrigin());
2480
2481
            $originOwnerId = $item->getUser()->getId();
2482
        } elseif (PortfolioAttachment::TYPE_COMMENT === $attachment->getOriginType()) {
2483
            $comment = $em->find(PortfolioComment::class, $attachment->getOrigin());
2484
2485
            $originOwnerId = $comment->getAuthor()->getId();
2486
        } else {
2487
            api_not_allowed(true);
2488
        }
2489
2490
        $userDirectory = UserManager::getUserPathById($originOwnerId, 'system');
2491
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2492
        $attachmentFilename = $attachmentsDirectory.$attachment->getPath();
2493
2494
        if (!Security::check_abs_path($attachmentFilename, $attachmentsDirectory)) {
2495
            api_not_allowed(true);
2496
        }
2497
2498
        $downloaded = DocumentManager::file_send_for_download(
2499
            $attachmentFilename,
2500
            true,
2501
            $attachment->getFilename()
2502
        );
2503
2504
        if (!$downloaded) {
2505
            api_not_allowed(true);
2506
        }
2507
    }
2508
2509
    public function deleteAttachment(HttpRequest $httpRequest)
2510
    {
2511
        $currentUserId = api_get_user_id();
2512
2513
        $path = $httpRequest->query->get('file');
2514
2515
        if (empty($path)) {
2516
            api_not_allowed(true);
2517
        }
2518
2519
        $em = Database::getManager();
2520
        $fs = new Filesystem();
2521
2522
        $attachmentRepo = $em->getRepository(PortfolioAttachment::class);
2523
        $attachment = $attachmentRepo->findOneByPath($path);
2524
2525
        if (empty($attachment)) {
2526
            api_not_allowed(true);
2527
        }
2528
2529
        $originOwnerId = 0;
2530
        $itemId = 0;
2531
2532
        if (PortfolioAttachment::TYPE_ITEM === $attachment->getOriginType()) {
2533
            $item = $em->find(Portfolio::class, $attachment->getOrigin());
2534
            $originOwnerId = $item->getUser()->getId();
2535
            $itemId = $item->getId();
2536
        } elseif (PortfolioAttachment::TYPE_COMMENT === $attachment->getOriginType()) {
2537
            $comment = $em->find(PortfolioComment::class, $attachment->getOrigin());
2538
            $originOwnerId = $comment->getAuthor()->getId();
2539
            $itemId = $comment->getItem()->getId();
2540
        }
2541
2542
        if ($currentUserId !== $originOwnerId) {
2543
            api_not_allowed(true);
2544
        }
2545
2546
        $em->remove($attachment);
2547
        $em->flush();
2548
2549
        $userDirectory = UserManager::getUserPathById($originOwnerId, 'system');
2550
        $attachmentsDirectory = $userDirectory.'portfolio_attachments/';
2551
        $attachmentFilename = $attachmentsDirectory.$attachment->getPath();
2552
2553
        $fs->remove($attachmentFilename);
2554
2555
        if ($httpRequest->isXmlHttpRequest()) {
2556
            echo Display::return_message(get_lang('AttachmentFileDeleteSuccess'), 'success');
2557
        } else {
2558
            Display::addFlash(
2559
                Display::return_message(get_lang('AttachmentFileDeleteSuccess'), 'success')
2560
            );
2561
2562
            header('Location: '.$this->baseUrl.http_build_query(['action' => 'view', 'id' => $itemId]));
2563
        }
2564
2565
        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...
2566
    }
2567
2568
    /**
2569
     * @throws \Doctrine\ORM\OptimisticLockException
2570
     * @throws \Doctrine\ORM\ORMException
2571
     */
2572
    public function markAsHighlighted(Portfolio $item)
2573
    {
2574
        if ($item->getCourse()->getId() !== (int) api_get_course_int_id()) {
2575
            api_not_allowed(true);
2576
        }
2577
2578
        $item->setIsHighlighted(
2579
            !$item->isHighlighted()
2580
        );
2581
2582
        Database::getManager()->flush();
2583
2584
        Display::addFlash(
2585
            Display::return_message(
2586
                $item->isHighlighted() ? get_lang('MarkedAsHighlighted') : get_lang('UnmarkedAsHighlighted'),
2587
                'success'
2588
            )
2589
        );
2590
2591
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
2592
        exit;
2593
    }
2594
2595
    public function markAsTemplate(Portfolio $item)
2596
    {
2597
        if (!$this->itemBelongToOwner($item)) {
2598
            api_not_allowed(true);
2599
        }
2600
2601
        $item->setIsTemplate(
2602
            !$item->isTemplate()
2603
        );
2604
2605
        Database::getManager()->flush($item);
2606
2607
        Display::addFlash(
2608
            Display::return_message(
2609
                $item->isTemplate() ? get_lang('PortfolioItemSetAsTemplate') : get_lang('PortfolioItemUnsetAsTemplate'),
2610
                'success'
2611
            )
2612
        );
2613
2614
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $item->getId()]));
2615
        exit;
2616
    }
2617
2618
    public function markAsTemplateComment(PortfolioComment $comment)
2619
    {
2620
        if (!$this->commentBelongsToOwner($comment)) {
2621
            api_not_allowed(true);
2622
        }
2623
2624
        $comment->setIsTemplate(
2625
            !$comment->isTemplate()
2626
        );
2627
2628
        Database::getManager()->flush();
2629
2630
        Display::addFlash(
2631
            Display::return_message(
2632
                $comment->isTemplate() ? get_lang('PortfolioCommentSetAsTemplate') : get_lang('PortfolioCommentUnsetAsTemplate'),
2633
                'success'
2634
            )
2635
        );
2636
2637
        header("Location: $this->baseUrl".http_build_query(['action' => 'view', 'id' => $comment->getItem()->getId()]));
2638
        exit;
2639
    }
2640
2641
    /**
2642
     * @param bool $showHeader
2643
     */
2644
    private function renderView(string $content, string $toolName, array $actions = [], $showHeader = true)
2645
    {
2646
        global $this_section;
2647
2648
        $this_section = $this->course ? SECTION_COURSES : SECTION_SOCIAL;
2649
2650
        $view = new Template($toolName);
2651
2652
        if ($showHeader) {
2653
            $view->assign('header', $toolName);
2654
        }
2655
2656
        $actionsStr = '';
2657
2658
        if ($this->course) {
2659
            $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...
2660
        }
2661
2662
        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...
2663
            $actions = implode('', $actions);
2664
2665
            $actionsStr .= Display::toolbarAction('portfolio-toolbar', [$actions]);
2666
        }
2667
2668
        $view->assign('baseurl', $this->baseUrl);
2669
        $view->assign('actions', $actionsStr);
2670
2671
        $view->assign('content', $content);
2672
        $view->display_one_col_template();
2673
    }
2674
2675
    private function categoryBelongToOwner(PortfolioCategory $category): bool
2676
    {
2677
        if ($category->getUser()->getId() != $this->owner->getId()) {
2678
            return false;
2679
        }
2680
2681
        return true;
2682
    }
2683
2684
    private function addAttachmentsFieldToForm(FormValidator $form)
2685
    {
2686
        $form->addButton('add_attachment', get_lang('AddAttachment'), 'plus');
2687
        $form->addHtml('<div id="container-attachments" style="display: none;">');
2688
        $form->addFile('attachment_file[]', get_lang('FilesAttachment'));
2689
        $form->addText('attachment_comment[]', get_lang('Description'), false);
2690
        $form->addHtml('</div>');
2691
2692
        $script = "$(function () {
2693
            var attachmentsTemplate = $('#container-attachments').html();
2694
            var \$btnAdd = $('[name=\"add_attachment\"]');
2695
            var \$reference = \$btnAdd.parents('.form-group');
2696
2697
            \$btnAdd.on('click', function (e) {
2698
                e.preventDefault();
2699
2700
                $(attachmentsTemplate).insertBefore(\$reference);
2701
            });
2702
        })";
2703
2704
        $form->addHtml("<script>$script</script>");
2705
    }
2706
2707
    private function processAttachments(
2708
        FormValidator $form,
2709
        User $user,
2710
        int $originId,
2711
        int $originType
2712
    ) {
2713
        $em = Database::getManager();
2714
        $fs = new Filesystem();
2715
2716
        $comments = $form->getSubmitValue('attachment_comment');
2717
2718
        foreach ($_FILES['attachment_file']['error'] as $i => $attachmentFileError) {
2719
            if ($attachmentFileError != UPLOAD_ERR_OK) {
2720
                continue;
2721
            }
2722
2723
            $_file = [
2724
                'name' => $_FILES['attachment_file']['name'][$i],
2725
                'type' => $_FILES['attachment_file']['type'][$i],
2726
                'tmp_name' => $_FILES['attachment_file']['tmp_name'][$i],
2727
                'size' => $_FILES['attachment_file']['size'][$i],
2728
            ];
2729
2730
            if (empty($_file['type'])) {
2731
                $_file['type'] = DocumentManager::file_get_mime_type($_file['name']);
2732
            }
2733
2734
            $newFileName = add_ext_on_mime(stripslashes($_file['name']), $_file['type']);
2735
2736
            if (!filter_extension($newFileName)) {
2737
                Display::addFlash(Display::return_message(get_lang('UplUnableToSaveFileFilteredExtension'), 'error'));
2738
                continue;
2739
            }
2740
2741
            $newFileName = uniqid();
2742
            $attachmentsDirectory = UserManager::getUserPathById($user->getId(), 'system').'portfolio_attachments/';
2743
2744
            if (!$fs->exists($attachmentsDirectory)) {
2745
                $fs->mkdir($attachmentsDirectory, api_get_permissions_for_new_directories());
2746
            }
2747
2748
            $attachmentFilename = $attachmentsDirectory.$newFileName;
2749
2750
            if (is_uploaded_file($_file['tmp_name'])) {
2751
                $moved = move_uploaded_file($_file['tmp_name'], $attachmentFilename);
2752
2753
                if (!$moved) {
2754
                    Display::addFlash(Display::return_message(get_lang('UplUnableToSaveFile'), 'error'));
2755
                    continue;
2756
                }
2757
            }
2758
2759
            $attachment = new PortfolioAttachment();
2760
            $attachment
2761
                ->setFilename($_file['name'])
2762
                ->setComment($comments[$i])
2763
                ->setPath($newFileName)
2764
                ->setOrigin($originId)
2765
                ->setOriginType($originType)
2766
                ->setSize($_file['size']);
2767
2768
            $em->persist($attachment);
2769
            $em->flush();
2770
        }
2771
    }
2772
2773
    private function itemBelongToOwner(Portfolio $item): bool
2774
    {
2775
        if ($item->getUser()->getId() != $this->owner->getId()) {
2776
            return false;
2777
        }
2778
2779
        return true;
2780
    }
2781
2782
    private function commentBelongsToOwner(PortfolioComment $comment): bool
2783
    {
2784
        return $comment->getAuthor() === $this->owner;
2785
    }
2786
2787
    private function createFormTagFilter(bool $listByUser = false): FormValidator
2788
    {
2789
        $extraField = new ExtraField('portfolio');
2790
        $tagFieldInfo = $extraField->get_handler_field_info_by_tags('tags');
2791
2792
        $selectTagOptions = array_map(
2793
            function (array $tagOption) {
2794
                return $tagOption['tag'];
2795
            },
2796
            $tagFieldInfo['options'] ?? []
2797
        );
2798
2799
        $frmTagList = new FormValidator(
2800
            'frm_tag_list',
2801
            'get',
2802
            $this->baseUrl.($listByUser ? 'user='.$this->owner->getId() : ''),
2803
            '',
2804
            [],
2805
            FormValidator::LAYOUT_BOX
2806
        );
2807
2808
        $frmTagList->addDatePicker('date', get_lang('CreationDate'));
2809
2810
        $frmTagList->addSelect(
2811
            'tags',
2812
            get_lang('Tags'),
2813
            $selectTagOptions,
2814
            ['multiple' => 'multiple']
2815
        );
2816
2817
        $frmTagList->addText('text', get_lang('Search'), false)->setIcon('search');
2818
        $frmTagList->applyFilter('text', 'trim');
2819
        $frmTagList->addHtml('<br>');
2820
        $frmTagList->addButtonFilter(get_lang('Filter'));
2821
2822
        if ($this->course) {
2823
            $frmTagList->addHidden('cidReq', $this->course->getCode());
2824
            $frmTagList->addHidden('id_session', $this->session ? $this->session->getId() : 0);
2825
            $frmTagList->addHidden('gidReq', 0);
2826
            $frmTagList->addHidden('gradebook', 0);
2827
            $frmTagList->addHidden('origin', '');
2828
            $frmTagList->addHidden('categoryId', 0);
2829
            $frmTagList->addHidden('subCategoryIds', '');
2830
2831
            if ($listByUser) {
2832
                $frmTagList->addHidden('user', $this->owner->getId());
2833
            }
2834
        }
2835
2836
        return $frmTagList;
2837
    }
2838
2839
    /**
2840
     * @throws Exception
2841
     */
2842
    private function createFormStudentFilter(bool $listByUser = false, bool $listHighlighted = false): FormValidator
2843
    {
2844
        $frmStudentList = new FormValidator(
2845
            'frm_student_list',
2846
            'get',
2847
            $this->baseUrl,
2848
            '',
2849
            [],
2850
            FormValidator::LAYOUT_BOX
2851
        );
2852
2853
        $urlParams = http_build_query(
2854
            [
2855
                'a' => 'search_user_by_course',
2856
                'course_id' => $this->course->getId(),
2857
                'session_id' => $this->session ? $this->session->getId() : 0,
2858
            ]
2859
        );
2860
2861
        /** @var SelectAjax $slctUser */
2862
        $slctUser = $frmStudentList->addSelectAjax(
2863
            'user',
2864
            get_lang('SelectLearnerPortfolio'),
2865
            [],
2866
            [
2867
                'url' => api_get_path(WEB_AJAX_PATH)."course.ajax.php?$urlParams",
2868
                'placeholder' => get_lang('SearchStudent'),
2869
                'formatResult' => SelectAjax::templateResultForUsersInCourse(),
2870
                'formatSelection' => SelectAjax::templateSelectionForUsersInCourse(),
2871
            ]
2872
        );
2873
2874
        if ($listByUser) {
2875
            $slctUser->addOption(
2876
                $this->owner->getCompleteName(),
2877
                $this->owner->getId(),
2878
                [
2879
                    'data-avatarurl' => UserManager::getUserPicture($this->owner->getId()),
2880
                    'data-username' => $this->owner->getUsername(),
2881
                ]
2882
            );
2883
2884
            $link = Display::url(
2885
                get_lang('BackToMainPortfolio'),
2886
                $this->baseUrl
2887
            );
2888
        } else {
2889
            $link = Display::url(
2890
                get_lang('SeeMyPortfolio'),
2891
                $this->baseUrl.http_build_query(['user' => api_get_user_id()])
2892
            );
2893
        }
2894
2895
        $frmStudentList->addHtml("<p>$link</p>");
2896
2897
        if ($listHighlighted) {
2898
            $link = Display::url(
2899
                get_lang('BackToMainPortfolio'),
2900
                $this->baseUrl
2901
            );
2902
        } else {
2903
            $link = Display::url(
2904
                get_lang('SeeHighlights'),
2905
                $this->baseUrl.http_build_query(['list_highlighted' => true])
2906
            );
2907
        }
2908
2909
        $frmStudentList->addHtml("<p>$link</p>");
2910
2911
        return $frmStudentList;
2912
    }
2913
2914
    private function getCategoriesForIndex(?int $currentUserId = null, ?int $parentId = null): array
2915
    {
2916
        $categoriesCriteria = [];
2917
        if (isset($currentUserId)) {
2918
            $categoriesCriteria['user'] = $this->owner;
2919
        }
2920
        if (!api_is_platform_admin() && $currentUserId !== $this->owner->getId()) {
2921
            $categoriesCriteria['isVisible'] = true;
2922
        }
2923
        if (isset($parentId)) {
2924
            $categoriesCriteria['parentId'] = $parentId;
2925
        }
2926
2927
        return $this->em
2928
            ->getRepository(PortfolioCategory::class)
2929
            ->findBy($categoriesCriteria);
2930
    }
2931
2932
    private function getHighlightedItems()
2933
    {
2934
        $queryBuilder = $this->em->createQueryBuilder();
2935
        $queryBuilder
2936
            ->select('pi')
2937
            ->from(Portfolio::class, 'pi')
2938
            ->where('pi.course = :course')
2939
            ->andWhere('pi.isHighlighted = TRUE')
2940
            ->setParameter('course', $this->course);
2941
2942
        if ($this->session) {
2943
            $queryBuilder->andWhere('pi.session = :session');
2944
            $queryBuilder->setParameter('session', $this->session);
2945
        } else {
2946
            $queryBuilder->andWhere('pi.session IS NULL');
2947
        }
2948
2949
        $visibilityCriteria = [Portfolio::VISIBILITY_VISIBLE];
2950
2951
        if (api_is_allowed_to_edit()) {
2952
            $visibilityCriteria[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
2953
        }
2954
2955
        $queryBuilder
2956
            ->andWhere(
2957
                $queryBuilder->expr()->orX(
2958
                    'pi.user = :current_user',
2959
                    $queryBuilder->expr()->andX(
2960
                        'pi.user != :current_user',
2961
                        $queryBuilder->expr()->in('pi.visibility', $visibilityCriteria)
2962
                    )
2963
                )
2964
            )
2965
            ->setParameter('current_user', api_get_user_id());
2966
2967
        $queryBuilder->orderBy('pi.creationDate', 'DESC');
2968
2969
        return $queryBuilder->getQuery()->getResult();
2970
    }
2971
2972
    private function getItemsForIndex(
2973
        bool $listByUser = false,
2974
        FormValidator $frmFilterList = null
2975
    ) {
2976
        $currentUserId = api_get_user_id();
2977
2978
        if ($this->course) {
2979
            $queryBuilder = $this->em->createQueryBuilder();
2980
            $queryBuilder
2981
                ->select('pi')
2982
                ->from(Portfolio::class, 'pi')
2983
                ->where('pi.course = :course');
2984
2985
            $queryBuilder->setParameter('course', $this->course);
2986
2987
            if ($this->session) {
2988
                $queryBuilder->andWhere('pi.session = :session');
2989
                $queryBuilder->setParameter('session', $this->session);
2990
            } else {
2991
                $queryBuilder->andWhere('pi.session IS NULL');
2992
            }
2993
2994
            if ($frmFilterList && $frmFilterList->validate()) {
2995
                $values = $frmFilterList->exportValues();
2996
2997
                if (!empty($values['date'])) {
2998
                    $queryBuilder
2999
                        ->andWhere('pi.creationDate >= :date')
3000
                        ->setParameter(':date', api_get_utc_datetime($values['date'], false, true))
3001
                    ;
3002
                }
3003
3004
                if (!empty($values['tags'])) {
3005
                    $queryBuilder
3006
                        ->innerJoin(ExtraFieldRelTag::class, 'efrt', Join::WITH, 'efrt.itemId = pi.id')
3007
                        ->innerJoin(ExtraFieldEntity::class, 'ef', Join::WITH, 'ef.id = efrt.fieldId')
3008
                        ->andWhere('ef.extraFieldType = :efType')
3009
                        ->andWhere('ef.variable = :variable')
3010
                        ->andWhere('efrt.tagId IN (:tags)');
3011
3012
                    $queryBuilder->setParameter('efType', ExtraFieldEntity::PORTFOLIO_TYPE);
3013
                    $queryBuilder->setParameter('variable', 'tags');
3014
                    $queryBuilder->setParameter('tags', $values['tags']);
3015
                }
3016
3017
                if (!empty($values['text'])) {
3018
                    $queryBuilder->andWhere(
3019
                        $queryBuilder->expr()->orX(
3020
                            $queryBuilder->expr()->like('pi.title', ':text'),
3021
                            $queryBuilder->expr()->like('pi.content', ':text')
3022
                        )
3023
                    );
3024
3025
                    $queryBuilder->setParameter('text', '%'.$values['text'].'%');
3026
                }
3027
3028
                // Filters by category level 0
3029
                $searchCategories = [];
3030
                if (!empty($values['categoryId'])) {
3031
                    $searchCategories[] = $values['categoryId'];
3032
                    $subCategories = $this->getCategoriesForIndex(null, $values['categoryId']);
3033
                    if (count($subCategories) > 0) {
3034
                        foreach ($subCategories as $subCategory) {
3035
                            $searchCategories[] = $subCategory->getId();
3036
                        }
3037
                    }
3038
                    $queryBuilder->andWhere('pi.category IN('.implode(',', $searchCategories).')');
3039
                }
3040
3041
                // Filters by sub-category, don't show the selected values
3042
                $diff = [];
3043
                if (!empty($values['subCategoryIds']) && !('all' === $values['subCategoryIds'])) {
3044
                    $subCategoryIds = explode(',', $values['subCategoryIds']);
3045
                    $diff = array_diff($searchCategories, $subCategoryIds);
3046
                } else {
3047
                    if (trim($values['subCategoryIds']) === '') {
3048
                        $diff = $searchCategories;
3049
                    }
3050
                }
3051
                if (!empty($diff)) {
3052
                    unset($diff[0]);
3053
                    if (!empty($diff)) {
3054
                        $queryBuilder->andWhere('pi.category NOT IN('.implode(',', $diff).')');
3055
                    }
3056
                }
3057
            }
3058
3059
            if ($listByUser) {
3060
                $queryBuilder
3061
                    ->andWhere('pi.user = :user')
3062
                    ->setParameter('user', $this->owner);
3063
            }
3064
3065
            $visibilityCriteria = [Portfolio::VISIBILITY_VISIBLE];
3066
3067
            if (api_is_allowed_to_edit()) {
3068
                $visibilityCriteria[] = Portfolio::VISIBILITY_HIDDEN_EXCEPT_TEACHER;
3069
            }
3070
3071
            $queryBuilder
3072
                ->andWhere(
3073
                    $queryBuilder->expr()->orX(
3074
                        'pi.user = :current_user',
3075
                        $queryBuilder->expr()->andX(
3076
                            'pi.user != :current_user',
3077
                            $queryBuilder->expr()->in('pi.visibility', $visibilityCriteria)
3078
                        )
3079
                    )
3080
                )
3081
                ->setParameter('current_user', $currentUserId);
3082
3083
            $queryBuilder->orderBy('pi.creationDate', 'DESC');
3084
3085
            $items = $queryBuilder->getQuery()->getResult();
3086
        } else {
3087
            $itemsCriteria = [];
3088
            $itemsCriteria['category'] = null;
3089
            $itemsCriteria['user'] = $this->owner;
3090
3091
            if ($currentUserId !== $this->owner->getId()) {
3092
                $itemsCriteria['visibility'] = Portfolio::VISIBILITY_VISIBLE;
3093
            }
3094
3095
            $items = $this->em
3096
                ->getRepository(Portfolio::class)
3097
                ->findBy($itemsCriteria, ['creationDate' => 'DESC']);
3098
        }
3099
3100
        return $items;
3101
    }
3102
3103
    /**
3104
     * @throws \Doctrine\ORM\ORMException
3105
     * @throws \Doctrine\ORM\OptimisticLockException
3106
     * @throws \Doctrine\ORM\TransactionRequiredException
3107
     */
3108
    private function createCommentForm(Portfolio $item): string
3109
    {
3110
        $formAction = $this->baseUrl.http_build_query(['action' => 'view', 'id' => $item->getId()]);
3111
3112
        $templates = $this->em
3113
            ->getRepository(PortfolioComment::class)
3114
            ->findBy(
3115
                [
3116
                    'isTemplate' => true,
3117
                    'author' => $this->owner,
3118
                ]
3119
            );
3120
3121
        $form = new FormValidator('frm_comment', 'post', $formAction);
3122
        $form->addHeader(get_lang('AddNewComment'));
3123
        $form->addSelectFromCollection(
3124
            'template',
3125
            [
3126
                get_lang('Template'),
3127
                null,
3128
                '<span id="portfolio-spinner" class="fa fa-fw fa-spinner fa-spin" style="display: none;"
3129
                    aria-hidden="true" aria-label="'.get_lang('Loading').'"></span>',
3130
            ],
3131
            $templates,
3132
            [],
3133
            true,
3134
            'getExcerpt'
3135
        );
3136
        $form->addHtmlEditor('content', get_lang('Comments'), true, false, ['ToolbarSet' => 'Minimal']);
3137
        $form->addHidden('item', $item->getId());
3138
        $form->addHidden('parent', 0);
3139
        $form->applyFilter('content', 'trim');
3140
3141
        $this->addAttachmentsFieldToForm($form);
3142
3143
        $form->addButtonSave(get_lang('Save'));
3144
3145
        if ($form->validate()) {
3146
            $values = $form->exportValues();
3147
3148
            $parentComment = $this->em->find(PortfolioComment::class, $values['parent']);
3149
3150
            $comment = new PortfolioComment();
3151
            $comment
3152
                ->setAuthor($this->owner)
3153
                ->setParent($parentComment)
3154
                ->setContent($values['content'])
3155
                ->setDate(api_get_utc_datetime(null, false, true))
3156
                ->setItem($item);
3157
3158
            $this->em->persist($comment);
3159
            $this->em->flush();
3160
3161
            $this->processAttachments(
3162
                $form,
3163
                $comment->getAuthor(),
3164
                $comment->getId(),
3165
                PortfolioAttachment::TYPE_COMMENT
3166
            );
3167
3168
            $hook = HookPortfolioItemCommented::create();
3169
            $hook->setEventData(['comment' => $comment]);
3170
            $hook->notifyItemCommented();
3171
3172
            PortfolioNotifier::notifyTeachersAndAuthor($comment);
3173
3174
            Display::addFlash(
3175
                Display::return_message(get_lang('CommentAdded'), 'success')
3176
            );
3177
3178
            header("Location: $formAction");
3179
            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...
3180
        }
3181
3182
        $js = '<script>
3183
            $(function() {
3184
                $(\'#frm_comment_template\').on(\'change\', function () {
3185
                    $(\'#portfolio-spinner\').show();
3186
                
3187
                    $.getJSON(_p.web_ajax + \'portfolio.ajax.php?a=find_template_comment&comment=\' + this.value)
3188
                        .done(function(response) {
3189
                            CKEDITOR.instances.content.setData(response.content);
3190
                        })
3191
                        .fail(function () {
3192
                            CKEDITOR.instances.content.setData(\'\');
3193
                        })
3194
                        .always(function() {
3195
                          $(\'#portfolio-spinner\').hide();
3196
                        });
3197
                });
3198
            });
3199
        </script>';
3200
3201
        return $form->returnForm().$js;
3202
    }
3203
3204
    private function generateAttachmentList($post, bool $includeHeader = true): string
3205
    {
3206
        $attachmentsRepo = $this->em->getRepository(PortfolioAttachment::class);
3207
3208
        $postOwnerId = 0;
3209
3210
        if ($post instanceof Portfolio) {
3211
            $attachments = $attachmentsRepo->findFromItem($post);
3212
3213
            $postOwnerId = $post->getUser()->getId();
3214
        } elseif ($post instanceof PortfolioComment) {
3215
            $attachments = $attachmentsRepo->findFromComment($post);
3216
3217
            $postOwnerId = $post->getAuthor()->getId();
3218
        }
3219
3220
        if (empty($attachments)) {
3221
            return '';
3222
        }
3223
3224
        $currentUserId = api_get_user_id();
3225
3226
        $listItems = '<ul class="fa-ul">';
3227
3228
        $deleteIcon = Display::return_icon(
3229
            'delete.png',
3230
            get_lang('DeleteAttachment'),
3231
            ['style' => 'display: inline-block'],
3232
            ICON_SIZE_TINY
3233
        );
3234
        $deleteAttrs = ['class' => 'btn-portfolio-delete'];
3235
3236
        /** @var PortfolioAttachment $attachment */
3237
        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...
3238
            $downloadParams = http_build_query(['action' => 'download_attachment', 'file' => $attachment->getPath()]);
3239
            $deleteParams = http_build_query(['action' => 'delete_attachment', 'file' => $attachment->getPath()]);
3240
3241
            $listItems .= '<li>'
3242
                .'<span class="fa-li fa fa-paperclip" aria-hidden="true"></span>'
3243
                .Display::url(
3244
                    Security::remove_XSS($attachment->getFilename()),
3245
                    $this->baseUrl.$downloadParams
3246
                );
3247
3248
            if ($currentUserId === $postOwnerId) {
3249
                $listItems .= PHP_EOL.Display::url($deleteIcon, $this->baseUrl.$deleteParams, $deleteAttrs);
3250
            }
3251
3252
            if ($attachment->getComment()) {
3253
                $listItems .= '<p class="text-muted">'.Security::remove_XSS($attachment->getComment()).'</p>';
3254
            }
3255
3256
            $listItems .= '</li>';
3257
        }
3258
3259
        $listItems .= '</ul>';
3260
3261
        if ($includeHeader) {
3262
            $listItems = '<h1 class="h4">'.get_lang('AttachmentFiles').'</h1>'
3263
                .$listItems;
3264
        }
3265
3266
        return $listItems;
3267
    }
3268
3269
    private function generateItemContent(Portfolio $item): string
3270
    {
3271
        $originId = $item->getOrigin();
3272
3273
        if (empty($originId)) {
3274
            return $item->getContent();
3275
        }
3276
3277
        $em = Database::getManager();
3278
3279
        $originContent = '';
3280
        $originContentFooter = '';
3281
3282
        if (Portfolio::TYPE_ITEM === $item->getOriginType()) {
3283
            $origin = $em->find(Portfolio::class, $item->getOrigin());
3284
3285
            if ($origin) {
3286
                $originContent = $origin->getContent();
3287
                $originContentFooter = vsprintf(
3288
                    get_lang('OriginallyPublishedAsXTitleByYUser'),
3289
                    [
3290
                        "<cite>{$origin->getTitle(true)}</cite>",
3291
                        $origin->getUser()->getCompleteName(),
3292
                    ]
3293
                );
3294
            }
3295
        } elseif (Portfolio::TYPE_COMMENT === $item->getOriginType()) {
3296
            $origin = $em->find(PortfolioComment::class, $item->getOrigin());
3297
3298
            if ($origin) {
3299
                $originContent = $origin->getContent();
3300
                $originContentFooter = vsprintf(
3301
                    get_lang('OriginallyCommentedByXUserInYItem'),
3302
                    [
3303
                        $origin->getAuthor()->getCompleteName(),
3304
                        "<cite>{$origin->getItem()->getTitle(true)}</cite>",
3305
                    ]
3306
                );
3307
            }
3308
        }
3309
3310
        if ($originContent) {
3311
            return "<figure>
3312
                    <blockquote>$originContent</blockquote>
3313
                    <figcaption style=\"margin-bottom: 10px;\">$originContentFooter</figcaption>
3314
                </figure>
3315
                <div class=\"clearfix\">".Security::remove_XSS($item->getContent()).'</div>'
3316
            ;
3317
        }
3318
3319
        return Security::remove_XSS($item->getContent());
3320
    }
3321
3322
    private function getItemsInHtmlFormatted(array $items): array
3323
    {
3324
        $itemsHtml = [];
3325
3326
        /** @var Portfolio $item */
3327
        foreach ($items as $item) {
3328
            $itemCourse = $item->getCourse();
3329
            $itemSession = $item->getSession();
3330
3331
            $creationDate = api_convert_and_format_date($item->getCreationDate());
3332
            $updateDate = api_convert_and_format_date($item->getUpdateDate());
3333
3334
            $metadata = '<ul class="list-unstyled text-muted">';
3335
3336
            if ($itemSession) {
3337
                $metadata .= '<li>'.get_lang('Course').': '.$itemSession->getName().' ('
3338
                    .$itemCourse->getTitle().') </li>';
3339
            } elseif ($itemCourse) {
3340
                $metadata .= '<li>'.get_lang('Course').': '.$itemCourse->getTitle().'</li>';
3341
            }
3342
3343
            $metadata .= '<li>'.sprintf(get_lang('CreationDateXDate'), $creationDate).'</li>';
3344
3345
            if ($itemCourse) {
3346
                $propertyInfo = api_get_item_property_info(
3347
                    $itemCourse->getId(),
3348
                    TOOL_PORTFOLIO,
3349
                    $item->getId(),
3350
                    $itemSession ? $itemSession->getId() : 0
3351
                );
3352
3353
                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...
3354
                    $metadata .= '<li>'
3355
                        .sprintf(
3356
                            get_lang('UpdatedOnDateXByUserY'),
3357
                            api_convert_and_format_date($propertyInfo['lastedit_date'], DATE_TIME_FORMAT_LONG),
3358
                            api_get_user_entity($propertyInfo['lastedit_user_id'])->getCompleteName()
3359
                        )
3360
                        .'</li>';
3361
                }
3362
            } else {
3363
                $metadata .= '<li>'.sprintf(get_lang('UpdateDateXDate'), $updateDate).'</li>';
3364
            }
3365
3366
            if ($item->getCategory()) {
3367
                $metadata .= '<li>'.sprintf(get_lang('CategoryXName'), $item->getCategory()->getTitle()).'</li>';
3368
            }
3369
3370
            $metadata .= '</ul>';
3371
3372
            $itemContent = $this->generateItemContent($item);
3373
3374
            $itemsHtml[] = Display::panel($itemContent, Security::remove_XSS($item->getTitle()), '', 'info', $metadata);
3375
        }
3376
3377
        return $itemsHtml;
3378
    }
3379
3380
    private function getCommentsInHtmlFormatted(array $comments): array
3381
    {
3382
        $commentsHtml = [];
3383
3384
        /** @var PortfolioComment $comment */
3385
        foreach ($comments as $comment) {
3386
            $item = $comment->getItem();
3387
            $date = api_convert_and_format_date($comment->getDate());
3388
3389
            $metadata = '<ul class="list-unstyled text-muted">';
3390
            $metadata .= '<li>'.sprintf(get_lang('DateXDate'), $date).'</li>';
3391
            $metadata .= '<li>'.sprintf(get_lang('PortfolioItemTitleXName'), Security::remove_XSS($item->getTitle()))
3392
                .'</li>';
3393
            $metadata .= '</ul>';
3394
3395
            $commentsHtml[] = Display::panel(
3396
                Security::remove_XSS($comment->getContent()),
3397
                '',
3398
                '',
3399
                'default',
3400
                $metadata
3401
            );
3402
        }
3403
3404
        return $commentsHtml;
3405
    }
3406
3407
    private function fixImagesSourcesToHtml(string $htmlContent): string
3408
    {
3409
        $doc = new DOMDocument();
3410
        @$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

3410
        /** @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...
3411
3412
        $elements = $doc->getElementsByTagName('img');
3413
3414
        if (empty($elements->length)) {
3415
            return $htmlContent;
3416
        }
3417
3418
        $webCoursePath = api_get_path(WEB_COURSE_PATH);
3419
        $webUploadPath = api_get_path(WEB_UPLOAD_PATH);
3420
3421
        /** @var \DOMElement $element */
3422
        foreach ($elements as $element) {
3423
            $src = trim($element->getAttribute('src'));
3424
3425
            if (strpos($src, 'http') === 0) {
3426
                continue;
3427
            }
3428
3429
            if (strpos($src, '/app/upload/') === 0) {
3430
                $element->setAttribute(
3431
                    'src',
3432
                    preg_replace('/\/app/upload\//', $webUploadPath, $src, 1)
3433
                );
3434
3435
                continue;
3436
            }
3437
3438
            if (strpos($src, '/courses/') === 0) {
3439
                $element->setAttribute(
3440
                    'src',
3441
                    preg_replace('/\/courses\//', $webCoursePath, $src, 1)
3442
                );
3443
3444
                continue;
3445
            }
3446
        }
3447
3448
        return $doc->saveHTML();
3449
    }
3450
3451
    private function formatZipIndexFile(HTML_Table $tblItems, HTML_Table $tblComments): string
3452
    {
3453
        $htmlContent = Display::page_header($this->owner->getCompleteNameWithUsername());
3454
        $htmlContent .= Display::page_subheader2(get_lang('PortfolioItems'));
3455
3456
        $htmlContent .= $tblItems->getRowCount() > 0
3457
            ? $tblItems->toHtml()
3458
            : Display::return_message(get_lang('NoItemsInYourPortfolio'), 'warning');
3459
3460
        $htmlContent .= Display::page_subheader2(get_lang('PortfolioCommentsMade'));
3461
3462
        $htmlContent .= $tblComments->getRowCount() > 0
3463
            ? $tblComments->toHtml()
3464
            : Display::return_message(get_lang('YouHaveNotCommented'), 'warning');
3465
3466
        $webAssetsPath = api_get_path(WEB_PUBLIC_PATH).'assets/';
3467
3468
        $doc = new DOMDocument();
3469
        @$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

3469
        /** @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...
3470
3471
        $stylesheet1 = $doc->createElement('link');
3472
        $stylesheet1->setAttribute('rel', 'stylesheet');
3473
        $stylesheet1->setAttribute('href', $webAssetsPath.'bootstrap/dist/css/bootstrap.min.css');
3474
        $stylesheet2 = $doc->createElement('link');
3475
        $stylesheet2->setAttribute('rel', 'stylesheet');
3476
        $stylesheet2->setAttribute('href', $webAssetsPath.'fontawesome/css/font-awesome.min.css');
3477
        $stylesheet3 = $doc->createElement('link');
3478
        $stylesheet3->setAttribute('rel', 'stylesheet');
3479
        $stylesheet3->setAttribute('href', ChamiloApi::getEditorDocStylePath());
3480
3481
        $head = $doc->createElement('head');
3482
        $head->appendChild($stylesheet1);
3483
        $head->appendChild($stylesheet2);
3484
        $head->appendChild($stylesheet3);
3485
3486
        $doc->documentElement->insertBefore(
3487
            $head,
3488
            $doc->getElementsByTagName('body')->item(0)
3489
        );
3490
3491
        return $doc->saveHTML();
3492
    }
3493
3494
    /**
3495
     * It parsers a title for a variable in lang.
3496
     *
3497
     * @param $defaultDisplayText
3498
     *
3499
     * @return string
3500
     */
3501
    private function getLanguageVariable($defaultDisplayText)
3502
    {
3503
        $variableLanguage = api_replace_dangerous_char(strtolower($defaultDisplayText));
3504
        $variableLanguage = preg_replace('/[^A-Za-z0-9\_]/', '', $variableLanguage); // Removes special chars except underscore.
3505
        if (is_numeric($variableLanguage[0])) {
3506
            $variableLanguage = '_'.$variableLanguage;
3507
        }
3508
        $variableLanguage = api_underscore_to_camel_case($variableLanguage);
3509
3510
        return $variableLanguage;
3511
    }
3512
3513
    /**
3514
     * It translates the text as parameter.
3515
     *
3516
     * @param $defaultDisplayText
3517
     *
3518
     * @return mixed
3519
     */
3520
    private function translateDisplayName($defaultDisplayText)
3521
    {
3522
        $variableLanguage = $this->getLanguageVariable($defaultDisplayText);
3523
3524
        return isset($GLOBALS[$variableLanguage]) ? $GLOBALS[$variableLanguage] : $defaultDisplayText;
3525
    }
3526
}
3527